@cnbcool/mcp-server 0.4.0-beta.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,7 +36,7 @@ export default class CnbApiClient {
36
36
  const response = await fetch(url, options);
37
37
  if (!response.ok) {
38
38
  const errorText = await response.text();
39
- throw new Error(`API request failed: ${response.status} ${errorText}`);
39
+ throw new Error(`API request failed: ${response.status} \n${response.headers.get('traceparent')} \n${errorText} )`);
40
40
  }
41
41
  if (responseType === 'raw') {
42
42
  return response;
package/dist/api/issue.js CHANGED
@@ -25,3 +25,49 @@ export async function createIssue(repo, params) {
25
25
  header: { 'Content-Type': 'application/json' }
26
26
  });
27
27
  }
28
+ export async function updateIssue(repo, issueId, params) {
29
+ return CnbApiClient.getInstance().request('PATCH', `/${repo}/-/issues/${issueId}`, params, {
30
+ header: { 'Content-Type': 'application/json' }
31
+ });
32
+ }
33
+ export async function listIssueComments(repo, issueId, params) {
34
+ const cnbInst = CnbApiClient.getInstance();
35
+ const url = new URL(`/${repo}/-/issues/${issueId}/comments`, cnbInst.baseUrl);
36
+ if (params) {
37
+ for (const [key, value] of Object.entries(params)) {
38
+ if (value === undefined)
39
+ continue;
40
+ url.searchParams.set(key, value.toString());
41
+ }
42
+ }
43
+ return cnbInst.request('GET', `${url.pathname}${url.search}`);
44
+ }
45
+ export async function createIssueComment(repo, issueId, params) {
46
+ return CnbApiClient.getInstance().request('POST', `/${repo}/-/issues/${issueId}/comments`, params, {
47
+ header: { 'Content-Type': 'application/json' }
48
+ });
49
+ }
50
+ export async function updateIssueComment(repo, issueId, commentId, params) {
51
+ return CnbApiClient.getInstance().request('PATCH', `/${repo}/-/issues/${issueId}/comments/${commentId}`, params, {
52
+ header: { 'Content-Type': 'application/json' }
53
+ });
54
+ }
55
+ export async function listIssueLabels(repo, issueId) {
56
+ return CnbApiClient.getInstance().request('GET', `/${repo}/-/issues/${issueId}/labels`);
57
+ }
58
+ export async function addIssueLabels(repo, issueId, labels) {
59
+ return CnbApiClient.getInstance().request('POST', `/${repo}/-/issues/${issueId}/labels`, { labels }, {
60
+ header: { 'Content-Type': 'application/json' }
61
+ });
62
+ }
63
+ export async function setIssueLabels(repo, issueId, labels) {
64
+ return CnbApiClient.getInstance().request('PUT', `/${repo}/-/issues/${issueId}/labels`, { labels }, {
65
+ header: { 'Content-Type': 'application/json' }
66
+ });
67
+ }
68
+ export async function deleteIssueLabels(repo, issueId) {
69
+ await CnbApiClient.getInstance().request('DELETE', `/${repo}/-/issues/${issueId}/labels`);
70
+ }
71
+ export async function deleteIssueLabel(repo, issueId, labelName) {
72
+ await CnbApiClient.getInstance().request('DELETE', `/${repo}/-/issues/${issueId}/labels/${encodeURIComponent(labelName)}`);
73
+ }
@@ -0,0 +1,54 @@
1
+ import CnbApiClient from './client.js';
2
+ export async function listPulls(repo, params) {
3
+ const cnbInst = CnbApiClient.getInstance();
4
+ const url = new URL(`/${repo}/-/pulls`, cnbInst.baseUrl);
5
+ if (params) {
6
+ for (const [key, value] of Object.entries(params)) {
7
+ if (value === undefined)
8
+ continue;
9
+ url.searchParams.set(key, value.toString());
10
+ }
11
+ }
12
+ return cnbInst.request('GET', `${url.pathname}${url.search}`);
13
+ }
14
+ export async function getPull(repo, number) {
15
+ return CnbApiClient.getInstance().request('GET', `/${repo}/-/pulls/${number}`);
16
+ }
17
+ export async function createPull(repo, params) {
18
+ return CnbApiClient.getInstance().request('POST', `/${repo}/-/pulls`, params, {
19
+ header: { 'Content-Type': 'application/json' }
20
+ });
21
+ }
22
+ export async function updatePull(repo, number, params) {
23
+ return CnbApiClient.getInstance().request('PATCH', `/${repo}/-/pulls/${number}`, params, {
24
+ header: { 'Content-Type': 'application/json' }
25
+ });
26
+ }
27
+ export async function mergePull(repo, number, params) {
28
+ return CnbApiClient.getInstance().request('PUT', `/${repo}/-/pulls/${number}/merge`, params, {
29
+ header: { 'Content-Type': 'application/json' }
30
+ });
31
+ }
32
+ export async function listPullComments(repo, number, params) {
33
+ const cnbInst = CnbApiClient.getInstance();
34
+ const url = new URL(`/${repo}/-/pulls/${number}/comments`, cnbInst.baseUrl);
35
+ if (params) {
36
+ for (const [key, value] of Object.entries(params)) {
37
+ if (value === undefined)
38
+ continue;
39
+ url.searchParams.set(key, value.toString());
40
+ }
41
+ }
42
+ return cnbInst.request('GET', `${url.pathname}${url.search}`);
43
+ }
44
+ export async function createPullComment(repo, number, params) {
45
+ const response = await CnbApiClient.getInstance().request('POST', `/${repo}/-/pulls/${number}/comments`, params, {
46
+ header: { 'Content-Type': 'application/json' }
47
+ }, 'raw');
48
+ if (response.status === 201) {
49
+ return { message: 'Created' };
50
+ }
51
+ else {
52
+ return { status: response.status, message: response.statusText };
53
+ }
54
+ }
@@ -0,0 +1,13 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { registerTools } from '../tools/index.js';
3
+ // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-3.html#import-attributes
4
+ import packageJSON from '../../package.json' with { type: 'json' };
5
+ export function createMcpServer(req) {
6
+ const mcpServer = new McpServer({
7
+ name: 'cnb-mcp-server',
8
+ version: packageJSON.version
9
+ });
10
+ const token = req?.headers['authorization']?.split(' ')[1];
11
+ registerTools(mcpServer, token);
12
+ return mcpServer;
13
+ }
@@ -1,10 +1,3 @@
1
- export function getToken(req) {
2
- let token = req.headers['authorization']?.split(' ')[1];
3
- if (!token) {
4
- token = req.query['token'];
5
- }
6
- return token;
7
- }
8
1
  export function stopWithWrongTransport(res) {
9
2
  res.status(400).json({
10
3
  jsonrpc: '2.0',
package/dist/stdio.js CHANGED
@@ -1,16 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
3
  import dotenv from 'dotenv';
5
- import { registerTools } from './tools/index.js';
6
- // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-3.html#import-attributes
7
- import packageJSON from '../package.json' with { type: 'json' };
4
+ import { createMcpServer } from './helpers/createMcpServer.js';
8
5
  dotenv.config();
9
- const server = new McpServer({
10
- name: 'cnb-mcp-server',
11
- version: packageJSON.version
12
- });
13
- registerTools(server);
6
+ const server = createMcpServer();
14
7
  async function main() {
15
8
  console.error('server starting...');
16
9
  const transport = new StdioServerTransport();
@@ -1,16 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import express from 'express';
3
3
  import { randomUUID } from 'node:crypto';
4
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
4
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
6
5
  import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
7
6
  import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
8
7
  import dotenv from 'dotenv';
9
- import { registerTools } from './tools/index.js';
10
- import { getToken, stopWithWrongTransport } from './helpers.js';
11
- // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-3.html#import-attributes
12
- import packageJSON from '../package.json' with { type: 'json' };
8
+ import { createMcpServer } from './helpers/createMcpServer.js';
9
+ import { stopWithWrongTransport } from './helpers/sendResponse.js';
13
10
  dotenv.config();
11
+ const DEFAULT_APP_PORT = 3000;
14
12
  // Store transports for each session type
15
13
  const transports = {
16
14
  streamable: {},
@@ -45,12 +43,7 @@ app.post('/mcp', async (req, res) => {
45
43
  delete transports.streamable[transport.sessionId];
46
44
  }
47
45
  };
48
- const mcpServer = new McpServer({
49
- name: 'cnb-mcp-server',
50
- version: packageJSON.version
51
- });
52
- const token = getToken(req);
53
- registerTools(mcpServer, token);
46
+ const mcpServer = createMcpServer(req);
54
47
  await mcpServer.connect(transport);
55
48
  await transport.handleRequest(req, res, req.body);
56
49
  return;
@@ -82,12 +75,7 @@ app.get('/sse', async (req, res) => {
82
75
  res.on('close', () => {
83
76
  delete transports.sse[transport.sessionId];
84
77
  });
85
- const mcpServer = new McpServer({
86
- name: 'cnb-mcp-server',
87
- version: packageJSON.version
88
- });
89
- const token = getToken(req);
90
- registerTools(mcpServer, token);
78
+ const mcpServer = createMcpServer(req);
91
79
  await mcpServer.connect(transport);
92
80
  });
93
81
  app.post('/messages', async (req, res) => {
@@ -103,8 +91,12 @@ app.post('/messages', async (req, res) => {
103
91
  }
104
92
  await transport.handlePostMessage(req, res, req.body);
105
93
  });
106
- const server = app.listen(3000, () => {
107
- console.log('MCP Streamable HTTP Server listening on port 3000');
94
+ let port = parseInt(process.env.APP_PORT ?? '', 10);
95
+ if (isNaN(port)) {
96
+ port = DEFAULT_APP_PORT;
97
+ }
98
+ const server = app.listen(port, () => {
99
+ console.log(`MCP Streamable HTTP Server listening on port ${port}`);
108
100
  });
109
101
  process.on('SIGTERM', () => {
110
102
  console.log('SIGTERM signal received: closing HTTP server');
@@ -3,6 +3,7 @@ import registerGroupTools from './groupTools.js';
3
3
  import registerRepoTools from './repoTools.js';
4
4
  import registerIssueTools from './issueTools.js';
5
5
  import registerWorkspaceTools from './workspaceTools.js';
6
+ import registerPullTools from './pullTools.js';
6
7
  export function registerTools(server, token) {
7
8
  CnbApiClient.initialize({
8
9
  baseUrl: process.env.API_BASE_URL || 'https://api.cnb.cool',
@@ -12,4 +13,5 @@ export function registerTools(server, token) {
12
13
  registerRepoTools(server);
13
14
  registerIssueTools(server);
14
15
  registerWorkspaceTools(server);
16
+ registerPullTools(server);
15
17
  }
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { createIssue, getIssue, listIssues } from '../api/issue.js';
2
+ import { createIssue, createIssueComment, getIssue, listIssueComments, listIssues, updateIssue, updateIssueComment, listIssueLabels, addIssueLabels, setIssueLabels, deleteIssueLabels, deleteIssueLabel } from '../api/issue.js';
3
3
  export default function registerIssueTools(server) {
4
4
  server.tool('list-issues', '查询仓库的 Issues', {
5
5
  repo: z.string().describe('仓库路径'),
@@ -57,7 +57,7 @@ export default function registerIssueTools(server) {
57
57
  content: [
58
58
  {
59
59
  type: 'text',
60
- text: `Error listing issues: ${error instanceof Error ? error.message : String(error)}`
60
+ text: `Error listing issues: \n${error instanceof Error ? error.message : String(error)}`
61
61
  }
62
62
  ],
63
63
  isError: true
@@ -84,14 +84,14 @@ export default function registerIssueTools(server) {
84
84
  content: [
85
85
  {
86
86
  type: 'text',
87
- text: `Error listing issues: ${error instanceof Error ? error.message : String(error)}`
87
+ text: `Error listing issues: \n${error instanceof Error ? error.message : String(error)}`
88
88
  }
89
89
  ],
90
90
  isError: true
91
91
  };
92
92
  }
93
93
  });
94
- server.tool('create-issue', '创建一个 Issue', {
94
+ server.tool('create-issue', '创建一个 Issue. 如需添加 Issue 标签,需要另外调用 add-issue-labels', {
95
95
  repo: z.string().describe('仓库路径'),
96
96
  title: z.string().describe('Issue 标题'),
97
97
  body: z.preprocess((val) => (val === null ? undefined : val), z.string().optional()).describe('Issue 描述'),
@@ -125,7 +125,271 @@ export default function registerIssueTools(server) {
125
125
  content: [
126
126
  {
127
127
  type: 'text',
128
- text: `Error creating issue: ${error instanceof Error ? error.message : String(error)}`
128
+ text: `Error creating issue: \n${error instanceof Error ? error.message : String(error)}`
129
+ }
130
+ ],
131
+ isError: true
132
+ };
133
+ }
134
+ });
135
+ server.tool('update-issue', '更新一个 Issue。 如需更新 Issue 标签,需要另外调用 set-issue-labels', {
136
+ repo: z.string().describe('仓库路径'),
137
+ issueId: z.number().describe('Issue ID'),
138
+ title: z.preprocess((val) => (val === null ? undefined : val), z.string().optional()).describe('Issue 标题'),
139
+ body: z.preprocess((val) => (val === null ? undefined : val), z.string().optional()).describe('Issue 描述'),
140
+ priority: z.preprocess((val) => (val === null ? undefined : val), z.string().optional()).describe('Issue 优先级'),
141
+ state: z.preprocess((val) => (val === null ? undefined : val), z.string().optional()).describe('Issue 状态'),
142
+ state_reason: z
143
+ .preprocess((val) => (val === null ? undefined : val), z.enum(['completed', 'not_planned', 'reopened']).optional())
144
+ .describe('Issue 状态原因')
145
+ }, async ({ repo, issueId, title, body, priority, state, state_reason }) => {
146
+ try {
147
+ const issue = await updateIssue(repo, issueId, {
148
+ title,
149
+ body,
150
+ priority,
151
+ state,
152
+ state_reason
153
+ });
154
+ return {
155
+ content: [
156
+ {
157
+ type: 'text',
158
+ text: `Issue updated successfully:\n${JSON.stringify(issue, null, 2)}`
159
+ }
160
+ ]
161
+ };
162
+ }
163
+ catch (error) {
164
+ return {
165
+ content: [
166
+ {
167
+ type: 'text',
168
+ text: `Error updating issue: \n${error instanceof Error ? error.message : String(error)}`
169
+ }
170
+ ],
171
+ isError: true
172
+ };
173
+ }
174
+ });
175
+ server.tool('list-issue-comments', '查询 Issue 评论列表', {
176
+ repo: z.string().describe('仓库路径'),
177
+ issueId: z.number().describe('Issue ID'),
178
+ page: z.number().default(1).describe('第几页,从1开始'),
179
+ page_size: z.number().default(30).describe('每页多少条数据,默认是30')
180
+ }, async ({ repo, issueId, page, page_size }) => {
181
+ try {
182
+ const comments = await listIssueComments(repo, issueId, { page, page_size });
183
+ return {
184
+ content: [
185
+ {
186
+ type: 'text',
187
+ text: JSON.stringify(comments, null, 2)
188
+ }
189
+ ]
190
+ };
191
+ }
192
+ catch (error) {
193
+ return {
194
+ content: [
195
+ {
196
+ type: 'text',
197
+ text: `Error listing issue comments: \n${error instanceof Error ? error.message : String(error)}`
198
+ }
199
+ ],
200
+ isError: true
201
+ };
202
+ }
203
+ });
204
+ server.tool('create-issue-comment', '创建一个 Issue 评论', {
205
+ repo: z.string().describe('仓库路径'),
206
+ issueId: z.number().describe('Issue ID'),
207
+ body: z.string().describe('评论内容')
208
+ }, async ({ repo, issueId, body }) => {
209
+ try {
210
+ const comment = await createIssueComment(repo, issueId, { body });
211
+ return {
212
+ content: [
213
+ {
214
+ type: 'text',
215
+ text: `Comment created successfully:\n${JSON.stringify(comment, null, 2)}`
216
+ }
217
+ ]
218
+ };
219
+ }
220
+ catch (error) {
221
+ return {
222
+ content: [
223
+ {
224
+ type: 'text',
225
+ text: `Error creating comment: \n${error instanceof Error ? error.message : String(error)}`
226
+ }
227
+ ],
228
+ isError: true
229
+ };
230
+ }
231
+ });
232
+ server.tool('update-issue-comment', '更新一个 Issue 评论', {
233
+ repo: z.string().describe('仓库路径'),
234
+ issueId: z.number().describe('Issue ID'),
235
+ commentId: z.string().describe('评论 ID'),
236
+ body: z.string().describe('评论内容')
237
+ }, async ({ repo, issueId, commentId, body }) => {
238
+ try {
239
+ const comment = await updateIssueComment(repo, issueId, commentId, { body });
240
+ return {
241
+ content: [
242
+ {
243
+ type: 'text',
244
+ text: `Comment updated successfully:\n${JSON.stringify(comment, null, 2)}`
245
+ }
246
+ ]
247
+ };
248
+ }
249
+ catch (error) {
250
+ return {
251
+ content: [
252
+ {
253
+ type: 'text',
254
+ text: `Error updating comment: \n${error instanceof Error ? error.message : String(error)}`
255
+ }
256
+ ],
257
+ isError: true
258
+ };
259
+ }
260
+ });
261
+ server.tool('list-issue-labels', '查询指定Issue的标签', {
262
+ repo: z.string().describe('仓库路径'),
263
+ issueId: z.number().describe('Issue ID')
264
+ }, async ({ repo, issueId }) => {
265
+ try {
266
+ const labels = await listIssueLabels(repo, issueId);
267
+ return {
268
+ content: [
269
+ {
270
+ type: 'text',
271
+ text: JSON.stringify(labels, null, 2)
272
+ }
273
+ ]
274
+ };
275
+ }
276
+ catch (error) {
277
+ return {
278
+ content: [
279
+ {
280
+ type: 'text',
281
+ text: `Error listing issue labels: \n${error instanceof Error ? error.message : String(error)}`
282
+ }
283
+ ],
284
+ isError: true
285
+ };
286
+ }
287
+ });
288
+ server.tool('add-issue-labels', '为指定Issue添加标签', {
289
+ repo: z.string().describe('仓库路径'),
290
+ issueId: z.number().describe('Issue ID'),
291
+ labels: z.array(z.string()).describe('要添加的标签列表,每个标签需要从仓库标签列表中选择')
292
+ }, async ({ repo, issueId, labels }) => {
293
+ try {
294
+ const result = await addIssueLabels(repo, issueId, labels);
295
+ return {
296
+ content: [
297
+ {
298
+ type: 'text',
299
+ text: `Labels added successfully:\n${JSON.stringify(result, null, 2)}`
300
+ }
301
+ ]
302
+ };
303
+ }
304
+ catch (error) {
305
+ return {
306
+ content: [
307
+ {
308
+ type: 'text',
309
+ text: `Error adding labels: \n${error instanceof Error ? error.message : String(error)}`
310
+ }
311
+ ],
312
+ isError: true
313
+ };
314
+ }
315
+ });
316
+ server.tool('set-issue-labels', '设置Issue的标签(替换所有现有标签)', {
317
+ repo: z.string().describe('仓库路径'),
318
+ issueId: z.number().describe('Issue ID'),
319
+ labels: z.array(z.string()).describe('新的标签列表(将替换所有现有标签),每个标签需要从仓库标签列表中选择')
320
+ }, async ({ repo, issueId, labels }) => {
321
+ try {
322
+ const result = await setIssueLabels(repo, issueId, labels);
323
+ return {
324
+ content: [
325
+ {
326
+ type: 'text',
327
+ text: `Labels set successfully:\n${JSON.stringify(result, null, 2)}`
328
+ }
329
+ ]
330
+ };
331
+ }
332
+ catch (error) {
333
+ return {
334
+ content: [
335
+ {
336
+ type: 'text',
337
+ text: `Error setting labels: \n${error instanceof Error ? error.message : String(error)}`
338
+ }
339
+ ],
340
+ isError: true
341
+ };
342
+ }
343
+ });
344
+ server.tool('delete-issue-labels', '删除Issue的所有标签', {
345
+ repo: z.string().describe('仓库路径'),
346
+ issueId: z.number().describe('Issue ID')
347
+ }, async ({ repo, issueId }) => {
348
+ try {
349
+ await deleteIssueLabels(repo, issueId);
350
+ return {
351
+ content: [
352
+ {
353
+ type: 'text',
354
+ text: 'All labels deleted successfully'
355
+ }
356
+ ]
357
+ };
358
+ }
359
+ catch (error) {
360
+ return {
361
+ content: [
362
+ {
363
+ type: 'text',
364
+ text: `Error deleting labels: \n${error instanceof Error ? error.message : String(error)}`
365
+ }
366
+ ],
367
+ isError: true
368
+ };
369
+ }
370
+ });
371
+ server.tool('delete-issue-label', '删除Issue的指定标签', {
372
+ repo: z.string().describe('仓库路径'),
373
+ issueId: z.number().describe('Issue ID'),
374
+ labelName: z.string().describe('要删除的标签名称')
375
+ }, async ({ repo, issueId, labelName }) => {
376
+ try {
377
+ await deleteIssueLabel(repo, issueId, labelName);
378
+ return {
379
+ content: [
380
+ {
381
+ type: 'text',
382
+ text: `Label "${labelName}" deleted successfully`
383
+ }
384
+ ]
385
+ };
386
+ }
387
+ catch (error) {
388
+ return {
389
+ content: [
390
+ {
391
+ type: 'text',
392
+ text: `Error deleting label: \n${error instanceof Error ? error.message : String(error)}`
129
393
  }
130
394
  ],
131
395
  isError: true
@@ -0,0 +1,222 @@
1
+ import { z } from 'zod';
2
+ import { listPulls, getPull, createPull, updatePull, mergePull, listPullComments, createPullComment } from '../api/pull.js';
3
+ export default function registerPullTools(server) {
4
+ server.tool('list-pulls', '查询仓库的Pull Requests', {
5
+ repo: z.string().describe('仓库路径,格式为 {group}/{repo}'),
6
+ state: z
7
+ .preprocess((val) => (val === null ? undefined : val), z.enum(['open', 'closed', 'all']).optional())
8
+ .describe('Pull Request状态'),
9
+ sort: z
10
+ .preprocess((val) => (val === null ? undefined : val), z.enum(['created', 'updated']).optional())
11
+ .describe('排序字段'),
12
+ direction: z
13
+ .preprocess((val) => (val === null ? undefined : val), z.enum(['asc', 'desc']).optional())
14
+ .describe('排序方向'),
15
+ page: z.number().default(1).describe('页码'),
16
+ per_page: z.number().default(30).describe('每页数量')
17
+ }, async ({ repo, ...params }) => {
18
+ try {
19
+ const pulls = await listPulls(repo, params);
20
+ return {
21
+ content: [
22
+ {
23
+ type: 'text',
24
+ text: JSON.stringify(pulls, null, 2)
25
+ }
26
+ ]
27
+ };
28
+ }
29
+ catch (error) {
30
+ return {
31
+ content: [
32
+ {
33
+ type: 'text',
34
+ text: `Error listing pulls: \n${error instanceof Error ? error.message : String(error)}`
35
+ }
36
+ ],
37
+ isError: true
38
+ };
39
+ }
40
+ });
41
+ server.tool('get-pull', '获取单个Pull Request详情', {
42
+ repo: z.string().describe('仓库路径,格式为 {group}/{repo}'),
43
+ number: z.number().describe('Pull Request编号')
44
+ }, async ({ repo, number }) => {
45
+ try {
46
+ const pull = await getPull(repo, number);
47
+ return {
48
+ content: [
49
+ {
50
+ type: 'text',
51
+ text: JSON.stringify(pull, null, 2)
52
+ }
53
+ ]
54
+ };
55
+ }
56
+ catch (error) {
57
+ return {
58
+ content: [
59
+ {
60
+ type: 'text',
61
+ text: `Error getting pull request: \n${error instanceof Error ? error.message : String(error)}`
62
+ }
63
+ ],
64
+ isError: true
65
+ };
66
+ }
67
+ });
68
+ server.tool('create-pull', '创建Pull Request', {
69
+ repo: z.string().describe('目标仓库路径,格式为 {group}/{repo}'),
70
+ base: z.string().describe('目标仓库目标分支'),
71
+ head_repo: z.string().optional().describe('来源仓库路径,格式为 {group}/{repo},不填则为目标仓库'),
72
+ head: z.string().describe('来源仓库分支'),
73
+ title: z.string().describe('标题'),
74
+ body: z.preprocess((val) => (val === null ? undefined : val), z.string().optional()).describe('描述')
75
+ }, async ({ repo, ...params }) => {
76
+ try {
77
+ const pull = await createPull(repo, params);
78
+ return {
79
+ content: [
80
+ {
81
+ type: 'text',
82
+ text: JSON.stringify(pull, null, 2)
83
+ }
84
+ ]
85
+ };
86
+ }
87
+ catch (error) {
88
+ return {
89
+ content: [
90
+ {
91
+ type: 'text',
92
+ text: `Error creating pull request: \n${error instanceof Error ? error.message : String(error)}`
93
+ }
94
+ ],
95
+ isError: true
96
+ };
97
+ }
98
+ });
99
+ server.tool('update-pull', '更新Pull Request', {
100
+ repo: z.string().describe('仓库路径,格式为 {group}/{repo}'),
101
+ number: z.number().describe('Pull Request编号'),
102
+ title: z.preprocess((val) => (val === null ? undefined : val), z.string().optional()).describe('标题'),
103
+ body: z.preprocess((val) => (val === null ? undefined : val), z.string().optional()).describe('描述'),
104
+ state: z
105
+ .preprocess((val) => (val === null ? undefined : val), z.enum(['open', 'closed']).optional())
106
+ .describe('状态')
107
+ }, async ({ repo, number, ...params }) => {
108
+ try {
109
+ const pull = await updatePull(repo, number, params);
110
+ return {
111
+ content: [
112
+ {
113
+ type: 'text',
114
+ text: JSON.stringify(pull, null, 2)
115
+ }
116
+ ]
117
+ };
118
+ }
119
+ catch (error) {
120
+ return {
121
+ content: [
122
+ {
123
+ type: 'text',
124
+ text: `Error updating pull request: \n${error instanceof Error ? error.message : String(error)}`
125
+ }
126
+ ],
127
+ isError: true
128
+ };
129
+ }
130
+ });
131
+ server.tool('merge-pull', '合并Pull Request', {
132
+ repo: z.string().describe('仓库路径,格式为 {group}/{repo}'),
133
+ number: z.number().describe('Pull Request编号'),
134
+ merge_style: z
135
+ .preprocess((val) => (val === null ? undefined : val), z.enum(['merge', 'squash', 'rebase']).optional())
136
+ .describe('合并方式'),
137
+ commit_title: z.preprocess((val) => (val === null ? undefined : val), z.string()).describe('合并提交标题'),
138
+ commit_message: z
139
+ .preprocess((val) => (val === null ? undefined : val), z.string().optional())
140
+ .describe('合并提交信息')
141
+ }, async ({ repo, number, ...params }) => {
142
+ try {
143
+ const result = await mergePull(repo, number, params);
144
+ return {
145
+ content: [
146
+ {
147
+ type: 'text',
148
+ text: JSON.stringify(result, null, 2)
149
+ }
150
+ ]
151
+ };
152
+ }
153
+ catch (error) {
154
+ return {
155
+ content: [
156
+ {
157
+ type: 'text',
158
+ text: `Error merging pull request: \n${error instanceof Error ? error.message : String(error)}`
159
+ }
160
+ ],
161
+ isError: true
162
+ };
163
+ }
164
+ });
165
+ server.tool('list-pull-comments', '列出Pull Request的评论', {
166
+ repo: z.string().describe('仓库路径,格式为 {group}/{repo}'),
167
+ number: z.number().describe('Pull Request编号'),
168
+ page: z.number().default(1).describe('页码'),
169
+ per_page: z.number().default(30).describe('每页数量')
170
+ }, async ({ repo, number, ...params }) => {
171
+ try {
172
+ const comments = await listPullComments(repo, number, params);
173
+ return {
174
+ content: [
175
+ {
176
+ type: 'text',
177
+ text: JSON.stringify(comments, null, 2)
178
+ }
179
+ ]
180
+ };
181
+ }
182
+ catch (error) {
183
+ return {
184
+ content: [
185
+ {
186
+ type: 'text',
187
+ text: `Error listing pull request comments: \n${error instanceof Error ? error.message : String(error)}`
188
+ }
189
+ ],
190
+ isError: true
191
+ };
192
+ }
193
+ });
194
+ server.tool('create-pull-comment', '创建Pull Request评论', {
195
+ repo: z.string().describe('仓库路径,格式为 {group}/{repo}'),
196
+ number: z.number().describe('Pull Request编号'),
197
+ body: z.string().describe('评论内容')
198
+ }, async ({ repo, number, body }) => {
199
+ try {
200
+ await createPullComment(repo, number, { body });
201
+ return {
202
+ content: [
203
+ {
204
+ type: 'text',
205
+ text: JSON.stringify({ message: 'Comment created successfully' }, null, 2)
206
+ }
207
+ ]
208
+ };
209
+ }
210
+ catch (error) {
211
+ return {
212
+ content: [
213
+ {
214
+ type: 'text',
215
+ text: `Error creating pull request comment: \n${error instanceof Error ? error.message : String(error)}`
216
+ }
217
+ ],
218
+ isError: true
219
+ };
220
+ }
221
+ });
222
+ }
@@ -154,4 +154,57 @@ export default function registerRepoTools(server) {
154
154
  };
155
155
  }
156
156
  });
157
+ server.tool('get-current-repo', '获取当前仓库对应的CNB仓库信息', {
158
+ remote_url: z.string().describe('远程仓库URL, 需要先执行`git remote get-url origin`命令获取')
159
+ }, async ({ remote_url }) => {
160
+ try {
161
+ let repoPath = '';
162
+ if (remote_url.startsWith('git@')) {
163
+ // SSH 格式: git@example.com:group/repo.git
164
+ const match = remote_url.match(/git@[^:]+:(.+?)(?:\.git)?$/);
165
+ if (match) {
166
+ repoPath = match[1];
167
+ }
168
+ }
169
+ else if (remote_url.startsWith('http')) {
170
+ // HTTPS 格式: https://example.com/group/repo.git
171
+ const match = remote_url.match(/https?:\/\/[^/]+\/(.+?)(?:\.git)?$/);
172
+ if (match) {
173
+ repoPath = match[1];
174
+ }
175
+ }
176
+ if (!repoPath) {
177
+ return {
178
+ content: [
179
+ {
180
+ type: 'text',
181
+ text: `无法从远程仓库URL解析出仓库路径: ${remote_url}`
182
+ }
183
+ ],
184
+ isError: true
185
+ };
186
+ }
187
+ // 获取仓库信息
188
+ const data = await getRepository(repoPath);
189
+ return {
190
+ content: [
191
+ {
192
+ type: 'text',
193
+ text: JSON.stringify(data, null, 2)
194
+ }
195
+ ]
196
+ };
197
+ }
198
+ catch (error) {
199
+ return {
200
+ content: [
201
+ {
202
+ type: 'text',
203
+ text: `Error getting current repository: ${error instanceof Error ? error.message : String(error)}`
204
+ }
205
+ ],
206
+ isError: true
207
+ };
208
+ }
209
+ });
157
210
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cnbcool/mcp-server",
3
3
  "description": "CNB MCP Server. A comprehensive MCP server that provides seamless integration to the CNB's API(https://cnb.cool), offering a wide range of tools for repository management, pipelines operations and collaboration features",
4
- "version": "0.4.0-beta.4",
4
+ "version": "0.4.0",
5
5
  "main": "./dist/stdio.js",
6
6
  "bin": {
7
7
  "cnb-mcp-stdio": "dist/stdio.js",