关于 mcp-server-builder
mcp-server-builder 是面向开发者的技能,用于创建可通过自定义工具和资源扩展 Claude 的 MCP 服务。内容涵盖传输方式、实现、测试与部署。
主要功能
- 01配置 stdio 与 HTTP/SSE 传输
- 02定义并验证工具模式
- 03添加静态和动态资源
- 04创建可复用提示词
- 05使用 MCP Inspector 测试
适用场景
- 01为 Claude 添加自定义工具
- 02向客户端提供文件或数据资源
- 03本地测试并部署 MCP 服务
MCP 相关
该技能指导构建 MCP 服务,向 MCP 客户端提供工具、资源和提示词,并介绍本地 stdio 与远程 HTTP/SSE 传输。
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ patricio0312rev/ skills --skill mcp-server-builder安装后在 Claude Code 中输入/mcp-server-builder确认已加载。
详细介绍
MCP 服务构建器
创建 Model Context Protocol 服务,通过自定义工具和资源扩展 Claude 的能力。
核心工作流
- 定义用途:确定要添加哪些能力
- 选择传输方式:stdio(本地)或 HTTP/SSE(远程)
- 设计工具:定义工具架构和处理程序
- 添加资源:可选的文件/数据访问
- 创建提示词:可选的可复用提示词
- 本地测试:使用 MCP 检查器验证
- 部署:为 Claude Desktop 或 API 配置
MCP 架构概览
┌─────────────┐ MCP Protocol ┌─────────────┐
│ Claude │◄────────────────────►│ MCP Server │
│ (Host) │ JSON-RPC over stdio │ (Your App) │
└─────────────┘ └─────────────┘
│
▼
┌───────────┐
│ Tools │
│ Resources │
│ Prompts │
└───────────┘
项目设置
TypeScript MCP 服务
# 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/**/*"]
}
基本服务结构
// 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 };
}
工具
工具架构定义
// 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;
}
工具处理程序模式
// 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();
}
);
资源
静态资源
// 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}`);
}
});
动态资源
// 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}`);
});
提示词
可复用提示词
// 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}`);
}
});
错误处理
// 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 配置
// ~/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 传输
// 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");
});
测试
MCP 检查器
# Install MCP inspector
npx @modelcontextprotocol/inspector
# Run your server with inspector
npx @modelcontextprotocol/inspector node dist/index.js
单元测试
// 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();
});
});
完整示例:GitHub MCP 服务
// 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);
最佳实践
- 验证输入:使用 Zod 或类似工具验证架构
- 妥善处理错误:返回有意义的错误消息
- 使用环境变量:切勿将机密信息硬编码
- 限制外部调用频率:遵守 API 限制
- 适当记录日志:使用 stderr 记录日志(stdout 用于 MCP)
- 完善工具文档:清晰的描述有助于 Claude 正确使用工具
- 保持工具专注:每个工具只负责一项职责
- 使用检查器测试:部署前验证行为
输出检查清单
每个 MCP 服务都应包含:
- 包含名称和版本的服务
- 能力声明(工具/资源/提示词)
- 带有描述的工具架构
- 输入验证及清晰的错误消息
- 正确的错误处理和 MCP 错误代码
- 环境变量配置
- Claude Desktop 配置示例
- MCP 检查器测试
- 处理程序的单元测试
- 包含设置说明的 README
评论
支持 Markdown