gxcs-lowcode-mcp 0.0.0-stage → 0.2.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/README.md +9 -2
- package/package.json +15 -4
- package/server/index.mjs +308 -0
package/README.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
#
|
|
1
|
+
# gxcs-lowcode-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Read-only MCP stdio server that exposes the GXCS low-code platform REST API as 8 whitelisted `GET` tools for WorkBuddy.
|
|
4
|
+
|
|
5
|
+
- No third-party runtime dependencies (Node.js >= 18 built-ins only).
|
|
6
|
+
- Every tool is read-only: no create, update, delete, or arbitrary URL calls.
|
|
7
|
+
- Configuration is supplied through `API_BASE_URL` and `AUTH_TOKEN` environment variables. No credentials are bundled.
|
|
8
|
+
- `AUTH_TOKEN` is sent as `Authorization: Bearer <token>` (the prefix is added automatically).
|
|
9
|
+
|
|
10
|
+
This package is the distributable form of `server/index.mjs` used by the `gxcs-lowcode-query` WorkBuddy connector. It is not intended for direct CLI use.
|
package/package.json
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gxcs-lowcode-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Read-only MCP stdio server for the GXCS low-code platform, used by the WorkBuddy connector gxcs-lowcode-query.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gxcs-lowcode-mcp": "./server/index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"server/index.mjs",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18.0.0"
|
|
15
|
+
},
|
|
16
|
+
"license": "UNLICENSED"
|
|
17
|
+
}
|
package/server/index.mjs
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Local stdio MCP server for the GXCS low-code platform (read-only).
|
|
3
|
+
// No third-party dependencies: Node >= 18 built-ins only.
|
|
4
|
+
// Protocol: MCP JSON-RPC 2.0 over newline-delimited stdio.
|
|
5
|
+
// IMPORTANT: stdout is reserved for protocol messages. All diagnostics go to stderr.
|
|
6
|
+
|
|
7
|
+
import readline from 'node:readline';
|
|
8
|
+
|
|
9
|
+
const SERVER_NAME = 'gxcs-lowcode-query';
|
|
10
|
+
const SERVER_VERSION = '0.2.0';
|
|
11
|
+
const MAX_RESPONSE_CHARS = 120000;
|
|
12
|
+
|
|
13
|
+
const API_BASE_URL = String(process.env.API_BASE_URL || '').replace(/\/+$/, '');
|
|
14
|
+
const AUTH_TOKEN = String(process.env.AUTH_TOKEN || '');
|
|
15
|
+
|
|
16
|
+
function diag(message) {
|
|
17
|
+
// Never print AUTH_TOKEN or Authorization values.
|
|
18
|
+
process.stderr.write(`[${SERVER_NAME}] ${message}\n`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Whitelist: tool name -> HTTP method + path builder. Nothing else can be called.
|
|
22
|
+
const TOOLS = [
|
|
23
|
+
{
|
|
24
|
+
name: 'list_applications',
|
|
25
|
+
description: '分页查询低代码平台的应用列表。可按名称、分组、主键过滤。只读。',
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
pageNum: { type: 'integer', description: '页码,从 1 开始', minimum: 1 },
|
|
30
|
+
pageSize: { type: 'integer', description: '每页条数,建议 1-100', minimum: 1, maximum: 100 },
|
|
31
|
+
name: { type: 'string', description: '应用名称(模糊)' },
|
|
32
|
+
groupId: { type: 'integer', description: '应用分组 ID' },
|
|
33
|
+
id: { type: 'integer', description: '应用主键' },
|
|
34
|
+
delFlag: { type: 'integer', description: '删除标识:0 正常,1 已删除', enum: [0, 1] }
|
|
35
|
+
},
|
|
36
|
+
required: ['pageNum', 'pageSize']
|
|
37
|
+
},
|
|
38
|
+
build: (a) => ({ method: 'GET', path: '/ent/application/page', query: pick(a, ['pageNum', 'pageSize', 'name', 'groupId', 'id', 'delFlag']) })
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'get_application',
|
|
42
|
+
description: '按应用 ID 查询应用详情(名称、分组、可见范围、描述等)。只读。',
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: { id: { type: 'integer', description: '应用主键 ID' } },
|
|
46
|
+
required: ['id']
|
|
47
|
+
},
|
|
48
|
+
build: (a) => ({ method: 'GET', path: '/ent/application/detail', query: { id: requireInt(a.id, 'id') } })
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'list_application_groups',
|
|
52
|
+
description: '分页查询应用分组列表。只读。',
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
pageNum: { type: 'integer', description: '页码,从 1 开始', minimum: 1 },
|
|
57
|
+
pageSize: { type: 'integer', description: '每页条数', minimum: 1, maximum: 100 }
|
|
58
|
+
},
|
|
59
|
+
required: ['pageNum', 'pageSize']
|
|
60
|
+
},
|
|
61
|
+
build: (a) => ({ method: 'GET', path: '/ent/application/group/page', query: pick(a, ['pageNum', 'pageSize']) })
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'query_form_data',
|
|
65
|
+
description: '按表单 ID 分页查询业务数据(表单记录)。type 决定视图类型。只读。',
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: 'object',
|
|
68
|
+
properties: {
|
|
69
|
+
type: { type: 'integer', description: '类型:1 PC表格,2 移动端表格,3 消息通知,4 流程中心', enum: [1, 2, 3, 4] },
|
|
70
|
+
djid: { type: 'integer', description: '表单 ID' },
|
|
71
|
+
pageNum: { type: 'integer', description: '页码,从 1 开始', minimum: 1 },
|
|
72
|
+
pageSize: { type: 'integer', description: '每页条数', minimum: 1, maximum: 100 },
|
|
73
|
+
keyword: { type: 'string', description: '模糊检索词' },
|
|
74
|
+
paramStr: { type: 'string', description: '查询条件字符串(平台原生格式)' },
|
|
75
|
+
orderByField: { type: 'string', description: '排序字段,默认 id' },
|
|
76
|
+
isDesc: { type: 'integer', description: '1 正序,2 倒序', enum: [1, 2] }
|
|
77
|
+
},
|
|
78
|
+
required: ['type', 'pageNum', 'pageSize']
|
|
79
|
+
},
|
|
80
|
+
build: (a) => {
|
|
81
|
+
const type = requireInt(a.type, 'type');
|
|
82
|
+
if (![1, 2, 3, 4].includes(type)) throw new ToolError('type 必须是 1/2/3/4');
|
|
83
|
+
return {
|
|
84
|
+
method: 'GET',
|
|
85
|
+
path: `/ent/application/auth/form/data/page/${type}`,
|
|
86
|
+
query: pick(a, ['djid', 'pageNum', 'pageSize', 'keyword', 'paramStr', 'orderByField', 'isDesc'])
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'get_form_data_detail',
|
|
92
|
+
description: '按表单 ID 与记录主键查询单条业务数据的字段明细。只读。',
|
|
93
|
+
inputSchema: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
properties: {
|
|
96
|
+
djid: { type: 'integer', description: '表单 ID' },
|
|
97
|
+
id: { type: 'integer', description: '记录主键 ID' },
|
|
98
|
+
fields: { type: 'string', description: '需要返回的展示字段,逗号分隔' },
|
|
99
|
+
authParam: { type: 'string', description: '权限参数(平台原生)' }
|
|
100
|
+
},
|
|
101
|
+
required: ['id']
|
|
102
|
+
},
|
|
103
|
+
build: (a) => ({ method: 'GET', path: '/ent/application/auth/form/data/detail', query: pick(a, ['djid', 'id', 'fields', 'authParam']) })
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'list_form_display_fields',
|
|
107
|
+
description: '查询某表单在表格视图中的展示列(字段名、标题、类型)。常用于为 query_form_data 准备字段。只读。',
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: { djid: { type: 'integer', description: '表单 ID' } },
|
|
111
|
+
required: ['djid']
|
|
112
|
+
},
|
|
113
|
+
build: (a) => ({ method: 'GET', path: `/ent/application/auth/form/table/field/get/${requireInt(a.djid, 'djid')}`, query: {} })
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'list_process_categories',
|
|
117
|
+
description: '查询流程分类列表,可按分类名称过滤。只读。',
|
|
118
|
+
inputSchema: {
|
|
119
|
+
type: 'object',
|
|
120
|
+
properties: { categoryName: { type: 'string', description: '流程分类名称' } }
|
|
121
|
+
},
|
|
122
|
+
build: (a) => ({ method: 'GET', path: '/category/list', query: pick(a, ['categoryName']) })
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'list_business_rules',
|
|
126
|
+
description: '查询某表单配置的业务规则列表,可按状态、名称、编码过滤。只读。',
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
djId: { type: 'integer', description: '表单 ID' },
|
|
131
|
+
name: { type: 'string', description: '业务规则名称' },
|
|
132
|
+
code: { type: 'string', description: '业务规则编码' },
|
|
133
|
+
status: { type: 'integer', description: '状态:0 启用,1 停用', enum: [0, 1] }
|
|
134
|
+
},
|
|
135
|
+
required: ['djId']
|
|
136
|
+
},
|
|
137
|
+
build: (a) => ({ method: 'GET', path: `/business/rule/list/${requireInt(a.djId, 'djId')}`, query: pick(a, ['name', 'code', 'status']) })
|
|
138
|
+
}
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
class ToolError extends Error {}
|
|
142
|
+
|
|
143
|
+
function pick(obj, keys) {
|
|
144
|
+
const out = {};
|
|
145
|
+
for (const key of keys) {
|
|
146
|
+
const value = obj[key];
|
|
147
|
+
if (value === undefined || value === null || value === '') continue;
|
|
148
|
+
out[key] = value;
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function requireInt(value, name) {
|
|
154
|
+
const n = Number(value);
|
|
155
|
+
if (!Number.isInteger(n) || n < 0) throw new ToolError(`${name} 必须是非负整数`);
|
|
156
|
+
return n;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function callApi(spec) {
|
|
160
|
+
if (!API_BASE_URL) throw new ToolError('未配置 API_BASE_URL,请在连接器设置中填写 API 服务地址');
|
|
161
|
+
if (!AUTH_TOKEN) throw new ToolError('未配置 AUTH_TOKEN,请在连接器设置中填写访问凭证');
|
|
162
|
+
|
|
163
|
+
let base;
|
|
164
|
+
try {
|
|
165
|
+
base = new URL(API_BASE_URL);
|
|
166
|
+
} catch {
|
|
167
|
+
throw new ToolError('API_BASE_URL 不是合法 URL');
|
|
168
|
+
}
|
|
169
|
+
if (base.protocol !== 'http:' && base.protocol !== 'https:') {
|
|
170
|
+
throw new ToolError('API_BASE_URL 只支持 http/https');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const url = new URL(base.pathname.replace(/\/+$/, '') + spec.path, base.origin);
|
|
174
|
+
for (const [key, value] of Object.entries(spec.query || {})) {
|
|
175
|
+
if (Array.isArray(value)) {
|
|
176
|
+
for (const item of value) url.searchParams.append(key, String(item));
|
|
177
|
+
} else {
|
|
178
|
+
url.searchParams.set(key, String(value));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const controller = new AbortController();
|
|
183
|
+
const timer = setTimeout(() => controller.abort(), 25000);
|
|
184
|
+
let response;
|
|
185
|
+
try {
|
|
186
|
+
response = await fetch(url, {
|
|
187
|
+
method: spec.method,
|
|
188
|
+
headers: {
|
|
189
|
+
'Authorization': /^Bearer\s+/i.test(AUTH_TOKEN) ? AUTH_TOKEN : 'Bearer ' + AUTH_TOKEN,
|
|
190
|
+
'Accept': 'application/json'
|
|
191
|
+
},
|
|
192
|
+
signal: controller.signal
|
|
193
|
+
});
|
|
194
|
+
} catch (err) {
|
|
195
|
+
if (err?.name === 'AbortError') throw new ToolError('请求超时(25 秒),请检查 API 地址或网络可达性');
|
|
196
|
+
throw new ToolError(`请求失败:${err?.message || 'unknown error'}`);
|
|
197
|
+
} finally {
|
|
198
|
+
clearTimeout(timer);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const text = await response.text();
|
|
202
|
+
if (response.status === 401 || response.status === 403) {
|
|
203
|
+
throw new ToolError('凭证无效或已过期(HTTP 401/403),请在连接器设置中更新访问凭证后重试');
|
|
204
|
+
}
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
throw new ToolError(`服务端返回 HTTP ${response.status}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let payload;
|
|
210
|
+
try {
|
|
211
|
+
payload = JSON.parse(text);
|
|
212
|
+
} catch {
|
|
213
|
+
payload = text;
|
|
214
|
+
}
|
|
215
|
+
// Platform-level business errors are also surfaced to the model.
|
|
216
|
+
if (payload && typeof payload === 'object' && payload.success === false) {
|
|
217
|
+
throw new ToolError(`接口返回失败:code=${payload.code ?? '?'} msg=${payload.msg ?? '(无说明)'}`);
|
|
218
|
+
}
|
|
219
|
+
const serialized = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2);
|
|
220
|
+
return serialized.length > MAX_RESPONSE_CHARS
|
|
221
|
+
? serialized.slice(0, MAX_RESPONSE_CHARS) + '\n...[响应已截断,请缩小 pageSize 或增加过滤条件]'
|
|
222
|
+
: serialized;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function toolDefinitions() {
|
|
226
|
+
return TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function send(message) {
|
|
230
|
+
process.stdout.write(JSON.stringify(message) + '\n');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function ok(id, result) {
|
|
234
|
+
send({ jsonrpc: '2.0', id, result });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function fail(id, code, message) {
|
|
238
|
+
send({ jsonrpc: '2.0', id, error: { code, message } });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function handle(message) {
|
|
242
|
+
if (!message || typeof message !== 'object') return;
|
|
243
|
+
const { id, method, params } = message;
|
|
244
|
+
|
|
245
|
+
switch (method) {
|
|
246
|
+
case 'initialize':
|
|
247
|
+
ok(id, {
|
|
248
|
+
protocolVersion: params?.protocolVersion || '2024-11-05',
|
|
249
|
+
capabilities: { tools: { listChanged: false } },
|
|
250
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
|
|
251
|
+
});
|
|
252
|
+
return;
|
|
253
|
+
case 'notifications/initialized':
|
|
254
|
+
case 'notifications/cancelled':
|
|
255
|
+
return; // notification: no response
|
|
256
|
+
case 'ping':
|
|
257
|
+
ok(id, {});
|
|
258
|
+
return;
|
|
259
|
+
case 'tools/list':
|
|
260
|
+
ok(id, { tools: toolDefinitions() });
|
|
261
|
+
return;
|
|
262
|
+
case 'resources/list':
|
|
263
|
+
ok(id, { resources: [] });
|
|
264
|
+
return;
|
|
265
|
+
case 'prompts/list':
|
|
266
|
+
ok(id, { prompts: [] });
|
|
267
|
+
return;
|
|
268
|
+
case 'tools/call': {
|
|
269
|
+
const name = params?.name;
|
|
270
|
+
const tool = TOOLS.find((t) => t.name === name);
|
|
271
|
+
if (!tool) {
|
|
272
|
+
fail(id, -32602, `未知工具:${name}`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const args = params?.arguments && typeof params.arguments === 'object' ? params.arguments : {};
|
|
276
|
+
try {
|
|
277
|
+
const spec = tool.build(args);
|
|
278
|
+
const text = await callApi(spec);
|
|
279
|
+
ok(id, { content: [{ type: 'text', text }], isError: false });
|
|
280
|
+
} catch (err) {
|
|
281
|
+
const message = err instanceof ToolError ? err.message : `调用失败:${err?.message || 'unknown error'}`;
|
|
282
|
+
ok(id, { content: [{ type: 'text', text: message }], isError: true });
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
default:
|
|
287
|
+
if (id !== undefined) fail(id, -32601, `不支持的方法:${method}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
diag(`started (node ${process.version}, api_configured=${Boolean(API_BASE_URL)}, token_configured=${Boolean(AUTH_TOKEN)})`);
|
|
292
|
+
|
|
293
|
+
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
294
|
+
rl.on('line', (line) => {
|
|
295
|
+
if (!line.trim()) return;
|
|
296
|
+
let message;
|
|
297
|
+
try {
|
|
298
|
+
message = JSON.parse(line);
|
|
299
|
+
} catch {
|
|
300
|
+
diag('dropped a malformed JSON-RPC line');
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
Promise.resolve(handle(message)).catch((err) => {
|
|
304
|
+
diag(`handler error: ${err?.message || 'unknown'}`);
|
|
305
|
+
if (message?.id !== undefined) fail(message.id, -32603, '内部错误');
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
rl.on('close', () => diag('stdin closed, exiting'));
|