@apocdata-info/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 (74) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/LICENSE +191 -0
  3. package/README.md +230 -0
  4. package/dist/cache.d.ts +26 -0
  5. package/dist/cache.js +51 -0
  6. package/dist/cache.js.map +1 -0
  7. package/dist/client.js +113 -0
  8. package/dist/errors.d.ts +37 -0
  9. package/dist/errors.js +56 -0
  10. package/dist/errors.js.map +1 -0
  11. package/dist/index.d.ts +17 -0
  12. package/dist/index.js +259 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/resources.js +229 -0
  15. package/dist/server.d.ts +20 -0
  16. package/dist/server.js +69 -0
  17. package/dist/server.js.map +1 -0
  18. package/dist/tools/factor-registry.d.ts +17 -0
  19. package/dist/tools/factor-registry.js +36 -0
  20. package/dist/tools/factor-registry.js.map +1 -0
  21. package/dist/tools/index.d.ts +21 -0
  22. package/dist/tools/index.js +45 -0
  23. package/dist/tools/index.js.map +1 -0
  24. package/dist/tools/macro-dashboard.d.ts +17 -0
  25. package/dist/tools/macro-dashboard.js +30 -0
  26. package/dist/tools/macro-dashboard.js.map +1 -0
  27. package/dist/tools/macro-indicator.d.ts +17 -0
  28. package/dist/tools/macro-indicator.js +45 -0
  29. package/dist/tools/macro-indicator.js.map +1 -0
  30. package/dist/tools/quotes-snapshot.d.ts +17 -0
  31. package/dist/tools/quotes-snapshot.js +39 -0
  32. package/dist/tools/quotes-snapshot.js.map +1 -0
  33. package/dist/tools/stock-bars-daily.d.ts +17 -0
  34. package/dist/tools/stock-bars-daily.js +45 -0
  35. package/dist/tools/stock-bars-daily.js.map +1 -0
  36. package/dist/tools/stock-detail.d.ts +17 -0
  37. package/dist/tools/stock-detail.js +38 -0
  38. package/dist/tools/stock-detail.js.map +1 -0
  39. package/dist/tools/stock-financials.d.ts +17 -0
  40. package/dist/tools/stock-financials.js +36 -0
  41. package/dist/tools/stock-financials.js.map +1 -0
  42. package/dist/tools/stock-list.d.ts +17 -0
  43. package/dist/tools/stock-list.js +39 -0
  44. package/dist/tools/stock-list.js.map +1 -0
  45. package/dist/tools/stock-news.d.ts +17 -0
  46. package/dist/tools/stock-news.js +36 -0
  47. package/dist/tools/stock-news.js.map +1 -0
  48. package/dist/tools/stock-quote.d.ts +17 -0
  49. package/dist/tools/stock-quote.js +32 -0
  50. package/dist/tools/stock-quote.js.map +1 -0
  51. package/dist/tools/top10-holders.d.ts +17 -0
  52. package/dist/tools/top10-holders.js +35 -0
  53. package/dist/tools/top10-holders.js.map +1 -0
  54. package/dist/tools/trade-days.d.ts +17 -0
  55. package/dist/tools/trade-days.js +36 -0
  56. package/dist/tools/trade-days.js.map +1 -0
  57. package/dist/tools/types.d.ts +29 -0
  58. package/dist/tools/types.js +16 -0
  59. package/dist/tools/types.js.map +1 -0
  60. package/dist/tools.js +540 -0
  61. package/dist/transport.d.ts +44 -0
  62. package/dist/transport.js +87 -0
  63. package/dist/transport.js.map +1 -0
  64. package/dist/types.d.ts +54 -0
  65. package/dist/types.js +16 -0
  66. package/dist/types.js.map +1 -0
  67. package/package.json +67 -0
  68. package/scripts/client-unit-test.mjs +147 -0
  69. package/scripts/contract-test.mjs +167 -0
  70. package/scripts/coverage-test.mjs +126 -0
  71. package/scripts/error-path-test.mjs +110 -0
  72. package/scripts/freshness-probe.mjs +144 -0
  73. package/scripts/integration-test.mjs +284 -0
  74. package/scripts/mcp-e2e-test.mjs +196 -0
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP 协议层端到端测试。
4
+ *
5
+ * 启动 dist/index.js stdio server,通过 JSON-RPC 发请求,验证:
6
+ * 1. tools/list 返回当前注册工具,schema 结构正确
7
+ * 2. tools/call 正常调用 isError=false,meta 头被注入到 text 内容
8
+ * 3. tools/call 业务错误(success=false)isError=true
9
+ * 4. tools/call HTTP 4xx(未部署端点)isError=true
10
+ * 5. format=compact 真的返回 columns/rows 列式结构 + X-Tdc-Format 头
11
+ * 6. 未知 tool 名返回 isError=true
12
+ */
13
+
14
+ import { spawn } from 'node:child_process';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { dirname, resolve } from 'node:path';
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const SERVER = resolve(__dirname, '..', 'dist', 'index.js');
20
+
21
+ class McpClient {
22
+ constructor() {
23
+ this.proc = spawn('node', [SERVER], {
24
+ stdio: ['pipe', 'pipe', 'pipe'],
25
+ env: { ...process.env },
26
+ });
27
+ this.buf = '';
28
+ this.pending = new Map();
29
+ this.nextId = 1;
30
+ this.proc.stdout.on('data', (chunk) => this._onData(chunk));
31
+ this.proc.stderr.on('data', (chunk) => {
32
+ // 转发 server 的 debug 日志
33
+ if (process.env.VERBOSE) process.stderr.write(chunk);
34
+ });
35
+ }
36
+
37
+ _onData(chunk) {
38
+ this.buf += chunk.toString('utf-8');
39
+ let idx;
40
+ while ((idx = this.buf.indexOf('\n')) >= 0) {
41
+ const line = this.buf.slice(0, idx).trim();
42
+ this.buf = this.buf.slice(idx + 1);
43
+ if (!line) continue;
44
+ try {
45
+ const msg = JSON.parse(line);
46
+ if (msg.id !== undefined && this.pending.has(msg.id)) {
47
+ this.pending.get(msg.id).resolve(msg);
48
+ this.pending.delete(msg.id);
49
+ }
50
+ } catch (e) {
51
+ // 忽略非 JSON 行
52
+ }
53
+ }
54
+ }
55
+
56
+ request(method, params) {
57
+ const id = this.nextId++;
58
+ const msg = { jsonrpc: '2.0', id, method, params };
59
+ return new Promise((resolve, reject) => {
60
+ this.pending.set(id, { resolve, reject });
61
+ this.proc.stdin.write(JSON.stringify(msg) + '\n');
62
+ setTimeout(() => {
63
+ if (this.pending.has(id)) {
64
+ this.pending.delete(id);
65
+ reject(new Error(`Timeout waiting for response to ${method}`));
66
+ }
67
+ }, 15000);
68
+ });
69
+ }
70
+
71
+ notify(method, params) {
72
+ this.proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n');
73
+ }
74
+
75
+ close() {
76
+ this.proc.stdin.end();
77
+ this.proc.kill();
78
+ }
79
+ }
80
+
81
+ const checks = [];
82
+ function check(name, cond, detail = '') {
83
+ checks.push({ kind: 'PROD', name, pass: !!cond, detail });
84
+ }
85
+ function checkLag(name, cond, detail = '') {
86
+ checks.push({ kind: 'LAG', name, pass: !!cond, detail });
87
+ }
88
+
89
+ const client = new McpClient();
90
+
91
+ try {
92
+ // 1. initialize
93
+ const init = await client.request('initialize', {
94
+ protocolVersion: '2024-11-05',
95
+ capabilities: {},
96
+ clientInfo: { name: 'e2e', version: '0' },
97
+ });
98
+ client.notify('notifications/initialized');
99
+ check('initialize 返回 protocolVersion', init.result?.protocolVersion === '2024-11-05');
100
+ check('serverInfo.name = apocdata-mcp-server', init.result?.serverInfo?.name === 'apocdata-mcp-server');
101
+
102
+ // 2. tools/list
103
+ const list = await client.request('tools/list', {});
104
+ const tools = list.result?.tools ?? [];
105
+ const expectedToolCount = Number(process.env.APOCDATA_EXPECTED_TOOL_COUNT ?? 46);
106
+ check('tools/list 返回当前工具数量', tools.length === expectedToolCount, `expected ${expectedToolCount}, got ${tools.length}`);
107
+ const quote = tools.find(t => t.name === 'quote');
108
+ check('quote 工具有正确 schema', quote?.inputSchema?.required?.[0] === 'symbol');
109
+ const macro = tools.find(t => t.name === 'macro');
110
+ check('macro 工具 enum 含 GDP/CPI/PPI/PMI',
111
+ JSON.stringify(macro?.inputSchema?.properties?.type?.enum) === JSON.stringify(['GDP', 'CPI', 'PPI', 'PMI']));
112
+
113
+ // 3. 正常调用:stock(600519) → isError=false
114
+ const ok = await client.request('tools/call', {
115
+ name: 'stock',
116
+ arguments: { symbol: '600519' },
117
+ });
118
+ check('正常调用 isError=false', ok.result?.isError === false);
119
+ const okText = ok.result?.content?.[0]?.text ?? '';
120
+ check('正常调用 body 含贵州茅台', okText.includes('贵州茅台'));
121
+
122
+ // 4. 业务错误:holders(symbol=999999) → success=false → isError=true
123
+ const bizErr = await client.request('tools/call', {
124
+ name: 'holders',
125
+ arguments: { symbol: '999999' },
126
+ });
127
+ check('业务错误 isError=true', bizErr.result?.isError === true);
128
+ const bizText = bizErr.result?.content?.[0]?.text ?? '';
129
+ check('业务错误 body 含错误消息', bizText.includes('未找到股票'));
130
+
131
+ // 5. profile-full 是可选聚合工具:线上可能成功,也可能因服务未部署返回错误;两种响应都必须是合法 MCP tools/call 结果。
132
+ const profile = await client.request('tools/call', {
133
+ name: 'profile-full',
134
+ arguments: { symbol: '600519' },
135
+ });
136
+ check('profile-full 返回合法 MCP 结果',
137
+ profile.result?.isError === false || profile.result?.isError === true,
138
+ `response=${JSON.stringify(profile).slice(0, 120)}`);
139
+
140
+ // 6. format=compact:daily(symbol=600519, limit=3, format=compact)
141
+ // §5.3 advice 源码已有,线上待发版(注册或重启)
142
+ const compact = await client.request('tools/call', {
143
+ name: 'daily',
144
+ arguments: { symbol: '600519', limit: 3, format: 'compact' },
145
+ });
146
+ const compactText = compact.result?.content?.[0]?.text ?? '';
147
+ checkLag('compact 响应有 X-Tdc-Format=compact 头', compactText.includes('X-Tdc-Format'));
148
+ checkLag('compact 响应含 columns 字段', compactText.includes('"columns"'));
149
+ checkLag('compact 响应含 rows 字段', compactText.includes('"rows"'));
150
+
151
+ // 7. 未知 tool
152
+ const unknown = await client.request('tools/call', {
153
+ name: 'totally-fake-tool',
154
+ arguments: {},
155
+ });
156
+ check('未知 tool 名 isError=true', unknown.result?.isError === true);
157
+
158
+ // 8. resources/list 列出 3 个文档
159
+ const resList = await client.request('resources/list', {});
160
+ const resources = resList.result?.resources ?? [];
161
+ check('resources/list 返回 3 个 resource', resources.length === 3, `got ${resources.length}`);
162
+ const uris = resources.map(r => r.uri).sort();
163
+ check('resources 含 guide/scenarios/limits',
164
+ JSON.stringify(uris) === JSON.stringify(['apocdata://guide', 'apocdata://limits', 'apocdata://scenarios']));
165
+
166
+ // 9. resources/read 拉取 guide
167
+ const guide = await client.request('resources/read', { uri: 'apocdata://guide' });
168
+ const guideText = guide.result?.contents?.[0]?.text ?? '';
169
+ check('resources/read guide 含工具速查', guideText.includes('profile-full'));
170
+ check('resources/read guide 含元信息说明', guideText.includes('X-Tdc-RateLimit-Remaining'));
171
+
172
+ // 10. resources/read 未知 URI 返回 JSON-RPC error
173
+ const badRead = await client.request('resources/read', { uri: 'apocdata://nope' });
174
+ check('resources/read 未知 URI 返回 error response', badRead.error !== undefined,
175
+ `got ${JSON.stringify(badRead).slice(0, 80)}`);
176
+
177
+ } finally {
178
+ client.close();
179
+ }
180
+
181
+ // 报告
182
+ console.log('');
183
+ for (const c of checks) {
184
+ const sym = c.kind === 'LAG' ? (c.pass ? '✓' : '— LAG') : (c.pass ? '✓' : '✗');
185
+ console.log(`${sym} ${c.name}${c.detail ? ` (${c.detail})` : ''}`);
186
+ }
187
+
188
+ const prod = checks.filter(c => c.kind === 'PROD');
189
+ const lag = checks.filter(c => c.kind === 'LAG');
190
+ const prodPass = prod.filter(c => c.pass).length;
191
+ const lagPass = lag.filter(c => c.pass).length;
192
+
193
+ console.log('');
194
+ console.log(`PROD: ${prodPass}/${prod.length} pass`);
195
+ if (lag.length) console.log(`LAG : ${lagPass}/${lag.length} 线上已生效(其余待发版)`);
196
+ process.exit(prodPass === prod.length ? 0 : 1);