How antinomyhq/forgecode Implements the Model Context Protocol (MCP): Architecture and Limitations
Forgecode implements MCP as a lazy-loading dynamic tool provider that bridges remote STDIO and HTTP/SSE servers into locally namespaced functions, though it imposes fixed timeouts, requires manual cache refreshing, and lacks support for streaming responses.
Forgecode's approach to the Model Context Protocol (MCP) centers on treating external servers as dynamic tool sources that integrate seamlessly with its agentic workflow. The antinomyhq/forgecode implementation spans three architectural layers—Domain, Service, and Infrastructure—and emphasizes deterministic caching and transport abstraction to minimize reconnection overhead. This article examines how the codebase transforms MCP tools into executable functions and details the concrete limitations you will encounter when extending its capabilities.
MCP Architecture Overview
The codebase organizes MCP functionality into three distinct layers that separate configuration from execution. The Domain layer in crates/forge_domain/src/mcp.rs defines the data structures for server configurations and OAuth settings. The Service layer in crates/forge_services/src/mcp/service.rs orchestrates lazy initialization, connection management, and tool registration. Finally, the Infrastructure layer in crates/forge_infra/src/mcp_client.rs handles low-level transport concerns including STDIO child processes, HTTP/SSE streams, and OAuth token negotiation.
Domain Layer: Configuration and Cache Keys
Located in crates/forge_domain/src/mcp.rs, the domain layer provides the type system that governs how MCP servers are declared and indexed.
Server Configuration Types
The system supports two primary transport mechanisms through the McpServerConfig enum (lines 25‑55):
pub enum McpServerConfig {
Stdio(McpStdioServer), // exec a local binary (lines 25‑38)
Http(McpHttpServer), // HTTP / SSE endpoint (lines 41‑55)
}
STDIO servers require a command, arguments, and optional environment variables, while HTTP servers specify a URL, headers, request timeout, and OAuth settings. This distinction determines which transport logic the infrastructure layer invokes during connection establishment.
OAuth Flexibility
The McpOAuthSetting enum (lines 58‑73) supports three authentication strategies:
#[derive(Default)]
pub enum McpOAuthSetting {
#[default] AutoDetect, // try standard HTTP first, then OAuth if 401
Disabled, // never use OAuth
Configured(McpOAuthConfig), // explicit client‑id/secret, scopes, etc.
}
Deserialization is tolerant: the value false maps to Disabled, true or null to AutoDetect, and a full configuration object to Configured. This allows configuration files to toggle OAuth without restructuring the entire server definition.
Deterministic Cache Keys
To avoid unnecessary reconnections, McpConfig::cache_key() (lines 92‑103) computes a hash of the entire BTreeMap of servers. Because BTreeMap guarantees stable ordering, the hash changes if and only if the logical configuration changes—not merely the file modification time. This key drives the lazy-reload logic in the service layer.
Service Layer: Lazy Initialization and Tool Registration
The ForgeMcpService in crates/forge_services/src/mcp/service.rs acts as the orchestrator, ensuring that MCP connections are established only when needed and cached aggressively.
Lazy Initialization with Hash-Based Caching
The init_mcp method (lines 49‑51) checks whether the configuration has changed since the last invocation:
async fn init_mcp(&self) -> anyhow::Result<()> {
let mcp = self.manager.read_mcp_config(None).await?;
if !self.is_config_modified(&mcp).await { return Ok(()); }
self.update_mcp(mcp).await
}
If the cache key matches the stored hash, no reconnection occurs (lines 100‑104), allowing the system to skip expensive process spawns or HTTP handshakes during repeated tool calls.
Tool Discovery and Namespacing
When configuration changes are detected, update_mcp iterates over enabled servers and spawns a future per server via the connect method (lines 17‑31). Upon successful connection, the system registers every remote tool with a generated name to prevent collisions:
let tools = client.list().await?;
for mut tool in tools {
let generated_name = ToolName::new(
format!("mcp_{server_name}_tool_{}", tool.name.into_sanitized())
);
tool.name = generated_name.clone();
tool_map.insert(generated_name, ToolHolder { … });
}
This namespacing strategy (lines 62‑68) guarantees uniqueness by prefixing each tool with mcp_<server>_tool_<sanitized_original_name>, though this mangling is irreversible and replaces the original identifier.
Tool Call Forwarding
When the core engine invokes a tool, ForgeMcpService::call first ensures the MCP setup is current by calling self.init_mcp().await?, then looks up the generated tool name and forwards the request to the stored McpExecutor (lines 73‑80). Errors from the remote server are captured and returned as structured ToolOutput instances.
Infrastructure Layer: Transport and Connection
ForgeMcpClient in crates/forge_infra/src/mcp_client.rs implements the actual network and process management, handling template resolution and protocol selection.
STDIO Transport
For STDIO servers, the client spawns a child process using TokioChildProcess::builder and immediately drains stderr to avoid deadlocks (lines 18‑33). This ensures that MCP servers writing diagnostic logs to standard error do not block when their buffers fill.
HTTP/SSE and OAuth Handling
HTTP connections follow a fallback pattern. The client first attempts create_standard_http_connection (lines 78‑89). If this fails with an authentication error (401/unauthorized) and the server configuration uses AutoDetect mode, the client automatically retries with create_oauth_connection (lines 46‑71).
When OAuth is explicitly Disabled, the client never attempts OAuth (lines 36‑39). When an explicit McpOAuthConfig is supplied, the client connects directly using stored credentials without interactive prompts (lines 40‑44). Notably, the client never launches an interactive browser flow itself; authentication must be performed out-of-band using forge mcp login.
Template Resolution
Before connecting, get_resolved_config (lines 57‑66) expands Mustache placeholders in HTTP headers and URLs using environment variables. This occurs once at client creation, meaning changes to environment variables after initialization do not affect active connections.
Key Limitations of the MCP Implementation
While the architecture provides clean separation of concerns, several constraints affect production deployments and developer experience.
Fixed 5-Minute Timeout
All HTTP and SSE connections use a hard-coded 5-minute timeout (tokio::time::Duration::from_secs(300)) when accepting TCP listeners (lines 408‑410, 640‑642). This value is not exposed in McpHttpServer configuration, preventing users from tuning timeouts for long-running operations or high-latency networks.
Out-of-Band OAuth Authentication
The MCP client never initiates an interactive OAuth flow during normal operation. Users must run forge mcp login separately to acquire and store tokens before the service can authenticate. This decouples authentication from the tool invocation path, which can confuse end-users expecting automatic browser prompts when a 401 occurs.
No Automatic Reconnection
Once a server fails during update_mcp, it is recorded in failed_servers (lines 33‑46) and excluded from subsequent tool calls. The service never retries the connection until refresh_cache is invoked or the process restarts, meaning transient network failures require manual intervention to resolve.
Limited Transport Support
The transport layer is strictly limited to STDIO and HTTP/SSE protocols (matching on McpServerConfig::Stdio or Http). Custom transports such as WebSocket or gRPC are unsupported without modifying crates/forge_infra/src/mcp_client.rs.
Irreversible Tool-Name Mangling
The generated tool name pattern (mcp_<server>_tool_<sanitized>) replaces the original tool identifier during registration (lines 62‑68). The original name is not preserved, which may break tools that expect specific identifiers in their metadata or logging contexts.
No Streaming Support
The McpExecutor forwards tool calls and waits for a single ToolOutput response. MCP servers that stream incremental results cannot be leveraged, as the client aggregates all content before returning.
Manual Cache Invalidation
The only mechanism to detect changes to an MCP server’s tool set is the refresh_cache method (lines 84‑92), which must be invoked explicitly (e.g., via mcp reload). There is no filesystem watcher or automatic polling for configuration updates.
Limited Error Propagation
Connection errors are stored as formatted strings in failed_servers for UI display (lines 37‑44). Higher-level layers cannot programmatically distinguish between connection refused, authentication failure, or timeout errors without parsing these strings.
No Per-Tool Configuration
All tools from a single server share the same connection pool, timeout, and headers. Individual tools cannot request custom timeouts or additional headers, as the McpExecutor receives the entire client configuration rather than per-tool overrides.
Static Environment Resolution
Environment variable expansion via get_resolved_config occurs only once during client creation. If environment variables change at runtime (e.g., rotating API keys), the client does not re-render the URL or headers until manually reinitialized.
Practical Usage Example
Calling an MCP tool through Forgecode requires using the mangled tool name constructed by the service layer:
// Assuming an MCP config file defines a server named "mytools"
let call = ToolCallFull {
name: ToolName::new("mcp_mytools_tool_generate_image".into()),
arguments: json!({ "prompt": "sunrise over mountains" }),
};
let output = forge_mcp_service.call(call).await?;
println!("Image URL: {}", output.result);
Execution flow:
calltriggersForgeMcpService::call, which invokesinit_mcpto verify the cache key.- If the config changed,
update_mcpestablishes a connection viaForgeMcpClient::connect, selecting STDIO or HTTP transport. - For HTTP servers, the client tries standard headers first, falling back to OAuth if it receives a 401 and
AutoDetectis enabled. - The tool list is fetched, namespaced, and the call is forwarded to the remote server via the cached
McpExecutor.
Summary
- Lazy initialization: Forgecode uses deterministic hashing to avoid reconnecting to MCP servers unless configuration changes occur.
- Namespace isolation: Tools are prefixed with
mcp_<server>_tool_to prevent naming collisions across different servers. - Transport flexibility: Supports both STDIO child processes and HTTP/SSE endpoints with optional OAuth fallback.
- Operational constraints: Fixed 5-minute timeouts, manual cache invalidation via
refresh_cache, and no support for streaming responses limit high-availability scenarios. - Authentication model: OAuth requires pre-authentication via CLI commands; the client does not handle interactive flows during tool execution.
Frequently Asked Questions
Does antinomyhq/forgecode support WebSocket or gRPC MCP servers?
No. As implemented in crates/forge_infra/src/mcp_client.rs, the transport layer strictly handles STDIO child processes and HTTP/SSE connections. The McpServerConfig enum only contains variants for Stdio and Http, meaning WebSocket or gRPC transports require modifying the infrastructure layer source code.
How does forgecode handle OAuth authentication for MCP servers?
Forgecode supports three OAuth modes—AutoDetect, Disabled, and Configured—defined in crates/forge_domain/src/mcp.rs. However, the client in crates/forge_infra/src/mcp_client.rs never initiates an interactive browser flow. Users must run forge mcp login to acquire tokens, which the client then reads from the environment store. When AutoDetect is enabled, the client retries with OAuth only after receiving a 401 from the initial standard HTTP attempt.
Can I configure different timeouts for individual MCP tools?
No. The timeout is hard-coded to 5 minutes (300 seconds) in crates/forge_infra/src/mcp_client.rs (lines 408‑410) and applies to the entire HTTP transport layer. There is no per-tool or per-server timeout override in the current McpHttpServer configuration structure.
What happens if an MCP server crashes during a session?
If a server fails during the initial update_mcp call, it is added to the failed_servers list in crates/forge_services/src/mcp/service.rs (lines 33‑46) and excluded from subsequent tool lookups. The service does not automatically retry failed connections. To recover, you must invoke refresh_cache (lines 84‑92) or restart the Forgecode process to trigger a fresh discovery cycle.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →