Skip to content

Wire dependency resolution into the MCP-application-proxy mint site #1939

Description

@serguei-gorokhov

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

  • A declaring, admin-consented application invoked over /v1/deployments/{id}/mcp receives 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.
  • A required unresolvable dependency fails the MCP call with 403 and the resolver's message — never with 400 Invalid JSON request body.
  • A same-origin 307/308 upstream redirect (and a 429 retry, and a connection-failure retry) sends the same per-request key both times, carrying the same grants, and emits one grant/denial audit group for the request.
  • McpUpstreamAuthInjector mints at most one per-request key per request, and none at all when the controller pre-minted one.
  • An app with mcp.forwardPerRequestKey: false receives no Api-Key header and no grants; the call still succeeds.
  • A chained hop into a declaring app over MCP resolves that app's own declaration against the originating human, and the calling app's own grants do not satisfy it.
  • ResolveResourceDependenciesFn has 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.
  • Every dependency fixture added or edited by this story uses the braced grammar; the branch's only bare current-user occurrences are PR1/PR7's deliberate parse and regression fixtures.

Blocked by

#1938

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    • Status
      No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions