mcp-server-builder
SKILL.md
MCP Server Builder
Create Model Context Protocol servers to extend Claude's capabilities with custom tools and resources.
Core Workflow
- Define purpose: Identify what capabilities to add
- Choose transport: stdio (local) or HTTP/SSE (remote)
- Design tools: Define tool schemas and handlers
- Add resources: Optional file/data access
- Create prompts: Optional reusable prompts
- Test locally: Verify with MCP inspector
- Deploy: Configure for Claude Desktop or API
MCP Architecture Overview
┌─────────────┐ MCP Protocol ┌─────────────┐
│ Claude │◄────────────────────►│ MCP Server │
│ (Host) │ JSON-RPC over stdio │ (Your App) │
└─────────────┘ └─────────────┘
│
▼
┌───────────┐
│ Tools │
│ Resources │
│ Prompts │
└───────────┘
Project Setup
TypeScript MCP Server
# Create project
mkdir my-mcp-server && cd my-mcp-server
npm init -y
# Install dependencies
npm install @modelcontextprotocol/sdk zod
# Dev dependencies
npm install -D typescript @types/node tsx
// package.json
{
"name": "my-mcp-server",
"version": "1.0.0",
"type": "module",
"main": "dist/index.js",
"bin": {
"my-mcp-server": "./dist/index.js"
},
"scripts": {
"build": "tsc",
"dev": "tsx watch src/index.ts",
"start": "node dist/index.js"
}
}
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["src/**/*"]
}
Basic Server Structure
// src/index.ts
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListPromptsRequestSchema,
GetPromptRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// Create server instance
const server = new Server(
{
name: "my-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
resources: {},
prompts: {},
},
}
);
// Define tools
const TOOLS = [
{
name: "get_weather",
description: "Get current weather for a location",
inputSchema: {
type: "object" as const,
properties: {
location: {
type: "string",
description: "City name or coordinates",
},
units: {
type: "string",
enum: ["celsius", "fahrenheit"],
default: "celsius",
},
},
required: ["location"],
},
},
{
name: "search_database",
description: "Search the internal database",
inputSchema: {
type: "object" as const,
properties: {
query: {
type: "string",
description: "Search query",
},
limit: {
type: "number",
default: 10,
},
},
required: ["query"],
},
},
];
// List tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools: TOOLS };
});
// Call tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "get_weather": {
const { location, units = "celsius" } = args as {
location: string;
units?: string;
};
// Implement your logic here
const weather = await fetchWeather(location, units);
return {
content: [
{
type: "text",
text: JSON.stringify(weather, null, 2),
},
],
};
}
case "search_database": {
const { query, limit = 10 } = args as { query: string; limit?: number };
const results = await searchDatabase(query, limit);
return {
content: [
{
type: "text",
text: JSON.stringify(results, null, 2),
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
});
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP Server running on stdio");
}
main().catch(console.error);
// Helper functions (implement your logic)
async function fetchWeather(location: string, units: string) {
// Your implementation
return { location, temperature: 22, units, condition: "sunny" };
}
async function searchDatabase(query: string, limit: number) {
// Your implementation
return { query, results: [], total: 0 };
}
Tools
Tool Schema Definition
// src/tools/index.ts
import { z } from "zod";
// Define input schemas with Zod for validation
export const GetWeatherSchema = z.object({
location: z.string().describe("City name or coordinates"),
units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
});
export const SearchSchema = z.object({
query: z.string().min(1).describe("Search query"),
filters: z
.object({
category: z.string().optional(),
dateFrom: z.string().optional(),
dateTo: z.string().optional(),
})
.optional(),
limit: z.number().min(1).max(100).default(10),
});
// Convert Zod schema to JSON Schema for MCP
export function zodToJsonSchema(schema: z.ZodObject<any>) {
// Use zod-to-json-schema package in production
return schema;
}
Tool Handler Pattern
// src/tools/handlers.ts
import { z } from "zod";
type ToolHandler<T extends z.ZodSchema> = (
args: z.infer<T>
) => Promise<{ content: Array<{ type: string; text: string }> }>;
export function createToolHandler<T extends z.ZodSchema>(
schema: T,
handler: (args: z.infer<T>) => Promise<any>
): ToolHandler<T> {
return async (rawArgs) => {
// Validate input
const args = schema.parse(rawArgs);
// Execute handler
const result = await handler(args);
// Format response
return {
content: [
{
type: "text",
text: typeof result === "string" ? result : JSON.stringify(result, null, 2),
},
],
};
};
}
// Usage
export const handleGetWeather = createToolHandler(
GetWeatherSchema,
async ({ location, units }) => {
const response = await fetch(
`https://api.weather.com/v1/current?location=${location}&units=${units}`
);
return response.json();
}
);
Resources
Static Resources
// src/resources/index.ts
const RESOURCES = [
{
uri: "config://app-settings",
name: "Application Settings",
description: "Current application configuration",
mimeType: "application/json",
},
{
uri: "file://docs/readme",
name: "Documentation",
description: "Project documentation",
mimeType: "text/markdown",
},
];
// List resources handler
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return { resources: RESOURCES };
});
// Read resource handler
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
switch (uri) {
case "config://app-settings":
return {
contents: [
{
uri,
mimeType: "application/json",
text: JSON.stringify(getAppSettings(), null, 2),
},
],
};
case "file://docs/readme":
const content = await fs.readFile("./README.md", "utf-8");
return {
contents: [
{
uri,
mimeType: "text/markdown",
text: content,
},
],
};
default:
throw new Error(`Unknown resource: ${uri}`);
}
});
Dynamic Resources
// Resources with templates (e.g., database records)
const RESOURCE_TEMPLATES = [
{
uriTemplate: "db://users/{userId}",
name: "User Profile",
description: "Get user by ID",
mimeType: "application/json",
},
];
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
return { resourceTemplates: RESOURCE_TEMPLATES };
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
// Parse URI to extract parameters
const userMatch = uri.match(/^db:\/\/users\/(\w+)$/);
if (userMatch) {
const userId = userMatch[1];
const user = await db.users.findById(userId);
return {
contents: [
{
uri,
mimeType: "application/json",
text: JSON.stringify(user, null, 2),
},
],
};
}
throw new Error(`Unknown resource: ${uri}`);
});
Prompts
Reusable Prompts
// src/prompts/index.ts
const PROMPTS = [
{
name: "code_review",
description: "Review code for best practices and issues",
arguments: [
{
name: "language",
description: "Programming language",
required: true,
},
{
name: "focus",
description: "What to focus on (security, performance, style)",
required: false,
},
],
},
{
name: "explain_error",
description: "Explain an error message and suggest fixes",
arguments: [
{
name: "error",
description: "The error message",
required: true,
},
{
name: "context",
description: "Additional context about what you were doing",
required: false,
},
],
},
];
// List prompts handler
server.setRequestHandler(ListPromptsRequestSchema, async () => {
return { prompts: PROMPTS };
});
// Get prompt handler
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "code_review":
return {
description: "Code review prompt",
messages: [
{
role: "user",
content: {
type: "text",
text: `Please review the following ${args?.language || "code"} code.
${args?.focus ? `Focus particularly on: ${args.focus}` : ""}
Look for:
- Bugs and potential issues
- Security vulnerabilities
- Performance improvements
- Code style and best practices
- Suggestions for improvement`,
},
},
],
};
case "explain_error":
return {
description: "Error explanation prompt",
messages: [
{
role: "user",
content: {
type: "text",
text: `I encountered this error:
\`\`\`
${args?.error}
\`\`\`
${args?.context ? `Context: ${args.context}` : ""}
Please explain:
1. What this error means
2. Common causes
3. How to fix it
4. How to prevent it in the future`,
},
},
],
};
default:
throw new Error(`Unknown prompt: ${name}`);
}
});
Error Handling
// src/utils/errors.ts
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
export class ToolError extends Error {
constructor(
message: string,
public code: ErrorCode = ErrorCode.InternalError
) {
super(message);
this.name = "ToolError";
}
toMcpError(): McpError {
return new McpError(this.code, this.message);
}
}
// Usage in handlers
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params;
// ... handle tool
} catch (error) {
if (error instanceof ToolError) {
throw error.toMcpError();
}
if (error instanceof z.ZodError) {
throw new McpError(
ErrorCode.InvalidParams,
`Invalid parameters: ${error.errors.map((e) => e.message).join(", ")}`
);
}
throw new McpError(
ErrorCode.InternalError,
error instanceof Error ? error.message : "Unknown error"
);
}
});
Claude Desktop Configuration
// ~/Library/Application Support/Claude/claude_desktop_config.json (macOS)
// %APPDATA%\Claude\claude_desktop_config.json (Windows)
{
"mcpServers": {
"my-mcp-server": {
"command": "node",
"args": ["/path/to/my-mcp-server/dist/index.js"],
"env": {
"API_KEY": "your-api-key"
}
},
"npx-server": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/dir"]
}
}
}
HTTP/SSE Transport
// src/http-server.ts
import express from "express";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
const app = express();
const server = new Server(/* ... */);
// SSE endpoint for MCP
app.get("/mcp", async (req, res) => {
const transport = new SSEServerTransport("/mcp/messages", res);
await server.connect(transport);
});
// Message endpoint
app.post("/mcp/messages", express.json(), async (req, res) => {
// Handle incoming messages
});
app.listen(3000, () => {
console.log("MCP HTTP server running on port 3000");
});
Testing
MCP Inspector
# Install MCP inspector
npx @modelcontextprotocol/inspector
# Run your server with inspector
npx @modelcontextprotocol/inspector node dist/index.js
Unit Tests
// tests/tools.test.ts
import { describe, it, expect } from "vitest";
import { handleGetWeather } from "../src/tools/handlers";
describe("get_weather tool", () => {
it("returns weather data for valid location", async () => {
const result = await handleGetWeather({
location: "New York",
units: "celsius",
});
expect(result.content[0].type).toBe("text");
const data = JSON.parse(result.content[0].text);
expect(data).toHaveProperty("temperature");
});
it("throws error for invalid location", async () => {
await expect(
handleGetWeather({ location: "", units: "celsius" })
).rejects.toThrow();
});
});
Complete Example: GitHub MCP Server
// src/index.ts
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { Octokit } from "@octokit/rest";
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
const server = new Server(
{ name: "github-mcp", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
const TOOLS = [
{
name: "list_issues",
description: "List issues in a GitHub repository",
inputSchema: {
type: "object" as const,
properties: {
owner: { type: "string", description: "Repository owner" },
repo: { type: "string", description: "Repository name" },
state: { type: "string", enum: ["open", "closed", "all"], default: "open" },
},
required: ["owner", "repo"],
},
},
{
name: "create_issue",
description: "Create a new issue",
inputSchema: {
type: "object" as const,
properties: {
owner: { type: "string" },
repo: { type: "string" },
title: { type: "string" },
body: { type: "string" },
labels: { type: "array", items: { type: "string" } },
},
required: ["owner", "repo", "title"],
},
},
];
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "list_issues": {
const { owner, repo, state = "open" } = args as any;
const { data } = await octokit.issues.listForRepo({ owner, repo, state });
return {
content: [{
type: "text",
text: JSON.stringify(
data.map((i) => ({ number: i.number, title: i.title, state: i.state })),
null,
2
),
}],
};
}
case "create_issue": {
const { owner, repo, title, body, labels } = args as any;
const { data } = await octokit.issues.create({ owner, repo, title, body, labels });
return {
content: [{
type: "text",
text: `Created issue #${data.number}: ${data.html_url}`,
}],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
});
const transport = new StdioServerTransport();
server.connect(transport);
Best Practices
- Validate inputs: Use Zod or similar for schema validation
- Handle errors gracefully: Return meaningful error messages
- Use environment variables: Never hardcode secrets
- Rate limit external calls: Respect API limits
- Log appropriately: Use stderr for logs (stdout is for MCP)
- Document tools well: Clear descriptions help Claude use them correctly
- Keep tools focused: Single responsibility per tool
- Test with inspector: Verify behavior before deployment
Output Checklist
Every MCP server should include:
- Server with name and version
- Capabilities declaration (tools/resources/prompts)
- Tool schemas with descriptions
- Input validation with clear error messages
- Proper error handling and MCP error codes
- Environment variable configuration
- Claude Desktop config example
- MCP inspector testing
- Unit tests for handlers
- README with setup instructions
Weekly Installs
10
Repository
patricio0312rev/skillsFirst Seen
10 days ago
Installed on
claude-code8
gemini-cli7
antigravity7
windsurf7
github-copilot7
codex7