Skip to content

Find a component

Loading the directory…

↑ ↓ to select · Enter to open · Esc to closeBrowse all →
Contributor login

MCP Server

Application MCP server implementation

Bijection source
git clone https://github.com/bijectionhq/mcp.git
cd mcp
git checkout e463894c4c6b158fbf1a9953c711f7278d2075f9

Bijection repository access is required. Request access.

Description

Application MCP server implementation

Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.

Included in Bijection’s source workspace

This package has a pinned source revision in Bijection’s workspace. Source availability does not establish npm publication or runtime qualification.

Getting started

  1. Get the source at the revision above and read its package documentation.
  2. Check the required Bijection SDK version, dependencies, and external service credentials.
  3. Follow the package setup instructions and the guide to using components.
  4. Exercise the component in your deployment according to your project’s verification rules.

Open source

Maintained source: bijectionhq/mcp. Based on convex/mcp. License: Apache-2.0. Consult the repository for license terms, notices, and contributor attribution.

From the upstream publisher

Turn your Convex app into an MCP server, so your users can use your app from their AI agents.

Explore the demo ↗

Upstream overview

From the upstream package documentation.

Convex MCP turns a Convex app into a remote MCP server with OAuth 2.1 authorization built in. Developers define tools using Convex validators, register HTTP routes, and add a consent page; users then paste one URL into Claude, ChatGPT, Cursor, or Claude Code to connect their AI agent to their own data. The component handles the full OAuth 2.1 flow including PKCE, rotating refresh tokens, dynamic client registration, and a device flow for agents that start from a pasted URL, all running inside the Convex deployment with no separate auth provider.

Use cases

From the upstream package documentation.

  • You want to let users connect their Claude or Cursor agent to your app so the agent can read and write their data through tools you control
  • You are building a SaaS product and need users to authorize third-party AI agents against their accounts using a standards-compliant OAuth 2.1 flow without standing up a separate auth server
  • You want to expose scoped read-only or read-write tool sets so users can grant minimal permissions to an agent from a settings page
  • You need CLI or headless agent support and want to issue API key bearer tokens that agents can use with standard MCP clients like claude mcp add
  • You want an audit-ready MCP integration where tokens are stored hashed, expired credentials are cleaned up by a cron, and unauthenticated endpoints are rate limited

How it works

From the upstream package documentation.

Define an McpServer instance in convex/mcp.ts, passing tool definitions built with the tool helper. Each tool declares Convex validator schemas for args and returns, which are converted to JSON Schema for MCP clients. Tool handlers receive a verified user object containing userId, scopes, clientId, and connectionId, and should delegate to internal Convex queries or mutations that enforce ownership the same way the app UI does.

Register routes by calling mcp.registerRoutes(http) in convex/http.ts. This mounts the MCP endpoint at /mcp, plus OAuth 2.1 discovery documents at /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource. The component speaks MCP protocol versions 2026-07-28, 2025-11-25, 2025-06-18, and 2025-03-26 over Streamable HTTP on the same endpoint.

Export getAuthRequest and authorize from the mcp.api({ getUserId }) call and wire them into a consent page at consentUrl. When an agent connects, the user lands on that page, sees which scopes are requested, and approves or denies. For the device flow, where an agent fetches GET /mcp and receives instructions, the consent page must also display and validate a userCode before calling authorize. Users can later list connected agents and revoke them via listConnections and revokeConnection, and create bearer-token API keys via createApiKey.

Upstream package FAQ

What MCP protocol versions does @convex-dev/mcp support?

The Convex MCP component supports MCP versions 2026-07-28, 2025-11-25, 2025-06-18, and 2025-03-26 on the same endpoint over Streamable HTTP. The latest version is stateless and includes server/discover support without requiring an initialize handshake.

Do I need a separate auth provider to use Convex MCP?

No. The @convex-dev/mcp component includes a full OAuth 2.1 authorization server that runs inside your Convex deployment. It supports discovery per RFC 9728 and RFC 8414, dynamic client registration, PKCE, and rotating refresh tokens with replay detection. You provide a consent page in your frontend using your existing auth system such as Convex Auth, Clerk, or WorkOS.

