UNPKG

cnb-mcp-server

Version:

MCP Server for the cnb API, enabling file operations, repository management, search functionality, and more.

427 lines (426 loc) 16.8 kB
#!/usr/bin/env node /** * This is a template MCP server that implements a simple notes system. * It demonstrates core MCP concepts like resources and tools by allowing: * - Listing notes as resources * - Reading individual notes * - Creating new notes via a tool * - Summarizing all notes via a prompt */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; import * as path from 'path'; import * as repository from './operations/repository.js'; import * as files from './operations/files.js'; import * as issues from './operations/issues.js'; import { CNBValidationError, CNBResourceNotFoundError, CNBAuthenticationError, CNBPermissionError, CNBRateLimitError, CNBConflictError, isCNBError, } from './common/errors.js'; import { VERSION } from "./common/version.js"; import { defaultLogger as logger, LogLevel } from './common/logger.js'; /** * Simple in-memory storage for notes. * In a real implementation, this would likely be backed by a database. */ const notes = { "1": { title: "First Note", content: "This is note 1" }, "2": { title: "Second Note", content: "This is note 2" } }; /** * Create an MCP server with capabilities for resources (to list/read notes), * tools (to create new notes), and prompts (to summarize notes). */ const server = new Server({ name: "cnb-mcp-server", version: VERSION, }, { capabilities: { resources: {}, tools: {}, prompts: {}, }, }); /** * Handler for listing available notes as resources. * Each note is exposed as a resource with: * - A note:// URI scheme * - Plain text MIME type * - Human readable name and description (now including the note title) */ server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources: Object.entries(notes).map(([id, note]) => ({ uri: `note:///${id}`, mimeType: "text/plain", name: note.title, description: `A text note: ${note.title}` })) }; }); /** * Handler for reading the contents of a specific note. * Takes a note:// URI and returns the note content as plain text. */ server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const url = new URL(request.params.uri); const id = url.pathname.replace(/^\//, ''); const note = notes[id]; if (!note) { throw new Error(`Note ${id} not found`); } return { contents: [{ uri: request.params.uri, mimeType: "text/plain", text: note.content }] }; }); /** * Handler that lists available tools. * Exposes a single "create_note" tool that lets clients create new notes. */ server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ /*{ name: "create_or_update_file", description: "在CNB仓库中创建或更新单个文件", inputSchema: zodToJsonSchema(files.CreateOrUpdateFileSchema), }, */ { name: "search_repositories", description: "搜索CNB仓库,使用新的公共仓库搜索接口", inputSchema: zodToJsonSchema(repository.SearchRepositoriesSchema), }, { name: "create_repository", description: "在您的账户中创建新的CNB仓库", inputSchema: zodToJsonSchema(repository.CreateRepositoryOptionsSchema), }, { name: "get_file_contents", description: "获取CNB仓库中文件或目录的内容", inputSchema: zodToJsonSchema(files.GetFileContentsSchema), }, /*{ name: "push_files", description: "在单个提交中向CNB仓库推送多个文件", inputSchema: zodToJsonSchema(files.PushFilesSchema), },*/ { name: "create_issue", description: "在CNB仓库中创建新的Issue", inputSchema: zodToJsonSchema(issues.CreateIssueSchema), }, { name: "fork_repository", description: "将CNB仓库Fork到您的账户或指定组织", inputSchema: zodToJsonSchema(repository.ForkRepositorySchema), }, /*{ name: "create_branch", description: "在CNB仓库中创建新分支", inputSchema: zodToJsonSchema(branches.CreateBranchSchema), },*/ { name: "list_issues", description: "列出CNB仓库中的Issue,支持筛选", inputSchema: zodToJsonSchema(issues.ListIssuesOptionsSchema) }, { name: "update_issue", description: "更新CNB仓库中的Issue", inputSchema: zodToJsonSchema(issues.UpdateIssueOptionsSchema) }, { name: "add_issue_comment", description: "给Issue添加评论", inputSchema: zodToJsonSchema(issues.IssueCommentSchema) }, { name: "get_issue", description: "获取CNB仓库中Issue的详细信息", inputSchema: zodToJsonSchema(issues.GetIssueSchema) }, { name: "get_user_groups", description: "获取当前用户所在的组织列表", inputSchema: zodToJsonSchema(z.object({})), } ] }; }); /** * Handler for the create_note tool. * Creates a new note with the provided title and content, and returns success message. */ server.setRequestHandler(CallToolRequestSchema, async (request) => { try { if (!request.params.arguments) { throw new Error("Arguments are required"); } switch (request.params.name) { case "fork_repository": { const args = repository.ForkRepositorySchema.parse(request.params.arguments); const fork = await repository.forkRepository(args.owner, args.repo, args.group, args.branch, args.name, args.description); return { content: [{ type: "text", text: JSON.stringify(fork, null, 2) }], }; } /* case "create_branch": { const args = branches.CreateBranchSchema.parse(request.params.arguments); const branch = await branches.createBranchFromRef( args.owner, args.repo, args.branch, args.from_branch ); return { content: [{ type: "text", text: JSON.stringify(branch, null, 2) }], }; } */ case "search_repositories": { const args = repository.SearchRepositoriesSchema.parse(request.params.arguments); const results = await repository.searchRepositories(args.query, args.page, args.perPage); return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }], }; } case "create_repository": { const args = repository.CreateRepositoryOptionsSchema.parse(request.params.arguments); const result = await repository.createRepository(args); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "get_file_contents": { const args = files.GetFileContentsSchema.parse(request.params.arguments); const content = await files.getFileContents(args.owner, args.repo, args.path, args.ref); return { content: [{ type: "text", text: JSON.stringify(content, null, 2) }], }; } /* case "create_or_update_file": { const args = files.CreateOrUpdateFileSchema.parse(request.params.arguments); const result = await files.createOrUpdateFile( args.owner, args.repo, args.path, args.message, args.content, args.branch, args.sha ); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } */ case "create_issue": { const args = issues.CreateIssueSchema.parse(request.params.arguments); logger.info(`创建Issue: ${args.owner}/${args.repo} - ${args.title}`); try { const newIssue = await issues.createIssue(args.owner, args.repo, args.title, args.body, args.assignees, args.labels, args.priority); return { content: [{ type: "text", text: JSON.stringify(newIssue, null, 2) }], }; } catch (error) { logger.error(`创建Issue失败: ${error.message}`); throw error; } } case "get_issue": { const args = issues.GetIssueSchema.parse(request.params.arguments); logger.info(`获取Issue信息: ${args.owner}/${args.repo}#${args.issue_number}`); try { const issue = await issues.getIssue(args.owner, args.repo, args.issue_number); return { content: [{ type: "text", text: JSON.stringify(issue, null, 2) }], }; } catch (error) { logger.error(`获取Issue失败: ${error.message}`); throw error; } } case "update_issue": { const args = issues.UpdateIssueOptionsSchema.parse(request.params.arguments); logger.info(`更新Issue: ${args.owner}/${args.repo}#${args.issue_number}`); const issue = await issues.updateIssue(args.owner, args.repo, args.issue_number, { title: args.title, body: args.body, state: args.state, assignees: args.assignees, labels: args.labels, priority: args.priority }); return { content: [{ type: "text", text: JSON.stringify(issue, null, 2) }], }; } case "list_issues": { const args = issues.ListIssuesOptionsSchema.parse(request.params.arguments); logger.info(`列出Issues: ${args.owner}/${args.repo}`); try { const issuesList = await issues.listIssues(args.owner, args.repo, { state: args.state, labels: args.labels, sort: args.sort, direction: args.direction, since: args.since, page: args.page, perPage: args.perPage }); return { content: [{ type: "text", text: JSON.stringify(issuesList, null, 2) }], }; } catch (error) { logger.error(`列出Issues失败: ${error.message}`); throw error; } } case "add_issue_comment": { const args = issues.IssueCommentSchema.parse(request.params.arguments); logger.info(`添加Issue评论: ${args.owner}/${args.repo}#${args.issue_number}`); try { const comment = await issues.addIssueComment(args.owner, args.repo, args.issue_number, args.body); return { content: [{ type: "text", text: JSON.stringify(comment, null, 2) }], }; } catch (error) { logger.error(`添加Issue评论失败: ${error.message}`); throw error; } } case "get_user_groups": { const groups = await repository.getUserGroups(); return { content: [{ type: "text", text: JSON.stringify(groups, null, 2) }], }; } default: throw new Error(`Unknown tool: ${request.params.name}`); } } catch (error) { if (isCNBError(error)) { throw new Error(formatCNBError(error)); } else { throw error; } } }); /** * Handler that lists available prompts. * Exposes a single "summarize_notes" prompt that summarizes all notes. */ server.setRequestHandler(ListPromptsRequestSchema, async () => { return { prompts: [ { name: "summarize_notes", description: "Summarize all notes", } ] }; }); /** * Handler for the summarize_notes prompt. * Returns a prompt that requests summarization of all notes, with the notes' contents embedded as resources. */ server.setRequestHandler(GetPromptRequestSchema, async (request) => { if (request.params.name !== "summarize_notes") { throw new Error("Unknown prompt"); } const embeddedNotes = Object.entries(notes).map(([id, note]) => ({ type: "resource", resource: { uri: `note:///${id}`, mimeType: "text/plain", text: note.content } })); return { messages: [ { role: "user", content: { type: "text", text: "Please summarize the following notes:" } }, ...embeddedNotes.map(note => ({ role: "user", content: note })), { role: "user", content: { type: "text", text: "Provide a concise summary of all the notes above." } } ] }; }); function formatCNBError(error) { let message = `CNB API Error: ${error.message}`; if (error instanceof CNBValidationError) { message = `Validation Error: ${error.message}`; if (error.response) { message += `\nDetails: ${JSON.stringify(error.response)}`; } } else if (error instanceof CNBResourceNotFoundError) { message = `Not Found: ${error.message}`; } else if (error instanceof CNBAuthenticationError) { message = `Authentication Failed: ${error.message}`; } else if (error instanceof CNBPermissionError) { message = `Permission Denied: ${error.message}`; } else if (error instanceof CNBRateLimitError) { message = `Rate Limit Exceeded: ${error.message}\nResets at: ${error.resetAt.toISOString()}`; } else if (error instanceof CNBConflictError) { message = `Conflict: ${error.message}`; } return message; } /** * Start the server using stdio transport. * This allows the server to communicate via standard input/output streams. */ export async function main(args) { // 解析环境变量,配置日志 const logLevel = process.env.CNB_LOG_LEVEL || LogLevel.INFO; const logFile = process.env.CNB_LOG_FILE || path.join(process.cwd(), 'log.txt'); // 更新默认日志配置 logger.level = logLevel; logger.filePath = logFile; // 确保日志文件正确初始化 logger.initialize(); logger.info('CNB MCP Server starting', { version: VERSION }); // 否则作为MCP服务器运行 logger.info('Running in MCP server mode'); const transport = new StdioServerTransport(); await server.connect(transport); logger.info('CNB MCP Server running on stdio'); console.error("CNB MCP Server running on stdio"); } main().catch((error) => { logger.error('Server error', error); console.error("Server error:", error); process.exit(1); });