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

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.
@@ -20,7 +20,8 @@ export default class CnbApiClient {
20
20
  }
21
21
  return CnbApiClient.instance;
22
22
  }
23
- async request(method, path, body, config) {
23
+ async request(method, path, body, config, responseType = 'json' // 'json' | 'text' | 'raw'
24
+ ) {
24
25
  const url = `${this._baseUrl}${path}`;
25
26
  const headers = {
26
27
  Authorization: `Bearer ${this._token}`,
@@ -37,6 +38,12 @@ export default class CnbApiClient {
37
38
  const errorText = await response.text();
38
39
  throw new Error(`API request failed: ${response.status} ${errorText}`);
39
40
  }
41
+ if (responseType === 'raw') {
42
+ return response;
43
+ }
44
+ if (responseType === 'text') {
45
+ return response.text();
46
+ }
40
47
  return response.json();
41
48
  }
42
49
  }
@@ -0,0 +1,46 @@
1
+ import CnbApiClient from './client.js';
2
+ export async function listGroups(params) {
3
+ const cnbInst = CnbApiClient.getInstance();
4
+ const url = new URL('/user/groups', 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 listSubGroups(group, params) {
15
+ const cnbInst = CnbApiClient.getInstance();
16
+ const url = new URL(`/user/groups/${group}`, cnbInst.baseUrl);
17
+ if (params) {
18
+ for (const [key, value] of Object.entries(params)) {
19
+ if (value === undefined)
20
+ continue;
21
+ url.searchParams.set(key, value.toString());
22
+ }
23
+ }
24
+ return cnbInst.request('GET', `${url.pathname}${url.search}`);
25
+ }
26
+ export async function getGroup(group) {
27
+ const cnbInst = CnbApiClient.getInstance();
28
+ return cnbInst.request('GET', `/${group}`);
29
+ }
30
+ export async function createGroup(params) {
31
+ const body = Object.entries(params).reduce((acc, [key, value]) => {
32
+ if (value === undefined)
33
+ return acc;
34
+ Object.assign(acc, { [key]: value });
35
+ return acc;
36
+ }, {});
37
+ const response = await CnbApiClient.getInstance().request('POST', '/groups', body, {
38
+ header: { 'Content-Type': 'application/json' }
39
+ }, 'raw');
40
+ if (response.status === 201) {
41
+ return { message: 'Created' };
42
+ }
43
+ else {
44
+ return { status: response.status, message: response.statusText };
45
+ }
46
+ }
@@ -26,3 +26,20 @@ export async function listGroupRepositories(group, params) {
26
26
  export async function getRepository(repo) {
27
27
  return CnbApiClient.getInstance().request('GET', `/${repo}`);
28
28
  }
29
+ export async function createRepository(group, params) {
30
+ const body = Object.entries(params).reduce((acc, [key, value]) => {
31
+ if (value === undefined)
32
+ return acc;
33
+ Object.assign(acc, { [key]: value });
34
+ return acc;
35
+ }, {});
36
+ const response = await CnbApiClient.getInstance().request('POST', `/${group}/-/repos`, body, {
37
+ header: { 'Content-Type': 'application/json' }
38
+ }, 'raw');
39
+ if (response.status === 201) {
40
+ return { message: 'Created' };
41
+ }
42
+ else {
43
+ return { status: response.status, message: response.statusText };
44
+ }
45
+ }
@@ -0,0 +1,5 @@
1
+ import CnbApiClient from './client.js';
2
+ export async function getUser() {
3
+ const cnbInst = CnbApiClient.getInstance();
4
+ return cnbInst.request('GET', '/user');
5
+ }
@@ -0,0 +1,17 @@
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
+ export function stopWithWrongTransport(res) {
9
+ res.status(400).json({
10
+ jsonrpc: '2.0',
11
+ error: {
12
+ code: -32000,
13
+ message: 'Bad Request: Session exists but uses a different transport protocol'
14
+ },
15
+ id: null
16
+ });
17
+ }
@@ -7,6 +7,7 @@ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
7
7
  import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
8
8
  import dotenv from 'dotenv';
9
9
  import { registerTools } from './tools/index.js';
10
+ import { getToken, stopWithWrongTransport } from './helpers.js';
10
11
  // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-3.html#import-attributes
11
12
  import packageJSON from '../package.json' with { type: 'json' };
12
13
  dotenv.config();
@@ -16,14 +17,6 @@ const transports = {
16
17
  sse: {}
17
18
  };
18
19
  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
20
  app.use(express.json());
28
21
  app.post('/mcp', async (req, res) => {
29
22
  const sessionId = req.headers['mcp-session-id'];
@@ -31,6 +24,10 @@ app.post('/mcp', async (req, res) => {
31
24
  // Reuse existing transport
32
25
  if (sessionId && transports.streamable[sessionId]) {
33
26
  transport = transports.streamable[sessionId];
27
+ if (!(transport instanceof StreamableHTTPServerTransport)) {
28
+ stopWithWrongTransport(res);
29
+ return;
30
+ }
34
31
  await transport.handleRequest(req, res, req.body);
35
32
  return;
36
33
  }
@@ -52,7 +49,7 @@ app.post('/mcp', async (req, res) => {
52
49
  name: 'cnb-mcp-server',
53
50
  version: packageJSON.version
54
51
  });
55
- const token = req.headers['authorization'].split(' ')[1];
52
+ const token = getToken(req);
56
53
  registerTools(mcpServer, token);
57
54
  await mcpServer.connect(transport);
58
55
  await transport.handleRequest(req, res, req.body);
@@ -75,33 +72,36 @@ const handleSessionRequest = async (req, res) => {
75
72
  return;
76
73
  }
77
74
  const transport = transports.streamable[sessionId];
78
- await transport.handleRequest(req, res);
75
+ await transport.handleRequest(req, res, req.body);
79
76
  };
80
77
  app.get('/mcp', handleSessionRequest);
81
78
  app.delete('/mcp', handleSessionRequest);
82
- const mcpServer = new McpServer({
83
- name: 'cnb-mcp-server',
84
- version: packageJSON.version
85
- });
86
79
  app.get('/sse', async (req, res) => {
87
80
  const transport = new SSEServerTransport('/messages', res);
88
81
  transports.sse[transport.sessionId] = transport;
89
82
  res.on('close', () => {
90
83
  delete transports.sse[transport.sessionId];
91
84
  });
92
- const token = req.headers['authorization'].split(' ')[1];
85
+ const mcpServer = new McpServer({
86
+ name: 'cnb-mcp-server',
87
+ version: packageJSON.version
88
+ });
89
+ const token = getToken(req);
93
90
  registerTools(mcpServer, token);
94
91
  await mcpServer.connect(transport);
95
92
  });
96
93
  app.post('/messages', async (req, res) => {
97
94
  const sessionId = req.query.sessionId;
98
95
  const transport = transports.sse[sessionId];
99
- if (transport) {
100
- await transport.handlePostMessage(req, res);
101
- }
102
- else {
96
+ if (!transport) {
103
97
  res.status(400).send('No transport found for sessionId');
98
+ return;
99
+ }
100
+ if (!(transport instanceof SSEServerTransport)) {
101
+ stopWithWrongTransport(res);
102
+ return;
104
103
  }
104
+ await transport.handlePostMessage(req, res, req.body);
105
105
  });
106
106
  const server = app.listen(3000, () => {
107
107
  console.log('MCP Streamable HTTP Server listening on port 3000');
@@ -0,0 +1,121 @@
1
+ import { z } from 'zod';
2
+ import { createGroup, getGroup, listGroups, listSubGroups } from '../api/group.js';
3
+ export default function registerGroupTools(server) {
4
+ server.tool('list-groups', '获取当前用户拥有权限的顶层组织列表', {
5
+ page: z.number().default(1).describe('第几页,从1开始'),
6
+ page_size: z.number().default(10).describe('每页多少条数据'),
7
+ search: z.preprocess((val) => (val === null ? undefined : val), z.string().optional()).describe('仓库关键字'),
8
+ role: z
9
+ .preprocess((val) => (val === null ? undefined : val), z.enum(['Guest', 'Reporter', 'Developer', 'Master', 'Owner']).optional())
10
+ .describe('最小仓库权限')
11
+ }, async ({ page, page_size, search, role }) => {
12
+ try {
13
+ const repos = await listGroups({ page, page_size, search, role });
14
+ return {
15
+ content: [
16
+ {
17
+ type: 'text',
18
+ text: JSON.stringify(repos, null, 2)
19
+ }
20
+ ]
21
+ };
22
+ }
23
+ catch (error) {
24
+ return {
25
+ content: [
26
+ {
27
+ type: 'text',
28
+ text: `Error listing repositories: ${error instanceof Error ? error.message : String(error)}`
29
+ }
30
+ ],
31
+ isError: true
32
+ };
33
+ }
34
+ });
35
+ server.tool('list-sub-groups', '查询当前用户在指定组织下拥有指定权限的子组织列表', {
36
+ group: z.string().describe('组织名称'),
37
+ page: z.number().default(1).describe('第几页,从1开始'),
38
+ page_size: z.number().default(10).describe('每页多少条数据'),
39
+ access: z.preprocess((val) => (val === null ? undefined : val), z.number().optional()).describe('权限等级')
40
+ }, async ({ group, page, page_size, access }) => {
41
+ try {
42
+ const subGroups = await listSubGroups(group, { page, page_size, access });
43
+ return {
44
+ content: [
45
+ {
46
+ type: 'text',
47
+ text: JSON.stringify(subGroups, null, 2)
48
+ }
49
+ ]
50
+ };
51
+ }
52
+ catch (error) {
53
+ return {
54
+ content: [
55
+ {
56
+ type: 'text',
57
+ text: `Error listing repositories: ${error instanceof Error ? error.message : String(error)}`
58
+ }
59
+ ],
60
+ isError: true
61
+ };
62
+ }
63
+ });
64
+ server.tool('get-group', '获取指定组织信息', {
65
+ group: z.string().describe('组织路径')
66
+ }, async ({ group }) => {
67
+ try {
68
+ const data = await getGroup(group);
69
+ return {
70
+ content: [
71
+ {
72
+ type: 'text',
73
+ text: JSON.stringify(data, null, 2)
74
+ }
75
+ ]
76
+ };
77
+ }
78
+ catch (error) {
79
+ return {
80
+ content: [
81
+ {
82
+ type: 'text',
83
+ text: `Error getting repository: ${error instanceof Error ? error.message : String(error)}`
84
+ }
85
+ ],
86
+ isError: true
87
+ };
88
+ }
89
+ });
90
+ server.tool('create-group', '创建新组织', {
91
+ path: z.string().describe('组织路径'),
92
+ description: z.preprocess((val) => (val === null ? undefined : val), z.string().optional()).describe('组织描述'),
93
+ remark: z.preprocess((val) => (val === null ? undefined : val), z.string().optional()).describe('仓库备注'),
94
+ bind_domain: z
95
+ .preprocess((val) => (val === null ? undefined : val), z.string().optional())
96
+ .describe('根组织绑定的域名')
97
+ }, async ({ path, description, remark, bind_domain }) => {
98
+ try {
99
+ const data = await createGroup({ path, description, remark, bind_domain });
100
+ return {
101
+ content: [
102
+ {
103
+ type: 'text',
104
+ text: JSON.stringify(data, null, 2)
105
+ }
106
+ ]
107
+ };
108
+ }
109
+ catch (error) {
110
+ return {
111
+ content: [
112
+ {
113
+ type: 'text',
114
+ text: `Error creating repository: ${error instanceof Error ? error.message : String(error)}`
115
+ }
116
+ ],
117
+ isError: true
118
+ };
119
+ }
120
+ });
121
+ }
@@ -1,4 +1,5 @@
1
1
  import CnbApiClient from '../api/client.js';
2
+ import registerGroupTools from './groupTools.js';
2
3
  import registerRepoTools from './repoTools.js';
3
4
  import registerIssueTools from './issueTools.js';
4
5
  import registerWorkspaceTools from './workspaceTools.js';
@@ -7,6 +8,7 @@ export function registerTools(server, token) {
7
8
  baseUrl: process.env.API_BASE_URL || 'https://api.cnb.cool',
8
9
  token: process.env.API_TOKEN || token || ''
9
10
  });
11
+ registerGroupTools(server);
10
12
  registerRepoTools(server);
11
13
  registerIssueTools(server);
12
14
  registerWorkspaceTools(server);
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { getRepository, listGroupRepositories, listRepositories } from '../api/repository.js';
2
+ import { createRepository, getRepository, listGroupRepositories, listRepositories } from '../api/repository.js';
3
+ import { getUser } from '../api/user.js';
3
4
  export default function registerRepoTools(server) {
4
5
  server.tool('list-repositories', '获取当前用户拥有指定权限及其以上权限的仓库', {
5
6
  page: z.number().default(1).describe('第几页,从1开始,默认值是1'),
@@ -116,4 +117,41 @@ export default function registerRepoTools(server) {
116
117
  };
117
118
  }
118
119
  });
120
+ server.tool('create-repository', '创建仓库', {
121
+ group: z.preprocess((val) => (val === null ? undefined : val), z.string().optional()).describe('仓库所属分组'),
122
+ name: z.string().describe('仓库名称'),
123
+ description: z.preprocess((val) => (val === null ? undefined : val), z.string().optional()).describe('仓库描述'),
124
+ license: z.preprocess((val) => (val === null ? undefined : val), z.string().optional()).describe('仓库许可'),
125
+ visibility: z
126
+ .preprocess((val) => (val === null ? undefined : val), z.enum(['public', 'private', 'secret']).default('public'))
127
+ .describe('仓库可见性')
128
+ }, async ({ group, name, description, license, visibility }) => {
129
+ let repoGroup = group;
130
+ if (!repoGroup) {
131
+ const { username = '' } = await getUser();
132
+ repoGroup = username;
133
+ }
134
+ try {
135
+ const data = await createRepository(repoGroup, { name, description, license, visibility });
136
+ return {
137
+ content: [
138
+ {
139
+ type: 'text',
140
+ text: JSON.stringify(data, null, 2)
141
+ }
142
+ ]
143
+ };
144
+ }
145
+ catch (error) {
146
+ return {
147
+ content: [
148
+ {
149
+ type: 'text',
150
+ text: `Error creating repository: ${error instanceof Error ? error.message : String(error)}`
151
+ }
152
+ ],
153
+ isError: true
154
+ };
155
+ }
156
+ });
119
157
  }
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.2",
4
+ "version": "0.4.0-beta.4",
5
5
  "main": "./dist/stdio.js",
6
6
  "bin": {
7
7
  "cnb-mcp-stdio": "dist/stdio.js",