On-demand tool discovery for all your MCPs and per-subagent MCP controls to preserve context window.
io.github.roddutra/agent-mcp-gateway β Model Context Protocol (MCP) Gateway
This MCP server provides an on-demand gateway that aggregates multiple MCP servers and adds policy-based access control for agents and subagents. It is designed to avoid loading all tool definitions upfront, enabling tool discovery as needed to preserve MCP context window usage.
π οΈ Key Features
Aggregates multiple MCP servers
On-demand tool discovery (not loading all tool definitions upfront)
Policy-based access control for agents and subagents
Status: M0: Foundation
π Use Cases
Scenarios with multiple MCP servers where tool availability should be discovered on demand
Agent/subagent setups requiring different access policies
β‘ Developer Benefits
Reduces tool definition βcontext window wasteβ by discovering tools when needed
Centralizes MCP server aggregation and access control
β οΈ Limitations
Readme excerpt indicates only βM0: Foundationβ status; no additional capabilities are specified
A Model Context Protocol (MCP) gateway that aggregates multiple MCP servers and provides policy-based access control for agents and subagents. Solves Claude Code's MCP context window waste by enabling on-demand tool discovery instead of loading all tool definitions upfront.
When multiple MCP servers are configured in development environments (Claude Code, Cursor, VS Code), all tool definitions from all servers load into every agent's and subagent's context window at startup:
5,000-50,000+ tokens consumed upfront
80-95% of loaded tools never used by individual agents
Context needed for actual work gets wasted on unused tool definitions
The Solution
The Agent MCP Gateway acts as a single MCP server that proxies to multiple downstream MCP servers based on configurable per-agent rules:
3 gateway tools load at startup (~2k tokens)
Agents discover and request specific tools on-demand
90%+ context reduction
Policy-based access control per agent/subagent
How It Works
The gateway sits between agents and downstream MCP servers, exposing only 3 lightweight tools. When an agent needs specific functionality, it discovers available servers and tools through the gateway, which filters visibility based on policy rules - agents only see servers and tools they have access to. This reduces each agent's context window to only relevant tools, while the gateway handles proxying authorized requests to downstream servers.
Note: The env variables are optional if using default config locations. See Environment Variables Reference for all options.
3. Configure Your Agents
The gateway's tool descriptions are self-documenting, but for proper access control you should configure how your agents identify themselves. Choose the approach that fits your use case:
Approach 1: Multi-Agent Mode (Recommended)
For different agents with different permissions, configure each agent to pass its identity.
Add this to your agent's system prompt (e.g., CLAUDE.md, .claude/agents/agent-name.md):
markdown
## MCP Gateway Access**Available Tools (via agent-mcp-gateway):**
You have access to MCP servers through the agent-mcp-gateway. The specific servers and tools available to you are determined by the gateway's access control rules.
**Tool Discovery Process:**
When you need to use tools from downstream MCP servers:
1. Use `agent_id: "YOUR_AGENT_NAME"` in ALL gateway tool calls for proper access control
2. Call `list_servers` to discover which servers you have access to
3. Call `get_server_tools` with the specific server name to discover available tools
4. Use `execute_tool` to invoke tools with appropriate parameters
5. If you cannot access a tool you need, immediately notify the user
**Important:** Always include `agent_id: "YOUR_AGENT_NAME"` in your gateway tool calls. This ensures proper access control and audit logging.
Replace YOUR_AGENT_NAME with your agent's identifier (e.g., "researcher", "backend", "admin").
For simpler setups where all agents should have the same permissions, or when using MCP clients without system prompt configuration (e.g., Claude Desktop), configure a default agent using either method:
Option A: Environment Variable
bash
# Set in your MCP client configurationexport GATEWAY_DEFAULT_AGENT=developer
Note: The agent specified (e.g., "developer") must exist in your .mcp-gateway-rules.json file with appropriate permissions.
Defines the downstream MCP servers the gateway will proxy to. Uses the standard MCP config format compatible with Claude Code and other coding agents:
json
{"mcpServers":{"brave-search":{"description":"Web search via Brave Search API","command":"npx","args":["-y","@modelcontextprotocol/server-brave-search"],"env":{"BRAVE_API_KEY":"${BRAVE_API_KEY}"}},"postgres":{"description":"PostgreSQL database access and query execution","command":"uvx","args":["mcp-server-postgres"],"env":{"DATABASE_URL":"${DATABASE_URL}"}},"remote-server":{"description":"Custom remote API integration","url":"https://example.com/mcp","transport":"http","headers":{"Authorization":"Bearer ${API_TOKEN}"}}}}
Server Descriptions (Recommended):
Adding a description field to each server helps AI agents understand what each server provides and when to use it. Descriptions are always returned by list_servers, enabling agents to make informed decisions about which servers to query for tools. While optional, descriptions significantly improve agent tool discovery and decision-making.
Supported Transports:
stdio - Local servers via npx/uvx (specified with command + args)
http - Remote HTTP servers (specified with url)
Environment Variables:
Use ${VAR_NAME} syntax for environment variable substitution
Set variables before running: export BRAVE_API_KEY=your-key
Important - GUI Applications (Claude Desktop, etc.):
If you use ${VAR_NAME} syntax in .mcp.json, note that macOS GUI applications run in isolated environments without access to your shell's environment variables. For Claude Desktop and similar apps, add API keys to the gateway's env object in your MCP client configuration:
The gateway validates configurations at startup and during hot reload. Example output:
code
β Configuration loaded from .mcp.json
β Warning: Agent 'researcher' references undefined server 'unknown-server'
βΉ These rules will be ignored until the server is added
OAuth-protected downstream servers (Notion, GitHub) are automatically supported via auto-detection when servers return HTTP 401. The gateway uses FastMCP's OAuth support to handle authentication flows transparently - browser opens once for initial authentication, then tokens are cached for future use. See OAuth User Guide for detailed setup and troubleshooting.
OAuth Limitations:
The gateway supports OAuth servers that implement Dynamic Client Registration (RFC 7591).
β Supported: OAuth with auto-detection (e.g., Notion MCP)
β Not Supported: OAuth with pre-registered apps (e.g., GitHub OAuth flow)
π‘ For GitHub MCP: Use Personal Access Token instead
Default agent identity when agent_id not provided (optional)
None
export GATEWAY_DEFAULT_AGENT=developer
GATEWAY_DEBUG
Enable debug mode to expose get_gateway_status tool
false
export GATEWAY_DEBUG=true
GATEWAY_AUDIT_LOG
Path to audit log file
~/.cache/agent-mcp-gateway/logs/audit.jsonl
export GATEWAY_AUDIT_LOG=./audit.jsonl
GATEWAY_TRANSPORT
Transport protocol (stdio or http)
stdio
export GATEWAY_TRANSPORT=stdio
GATEWAY_INIT_STRATEGY
Initialization strategy (eager or lazy)
eager
export GATEWAY_INIT_STRATEGY=eager
Note on GUI Applications: macOS GUI applications (Claude Desktop, etc.) run in isolated environments without access to shell environment variables. If using ${VAR_NAME} syntax in .mcp.json, add required API keys to the gateway's env object in your MCP client configuration.
Usage
The gateway runs automatically when your MCP client starts. See Quick Start for adding it to your MCP client configuration.
Custom configuration paths can be specified via environment variables in your MCP client config:
Loading MCP server configuration from: .mcp.json
Loading gateway rules from: .mcp-gateway-rules.json
Audit log will be written to: ~/.cache/agent-mcp-gateway/logs/audit.jsonl
Initializing proxy connections to downstream servers...
- 2 proxy client(s) initialized
* brave-search: ready
* postgres: ready
- Metrics collector initialized
- Access control middleware registered
Agent MCP Gateway initialized successfully
- 2 MCP server(s) configured
- 3 agent(s) configured
- Default policy: deny unknown agents
- 3 gateway tools available: list_servers, get_server_tools, execute_tool
(4 tools if GATEWAY_DEBUG=true: includes get_gateway_status)
Gateway is ready. Running with stdio transport...
Gateway Tools
The gateway exposes exactly 3 tools to agents. All tools accept an optional agent_id parameter for access control. When agent_id is not provided, the gateway uses a fallback chain to determine agent identity (see Agent Identity Modes).
For Agent Developers: To configure your agents to properly use these gateway tools with access control, see Configure Your Agents.
1. list_servers
Lists MCP servers available to the calling agent based on policy rules.
Parameters:
agent_id (string, optional) - Identifier of the agent making the request (see Agent Identity Modes)
include_metadata (boolean, optional) - Include technical details like transport, command, and url (default: false)
Returns:
json
[{"name":"brave-search","description":"Web search via Brave Search API"},{"name":"postgres","description":"PostgreSQL database access and query execution"}]
With include_metadata=true:
json
[{"name":"brave-search","description":"Web search via Brave Search API","transport":"stdio","command":"npx"},{"name":"postgres","description":"PostgreSQL database access and query execution","transport":"stdio","command":"uvx"}]
Note: Server descriptions are always included (when configured in .mcp.json) to help agents understand what each server provides. The include_metadata flag only controls whether technical details (transport, command, url) are included.
Example:
python
# Basic usage - returns names and descriptions
result = await client.call_tool("list_servers", {
"agent_id": "researcher"
})
# With technical metadata
result = await client.call_tool("list_servers", {
"agent_id": "researcher",
"include_metadata": True
})
2. get_server_tools
Retrieves tool definitions from a specific MCP server, filtered by agent permissions.
Parameters:
agent_id (string, optional) - Identifier of the agent (see Agent Identity Modes)
server (string, required) - Name of the downstream MCP server
names (string, optional) - Comma-separated list of tool names (e.g., "tool1,tool2,tool3") or single tool name
max_schema_tokens (integer, optional) - Token budget limit for schemas
Returns:
json
{"tools":[{"name":"brave_web_search","description":"Search the web using Brave Search","inputSchema":{"type":"object","properties":{"query":{"type":"string"}},"required":["query"]}}],"server":"brave-search","total_available":5,"returned":1,"tokens_used":150}
Example:
python
# Get all allowed tools
tools = await client.call_tool("get_server_tools", {
"agent_id": "researcher",
"server": "brave-search"
})
# Get specific tools by name (comma-separated)
tools = await client.call_tool("get_server_tools", {
"agent_id": "researcher",
"server": "brave-search",
"names": "brave_web_search,brave_local_search"
})
# Get specific tools by pattern
tools = await client.call_tool("get_server_tools", {
"agent_id": "backend",
"server": "postgres",
"pattern": "get_*"
})
# Limit token usage
tools = await client.call_tool("get_server_tools", {
"agent_id": "researcher",
"server": "brave-search",
"max_schema_tokens": 1000
})
3. execute_tool
Executes a tool on a downstream MCP server with transparent result forwarding.
Parameters:
agent_id (string, optional) - Identifier of the agent (see Agent Identity Modes)
server (string, required) - Name of the downstream MCP server
tool (string, required) - Name of the tool to execute
args (object, required) - Arguments to pass to the tool
timeout_ms (integer, optional) - Timeout in milliseconds
# Execute a tool
result = await client.call_tool("execute_tool", {
"agent_id": "researcher",
"server": "brave-search",
"tool": "brave_web_search",
"args": {
"query": "FastMCP documentation"
}
})
# With timeout
result = await client.call_tool("execute_tool", {
"agent_id": "backend",
"server": "postgres",
"tool": "query",
"args": {
"sql": "SELECT * FROM users LIMIT 10"
},
"timeout_ms": 5000
})
4. get_gateway_status (Debug Mode Only)
Returns comprehensive gateway health and diagnostics information.
Important: This tool is only available when debug mode is enabled (via GATEWAY_DEBUG=true environment variable or --debug CLI flag). See Security Considerations for details.
Parameters:
agent_id (string, optional) - Identifier of the agent (see Agent Identity Modes)
Returns:
json
{"reload_status":{"mcp_config":{"last_attempt":"2025-10-30T10:30:00Z","last_success":"2025-10-30T10:30:00Z","last_error":null,"attempt_count":1,"success_count":1},"gateway_rules":{"last_attempt":"2025-10-30T10:35:00Z","last_success":"2025-10-30T10:35:00Z","last_error":null,"attempt_count":2,"success_count":2,"last_warnings":[]}},"policy_state":{"total_agents":3,"agent_ids":["researcher","backend","admin"],"defaults":{"deny_on_missing_agent":true}},"available_servers":["brave-search","postgres"],"config_paths":{"mcp_config":"/path/to/.mcp.json","gateway_rules":"/path/to/.mcp-gateway-rules.json"},"message":"Gateway is operational. Check reload_status for hot reload health."}
Example:
python
# Check gateway health and reload status (requires GATEWAY_DEBUG=true)
status = await client.call_tool("get_gateway_status", {
"agent_id": "admin"
})
# Verify last reload was successfulif status["reload_status"]["gateway_rules"]["last_error"]:
print("Warning: Last rule reload failed!")
Error Handling
All tools return structured errors with clear messages:
json
{"error":{"code":"DENIED_BY_POLICY","message":"Agent 'frontend' denied access to tool 'drop_table'","rule":"agents.frontend.deny.tools.postgres[0]"}}
Error Codes:
DENIED_BY_POLICY - Agent lacks permission
SERVER_UNAVAILABLE - Downstream server unreachable
TOOL_NOT_FOUND - Requested tool doesn't exist
TIMEOUT - Operation exceeded time limit
INVALID_AGENT_ID - Missing or unknown agent identifier
FALLBACK_AGENT_NOT_IN_RULES - Configured fallback agent not found in gateway rules
NO_FALLBACK_CONFIGURED - No agent_id provided and no fallback agent configured
Complete Workflow Example
Here's a minimal working example showing the typical gateway workflow:
python
from fastmcp import Client
asyncdefgateway_workflow():
asyncwith Client('agent-mcp-gateway') as client:
# 1. Discover available servers
servers = await client.call_tool('list_servers', {
'agent_id': 'researcher'
})
# Response: [{"name": "brave-search", "description": "Web search..."}]# 2. Get tools from specific server
tools = await client.call_tool('get_server_tools', {
'agent_id': 'researcher',
'server': 'brave-search'
})
# Response: {"tools": [...], "server": "brave-search", ...}# 3. Execute a tool
result = await client.call_tool('execute_tool', {
'agent_id': 'researcher',
'server': 'brave-search',
'tool': 'brave_web_search',
'args': {'query': 'MCP protocol documentation'}
})
# Response: {"content": [...search results...], "isError": false}
This workflow demonstrates on-demand tool discovery - load definitions only when needed, not upfront.
Agent Identity Modes
The gateway supports two deployment modes for handling agent identity:
Multi-Agent Mode (Recommended)
Use when different agents need different permissions (production, multi-agent systems):
Security Note: The fallback mechanism follows the principle of least privilege - it never grants implicit "allow all" access, only the explicitly configured agent's permissions.
Security Considerations
Rules File Location: Store .mcp-gateway-rules.json in-project for context optimization only. For production access control, store outside project directory (e.g., ~/.claude/mcp-gateway-rules.json) to prevent agents from reading/modifying permissions.
Debug Mode: The get_gateway_status tool exposes gateway internals and is only available when GATEWAY_DEBUG=true. Disable in production environments.
For comprehensive security guidance: See Security Guide for detailed information on rules file security, debug mode considerations, agent impersonation risks, and production best practices.
Troubleshooting
Gateway Won't Start
Symptom: Error on startup or gateway fails to initialize
Solutions:
Check configuration files exist: Verify .mcp.json and .mcp-gateway-rules.json are in the expected location
Validate JSON syntax: Use python -m json.tool < .mcp.json to check for syntax errors
Check Python version: Ensure Python 3.12+ is installed (python --version)
Verify dependencies: Run uv sync to ensure all packages are installed
Can't Connect to Downstream Server
Symptom:SERVER_UNAVAILABLE error when calling tools
Solutions:
Verify server configuration: Check server is properly defined in .mcp.json
Test stdio servers: Ensure command is available (npx --version, uvx --version)
Check environment variables: Verify API keys and credentials are set
Test HTTP servers: Try accessing server URL directly in browser
Review startup logs: Look for server initialization errors in gateway output
Permission Denied Errors
Symptom:DENIED_BY_POLICY when agent tries to use a tool
Expected: Search results from Brave (if server configured and running).
Troubleshooting:
Check Logs pane for errors
Verify agent_id exists in rules file
Confirm downstream servers configured
Review Message pane for policy denials
Development
Local Installation
Clone and install in development mode:
bash
# Clone repository
git clone https://github.com/roddutra/agent-mcp-gateway.git
cd agent-mcp-gateway
# Install dependencies
uv sync# Create local config files from examplescp config/.mcp.json.example .mcp.json
cp config/.mcp-gateway-rules.json.example .mcp-gateway-rules.json
# Run locally
uv run python main.py --help
Add Local Gateway to MCP Client
bash
# Claude Code CLI
claude mcp add agent-mcp-gateway \
uv run --directory /path/to/agent-mcp-gateway python main.py
# Or manual configuration
{
"mcpServers": {
"agent-mcp-gateway": {
"command": "uv",
"args": ["run", "--directory", "/path/to/agent-mcp-gateway", "python", "main.py"],
"env": {
"GATEWAY_DEFAULT_AGENT": "developer"
}
}
}
}
Note: The --directory flag tells uv run to change to the project directory before running, ensuring it finds pyproject.toml and the gateway configuration files.
Project Structure
code
agent-mcp-gateway/
βββ src/ # Core gateway implementation
βββ tests/ # Test suite
βββ config/ # Configuration examples
βββ docs/ # Documentation and specifications
βββ main.py # Entry point
βββ pyproject.toml # Python dependencies
Running in Development
bash
# Run locally
uv run python main.py
# With debug mode
uv run python main.py --debug
Testing
bash
# Run all tests
uv run pytest
# Run with coverage
uv run pytest --cov=src --cov-report=term
# Run specific test file
uv run pytest tests/test_gateway.py -v
# Run tests in watch mode
uv run pytest-watch
# Generate HTML coverage report
uv run pytest --cov=src --cov-report=html
open htmlcov/index.html
Testing with MCP Inspector:
bash
# Basic usage (uses local config files)
npx @modelcontextprotocol/inspector uv run python main.py
# With debug mode
npx @modelcontextprotocol/inspector uv run python main.py --debug
# With custom config paths
GATEWAY_MCP_CONFIG=.mcp.json \
GATEWAY_RULES=.mcp-gateway-rules.json \
GATEWAY_DEFAULT_AGENT=researcher \
npx @modelcontextprotocol/inspector uv run python main.py
Manual testing with FastMCP Client:
bash
uv run python -c "
import asyncio
from fastmcp import Client
async def test():
async with Client('main.py') as client:
result = await client.call_tool('list_servers', {'agent_id': 'researcher'})
print(result)
asyncio.run(test())
"
Adding a New Feature
Update specs: Document in relevant milestone file
Write tests first: Create test file in tests/
Implement feature: Add code in src/
Run tests: uv run pytest
Check coverage: uv run pytest --cov=src
Update docs: Document in README and relevant files
Middleware intercepts: Extracts and validates agent_id
Tool validates: Checks PolicyEngine for server/tool access
Proxy forwards: ProxyManager routes to downstream server
Session isolated: Each request gets fresh connection
Result returns: Transparently forwarded to agent
Audit logged: Operation recorded with metrics
Performance Characteristics
Context reduction: 90%+ (2k tokens vs 5,000-50,000+)
Added latency: <100ms (P95)
Gateway overhead: <30ms per operation
Session isolation: Automatic per-request
Concurrent requests: Fully supported
Future Features
M2: Production (Planned)
π§ Status: Not yet implemented
Features:
HTTP transport for gateway server
Health check endpoints
Enhanced error handling
Metrics export API
Connection pooling optimization
Rate limiting
When available:
bash
# Run with HTTP transportexport GATEWAY_TRANSPORT=http
export GATEWAY_PORT=8080
uv run python main.py
# Health check endpoint
curl http://localhost:8080/health
# Metrics endpoint
curl http://localhost:8080/metrics
M3: Developer Experience (Planned)
π§ Status: Not yet implemented
Features:
Single-agent mode (bypass agent_id requirement)
Config validation CLI tool
Docker container with examples
Interactive setup wizard
VS Code extension
When available:
bash
# Single-agent mode (no agent_id required)export GATEWAY_DEFAULT_AGENT=developer
uv run python main.py
# Validate configs
uv run python -m src.cli validate
# Run with Docker
docker run -v ./config:/config agent-mcp-gateway