How does Convex MCP handle tool argument validation?

Tool args and returns are defined using Convex validators from the convex/values package. The component converts these automatically to JSON Schema for the MCP inputSchema and outputSchema fields. If an agent passes arguments that do not match the schema, the error is returned to the model as a tool error it can self-correct, rather than throwing an unhandled exception.

Can users connect agents without an MCP client installed?

Yes. An agent that can only make HTTP requests can fetch GET /mcp from the Convex MCP server and receives step-by-step instructions for signing in via the OAuth device flow (RFC 8628). The user gets a link to the consent page, approves the connection, and the agent receives a short-lived access token with no refresh token to avoid credentials appearing in chat transcripts.

How do I restrict what an agent can do with Convex MCP scopes?

Define named scopes on your McpServer instance, then assign each tool a scope property matching one of those keys. A connection only sees and can invoke tools within the scopes the user granted at approval time. Calling a tool outside granted scopes returns HTTP 403 with an insufficient_scope error. Users can also connect read-only agents by granting only read scopes.

Use with agents and CLI

Give your coding agent the component’s source identity and setup guidance.

agent-prompt.md
Help me integrate MCP Server with Bijection.

Read: https://bijection.com/components/mcp/SKILL.md

Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.

Upstream README.md

View repository ↗

Documentation from convex/mcp, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.

Let your users use your app from their AI agents. This component turns a Convex app into a remote MCP server with sign-in built in. Your users paste one URL into Claude, ChatGPT, Cursor or Claude Code, approve the agent on a page in your app, and the agent can then work with their data through tools you define.

export const mcp = new McpServer(components.mcp, {
  name: "todos",
  version: "1.0.0",
  consentUrl: `${process.env.SITE_URL}/connect`,
  tools: {
    add_todo: tool({
      description: "Add a todo for the user.",
      args: { text: v.string() },
      handler: (ctx, args, user) =>
        ctx.runMutation(internal.todos.add, { userId: user.userId, ...args }),
    }),
  },
});
  • Tools from Convex validators. args and returns become JSON Schema (inputSchema / outputSchema). Bad arguments go back to the model as tool errors it can fix.
  • A full OAuth 2.1 authorization server. It supports discovery (RFC 9728 / RFC 8414), dynamic client registration, PKCE, rotating refresh tokens with replay detection, and audience-bound tokens. It works with claude.ai connectors and any spec-compliant client. Client ID metadata documents are available as an opt-in for hosts you allowlist.
  • Paste the URL into any agent chat. An agent that can only make HTTP requests fetches /mcp and gets instructions. It then shows the user a clickable approval link, gets a short-lived token, and starts calling tools. No MCP client setup is needed. This uses the OAuth device flow, RFC 8628.
  • Your auth, your consent page. Users sign in with whatever your app already uses (Convex Auth, Clerk, WorkOS, …), then approve the agent.
  • Connections UI. Users can list connected agents, disconnect them, and create API keys for CLI agents.
  • Scopes. You can let users connect a read-only agent.
  • Secrets are stored hashed. Tokens are opaque and stored as SHA-256 only. Expired credentials and unused client registrations are cleaned up by a cron, and the unauthenticated endpoints are rate limited.

Speaks MCP 2026-07-28 (stateless, with server/discover, no initialize) and the earlier 2025-11-25, 2025-06-18 and 2025-03-26 versions on the same endpoint, over Streamable HTTP. Everything runs inside your Convex deployment: no separate server or auth provider.

Found a bug? Feature request? File it here.

Quick start: paste this into your coding agent

Open your Convex app in Claude Code, Codex, Cursor or any coding agent and paste:

Add an MCP server to this Convex app so our users can use it from their AI
agents (Claude, ChatGPT, Cursor, Claude Code).

1. Add `@convex-dev/mcp@alpha` with this project's package manager (npm,
   pnpm, yarn or bun — match the lockfile).
