# Convex MCP Gateway

Auth-aware MCP server for Convex: expose Convex functions as MCP tools with per-tool scopes, OAuth 2.1 discovery, optional DCR bridge, and audit log.

This is an upstream package. It has not been adapted or qualified for Bijection.

- Package: convex-mcp-gateway
- Source: https://github.com/tfohlmeister/convex-mcp-gateway
- npm: https://www.npmjs.com/package/convex-mcp-gateway
- Directory: https://bijection.com/components/convex-mcp-gateway

Read the upstream README for setup, license, and compatibility before using it.

## Upstream package documentation

Recorded 2026-10-23. This describes convex-mcp-gateway; check the Bijection adaptation separately.

An auth-aware MCP (Model Context Protocol) server component that exposes Convex functions as MCP tools for AI agents. Provides OAuth 2.1 protected-resource discovery, per-tool authorization with scopes/roles, comprehensive audit logging, and wire-error sanitization. Built as a Convex Component with type-safe tool registration and full integration with Convex's existing auth system.

### Use cases

• **Expose internal APIs to AI agents** - Transform your existing Convex queries/mutations into MCP tools that Claude, GPT, or custom agents can discover and call with proper authentication

• **Build role-based agent workflows** - Create different tool catalogs for different user roles, where agents only see and can invoke functions their authenticated user has permission to access

• **Audit AI agent interactions** - Track every tool call with detailed logging including argument redaction, identity context, and error details for compliance and debugging

• **Bridge existing auth systems with MCP** - Use your current Clerk/Auth0/custom JWT setup without changes while providing OAuth 2.1 discovery for MCP clients

• **Prototype agent integrations locally** - Test full MCP workflows in development with the included convex-test helper for end-to-end testing

### How it works

You register Convex functions as MCP tools using `defineMcpQuery`, `defineMcpMutation`, or `defineMcpAction` with type-safe argument validation and optional metadata for authorization. The component handles the complete MCP 2025-06-18 Streamable HTTP protocol including session management, content negotiation, and identity-bound cleanup.

A single `authorize` callback controls both tool visibility (`tools/list`) and execution (`tools/call`) using your existing `ctx.auth.getUserIdentity()` flow. The gateway automatically filters the tool catalog based on what each authenticated user can actually invoke, ensuring agents only see relevant capabilities.

The component mounts as a standard Convex httpAction in your router and provides OAuth 2.1 protected-resource discovery with RFC 9728 metadata endpoints and proper `WWW-Authenticate` headers. Every tool invocation gets logged to an audit table with configurable argument redaction, while wire errors are sanitized to prevent information leakage.

### Upstream README

# convex-mcp-gateway

