Parent: #1930
What to build
ApplicationMcpProxyController's enhancement chain never included ResolveResourceDependenciesFn — an application invoked over /v1/deployments/{id}/mcp got no dependency grants however it declared and however consented it was (a documented v1 limitation). Adding the function to the chain as-is is not a one-line change:
- The chain is typed over
ObjectNode, not RequestObject, and this controller mints its per-request key after the chain runs (inside McpUpstreamAuthInjector, at header-injection time) — a naive wiring would NPE on a null proxyApiKeyData on every call.
ResolveResourceDependenciesFn is genericized (<T> extends BaseRequestFunction<T>) so the one implementation joins both RequestObject-typed chains (the three conversation mint sites) and the ObjectNode-typed MCP chain — the request body was never read by this function to begin with. No adapter class.
McpProxyController gains a preAssignsPerRequestKey() hook (gated on the app's own mcp.forwardPerRequestKey, default true): when set, the per-request key is created before the enhancement chain runs (so the chain can bake a grant into it) and persisted to Redis only after the chain succeeds — the same create/chain/assign order DeploymentPostController already uses. The body-handling dispatch moves from the Vert.x event loop onto the task executor to match, since the chain now does blocking consent/permission/Redis work.
McpUpstreamAuthInjector's key mint is made idempotent: handleProxyRequest re-enters on every 429 retry, connection failure, and same-origin 307/308 redirect (the common Starlette/FastMCP trailing-slash case), and the old unconditional mint would replace the grant-bearing key with a fresh, grant-free one on the very first such re-entry — and orphan the previous Redis-backed key, since only the current key is invalidated on completion. It now reuses an already-assigned key instead.
- A required dependency with nowhere to bake a grant into (
forwardPerRequestKey disabled) is treated exactly like an unconsented one — still hard-fails the call rather than silently succeeding for an app that can never receive the access it declared as required.
Known edge cases, recorded for the team (not fixed here): a required dependency combined with forwardPerRequestKey: false is a permanent, unvalidated 403 with no write-time signal — a write-time cross-field check belongs in ResourceDependencyValidator as separate follow-up. tools/call allow-list enforcement (isToolCallAllowed) is gated on a client-controlled Content-Type header, a pre-existing gap this story does not introduce but now places a second security-relevant function (this one) behind the same gate; the grant-baking side is fail-closed under that bypass.
Mint-timing restructure flagged for review: request-handling order and threading change in a shared controller (McpProxyController) — key mint moved before the chain, dispatch moved off the event loop, injector mint made idempotent. This story had the deepest review pass of the series for exactly that reason.
Acceptance criteria
Blocked by
#1938
Parent: #1930
What to build
ApplicationMcpProxyController's enhancement chain never includedResolveResourceDependenciesFn— an application invoked over/v1/deployments/{id}/mcpgot no dependency grants however it declared and however consented it was (a documented v1 limitation). Adding the function to the chain as-is is not a one-line change:ObjectNode, notRequestObject, and this controller mints its per-request key after the chain runs (insideMcpUpstreamAuthInjector, at header-injection time) — a naive wiring would NPE on a nullproxyApiKeyDataon every call.ResolveResourceDependenciesFnis genericized (<T> extends BaseRequestFunction<T>) so the one implementation joins bothRequestObject-typed chains (the three conversation mint sites) and theObjectNode-typed MCP chain — the request body was never read by this function to begin with. No adapter class.McpProxyControllergains apreAssignsPerRequestKey()hook (gated on the app's ownmcp.forwardPerRequestKey, default true): when set, the per-request key is created before the enhancement chain runs (so the chain can bake a grant into it) and persisted to Redis only after the chain succeeds — the same create/chain/assign orderDeploymentPostControlleralready uses. The body-handling dispatch moves from the Vert.x event loop onto the task executor to match, since the chain now does blocking consent/permission/Redis work.McpUpstreamAuthInjector's key mint is made idempotent:handleProxyRequestre-enters on every 429 retry, connection failure, and same-origin 307/308 redirect (the common Starlette/FastMCP trailing-slash case), and the old unconditional mint would replace the grant-bearing key with a fresh, grant-free one on the very first such re-entry — and orphan the previous Redis-backed key, since only the current key is invalidated on completion. It now reuses an already-assigned key instead.forwardPerRequestKeydisabled) is treated exactly like an unconsented one — still hard-fails the call rather than silently succeeding for an app that can never receive the access it declared as required.Known edge cases, recorded for the team (not fixed here): a required dependency combined with
forwardPerRequestKey: falseis a permanent, unvalidated 403 with no write-time signal — a write-time cross-field check belongs inResourceDependencyValidatoras separate follow-up.tools/callallow-list enforcement (isToolCallAllowed) is gated on a client-controlled Content-Type header, a pre-existing gap this story does not introduce but now places a second security-relevant function (this one) behind the same gate; the grant-baking side is fail-closed under that bypass.Mint-timing restructure flagged for review: request-handling order and threading change in a shared controller (
McpProxyController) — key mint moved before the chain, dispatch moved off the event loop, injector mint made idempotent. This story had the deepest review pass of the series for exactly that reason.Acceptance criteria
/v1/deployments/{id}/mcpreceives its dependency grants on the per-request key its upstream is handed; the in-handler target call succeeds and an out-of-folder call 403s.400 Invalid JSON request body.McpUpstreamAuthInjectormints at most one per-request key per request, and none at all when the controller pre-minted one.mcp.forwardPerRequestKey: falsereceives noApi-Keyheader and no grants; the call still succeeds.ResolveResourceDependenciesFnhas exactly one implementation; all four mint sites construct it with a diamond; the MCP enhancement chain runs on the task executor, not the event loop.current-useroccurrences are PR1/PR7's deliberate parse and regression fixtures.Blocked by
#1938