Skip to main content
Glama
fastmcp-implementation-guide.md21.8 kB
# FastMCP 2.0 Implementation Guide for MCP Gateway Server A practical guide to using FastMCP 2.0 features for building an MCP gateway that proxies to downstream servers with per-agent access control. ## Core FastMCP 2.0 Features for Gateway Pattern ### 1. FastMCP.as_proxy() - Automatic Gateway Creation The `as_proxy()` class method creates a proxy server that automatically connects to multiple downstream MCP servers, discovers their tools, and exposes them through a unified interface. **Key capabilities:** - Accepts MCPConfig dictionary or single MCPClient - Automatically spawns stdio processes (uvx, npx, python) - Establishes HTTP/SSE connections to remote servers - Discovers and aggregates tools from all downstream servers - Prefixes tool names with server identifier to avoid conflicts - Routes tool calls to appropriate downstream server based on prefix **Basic usage:** ```python from fastmcp import FastMCP config = { "mcpServers": { "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": {"GITHUB_TOKEN": "ghp_xxxxx"} }, "filesystem": { "command": "uvx", "args": ["mcp-server-filesystem", "/workspace"] }, "weather": { "url": "https://weather-api.example.com/mcp" } } } gateway = FastMCP.as_proxy(config, name="MCP Gateway Gateway") ``` **Configuration options:** - `config`: MCPConfig dict or MCPClient instance - `name`: Server name exposed to upstream clients - `instructions`: Optional system instructions for the gateway - `prefixes`: Optional custom prefixes instead of server names ### 2. MCPConfig Format - Defining Downstream Servers FastMCP 2.0 natively supports the standard MCPConfig format used by Claude Desktop and other MCP clients. **Structure:** ```python { "mcpServers": { "<server_identifier>": { # For stdio transport (local processes) "command": "npx" | "uvx" | "python" | "node", "args": ["list", "of", "arguments"], "env": { "ENV_VAR": "value" }, # OR for HTTP transport (remote servers) "url": "https://remote-server.com/mcp", "headers": { "Authorization": "Bearer token" } } } } ``` **Stdio transport examples:** ```python # npx-based server "brave-search": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-brave-search"], "env": { "BRAVE_API_KEY": "BSA_xxxxx" } } # uvx-based server "fetch": { "command": "uvx", "args": ["mcp-server-fetch"] } # Direct Python execution "context7": { "command": "python", "args": ["-m", "context7_mcp.server"], "env": { "CONTEXT7_API_KEY": "ctx7_xxxxx" } } ``` **HTTP transport example:** ```python "remote-analytics": { "url": "https://analytics.company.com/mcp", "headers": { "Authorization": "Bearer company_token_here", "X-Client-ID": "gateway-v1" } } ``` ### 3. Dynamic Configuration Loading FastMCP 2.0 can load configurations dynamically from JSON files or environment variables, enabling users to manage their downstream servers without code changes. **From JSON file:** ```python import json from pathlib import Path def load_gateway_config(config_path: str) -> dict: """Load MCPConfig from JSON file""" with open(config_path, 'r') as f: return json.load(f) config = load_gateway_config("~/.config/mcp-gateway/servers.json") gateway = FastMCP.as_proxy(config, name="Dynamic Gateway") ``` **Example config file (`servers.json`):** ```json { "mcpServers": { "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" } }, "playwright": { "command": "uvx", "args": ["mcp-server-playwright"] } } } ``` ### 4. Middleware System - Per-Agent Access Control FastMCP 2.0's middleware system works across all transports (stdio, HTTP, SSE) and provides hooks for intercepting MCP operations. **Core middleware features:** - `on_call_tool`: Intercept tool call requests - `on_list_tools`: Filter available tools - `on_list_resources`: Filter resources - `MiddlewareContext`: Access to session_id, method, message, and server context - State management: Persist data across requests via `set_state()`/`get_state()` **Base middleware structure:** ```python from fastmcp.server.middleware import Middleware, MiddlewareContext from mcp.types import Tool class AgentAccessMiddleware(Middleware): """ Middleware to enforce per-agent access rules for servers and tools """ async def on_call_tool(self, context: MiddlewareContext, call_next): """Intercept tool calls and enforce access rules""" # Implementation details below pass async def on_list_tools(self, context: MiddlewareContext, call_next): """Filter tools list based on agent permissions""" # Implementation details below pass ``` ### 5. Extracting Agent Identity from Tool Calls Agents calling the gateway should provide an `agent_id` parameter with their tool calls. FastMCP middleware can extract this from the tool arguments. **Implementation:** ```python async def on_call_tool(self, context: MiddlewareContext, call_next): """Extract agent_id from tool arguments""" # Get the tool call message tool_call = context.message tool_name = tool_call.name # e.g., "github_create_issue" arguments = tool_call.arguments or {} # Extract agent_id from arguments agent_id = arguments.get("agent_id") if not agent_id: # Fallback to session_id or default agent agent_id = context.session_id or "default_agent" # Remove agent_id from arguments before forwarding # to downstream server (they don't need to know about it) clean_arguments = {k: v for k, v in arguments.items() if k != "agent_id"} # Store agent_id in context for other middleware/tools context.set_state("current_agent", agent_id) # Proceed with cleaned arguments return await call_next(context) ``` **Agent tool call format:** ```python # When agent calls a tool, it includes agent_id { "method": "tools/call", "params": { "name": "github_create_issue", "arguments": { "agent_id": "frontend_agent", # <-- Agent identification "repository": "myorg/myrepo", "title": "Bug report", "body": "Description..." } } } ``` ### 6. Access Rules Configuration Define which agents can access which servers and tools through a configuration structure. **Rules format:** ```python access_rules = { "main_orchestrator": { "servers": [], # No direct server access "tools": ["list_servers", "get_server_tools"] # Only discovery tools }, "researcher_agent": { "servers": ["brave-search", "fetch", "context7"], "tools": "*" # All tools from allowed servers }, "frontend_agent": { "servers": ["context7", "playwright"], "tools": { "context7": "*", # All tools from context7 "playwright": [ # Limited tools from playwright "playwright_navigate", "playwright_screenshot", "playwright_click" ] } }, "backend_agent": { "servers": ["github", "database"], "tools": "*" } } ``` **Loading from JSON file:** ```json { "access_rules": { "researcher_agent": { "servers": ["brave-search", "fetch"], "tools": "*" }, "frontend_agent": { "servers": ["playwright"], "tools": { "playwright": ["playwright_navigate", "playwright_screenshot"] } } } } ``` ### 7. Complete Middleware Implementation Full implementation of access control middleware with both server-level and tool-level filtering. ```python from fastmcp.server.middleware import Middleware, MiddlewareContext from fastmcp.exceptions import ToolError from mcp.types import Tool class AgentAccessControl(Middleware): """ Enforces per-agent access rules for downstream servers and tools """ def __init__(self, access_rules: dict): """ Args: access_rules: Dict mapping agent names to their allowed servers/tools """ self.access_rules = access_rules def _get_server_from_tool(self, tool_name: str) -> str: """Extract server prefix from prefixed tool name""" # Tool format: "servername_toolname" parts = tool_name.split("_", 1) return parts[0] if len(parts) > 1 else "" def _is_tool_allowed(self, agent_id: str, tool_name: str) -> bool: """Check if agent has permission to call this tool""" # Get agent rules or deny by default agent_rules = self.access_rules.get(agent_id) if not agent_rules: return False # Extract server from prefixed tool name server = self._get_server_from_tool(tool_name) # Check if agent has access to this server allowed_servers = agent_rules.get("servers", []) if server not in allowed_servers: return False # Check tool-level permissions tools_config = agent_rules.get("tools") # If tools is "*", allow all tools from allowed servers if tools_config == "*": return True # If tools is a dict, check server-specific tool rules if isinstance(tools_config, dict): server_tools = tools_config.get(server, []) # If server has "*", allow all its tools if server_tools == "*": return True # Check if specific tool is in allowed list # Remove server prefix for comparison tool_base_name = tool_name.replace(f"{server}_", "", 1) return tool_base_name in server_tools return False async def on_call_tool(self, context: MiddlewareContext, call_next): """Enforce access rules on tool calls""" tool_call = context.message arguments = tool_call.arguments or {} # Extract agent identity agent_id = arguments.get("agent_id") or context.session_id if not agent_id: raise ToolError("No agent identity provided") # Check if tool is allowed for this agent if not self._is_tool_allowed(agent_id, tool_call.name): raise ToolError( f"Agent '{agent_id}' is not authorized to call tool " f"'{tool_call.name}'" ) # Store agent name in context state context.set_state("current_agent", agent_id) # Remove agent_id from arguments before forwarding clean_arguments = {k: v for k, v in arguments.items() if k != "agent_id"} # Update message with cleaned arguments tool_call.arguments = clean_arguments # Allow the request to proceed return await call_next(context) async def on_list_tools(self, context: MiddlewareContext, call_next): """Filter tools list based on agent permissions""" # Get the full list of tools from downstream servers response = await call_next(context) # Extract agent identity from stored state or session agent_id = context.get_state("current_agent") or context.session_id if not agent_id or agent_id not in self.access_rules: # Return empty list for unknown agents return [] # Filter tools based on agent permissions filtered_tools = [ tool for tool in response.tools if self._is_tool_allowed(agent_id, tool.name) ] # Return filtered response response.tools = filtered_tools return response ``` ### 8. Adding Custom Discovery Tools Add `list_servers` and `get_server_tools` as native tools on the gateway itself, not proxied from downstream servers. ```python from fastmcp import FastMCP, Context from mcp.types import Tool @gateway.tool async def list_servers(agent_id: Optional[str] = None, ctx: Context) -> list[dict]: """ List all MCP servers available to the calling agent Args: agent_id: Identifier of the agent making the request (optional, uses fallback chain) Returns: List of server information dicts """ # Get agent's access rules access_rules = ctx.get_state("access_rules") or {} agent_rules = access_rules.get(agent_id, {}) allowed_servers = agent_rules.get("servers", []) # Get full server config mcp_config = ctx.get_state("mcp_config") or {} all_servers = mcp_config.get("mcpServers", {}) # Return info for allowed servers only server_list = [] for server_name in allowed_servers: if server_name in all_servers: server_config = all_servers[server_name] server_list.append({ "name": server_name, "transport": "stdio" if "command" in server_config else "http", "description": server_config.get("description", "") }) return server_list @gateway.tool async def get_server_tools( server_name: str, agent_id: Optional[str] = None, ctx: Context ) -> list[dict]: """ Get all tools available from a specific server for the calling agent Args: server_name: Name of the downstream MCP server agent_id: Identifier of the agent making the request Returns: List of tool information dicts """ # Verify agent has access to this server access_rules = ctx.get_state("access_rules") or {} agent_rules = access_rules.get(agent_id, {}) allowed_servers = agent_rules.get("servers", []) if server_name not in allowed_servers: return [] # Get all tools from the gateway all_tools = await ctx.list_tools() # Filter to tools from requested server server_tools = [ { "name": tool.name.replace(f"{server_name}_", "", 1), "description": tool.description or "", "input_schema": tool.inputSchema } for tool in all_tools if tool.name.startswith(f"{server_name}_") ] # Apply tool-level filtering based on agent rules tools_config = agent_rules.get("tools") if tools_config == "*": return server_tools if isinstance(tools_config, dict): allowed_tool_names = tools_config.get(server_name, []) if allowed_tool_names == "*": return server_tools # Filter to allowed tools only return [ tool for tool in server_tools if tool["name"] in allowed_tool_names ] return [] ``` ### 9. State Management for Configuration Store configurations in gateway context so middleware and tools can access them. ```python from fastmcp import FastMCP # Load configurations mcp_config = load_gateway_config("servers.json") access_rules = load_access_rules("access_rules.json") # Create gateway with proxy gateway = FastMCP.as_proxy(mcp_config, name="MCP Gateway Gateway") # Store configs in gateway context for middleware/tools to access gateway.set_state("mcp_config", mcp_config) gateway.set_state("access_rules", access_rules) # Add middleware gateway.add_middleware(AgentAccessControl(access_rules)) # Add custom tools (list_servers and get_server_tools from above) # ... (decorator-based tools defined earlier) # Run the gateway gateway.run(transport="http", port=8000) ``` ### 10. ProxyClient for Advanced Scenarios For more granular control over downstream connections, use `ProxyClient` directly instead of `as_proxy()`. **Use cases:** - Custom connection lifecycle management - Session-specific downstream connections - Advanced routing logic - Error handling and retries **Basic usage:** ```python from fastmcp.client import ProxyClient # Create client for specific downstream server github_client = ProxyClient( command="npx", args=["-y", "@modelcontextprotocol/server-github"], env={"GITHUB_TOKEN": "ghp_xxxxx"} ) # Or for HTTP server weather_client = ProxyClient( url="https://weather-api.example.com/mcp" ) # Or for OAuth-protected HTTP server notion_client = ProxyClient( url="https://mcp.notion.com/mcp", auth="oauth" # Enables OAuth auto-detection ) # Note: OAuth activates only if server returns 401 # Browser opens for initial authentication, tokens cached thereafter # Connect and use async with github_client: tools = await github_client.list_tools() result = await github_client.call_tool( "create_issue", {"repo": "myorg/myrepo", "title": "Bug"} ) ``` **When to use ProxyClient:** - Implementing custom routing logic beyond simple prefixing - Need to manage connections with specific lifecycle requirements - Building a multi-tenant gateway with per-user server instances - Advanced error handling or connection pooling ### 11. Complete Gateway Setup Example Putting it all together: ```python from fastmcp import FastMCP, Context from fastmcp.server.middleware import Middleware, MiddlewareContext from fastmcp.exceptions import ToolError import json from pathlib import Path # Load configurations def load_config(path: str) -> dict: with open(Path(path).expanduser(), 'r') as f: return json.load(f) mcp_config = load_config("~/.config/mcp-gateway/servers.json") access_rules = load_config("~/.config/mcp-gateway/access_rules.json") # Create gateway with automatic proxying gateway = FastMCP.as_proxy( mcp_config, name="MCP Gateway Gateway", instructions="Central gateway for managing access to downstream MCP servers" ) # Store configs in gateway state gateway.set_state("mcp_config", mcp_config) gateway.set_state("access_rules", access_rules) # Add access control middleware gateway.add_middleware(AgentAccessControl(access_rules)) # Add discovery tools @gateway.tool async def list_servers(agent_id: str, ctx: Context) -> list[dict]: """List servers available to agent""" # Implementation from section 8 pass @gateway.tool async def get_server_tools( server_name: str, agent_id: str, ctx: Context ) -> list[dict]: """Get tools from specific server""" # Implementation from section 8 pass # Run gateway on HTTP if __name__ == "__main__": gateway.run(transport="http", port=8000) ``` ### 12. Alternative: mount() for Manual Composition If you need more control than `as_proxy()` provides, use `mount()` to manually compose multiple FastMCP servers. ```python from fastmcp import FastMCP # Create individual proxy servers github = FastMCP.as_proxy({ "mcpServers": { "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"] } } }) filesystem = FastMCP.as_proxy({ "mcpServers": { "fs": { "command": "uvx", "args": ["mcp-server-filesystem", "/workspace"] } } }) # Create main gateway and mount sub-servers gateway = FastMCP(name="Main Gateway") gateway.mount("/github", github) gateway.mount("/filesystem", filesystem) # Add middleware and custom tools to main gateway gateway.add_middleware(AgentAccessControl(access_rules)) # Run gateway.run(transport="http", port=8000) ``` **Note:** `as_proxy()` is simpler and recommended for most gateway use cases. ## Summary: Key FastMCP 2.0 Features for Gateway 1. **FastMCP.as_proxy()**: One-line gateway creation from MCPConfig 2. **MCPConfig format**: Standard configuration for stdio and HTTP servers 3. **Automatic tool discovery**: Finds and prefixes tools from all downstream servers 4. **Middleware system**: Cross-transport hooks for access control 5. **MiddlewareContext**: Access to session_id, method, message, and state 6. **State management**: Store configs and agent data with `set_state()`/`get_state()` 7. **Custom tools**: Add gateway-specific tools like `list_servers` via decorators 8. **ProxyClient**: Fine-grained control for advanced routing scenarios 9. **Dynamic configuration**: Load server and access rules from JSON files 10. **Tool filtering**: Per-agent tool list filtering via `on_list_tools` middleware ## Implementation Checklist - [ ] Define MCPConfig JSON with all downstream servers - [ ] Define access_rules JSON with per-agent server/tool permissions - [ ] Create gateway with `FastMCP.as_proxy(mcp_config)` - [ ] Store configs in gateway state for middleware/tools to access - [ ] Implement `AgentAccessControl` middleware - [ ] Add `list_servers` and `get_server_tools` custom tools - [ ] Register middleware with `gateway.add_middleware()` - [ ] Test with different agent identities - [ ] Run gateway with `gateway.run(transport="http", port=8000)` - [ ] Configure Claude Code to use gateway as MCP server ## Next Steps 1. Start with a minimal config of 2-3 downstream servers 2. Implement basic middleware without tool-level filtering 3. Test agent access control with sample agents 4. Add tool-level filtering once server-level filtering works 5. Implement the discovery tools (list_servers, get_server_tools) 6. Build configuration management UI (future enhancement)

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/roddutra/agent-mcp-gateway'

If you have feedback or need assistance with the MCP directory API, please join our Discord server