@leadfund/lead-mcp 202610.1.0 → 202610.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.
@@ -0,0 +1,284 @@
1
+ /**
2
+ * HTTP(S) 传输端到端测试
3
+ *
4
+ * <p>覆盖两种云端协议:</p>
5
+ * <ul>
6
+ * <li>{@code sse} —— GET /sse 建流 + POST /messages 回传</li>
7
+ * <li>{@code http} —— Streamable HTTP 的 POST /mcp</li>
8
+ * </ul>
9
+ *
10
+ * <p>同时验证鉴权:没有 Bearer 令牌一律 401。</p>
11
+ *
12
+ * <p>运行:node test/http.test.mjs</p>
13
+ */
14
+
15
+ import { spawn } from "node:child_process";
16
+ import { createServer } from "node:http";
17
+ import fs from "node:fs";
18
+ import os from "node:os";
19
+ import path from "node:path";
20
+ import assert from "node:assert/strict";
21
+
22
+ const CLI = path.join(
23
+ path.dirname(new URL(import.meta.url).pathname),
24
+ "..",
25
+ "src",
26
+ "cli.js",
27
+ );
28
+ const AUTH_TOKEN = "test-token-123";
29
+
30
+ let passed = 0;
31
+
32
+ /**
33
+ * 断言
34
+ *
35
+ * @param {string} name 用例名
36
+ * @param {Function} fn 断言函数
37
+ */
38
+ async function test(name, fn) {
39
+ try {
40
+ await fn();
41
+ passed += 1;
42
+ console.log(`✓ ${name}`);
43
+ } catch (error) {
44
+ console.error(`✗ ${name}\n ${error.message}`);
45
+ process.exitCode = 1;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * 起一个 mock 的 dmmp-system
51
+ */
52
+ async function startMockBackend() {
53
+ const server = createServer((req, res) => {
54
+ let body = "";
55
+ req.on("data", (c) => {
56
+ body += c;
57
+ });
58
+ req.on("end", () => {
59
+ res.setHeader("Content-Type", "application/json");
60
+ res.end(
61
+ JSON.stringify({
62
+ code: 0,
63
+ message: "接口调用成功",
64
+ data: {
65
+ found: true,
66
+ memberId: "M10086",
67
+ memberIdList: ["M10086"],
68
+ name: "张*",
69
+ mobileFormat: "138****0000",
70
+ idTypeFormat: "身份证",
71
+ idNoFormat: "310101********1234",
72
+ idValidityFormat: "长期有效",
73
+ },
74
+ }),
75
+ );
76
+ });
77
+ });
78
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
79
+ return { server, port: server.address().port };
80
+ }
81
+
82
+ /**
83
+ * 起一个 lead-mcp 的 HTTP 模式实例
84
+ */
85
+ async function startHttpMode(transport, extraArgs = []) {
86
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "lead-mcp-http-"));
87
+ const child = spawn(
88
+ process.execPath,
89
+ [
90
+ CLI,
91
+ "--transport",
92
+ transport,
93
+ "--auth-token",
94
+ AUTH_TOKEN,
95
+ "--http-port",
96
+ "0",
97
+ ...extraArgs,
98
+ ],
99
+ { stdio: ["pipe", "pipe", "pipe"] },
100
+ );
101
+
102
+ let stderr = "";
103
+ child.stderr.on("data", (c) => {
104
+ stderr += c.toString();
105
+ });
106
+
107
+ // 从启动日志里拿实际端口(--http-port 0 表示随机端口)
108
+ const port = await new Promise((resolve, reject) => {
109
+ const timer = setTimeout(
110
+ () => reject(new Error(`启动超时\n${stderr}`)),
111
+ 15000,
112
+ );
113
+ child.stderr.on("data", () => {
114
+ const match = stderr.match(/端点=http:\/\/[^:]+:(\d+)/);
115
+ if (match) {
116
+ clearTimeout(timer);
117
+ resolve(Number(match[1]));
118
+ }
119
+ });
120
+ });
121
+
122
+ return { child, port, getStderr: () => stderr };
123
+ }
124
+
125
+ /**
126
+ * SSE:建立流,拿到 sessionId,再通过 POST /messages 发 initialize
127
+ */
128
+ async function sseHandshake(port, withAuth = true) {
129
+ const headers = withAuth ? { Authorization: `Bearer ${AUTH_TOKEN}` } : {};
130
+ const controller = new AbortController();
131
+ const res = await fetch(`http://127.0.0.1:${port}/sse`, {
132
+ headers,
133
+ signal: controller.signal,
134
+ });
135
+
136
+ const reader = res.body.getReader();
137
+ const decoder = new TextDecoder();
138
+ let buffer = "";
139
+ let sessionId = null;
140
+ let firstMessage = null;
141
+
142
+ // 读第一帧:event: endpoint 里带 sessionId
143
+ while (!sessionId) {
144
+ const { value, done } = await reader.read();
145
+ if (done) break;
146
+ buffer += decoder.decode(value, { stream: true });
147
+ const match = buffer.match(/sessionId=([0-9a-f-]+)/);
148
+ if (match) {
149
+ sessionId = match[1];
150
+ }
151
+ }
152
+
153
+ const post = async (message) => {
154
+ await fetch(`http://127.0.0.1:${port}/messages?sessionId=${sessionId}`, {
155
+ method: "POST",
156
+ headers: { "Content-Type": "application/json", ...headers },
157
+ body: JSON.stringify(message),
158
+ });
159
+ };
160
+
161
+ await post({
162
+ jsonrpc: "2.0",
163
+ id: 1,
164
+ method: "initialize",
165
+ params: {
166
+ protocolVersion: "2024-11-05",
167
+ capabilities: {},
168
+ clientInfo: { name: "http-test", version: "0.0.1" },
169
+ },
170
+ });
171
+
172
+ // 从流里读 initialize 的响应
173
+ const deadline = Date.now() + 10000;
174
+ while (!firstMessage && Date.now() < deadline) {
175
+ const { value, done } = await reader.read();
176
+ if (done) break;
177
+ buffer += decoder.decode(value, { stream: true });
178
+ const match = buffer.match(/data: (\{.*?\})\r?\n/);
179
+ if (match) {
180
+ firstMessage = JSON.parse(match[1]);
181
+ }
182
+ }
183
+
184
+ controller.abort();
185
+ return { sessionId, firstMessage };
186
+ }
187
+
188
+ async function main() {
189
+ const mock = await startMockBackend();
190
+ const baseUrl = `http://127.0.0.1:${mock.port}`;
191
+
192
+ // 配置文件里指定后端地址
193
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "lead-mcp-cfg-"));
194
+ const configFile = path.join(tmpDir, "config.json");
195
+ fs.writeFileSync(
196
+ configFile,
197
+ JSON.stringify({ environments: { test: [baseUrl] } }),
198
+ );
199
+
200
+ // ---- SSE ----
201
+ const sse = await startHttpMode("sse", [
202
+ "--base-url",
203
+ baseUrl,
204
+ "--env",
205
+ "test",
206
+ ]);
207
+
208
+ await test("SSE:未带令牌返回 401", async () => {
209
+ const res = await fetch(`http://127.0.0.1:${sse.port}/sse`);
210
+ assert.equal(res.status, 401);
211
+ });
212
+
213
+ await test("SSE:健康检查不鉴权", async () => {
214
+ const res = await fetch(`http://127.0.0.1:${sse.port}/health`);
215
+ assert.equal(res.status, 200);
216
+ const body = await res.json();
217
+ assert.equal(body.ok, true);
218
+ assert.equal(body.transport, "sse");
219
+ });
220
+
221
+ await test("SSE:握手并能查到客户信息", async () => {
222
+ const { sessionId, firstMessage } = await sseHandshake(sse.port);
223
+ assert.ok(sessionId, "应拿到 sessionId");
224
+ assert.ok(firstMessage, "应收到 initialize 响应");
225
+ assert.equal(firstMessage.result.serverInfo.name, "lead-mcp");
226
+ });
227
+
228
+ sse.child.kill();
229
+
230
+ // ---- Streamable HTTP ----
231
+ const stream = await startHttpMode("http", ["--base-url", baseUrl]);
232
+
233
+ await test("HTTP:未带令牌返回 401", async () => {
234
+ const res = await fetch(`http://127.0.0.1:${stream.port}/mcp`, {
235
+ method: "POST",
236
+ headers: { "Content-Type": "application/json" },
237
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize" }),
238
+ });
239
+ assert.equal(res.status, 401);
240
+ });
241
+
242
+ await test("HTTP:initialize 返回服务器信息", async () => {
243
+ const res = await fetch(`http://127.0.0.1:${stream.port}/mcp`, {
244
+ method: "POST",
245
+ headers: {
246
+ "Content-Type": "application/json",
247
+ Accept: "application/json, text/event-stream",
248
+ Authorization: `Bearer ${AUTH_TOKEN}`,
249
+ },
250
+ body: JSON.stringify({
251
+ jsonrpc: "2.0",
252
+ id: 1,
253
+ method: "initialize",
254
+ params: {
255
+ protocolVersion: "2024-11-05",
256
+ capabilities: {},
257
+ clientInfo: { name: "http-test", version: "0.0.1" },
258
+ },
259
+ }),
260
+ });
261
+ assert.equal(res.status, 200);
262
+ const text = await res.text();
263
+ // Streamable HTTP 可能以 SSE 帧返回,data 后面是完整的嵌套 JSON
264
+ const match = text.match(/data: (\{[\s\S]*\})/) ?? text.match(/(\{[\s\S]*\})/);
265
+ assert.ok(match, `应返回 MCP 响应,实际: ${text.slice(0, 200)}`);
266
+ const body = JSON.parse(match[1]);
267
+ assert.equal(body.result.serverInfo.name, "lead-mcp");
268
+ });
269
+
270
+ stream.child.kill();
271
+ mock.server.close();
272
+ fs.rmSync(tmpDir, { recursive: true, force: true });
273
+
274
+ if (process.exitCode) {
275
+ console.error("\n有用例失败 ❌");
276
+ } else {
277
+ console.log(`\n全部通过(${passed} 个用例)✅`);
278
+ }
279
+ }
280
+
281
+ main().catch((error) => {
282
+ console.error("HTTP 测试失败:", error);
283
+ process.exit(1);
284
+ });