Skip to content

McpServer: middleware and visibility can't use per-request services from HttpRouter middleware under layerHttp #9854

Open

Description

@dbrxnds

On v4/next-minor (pkg.pr.new 7ce7d03), tools can depend on per-request services provided by HttpRouter middleware: layerHttp converts the remaining McpRequest.From<"Requires", R> into HttpRouter.Request.From<"Requires", R>. MCP middleware and visibility checks can't. MiddlewareLayer turns any requirement outside TransportServices into MissingRequestDependencies, so it can only be satisfied by .combine or the transport.

class CurrentUser extends Context.Service<CurrentUser, { readonly id: number }>()("CurrentUser") {}

// HTTP auth stays at the HTTP layer: OAuth discovery and refresh need a 401 with WWW-Authenticate,
// and MCP middleware never runs for initialize/ping
const Auth = HttpRouter.middleware<{ provides: CurrentUser }>()(/* bearer token -> user */)

const Policy = McpServer.visibility(
  Effect.succeed(({ target }) =>
    Effect.gen(function* () {
      const user = yield* CurrentUser
      return canUse(user, target)
    })
  )
)

Policy.layer
// ^? MissingRequestDependencies<CurrentUser>, although Auth provides CurrentUser to every request under layerHttp

The workaround is an MCP middleware that re-provides what is already on the fiber:

const Bridge = McpServer.middleware<{ provides: CurrentUser }>()((effect) =>
  Effect.gen(function* () {
    const user = yield* Effect.serviceOption(CurrentUser)
    if (Option.isNone(user)) return yield* Effect.die(new Error("missing HTTP auth"))
    return yield* effect.pipe(CurrentUser.provide(user.value))
  })
)

Policy.combine(Bridge).layer

Doing the authentication itself in a combined MCP middleware would mean resolving the token and user a second time on every request.

Proposal

Drop the conditional and always put the per-request requirements in the layer input, as registrations already do. layerHttp then forwards them to HttpRouter, while layer / layerStdio still surface them as unmet requirements:

type MiddlewareLayer<Config extends MiddlewareConfig, R> = Layer.Layer<
  McpRequest.From<"Requires", Config["provides"]>,
  Config["layerError"],
  R | Config["layerRequires"] | McpRequest.From<"Requires", Config["requires"]>
>

The runtime already handles this. With only the .d.ts patched and the bridge removed, through layerHttp + HttpRouter.toWebHandler:

  • tools/list hides the tool the policy rejects
  • the visible tool receives CurrentUser
  • calling the hidden tool returns -32602 Tool '…' not found

Trade-off: an unmet requirement then shows up as a missing layer requirement at the server instead of the named MissingRequestDependencies at .layer. Happy to open a PR if this direction is fine.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions