@cnbcool/mcp-server 0.4.0-beta.3 → 0.4.0-beta.5

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/group.js CHANGED
@@ -34,7 +34,13 @@ export async function createGroup(params) {
34
34
  Object.assign(acc, { [key]: value });
35
35
  return acc;
36
36
  }, {});
37
- return CnbApiClient.getInstance().request('POST', '/groups', body, {
37
+ const response = await CnbApiClient.getInstance().request('POST', '/groups', body, {
38
38
  header: { 'Content-Type': 'application/json' }
39
- });
39
+ }, 'raw');
40
+ if (response.status === 201) {
41
+ return { message: 'Created' };
42
+ }
43
+ else {
44
+ return { status: response.status, message: response.statusText };
45
+ }
40
46
  }
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
+ }
@@ -0,0 +1,10 @@
1
+ export function stopWithWrongTransport(res) {
2
+ res.status(400).json({
3
+ jsonrpc: '2.0',
4
+ error: {
5
+ code: -32000,
6
+ message: 'Bad Request: Session exists but uses a different transport protocol'
7
+ },
8
+ id: null
9
+ });
10
+ }
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,29 +1,20 @@
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
- // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-3.html#import-attributes
11
- import packageJSON from '../package.json' with { type: 'json' };
8
+ import { createMcpServer } from './helpers/createMcpServer.js';
9
+ import { stopWithWrongTransport } from './helpers/sendResponse.js';
12
10
  dotenv.config();
11
+ const DEFAULT_APP_PORT = 3000;
13
12
  // Store transports for each session type
14
13
  const transports = {
15
14
  streamable: {},
16
15
  sse: {}
17
16
  };
18
17
  const app = express();
19
- app.use((req, res, next) => {
20
- const token = req.headers['authorization'];
21
- if (!token) {
22
- res.status(401).json({ error: 'Unauthorized' });
23
- return;
24
- }
25
- next();
26
- });
27
18
  app.use(express.json());
28
19
  app.post('/mcp', async (req, res) => {
29
20
  const sessionId = req.headers['mcp-session-id'];
@@ -31,6 +22,10 @@ app.post('/mcp', async (req, res) => {
31
22
  // Reuse existing transport
32
23
  if (sessionId && transports.streamable[sessionId]) {
33
24
  transport = transports.streamable[sessionId];
25
+ if (!(transport instanceof StreamableHTTPServerTransport)) {
26
+ stopWithWrongTransport(res);
27
+ return;
28
+ }
34
29
  await transport.handleRequest(req, res, req.body);
35
30
  return;
36
31
  }
@@ -48,12 +43,7 @@ app.post('/mcp', async (req, res) => {
48
43
  delete transports.streamable[transport.sessionId];
49
44
  }
50
45
  };
51
- const mcpServer = new McpServer({
52
- name: 'cnb-mcp-server',
53
- version: packageJSON.version
54
- });
55
- const token = req.headers['authorization'].split(' ')[1];
56
- registerTools(mcpServer, token);
46
+ const mcpServer = createMcpServer(req);
57
47
  await mcpServer.connect(transport);
58
48
  await transport.handleRequest(req, res, req.body);
59
49
  return;
@@ -75,36 +65,38 @@ const handleSessionRequest = async (req, res) => {
75
65
  return;
76
66
  }
77
67
  const transport = transports.streamable[sessionId];
78
- await transport.handleRequest(req, res);
68
+ await transport.handleRequest(req, res, req.body);
79
69
  };
80
70
  app.get('/mcp', handleSessionRequest);
81
71
  app.delete('/mcp', handleSessionRequest);
82
- const mcpServer = new McpServer({
83
- name: 'cnb-mcp-server',
84
- version: packageJSON.version
85
- });
86
72
  app.get('/sse', async (req, res) => {
87
73
  const transport = new SSEServerTransport('/messages', res);
88
74
  transports.sse[transport.sessionId] = transport;
89
75
  res.on('close', () => {
90
76
  delete transports.sse[transport.sessionId];
91
77
  });
92
- const token = req.headers['authorization'].split(' ')[1];
93
- registerTools(mcpServer, token);
78
+ const mcpServer = createMcpServer(req);
94
79
  await mcpServer.connect(transport);
95
80
  });
96
81
  app.post('/messages', async (req, res) => {
97
82
  const sessionId = req.query.sessionId;
98
83
  const transport = transports.sse[sessionId];
99
- if (transport) {
100
- await transport.handlePostMessage(req, res);
101
- }
102
- else {
84
+ if (!transport) {
103
85
  res.status(400).send('No transport found for sessionId');
86
+ return;
87
+ }
88
+ if (!(transport instanceof SSEServerTransport)) {
89
+ stopWithWrongTransport(res);
90
+ return;
104
91
  }
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
+ }
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.3",
4
+ "version": "0.4.0-beta.5",
5
5
  "main": "./dist/stdio.js",
6
6
  "bin": {
7
7
  "cnb-mcp-stdio": "dist/stdio.js",