Per-request plugin & MCP-session re-initialisation (session pool not engaging) + observability adapter kwarg error
Summary
On a CPEX-based deployment of the ContextForge MCP gateway, the gateway rebuilds the entire CPEX plugin manager (all configured plugins) on every request, and re-initialises the upstream MCP session per request despite MCP_SESSION_POOL_ENABLED=true. A third, minor issue: the observability adapter raises a TypeError on every hook. Together these cause sustained wasted CPU, added per-request latency, and mid-call upstream stream resets that can break clients proxying through the gateway.
This breaks the requested work into three concrete, independently-shippable fixes.
Environment
- ContextForge MCP gateway (CPEX plugin framework,
cpex.framework.*), recent 1.0.x line
- WSGI/ASGI server with multiple workers and
preload enabled
MCP_SESSION_POOL_ENABLED=true
1. PluginManager is rebuilt on every request (should be a per-worker singleton)
Symptom. Every inbound request emits a full plugin-manager bootstrap, tagged with a distinct request_id:
{"name": "cpex.framework.manager", "message": "Plugin manager initialized with N plugins", "request_id": "<request_id>"}
{"name": "cpex.framework.manager", "message": "Loaded plugin: <PluginName> (mode: sequential)", "request_id": "<request_id>"}
… (repeated for every configured plugin, on every request)
Evidence. Roughly one full plugin-manager rebuild per request (distinct request_id each time) — config parse, plugin import/instantiation, and hook registration repeated on every call. Under normal load this is many thousands of rebuilds per day.
Root cause. The plugin manager is constructed in request scope instead of being reused at the worker/process level.
Requested fix. Make the PluginManager a process/worker singleton, initialised once at worker startup — re-apply the borg-singleton pattern (shared-state conditional guard in the constructor) inside cpex.framework.manager, or cache the manager at the integration boundary. (Ref: mcp-context-forge#2010, #2017.)
Implementation notes.
- With
preload enabled, prefer lazy per-worker init (post-fork) guarded by a lock, rather than import-time init, unless all plugins are fork-safe (some may hold sockets/threads).
- The manager must be stateless with respect to the request — per-request identity/tenant/headers must be passed as hook call arguments/context, not stored on
self. Any identity/auth-injecting plugin is the one to audit here.
2. Upstream MCP session re-initialised per request (MCP_SESSION_POOL_ENABLED not engaging)
Symptom. Each proxied request opens a new upstream MCP session (initialize + tool-list handshake) rather than reusing a pooled one, adding session-setup latency per call and creating churn on the upstream server.
Root cause. The session/pool appears to be created in request scope (so the pool is always empty) or keyed per-request, so MCP_SESSION_POOL_ENABLED=true never actually reuses a session.
Requested fix. Reuse the upstream MCP session per (worker × upstream server) so MCP_SESSION_POOL_ENABLED actually engages. (Ref: ADR-032, #4799.)
Implementation notes.
- Hoist the pool to worker/module scope and key it by
(upstream_server_id, auth_identity) when sessions carry per-identity auth, or (upstream_server_id) when auth is attached per tool-call header. If auth is applied per session, the pool key must include identity to avoid cross-tenant session reuse.
- A single MCP stream is generally not safe for interleaved concurrent calls — use one session per worker (workers serialise) or a bounded lease → call → return/discard pool with a health-check on checkout.
3. Observability adapter raises on every hook (start_span kwarg mismatch)
Symptom. Repeated on every observability hook (several times per request):
ObservabilityServiceAdapter.start_span() got an unexpected keyword argument 'db'
The observability adapter is called with a db= keyword the adapter signature doesn't accept, so each hook throws and is logged as a WARNING.
Requested fix. Align the signature — add db=None (and **kwargs for forward-compat) to ObservabilityServiceAdapter.start_span(...), threading db through if it's needed for span/DB correlation or accepting-and-ignoring it otherwise. Low-risk, isolated; removes many exception constructions + WARNINGs per request.
Downstream impact (why this is more than cosmetic)
The per-request rebuild churn (§1) can cause the gateway to reset/close upstream MCP streams mid-call under load. A client proxying a tool call through the gateway then sees its in-flight call cancelled partway through, which surfaces as request failures on that client. Beyond correctness, the rebuild and per-request session handshake add latency to every proxied tool call.
Reproduction
- Deploy with
MCP_SESSION_POOL_ENABLED=true and multiple workers.
- Send N sequential tool-proxy requests to an upstream MCP server.
- Observe: (a) one
Plugin manager initialized with N plugins per request with a distinct request_id; (b) a new upstream session handshake per request; (c) repeated start_span() got an unexpected keyword argument 'db' WARNINGs per request.
Acceptance criteria
References
- Borg-singleton pattern:
mcp-context-forge#2010, #2017
- Session reuse: ADR-032,
#4799
Per-request plugin & MCP-session re-initialisation (session pool not engaging) + observability adapter kwarg error
Summary
On a CPEX-based deployment of the ContextForge MCP gateway, the gateway rebuilds the entire CPEX plugin manager (all configured plugins) on every request, and re-initialises the upstream MCP session per request despite
MCP_SESSION_POOL_ENABLED=true. A third, minor issue: the observability adapter raises aTypeErroron every hook. Together these cause sustained wasted CPU, added per-request latency, and mid-call upstream stream resets that can break clients proxying through the gateway.This breaks the requested work into three concrete, independently-shippable fixes.
Environment
cpex.framework.*), recent 1.0.x linepreloadenabledMCP_SESSION_POOL_ENABLED=true1.
PluginManageris rebuilt on every request (should be a per-worker singleton)Symptom. Every inbound request emits a full plugin-manager bootstrap, tagged with a distinct
request_id:Evidence. Roughly one full plugin-manager rebuild per request (distinct
request_ideach time) — config parse, plugin import/instantiation, and hook registration repeated on every call. Under normal load this is many thousands of rebuilds per day.Root cause. The plugin manager is constructed in request scope instead of being reused at the worker/process level.
Requested fix. Make the
PluginManagera process/worker singleton, initialised once at worker startup — re-apply the borg-singleton pattern (shared-state conditional guard in the constructor) insidecpex.framework.manager, or cache the manager at the integration boundary. (Ref:mcp-context-forge#2010,#2017.)Implementation notes.
preloadenabled, prefer lazy per-worker init (post-fork) guarded by a lock, rather than import-time init, unless all plugins are fork-safe (some may hold sockets/threads).self. Any identity/auth-injecting plugin is the one to audit here.2. Upstream MCP session re-initialised per request (
MCP_SESSION_POOL_ENABLEDnot engaging)Symptom. Each proxied request opens a new upstream MCP session (initialize + tool-list handshake) rather than reusing a pooled one, adding session-setup latency per call and creating churn on the upstream server.
Root cause. The session/pool appears to be created in request scope (so the pool is always empty) or keyed per-request, so
MCP_SESSION_POOL_ENABLED=truenever actually reuses a session.Requested fix. Reuse the upstream MCP session per (worker × upstream server) so
MCP_SESSION_POOL_ENABLEDactually engages. (Ref: ADR-032,#4799.)Implementation notes.
(upstream_server_id, auth_identity)when sessions carry per-identity auth, or(upstream_server_id)when auth is attached per tool-call header. If auth is applied per session, the pool key must include identity to avoid cross-tenant session reuse.3. Observability adapter raises on every hook (
start_spankwarg mismatch)Symptom. Repeated on every observability hook (several times per request):
The observability adapter is called with a
db=keyword the adapter signature doesn't accept, so each hook throws and is logged as a WARNING.Requested fix. Align the signature — add
db=None(and**kwargsfor forward-compat) toObservabilityServiceAdapter.start_span(...), threadingdbthrough if it's needed for span/DB correlation or accepting-and-ignoring it otherwise. Low-risk, isolated; removes many exception constructions + WARNINGs per request.Downstream impact (why this is more than cosmetic)
The per-request rebuild churn (§1) can cause the gateway to reset/close upstream MCP streams mid-call under load. A client proxying a tool call through the gateway then sees its in-flight call cancelled partway through, which surfaces as request failures on that client. Beyond correctness, the rebuild and per-request session handshake add latency to every proxied tool call.
Reproduction
MCP_SESSION_POOL_ENABLED=trueand multiple workers.Plugin manager initialized with N pluginsper request with a distinctrequest_id; (b) a new upstream session handshake per request; (c) repeatedstart_span() got an unexpected keyword argument 'db'WARNINGs per request.Acceptance criteria
Plugin manager initializedcount drops from ~1/request to ~1/worker).start_span()no longer raises; the WARNING is gone.References
mcp-context-forge#2010,#2017#4799