mcp-patterns/api-routers

API Routers

Use Case: Define typed, validated API routes for MCP servers using TanStack Start's file-based routing and server functions. This pattern gives you automatic type inference, request validation, and a documented contract for agents.

Prerequisites

  • TanStack Start project with file-based routing already set up
  • TypeScript 5+ for full type inference
  • Zod (optional but recommended) for input validation

Setup & Configuration

The route tree is auto-generated. Routes live in src/routes/ and are mapped to URL paths automatically by the file system convention:

text
src/routes/
  index.tsx              ->  /
  mcp-patterns/
    index.tsx            ->  /mcp-patterns
    database-queries.tsx ->  /mcp-patterns/database-queries
  api/
    og.tsx               ->  /api/og (server-side OG image)

Core Implementation

Define a server function with typed input/output. The validator runs on both the client (optimistic checks) and server (authoritative check):

typescript
import { createServerFn } from "@tanstack/react-start";
import { z } from "zod";

const BookmarkSchema = z.object({
  title: z.string().min(1).max(200),
  url: z.string().url(),
  tags: z.array(z.string()).max(10).default([]),
});

export const createBookmark = createServerFn({ method: "POST" })
  .validator((d: unknown) => BookmarkSchema.parse(d))
  .handler(async ({ data }) => {
    // Persist to database (see database-queries pattern)
    return { id: crypto.randomUUID(), ...data };
  });

export const listBookmarks = createServerFn({ method: "GET" })
  .validator((d: unknown) => z.object({ tag: z.string().optional() }).parse(d))
  .handler(async ({ data }) => {
    const db = getDb();
    const result = data.tag
      ? await db.execute({ sql: "SELECT * FROM bookmarks WHERE ? IN (tags)", args: [data.tag] })
      : await db.execute("SELECT * FROM bookmarks ORDER BY created_at DESC");
    return result.rows;
  });

Call it from the client, wrapping the payload under a data key:

typescript
import { createBookmark, listBookmarks } from "./routes/bookmarks";

// CORRECT: wrap in { data }
const result = await createBookmark({
  data: { title: "MCP Spec", url: "https://modelcontextprotocol.io", tags: ["mcp"] },
});

// WRONG: await createBookmark({ title: "MCP Spec", url: "..." })
// -> data is undefined on the server, handler crashes

// Query with optional filter
const allBookmarks = await listBookmarks({ data: {} });
const tagged = await listBookmarks({ data: { tag: "mcp" } });

For MCP tool definitions, expose the function as a tool with a schema the agent can discover:

typescript
const MCP_TOOLS = [
  {
    name: "create_bookmark",
    description: "Save a bookmark with optional tags",
    inputSchema: {
      type: "object",
      properties: {
        title: { type: "string", description: "Page title" },
        url: { type: "string", format: "uri" },
        tags: { type: "array", items: { type: "string" } },
      },
      required: ["title", "url"],
    },
    handler: async (args: Record<string, unknown>) => {
      "use server";
      return createBookmark({ data: args as any });
    },
  },
];

Deployment Notes

  • Method convention: Always pass arguments to createServerFn wrapped in { data } - top-level fields send an empty body to the server.
  • Error handling: Server functions throw on unhandled errors. Use try/catch and return structured error objects so the agent can react gracefully.
  • Rate limits: Add a simple in-memory rate limiter for production. Serverless instances are ephemeral, so consider Turso or Redis for distributed rate tracking.
  • CORS: MCP clients may call from different origins. Set CORS headers at the server function level or via Nitro hooks.
  • Timeouts: Serverless functions have a 10-30s timeout. For long-running queries, use a polling pattern or stream results.