QUESTPIE
Getting Started

Connect an AI agent with MCP and OAuth

Scaffold a QUESTPIE app with MCP, expose safe CRUD and a custom tool, then connect Claude and VS Code through per-user OAuth 2.1.

This tutorial starts with a fresh TanStack Start app and ends with two AI clients calling your QUESTPIE data through the remote MCP endpoint:

  • Claude, through a remote custom connector.
  • VS Code, through a workspace mcp.json entry.

Both clients authenticate as a real QUESTPIE user. They register through Dynamic Client Registration (DCR), use authorization code + PKCE, show the user a consent screen, and receive a token bound to this app's /api/mcp endpoint. Every tool call is then limited by all three gates:

MCP policy ∩ consented OAuth scopes ∩ collection/access rules

You will expose read and write tools for the starter posts collection, keep delete disabled, and add a typed posts.countPublished custom tool.

Prerequisites

  • Bun 1.3 or newer.
  • Docker for the local Postgres database.
  • A public HTTPS deployment for Claude. Claude's remote connectors run from Anthropic's cloud, including when you use Claude Desktop, so they cannot reach a server that only exists on localhost. See Anthropic's remote connector network requirements.
  • VS Code with GitHub Copilot if you want to follow the IDE section. VS Code can connect to a local or remote HTTP MCP server.

1. Scaffold the app with MCP

Run the published scaffolder non-interactively:

Terminal
bunx create-questpie@latest questpie-mcp-agent \
  --template tanstack-start \
  --modules admin,openapi,mcp \
  --yes
cd questpie-mcp-agent

Explicit module selection replaces the defaults, so include all three modules. The result contains:

  • adminModule, which also brings the OAuth provider, account tables, login, and consent UI.
  • openApiModule, for the REST reference at /api/docs.
  • mcpModule, for the OAuth-protected Streamable HTTP endpoint at /api/mcp.

Verify the generated app before changing it:

bun run scaffold:verify

This command regenerates the app and runs TypeScript. It does not mutate the database.

2. Start the local database

Start Postgres, then create the local development schema:

docker compose up -d
bun run db:push

db:push is local development only

bun run db:push calls questpie push. Never run it against production, in a deployment job, or in an init container. --force does not make it production-safe. For production, generate a migration with bun run migrate:create, review and commit it, then apply it with bun run migrate during deployment.

Start the app:

bun run dev

Open http://localhost:3000/admin. The first visit redirects to the one-time setup screen. Create the first admin account and keep those credentials for the OAuth sign-in later.

3. Open only the MCP operations you need

HTTP MCP is read-only by default. Add config/mcp.ts to allow authenticated agents to read and create/update posts while keeping deletion closed:

src/questpie/server/config/mcp.ts
import { mcpConfig } from "@questpie/mcp";

export default mcpConfig({
  crud: {
    collections: {
      posts: { read: true, write: true, delete: false },
    },
    globals: {
      siteSettings: { read: true, write: false },
    },
  },
});

This contributes these relevant tools:

ToolMCP policyOAuth scope
collections.posts.list / count / getallowedcollections:posts:read or collections:read
collections.posts.create / updateallowedcollections:posts:write or collections:write
collections.posts.deletehiddenunavailable even if a token has a delete scope
globals.siteSettings.getallowedglobals:siteSettings:read
globals.siteSettings.updatehiddenunavailable

The OAuth scope does not grant database access by itself. The signed-in user's normal collection .access() rules still run and can further narrow every operation.

4. Add a typed custom tool

Create a file-convention MCP tool that counts published posts:

src/questpie/server/mcp-tools/count-published-posts.ts
import { mcpTool } from "@questpie/mcp";
import { z } from "zod";

export default mcpTool("posts.countPublished", {
  description: "Count published posts.",
  inputSchema: z.object({}),
  outputSchema: z.object({ count: z.number() }),
  annotations: { readOnlyHint: true },
}).handler(async ({ ctx }) => {
  const count = await ctx.collections.posts.count({
    where: { published: true },
  });

  return {
    structuredContent: { count },
    content: [{ type: "text", text: `${count} published posts` }],
  };
});

Custom tool results include both structuredContent for the model and an MCP content block. The public handler type requires content; do not return only structuredContent.

This example does not declare an extra custom-tool scope. The HTTP endpoint still requires an authenticated principal, and the ctx.collections.posts.count() call still runs the user's collection access rules. Add scopes or access to the tool when it needs a narrower rule; see custom tool scopes.

Regenerate and type-check both new files:

bun run scaffold:verify

Codegen should report one discovered mcpTools entry.

5. Understand the OAuth flow