2. Read node_modules/@convex-dev/mcp/skills/convex-mcp/SKILL.md and its
   references, and follow it: study the app, then propose the MCP tools
   (names, descriptions, args, scopes, annotations) as a table and wait for
   my OK before writing code.
3. Wire it up as the skill describes: convex.config.ts, convex/mcp.ts with
   the tools on internal functions that take the verified userId, http.ts,
   a /connect consent page using our existing sign-in, and a "Connected
   agents" section in settings with API keys.
4. Verify against the dev deployment: create an API key, run tools/list,
   and call every tool once.
5. Then read node_modules/@convex-dev/mcp/skills/convex-mcp-security/SKILL.md
   and run that audit with two test users. Fix what it finds and show me the
   report.

Don't deploy to production or touch real user data.

The agent will stop after step 2 to show you the proposed tools.

Already have an MCP server built with this component? Paste this to audit it:

Read node_modules/@convex-dev/mcp/skills/convex-mcp-security/SKILL.md and
audit this app's MCP server as it describes, against our dev deployment with
two test users (mint keys with createApiKeyForUser if we export it). Prove
each finding, fix it, re-prove it, and show me the report. Don't touch
production or real user data.

Installation

npm install @convex-dev/mcp@alpha   # or pnpm add / yarn add / bun add
// convex/convex.config.ts
import { defineApp } from "convex/server";
import mcp from "@convex-dev/mcp/convex.config.js";

const app = defineApp();
app.use(mcp);
export default app;

The MCP and OAuth discovery routes are served from your app's HTTP router at the origin root (/.well-known/...). Don't give the app itself an httpPrefix.

Usage

1. Define the server and tools

Tool handlers run in an HTTP action. There's no ctx.auth identity there: the verified user comes in as user.userId. Pass it to internal functions, and check ownership there just like your UI code does.

// convex/mcp.ts
import { McpServer, tool } from "@convex-dev/mcp";
// Convex Auth 0.0.x; with Convex Auth v2 import from "@convex-dev/auth/core".
import { getAuthUserId } from "@convex-dev/auth/server";
import { v } from "convex/values";
import { components, internal } from "./_generated/api";
import type { Id } from "./_generated/dataModel";

export const mcp = new McpServer(components.mcp, {
  name: "todos",
  title: "Todos",
  version: "1.0.0",
  instructions: "Manage the user's todo list.",
  // Your frontend's consent page. Set it explicitly.
  consentUrl: process.env.MCP_CONSENT_URL!,
  scopes: {
    "todos:read": "See your todos",
    "todos:write": "Add and complete your todos",
  },
  tools: {
    list_todos: tool({
      description: "List the user's todos, optionally filtered by completion.",
      args: { done: v.optional(v.boolean()) },
      returns: v.object({
        todos: v.array(v.object({ id: v.string(), text: v.string(), done: v.boolean() })),
      }),
      annotations: { readOnlyHint: true },
      scope: "todos:read",
      handler: async (ctx, args, user) => ({
        todos: await ctx.runQuery(internal.todos.listForUser, {
          userId: user.userId as Id<"users">,
          ...args,
        }),
      }),
    }),
  },
});

// Functions your frontend uses for the consent page and settings.
export const {
  getAuthRequest,
  authorize,
  listConnections,
  revokeConnection,
  createApiKey,
} = mcp.api({ getUserId: getAuthUserId });

For the CLI and tests, also export the internal helpers. They can only be called with deploy credentials:

export const { createApiKeyForUser } = mcp.internalApi();
// npx convex run mcp:createApiKeyForUser '{"userId":"<users id>"}'

To type user.userId as your own ID type, use createTool<Id<"users">>() in place of tool. Then annotate the server's type, which breaks a TypeScript inference cycle when the same file exports functions:

const tool = createTool<Id<"users">>();
export const mcp: McpServer<Id<"users">> = new McpServer(components.mcp, { ... });

