diff --git a/docs/cli/tg-set-mcp-tool.md b/docs/cli/tg-set-mcp-tool.md index 6d693e6e..90f137a0 100644 --- a/docs/cli/tg-set-mcp-tool.md +++ b/docs/cli/tg-set-mcp-tool.md @@ -3,12 +3,12 @@ ## Synopsis ``` -tg-set-mcp-tool [OPTIONS] --name NAME --tool-url URL +tg-set-mcp-tool [OPTIONS] --id ID --tool-url URL [--auth-token TOKEN] ``` ## Description -The `tg-set-mcp-tool` command configures and registers MCP (Model Control Protocol) tools in the TrustGraph system. It allows defining MCP tool configurations with name and URL. Tools are stored in the 'mcp' configuration group for discovery and execution. +The `tg-set-mcp-tool` command configures and registers MCP (Model Control Protocol) tools in the TrustGraph system. It allows defining MCP tool configurations with id, URL, and optional authentication token. Tools are stored in the 'mcp' configuration group for discovery and execution. This command is useful for: - Registering MCP tool endpoints for agent use @@ -25,16 +25,27 @@ The command stores MCP tool configurations in the 'mcp' configuration group, sep - Default: `http://localhost:8088/` (or `TRUSTGRAPH_URL` environment variable) - Should point to a running TrustGraph API instance -- `--name NAME` - - **Required.** MCP tool name identifier +- `-i, --id ID` + - **Required.** MCP tool identifier - Used to reference the MCP tool in configurations - Must be unique within the MCP tool registry +- `-r, --remote-name NAME` + - **Optional.** Remote MCP tool name used by the MCP server + - If not specified, defaults to the value of `--id` + - Use when the MCP server expects a different tool name + - `--tool-url URL` - **Required.** MCP tool URL endpoint - Should point to the MCP server endpoint providing the tool functionality - Must be a valid URL accessible by the TrustGraph system +- `--auth-token TOKEN` + - **Optional.** Bearer token for authentication + - Used to authenticate with secured MCP endpoints + - Token is sent as `Authorization: Bearer {TOKEN}` header + - Stored in plaintext in configuration (see Security Considerations) + - `-h, --help` - Show help message and exit @@ -44,54 +55,96 @@ The command stores MCP tool configurations in the 'mcp' configuration group, sep Register a weather service MCP tool: ```bash -tg-set-mcp-tool --name weather --tool-url "http://localhost:3000/weather" +tg-set-mcp-tool --id weather --tool-url "http://localhost:3000/weather" ``` ### Calculator MCP Tool Register a calculator MCP tool: ```bash -tg-set-mcp-tool --name calculator --tool-url "http://mcp-tools.example.com/calc" +tg-set-mcp-tool --id calculator --tool-url "http://mcp-tools.example.com/calc" ``` ### Remote MCP Service Register a remote MCP service: ```bash -tg-set-mcp-tool --name document-processor \ +tg-set-mcp-tool --id document-processor \ --tool-url "https://api.example.com/mcp/documents" ``` +### Secured MCP Tool with Authentication + +Register an MCP tool that requires bearer token authentication: +```bash +tg-set-mcp-tool --id secure-tool \ + --tool-url "https://api.example.com/mcp" \ + --auth-token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +``` + +### MCP Tool with Remote Name + +Register an MCP tool where the server uses a different name: +```bash +tg-set-mcp-tool --id my-weather \ + --remote-name weather_v2 \ + --tool-url "http://weather-server:3000/api" +``` + ### Custom API URL Register MCP tool with custom TrustGraph API: ```bash tg-set-mcp-tool -u http://trustgraph.example.com:8088/ \ - --name custom-mcp --tool-url "http://custom.mcp.com/api" + --id custom-mcp --tool-url "http://custom.mcp.com/api" ``` ### Local Development Setup Register MCP tools for local development: ```bash -tg-set-mcp-tool --name dev-tool --tool-url "http://localhost:8080/mcp" +tg-set-mcp-tool --id dev-tool --tool-url "http://localhost:8080/mcp" +``` + +### Production Setup with Authentication + +Register authenticated MCP tools for production: +```bash +# Using environment variable for token +export MCP_AUTH_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +tg-set-mcp-tool --id prod-tool \ + --tool-url "https://prod-mcp.example.com/api" \ + --auth-token "$MCP_AUTH_TOKEN" ``` ## MCP Tool Configuration -MCP tools are configured with minimal metadata: +MCP tools are configured with the following metadata: -- **name**: Unique identifier for the tool +- **id**: Unique identifier for the tool (configuration key) +- **remote-name**: Name used by the MCP server (optional, defaults to id) - **url**: Endpoint URL for the MCP server +- **auth-token**: Bearer token for authentication (optional) The configuration is stored as JSON in the 'mcp' configuration group: + +**Basic configuration:** ```json { - "name": "weather", + "remote-name": "weather", "url": "http://localhost:3000/weather" } ``` +**Configuration with authentication:** +```json +{ + "remote-name": "secure-tool", + "url": "https://api.example.com/mcp", + "auth-token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +} +``` + ## Advanced Usage ### Updating Existing MCP Tools @@ -99,7 +152,15 @@ The configuration is stored as JSON in the 'mcp' configuration group: Update an existing MCP tool configuration: ```bash # Update MCP tool URL -tg-set-mcp-tool --name weather --tool-url "http://new-weather-server:3000/api" +tg-set-mcp-tool --id weather --tool-url "http://new-weather-server:3000/api" + +# Add authentication to existing tool +tg-set-mcp-tool --id weather \ + --tool-url "http://weather-server:3000/api" \ + --auth-token "new-token-here" + +# Remove authentication (by setting tool without auth-token) +tg-set-mcp-tool --id weather --tool-url "http://weather-server:3000/api" ``` ### Batch MCP Tool Registration @@ -108,22 +169,33 @@ Register multiple MCP tools in a script: ```bash #!/bin/bash # Register a suite of MCP tools -tg-set-mcp-tool --name search --tool-url "http://search-mcp:3000/api" -tg-set-mcp-tool --name translate --tool-url "http://translate-mcp:3000/api" -tg-set-mcp-tool --name summarize --tool-url "http://summarize-mcp:3000/api" +tg-set-mcp-tool --id search --tool-url "http://search-mcp:3000/api" +tg-set-mcp-tool --id translate --tool-url "http://translate-mcp:3000/api" +tg-set-mcp-tool --id summarize --tool-url "http://summarize-mcp:3000/api" + +# Register secured tools with authentication +tg-set-mcp-tool --id secure-search \ + --tool-url "https://secure-search:3000/api" \ + --auth-token "$SEARCH_TOKEN" +tg-set-mcp-tool --id secure-translate \ + --tool-url "https://secure-translate:3000/api" \ + --auth-token "$TRANSLATE_TOKEN" ``` ### Environment-Specific Configuration Configure MCP tools for different environments: ```bash -# Development environment +# Development environment (no auth) export TRUSTGRAPH_URL="http://dev.trustgraph.com:8088/" -tg-set-mcp-tool --name dev-mcp --tool-url "http://dev.mcp.com/api" +tg-set-mcp-tool --id dev-mcp --tool-url "http://dev.mcp.com/api" -# Production environment +# Production environment (with auth) export TRUSTGRAPH_URL="http://prod.trustgraph.com:8088/" -tg-set-mcp-tool --name prod-mcp --tool-url "http://prod.mcp.com/api" +export PROD_MCP_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +tg-set-mcp-tool --id prod-mcp \ + --tool-url "https://prod.mcp.com/api" \ + --auth-token "$PROD_MCP_TOKEN" ``` ### MCP Tool Validation @@ -131,10 +203,10 @@ tg-set-mcp-tool --name prod-mcp --tool-url "http://prod.mcp.com/api" Verify MCP tool registration: ```bash # Register MCP tool and verify -tg-set-mcp-tool --name test-mcp --tool-url "http://test.mcp.com/api" +tg-set-mcp-tool --id test-mcp --tool-url "http://test.mcp.com/api" -# Check if MCP tool was registered -tg-show-mcp-tools | grep test-mcp +# Check if MCP tool was registered and view auth status +tg-show-mcp-tools ``` ## Error Handling @@ -149,15 +221,15 @@ The command handles various error conditions: Common error scenarios: ```bash # Missing required field -tg-set-mcp-tool --name tool1 +tg-set-mcp-tool --id tool1 # Output: Exception: Must specify --tool-url for MCP tool -# Missing name +# Missing id tg-set-mcp-tool --tool-url "http://example.com/mcp" -# Output: Exception: Must specify --name for MCP tool +# Output: Exception: Must specify --id for MCP tool # Invalid API URL -tg-set-mcp-tool -u "invalid-url" --name tool1 --tool-url "http://mcp.com" +tg-set-mcp-tool -u "invalid-url" --id tool1 --tool-url "http://mcp.com" # Output: Exception: [API connection error] ``` @@ -168,9 +240,9 @@ tg-set-mcp-tool -u "invalid-url" --name tool1 --tool-url "http://mcp.com" View registered MCP tools: ```bash # Register MCP tool -tg-set-mcp-tool --name new-mcp --tool-url "http://new.mcp.com/api" +tg-set-mcp-tool --id new-mcp --tool-url "http://new.mcp.com/api" -# View all MCP tools +# View all MCP tools (shows auth status) tg-show-mcp-tools ``` @@ -178,11 +250,13 @@ tg-show-mcp-tools Use MCP tools in agent workflows: ```bash -# Register MCP tool -tg-set-mcp-tool --name weather --tool-url "http://weather.mcp.com/api" +# Register MCP tool with authentication +tg-set-mcp-tool --id weather \ + --tool-url "https://weather.mcp.com/api" \ + --auth-token "$WEATHER_TOKEN" -# Invoke MCP tool directly -tg-invoke-mcp-tool --name weather --input "location=London" +# Invoke MCP tool directly (auth handled automatically) +tg-invoke-mcp-tool --name weather --parameters '{"location": "London"}' ``` ### With Configuration Management @@ -190,21 +264,24 @@ tg-invoke-mcp-tool --name weather --input "location=London" MCP tools integrate with configuration management: ```bash # Register MCP tool -tg-set-mcp-tool --name config-mcp --tool-url "http://config.mcp.com/api" +tg-set-mcp-tool --id config-mcp --tool-url "http://config.mcp.com/api" -# View configuration including MCP tools -tg-show-config +# View all MCP tool configurations +tg-show-mcp-tools ``` ## Best Practices -1. **Clear Naming**: Use descriptive, unique MCP tool names +1. **Clear Naming**: Use descriptive, unique MCP tool identifiers 2. **Reliable URLs**: Ensure MCP endpoints are stable and accessible -3. **Health Checks**: Verify MCP endpoints are operational before registration -4. **Documentation**: Document MCP tool capabilities and usage -5. **Error Handling**: Implement proper error handling for MCP endpoints -6. **Security**: Use secure URLs (HTTPS) when possible -7. **Monitoring**: Monitor MCP tool availability and performance +3. **Use HTTPS**: Always use HTTPS URLs when authentication is required +4. **Secure Tokens**: Store auth tokens in environment variables, not in scripts +5. **Token Rotation**: Regularly rotate authentication tokens +6. **Health Checks**: Verify MCP endpoints are operational before registration +7. **Documentation**: Document MCP tool capabilities and usage +8. **Error Handling**: Implement proper error handling for MCP endpoints +9. **Monitoring**: Monitor MCP tool availability and performance +10. **Access Control**: Restrict access to configuration system containing tokens ## Troubleshooting @@ -248,10 +325,45 @@ The Model Control Protocol (MCP) is a standardized interface for AI model tools: When registering MCP tools: 1. **URL Validation**: Ensure URLs are legitimate and secure -2. **Network Security**: Use HTTPS when possible -3. **Access Control**: Implement proper authentication for MCP endpoints -4. **Input Validation**: Validate all inputs to MCP tools -5. **Error Handling**: Don't expose sensitive information in error messages +2. **Network Security**: Always use HTTPS for authenticated endpoints +3. **Token Storage**: Auth tokens are stored in plaintext in the configuration system + - Ensure proper access control on the configuration storage + - Use short-lived tokens when possible + - Implement token rotation policies +4. **Token Transmission**: Use HTTPS to prevent token interception +5. **Access Control**: Implement proper authentication for MCP endpoints +6. **Token Exposure**: + - Use environment variables to pass tokens to the command + - Don't hardcode tokens in scripts or commit them to version control + - The `tg-show-mcp-tools` command masks token values for security +7. **Input Validation**: Validate all inputs to MCP tools +8. **Error Handling**: Don't expose sensitive information in error messages +9. **Least Privilege**: Grant tokens minimum required permissions +10. **Audit Logging**: Monitor configuration changes for security events + +### Authentication Best Practices + +When using the `--auth-token` parameter: + +- **Store tokens securely**: Use environment variables or secrets management systems +- **Use HTTPS**: Always use HTTPS URLs when providing authentication tokens +- **Rotate regularly**: Implement a token rotation schedule +- **Monitor usage**: Track which services are accessing authenticated endpoints +- **Revoke on compromise**: Have a process to quickly revoke and rotate compromised tokens + +Example secure workflow: +```bash +# Store token in environment variable (not in script) +export MCP_TOKEN=$(cat /secure/path/to/token) + +# Use HTTPS for authenticated endpoints +tg-set-mcp-tool --id secure-service \ + --tool-url "https://secure.example.com/mcp" \ + --auth-token "$MCP_TOKEN" + +# Clear token from environment after use +unset MCP_TOKEN +``` ## Related Commands diff --git a/docs/tech-specs/mcp-tool-bearer-token.md b/docs/tech-specs/mcp-tool-bearer-token.md new file mode 100644 index 00000000..f3f75d29 --- /dev/null +++ b/docs/tech-specs/mcp-tool-bearer-token.md @@ -0,0 +1,554 @@ +# MCP Tool Bearer Token Authentication Specification + +> **⚠️ IMPORTANT: SINGLE-TENANT ONLY** +> +> This specification describes a **basic, service-level authentication mechanism** for MCP tools. It is **NOT** a complete authentication solution and is **NOT suitable** for: +> - Multi-user environments +> - Multi-tenant deployments +> - Federated authentication +> - User context propagation +> - Per-user authorization +> +> This feature provides **one static token per MCP tool**, shared across all users and sessions. If you need per-user or per-tenant authentication, this is not the right solution. + +## Overview +**Feature Name**: MCP Tool Bearer Token Authentication Support +**Author**: Claude Code Assistant +**Date**: 2025-11-11 +**Status**: In Development + +### Executive Summary + +Enable MCP tool configurations to specify optional bearer tokens for authenticating with protected MCP servers. This allows TrustGraph to securely invoke MCP tools hosted on servers that require authentication, without modifying the agent or tool invocation interfaces. + +**IMPORTANT**: This is a basic authentication mechanism designed for single-tenant, service-to-service authentication scenarios. It is **NOT** suitable for: +- Multi-user environments where different users need different credentials +- Multi-tenant deployments requiring per-tenant isolation +- Federated authentication scenarios +- User-level authentication or authorization +- Dynamic credential management or token refresh + +This feature provides a static, system-wide bearer token per MCP tool configuration, shared across all users and invocations of that tool. + +### Problem Statement + +Currently, MCP tools can only connect to publicly accessible MCP servers. Many production MCP deployments require authentication via bearer tokens for security. Without authentication support: +- MCP tools cannot connect to secured MCP servers +- Users must either expose MCP servers publicly or implement reverse proxies +- No standardized way to pass credentials to MCP connections +- Security best practices cannot be enforced on MCP endpoints + +### Goals + +- [ ] Allow MCP tool configurations to specify optional `auth-token` parameter +- [ ] Update MCP tool service to use bearer tokens when connecting to MCP servers +- [ ] Update CLI tools to support setting/displaying auth tokens +- [ ] Maintain backward compatibility with unauthenticated MCP configurations +- [ ] Document security considerations for token storage + +### Non-Goals +- Dynamic token refresh or OAuth flows (static tokens only) +- Encryption of stored tokens (configuration system security is out of scope) +- Alternative authentication methods (Basic auth, API keys, etc.) +- Token validation or expiration checking +- **Per-user authentication**: This feature does NOT support user-specific credentials +- **Multi-tenant isolation**: This feature does NOT provide per-tenant token management +- **Federated authentication**: This feature does NOT integrate with identity providers (SSO, OAuth, SAML, etc.) +- **Context-aware authentication**: Tokens are not passed based on user context or session + +## Background and Context + +### Current State +MCP tool configurations are stored in the `mcp` configuration group with this structure: +```json +{ + "remote-name": "tool_name", + "url": "http://mcp-server:3000/api" +} +``` + +The MCP tool service connects to servers using `streamablehttp_client(url)` without any authentication headers. + +### Limitations + +**Current System Limitations:** +1. **No authentication support**: Cannot connect to secured MCP servers +2. **Security exposure**: MCP servers must be publicly accessible or use network-level security only +3. **Production deployment issues**: Cannot follow security best practices for API endpoints + +**Limitations of This Solution:** +1. **Single-tenant only**: One static token per MCP tool, shared across all users +2. **No per-user credentials**: Cannot authenticate as different users or pass user context +3. **No multi-tenant support**: Cannot isolate credentials by tenant or organization +4. **Static tokens only**: No support for token refresh, rotation, or expiration handling +5. **Service-level authentication**: Authenticates the TrustGraph service, not individual users +6. **Shared security context**: All invocations of an MCP tool use the same credential + +### Use Case Applicability + +**✅ Appropriate Use Cases:** +- Single-tenant TrustGraph deployments +- Service-to-service authentication (TrustGraph → MCP Server) +- Development and testing environments +- Internal MCP tools accessed by the TrustGraph system +- Scenarios where all users share the same MCP tool access level +- Static, long-lived service credentials + +**❌ Inappropriate Use Cases:** +- Multi-user systems requiring per-user authentication +- Multi-tenant SaaS deployments with tenant isolation requirements +- Federated authentication scenarios (SSO, OAuth, SAML) +- Systems requiring user context propagation to MCP servers +- Environments needing dynamic token refresh or short-lived tokens +- Applications where different users need different permission levels +- Compliance requirements for user-level audit trails + +**Example Appropriate Scenario:** +A single-organization TrustGraph deployment where all employees use the same internal MCP tool (e.g., company database lookup). The MCP server requires authentication to prevent external access, but all internal users have the same access level. + +**Example Inappropriate Scenario:** +A multi-tenant TrustGraph SaaS platform where Tenant A and Tenant B each need to access their own isolated MCP servers with separate credentials. This feature does NOT support per-tenant token management. + +### Related Components +- **trustgraph-flow/trustgraph/agent/mcp_tool/service.py**: MCP tool invocation service +- **trustgraph-cli/trustgraph/cli/set_mcp_tool.py**: CLI tool for creating/updating MCP configurations +- **trustgraph-cli/trustgraph/cli/show_mcp_tools.py**: CLI tool for displaying MCP configurations +- **MCP Python SDK**: `streamablehttp_client` from `mcp.client.streamable_http` + +## Requirements + +### Functional Requirements + +1. **MCP Configuration Auth Token**: MCP tool configurations MUST support an optional `auth-token` field +2. **Bearer Token Usage**: MCP tool service MUST send `Authorization: Bearer {token}` header when auth-token is configured +3. **CLI Support**: `tg-set-mcp-tool` MUST accept optional `--auth-token` parameter +4. **Token Display**: `tg-show-mcp-tools` MUST indicate when auth-token is configured (masked for security) +5. **Backward Compatibility**: Existing MCP tool configurations without auth-token MUST continue to work + +### Non-Functional Requirements +1. **Backward Compatibility**: Zero breaking changes for existing MCP tool configurations +2. **Performance**: No significant performance impact on MCP tool invocation +3. **Security**: Tokens stored in configuration (document security implications) + +### User Stories + +1. As a **DevOps engineer**, I want to configure bearer tokens for MCP tools so that I can secure MCP server endpoints +2. As a **CLI user**, I want to set auth tokens when creating MCP tools so that I can connect to protected servers +3. As a **system administrator**, I want to see which MCP tools have authentication configured so that I can audit security settings + +## Design + +### High-Level Architecture +Extend MCP tool configuration and service to support bearer token authentication: +1. Add optional `auth-token` field to MCP tool configuration schema +2. Modify MCP tool service to read auth-token and pass to HTTP client +3. Update CLI tools to support setting and displaying auth tokens +4. Document security considerations and best practices + +### Configuration Schema + +**Current Schema**: +```json +{ + "remote-name": "tool_name", + "url": "http://mcp-server:3000/api" +} +``` + +**New Schema** (with optional auth-token): +```json +{ + "remote-name": "tool_name", + "url": "http://mcp-server:3000/api", + "auth-token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +} +``` + +**Field Descriptions**: +- `remote-name` (optional): Name used by MCP server (defaults to config key) +- `url` (required): MCP server endpoint URL +- `auth-token` (optional): Bearer token for authentication + +### Data Flow + +1. **Configuration Storage**: User runs `tg-set-mcp-tool --id my-tool --tool-url http://server/api --auth-token xyz123` +2. **Config Loading**: MCP tool service receives config update via `on_mcp_config()` callback +3. **Tool Invocation**: When tool is invoked: + - Service reads `auth-token` from config (if present) + - Creates headers dict: `{"Authorization": "Bearer {token}"}` + - Passes headers to `streamablehttp_client(url, headers=headers)` + - MCP server validates token and processes request + +### API Changes +No external API changes - configuration schema extension only. + +### Component Details + +#### Component 1: service.py (MCP Tool Service) +**File**: `trustgraph-flow/trustgraph/agent/mcp_tool/service.py` + +**Purpose**: Invoke MCP tools on remote servers + +**Changes Required** (in `invoke_tool()` method): +1. Check for `auth-token` in `self.mcp_services[name]` config +2. Build headers dict with Authorization header if token exists +3. Pass headers to `streamablehttp_client(url, headers=headers)` + +**Current Code** (lines 42-89): +```python +async def invoke_tool(self, name, parameters): + try: + if name not in self.mcp_services: + raise RuntimeError(f"MCP service {name} not known") + if "url" not in self.mcp_services[name]: + raise RuntimeError(f"MCP service {name} URL not defined") + + url = self.mcp_services[name]["url"] + + if "remote-name" in self.mcp_services[name]: + remote_name = self.mcp_services[name]["remote-name"] + else: + remote_name = name + + logger.info(f"Invoking {remote_name} at {url}") + + # Connect to a streamable HTTP server + async with streamablehttp_client(url) as ( + read_stream, + write_stream, + _, + ): + # ... rest of method +``` + +**Modified Code**: +```python +async def invoke_tool(self, name, parameters): + try: + if name not in self.mcp_services: + raise RuntimeError(f"MCP service {name} not known") + if "url" not in self.mcp_services[name]: + raise RuntimeError(f"MCP service {name} URL not defined") + + url = self.mcp_services[name]["url"] + + if "remote-name" in self.mcp_services[name]: + remote_name = self.mcp_services[name]["remote-name"] + else: + remote_name = name + + # Build headers with optional bearer token + headers = {} + if "auth-token" in self.mcp_services[name]: + token = self.mcp_services[name]["auth-token"] + headers["Authorization"] = f"Bearer {token}" + + logger.info(f"Invoking {remote_name} at {url}") + + # Connect to a streamable HTTP server with headers + async with streamablehttp_client(url, headers=headers) as ( + read_stream, + write_stream, + _, + ): + # ... rest of method (unchanged) +``` + +#### Component 2: set_mcp_tool.py (CLI Configuration Tool) +**File**: `trustgraph-cli/trustgraph/cli/set_mcp_tool.py` + +**Purpose**: Create/update MCP tool configurations + +**Changes Required**: +1. Add `--auth-token` optional argument to argparse +2. Include `auth-token` in configuration JSON when provided + +**Current Arguments**: +- `--id` (required): MCP tool identifier +- `--remote-name` (optional): Remote MCP tool name +- `--tool-url` (required): MCP tool URL endpoint +- `-u, --api-url` (optional): TrustGraph API URL + +**New Argument**: +- `--auth-token` (optional): Bearer token for authentication + +**Modified Configuration Building**: +```python +# Build configuration object +config = { + "url": args.tool_url, +} + +if args.remote_name: + config["remote-name"] = args.remote_name + +if args.auth_token: + config["auth-token"] = args.auth_token + +# Store configuration +api.config().put([ + ConfigValue(type="mcp", key=args.id, value=json.dumps(config)) +]) +``` + +#### Component 3: show_mcp_tools.py (CLI Display Tool) +**File**: `trustgraph-cli/trustgraph/cli/show_mcp_tools.py` + +**Purpose**: Display MCP tool configurations + +**Changes Required**: +1. Add "Auth" column to output table +2. Display "Yes" or "No" based on presence of auth-token +3. Do not display actual token value (security) + +**Current Output**: +``` +ID Remote Name URL +---------- ------------- ------------------------ +my-tool my-tool http://server:3000/api +``` + +**New Output**: +``` +ID Remote Name URL Auth +---------- ------------- ------------------------ ------ +my-tool my-tool http://server:3000/api Yes +other-tool other-tool http://other:3000/api No +``` + +#### Component 4: Documentation +**File**: `docs/cli/tg-set-mcp-tool.md` + +**Changes Required**: +1. Document new `--auth-token` parameter +2. Provide example usage with authentication +3. Document security considerations + +## Implementation Plan + +### Phase 1: Create Technical Specification +- [x] Write comprehensive tech spec documenting all changes + +### Phase 2: Update MCP Tool Service +- [ ] Modify `invoke_tool()` in `service.py` to read auth-token from config +- [ ] Build headers dict and pass to `streamablehttp_client` +- [ ] Test with authenticated MCP server + +### Phase 3: Update CLI Tools +- [ ] Add `--auth-token` argument to `set_mcp_tool.py` +- [ ] Include auth-token in configuration JSON +- [ ] Add "Auth" column to `show_mcp_tools.py` output +- [ ] Test CLI tool changes + +### Phase 4: Update Documentation +- [ ] Document `--auth-token` parameter in `tg-set-mcp-tool.md` +- [ ] Add security considerations section +- [ ] Provide example usage + +### Phase 5: Testing +- [ ] Test MCP tool with auth-token connects successfully +- [ ] Test backward compatibility (tools without auth-token still work) +- [ ] Test CLI tools accept and store auth-token correctly +- [ ] Test show command displays auth status correctly + +### Code Changes Summary +| File | Change Type | Lines | Description | +|------|------------|-------|-------------| +| `service.py` | Modified | ~52-66 | Add auth-token reading and header building | +| `set_mcp_tool.py` | Modified | ~30-60 | Add --auth-token argument and config storage | +| `show_mcp_tools.py` | Modified | ~40-70 | Add Auth column to display | +| `tg-set-mcp-tool.md` | Modified | Various | Document new parameter | + +## Testing Strategy + +### Unit Tests +- **Auth Token Reading**: Test `invoke_tool()` correctly reads auth-token from config +- **Header Building**: Test Authorization header is built correctly with Bearer prefix +- **Backward Compatibility**: Test tools without auth-token work unchanged +- **CLI Argument Parsing**: Test `--auth-token` argument is parsed correctly + +### Integration Tests +- **Authenticated Connection**: Test MCP tool service connects to authenticated server +- **End-to-End**: Test CLI → config storage → service invocation with auth token +- **Token Not Required**: Test connection to unauthenticated server still works + +### Manual Testing +- **Real MCP Server**: Test with actual MCP server requiring bearer token authentication +- **CLI Workflow**: Test complete workflow: set tool with auth → invoke tool → verify success +- **Display Masking**: Verify auth status shown but token value not exposed + +## Migration and Rollout + +### Migration Strategy +No migration required - this is purely additive functionality: +- Existing MCP tool configurations without `auth-token` continue to work unchanged +- New configurations can optionally include `auth-token` field +- CLI tools accept but don't require `--auth-token` parameter + +### Rollout Plan +1. **Phase 1**: Deploy core service changes to development/staging +2. **Phase 2**: Deploy CLI tool updates +3. **Phase 3**: Update documentation +4. **Phase 4**: Production rollout with monitoring + +### Rollback Plan +- Core changes are backward compatible - existing tools unaffected +- If issues arise, auth-token handling can be disabled by removing header building logic +- CLI changes are independent and can be rolled back separately + +## Security Considerations + +### ⚠️ Critical Limitation: Single-Tenant Authentication Only + +**This authentication mechanism is NOT suitable for multi-user or multi-tenant environments.** + +- **Shared credentials**: All users and invocations share the same token per MCP tool +- **No user context**: The MCP server cannot distinguish between different TrustGraph users +- **No tenant isolation**: All tenants share the same credential for each MCP tool +- **Audit trail limitation**: MCP server logs show all requests from the same credential +- **Permission scope**: Cannot enforce different permission levels for different users + +**Do NOT use this feature if:** +- Your TrustGraph deployment serves multiple organizations (multi-tenant) +- You need to track which user accessed which MCP tool +- Different users require different permission levels +- You need to comply with user-level audit requirements +- Your MCP server enforces per-user rate limits or quotas + +**Alternative solutions for multi-user/multi-tenant scenarios:** +- Implement user context propagation through custom headers +- Deploy separate TrustGraph instances per tenant +- Use network-level isolation (VPCs, service meshes) +- Implement a proxy layer that handles per-user authentication + +### Token Storage +**Risk**: Auth tokens stored in plaintext in configuration system + +**Mitigation**: +- Document that tokens are stored unencrypted +- Recommend using short-lived tokens when possible +- Recommend proper access control on configuration storage +- Consider future enhancement for encrypted token storage + +### Token Exposure +**Risk**: Tokens could be exposed in logs or CLI output + +**Mitigation**: +- Do not log token values (only log "auth configured: yes/no") +- CLI show command displays masked status only, not actual token +- Do not include tokens in error messages + +### Network Security +**Risk**: Tokens transmitted over unencrypted connections + +**Mitigation**: +- Document recommendation to use HTTPS URLs for MCP servers +- Warn users about plaintext transmission risk with HTTP + +### Configuration Access +**Risk**: Unauthorized access to configuration system exposes tokens + +**Mitigation**: +- Document importance of securing configuration system access +- Recommend principle of least privilege for configuration access +- Consider audit logging for configuration changes (future enhancement) + +### Multi-User Environments +**Risk**: In multi-user deployments, all users share the same MCP credentials + +**Understanding the Risk**: +- User A and User B both use the same token when accessing an MCP tool +- MCP server cannot distinguish between different TrustGraph users +- No way to enforce per-user permissions or rate limits +- Audit logs on MCP server show all requests from same credential +- If one user's session is compromised, attacker has same MCP access as all users + +**This is NOT a bug - it's a fundamental limitation of this design.** + +## Performance Impact +- **Minimal overhead**: Header building adds negligible processing time +- **Network impact**: Additional HTTP header adds ~50-200 bytes per request +- **Memory usage**: Negligible increase for storing token string in config + +## Documentation + +### User Documentation +- [ ] Update `tg-set-mcp-tool.md` with `--auth-token` parameter +- [ ] Add security considerations section +- [ ] Provide example usage with bearer token +- [ ] Document token storage implications + +### Developer Documentation +- [ ] Add inline comments for auth token handling in `service.py` +- [ ] Document header building logic +- [ ] Update MCP tool configuration schema documentation + +## Open Questions +1. **Token encryption**: Should we implement encrypted token storage in configuration system? +2. **Token refresh**: Future support for OAuth refresh flows or token rotation? +3. **Alternative auth methods**: Should we support Basic auth, API keys, or other methods? + +## Alternatives Considered + +1. **Environment variables for tokens**: Store tokens in env vars instead of config + - **Rejected**: Complicates deployment and configuration management + +2. **Separate secrets store**: Use dedicated secrets management system + - **Deferred**: Out of scope for initial implementation, consider future enhancement + +3. **Multiple auth methods**: Support Basic, API key, OAuth, etc. + - **Rejected**: Bearer tokens cover most use cases, keep initial implementation simple + +4. **Encrypted token storage**: Encrypt tokens in configuration system + - **Deferred**: Configuration system security is broader concern, defer to future work + +5. **Per-invocation tokens**: Allow tokens to be passed at invocation time + - **Rejected**: Violates separation of concerns, agent shouldn't handle credentials + +## References +- [MCP Protocol Specification](https://github.com/modelcontextprotocol/spec) +- [HTTP Bearer Authentication (RFC 6750)](https://tools.ietf.org/html/rfc6750) +- [Current MCP Tool Service](../trustgraph-flow/trustgraph/agent/mcp_tool/service.py) +- [MCP Tool Arguments Specification](./mcp-tool-arguments.md) + +## Appendix + +### Example Usage + +**Setting MCP tool with authentication**: +```bash +tg-set-mcp-tool \ + --id secure-tool \ + --tool-url https://secure-server.example.com/mcp \ + --auth-token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` + +**Showing MCP tools**: +```bash +tg-show-mcp-tools + +ID Remote Name URL Auth +----------- ----------- ------------------------------------ ------ +secure-tool secure-tool https://secure-server.example.com/mcp Yes +public-tool public-tool http://localhost:3000/mcp No +``` + +### Configuration Example + +**Stored in configuration system**: +```json +{ + "type": "mcp", + "key": "secure-tool", + "value": "{\"url\": \"https://secure-server.example.com/mcp\", \"auth-token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\"}" +} +``` + +### Security Best Practices + +1. **Use HTTPS**: Always use HTTPS URLs for MCP servers with authentication +2. **Short-lived tokens**: Use tokens with expiration when possible +3. **Least privilege**: Grant tokens minimum required permissions +4. **Access control**: Restrict access to configuration system +5. **Token rotation**: Rotate tokens regularly +6. **Audit logging**: Monitor configuration changes for security events diff --git a/tests/unit/test_agent/test_agent_step_arguments.py b/tests/unit/test_agent/test_agent_step_arguments.py new file mode 100644 index 00000000..7243721d --- /dev/null +++ b/tests/unit/test_agent/test_agent_step_arguments.py @@ -0,0 +1,376 @@ +""" +Unit tests for AgentStep arguments type conversion + +Tests the fix for converting agent tool arguments to strings when creating +AgentStep records, ensuring compatibility with Pulsar schema that requires +Map(String()) for the arguments field. +""" + +import pytest +from unittest.mock import Mock, AsyncMock +from trustgraph.schema import AgentStep +from trustgraph.agent.react.types import Action + + +class TestAgentStepArgumentsConversion: + """Test cases for AgentStep arguments string conversion""" + + def test_agent_step_with_integer_arguments(self): + """Test that integer arguments are converted to strings""" + # Arrange + action = Action( + thought="Set volume to 10", + name="set_volume", + arguments={"volume_level": 10, "device": "speaker"}, + observation="Volume set successfully" + ) + + # Act - simulate the conversion that happens in service.py + agent_step = AgentStep( + thought=action.thought, + action=action.name, + arguments={k: str(v) for k, v in action.arguments.items()}, + observation=action.observation + ) + + # Assert + assert agent_step.arguments["volume_level"] == "10" + assert isinstance(agent_step.arguments["volume_level"], str) + assert agent_step.arguments["device"] == "speaker" + assert isinstance(agent_step.arguments["device"], str) + + def test_agent_step_with_float_arguments(self): + """Test that float arguments are converted to strings""" + # Arrange + action = Action( + thought="Set temperature", + name="set_temperature", + arguments={"temperature": 23.5, "unit": "celsius"}, + observation="Temperature set" + ) + + # Act + agent_step = AgentStep( + thought=action.thought, + action=action.name, + arguments={k: str(v) for k, v in action.arguments.items()}, + observation=action.observation + ) + + # Assert + assert agent_step.arguments["temperature"] == "23.5" + assert isinstance(agent_step.arguments["temperature"], str) + assert agent_step.arguments["unit"] == "celsius" + + def test_agent_step_with_boolean_arguments(self): + """Test that boolean arguments are converted to strings""" + # Arrange + action = Action( + thought="Enable feature", + name="toggle_feature", + arguments={"enabled": True, "feature_name": "dark_mode"}, + observation="Feature toggled" + ) + + # Act + agent_step = AgentStep( + thought=action.thought, + action=action.name, + arguments={k: str(v) for k, v in action.arguments.items()}, + observation=action.observation + ) + + # Assert + assert agent_step.arguments["enabled"] == "True" + assert isinstance(agent_step.arguments["enabled"], str) + assert agent_step.arguments["feature_name"] == "dark_mode" + + def test_agent_step_with_none_arguments(self): + """Test that None arguments are converted to strings""" + # Arrange + action = Action( + thought="Check status", + name="get_status", + arguments={"filter": None, "category": "all"}, + observation="Status retrieved" + ) + + # Act + agent_step = AgentStep( + thought=action.thought, + action=action.name, + arguments={k: str(v) for k, v in action.arguments.items()}, + observation=action.observation + ) + + # Assert + assert agent_step.arguments["filter"] == "None" + assert isinstance(agent_step.arguments["filter"], str) + assert agent_step.arguments["category"] == "all" + + def test_agent_step_with_mixed_type_arguments(self): + """Test that mixed type arguments are all converted to strings""" + # Arrange + action = Action( + thought="Configure device", + name="configure_device", + arguments={ + "name": "Hifi", + "volume_level": 10, + "bass_boost": 1.5, + "enabled": True, + "preset": None + }, + observation="Device configured" + ) + + # Act + agent_step = AgentStep( + thought=action.thought, + action=action.name, + arguments={k: str(v) for k, v in action.arguments.items()}, + observation=action.observation + ) + + # Assert - all values should be strings + assert all(isinstance(v, str) for v in agent_step.arguments.values()) + assert agent_step.arguments["name"] == "Hifi" + assert agent_step.arguments["volume_level"] == "10" + assert agent_step.arguments["bass_boost"] == "1.5" + assert agent_step.arguments["enabled"] == "True" + assert agent_step.arguments["preset"] == "None" + + def test_agent_step_with_string_arguments(self): + """Test that string arguments remain strings (no double conversion)""" + # Arrange + action = Action( + thought="Search for information", + name="search", + arguments={"query": "test query", "limit": "10"}, + observation="Search completed" + ) + + # Act + agent_step = AgentStep( + thought=action.thought, + action=action.name, + arguments={k: str(v) for k, v in action.arguments.items()}, + observation=action.observation + ) + + # Assert + assert agent_step.arguments["query"] == "test query" + assert agent_step.arguments["limit"] == "10" + assert all(isinstance(v, str) for v in agent_step.arguments.values()) + + def test_agent_step_with_empty_arguments(self): + """Test that empty arguments dict works correctly""" + # Arrange + action = Action( + thought="Perform action", + name="do_something", + arguments={}, + observation="Action completed" + ) + + # Act + agent_step = AgentStep( + thought=action.thought, + action=action.name, + arguments={k: str(v) for k, v in action.arguments.items()}, + observation=action.observation + ) + + # Assert + assert agent_step.arguments == {} + assert isinstance(agent_step.arguments, dict) + + def test_agent_step_with_numeric_string_values(self): + """Test arguments that are already strings containing numbers""" + # Arrange + action = Action( + thought="Process order", + name="process_order", + arguments={"order_id": "12345", "quantity": 10}, + observation="Order processed" + ) + + # Act + agent_step = AgentStep( + thought=action.thought, + action=action.name, + arguments={k: str(v) for k, v in action.arguments.items()}, + observation=action.observation + ) + + # Assert + assert agent_step.arguments["order_id"] == "12345" + assert agent_step.arguments["quantity"] == "10" + assert all(isinstance(v, str) for v in agent_step.arguments.values()) + + def test_agent_step_conversion_preserves_keys(self): + """Test that argument keys are preserved during conversion""" + # Arrange + action = Action( + thought="Test", + name="test_action", + arguments={ + "param1": 1, + "param_two": 2, + "PARAM_THREE": 3, + "param-four": 4 + }, + observation="Done" + ) + + # Act + agent_step = AgentStep( + thought=action.thought, + action=action.name, + arguments={k: str(v) for k, v in action.arguments.items()}, + observation=action.observation + ) + + # Assert - verify all keys are preserved + assert set(agent_step.arguments.keys()) == { + "param1", "param_two", "PARAM_THREE", "param-four" + } + # Verify values are converted + assert agent_step.arguments["param1"] == "1" + assert agent_step.arguments["param_two"] == "2" + assert agent_step.arguments["PARAM_THREE"] == "3" + assert agent_step.arguments["param-four"] == "4" + + def test_real_world_home_assistant_example(self): + """Test with real-world Home Assistant volume control example""" + # Arrange - this is the exact scenario from the bug report + action = Action( + thought='The user wants to set the volume of the Hifi. The `set_device_volume` tool can be used for this purpose. The device name is "Hifi" and the desired volume level is 10.', + name='set_device_volume', + arguments={'name': 'Hifi', 'volume_level': 10}, + observation='{"speech": {}, "response_type": "action_done", "data": {"targets": [], "success": [{"name": "Hifi", "type": "entity", "id": "media_player.hifi"}], "failed": []}}' + ) + + # Act - this should not raise TypeError + agent_step = AgentStep( + thought=action.thought, + action=action.name, + arguments={k: str(v) for k, v in action.arguments.items()}, + observation=action.observation + ) + + # Assert + assert agent_step.arguments["name"] == "Hifi" + assert agent_step.arguments["volume_level"] == "10" + assert isinstance(agent_step.arguments["volume_level"], str) + + def test_multiple_actions_in_history(self): + """Test converting multiple actions in history (as done in service.py)""" + # Arrange + history = [ + Action( + thought="First action", + name="action1", + arguments={"count": 5}, + observation="Done 1" + ), + Action( + thought="Second action", + name="action2", + arguments={"enabled": True, "name": "test"}, + observation="Done 2" + ), + Action( + thought="Third action", + name="action3", + arguments={"value": 3.14}, + observation="Done 3" + ) + ] + + # Act - simulate the list comprehension in service.py + agent_steps = [ + AgentStep( + thought=h.thought, + action=h.name, + arguments={k: str(v) for k, v in h.arguments.items()}, + observation=h.observation + ) + for h in history + ] + + # Assert + assert len(agent_steps) == 3 + + # First action + assert agent_steps[0].arguments["count"] == "5" + assert isinstance(agent_steps[0].arguments["count"], str) + + # Second action + assert agent_steps[1].arguments["enabled"] == "True" + assert agent_steps[1].arguments["name"] == "test" + assert all(isinstance(v, str) for v in agent_steps[1].arguments.values()) + + # Third action + assert agent_steps[2].arguments["value"] == "3.14" + assert isinstance(agent_steps[2].arguments["value"], str) + + def test_arguments_with_special_characters(self): + """Test arguments containing special characters are properly converted""" + # Arrange + action = Action( + thought="Process data", + name="process", + arguments={ + "text": "Hello, World!", + "path": "/home/user/file.txt", + "pattern": "test-*-pattern", + "count": 42 + }, + observation="Processed" + ) + + # Act + agent_step = AgentStep( + thought=action.thought, + action=action.name, + arguments={k: str(v) for k, v in action.arguments.items()}, + observation=action.observation + ) + + # Assert + assert agent_step.arguments["text"] == "Hello, World!" + assert agent_step.arguments["path"] == "/home/user/file.txt" + assert agent_step.arguments["pattern"] == "test-*-pattern" + assert agent_step.arguments["count"] == "42" + assert all(isinstance(v, str) for v in agent_step.arguments.values()) + + def test_zero_and_negative_numbers(self): + """Test that zero and negative numbers are converted correctly""" + # Arrange + action = Action( + thought="Test edge cases", + name="edge_test", + arguments={ + "zero": 0, + "negative": -5, + "negative_float": -3.14, + "positive": 10 + }, + observation="Done" + ) + + # Act + agent_step = AgentStep( + thought=action.thought, + action=action.name, + arguments={k: str(v) for k, v in action.arguments.items()}, + observation=action.observation + ) + + # Assert + assert agent_step.arguments["zero"] == "0" + assert agent_step.arguments["negative"] == "-5" + assert agent_step.arguments["negative_float"] == "-3.14" + assert agent_step.arguments["positive"] == "10" + assert all(isinstance(v, str) for v in agent_step.arguments.values()) diff --git a/tests/unit/test_agent/test_mcp_tool_auth.py b/tests/unit/test_agent/test_mcp_tool_auth.py new file mode 100644 index 00000000..82877226 --- /dev/null +++ b/tests/unit/test_agent/test_mcp_tool_auth.py @@ -0,0 +1,233 @@ +""" +Unit tests for MCP tool bearer token authentication + +Tests the authentication feature added to MCP tool service that allows +configuring optional bearer tokens for MCP server connections. +""" + +import pytest +from unittest.mock import Mock, AsyncMock, patch, MagicMock +import json + + +class TestMcpToolAuthentication: + """Test cases for MCP tool bearer token authentication""" + + def test_mcp_tool_with_auth_token_header_building(self): + """Test that auth token is correctly formatted in headers""" + # Arrange + mcp_config = { + "url": "https://secure.example.com/mcp", + "remote-name": "secure-tool", + "auth-token": "test-token-12345" + } + + # Act - simulate header building logic from service.py + headers = {} + if "auth-token" in mcp_config and mcp_config["auth-token"]: + token = mcp_config["auth-token"] + headers["Authorization"] = f"Bearer {token}" + + # Assert + assert "Authorization" in headers + assert headers["Authorization"] == "Bearer test-token-12345" + assert headers["Authorization"].startswith("Bearer ") + + def test_mcp_tool_without_auth_token_header_building(self): + """Test that no auth header is added when token is not present (backward compatibility)""" + # Arrange + mcp_config = { + "url": "http://public.example.com/mcp", + "remote-name": "public-tool" + # No auth-token field + } + + # Act - simulate header building logic from service.py + headers = {} + if "auth-token" in mcp_config and mcp_config["auth-token"]: + token = mcp_config["auth-token"] + headers["Authorization"] = f"Bearer {token}" + + # Assert + assert headers == {} + assert "Authorization" not in headers + + def test_mcp_config_with_auth_token(self): + """Test MCP configuration parsing with auth-token""" + # Arrange + config = { + "mcp": { + "secure-tool": json.dumps({ + "url": "https://secure.example.com/mcp", + "remote-name": "secure-tool", + "auth-token": "test-token-xyz" + }), + "public-tool": json.dumps({ + "url": "http://public.example.com/mcp", + "remote-name": "public-tool" + }) + } + } + + # Act - simulate on_mcp_config + mcp_services = { + k: json.loads(v) + for k, v in config["mcp"].items() + } + + # Assert + assert "secure-tool" in mcp_services + assert mcp_services["secure-tool"]["auth-token"] == "test-token-xyz" + assert mcp_services["secure-tool"]["url"] == "https://secure.example.com/mcp" + + assert "public-tool" in mcp_services + assert "auth-token" not in mcp_services["public-tool"] + assert mcp_services["public-tool"]["url"] == "http://public.example.com/mcp" + + def test_auth_token_with_empty_string(self): + """Test that empty auth-token string is treated as no auth""" + # Arrange + config_data = { + "url": "https://example.com/mcp", + "remote-name": "test-tool", + "auth-token": "" + } + + # Act - simulate header building logic + headers = {} + if "auth-token" in config_data and config_data["auth-token"]: + headers["Authorization"] = f"Bearer {config_data['auth-token']}" + + # Assert + assert headers == {}, "Empty auth-token should not add Authorization header" + + def test_auth_token_with_special_characters(self): + """Test auth token with special characters (JWT-like)""" + # Arrange + jwt_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + + config_data = { + "url": "https://example.com/mcp", + "auth-token": jwt_token + } + + # Act - simulate header building + headers = {} + if "auth-token" in config_data and config_data["auth-token"]: + token = config_data["auth-token"] + headers["Authorization"] = f"Bearer {token}" + + # Assert + assert headers["Authorization"] == f"Bearer {jwt_token}" + assert "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" in headers["Authorization"] + + def test_multiple_tools_with_different_auth_configs(self): + """Test handling multiple MCP tools with mixed auth configurations""" + # Arrange + mcp_services = { + "tool-a": { + "url": "https://a.example.com/mcp", + "auth-token": "token-a" + }, + "tool-b": { + "url": "https://b.example.com/mcp", + "auth-token": "token-b" + }, + "tool-c": { + "url": "http://c.example.com/mcp" + # No auth-token + } + } + + # Act - simulate header building for each tool + headers_a = {} + if "auth-token" in mcp_services["tool-a"] and mcp_services["tool-a"]["auth-token"]: + headers_a["Authorization"] = f"Bearer {mcp_services['tool-a']['auth-token']}" + + headers_b = {} + if "auth-token" in mcp_services["tool-b"] and mcp_services["tool-b"]["auth-token"]: + headers_b["Authorization"] = f"Bearer {mcp_services['tool-b']['auth-token']}" + + headers_c = {} + if "auth-token" in mcp_services["tool-c"] and mcp_services["tool-c"]["auth-token"]: + headers_c["Authorization"] = f"Bearer {mcp_services['tool-c']['auth-token']}" + + # Assert + assert headers_a == {"Authorization": "Bearer token-a"} + assert headers_b == {"Authorization": "Bearer token-b"} + assert headers_c == {} + + def test_auth_token_not_logged(self): + """Test that auth tokens are not exposed in logs""" + # This is more of a guideline test - in real implementation, + # we should ensure tokens are never logged + + # Arrange + auth_token = "super-secret-token-123" + config = { + "url": "https://secure.example.com/mcp", + "auth-token": auth_token + } + + # Act - simulate log-safe representation + def get_log_safe_config(cfg): + """Return config with sensitive data masked""" + safe_config = cfg.copy() + if "auth-token" in safe_config and safe_config["auth-token"]: + safe_config["auth-token"] = "****" + return safe_config + + log_safe = get_log_safe_config(config) + + # Assert + assert log_safe["auth-token"] == "****" + assert auth_token not in str(log_safe) + assert "url" in log_safe + assert log_safe["url"] == "https://secure.example.com/mcp" + + def test_auth_token_with_remote_name_configuration(self): + """Test MCP tool configuration with both auth-token and remote-name""" + # Arrange + mcp_config = { + "url": "https://server.example.com/mcp", + "remote-name": "actual_tool_name", + "auth-token": "my-token-456" + } + + # Act - simulate header building and remote name extraction + headers = {} + if "auth-token" in mcp_config and mcp_config["auth-token"]: + token = mcp_config["auth-token"] + headers["Authorization"] = f"Bearer {token}" + + remote_name = mcp_config.get("remote-name", "default-name") + + # Assert + assert headers["Authorization"] == "Bearer my-token-456" + assert remote_name == "actual_tool_name" + assert "url" in mcp_config + assert mcp_config["url"] == "https://server.example.com/mcp" + + def test_bearer_token_format(self): + """Test that Bearer token format is correct""" + # Arrange + tokens = [ + "simple-token", + "token_with_underscore", + "token-with-dash", + "TokenWithMixedCase123", + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.payload.signature" + ] + + # Act & Assert + for token in tokens: + headers = {} + if token: + headers["Authorization"] = f"Bearer {token}" + + # Verify format is "Bearer " with single space + assert headers["Authorization"].startswith("Bearer ") + assert headers["Authorization"] == f"Bearer {token}" + # Verify no extra spaces + assert headers["Authorization"].count("Bearer") == 1 + assert headers["Authorization"].split("Bearer ")[1] == token diff --git a/trustgraph-cli/trustgraph/cli/set_mcp_tool.py b/trustgraph-cli/trustgraph/cli/set_mcp_tool.py index b48c6d86..05e3823c 100644 --- a/trustgraph-cli/trustgraph/cli/set_mcp_tool.py +++ b/trustgraph-cli/trustgraph/cli/set_mcp_tool.py @@ -7,6 +7,7 @@ specification. This script stores MCP tool configurations with: - id: Unique identifier for the tool - remote-name: Name used by the MCP server (defaults to id) - url: MCP server endpoint URL +- auth-token: Optional bearer token for authentication Configurations are stored in the 'mcp' configuration group and can be referenced by agent tools using the 'mcp-tool' type. @@ -25,17 +26,24 @@ def set_mcp_tool( id : str, remote_name : str, tool_url : str, + auth_token : str = None, ): api = Api(url).config() + # Build the MCP tool configuration + config = { + "remote-name": remote_name, + "url": tool_url, + } + + if auth_token: + config["auth-token"] = auth_token + # Store the MCP tool configuration in the 'mcp' group values = api.put([ ConfigValue( - type="mcp", key=id, value=json.dumps({ - "remote-name": remote_name, - "url": tool_url, - }) + type="mcp", key=id, value=json.dumps(config) ) ]) @@ -45,12 +53,15 @@ def main(): prog='tg-set-mcp-tool', description=__doc__, epilog=textwrap.dedent(''' - MCP tools are configured with just a name and URL. The URL should point + MCP tools are configured with a name and URL. The URL should point to the MCP server endpoint that provides the tool functionality. - + Optionally, an auth-token can be provided for secured endpoints. + Examples: %(prog)s --id weather --tool-url "http://localhost:3000/weather" %(prog)s --id calculator --tool-url "http://mcp-tools.example.com/calc" + %(prog)s --id secure-tool --tool-url "https://api.example.com/mcp" \\ + --auth-token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ''').strip(), formatter_class=argparse.RawDescriptionHelpFormatter ) @@ -79,6 +90,12 @@ def main(): help='MCP tool URL endpoint', ) + parser.add_argument( + '--auth-token', + required=False, + help='Bearer token for authentication (optional)', + ) + args = parser.parse_args() try: @@ -98,7 +115,8 @@ def main(): url=args.api_url, id=args.id, remote_name=remote_name, - tool_url=args.tool_url + tool_url=args.tool_url, + auth_token=args.auth_token ) except Exception as e: diff --git a/trustgraph-cli/trustgraph/cli/show_mcp_tools.py b/trustgraph-cli/trustgraph/cli/show_mcp_tools.py index c22b69ed..da0154ed 100644 --- a/trustgraph-cli/trustgraph/cli/show_mcp_tools.py +++ b/trustgraph-cli/trustgraph/cli/show_mcp_tools.py @@ -27,6 +27,12 @@ def show_config(url): table.append(("remote-name", data["remote-name"])) table.append(("url", data["url"])) + # Display auth status (masked for security) + if "auth-token" in data and data["auth-token"]: + table.append(("auth", "Yes (configured)")) + else: + table.append(("auth", "No")) + print() print(tabulate.tabulate( diff --git a/trustgraph-flow/trustgraph/agent/mcp_tool/service.py b/trustgraph-flow/trustgraph/agent/mcp_tool/service.py index 96ff73f7..3858d06b 100755 --- a/trustgraph-flow/trustgraph/agent/mcp_tool/service.py +++ b/trustgraph-flow/trustgraph/agent/mcp_tool/service.py @@ -56,10 +56,16 @@ class Service(ToolService): else: remote_name = name + # Build headers with optional bearer token + headers = {} + if "auth-token" in self.mcp_services[name]: + token = self.mcp_services[name]["auth-token"] + headers["Authorization"] = f"Bearer {token}" + logger.info(f"Invoking {remote_name} at {url}") - # Connect to a streamable HTTP server - async with streamablehttp_client(url) as ( + # Connect to a streamable HTTP server with headers + async with streamablehttp_client(url, headers=headers) as ( read_stream, write_stream, _, diff --git a/trustgraph-flow/trustgraph/agent/react/service.py b/trustgraph-flow/trustgraph/agent/react/service.py index 06bf7610..30b2df7a 100755 --- a/trustgraph-flow/trustgraph/agent/react/service.py +++ b/trustgraph-flow/trustgraph/agent/react/service.py @@ -317,7 +317,7 @@ class Processor(AgentService): AgentStep( thought=h.thought, action=h.name, - arguments=h.arguments, + arguments={k: str(v) for k, v in h.arguments.items()}, observation=h.observation ) for h in history