@bigdata-oflow/mcp-server 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 +89 -0
  2. package/package.json +26 -0
  3. package/src/index.js +198 -0
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # mcp-server-oflow (Node.js)
2
+
3
+ oflow MCP server 的 Node.js 实现,支持通过 npx 直接运行。
4
+
5
+ ## 安装 & 运行
6
+
7
+ 通过 npx 直接运行(无需安装):
8
+
9
+ ```bash
10
+ npx mcp-server-oflow
11
+ ```
12
+
13
+ 或全局安装后运行:
14
+
15
+ ```bash
16
+ npm install -g mcp-server-oflow
17
+ mcp-server-oflow
18
+ ```
19
+
20
+ ## 配置
21
+
22
+ 在 `mcp_servers.json` 中添加:
23
+
24
+ ### stdio 模式
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "mcp-server-oflow": {
30
+ "command": "npx",
31
+ "args": ["-y", "mcp-server-oflow"],
32
+ "env": {
33
+ "APP_ID": "YOUR APP ID",
34
+ "APP_SECRET": "YOUR APP SECRET"
35
+ }
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ ### SSE 模式
42
+
43
+ 服务端启动:
44
+
45
+ ```bash
46
+ npx mcp-server-oflow --transport=sse --port=8001
47
+ ```
48
+
49
+ 客户端配置:
50
+
51
+ ```json
52
+ {
53
+ "mcp-server-oflow": {
54
+ "url": "http://localhost:8001/sse",
55
+ "headers": {},
56
+ "timeout": 60,
57
+ "sse_read_timeout": 60
58
+ }
59
+ }
60
+ ```
61
+
62
+ ## 环境变量
63
+
64
+ | 变量 | 说明 | 必填 |
65
+ |------|------|------|
66
+ | `APP_ID` | oflow 接口验签 App ID | stdio 模式必填 |
67
+ | `APP_SECRET` | oflow 接口验签 App Secret | stdio 模式必填 |
68
+ | `AREA` | 区域选择:`CN`(默认) / `SGP` / `INDIA` | 否 |
69
+ | `USER_ID` | 用户标识 | 否 |
70
+
71
+ APP_ID 和 APP_SECRET 是 oflow 接口验签秘钥,联系 oflow 管理员(谭集友-W9009858)申请开通。
72
+
73
+ > SSE 模式下,客户端调用 call_tool 时也可以把 `app_id` 和 `app_secret` 放在 POST 请求 `arguments` 参数中传入。
74
+
75
+ ## CLI 参数
76
+
77
+ ```
78
+ --transport 传输模式: stdio (默认) 或 sse
79
+ --host SSE 模式监听地址 (默认: 0.0.0.0)
80
+ --port SSE 模式监听端口 (默认: 8000)
81
+ ```
82
+
83
+ ## 开发
84
+
85
+ ```bash
86
+ cd nodejs
87
+ npm install
88
+ npm start
89
+ ```
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@bigdata-oflow/mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for oflow",
5
+ "type": "module",
6
+ "bin": {
7
+ "mcp-server-oflow": "src/index.js"
8
+ },
9
+ "files": [
10
+ "src/"
11
+ ],
12
+ "scripts": {
13
+ "start": "node src/index.js"
14
+ },
15
+ "dependencies": {
16
+ "@modelcontextprotocol/sdk": "^1.27.0",
17
+ "express": "^4.21.0"
18
+ },
19
+ "engines": {
20
+ "node": ">=18.0.0"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "license": "MIT"
26
+ }
package/src/index.js ADDED
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
6
+ import {
7
+ ListToolsRequestSchema,
8
+ CallToolRequestSchema,
9
+ } from "@modelcontextprotocol/sdk/types.js";
10
+ import { createHash } from "node:crypto";
11
+ import { parseArgs } from "node:util";
12
+ import express from "express";
13
+
14
+ const URLS = {
15
+ CN: "http://oflow.oppoer.me",
16
+ SGP: "http://oflowf.oppoer.me",
17
+ INDIA: "http://oflow.inali.oppoer.me",
18
+ };
19
+
20
+ function data2str(data) {
21
+ const keys = Object.keys(data).sort();
22
+ return keys
23
+ .map((key) => {
24
+ let value = data[key];
25
+ if (value === true) value = "true";
26
+ if (value === false) value = "false";
27
+ if (value == null) value = "null";
28
+ return `${key}=${value}`;
29
+ })
30
+ .join("&");
31
+ }
32
+
33
+ function generateSignature(args, appKey, url) {
34
+ delete args.sign;
35
+ const argsStr = data2str(args);
36
+ const strToMd5 = `${argsStr}:${url}:${appKey}`;
37
+ return createHash("md5").update(strToMd5, "utf-8").digest("hex").toLowerCase();
38
+ }
39
+
40
+ async function fetchToolsList(baseUrl) {
41
+ const path = "/mcp/list_tools";
42
+ const response = await fetch(baseUrl + path);
43
+ return response.json();
44
+ }
45
+
46
+ function createServer() {
47
+ let toolsMeta = {};
48
+
49
+ const server = new Server(
50
+ { name: "mcp-server-oflow", version: "0.1.1" },
51
+ { capabilities: { tools: {} } }
52
+ );
53
+
54
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
55
+ const area = process.env.AREA || "CN";
56
+ const url = URLS[area] || URLS.CN;
57
+ try {
58
+ const data = await fetchToolsList(url);
59
+ toolsMeta = data.tools_meta || {};
60
+ const tools = (data.tools || []).map((item) => ({
61
+ name: item.name,
62
+ description: item.description,
63
+ inputSchema: item.parameters || {},
64
+ }));
65
+ return { tools };
66
+ } catch (e) {
67
+ console.error(`ERROR: get oflow mcp tools failed: ${e.message}`);
68
+ return { tools: [] };
69
+ }
70
+ });
71
+
72
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
73
+ const { name, arguments: rawArgs } = request.params;
74
+ const args = { ...(rawArgs || {}) };
75
+ const meta = toolsMeta[name] || {};
76
+ const method = meta.method;
77
+ const path = meta.path;
78
+ const area = args.area || "CN";
79
+ delete args.area;
80
+
81
+ const appId = process.env.APP_ID || args.app_id;
82
+ const appSecret = process.env.APP_SECRET || args.app_secret;
83
+ delete args.app_id;
84
+ delete args.app_secret;
85
+
86
+ if (!appId || !appSecret) {
87
+ return {
88
+ content: [{ type: "text", text: "not found ENV: APP_ID & APP_SECRET" }],
89
+ };
90
+ }
91
+
92
+ if (!method || !path) {
93
+ return {
94
+ content: [{ type: "text", text: "not found tools result" }],
95
+ };
96
+ }
97
+
98
+ try {
99
+ const url = URLS[area] || URLS.CN;
100
+ args.app_id = appId;
101
+ args.request_id = Math.floor(Date.now() / 1000);
102
+ if (process.env.USER_ID) {
103
+ args.user = process.env.USER_ID;
104
+ }
105
+ args.sign = generateSignature(args, appSecret, path);
106
+
107
+ let response;
108
+ const upperMethod = method.toUpperCase();
109
+
110
+ if (upperMethod === "PATCH" || upperMethod === "POST") {
111
+ response = await fetch(url + path, {
112
+ method: upperMethod,
113
+ headers: { "Content-Type": "application/json" },
114
+ body: JSON.stringify(args),
115
+ signal: AbortSignal.timeout(60000),
116
+ });
117
+ } else {
118
+ const params = new URLSearchParams();
119
+ for (const [k, v] of Object.entries(args)) {
120
+ params.append(k, String(v));
121
+ }
122
+ response = await fetch(`${url}${path}?${params}`, {
123
+ signal: AbortSignal.timeout(60000),
124
+ });
125
+ }
126
+
127
+ const rawText = await response.text();
128
+ let text;
129
+ try {
130
+ text = JSON.stringify(JSON.parse(rawText), null, 2);
131
+ } catch {
132
+ text = rawText;
133
+ }
134
+ return { content: [{ type: "text", text }] };
135
+ } catch (e) {
136
+ return {
137
+ content: [{ type: "text", text: `call tool failed, reason: ${e.message}` }],
138
+ };
139
+ }
140
+ });
141
+
142
+ return server;
143
+ }
144
+
145
+ async function main() {
146
+ const { values } = parseArgs({
147
+ options: {
148
+ transport: { type: "string", default: "stdio", short: "t" },
149
+ host: { type: "string", default: "0.0.0.0" },
150
+ port: { type: "string", default: "8000" },
151
+ },
152
+ });
153
+
154
+ const transport = values.transport;
155
+ const port = parseInt(values.port, 10);
156
+
157
+ if (transport === "sse") {
158
+ const app = express();
159
+ app.use(express.json());
160
+
161
+ const sessions = new Map();
162
+
163
+ app.get("/sse", async (req, res) => {
164
+ const sseTransport = new SSEServerTransport("/messages", res);
165
+ const server = createServer();
166
+ sessions.set(sseTransport.sessionId, { server, transport: sseTransport });
167
+
168
+ res.on("close", () => {
169
+ sessions.delete(sseTransport.sessionId);
170
+ });
171
+
172
+ await server.connect(sseTransport);
173
+ });
174
+
175
+ app.post("/messages", async (req, res) => {
176
+ const sessionId = req.query.sessionId;
177
+ const session = sessions.get(sessionId);
178
+ if (session) {
179
+ await session.transport.handlePostMessage(req, res);
180
+ } else {
181
+ res.status(404).json({ error: "Session not found" });
182
+ }
183
+ });
184
+
185
+ app.listen(port, values.host, () => {
186
+ console.log(
187
+ `MCP server started with SSE transport on http://${values.host}:${port}/sse`
188
+ );
189
+ });
190
+ } else {
191
+ const server = createServer();
192
+ const stdioTransport = new StdioServerTransport();
193
+ await server.connect(stdioTransport);
194
+ console.error("MCP server started with stdio transport");
195
+ }
196
+ }
197
+
198
+ main().catch(console.error);