MCP auth for the simple case (#557)

* MCP auth token header

* Mention limitations

* Fix AgentStep schema error by converting argument values to strings.

* Added tests for MCP auth and agent step parsing
This commit is contained in:
cybermaggedon 2025-11-11 12:28:53 +00:00 committed by GitHub
parent d9d4c91363
commit 4c3db4dbbe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1361 additions and 56 deletions

View file

@ -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

View file

@ -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

View file

@ -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())

View file

@ -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 <token>" 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

View file

@ -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:

View file

@ -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(

View file

@ -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,
_,

View file

@ -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