第一个 MCP 服务器
本教程将指导你创建一个简单的 MCP 服务器,实现基本的工具和资源功能。这个示例服务器将提供一些实用的文件操作工具。
项目设置
如果你还没有完成环境搭建,请先参考开发环境搭建。
1. 初始化项目
# 创建项目
mkdir hello-mcp-server
cd hello-mcp-server
# 初始化 package.json
npm init -y
# 安装依赖
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node tsx
2. 创建基础结构
# 创建源代码目录
mkdir -p src/{tools,resources,utils}
实现服务器
1. 创建主入口文件
创建 src/index.ts:
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { setupFileTools } from './tools/file-tools.js';
import { setupTextResources } from './resources/text-resources.js';
async function createServer() {
const server = new Server(
{
name: 'hello-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
// 设置工具
setupFileTools(server);
// 设置资源
setupTextResources(server);
// 错误处理
server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
await server.close();
process.exit(0);
});
return server;
}
async function main() {
const server = await createServer();
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Hello MCP Server running on stdio');
}
main().catch((error) => {
console.error('Server error:', error);
process.exit(1);
});
2. 实现文件操作工具
创建 src/tools/file-tools.ts:
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { CallToolRequestSchema, ListToolsRequestSchema, McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { promises as fs } from 'fs';
import path from 'path';
export function setupFileTools(server: Server) {
// 列出可用工具
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'read_file',
description: '读取文件内容',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: '文件路径',
},
},
required: ['path'],
},
},
{
name: 'write_file',
description: '写入文件内容',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: '文件路径',
},
content: {
type: 'string',
description: '文件内容',
},
},
required: ['path', 'content'],
},
},
{
name: 'list_directory',
description: '列出目录内容',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: '目录路径',
default: '.',
},
},
required: ['path'],
},
},
],
};
});
// 处理工具调用
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'read_file':
return await readFile(args.path);
case 'write_file':
return await writeFile(args.path, args.content);
case 'list_directory':
return await listDirectory(args.path);
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${name}`
);
}
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Tool execution failed: ${error instanceof Error ? error.message : String(error)}`
);
}
});
}
async function readFile(filePath: string) {
try {
const content = await fs.readFile(filePath, 'utf-8');
return {
content: [
{
type: 'text',
text: content,
},
],
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to read file: ${error instanceof Error ? error.message : String(error)}`
);
}
}
async function writeFile(filePath: string, content: string) {
try {
// 确保目录存在
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, content, 'utf-8');
return {
content: [
{
type: 'text',
text: `Successfully wrote to ${filePath}`,
},
],
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to write file: ${error instanceof Error ? error.message : String(error)}`
);
}
}
async function listDirectory(dirPath: string) {
try {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
const result = entries.map(entry => ({
name: entry.name,
type: entry.isDirectory() ? 'directory' : 'file',
}));
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to list directory: ${error instanceof Error ? error.message : String(error)}`
);
}
}
3. 实现文本资源
创建 src/resources/text-resources.ts:
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
interface TextResource {
uri: string;
name: string;
description: string;
mimeType: string;
content: string;
}
// 示例资源
const resources: TextResource[] = [
{
uri: 'text://welcome',
name: 'Welcome Message',
description: 'A welcome message from the MCP server',
mimeType: 'text/plain',
content: 'Welcome to your first MCP server! This server provides file operations tools.',
},
{
uri: 'text://help',
name: 'Help Information',
description: 'Help information for using this server',
mimeType: 'text/markdown',
content: `# Help - Hello MCP Server
This server provides the following tools:
## Tools
- **read_file**: Read the contents of a file
- **write_file**: Write content to a file
- **list_directory**: List the contents of a directory
## Resources
- **Welcome Message**: A welcome message
- **Help**: This help information
## Usage
1. Use the tools through Claude Code
2. Access resources for additional information
3. Check the server logs for any errors`,
},
];
export function setupTextResources(server: Server) {
// 列出所有资源
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: resources.map(r => ({
uri: r.uri,
name: r.name,
description: r.description,
mimeType: r.mimeType,
})),
};
});
// 读取特定资源
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
const resource = resources.find(r => r.uri === uri);
if (!resource) {
throw new Error(`Resource not found: ${uri}`);
}
return {
contents: [
{
uri: resource.uri,
mimeType: resource.mimeType,
text: resource.content,
},
],
};
});
}
4. 配置 TypeScript
创建 tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "node",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
测试服务器
1. 构建项目
# 编译 TypeScript
npm run build
# 或者直接运行开发模式
npm run dev
2. 配置 Claude Code
在你的 claude_desktop_config.json 中添加:
{
"mcpServers": {
"hello-mcp-server": {
"command": "node",
"args": ["/path/to/hello-mcp-server/dist/index.js"],
"env": {}
}
}
}
3. 测试功能
重启 Claude Code 后,你应该能够:
- 看到服务器提供的工具
- 使用文件操作工具
- 访问文本资源
扩展功能
添加新工具
- 在
src/tools/目录下创建新文件 - 导出设置函数
- 在
src/index.ts中调用
添加新资源
- 在
src/resources/目录下创建新文件 - 定义资源数据
- 注册资源处理器
添加错误处理
// 示例:添加全局错误处理
process.on('uncaughtException', (error) => {
console.error('Uncaught exception:', error);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection at:', promise, 'reason:', reason);
process.exit(1);
});
最佳实践
- 输入验证:始终验证工具参数
- 错误处理:提供清晰的错误消息
- 日志记录:记录重要操作
- 资源清理:正确处理文件和连接
- 类型安全:充分利用 TypeScript
下一步
现在你已经创建了一个基础的 MCP 服务器,你可以:
完整代码示例
完整的示例代码可以在以下位置找到:
- GitHub 仓库:hello-mcp-server
- 本地文件:
/path/to/hello-mcp-server/src/