Command Palette

Search for a command to run...

UnylyUnyly
Весь каталог

Convex Mcp Gateway

БесплатноНе проверен

Auth-aware Convex component that exposes selected Convex functions as MCP tools.

GitHubEmbed

Описание

Auth-aware Convex component that exposes selected Convex functions as MCP tools.

README

Auth-aware MCP server for Convex. 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.

Convex Component tests npm license

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. Declare the list once and pass it as tools to handleMcpRequest, the registry auto-syncs on connect (no separate registration mutation), or call gateway.register(...) for imperative/dynamic catalogs
  • MCP 2025-06-18 Streamable HTTP: sessions, Accept negotiation, MCP-Protocol-Version validation, identity-bound DELETE, single- frame SSE
  • MCP resources: defineMcpResource / defineMcpResourceTemplate serve resources/list, resources/read, and resources/templates/list (RFC 6570). Central authorizeResource hook, opt-in resource audit, runtime shape validation, and opt-in resources/subscribe capability. See Resources & templates
  • 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
  • requireAuth for all-private servers: opt-in 401-challenge on anonymous requests so browser clients (claude.ai) begin the OAuth flow instead of seeing an empty tools/list and never prompting a login
  • initializeInstructions: optional server-level guidance returned in the MCP initialize result's instructions field, surfaced to the LLM without bloating individual tool descriptions
  • 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

What it does

Architecture overview

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; in-depth sequence and data-flow diagrams live in docs/architecture.md.

Try it

The companion repo 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:

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

pnpm add convex-mcp-gateway
// 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;
// convex/mcp.ts, declare your tools once
import { v } from "convex/values";
import { defineMcpQuery } from "convex-mcp-gateway";
import { api } from "./_generated/api.js";

export const tools = [
  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()) },
  }),
];
// 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";
import { tools } from "./mcp.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();
// Pass `tools` and the registry syncs on each connect, no separate
// registration mutation to run.
const mcp = httpAction(async (ctx, req) =>
  gateway.handleMcpRequest(ctx, req, { authorize, tools }),
);
// 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;
npx convex dev --once
# No registration step: passing `tools` to handleMcpRequest syncs the
# registry on the initialize below.

# 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 for the full walkthrough.

If your server has no public tools and you connect it to a browser client like claude.ai, add requireAuth: true to the handleMcpRequest options. Without it, anonymous initialize / tools/list return 200 (empty) and the client never starts OAuth, it only reacts to a 401. See OAuth: all-private servers.

To give the model server-level guidance — how to use the server as a whole, not any single tool — pass initializeInstructions to handleMcpRequest. It populates the initialize result's instructions field and is omitted when unset, so the default response shape is unchanged. It's a best-effort hint (per the spec clients MAY use it; some ignore it), so keep it short and don't rely on it for hard constraints.

Resources

Alongside tools, the gateway serves MCP resources (read-only content). Declare a concrete resource with defineMcpResource and pass it to the same handleMcpRequest call as your tools:

// convex/mcp.ts
import { defineMcpResource } from "convex-mcp-gateway";
import { api } from "./_generated/api.js";

export const resources = [
  defineMcpResource({
    uri: "invoices://summary",
    name: "invoice-summary",
    title: "Invoice summary",
    mimeType: "application/json",
    // Read handlers receive the resolved caller identity; anonymous
    // resource requests are rejected before this runs.
    read: async (ctx, { uri, identity }) => {
      const summary = await ctx.runQuery(api.invoices.summary, {});
      return [
        {
          uri,
          mimeType: "application/json",
          text: JSON.stringify({ ...summary, caller: identity.subject }),
        },
      ];
    },
  }),
];
// convex/http.ts — add `resources` to the same mount as `tools`
const mcp = httpAction(async (ctx, req) =>
  gateway.handleMcpRequest(ctx, req, { authorize, tools, resources }),
);

Supported methods: resources/list, resources/read, resources/templates/list (RFC 6570 templates via defineMcpResourceTemplate), and opt-in resources/subscribe / resources/unsubscribe. A central authorizeResource hook gates list/read, resource operations are auditable, and reads run only for authenticated callers. A complete, runnable example (concrete resource + template + per-resource auth + audit + subscription) is wired into example/convex; see Resources & templates for the full guide.

Documentation

  • Getting Started: install, register, call, in five minutes
  • Architecture: component model, data flow, sequence diagrams
  • Authorization: authorizer contract, mode: "list" vs "call", scope/role recipes
  • Resources & templates: concrete resources vs RFC 6570 templates, resources/templates/list, read resolution
  • OAuth 2.1 setup: RFC 9728 discovery, host-side mount, multi-tenant, requireAuth for all-private servers
  • OAuth bridge mode: 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: reading, filtering, redacting, pruning
  • Recipe: Better Auth on Convex: opaque-token resolveIdentity, split-domain discovery, and browser login continuation for @convex-dev/better-auth as the IdP
  • Testing: convex-test patterns, identity injection, swappable authorizers

Design choices, briefly

  • One authorize callback, deny-by-default. The component has no notion of scopes, roles, JWT claims, or tenants. You pass a single JS callback to handleMcpRequest that decides per call (it runs host-side where ctx.auth works). Until you pass it, nothing reaches your tools.
  • 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

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 for convex-test patterns and CONTRIBUTING.md for the contribution workflow.

Roadmap

By design the gateway is the resource-server half of MCP's OAuth profile only; it will not ship a bundled authorization server (that duplicates what every IdP already does and would put two security-critical surfaces in one component). Bring your own OAuth 2.1 / OIDC issuer. On the radar beyond that:

  • Capability tokens for agent-spawning workflows (small JWT helper, not a full AS: gateway.signCapabilityToken({ tools, runId, ttl }) plus authorizer-side validation)
  • mcpPrompt MCP primitive (the mcpResource primitive has shipped, see Resources & templates)
  • Pre-baked multi-tenant patterns (per-tenant URL, RFC 8707 audience binding)
  • 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 (do not file a public issue).

License

Apache-2.0

from github.com/tfohlmeister/convex-mcp-gateway

Установить Convex Mcp Gateway в Claude Desktop, Claude Code, Cursor

Рекомендуется · одна команда, все IDE
unyly install convex-mcp-gateway

Ставит в Claude Desktop, Claude Code, Cursor и VS Code — сам разбирается с npx, uvx и сборкой из исходников.

Впервые? Поставь CLI: curl -fsSL https://unyly.org/install | sh

Или настроить вручную

Выполни в терминале:

claude mcp add convex-mcp-gateway -- npx -y convex-mcp-gateway

FAQ

Convex Mcp Gateway MCP бесплатный?

Да, Convex Mcp Gateway MCP бесплатный — установка в пару кликов через Unyly без оплаты.

Нужен ли API-ключ для Convex Mcp Gateway?

Нет, Convex Mcp Gateway работает без API-ключей и переменных окружения.

Convex Mcp Gateway — hosted или self-hosted?

Self-hosted: сервер запускается локально на твоей машине командой из раздела установки.

Как установить Convex Mcp Gateway в Claude Desktop, Claude Code или Cursor?

Открой Convex Mcp Gateway на unyly.org, выбери вкладку своего клиента (Claude Desktop, Claude Code, Cursor) и нажми Install — конфиг сгенерируется автоматически, без правки JSON.

Похожие MCP

Compare Convex Mcp Gateway with

Не уверен что выбрать?

Найди свой стек за 60 секунд

Автор?

Embed-бейдж для README

Похожее

Все в категории ai