Pointing a compliant client at /api/mcp is enough to start the flow:

  1. The unauthenticated MCP request receives 401 and a WWW-Authenticate challenge pointing to protected-resource metadata.
  2. The client reads /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server.
  3. DCR creates a public OAuth client at /api/auth/oauth2/register; no hand-created client secret is needed.
  4. The client opens /api/auth/oauth2/authorize with a PKCE challenge and the /api/mcp resource indicator.
  5. You sign in at /admin/login and approve the requested scopes at /admin/oauth/consent.
  6. The client exchanges the code at /api/auth/oauth2/token, then retries /api/mcp with the audience-bound access token.

The access token expires after one hour. Its effective permission is the intersection of consented scopes and the signed-in user's RBAC; OAuth can narrow access but cannot elevate it.

You can inspect discovery on a running deployment without obtaining a token:

curl https://cms.example.com/.well-known/oauth-protected-resource
curl https://cms.example.com/.well-known/oauth-authorization-server
curl -i https://cms.example.com/api/mcp

The final request should be unauthorized and include the discovery challenge. A public 200 from /api/mcp without a session or token is a security problem.

6. Prepare the public deployment

Set the deployed app origin exactly; OAuth uses it to bind tokens to the MCP audience:

Production environment
APP_URL=https://cms.example.com
BETTER_AUTH_SECRET=<a-strong-production-secret>
DATABASE_URL=<production-postgres-url>

Generate the migration in development, review it, and commit it:

bun run migrate:create
git add src/questpie/server/migrations
git commit -m "chore: add initial database migration"

During deployment, apply only committed migrations:

bun run migrate

Never replace migrate with push

Production uses questpie migrate, not questpie push. Do not put push, db:push, or push --force in a container command, init container, CI deploy step, or production runbook.

Before connecting a client, confirm that /api/mcp, both root /.well-known/* documents, /api/auth/oauth2/register, /admin/login, and /admin/oauth/consent all route to the same deployment.

7. Connect Claude

Claude remote custom connectors support Streamable HTTP, OAuth, and DCR. QUESTPIE therefore needs only the endpoint URL; leave the optional advanced client ID and secret empty.

For an individual account:

  1. Open Customize → Connectors.
  2. Select + → Add custom connector.
  3. Enter a name and https://cms.example.com/api/mcp.
  4. Leave advanced OAuth credentials empty and add the connector.
  5. Select Connect, sign in with the admin account you created, and review the consent screen before approving it.
  6. Enable the connector for a conversation from the + → Connectors menu.

Team and Enterprise plans require an Owner to add the connector under Organization settings → Connectors before members connect their own accounts. Anthropic documents both flows in Get started with custom connectors using remote MCP.

Do not put this remote server in claude_desktop_config.json. That file is for local stdio servers; Claude's remote connectors are configured through Connectors and run from Anthropic's cloud.

8. Connect VS Code

Add a workspace MCP configuration:

.vscode/mcp.json
{
  "servers": {
    "questpie": {
      "type": "http",
      "url": "https://cms.example.com/api/mcp"
    }
  }
}

VS Code discovers the server, registers through DCR, then completes browser login and consent. See the official VS Code MCP developer guide and MCP server configuration guide.

Open the Command Palette and run MCP: List Servers. Start questpie, confirm that you trust the server, then complete the browser login and consent flow. VS Code lists the discovered tools in Chat's tool picker. If you need to discard a stale DCR registration, run Authentication: Remove Dynamic Authentication Providers and reconnect.

For local-only testing, change the URL to http://localhost:3000/api/mcp. Keep APP_URL=http://localhost:3000 so the issued token's audience matches the endpoint exactly.

9. Exercise CRUD and the custom tool

Ask the connected client to make explicit calls while you learn the surface:

Use collections.posts.list to show the titles and published state of my posts.
Use collections.posts.create to create a draft post titled "MCP is connected".
Call posts.countPublished and return the count.

Then open /admin/collections/posts and confirm that the created draft exists. If the write tool is missing, check both sides of the gate:

  • config/mcp.ts must set posts.write: true.
  • The OAuth grant must include collections:posts:write or collections:write.
  • The signed-in user must pass the collection's normal create/update access rules.

Delete remains unavailable because the server policy set delete: false, regardless of what the model asks for.

  • MCP integration is the complete tool, resource, policy, transport, and custom-tool reference.
  • MCP over OAuth 2.1 covers DCR, PKCE, consent, scopes, token verification, and scopes ∩ RBAC in depth.
  • AI integration is the separate server-side worker and resumable execution package. You do not need @questpie/ai to expose an MCP server to Claude or an IDE.
  • Access control is the security boundary beneath MCP policy and OAuth scopes.

On this page