@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.
- package/README.md +89 -0
- package/package.json +26 -0
- 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);
|