Guides

MCP tools

Expose routes and defineMcpTool files as MCP tools that Claude, Cursor, and other MCP clients call as a signed-in project user.

Every project backend can serve an MCP (Model Context Protocol) endpoint at POST {apiUrl}/mcp. Tools come from two places: defineApi routes that set mcp, and defineMcpTool files under robodev/mcp/ (or packages/be/mcp/). Nothing is exposed by default. robodev dev serves the same endpoint on http://localhost:{port}/mcp and prints it at start. The project's MCP page in the dashboard (/projects/:id/mcp) lists the tools and has Copy MCP URL.

The endpoint moved from /_robodev/mcp to /mcp. /_robodev/mcp now returns 404, and grants issued for it no longer work: remove clients added before the move and add them again with the new URL. A route file api/mcp.ts still serves /api/mcp, and a browser opening /mcp still gets your app's page; only MCP requests go to the endpoint.

Expose a route

Set mcp: true to expose a route under its default name, <method>_<path slug>: GET /planets/:id becomes get_planets_by_id and POST / becomes post_root. mcp: { name, description } overrides both. The description falls back to businessLogic, then "<METHOD> <path>", so write businessLogic for an agent. The tool input is { params?, query?, body? }: params holds the path params (required when the path has any), query and body follow the route schemas. The call runs the route exactly like HTTP — validation, pagination, timeout, and request logs (marked via MCP). rawBody and multipart routes cannot be tools; deploy fails with invalid_api_definition.

robodev/api/planets/[id].ts

import { defineApi, z } from "@robodev-ai/sdk";

export const get = defineApi({
  auth: "required",
  businessLogic: "Read one planet the signed-in user owns, by id.",
  response: z.object({ id: z.string(), name: z.string() }),
  mcp: true, // tool get_planets_by_id, input { params: { id } }
  handler: async ({ params, db, user }) => loadOwnedPlanet(db, params.id!, user!.id),
});

defineMcpTool

For a tool that is not an HTTP route, default-export defineMcpTool from robodev/mcp/<name>.ts. The name defaults to the path under mcp/ (mcp/billing/refund-invoice.ts → billing_refund_invoice); description is required. input is a Zod object (default {}), output is optional and validated. The handler gets the same ctx as defineApi (db, email, llm, agent, storage, pdf, env, jobs, sockets, push, user) plus ctx.input. Tool names are 1–64 letters, digits, _, . or -, and must be unique across routes and mcp/ files.

robodev/mcp/billing/refund-invoice.ts

import { defineMcpTool, z } from "@robodev-ai/sdk";

export default defineMcpTool({
  description: "Refund an invoice the signed-in user owns. Returns the refund status.",
  input: z.object({ invoiceId: z.string() }),
  output: z.object({ ok: z.boolean() }),
  auth: "required",
  handler: async ({ input, user }) => {
    // look up input.invoiceId for user.id, refund it
    return { ok: true };
  },
});

Sign-in and auth

  • Each tool keeps its auth: false is public, "optional" and "required" use Robodev Auth, and a custom verifier runs against the MCP request headers.
  • tools/list is filtered per caller: required tools appear only to a signed-in user, and a custom-verifier tool only when its verifier accepts. A hidden tool is "Unknown tool", same as a missing one.
  • When Robodev Auth is on and any tool is not public, an anonymous request gets 401 with WWW-Authenticate pointing at /.well-known/oauth-protected-resource/mcp. MCP clients then register (dynamic client registration or a client ID metadata document), open the project's sign-in and consent page at /_robodev/oauth/authorize, and exchange the code with PKCE.
  • The consent page offers email + password (and Create account when sign-ups are on) and Continue with Google when Google is on — the project's Auth settings decide. Access tokens are opaque rdpm_ values that work only on /mcp; a project access token (Bearer) also works there.
  • The consent page remembers a sign-in for 30 days in that browser, so the next client you connect shows only Approve. "Not you? Use another account" signs it out. The 30 days are counted from sign-in and are not extended.
  • With Robodev Auth off, only public tools are listed and callable, and there is no sign-in.

Connect Claude or Cursor

Add a custom connector (Claude) or an MCP server (Cursor, MCP Inspector) with the URL from Copy MCP URL, for example https://my-app.robodev.dev/mcp. The client discovers the authorization server itself; sign in as a project user and click Allow. Each host is its own resource: a token issued on a custom domain does not work on the slug host.

.cursor/mcp.json

{
  "mcpServers": {
    "my-app": { "url": "https://my-app.robodev.dev/mcp" }
  }
}

