@leju-gym/gym-mcp 0.0.1

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/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@leju-gym/gym-mcp",
3
+ "version": "0.0.1",
4
+ "description": "Gym MCP Server — 通过 MCP 协议调用 gym CLI,AI 助手可直接查询 KuavoDataHub 项目列表等数据",
5
+ "bin": {
6
+ "gym-mcp": "src/mcp.js"
7
+ },
8
+ "files": [
9
+ "src/"
10
+ ],
11
+ "scripts": {
12
+ "mcp": "node src/mcp.js"
13
+ },
14
+ "keywords": [
15
+ "mcp",
16
+ "gym",
17
+ "kuavo",
18
+ "robotics",
19
+ "data-management"
20
+ ],
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://gitlab.lejurobot.com/kuavo/kuavodatahubai.git",
25
+ "directory": "projects/gym-mcp"
26
+ },
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.29.0",
29
+ "log4js": "^6.9.1"
30
+ }
31
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @ModuleName: AppHelper
3
+ * @ModuleDesc: 日志组件(log4js + console 兜底)
4
+ */
5
+
6
+ const path = require("path");
7
+ const log4js = require("log4js");
8
+
9
+ class AppHelper {
10
+ _log;
11
+
12
+ constructor() {
13
+ try {
14
+ log4js.configure(path.join(__dirname, "log4js.json"));
15
+ this._log = log4js.getLogger();
16
+ } catch (e) {
17
+ this._log = {
18
+ error: (...args) => console.error(...args),
19
+ warn: (...args) => console.warn(...args),
20
+ info: (...args) => console.log(...args),
21
+ debug: (...args) => console.debug(...args),
22
+ };
23
+ }
24
+ }
25
+
26
+ error(...data) { this._log?.error(...data); }
27
+ warn(...data) { this._log?.warn(...data); }
28
+ info(...data) { this._log?.info(...data); }
29
+ debug(...data) { this._log?.debug(...data); }
30
+ }
31
+
32
+ module.exports = new AppHelper();
@@ -0,0 +1,19 @@
1
+ {
2
+ "appenders": {
3
+ "console": { "type": "console" },
4
+ "file": {
5
+ "type": "dateFile",
6
+ "filename": "logs/gym-mcp.log",
7
+ "pattern": "yyyy-MM-dd",
8
+ "keepFileExt": true,
9
+ "numBackups": 30,
10
+ "compress": true
11
+ }
12
+ },
13
+ "categories": {
14
+ "default": {
15
+ "appenders": ["console", "file"],
16
+ "level": "debug"
17
+ }
18
+ }
19
+ }
package/src/mcp.js ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @ModuleName: mcp
4
+ * @ModuleMethod: src
5
+ * @ModuleUsage: MCP 协议入口(stdio 传输)
6
+ * @ModuleDesc: 通过标准输入输出与 MCP 客户端通信,
7
+ * 注册 ToolService 中的所有工具
8
+ */
9
+
10
+ const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
11
+ const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
12
+ const {
13
+ CallToolRequestSchema,
14
+ ListToolsRequestSchema,
15
+ } = require("@modelcontextprotocol/sdk/types.js");
16
+ const toolService = require("./services/mcp/ToolService");
17
+ const appHelper = require("./AppHelper");
18
+
19
+ const VERSION = "0.0.1";
20
+
21
+ async function main() {
22
+ const server = new Server({ name: "gym-mcp", version: VERSION }, { capabilities: { tools: {} } });
23
+
24
+ // tools/list
25
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
26
+ return { tools: toolService.listTools() };
27
+ });
28
+
29
+ // tools/call
30
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
31
+ const { name, arguments: args } = request.params;
32
+ appHelper.info("mcp call", name, JSON.stringify(args));
33
+ const result = await toolService.callTool(name, args || {});
34
+ return {
35
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
36
+ };
37
+ });
38
+
39
+ const transport = new StdioServerTransport();
40
+ await server.connect(transport);
41
+ appHelper.info("gym-mcp MCP server started");
42
+ }
43
+
44
+ main().catch((e) => {
45
+ console.error("MCP server fatal:", e);
46
+ process.exit(1);
47
+ });
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @ModuleName: ToolService
3
+ * @ModuleMethod: src/services/mcp
4
+ * @ModuleUsage: MCP 工具注册与调用服务
5
+ * @ModuleDesc: 通过 spawn gym CLI 二进制 + --json 模式调用本地 gym 工具,
6
+ * 逻辑零重复,认证完全复用 gym config login
7
+ */
8
+
9
+ const appHelper = require('../../AppHelper');
10
+ const { execFile } = require('child_process');
11
+ const { promisify } = require('util');
12
+ const execFileAsync = promisify(execFile);
13
+
14
+ // ─── Gym CLI 调用 ─────────────────────────────────────────────
15
+
16
+ function gymBin() {
17
+ return process.env.GYM_BIN || 'gym';
18
+ }
19
+
20
+ async function runGym(args = []) {
21
+ const bin = gymBin();
22
+ appHelper.info('runGym', bin, args.join(' '));
23
+ try {
24
+ const { stdout, stderr } = await execFileAsync(bin, args, {
25
+ timeout: 30_000,
26
+ maxBuffer: 1024 * 1024,
27
+ });
28
+ if (stderr) appHelper.warn('runGym stderr', stderr);
29
+ return JSON.parse(stdout);
30
+ } catch (e) {
31
+ if (e.code === 'ENOENT') {
32
+ return { error: 'gym CLI 未安装或不在 PATH 中' };
33
+ }
34
+ // gym --json 模式下错误也走 stdout JSON
35
+ try { return JSON.parse(e.stdout || ''); } catch {}
36
+ return { error: e.stderr || e.message };
37
+ }
38
+ }
39
+
40
+ // ─── 工具定义 ──────────────────────────────────────────────────
41
+
42
+ const toolDefinitions = [
43
+ {
44
+ name: 'echo',
45
+ description: '回显输入内容,用于测试连通性',
46
+ inputSchema: {
47
+ type: 'object',
48
+ properties: {
49
+ message: { type: 'string', description: '要回显的消息' },
50
+ },
51
+ required: ['message'],
52
+ },
53
+ },
54
+ {
55
+ name: 'gym_project_list',
56
+ description:
57
+ '查询组织及其下属项目列表(调用本地 gym CLI,复用已登录的认证状态)。返回每个组织下的所有项目 ID、名称、编码。',
58
+ inputSchema: {
59
+ type: 'object',
60
+ properties: {
61
+ page: { type: 'number', description: '页码,默认 1' },
62
+ limit: { type: 'number', description: '每页数量,默认 10' },
63
+ },
64
+ },
65
+ },
66
+ ];
67
+
68
+ // ─── ToolService ────────────────────────────────────────────────
69
+
70
+ class ToolService {
71
+ listTools() {
72
+ return toolDefinitions.map(t => ({
73
+ name: t.name,
74
+ description: t.description,
75
+ inputSchema: t.inputSchema,
76
+ }));
77
+ }
78
+
79
+ async callTool(toolName, args) {
80
+ const tool = toolDefinitions.find(t => t.name === toolName);
81
+ if (!tool) {
82
+ return { ok: false, message: `Unknown tool: ${toolName}` };
83
+ }
84
+
85
+ try {
86
+ appHelper.info('callTool', toolName, JSON.stringify(args));
87
+
88
+ switch (toolName) {
89
+ case 'echo':
90
+ return { ok: true, data: { echo: args.message || '' } };
91
+
92
+ case 'gym_project_list': {
93
+ const page = args.page || 1;
94
+ const limit = args.limit || 10;
95
+ const result = await runGym([
96
+ 'project', 'list',
97
+ '--page', String(page),
98
+ '--limit', String(limit),
99
+ '--json',
100
+ ]);
101
+ return result.error
102
+ ? { ok: false, message: result.error }
103
+ : { ok: true, data: result };
104
+ }
105
+
106
+ default:
107
+ return { ok: false, message: `Tool not implemented: ${toolName}` };
108
+ }
109
+ } catch (e) {
110
+ appHelper.error('callTool', toolName, e);
111
+ return { ok: false, message: e.message };
112
+ }
113
+ }
114
+ }
115
+
116
+ module.exports = new ToolService();