ADR 020: Deprecate Context Forge, Serve MCP Directly from the Monolith
Author: jomcgi Status: Accepted Created: 2026-06-23 Supersedes: 003 - Context ForgeRelates to: 006 - OIDC Auth MCP Gateway (auth has since moved to the Cloudflare Access edge), 019 - Substrate Executor + AgentWorkflow (the agent-platform deprecation that removed the other MCP consumers)
Problem
ADR 003 adopted Context Forge (IBM mcpgateway) as a federating MCP gateway: many MCP servers behind one endpoint, RBAC-scoped toolsets per agent team, tool-registration validation, and an OAuth flow. That made sense when the agent platform had several MCP consumers and the gateway was the natural seam for auth and federation.
That world is gone. After the agent-platform deprecation (the teardown following ADR 019), three things are now true:
- Auth is at the edge, not in the gateway.
mcp.jomcgi.devis gated by Cloudflare Access (Zero Trust); Context Forge trusts the Cloudflare-authenticated request and adds no auth of its own. - There is exactly one backend. The monolith is the only connected MCP server. The federation that justified a gateway has nothing left to federate.
- The monolith already serves MCP itself.
app/main.pymountsFastMCP("Monolith")at/mcp. A direct port-forward probe returnsHTTP 200,content-type: text/event-stream, and a valid MCP SSE handshake. No gateway is required for the protocol to work.
So Context Forge is now single-backend pass-through middleware: no federation value, no auth value, and a standing operational cost, it caches tool catalogs and does not auto-refresh them, so new or edited monolith tools stay invisible until a manual gateway refresh (see the Context Forge tool-refresh reference). It earns its removal the same way the agent platform did, except it is live infrastructure, so this is a migration, not a delete.
Decision
Deprecate Context Forge. Serve the monolith's MCP directly. The path becomes claude.ai connector to mcp.jomcgi.dev (Cloudflare Access, unchanged) to the Cloudflare tunnel to the monolith's /mcp endpoint, with Context Forge removed from the path entirely. Then delete Context Forge (mcpgateway + its Postgres + Redis), the orphaned mcp-oauth-proxy service, and the mcp namespace.
Execution is deferred. This ADR records the decision and a validated migration plan. The live mcp.jomcgi.dev route is not touched by this ADR.
| Aspect | Today (via Context Forge) | Decided (direct) |
|---|---|---|
| Auth | Cloudflare Access at the edge | Cloudflare Access at the edge (unchanged) |
| Gateway | Context Forge federates one backend | None; the monolith is the endpoint |
| Tool names | monolith-<fn-with-hyphens> (CF prefixes monolith- and swaps _->-) | native FastMCP names (the function names, with underscores) |
| Tool-catalog refresh | manual POST after every tool change | not applicable; the monolith is the source of truth |
| Services to run | mcpgateway + Postgres + Redis (+ a dead oauth-proxy) | none |
Validated facts (probed from the cluster)
- The monolith
/mcpSSE endpoint is alive and speaks the protocol (200,text/event-stream,event: endpointhandshake) with Context Forge out of the path. - There are 41 MCP tools, all registered with
@mcp.tool(no explicitname=), so their MCP names are the function names. - Context Forge's only transform is name-mangling: native
search_knowledgeis exposed asmonolith-search-knowledge; nativemonolith_agent_notifybecomes the double-prefixedmonolith-monolith-agent-notify. So the rename is deterministic: strip themonolith-gateway prefix and convert-back to_. No per-tool judgment is needed; the full map can be generated mechanically and applied to the claude.ai connector config and the routine YAMLs.
Architecture
graph LR
subgraph Before
A1[claude.ai connector] --> B1[mcp.jomcgi.dev<br/>Cloudflare Access]
B1 --> C1[CF tunnel] --> D1[Context Forge<br/>mcpgateway + pg + redis]
D1 --> E1[monolith /mcp]
end
subgraph After
A2[claude.ai connector] --> B2[mcp.jomcgi.dev<br/>Cloudflare Access]
B2 --> C2[CF tunnel] --> E2[monolith /mcp<br/>FastMCP]
end
style D1 fill:#999,color:#fffMigration plan (deferred execution)
- Resolve transport. The monolith mounts FastMCP over SSE; switch it to streamable-HTTP (
http_app(transport="http", ...), a one-line change and the transport modern claude.ai connectors prefer), or validate the SSE endpoint via a throwaway parallel test connector. The curl probe confirms the server side; only the claude.ai client handshake is unverified, and streamable-HTTP removes that risk. - Repoint the route. Change the
mcp.jomcgi.devCloudflare tunnel backend from the Context Forge service to the monolith service/mcp. Cloudflare Access on the hostname is untouched. - Rename the 41 tools in the claude.ai connector and every routine YAML in
projects/monolith/claude_routines/(and the CLAUDE.md references), using the deterministic CF-to-native map. - Validate end to end by firing a routine against the direct endpoint.
- Delete Context Forge: the
mcpgatewaychart (gateway + Postgres + Redis), the deadmcp-oauth-proxyservice, and themcpnamespace, with the same finalizer-first / cascade discipline used in the agent-platform teardown.
Alternatives Considered
- Keep Context Forge. Rejected: single-backend pass-through with no auth or federation value, plus the standing manual tool-refresh cost.
- Build a name-compatibility shim (make the monolith emit the CF-mangled names so nothing renames). Rejected: it preserves cruft to avoid a one-time deterministic rename; the native names are cleaner and the rename is mechanical.
- Weighted traffic split (Envoy
HTTPRoute) as a canary. Rejected: the two backends are not interchangeable, they expose different tool names (and possibly different transports), so a 50/50 split would make routines fail on whichever half lacked the expected tool. MCP sessions are sticky (long-lived SSE), so it is per-session roulette, not a clean validation. A parallel test connector gives the same safety without the equivalence problem, and a single-user audience does not need production traffic splitting.
Security
Baseline in docs/security.md. No new exposure:
- Auth is unchanged. Cloudflare Access remains on
mcp.jomcgi.devat the edge, gating every request before it reaches the cluster. This is already where auth lives; Context Forge only trusted it. - The monolith
/mcpstays internal. It is reachable only through the Cloudflare-gated tunnel, exactly as Context Forge was; removing the gateway does not put it on the public internet. - ADR 006's gateway-level OIDC is superseded by edge auth, which is already the operative reality.
Risks
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| claude.ai connector cannot use the monolith's SSE transport directly | Medium | Medium | Switch the monolith to streamable-HTTP (one line) or validate SSE with a parallel test connector before repointing |
| Routine breakage from the tool rename | Medium | Medium | The CF-to-native map is deterministic; apply it to all routines + the connector, then validate a routine before deleting Context Forge |
Cutover leaves mcp.jomcgi.dev pointing at a missing backend mid-flight | Low | High | Repoint and rename atomically; keep Context Forge running until a routine fires against the direct endpoint |
| A second gateway (e.g. a GitHub MCP server) turns out to depend on Context Forge | Low | Low | The monolith is the only connected backend today; confirm before deletion |
Open Questions
- Streamable-HTTP versus SSE as the final monolith MCP transport. Lean: streamable-HTTP, for connector compatibility.
- Is any non-monolith gateway (the GitHub gateway noted in CLAUDE.md) actually in use? If not, it goes with Context Forge; if so, it needs a home first.
- Does the claude.ai connector need re-registration (new URL/transport) or only a settings edit, when the backend changes?
References
| Resource | Relevance |
|---|---|
| 003 - Context Forge | The gateway being deprecated |
| 006 - OIDC Auth MCP Gateway | Gateway-level auth, superseded by Cloudflare Access at the edge |
| 019 - Substrate Executor + AgentWorkflow | The agent-platform deprecation that removed the other MCP consumers |
projects/monolith/app/main.py | Mounts FastMCP("Monolith") at /mcp (the direct endpoint) |
projects/monolith/claude_routines/ | The routine YAMLs whose tool references the rename touches |