@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.
- package/dist/api/client.js +1 -1
- package/dist/api/issue.js +46 -0
- package/dist/api/pull.js +54 -0
- package/dist/helpers/createMcpServer.js +13 -0
- package/dist/{helpers.js → helpers/sendResponse.js} +0 -7
- package/dist/stdio.js +2 -9
- package/dist/streamable.js +11 -19
- package/dist/tools/index.js +2 -0
- package/dist/tools/issueTools.js +269 -5
- package/dist/tools/pullTools.js +222 -0
- package/dist/tools/repoTools.js +53 -0
- package/package.json +1 -1
package/dist/api/client.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/api/pull.js
ADDED
|
@@ -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
|
+
}
|
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 {
|
|
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 =
|
|
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();
|
package/dist/streamable.js
CHANGED
|
@@ -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 {
|
|
10
|
-
import {
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
107
|
-
|
|
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');
|
package/dist/tools/index.js
CHANGED
|
@@ -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
|
}
|
package/dist/tools/issueTools.js
CHANGED
|
@@ -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/dist/tools/repoTools.js
CHANGED
|
@@ -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
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"main": "./dist/stdio.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cnb-mcp-stdio": "dist/stdio.js",
|