项目结构
bash
src/
├── index.ts # 入口文件
├── server.ts # 服务器启动逻辑
├── common/ # 公共模块
│ └── index.ts # 配置和工具函数
├── tools/ # MCP工具实现
│ ├── index.ts # 工具注册
│ └── manage/ # 业务功能
│ ├── upload-rule.ts
│ └── download.ts
└── utils/ # 工具函数
├── common.ts
├── uploadCommon.ts
└── workspaceScanner.ts
核心依赖
json
{
"@modelcontextprotocol/sdk": "^1.10.2",
"axios": "^1.10.0",
"form-data": "^4.0.4"
}
1. 项目初始化
package.json配置
json
{
"name": "@company/mcp-server-demo",
"type": "module",
"main": "./dist/index.js",
"bin": {
"mcp-server-demo": "./dist/index.js"
}
}
tsconfig.json配置
json
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"outDir": "./dist",
"esModuleInterop": true
}
}
2. 入口文件实现
src/index.ts
typescript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { startServer } from "./server.js";
import { registerBusinessTool } from "./tools/index.js";
async function main() {
const server = new Server({
name: packageInfo.name,
version: packageInfo.version,
}, {
capabilities: {
tools: {
listChanged: true,
},
},
});
// 注册工具
registerBusinessTool(server);
// 启动服务
await startServer(server);
}
main();
3. 服务器启动逻辑
src/server.ts
typescript
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
async function startStdioMode(server: Server) {
const transport = new StdioServerTransport();
await server.connect(transport);
}
export async function startServer(server: Server) {
try {
await startStdioMode(server);
} catch (error) {
process.exit(1);
}
}
4. 工具注册与实现
src/tools/index.ts
typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
export function registerBusinessTool(server: Server) {
// 注册工具列表
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
UPLOAD_RULE_TOOL,
DOWNLOAD_PROJECT_RULES_TOOL,
],
}));
// 注册工具调用处理器
server.setRequestHandler(CallToolRequestSchema, async (request: any) => {
switch (request.params.name) {
case "upload_rule":
return await handleUploadRule(request);
case "get_download_rules":
return await handleDownloadRules(request);
default:
return {
content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }],
isError: true
};
}
});
}
5. 工具定义
上传工具定义
typescript
export const UPLOAD_RULE_TOOL = {
name: 'upload',
description: '上传文件',
inputSchema: {
type: 'object',
properties: {
filePaths: {
type: 'array',
items: { type: 'string' },
description: '要上传的文件路径列表(必须是绝对路径)'
},
teamSelection: {
type: 'string',
description: '团队选择,如:frontend、backend、testing等'
},
ruleTypeName: {
type: 'string',
enum: ['requirement_rules', 'project_rules', 'common_rules'],
description: '规则类型'
},
},
required: ['filePaths', 'teamSelection', 'ruleTypeName']
}
};
下载工具定义
typescript
export const DOWNLOAD_PROJECT_RULES_TOOL = {
name: "get_download_rules",
description: '下载文件',
inputSchema: {
type: "object",
properties: {
projectNameList: {
type: "array",
description: "项目名集合",
items: { type: "string" }
},
workspaceProjectList: {
type: "array",
description: "工作区项目名集合",
items: { type: "string" }
}
},
required: ["projectNameList", "workspaceProjectList"]
},
};
6. 业务逻辑实现
上传功能核心代码
typescript
export async function handleUploadRule(request: any, filePaths: string[], teamSelection: string, ruleTypeName: string) {
try {
// 1. 验证文件路径
const invalidPaths = filePaths.filter(path => !isAbsolute(path));
if (invalidPaths.length > 0) {
return createErrorResponse("文件路径不是绝对路径", invalidPaths.join(', '));
}
// 2. 读取文件内容
const formData = new FormData();
for (const filePath of filePaths) {
const content = await readFile(filePath, 'utf-8');
const fileName = basename(filePath);
formData.append('file', Buffer.from(content), {
filename: fileName,
contentType: getMimeType(extname(filePath))
});
}
// 3. 添加其他参数
formData.append('team', teamId);
formData.append('ruleTypeId', ruleTypeId);
formData.append('creator', getCurrentUserEmail());
// 4. 发送请求
const response = await axios.post(`${baseUrl}/api/rules/upload`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 30000
});
return {
content: [{ type: "text", text: "上传成功!" }],
isError: false
};
} catch (error) {
return createErrorResponse("上传失败", error.message);
}
}
下载功能核心代码
typescript
export async function handleDownloadRules(request: any, projectNameList: string[], workspaceProjectList: string[]) {
try {
// 1. 构建请求参数
const params = new URLSearchParams();
projectNameList.forEach(name => params.append('projectNameList', name));
// 2. 发起下载请求
const response = await fetch(`${baseUrl}/api/rules/download?${params}`);
const data = await response.json();
// 3. 保存文件到本地
const savedFiles: string[] = [];
for (const fileInfo of data.files) {
const targetPath = await determineFilePath(fileInfo.filePath, projectNameList);
await ensureDirectoryExists(dirname(targetPath));
await writeFile(targetPath, fileInfo.content, 'utf-8');
savedFiles.push(targetPath);
}
return {
content: [{
type: "text",
text: `下载完成!保存了 ${savedFiles.length} 个文件`
}],
isError: false
};
} catch (error) {
return createErrorResponse("下载失败", error.message);
}
}
7. 工具函数
错误处理
typescript
export function createErrorResponse(title: string, message: string) {
return {
content: [{
type: "text",
text: `❌ **${title}**\n\n${message}`
}],
isError: true
};
}
文件路径处理
typescript
export async function determineFilePath(remotePath: string, projectList: string[]): Promise<string> {
const hasCursorRules = remotePath.includes('.cursor/rules');
if (hasCursorRules) {
// 保存到 .cursor/rules 目录
const projectName = extractProjectName(remotePath);
const projectPath = await findProjectPath(projectName, projectList);
return join(projectPath, '.cursor', 'rules', getRelativePath(remotePath));
} else {
// 保存到 ProjectRules 目录
const parentDir = dirname(process.cwd());
return join(parentDir, 'ProjectRules', remotePath);
}
}
8. 构建和部署
构建脚本
json
{
"scripts": {
"build": "tsc && chmod 755 dist/index.js",
"dev": "NODE_OPTIONS=\"--loader ts-node/esm\" node src/index.ts",
"start": "node dist/index.js"
}
}
postbuild处理
javascript
// scripts/postbuild.mjs
import { readFileSync, writeFileSync } from 'fs';
// 添加shebang到入口文件
const indexPath = './dist/index.js';
let content = readFileSync(indexPath, 'utf8');
if (!content.startsWith('#!/usr/bin/env node')) {
content = '#!/usr/bin/env node\n' + content;
writeFileSync(indexPath, content);
}
9. 关键实现要点
MCP通信协议
- 使用stdio模式进行标准输入输出通信
- 支持工具列表查询和工具调用
- 返回标准化的响应格式
文件处理策略
- 支持多文件批量操作
- 智能识别项目结构
- 自动创建目录结构
错误处理机制
- 统一的错误响应格式
- 详细的错误信息提示
- 优雅的异常处理
工作区集成
- 自动检测工作区项目
- 支持单项目和多项目模式
- 智能文件路径解析
10. 测试和调试
本地测试
bash
# 开发模式
npm run dev
# 构建测试
npm run build
npm start
MCP客户端集成测试
json
{
"mcpServers": {
"rule-server": {
"command": "node",
"args": ["/path/to/dist/index.js"]
}
}
}
通过以上步骤,即可完成一个功能完整的MCP服务器实现。