Skip to content
Browse documentation
On this page

MCP server

Let AI clients like Claude use your app through an MCP endpoint, with OAuth sign-in and team-scoped tools.

Emberstack serves a Model Context Protocol endpoint at /api/mcp, so AI clients such as Claude can read and create data for a signed-in user. Sign-in uses the Better Auth MCP plugin, which makes Better Auth an OAuth 2.1 server.

How a client connects

  1. The client calls /api/mcp, gets a 401, and finds the authorization server through /.well-known/oauth-protected-resource.
  2. It registers itself and sends the user to sign in at /login, with a password or a passkey.
  3. The user approves access on /oauth/consent. The grant is for the team that's active at that moment.
  4. The client gets an access token for /api/mcp and calls the tools with it. Refresh tokens rotate on every use.

Tools

The starter ships four tools in src/pages/api/mcp.ts, all scoped to the user and the team in the token:

  • list_notes and get_note read the user's notes.
  • create_note creates a draft note.
  • list_activity lists the user's recent activity.

Membership is checked on every call, so someone removed from the team loses access straight away.

Adding a tool

Register it in buildServer() in src/pages/api/mcp.ts with a Zod input schema, and scope every query by userId and organizationId:

server.registerTool(
  'list_projects',
  {
    description: 'List the projects in this team.',
    inputSchema: z.object({ limit: z.number().int().min(1).max(100).default(20) }),
    annotations: { readOnlyHint: true },
  },
  async ({ limit }) =>
    text(await db.select().from(projects).where(eq(projects.organizationId, organizationId)).limit(limit)),
);

Tokens carry the mcp scope. If a tool needs more access, add a scope to MCP_SCOPES in src/lib/betterauth.ts and check it in the handler.

Settings

  • The endpoint and token audience come from PUBLIC_URL (<PUBLIC_URL>/api/mcp), so set it to your real domain.
  • The OAuth tables are created by the app migrations; there's nothing else to configure.
  • Deleting an account revokes its refresh tokens and consents.

Testing locally

Run the Worker with bunx wrangler dev --port 3000 --local-upstream localhost:3000. Without --local-upstream, Wrangler rewrites the host to your production domain and Better Auth rejects the origin. Point an MCP client or the MCP Inspector at http://localhost:3000/api/mcp.