getUserId decides who a connection acts for. Return a stable ID that can't collide across identity providers, such as your users table ID or identity.tokenIdentifier.

2. Register the routes

// convex/http.ts
import { httpRouter } from "convex/server";
import { mcp } from "./mcp";

const http = httpRouter();
mcp.registerRoutes(http);
export default http;

Your MCP server URL is https://<deployment>.convex.site/mcp. Two URLs are easy to confuse:

  • consentUrl is the page on your frontend where users approve agents.
  • siteUrl is the public origin of your Convex HTTP actions, which is the server URL users paste. It defaults to CONVEX_SITE_URL. Pass it if you use a custom domain for HTTP actions.

Every authorization creates its own connection, so a user can connect the same agent on several machines.

3. Add a consent page

When an agent connects, the user is sent to consentUrl?request=<id>. On that page, make sure the user is signed in (coming back to the same URL afterwards), show who's asking, and let them decide. Don't let it be framed: send Content-Security-Policy: frame-ancestors 'none' where your host can. On static hosts that can't set headers, refuse to render the approval UI when window.top !== window.self. The example does both.

const requestId = new URLSearchParams(location.search).get("request")!;
const request = useQuery(api.mcp.getAuthRequest, { requestId });
const authorize = useAction(api.mcp.authorize);
// request: { clientName, redirectUri, scopes: [{ name, description }], status }

const decide = async (approve: boolean) => {
  const { redirectUrl } = await authorize({ requestId, approve });
  window.location.assign(redirectUrl); // back to the agent
};

Agents that started from a pasted URL (device flow)

An agent with only an HTTP tool can fetch GET /mcp, which returns step-by-step instructions, and sign in with the device flow, using the public client ID mcp-agent. The user gets a link to your consent page, consentUrl?request=…. For these requests, getAuthRequest returns kind: "device" and a userCode. Your consent page should:

  • show the userCode and require the user to confirm it matches what their agent shows (a checkbox) before enabling Allow, then pass it to authorize({ requestId, approve: true, userCode }). The server refuses device approvals without the matching code, so a consent page that skips this step fails safely instead of becoming a phishing hole;
  • label clientName as unverified, since the agent reports it about itself;
  • after authorize, which returns redirectUrl: null for device requests, tell the user to go back to their agent;
  • if opened without ?request=, offer a box to type the code, which calls findAuthRequest({ userCode }). That lookup is rate-limited per user.

Device-flow tokens are short-lived access tokens with no refresh token, since they may end up in chat transcripts. The tool list isn't shown on the public guide unless you set describeTools: true. Set deviceFlow: false to turn the feature off. The example consent page (example/src/App.tsx) implements all of this.

4. Show connected agents

Settings can use listConnections, revokeConnection({ id }) and createApiKey({ name }). The API key is returned once. CLI agents use it as a bearer token:

claude mcp add --transport http todos https://<deployment>.convex.site/mcp \
  --header "Authorization: Bearer <key>"

See example/ for a complete app with a consent page and a connections UI.

Tool results

  • Return values. A string becomes text content. Any other value becomes JSON text content, plus structuredContent when returns is set and the value is an object. bigint values are encoded as strings and bytes as base64.
  • returns is enforced. A result that doesn't match it, including one with extra fields, is logged and becomes a generic tool error. Nothing you didn't declare reaches the agent.
  • Full control. Return callToolResult({ content: [...], isError }) to shape the result yourself, for example to return images.
  • Errors. Throw ConvexError("…") to send the model a message it can act on. Other errors are logged, and the model gets a generic failure.

Scopes

Give a tool scope: "x" (it must be a key of scopes). A connection only sees and can call tools in its granted scopes. A call outside those scopes gets HTTP 403 insufficient_scope. When a client requests no scope, the connection gets all of them.

Authorization

