worsoft-frontend-codegen-mcp 0.1.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.
Files changed (3) hide show
  1. package/README.md +69 -0
  2. package/mcp_server.js +418 -0
  3. package/package.json +35 -0
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # worsoft-frontend-codegen-mcp
2
+
3
+ Worsoft frontend code generation MCP server.
4
+
5
+ This package exposes a stdio MCP server with one tool:
6
+
7
+ - `worsoft_codegen_generate_frontend`
8
+
9
+ The tool delegates to a running Worsoft code generation service and supports:
10
+
11
+ - single-table generation by `tableIds`
12
+ - single-table generation by `dsName + tableName`
13
+ - master-child template generation by `style + childTableName + mainField + childField`
14
+ - render-only mode and write-to-disk mode
15
+
16
+ ## Requirements
17
+
18
+ - Node.js 18+
19
+ - A running Worsoft code generation backend reachable from this machine
20
+
21
+ ## Environment variables
22
+
23
+ - `WORSOFT_CODEGEN_BASE_URL`
24
+ - `WORSOFT_CODEGEN_EXECUTE_PATH`
25
+ - `WORSOFT_CODEGEN_EXECUTE_URL`
26
+
27
+ If `WORSOFT_CODEGEN_EXECUTE_URL` is set, it takes priority.
28
+
29
+ ## MCP configuration example
30
+
31
+ ```json
32
+ {
33
+ "mcpServers": {
34
+ "worsoft-codegen": {
35
+ "command": "npx",
36
+ "args": [
37
+ "-y",
38
+ "worsoft-frontend-codegen-mcp"
39
+ ],
40
+ "env": {
41
+ "WORSOFT_CODEGEN_BASE_URL": "http://127.0.0.1:9999",
42
+ "WORSOFT_CODEGEN_EXECUTE_PATH": "/admin/generator/mcp/execute"
43
+ }
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ ## Tool arguments
50
+
51
+ - `tableIds`
52
+ - `dsName`
53
+ - `tableName`
54
+ - `style`
55
+ - `childTableName`
56
+ - `mainField`
57
+ - `childField`
58
+ - `frontendPath`
59
+ - `baseUrl`
60
+ - `executeUrl`
61
+ - `overwrite`
62
+ - `writeToDisk`
63
+
64
+ ## Notes
65
+
66
+ - Pass 64-bit ids such as `tableIds` and `style` as strings when possible.
67
+ - `frontendPath` must be an absolute path.
68
+ - This package does not include the Worsoft backend service. It only wraps the MCP transport and forwards requests to your running Worsoft service.
69
+
package/mcp_server.js ADDED
@@ -0,0 +1,418 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const http = require('http');
5
+ const https = require('https');
6
+ const { URL } = require('url');
7
+
8
+ const SERVER_NAME = 'worsoft-codegen';
9
+ const SERVER_VERSION = '0.5.1';
10
+ const PROTOCOL_VERSION = '2024-11-05';
11
+ const TOOL_NAME = 'worsoft_codegen_generate_frontend';
12
+ const DEFAULT_BASE_URL = process.env.WORSOFT_CODEGEN_BASE_URL || 'http://127.0.0.1:5003';
13
+ const DEFAULT_EXECUTE_PATH = process.env.WORSOFT_CODEGEN_EXECUTE_PATH || '/admin/generator/mcp/execute';
14
+ const DEFAULT_EXECUTE_URL = process.env.WORSOFT_CODEGEN_EXECUTE_URL || '';
15
+ const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER;
16
+
17
+ const TOOL_SCHEMA = {
18
+ type: 'object',
19
+ properties: {
20
+ tableIds: {
21
+ type: 'array',
22
+ items: {
23
+ oneOf: [
24
+ { type: 'string', pattern: '^[0-9]+$' },
25
+ { type: 'integer' }
26
+ ]
27
+ },
28
+ minItems: 1,
29
+ description: 'Optional. Generate by table config ids. Prefer strings for 64-bit ids.'
30
+ },
31
+ dsName: {
32
+ type: 'string',
33
+ description: 'Optional data source name when generating by table name.'
34
+ },
35
+ tableName: {
36
+ type: 'string',
37
+ description: 'Optional main table name when generating by table name.'
38
+ },
39
+ style: {
40
+ oneOf: [
41
+ { type: 'string', pattern: '^[0-9]+$' },
42
+ { type: 'integer' }
43
+ ],
44
+ description: 'Optional template group id. Prefer string for 64-bit ids.'
45
+ },
46
+ childTableName: {
47
+ type: 'string',
48
+ description: 'Optional child table name for master-child templates.'
49
+ },
50
+ mainField: {
51
+ type: 'string',
52
+ description: 'Optional relation field on the main table.'
53
+ },
54
+ childField: {
55
+ type: 'string',
56
+ description: 'Optional relation field on the child table.'
57
+ },
58
+ frontendPath: {
59
+ type: 'string',
60
+ description: 'Absolute frontend output root path.'
61
+ },
62
+ baseUrl: {
63
+ type: 'string',
64
+ description: 'Optional base service url, for example http://127.0.0.1:9999.'
65
+ },
66
+ executeUrl: {
67
+ type: 'string',
68
+ description: 'Optional full execute url. Higher priority than baseUrl.'
69
+ },
70
+ overwrite: {
71
+ type: 'boolean',
72
+ default: true,
73
+ description: 'Whether to overwrite existing files.'
74
+ },
75
+ writeToDisk: {
76
+ type: 'boolean',
77
+ default: true,
78
+ description: 'Whether to write files. False means render only.'
79
+ }
80
+ },
81
+ required: ['frontendPath'],
82
+ anyOf: [
83
+ { required: ['tableIds'] },
84
+ { required: ['dsName', 'tableName'] }
85
+ ],
86
+ additionalProperties: false
87
+ };
88
+
89
+ function writeMessage(payload) {
90
+ const body = Buffer.from(JSON.stringify(payload), 'utf8');
91
+ process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
92
+ process.stdout.write(body);
93
+ }
94
+
95
+ function successResponse(id, result) {
96
+ return { jsonrpc: '2.0', id, result };
97
+ }
98
+
99
+ function errorResponse(id, code, message) {
100
+ return { jsonrpc: '2.0', id, error: { code, message } };
101
+ }
102
+
103
+ function toolErrorResult(message) {
104
+ return {
105
+ content: [{ type: 'text', text: message }],
106
+ isError: true
107
+ };
108
+ }
109
+
110
+ function toolResultFromResponse(response) {
111
+ const text = JSON.stringify(response, null, 2);
112
+ return {
113
+ content: [{ type: 'text', text }],
114
+ structuredContent: response,
115
+ isError: false
116
+ };
117
+ }
118
+
119
+ function trimOptionalString(value, name) {
120
+ if (value === undefined || value === null) {
121
+ return null;
122
+ }
123
+ if (typeof value !== 'string' || !value.trim()) {
124
+ throw new Error(`${name} must be a non-empty string`);
125
+ }
126
+ return value.trim();
127
+ }
128
+
129
+ function ensureHttpUrl(value, name) {
130
+ if (!value) {
131
+ return null;
132
+ }
133
+ let parsed;
134
+ try {
135
+ parsed = new URL(value);
136
+ } catch {
137
+ throw new Error(`${name} must be a valid HTTP URL`);
138
+ }
139
+ if (!['http:', 'https:'].includes(parsed.protocol) || !parsed.host) {
140
+ throw new Error(`${name} must be a valid HTTP URL`);
141
+ }
142
+ return value;
143
+ }
144
+
145
+ function normalizeId(value, name) {
146
+ if (value === undefined || value === null) {
147
+ return null;
148
+ }
149
+ if (typeof value === 'string') {
150
+ const trimmed = value.trim();
151
+ if (!/^\d+$/.test(trimmed)) {
152
+ throw new Error(`${name} must contain digits only`);
153
+ }
154
+ return trimmed;
155
+ }
156
+ if (typeof value === 'number') {
157
+ if (!Number.isInteger(value)) {
158
+ throw new Error(`${name} must be an integer or digit string`);
159
+ }
160
+ if (!Number.isSafeInteger(value)) {
161
+ throw new Error(`${name} exceeds JS safe integer range; pass it as a string`);
162
+ }
163
+ return String(value);
164
+ }
165
+ throw new Error(`${name} must be an integer or digit string`);
166
+ }
167
+
168
+ function normalizeIdList(value) {
169
+ if (value === undefined || value === null) {
170
+ return null;
171
+ }
172
+ if (!Array.isArray(value) || value.length === 0) {
173
+ throw new Error('tableIds must be a non-empty array');
174
+ }
175
+ return value.map((item, index) => normalizeId(item, `tableIds[${index}]`));
176
+ }
177
+
178
+ function validateArguments(argumentsObject) {
179
+ if (!argumentsObject || typeof argumentsObject !== 'object' || Array.isArray(argumentsObject)) {
180
+ throw new Error('arguments must be an object');
181
+ }
182
+ const tableIds = normalizeIdList(argumentsObject.tableIds);
183
+ const dsName = trimOptionalString(argumentsObject.dsName, 'dsName');
184
+ const tableName = trimOptionalString(argumentsObject.tableName, 'tableName');
185
+ if ((!tableIds || tableIds.length === 0) && (!dsName || !tableName)) {
186
+ throw new Error('Provide tableIds, or provide both dsName and tableName');
187
+ }
188
+ const style = normalizeId(argumentsObject.style, 'style');
189
+ const frontendPath = trimOptionalString(argumentsObject.frontendPath, 'frontendPath');
190
+ const baseUrl = ensureHttpUrl(trimOptionalString(argumentsObject.baseUrl, 'baseUrl'), 'baseUrl');
191
+ const executeUrl = ensureHttpUrl(trimOptionalString(argumentsObject.executeUrl, 'executeUrl'), 'executeUrl');
192
+ const childTableName = trimOptionalString(argumentsObject.childTableName, 'childTableName');
193
+ const mainField = trimOptionalString(argumentsObject.mainField, 'mainField');
194
+ const childField = trimOptionalString(argumentsObject.childField, 'childField');
195
+ const overwrite = argumentsObject.overwrite === undefined ? true : argumentsObject.overwrite;
196
+ const writeToDisk = argumentsObject.writeToDisk === undefined ? true : argumentsObject.writeToDisk;
197
+ if (typeof overwrite !== 'boolean') {
198
+ throw new Error('overwrite must be a boolean');
199
+ }
200
+ if (typeof writeToDisk !== 'boolean') {
201
+ throw new Error('writeToDisk must be a boolean');
202
+ }
203
+ return {
204
+ tableIds,
205
+ dsName,
206
+ tableName,
207
+ style,
208
+ childTableName,
209
+ mainField,
210
+ childField,
211
+ frontendPath,
212
+ baseUrl,
213
+ executeUrl,
214
+ overwrite,
215
+ writeToDisk
216
+ };
217
+ }
218
+
219
+ function resolveExecuteUrl(argumentsObject) {
220
+ if (argumentsObject.executeUrl) {
221
+ return argumentsObject.executeUrl;
222
+ }
223
+ if (argumentsObject.baseUrl) {
224
+ return argumentsObject.baseUrl.replace(/\/+$/, '') + DEFAULT_EXECUTE_PATH;
225
+ }
226
+ if (DEFAULT_EXECUTE_URL) {
227
+ return DEFAULT_EXECUTE_URL;
228
+ }
229
+ return DEFAULT_BASE_URL.replace(/\/+$/, '') + DEFAULT_EXECUTE_PATH;
230
+ }
231
+
232
+ function callExecute(argumentsObject) {
233
+ const executeUrl = resolveExecuteUrl(argumentsObject);
234
+ const payload = {
235
+ frontendPath: argumentsObject.frontendPath,
236
+ overwrite: argumentsObject.overwrite,
237
+ writeToDisk: argumentsObject.writeToDisk
238
+ };
239
+ if (argumentsObject.tableIds) {
240
+ payload.tableIds = argumentsObject.tableIds;
241
+ }
242
+ if (argumentsObject.dsName && argumentsObject.tableName) {
243
+ payload.dsName = argumentsObject.dsName;
244
+ payload.tableName = argumentsObject.tableName;
245
+ }
246
+ if (argumentsObject.style !== null) {
247
+ payload.style = argumentsObject.style;
248
+ }
249
+ for (const key of ['childTableName', 'mainField', 'childField']) {
250
+ if (argumentsObject[key]) {
251
+ payload[key] = argumentsObject[key];
252
+ }
253
+ }
254
+ const parsedUrl = new URL(executeUrl);
255
+ const transport = parsedUrl.protocol === 'https:' ? https : http;
256
+ const body = Buffer.from(JSON.stringify(payload), 'utf8');
257
+
258
+ return new Promise((resolve, reject) => {
259
+ const req = transport.request(
260
+ {
261
+ protocol: parsedUrl.protocol,
262
+ hostname: parsedUrl.hostname,
263
+ port: parsedUrl.port || undefined,
264
+ path: `${parsedUrl.pathname}${parsedUrl.search}`,
265
+ method: 'POST',
266
+ headers: {
267
+ 'Content-Type': 'application/json; charset=utf-8',
268
+ 'Content-Length': String(body.length)
269
+ },
270
+ timeout: 60000
271
+ },
272
+ (res) => {
273
+ const chunks = [];
274
+ res.on('data', (chunk) => chunks.push(chunk));
275
+ res.on('end', () => {
276
+ const text = Buffer.concat(chunks).toString('utf8');
277
+ if ((res.statusCode || 0) < 200 || (res.statusCode || 0) >= 300) {
278
+ reject(new Error(`HTTP ${res.statusCode}: ${text}`));
279
+ return;
280
+ }
281
+ try {
282
+ resolve({
283
+ executeUrl,
284
+ data: JSON.parse(text)
285
+ });
286
+ } catch (error) {
287
+ reject(new Error(`Response is not valid JSON: ${error.message}`));
288
+ }
289
+ });
290
+ }
291
+ );
292
+
293
+ req.on('timeout', () => {
294
+ req.destroy(new Error(`Request to ${executeUrl} timed out`));
295
+ });
296
+ req.on('error', (error) => {
297
+ reject(new Error(`Failed to reach ${executeUrl}: ${error.message}`));
298
+ });
299
+ req.write(body);
300
+ req.end();
301
+ });
302
+ }
303
+
304
+ async function handleRequest(message) {
305
+ const method = message.method;
306
+ const messageId = message.id;
307
+ const params = message.params || {};
308
+
309
+ if (method === 'initialize') {
310
+ return successResponse(messageId, {
311
+ protocolVersion: PROTOCOL_VERSION,
312
+ capabilities: { tools: { listChanged: false } },
313
+ serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
314
+ });
315
+ }
316
+
317
+ if (method === 'notifications/initialized') {
318
+ return null;
319
+ }
320
+
321
+ if (method === 'ping') {
322
+ return successResponse(messageId, {});
323
+ }
324
+
325
+ if (method === 'tools/list') {
326
+ return successResponse(messageId, {
327
+ tools: [
328
+ {
329
+ name: TOOL_NAME,
330
+ description: 'Generate Worsoft frontend files and return a per-file manifest. Supports tableIds or dsName plus tableName, including master-child templates.',
331
+ inputSchema: TOOL_SCHEMA
332
+ }
333
+ ]
334
+ });
335
+ }
336
+
337
+ if (method === 'tools/call') {
338
+ if (params.name !== TOOL_NAME) {
339
+ return successResponse(messageId, toolErrorResult(`Unknown tool: ${params.name}`));
340
+ }
341
+ try {
342
+ const argumentsObject = validateArguments(params.arguments || {});
343
+ const response = await callExecute(argumentsObject);
344
+ return successResponse(messageId, toolResultFromResponse(response));
345
+ } catch (error) {
346
+ return successResponse(messageId, toolErrorResult(error.message));
347
+ }
348
+ }
349
+
350
+ return errorResponse(messageId, -32601, `Method not found: ${method}`);
351
+ }
352
+
353
+ let buffer = Buffer.alloc(0);
354
+ let processing = false;
355
+
356
+ async function drainBuffer() {
357
+ if (processing) {
358
+ return;
359
+ }
360
+ processing = true;
361
+ try {
362
+ while (true) {
363
+ const separator = buffer.indexOf('\r\n\r\n');
364
+ if (separator === -1) {
365
+ return;
366
+ }
367
+ const headerText = buffer.subarray(0, separator).toString('utf8');
368
+ const headers = {};
369
+ for (const line of headerText.split('\r\n')) {
370
+ if (!line.trim()) {
371
+ continue;
372
+ }
373
+ const index = line.indexOf(':');
374
+ if (index === -1) {
375
+ continue;
376
+ }
377
+ headers[line.slice(0, index).trim().toLowerCase()] = line.slice(index + 1).trim();
378
+ }
379
+ const contentLength = Number(headers['content-length'] || '0');
380
+ if (!Number.isFinite(contentLength) || contentLength < 0) {
381
+ throw new Error('Missing valid Content-Length');
382
+ }
383
+ const totalLength = separator + 4 + contentLength;
384
+ if (buffer.length < totalLength) {
385
+ return;
386
+ }
387
+ const body = buffer.subarray(separator + 4, totalLength).toString('utf8');
388
+ buffer = buffer.subarray(totalLength);
389
+ const message = JSON.parse(body);
390
+ const response = await handleRequest(message);
391
+ if (response) {
392
+ writeMessage(response);
393
+ }
394
+ }
395
+ } finally {
396
+ processing = false;
397
+ if (buffer.length > 0) {
398
+ setImmediate(() => {
399
+ drainBuffer().catch((error) => {
400
+ console.error(error.stack || String(error));
401
+ process.exit(1);
402
+ });
403
+ });
404
+ }
405
+ }
406
+ }
407
+
408
+ process.stdin.on('data', (chunk) => {
409
+ buffer = Buffer.concat([buffer, chunk]);
410
+ drainBuffer().catch((error) => {
411
+ console.error(error.stack || String(error));
412
+ process.exit(1);
413
+ });
414
+ });
415
+
416
+ process.stdin.on('end', () => {
417
+ process.exit(0);
418
+ });
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "worsoft-frontend-codegen-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Worsoft frontend code generation MCP server.",
5
+ "license": "UNLICENSED",
6
+ "author": "worsoft <sw@worsoft.vip>",
7
+ "homepage": "https://www.worsoft.com",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://www.worsoft.com"
11
+ },
12
+ "bugs": {
13
+ "url": "https://www.worsoft.com"
14
+ },
15
+ "keywords": [
16
+ "mcp",
17
+ "worsoft",
18
+ "codegen",
19
+ "frontend"
20
+ ],
21
+ "type": "commonjs",
22
+ "bin": {
23
+ "worsoft-frontend-codegen-mcp": "mcp_server.js"
24
+ },
25
+ "files": [
26
+ "mcp_server.js",
27
+ "README.md"
28
+ ],
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ }
35
+ }