From prototyping-skills
This skill generates or scaffolds an MCP (Model Context Protocol) server package using the official @modelcontextprotocol/sdk. It should be used when the user asks to create an MCP server, add MCP tools, expose functionality via MCP, build a Model Context Protocol integration, or work inside a packages/mcp directory. It also applies when the user mentions "MCP", "tool for Claude", "tool for AI", "Claude integration", or wants to make core functionality available to LLM agents.
npx claudepluginhub kjgarza/marketplace-claude --plugin prototyping-skillsThis skill is limited to using the following tools:
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Implements structured self-debugging workflow for AI agent failures: capture errors, diagnose patterns like loops or context overflow, apply contained recoveries, and generate introspection reports.
Generate an MCP server using the official @modelcontextprotocol/sdk package
running on Bun. The server exposes core package functionality as MCP tools.
These are team defaults. In execution mode, follow them unless the project CLAUDE.md
overrides them. In Plan mode, suggest alternatives using the deviation protocol from
the prototyping-skills:team-conventions skill: state the default, name the alternative,
explain the trade-off, flag the blast radius, let the human decide.
@modelcontextprotocol/sdk. Not a custom implementation.Bun.env not process.env.@repo/core. MCP tool handlers are thin wrappers.@repo/types. Never duplicate..describe() on every parameter.packages/mcp/
├── src/
│ ├── index.ts # Server entry point, transport setup
│ ├── tools/ # One file per tool or tool group
│ │ └── [tool-name].ts
│ └── lib/ # Shared utilities
├── package.json
└── tsconfig.json
package.json must include:
{
"type": "module",
"bin": {
"mcp-server": "./src/index.ts"
},
"scripts": {
"dev": "bun --watch src/index.ts",
"start": "bun src/index.ts",
"inspect": "bunx @modelcontextprotocol/inspector bun src/index.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1",
"zod": "^3",
"@repo/types": "workspace:*",
"@repo/core": "workspace:*"
}
}
// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { registerItemTools } from "./tools/items";
const server = new McpServer({
name: "my-prototype-mcp",
version: "0.1.0",
});
registerItemTools(server);
const transport = new StdioServerTransport();
await server.connect(transport);
Each tool file registers tools on the server instance using Zod schemas for input validation:
// src/tools/items.ts
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { getItem, listItems, createItem } from "@repo/core";
export function registerItemTools(server: McpServer) {
server.tool(
"list-items",
"List all items, optionally filtered by status",
{
status: z.enum(["active", "archived"]).optional()
.describe("Filter by item status"),
limit: z.number().min(1).max(100).default(20)
.describe("Maximum number of items to return"),
},
async ({ status, limit }) => {
const items = await listItems({ status, limit });
return {
content: [{ type: "text", text: JSON.stringify(items, null, 2) }],
};
}
);
server.tool(
"get-item",
"Get a single item by its ID",
{
id: z.string().describe("The item ID to retrieve"),
},
async ({ id }) => {
const item = await getItem(id);
if (!item) {
return {
content: [{ type: "text", text: `Item not found: ${id}` }],
isError: true,
};
}
return {
content: [{ type: "text", text: JSON.stringify(item, null, 2) }],
};
}
);
server.tool(
"create-item",
"Create a new item with the given name and optional description",
{
name: z.string().min(1).describe("Name for the new item"),
description: z.string().optional().describe("Optional description"),
},
async ({ name, description }) => {
const item = await createItem({ name, description });
return {
content: [{ type: "text", text: JSON.stringify(item, null, 2) }],
};
}
);
}
server.resource(
"config",
"config://app",
"Current application configuration",
async (uri) => ({
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(getConfig(), null, 2),
},
],
})
);
When using the actor pattern in @repo/core (e.g., domain entities powered by provide()),
these rules are non-negotiable:
provide() callback// WRONG — will throw or return undefined
const data = Entity.validate(rawData);
const summary = Entity.getSummary();
// CORRECT — always wrap in provide()
const result = Entity.provide(Entity.validate(rawData), () => ({
summary: Entity.getSummary(),
count: Entity.getCount(),
}));
// In actors/index.ts
export { Entity as EntityActor } from "./entity";
// In parent index.ts
export { EntityActor } from "./actors";
.passthrough() in Zod schemas for API responsesAPI and MCP responses from external sources may include fields not in your schema.
Always use .passthrough() to avoid silently stripping data:
export const EntitySchema = z.object({
id: z.string(),
name: z.string(),
}).passthrough();
// CORRECT — validate first, then provide
Entity.provide(Entity.validate(data), () => {
// safe to call actor methods here
});
// WRONG — providing unvalidated data
Entity.provide(data, () => { /* ... */ });
When an MCP tool wraps a core actor, follow this pattern:
server.tool(
"get-entity-summary",
"Get a summary of the entity including computed fields",
{
id: z.string().describe("The entity ID"),
},
async ({ id }) => {
const raw = await fetchEntity(id);
const result = EntityActor.provide(EntityActor.validate(raw), () => ({
summary: EntityActor.getSummary(),
stats: EntityActor.getStats(),
}));
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
);
list-items, get-item, create-item..describe() on every parameter. This becomes the parameter description in the MCP schema — LLMs rely on it heavily..passthrough() on schemas used for external API responses — never silently drop fields.JSON.stringify with pretty-printing.{ isError: true } with a descriptive message. Never throw unhandled exceptions.@repo/core. Tool handlers: ~5-15 lines.provide(). This is the single most common mistake — never call actor methods outside the callback.The MCP server always uses stdio transport locally. The UI never connects to it directly.
packages/api, port 3001) wraps it and exposes REST endpoints. The Next.js UI fetches from http://localhost:3001/api/... — not from stdio.UI (Next.js :3000) → Hono API (:3001) → MCP stdio process → @repo/core
Never try to connect a browser or Next.js server directly to a stdio MCP process.
Test the MCP server using:
cd packages/mcp
bun run inspect
This opens the MCP Inspector UI where tools can be tested interactively.
After scaffolding, create these tasks to confirm the MCP server and its HTTP wrapper are working.
MCP Inspector check — create after registering tools:
TaskCreate({
subject: "Verify MCP tools with Inspector",
description: "Run: cd packages/mcp && bun run inspect\nIn the Inspector UI: list available tools, call each tool with sample inputs, confirm JSON responses are correct and match expected schema."
})
API curl check — create after the Hono API layer is scaffolded:
TaskCreate({
subject: "Verify API endpoints with curl",
description: "Run these commands and confirm JSON responses:\n curl http://localhost:3001/api/[resource] | python3 -m json.tool\n curl -X POST http://localhost:3001/api/[resource] -H 'Content-Type: application/json' -d '{}' | python3 -m json.tool\nExpect: 200 responses with valid JSON. Fix any 404/500 before the UI consumes these routes."
})
To use this MCP server locally, add to claude_desktop_config.json or .mcp.json:
{
"mcpServers": {
"my-prototype": {
"command": "bun",
"args": ["packages/mcp/src/index.ts"],
"cwd": "/path/to/monorepo"
}
}
}