The component authenticates the connection and gives your tool the verified user (userId, plus the connection's scopes, clientId and connectionId). Deciding what that user may do is your app's job. Do it the same way your UI does, inside the transaction that reads or writes:

  1. Use one access helper for the UI and for MCP. Put the check in a function like requireList(ctx, actor, listId, "view" | "edit" | "manage"). Call it from your UI mutations and from the internal functions your tools call, so the two paths can't drift apart. Return the same error for "not found" and "not allowed".
  2. Pass the whole MCP user, not just the ID. Internal functions take { user: vMcpUser(v.id("users")) }. Your helper can then let a connection's scopes narrow what the user may do. For example, a todos:read connection acts as a viewer even on the user's own list.
  3. Plug in whatever already decides access in your app:
    • Your own tables, such as owners, members and roles. The example (example/convex/access.ts) shares lists with viewer and editor roles.
    • A sharing or authorization component. Call its transactional check, such as requireAccess(ctx, { resourceRef, actor }, "edit"), from your helper.
    • An external engine (OpenFGA, Cerbos, WorkOS FGA). These can't be called from a query or mutation, which can't make network requests. Copy the relationships you need into indexed Convex tables, check those inside the transaction, and keep them in sync from actions or webhooks. Calling the engine from an action and then writing in a separate mutation leaves a gap between the check and the write. Only do that if a short delay in revocation is acceptable.
  4. Lists and searches should query what the user can reach (owned plus shared, through an index), not fetch everything and then filter.

Security checklist

Before you ship, make sure that:

  • Tools act only as user.userId. No tool argument decides whose data is touched.
  • The functions tools call are internal*. A public function that trusts a userId argument can be called by anyone.
  • Every ID from the model is checked for access with the same helper your UI uses, including IDs of child objects.
  • Every tool that returns an object declares returns. It's enforced, so it is also the allowlist of fields that reach the agent.
  • Reads and writes have separate scopes, and every tool is annotated (readOnlyHint, destructiveHint, openWorldHint).
  • Tools that send data out are behind a server-side policy, such as a recipient allowlist or confirmation in your app, if your server also returns text written by other people. Annotations don't stop prompt injection.
  • The consent page can't be framed (with a frame-ancestors header, or the in-page check on static hosts) and labels client names as unverified.
  • For device-flow requests, the consent page shows the user code and requires the user to confirm it matches, before Allow is enabled.
  • expect(mcp.lint()).toEqual([]) passes in a test. lint() flags missing scopes, missing annotations and inconsistent scopes. It also logs once, on the first MCP request, unless you set warnings: false.

Then run the security audit skill (prompt above), which proves each item live with two users.

Designing your tools

This package ships two agent skills:

  • skills/convex-mcp reads your app and proposes a task-shaped tool surface (names, descriptions, args, scopes, annotations) before wiring it up.
  • skills/convex-mcp-security audits an app's MCP server — identity, cross-user access, public functions taking userId, data in results, prompt-injection and exfiltration paths, the consent page — proving each finding with two real users.
npx skills add get-convex/mcp

Testing

import { convexTest } from "convex-test";
import mcpTest from "@convex-dev/mcp/test";

const t = convexTest(schema, modules);
mcpTest.register(t); // or mcpTest.register(t, "myMcp") for a custom name
const res = await t.fetch("/mcp", { method: "POST", headers: { Authorization: `Bearer ${apiKey}` }, body });

register also registers the bundled rate limiter. Set process.env.CONVEX_SITE_URL in tests. See example/convex/mcp.test.ts.

Limits and roadmap

  • Short tool calls. Each tool call is a single HTTP action, so it has Convex's action time and memory limits. Keep calls short and return small results.
  • Device flow and the pasted-URL guide are on by default. Turn them off with deviceFlow: false.
  • Not yet supported: server-to-client streaming (SSE progress, sampling, elicitation), resources and prompts, and confidential clients.
  • Rate limiting. Client registration and /authorize are rate limited through a bundled @convex-dev/rate-limiter instance. Limits are global or per client, not per IP. The MCP endpoint itself isn't rate limited yet.
  • Client ID metadata documents are fetched from your backend, so they only work for hosts you list in clientMetadataDocumentHosts.

Keywords