Name and icon

MCP clients show the project name by default, with no icon. To set a display name, home page, and icons, default-export defineMcp from robodev/mcp.ts with title, websiteUrl, and icons. They are sent in serverInfo on initialize and on server/discover (MCP 2025-11-25 and later); clients that support icons, such as Cursor, show them in their server list. Claude's connector list uses the favicon of the MCP URL's host, so also serve /favicon.ico from your frontend.

robodev/mcp.ts

import { defineMcp } from "@robodev-ai/sdk";

export default defineMcp({
  title: "Acme Boards",
  websiteUrl: "/",
  icons: [
    { src: "/icon-512.png", mimeType: "image/png", sizes: ["512x512"] },
    { src: "/favicon.svg", mimeType: "image/svg+xml", sizes: ["any"] },
  ],
});
  • title: 1–100 characters. The server's name stays the project name.
  • websiteUrl and each icon src: a path like "/icon-512.png", resolved against the host the MCP client used (so files in the frontend's public/ folder work on the project host and custom domains), or an absolute https:// URL. An icon src may also be a base64 data:image/png, jpeg, or webp URI of at most 64 KB (serve SVG from a path). robodev dev also accepts http://localhost and http://127.0.0.1.
  • icons: 1–8 entries. mimeType is image/png, image/jpeg, image/svg+xml, or image/webp; PNG works in every client. sizes is a list like ["48x48"] or ["any"]. theme is "light" or "dark" for an icon drawn for that client theme.
  • An invalid value fails the deploy with invalid_mcp_config. Needs @robodev-ai/sdk 0.20.0 or later for the types; under robodev dev, robodev 0.65.0.

By default Robodev shows its own sign-in and consent page. To show yours instead, default-export defineMcp from robodev/mcp.ts (beside the mcp/ folder, not inside it) and set consentUrl. It is a path like "/mcp-consent", resolved against the host the MCP client used (the project host or a custom domain), or an absolute https:// URL, for example a frontend hosted elsewhere. robodev dev also accepts http://localhost and http://127.0.0.1. Anything else fails the deploy with invalid_mcp_config.

robodev/mcp.ts

import { defineMcp } from "@robodev-ai/sdk";

export default defineMcp({ consentUrl: "/mcp-consent" });

Robodev still checks the client, redirect URI, PKCE and resource, then redirects to your page with ?mcp_request=…. The request expires after 10 minutes. Your page uses the app's own sign-in: if the user is signed out, sign them in and keep mcp_request in the URL. Then show the client name and redirect host from preview, and call approve or deny. Both return redirectTo; navigate there to hand the result back to the MCP client. approve uses the signed-in user's access token.

src/pages/McpConsent.tsx

import { useEffect, useState } from "react";
import { createClient, type McpConsentPreview } from "@robodev-ai/client";

const client = createClient({ url: import.meta.env.VITE_ROBODEV_URL ?? "" });

export function McpConsent() {
  const requestId = client.mcp.consent.requestFromUrl();
  const [preview, setPreview] = useState<McpConsentPreview | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    if (!requestId) return;
    client.mcp.consent.preview(requestId).then(setPreview, (e) => setError(String(e.message ?? e)));
  }, [requestId]);

  if (!requestId || error) return <p>{error ?? "Go back to the app and connect again."}</p>;
  if (!preview) return null;
  // Signed out? Show your sign-in first; keep ?mcp_request in the URL.

  return (
    <div>
      <h1>Allow {preview.client.name} to use {preview.projectName}?</h1>
      <p>It will return to {preview.redirectHost}.</p>
      <button
        onClick={async () =>
          location.assign((await client.mcp.consent.approve(requestId)).redirectTo)
        }
      >
        Allow
      </button>
      <button
        onClick={async () => location.assign((await client.mcp.consent.deny(requestId)).redirectTo)}
      >
        Deny
      </button>
    </div>
  );
}

The endpoints behind the helpers are GET /_robodev/oauth/consent?request=…, and POST /_robodev/oauth/consent/approve (Authorization: Bearer <app access token>) and /_robodev/oauth/consent/deny with { request }. They work from another origin. The MCP page in the dashboard shows which consent page the deployment uses.

Revocation

  • A password reset ends all of that user's MCP grants; their clients must sign in again.
  • Deleting the user deletes their grants and tokens.
  • Signing out of the app does not affect MCP clients.
  • A client can revoke its own grant at /_robodev/oauth/revoke (RFC 7009). Refresh tokens rotate; reusing an old one ends the grant.