gocallum

mcp-server-skills

9
3
# Install this skill:
npx skills add gocallum/nextjs16-agent-skills --skill "mcp-server-skills"

Install specific skill from multi-skill repository

# Description

Pattern for building MCP servers in Next.js with mcp-handler, shared Zod schemas, and reusable server actions.

# SKILL.md


name: mcp-server-skills
description: Pattern for building MCP servers in Next.js with mcp-handler, shared Zod schemas, and reusable server actions.


  • 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

// 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)

// 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

// 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)

{
  "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.

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.