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
JavaScript
/**
* 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);
});