Auth-aware MCP server for [Convex](https://convex.dev). Expose selected
Convex functions as MCP tools, bring your own JWT issuer, declare
scopes/roles per tool, get an audit log and OAuth 2.1 protected-resource
discovery for free.

Built as a [Convex Component](https://www.convex.dev/components).

## Features

- **Type-safe tool registration** — `defineMcpQuery` / `defineMcpMutation` /
  `defineMcpAction` declare a Convex function as an MCP tool with end-to-
  end-typed `args` and (optional) `returns` validators
- **MCP 2025-06-18 Streamable HTTP** — sessions, `Accept` negotiation,
  `MCP-Protocol-Version` validation, identity-bound `DELETE`, single-
  frame SSE
- **One authorize callback** — gates `tools/call` and filters `tools/list`
  with `mode: "list" | "call"`; uses your existing `ctx.auth.getUserIdentity()`
- **OAuth 2.1 protected-resource discovery** — RFC 9728 metadata,
  RFC 6750 `WWW-Authenticate` headers, multi-tenant ready
- **Optional OAuth bridge** — RFC 8414 AS metadata wrap + RFC 7591 DCR
  for browser MCP clients (claude.ai) against IdPs without DCR support
- **Audit log** — one row per call with per-tool argument redaction
  (verbatim / dropped / dotted-path redacted)
- **Wire-error sanitization** — generic message on the wire, full detail
  in audit; `ConvexError` passes through for deliberate user-facing
  messages
- **`convex-test` helper** — `convex-mcp-gateway/test` exports a one-line
  `register(t)` that hooks the component into a `convexTest` instance so
  your host tests can exercise the full `/mcp/` round-trip in-process.
  See [Testing](./docs/testing.md)

## What it does

![Architecture overview](./docs/diagrams/architecture.svg)

You mount the gateway in a single `httpAction` and pass an `authorize`
JS callback that decides per call whether the request goes through.
The gateway handles the JSON-RPC envelope, the Streamable-HTTP session
lifecycle, the OAuth discovery doc, the `WWW-Authenticate` headers, and
the audit log. Your existing Convex auth (Clerk, Auth0, Pocket-ID,
custom JWT issuer) just works.

A standalone editorial-styled version of the diagram is at
[`docs/diagrams/architecture.html`](./docs/diagrams/architecture.html);
in-depth sequence and data-flow diagrams live in
[`docs/architecture.md`](./docs/architecture.md).

## Try it

The companion repo
[**convex-mcp-gateway-demo**](https://github.com/tfohlmeister/convex-mcp-gateway-demo)
is a notes app that registers five tools (public, identity-gated,
role-gated) and shows the audit log live. Three run modes including
a local-backend setup that needs no Convex account:

```sh
git clone https://github.com/tfohlmeister/convex-mcp-gateway-demo
cd convex-mcp-gateway-demo
pnpm install && pnpm local:start    # in one terminal
pnpm convex:dev && pnpm convex:run mcp:registerDefaults && pnpm dev
```

## Quickstart

```sh
pnpm add convex-mcp-gateway
```

```ts
// convex/convex.config.ts
import { defineApp } from "convex/server";
import mcpGateway from "convex-mcp-gateway/convex.config";

const app = defineApp();
app.use(mcpGateway);
export default app;
```

```ts
// convex/mcp.ts — register tools
import { v } from "convex/values";
import { McpGateway, defineMcpQuery } from "convex-mcp-gateway";
import { api, components } from "./_generated/api.js";
import { internalMutation } from "./_generated/server.js";

const gateway = new McpGateway(components.mcpGateway);

export const registerDefaults = internalMutation({
  args: {},
  handler: async (ctx) => {
    await gateway.register(
      ctx,
      [
        defineMcpQuery({
          name: "invoices_summary",
          description: "Public invoice counter.",
          fn: api.invoices.summary,
          args: {},
          metadata: { public: true },
        }),
        defineMcpQuery({
          name: "invoices_list",
          description: "List invoices for the authenticated user.",
          fn: api.invoices.list,
          args: { status: v.optional(v.string()) },
        }),
      ],
    );
  },
});
```

```ts
// convex/http.ts — mount the gateway with your authorize callback
import { httpRouter } from "convex/server";
import {
  McpGateway,
  type McpAuthorizerHandler,
} from "convex-mcp-gateway";
import { components } from "./_generated/api.js";
import { httpAction } from "./_generated/server.js";

const gateway = new McpGateway(components.mcpGateway);

const authorize: McpAuthorizerHandler = async (ctx, { toolMetadata }) => {
  const meta = (toolMetadata ?? {}) as { public?: boolean };
  if (meta.public) return { allowed: true };
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) return { allowed: false, reason: "Unauthorized" };
  return { allowed: true };
};

const http = httpRouter();
const mcp = httpAction(async (ctx, req) =>
  gateway.handleMcpRequest(ctx, req, { authorize }),
);
// Mount both /mcp/ and /mcp — some clients (claude.ai) strip the
// trailing slash from the configured URL before POSTing.
for (const path of ["/mcp/", "/mcp"]) {
  http.route({ path, method: "POST", handler: mcp });
  http.route({ path, method: "GET", handler: mcp });
  http.route({ path, method: "DELETE", handler: mcp });
}
export default http;
```

```sh
npx convex dev --once
npx convex run mcp:registerDefaults

# Talk to it (Streamable HTTP — initialize first, then send commands).
SESSION=$(curl -sSD - -X POST "$CONVEX_SITE_URL/mcp/" \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}' \
  | awk '/^[Mm]cp-[Ss]ession-[Ii]d:/ {print $2}' | tr -d '\r')

curl -X POST "$CONVEX_SITE_URL/mcp/" \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H "mcp-session-id: $SESSION" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
```

The anonymous client sees only `invoices_summary` (the `public: true`
tool); calling `invoices_list` without a Bearer returns HTTP 401 with a
`WWW-Authenticate` header pointing at your discovery endpoint. Any
spec-compliant MCP client (the official MCP Inspector, IDE plugins,
agent runtimes) handles the session and OAuth handshakes
automatically. See [Getting Started](./docs/getting-started.md) for
the full walkthrough.

## Documentation

- **[Getting Started](./docs/getting-started.md)** — install, register,
  call, in five minutes
- **[Architecture](./docs/architecture.md)** — component model, data
  flow, sequence diagrams
- **[Authorization](./docs/authorization.md)** — authorizer contract,
  `mode: "list"` vs `"call"`, scope/role recipes
- **[OAuth 2.1 setup](./docs/oauth.md)** — RFC 9728 discovery, host-side
  mount, multi-tenant
- **[OAuth bridge mode](./docs/oauth-bridge.md)** — opt-in DCR + AS
  metadata wrap + userinfo token validation, for browser MCP clients
  (claude.ai) against IdPs that don't support Dynamic Client
  Registration (Pocket-ID, etc.)
- **[Audit log](./docs/audit-log.md)** — reading, filtering, redacting,
  pruning
- **[Testing](./docs/testing.md)** — convex-test patterns, identity
  injection, swappable authorizers

## Design choices, briefly

- **One authorizer query, deny-by-default.** The component has no
  notion of scopes, roles, JWT claims, or tenants. It calls one
  `internalQuery` per dispatch and lets it decide. Until that query is
  registered, nothing works.
- **Scope-aware `tools/list`.** The catalog visible to a caller equals
  the set of tools they could actually invoke; the same authorizer
  filters both with a `mode: "list"` discriminator.
- **Audit never alters dispatch.** Every audit write is best-effort;
  failures log and swallow. A successful tool mutation always returns
  `ok: true`.
- **Identity propagation is free.** Convex validates inbound Bearer
  tokens against your `auth.config.ts` before any function runs; the
  same identity flows into the authorizer and the tool handler.
- **OAuth discovery is opt-in.** Configure an authorization server, and
  401s carry `WWW-Authenticate` headers per RFC 6750 and the
  RFC 9728 path-prefix metadata URL.

## Local development

```sh
pnpm install
pnpm check                              # codegen + typecheck + tests + lint

# Iterate against a local Convex backend:
pnpm local:start                        # downloads the pinned binary, writes .env.local
                                        # runs on :3310 / :3311 (no Docker)
```

The local backend uses upstream test fixture credentials checked into
`get-convex/convex-backend` — public, deterministic, safe to commit.
See [docs/testing.md](./docs/testing.md) for `convex-test` patterns and
[CONTRIBUTING.md](./CONTRIBUTING.md) for the contribution workflow.

## Roadmap

**Auth model: bring your own IdP.** Any OAuth 2.1 / OIDC issuer that
Convex's `auth.config.ts` can validate against (Pocket-ID, Auth0,
Clerk, Keycloak, AWS Cognito, Authentik, custom JWT issuer) plugs in
without code changes. We deliberately do **not** ship a bundled
authorization server: building DCR + PKCE + token issuance + key
rotation duplicates what every IdP already does, and an MCP gateway
that's also an IdP would be two security-critical surfaces in one
component. The gateway only implements the resource-server side of
MCP's OAuth profile (RFC 9728 discovery, RFC 6750 `WWW-Authenticate`).
A separate bridge mode (RFC 8414 metadata + RFC 7591 DCR wrap) lets
hosts whose upstream IdP doesn't speak DCR still serve browser MCP
clients — see [docs/oauth-bridge.md](./docs/oauth-bridge.md).

### Future

- Capability tokens for agent-spawning workflows (small JWT helper,
  not a full AS: `gateway.signCapabilityToken({ tools, runId, ttl })`
  plus authorizer-side validation)
- `mcpResource` and `mcpPrompt` MCP primitives
- Multi-tenant pre-baked patterns (per-tenant URL, RFC 8707 audience
  binding)
- Audit-log UI
- Reverse direction (consume external MCP servers as Convex tools)
- Smithery.ai listing
- RFC 8693 token exchange for cross-domain identity

See the repo's GitHub issues for finer-grained tracking.

## Security

If you discover a vulnerability, please follow [SECURITY.md](./SECURITY.md)
(do not file a public issue).

## License

[Apache-2.0](./LICENSE)