home / skills / gocallum / nextjs16-agent-skills / mcp-server-skills

mcp-server-skills skill

/skills/mcp-server-skills

This skill enables building MCP servers in Next.js with shared Zod schemas and reusable actions for consistent, scalable tooling.

npx playbooks add skill gocallum/nextjs16-agent-skills --skill mcp-server-skills

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
4.1 KB
---
name: mcp-server-skills
description: Pattern for building MCP servers in Next.js with mcp-handler, shared Zod schemas, and reusable server actions.
---

## Links

- Model Context Protocol: https://modelcontextprotocol.io/
- mcp-handler (HTTP): https://www.npmjs.com/package/mcp-handler
- Reference implementation (Roll Dice): https://github.com/gocallum/rolldice-mcpserver
- Claude Desktop + mcp-remote bridge: https://www.npmjs.com/package/mcp-remote

## Folder Structure (Next.js App Router)

```
app/
  api/[transport]/route.ts   # One handler for all transports (e.g., /api/mcp)
  actions/mcp-actions.ts     # Server actions reusing the same logic/schemas
lib/
  dice.ts | tools.ts         # Zod schemas, tool definitions, pure logic
components/                  # UI that calls server actions for web testing
```

**Goal:** Keep `route.ts` minimal. Put logic + Zod schemas in `lib/*` so both the MCP handler and server actions share a single source of truth.

## Shared Zod Schema + Tool Definition

```ts
// lib/dice.ts
import { z } from "zod";

export const diceSchema = z.number().int().min(2);

export function rollDice(sides: number) {
  const validated = diceSchema.parse(sides);
  const value = 1 + Math.floor(Math.random() * validated);
  return { type: "text" as const, text: `🎲 You rolled a ${value}!` };
}

export const rollDiceTool = {
  name: "roll_dice",
  description: "Rolls an N-sided die",
  schema: { sides: diceSchema },
} as const;
```

## Reusable Server Actions (Web UI + Tests)

```ts
// app/actions/mcp-actions.ts
"use server";
import { rollDice as rollDiceCore, rollDiceTool } from "@/lib/dice";

export async function rollDice(sides: number) {
  try {
    const result = rollDiceCore(sides);
    return { success: true, result: { content: [result] } };
  } catch {
    return {
      success: false,
      error: { code: -32602, message: "Invalid parameters: sides must be >= 2" },
    };
  }
}

export async function listTools() {
  return {
    success: true,
    result: {
      tools: [
        {
          name: rollDiceTool.name,
          description: rollDiceTool.description,
          inputSchema: {
            type: "object",
            properties: { sides: { type: "number", minimum: 2 } },
            required: ["sides"],
          },
        },
      ],
    },
  };
}
```

Server actions call the same logic as the MCP handler and power the web UI, keeping responses aligned.

## Lightweight MCP Route

```ts
// app/api/[transport]/route.ts
import { createMcpHandler } from "mcp-handler";
import { rollDice, rollDiceTool } from "@/lib/dice";

const handler = createMcpHandler(
  (server) => {
    server.tool(
      rollDiceTool.name,
      rollDiceTool.description,
      rollDiceTool.schema,
      async ({ sides }) => ({ content: [rollDice(sides)] }),
    );
  },
  {}, // server options
  {
    basePath: "/api",     // must match folder path
    maxDuration: 60,
    verboseLogs: true,
  },
);

export { handler as GET, handler as POST };
```

**Pattern highlights**
- Route only wires `createMcpHandler`; no business logic inline.
- `server.tool` consumes the shared tool schema/description and calls shared logic.
- `basePath` should align with the folder (e.g., `/api/[transport]`).
- Works for SSE/HTTP transports; stdio can be added separately if needed.

## Claude Desktop Config (mcp-remote)

```jsonc
{
  "mcpServers": {
    "rolldice": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "http://localhost:3000/api/mcp"]
    }
  }
}
```

## Best Practices

1) **Single source of truth** β€” schemas + logic in `lib/*`; both MCP tools and server actions import them.  
2) **Validation first** β€” use Zod for inputs and reuse the same schema for UI + MCP.  
3) **Keep route.ts light** β€” only handler wiring, logging, and transport config.  
4) **Shared responses** β€” standardize `{ success, result | error }` shapes for tools and UI.  
5) **Vercel-friendly** β€” avoid stateful globals; configure `maxDuration` and `runtime` if needed.  
6) **Multiple transports** β€” expose `/api/[transport]` for HTTP/SSE; add stdio entrypoint when required.  
7) **Local testing** β€” hit server actions from the web UI to ensure MCP responses stay in sync.

Overview

This skill provides a pattern for building Model Context Protocol (MCP) servers in Next.js using mcp-handler, shared Zod schemas, and reusable server actions. It shows how to keep route handlers minimal while sharing validation and business logic between MCP endpoints and Next.js server actions. The setup is designed for Next.js App Router, Prisma-ready backends, and compatibility with AI SDKs and local testing tools.

How this skill works

Place validation and pure logic in a shared lib/ folder (Zod schemas, tool definitions, and functions). Wire a minimal app/api/[transport]/route.ts that uses createMcpHandler to register tools with their schemas and call the shared logic. Implement server actions (app/actions/*.ts) that call the same shared functions so the web UI and tests use identical behavior and response shapes.

When to use it

  • Building an MCP-compatible API that must match web UI behavior
  • When you want a single source of truth for input validation and tool metadata
  • Deploying to serverless platforms (Vercel) where route code should stay lightweight
  • Testing and iterating locally with server actions and mcp-remote bridges
  • Exposing multiple transports (HTTP, SSE, stdio) from the same codebase

Best practices

  • Keep Zod schemas and pure functions in lib/ so both MCP and server actions import them
  • Make route.ts only responsible for handler wiring, transport config, and logging
  • Reuse the same input schemas for UI forms and tool definitions to avoid drift
  • Return standardized shapes like { success, result | error } across MCP and server actions
  • Avoid stateful globals; set maxDuration and runtime for serverless deployments
  • Use server actions to validate and mirror MCP responses for easy local testing

Example use cases

  • A dice-rolling tool demonstrating a validated roll function and tool description
  • Exposing database-backed tools (Prisma) for retrieval and mutations via MCP and server actions
  • Testing AI tool integrations locally with a web UI that calls server actions and an MCP bridge (mcp-remote)
  • Adding new tools by defining a Zod schema and function in lib/ then registering via createMcpHandler

FAQ

How do I keep route.ts minimal?

Move all validation and business logic to lib/*. Only call createMcpHandler in route.ts and register tools with their schemas and handlers.

How do I test MCP responses locally?

Use server actions from the web UI to exercise the same logic. Optionally run mcp-remote or Claude Desktop with the bridge pointing at /api/mcp.