lingshu-solver 1.0.2 → 1.0.3

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 (4) hide show
  1. package/README.md +232 -239
  2. package/index.html +12237 -11816
  3. package/mcp-server.js +288 -286
  4. package/package.json +55 -56
package/mcp-server.js CHANGED
@@ -1,286 +1,288 @@
1
- #!/usr/bin/env node
2
- /**
3
- * 灵数求解器 · MCP stdio 服务端(零依赖)
4
- *
5
- * 手工实现 JSON-RPC 2.0 + 换行符分隔 JSON(与官方 MCP SDK 客户端一致,不依赖任何 MCP SDK)。
6
- * 核心求解能力来自同目录 solver-core.js(读取 index.html 的已验证核心脚本)。
7
- *
8
- * 暴露工具:
9
- * 1) solve —— 求解方程组,返回结构化结果(含可信层级 tier 与 truncated 标记)
10
- * 2) give_feedback —— 供 AI 智能体主动回报卡点或建议
11
- *
12
- * 安全原则:离线、零数据、结构化错误不泄露内部堆栈;调用日志仅记元数据(不记方程内容)。
13
- *
14
- * 运行:node mcp-server.js
15
- */
16
- 'use strict';
17
- const fs = require('fs');
18
- const path = require('path');
19
- const { solve } = require('./solver-core');
20
-
21
- const SERVER_NAME = 'lingshu-solver';
22
- const SERVER_VERSION = '4.1.0';
23
-
24
- // ---- 护栏常量(防畸形/恶意输入耗尽资源)----
25
- const MAX_TOTAL_CHARS = 100 * 1024; // 单次请求方程文本总长上限 100KB
26
- const MAX_EQ_COUNT = 64; // 方程数量上限
27
- const MAX_VAR_COUNT = 6; // 变量数量上限(与产品规格一致)
28
-
29
- // ---- 本地日志(仅元数据,零数据不外传)----
30
- const LOG_PATH = path.resolve(__dirname, 'calls.log');
31
- const FEEDBACK_PATH = path.resolve(__dirname, 'feedback.log');
32
- function appendLog(p) {
33
- try {
34
- fs.appendFileSync(LOG_PATH, JSON.stringify(p) + '\n');
35
- } catch (_e) { /* 日志失败不影响服务 */ }
36
- }
37
-
38
- // ---- 求解结果整理 ----
39
- // 数值格式化:固定 6 位小数(产品规格「6位小数有限网格」),与界面一致。
40
- // 解点 values 经 roundToGrid 吸附到 6 位网格,实际残差通常 ≤ 1e-9。
41
- const fmt6 = (v) => (typeof v === 'number' && isFinite(v)) ? v.toFixed(6) : String(v);
42
-
43
- function shapeResult(r) {
44
- const sols = Array.isArray(r.solutions) ? r.solutions : [];
45
- const meta = r.meta || {};
46
- // 变量名:优先用求解器识别结果,否则退化为 x1/x2/...
47
- const varNames = (Array.isArray(r.varNames) && r.varNames.length)
48
- ? r.varNames
49
- : (sols[0] && Array.isArray(sols[0].values) ? sols[0].values.map((_, i) => 'x' + (i + 1)) : []);
50
-
51
- // 推荐解:取范数最小者(与界面"距原点最近"一致)
52
- let recommended = null;
53
- let best = Infinity;
54
- for (const s of sols) {
55
- if (!s || !Array.isArray(s.values)) continue;
56
- let d = 0;
57
- for (const v of s.values) d += v * v;
58
- if (d < best) { best = d; recommended = s; }
59
- }
60
- const tierSet = new Set(sols.map(s => (s && s.tier) || 'unknown'));
61
- const allProven = sols.length > 0 && [...tierSet].every(t => t === 'proven');
62
- const typeName = r.resultType === 1 ? 'empty' : r.resultType === 3 ? 'infinite' : 'finite';
63
-
64
- // 每个解:保留机器友好字段(values/tier/certified)+ 人类可读 text;
65
- // 内部数值(residual/certifiedRadius)收进 internals,机器可整块跳过。
66
- const cleanSols = sols.map((s) => {
67
- const vals = Array.isArray(s.values) ? s.values : [];
68
- const text = varNames.map((vn, i) => `${vn}=${fmt6(vals[i])}`).join(', ');
69
- return {
70
- values: vals,
71
- tier: s.tier || 'unknown',
72
- certified: !!s.certified,
73
- text: text,
74
- internals: {
75
- residual: (typeof s.residual === 'number') ? s.residual : null,
76
- certifiedRadius: (typeof s.certifiedRadius === 'number') ? s.certifiedRadius : null
77
- }
78
- };
79
- });
80
- const recommendedClean = recommended ? cleanSols[sols.indexOf(recommended)] : null;
81
-
82
- // 人类可读总览(A)
83
- let summary;
84
- if (typeName === 'empty') {
85
- summary = '严格证明:该方程组无实数解。';
86
- } else if (typeName === 'infinite') {
87
- summary = `无限解集;给出距原点最近的推荐解(共展示 ${sols.length} 个候选)。`;
88
- } else {
89
- summary = `找到 ${sols.length} 个实数解${allProven ? '(全部经 Krawczyk 区间认证)' : ''}。`;
90
- }
91
-
92
- // 诊断信息(B):仅精选对调用方有用的少量字段,不再透传 meta 中 19 个内部运维字段,
93
- // 以降低机器侧 token 噪音。如需完整内部轨迹,可另接调试端点。
94
- const diagnostics = {
95
- solverVersion: meta.solverVersion || null,
96
- truncated: !!(r.truncated || meta.truncated),
97
- terminatedBy: meta.terminatedBy || null,
98
- provenCount: (typeof r.provenCount === 'number') ? r.provenCount : null,
99
- completeness: (typeof r.completeness === 'number') ? r.completeness : null
100
- };
101
-
102
- return {
103
- resultType: r.resultType,
104
- resultTypeName: typeName,
105
- certified: allProven,
106
- truncated: diagnostics.truncated,
107
- precisionDecimals: 6,
108
- solutionCount: sols.length,
109
- summary: summary,
110
- recommended: recommendedClean,
111
- solutions: cleanSols,
112
- warnings: r.warnings || [],
113
- diagnostics: diagnostics
114
- };
115
- }
116
-
117
- function doSolve(args) {
118
- const eqs = args && args.equations;
119
- if (!Array.isArray(eqs) || eqs.length === 0) {
120
- throw { type: 'invalid_input', message: 'equations 必须是非空字符串数组' };
121
- }
122
- if (eqs.length > MAX_EQ_COUNT) {
123
- throw { type: 'invalid_input', message: `方程数量超过上限 ${MAX_EQ_COUNT}` };
124
- }
125
- let total = 0;
126
- for (const e of eqs) {
127
- if (typeof e !== 'string') throw { type: 'invalid_input', message: '每条方程必须是字符串' };
128
- total += e.length;
129
- }
130
- if (total > MAX_TOTAL_CHARS) {
131
- throw { type: 'invalid_input', message: '方程文本总长超过 100KB 上限' };
132
- }
133
- const vars = (args && Array.isArray(args.variables)) ? args.variables : [];
134
- if (vars.length > MAX_VAR_COUNT) {
135
- throw { type: 'invalid_input', message: `变量数量超过上限 ${MAX_VAR_COUNT}` };
136
- }
137
- const domain = (args && args.domain) || undefined;
138
- const fastMode = !!(args && args.fastMode);
139
- const opts = (args && args.options) || {};
140
- // 输出精度固定为 6 位小数(产品规格「6位小数有限网格」),与界面一致,不提供位数切换
141
- const r = solve(eqs, vars, 6, domain, fastMode, opts);
142
- return shapeResult(r);
143
- }
144
-
145
- // ---- 工具定义 ----
146
- const TOOLS = [
147
- {
148
- name: 'solve',
149
- description: '求解实数方程组的确定性数值引擎(非大模型,无随机、同输入输出可复现)。适用:需可验证、可复现的实数解(代数或 sin/cos/tan/log/exp/sqrt/abs 等常见超越函数),尤其给 AI Agent 当"不会胡说"的数学后端。' +
150
- '不适用:纯符号推导/闭式证明、微分方程初值问题、整数/必不等于等强制约束(暂不支持)。' +
151
- '输入:equations 为含 "=" 的方程字符串数组,如 ["x^2+y^2=25","x+y=7"];variables 可选(不填自动识别,最多6个);domain 可选(如 {"x":[-30,30]}),否则默认每变量 ±1e6。' +
152
- '硬限制:变量 ≤6;方程 1–64 条且数量须 ≥ 变量数;单次方程文本 ≤100KB;输出固定 6 位小数(不可切换)。' +
153
- '输出(JSON):resultType=empty(严格证无实数解)/finite(有限已验证解)/infinite(无限解集,仅给距原点最近推荐解);summary=中文一句话总览;solutions[] 每解含 values[](6位小数数值)、tier(proven=Krawczyk已认证/likely/candidate)、certified、text(人类可读如"x=4.000000, y=3.000000"),残差等内部数值收在 internals 子块(机器可跳过);certified=是否全proven;recommended=距原点最近解的精简结构。' +
154
- 'truncated=true:预算内未完成全局分支判定、未证明已穷尽——不等于一定漏解,多数情况全部真解已找到;极端病态下可能遗漏个别解,可缩 domain 或提高 budget 重试。' +
155
- '错误返回 error.type(invalid_input=输入不合法/超限,internal_error=内部异常)。遇卡点或认为结果有误,请调用 give_feedback(内容仅落本地日志,不外传)。相同输入永远返回完全相同结果,可安全缓存与重复调用。',
156
- inputSchema: {
157
- type: 'object',
158
- properties: {
159
- equations: {
160
- type: 'array', items: { type: 'string' },
161
- description: '方程字符串数组,如 ["x^2 + y^2 = 25", "x + y = 7"]。支持 + - * / ^ sqrt log sin cos tan exp abs,以及 in-text 域约束 "x ∈ [-30,30]"。'
162
- },
163
- variables: {
164
- type: 'array', items: { type: 'string' },
165
- description: '变量名数组(可选)。不填则由方程文本自动识别,顺序按出现。最多 6 个。'
166
- },
167
- domain: {
168
- type: 'object',
169
- description: '显式搜索域(可选)。形如 {"x":[-30,30],"y":[-30,30]}。对"有限解·部分"演示或快增长函数(exp/sinh)建议显式给定,否则默认 ±1e6 可能剪枝失效并触发 truncated。'
170
- },
171
- fastMode: { type: 'boolean', description: '快速模式(默认 false)' },
172
- options: { type: 'object', description: '高级选项(可选),如 {budget:500000, maxDepth:28}' }
173
- },
174
- required: ['equations']
175
- }
176
- },
177
- {
178
- name: 'give_feedback',
179
- description: 'AI 智能体在调用 solve 遇到卡点、错误、或认为结果有问题时,主动回报。' +
180
- '回报内容仅落本地 feedback.log,不会外传。帮助作者持续改进。',
181
- inputSchema: {
182
- type: 'object',
183
- properties: {
184
- message: { type: 'string', description: '反馈内容:遇到了什么、期望什么、实际得到什么。' },
185
- context: { type: 'string', description: '可选上下文:触发场景、输入特征等。' }
186
- },
187
- required: ['message']
188
- }
189
- }
190
- ];
191
-
192
- // ---- stdio 帧格式:换行符分隔的 JSON(与官方 MCP SDK 客户端一致)----
193
- // 官方 SDK 的 serializeMessage 写 `JSON + '\n'`,ReadBuffer 按行解析;
194
- // 故服务端也必须用同样格式收发,否则 Claude/Cursor/Cline 等 stdio 客户端收不到响应。
195
- let lineBuf = '';
196
-
197
- function send(obj) {
198
- process.stdout.write(JSON.stringify(obj) + '\n');
199
- }
200
-
201
- function handle(msg) {
202
- const id = msg.id;
203
- const method = msg.method;
204
- const params = msg.params || {};
205
-
206
- if (method === 'initialize') {
207
- send({
208
- jsonrpc: '2.0', id,
209
- result: {
210
- protocolVersion: '2024-11-05',
211
- capabilities: { tools: {} },
212
- serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
213
- }
214
- });
215
- return;
216
- }
217
- if (method === 'tools/list') {
218
- send({ jsonrpc: '2.0', id, result: { tools: TOOLS } });
219
- return;
220
- }
221
- if (method === 'tools/call') {
222
- const name = params.name;
223
- const args = params.arguments || {};
224
- const t0 = Date.now();
225
- try {
226
- let result;
227
- if (name === 'solve') {
228
- result = doSolve(args);
229
- } else if (name === 'give_feedback') {
230
- const msg_fb = (args.message || '').toString().slice(0, 2000);
231
- fs.appendFileSync(FEEDBACK_PATH, JSON.stringify({
232
- ts: new Date().toISOString(), message: msg_fb, context: args.context || null
233
- }) + '\n');
234
- result = { acknowledged: true, note: '反馈已记录(本地,不外传)' };
235
- } else {
236
- throw { type: 'unknown_tool', message: '未知工具: ' + name };
237
- }
238
- const dt = Date.now() - t0;
239
- appendLog({
240
- ts: new Date().toISOString(), tool: name, status: 'ok',
241
- dtMs: dt, resultType: result.resultType, nSol: result.solutionCount,
242
- truncated: result.truncated
243
- });
244
- send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } });
245
- } catch (e) {
246
- const dt = Date.now() - t0;
247
- const errObj = (e && e.type) ? e : { type: 'internal_error', message: (e && e.message) || String(e) };
248
- appendLog({
249
- ts: new Date().toISOString(), tool: name, status: 'error',
250
- dtMs: dt, errorType: errObj.type
251
- });
252
- // 工具级错误:返回 isError=true 的工具结果(而非 JSON-RPC error 帧),
253
- // 让接入的 LLM 能读到结构化错误并自我纠正,而不是收到一个异常。
254
- send({
255
- jsonrpc: '2.0', id,
256
- result: { content: [{ type: 'text', text: JSON.stringify(errObj, null, 2) }], isError: true }
257
- });
258
- }
259
- return;
260
- }
261
- // 其他方法:忽略(含通知,无 id 不回包)
262
- }
263
-
264
- function pump() {
265
- let nl;
266
- while ((nl = lineBuf.indexOf('\n')) !== -1) {
267
- const line = lineBuf.slice(0, nl).replace(/\r$/, '');
268
- lineBuf = lineBuf.slice(nl + 1);
269
- if (!line.trim()) continue;
270
- try {
271
- const msg = JSON.parse(line);
272
- if (msg && msg.jsonrpc === '2.0') handle(msg);
273
- } catch (_e) { /* 畸形行忽略 */ }
274
- }
275
- }
276
-
277
- process.stdin.setEncoding('utf8');
278
- process.stdin.on('data', (chunk) => {
279
- lineBuf += chunk;
280
- pump();
281
- });
282
- // 不强制 process.exit,避免最后一个响应帧被截断丢失
283
- process.stdin.on('end', () => {});
284
-
285
- // 启动日志(仅元数据)
286
- appendLog({ ts: new Date().toISOString(), event: 'server_start', name: SERVER_NAME, version: SERVER_VERSION });
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 灵数求解器 · MCP stdio 服务端(零依赖)
4
+ *
5
+ * 手工实现 JSON-RPC 2.0 + 换行符分隔 JSON(与官方 MCP SDK 客户端一致,不依赖任何 MCP SDK)。
6
+ * 核心求解能力来自同目录 solver-core.js(读取 index.html 的已验证核心脚本)。
7
+ *
8
+ * 暴露工具:
9
+ * 1) solve —— 求解方程组,返回结构化结果(含可信层级 tier 与 truncated 标记)
10
+ * 2) give_feedback —— 供 AI 智能体主动回报卡点或建议
11
+ *
12
+ * 安全原则:离线、零数据、结构化错误不泄露内部堆栈;调用日志仅记元数据(不记方程内容)。
13
+ *
14
+ * 运行:node mcp-server.js
15
+ */
16
+ 'use strict';
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const { solve } = require('./solver-core');
20
+
21
+ const SERVER_NAME = 'lingshu-solver';
22
+ const SERVER_VERSION = '4.1.0';
23
+
24
+ // ---- 护栏常量(防畸形/恶意输入耗尽资源)----
25
+ const MAX_TOTAL_CHARS = 100 * 1024; // 单次请求方程文本总长上限 100KB
26
+ const MAX_EQ_COUNT = 64; // 方程数量上限
27
+ const MAX_VAR_COUNT = 6; // 变量数量上限(与产品规格一致)
28
+
29
+ // ---- 本地日志(仅元数据,零数据不外传)----
30
+ const LOG_PATH = path.resolve(__dirname, 'calls.log');
31
+ const FEEDBACK_PATH = path.resolve(__dirname, 'feedback.log');
32
+ function appendLog(p) {
33
+ try {
34
+ fs.appendFileSync(LOG_PATH, JSON.stringify(p) + '\n');
35
+ } catch (_e) { /* 日志失败不影响服务 */ }
36
+ }
37
+
38
+ // ---- 求解结果整理 ----
39
+ // 数值格式化:固定 6 位小数(产品规格「6位小数有限网格」),与界面一致。
40
+ // 解点 values 经 roundToGrid 吸附到 6 位网格,实际残差通常 ≤ 1e-9。
41
+ const fmt6 = (v) => (typeof v === 'number' && isFinite(v)) ? v.toFixed(6) : String(v);
42
+ // 确定性浮点吸附:消除 IEEE-754 末位 ULP 抖动,保证「同输入输出字节级可复现」
43
+ const detF = (v) => (typeof v === 'number' && isFinite(v)) ? Number(v.toFixed(12)) : null;
44
+
45
+ function shapeResult(r) {
46
+ const sols = Array.isArray(r.solutions) ? r.solutions : [];
47
+ const meta = r.meta || {};
48
+ // 变量名:优先用求解器识别结果,否则退化为 x1/x2/...
49
+ const varNames = (Array.isArray(r.varNames) && r.varNames.length)
50
+ ? r.varNames
51
+ : (sols[0] && Array.isArray(sols[0].values) ? sols[0].values.map((_, i) => 'x' + (i + 1)) : []);
52
+
53
+ // 推荐解:取范数最小者(与界面"距原点最近"一致)
54
+ let recommended = null;
55
+ let best = Infinity;
56
+ for (const s of sols) {
57
+ if (!s || !Array.isArray(s.values)) continue;
58
+ let d = 0;
59
+ for (const v of s.values) d += v * v;
60
+ if (d < best) { best = d; recommended = s; }
61
+ }
62
+ const tierSet = new Set(sols.map(s => (s && s.tier) || 'unknown'));
63
+ const allProven = sols.length > 0 && [...tierSet].every(t => t === 'proven');
64
+ const typeName = r.resultType === 1 ? 'empty' : r.resultType === 3 ? 'infinite' : 'finite';
65
+
66
+ // 每个解:保留机器友好字段(values/tier/certified)+ 人类可读 text;
67
+ // 内部数值(residual/certifiedRadius)收进 internals,机器可整块跳过。
68
+ const cleanSols = sols.map((s) => {
69
+ const vals = Array.isArray(s.values) ? s.values : [];
70
+ const text = varNames.map((vn, i) => `${vn}=${fmt6(vals[i])}`).join(', ');
71
+ return {
72
+ values: vals,
73
+ tier: s.tier || 'unknown',
74
+ certified: !!s.certified,
75
+ text: text,
76
+ internals: {
77
+ residual: detF(s.residual),
78
+ certifiedRadius: detF(s.certifiedRadius)
79
+ }
80
+ };
81
+ });
82
+ const recommendedClean = recommended ? cleanSols[sols.indexOf(recommended)] : null;
83
+
84
+ // 人类可读总览(A)
85
+ let summary;
86
+ if (typeName === 'empty') {
87
+ summary = '严格证明:该方程组无实数解。';
88
+ } else if (typeName === 'infinite') {
89
+ summary = `无限解集;给出距原点最近的推荐解(共展示 ${sols.length} 个候选)。`;
90
+ } else {
91
+ summary = `找到 ${sols.length} 个实数解${allProven ? '(全部经 Krawczyk 区间认证)' : ''}。`;
92
+ }
93
+
94
+ // 诊断信息(B):仅精选对调用方有用的少量字段,不再透传 meta 中 19 个内部运维字段,
95
+ // 以降低机器侧 token 噪音。如需完整内部轨迹,可另接调试端点。
96
+ const diagnostics = {
97
+ solverVersion: meta.solverVersion || null,
98
+ truncated: !!(r.truncated || meta.truncated),
99
+ terminatedBy: meta.terminatedBy || null,
100
+ provenCount: (typeof r.provenCount === 'number') ? r.provenCount : null,
101
+ completeness: detF(r.completeness)
102
+ };
103
+
104
+ return {
105
+ resultType: r.resultType,
106
+ resultTypeName: typeName,
107
+ certified: allProven,
108
+ truncated: diagnostics.truncated,
109
+ precisionDecimals: 6,
110
+ solutionCount: sols.length,
111
+ summary: summary,
112
+ recommended: recommendedClean,
113
+ solutions: cleanSols,
114
+ warnings: r.warnings || [],
115
+ diagnostics: diagnostics
116
+ };
117
+ }
118
+
119
+ function doSolve(args) {
120
+ const eqs = args && args.equations;
121
+ if (!Array.isArray(eqs) || eqs.length === 0) {
122
+ throw { type: 'invalid_input', message: 'equations 必须是非空字符串数组' };
123
+ }
124
+ if (eqs.length > MAX_EQ_COUNT) {
125
+ throw { type: 'invalid_input', message: `方程数量超过上限 ${MAX_EQ_COUNT}` };
126
+ }
127
+ let total = 0;
128
+ for (const e of eqs) {
129
+ if (typeof e !== 'string') throw { type: 'invalid_input', message: '每条方程必须是字符串' };
130
+ total += e.length;
131
+ }
132
+ if (total > MAX_TOTAL_CHARS) {
133
+ throw { type: 'invalid_input', message: '方程文本总长超过 100KB 上限' };
134
+ }
135
+ const vars = (args && Array.isArray(args.variables)) ? args.variables : [];
136
+ if (vars.length > MAX_VAR_COUNT) {
137
+ throw { type: 'invalid_input', message: `变量数量超过上限 ${MAX_VAR_COUNT}` };
138
+ }
139
+ const domain = (args && args.domain) || undefined;
140
+ const fastMode = !!(args && args.fastMode);
141
+ const opts = (args && args.options) || {};
142
+ // 输出精度固定为 6 位小数(产品规格「6位小数有限网格」),与界面一致,不提供位数切换
143
+ const r = solve(eqs, vars, 6, domain, fastMode, opts);
144
+ return shapeResult(r);
145
+ }
146
+
147
+ // ---- 工具定义 ----
148
+ const TOOLS = [
149
+ {
150
+ name: 'solve',
151
+ description: '求解实数方程组的确定性数值引擎(非大模型,无随机、同输入输出可复现)。适用:需可验证、可复现的实数解(代数或 sin/cos/tan/log/exp/sqrt/abs 等常见超越函数),尤其给 AI Agent 当"不会胡说"的数学后端。' +
152
+ '不适用:纯符号推导/闭式证明、微分方程初值问题、整数/必不等于等强制约束(暂不支持)。' +
153
+ '输入:equations 为含 "=" 的方程字符串数组,如 ["x^2+y^2=25","x+y=7"];variables 可选(不填自动识别,最多6个);domain 可选(如 {"x":[-30,30]}),否则默认每变量 ±1e6。' +
154
+ '硬限制:变量 ≤6;方程 1–64 条且数量须 ≥ 变量数;单次方程文本 ≤100KB;输出固定 6 位小数(不可切换)。' +
155
+ '输出(JSON):resultType=empty(严格证无实数解)/finite(有限已验证解)/infinite(无限解集,仅给距原点最近推荐解);summary=中文一句话总览;solutions[] 每解含 values[](6位小数数值)、tier(proven=Krawczyk已认证/likely/candidate)、certified、text(人类可读如"x=4.000000, y=3.000000"),残差等内部数值收在 internals 子块(机器可跳过);certified=是否全proven;recommended=距原点最近解的精简结构。' +
156
+ 'truncated=true:预算内未完成全局分支判定、未证明已穷尽——不等于一定漏解,多数情况全部真解已找到;极端病态下可能遗漏个别解,可缩 domain 或提高 budget 重试。' +
157
+ '错误返回 error.type(invalid_input=输入不合法/超限,internal_error=内部异常)。遇卡点或认为结果有误,请调用 give_feedback(内容仅落本地日志,不外传)。相同输入永远返回完全相同结果,可安全缓存与重复调用。',
158
+ inputSchema: {
159
+ type: 'object',
160
+ properties: {
161
+ equations: {
162
+ type: 'array', items: { type: 'string' },
163
+ description: '方程字符串数组,如 ["x^2 + y^2 = 25", "x + y = 7"]。支持 + - * / ^ sqrt log sin cos tan exp abs,以及 in-text 域约束 "x ∈ [-30,30]"。'
164
+ },
165
+ variables: {
166
+ type: 'array', items: { type: 'string' },
167
+ description: '变量名数组(可选)。不填则由方程文本自动识别,顺序按出现。最多 6 个。'
168
+ },
169
+ domain: {
170
+ type: 'object',
171
+ description: '显式搜索域(可选)。形如 {"x":[-30,30],"y":[-30,30]}。对"有限解·部分"演示或快增长函数(exp/sinh)建议显式给定,否则默认 ±1e6 可能剪枝失效并触发 truncated。'
172
+ },
173
+ fastMode: { type: 'boolean', description: '快速模式(默认 false)' },
174
+ options: { type: 'object', description: '高级选项(可选),如 {budget:500000, maxDepth:28}' }
175
+ },
176
+ required: ['equations']
177
+ }
178
+ },
179
+ {
180
+ name: 'give_feedback',
181
+ description: 'AI 智能体在调用 solve 遇到卡点、错误、或认为结果有问题时,主动回报。' +
182
+ '回报内容仅落本地 feedback.log,不会外传。帮助作者持续改进。',
183
+ inputSchema: {
184
+ type: 'object',
185
+ properties: {
186
+ message: { type: 'string', description: '反馈内容:遇到了什么、期望什么、实际得到什么。' },
187
+ context: { type: 'string', description: '可选上下文:触发场景、输入特征等。' }
188
+ },
189
+ required: ['message']
190
+ }
191
+ }
192
+ ];
193
+
194
+ // ---- stdio 帧格式:换行符分隔的 JSON(与官方 MCP SDK 客户端一致)----
195
+ // 官方 SDK 的 serializeMessage 写 `JSON + '\n'`,ReadBuffer 按行解析;
196
+ // 故服务端也必须用同样格式收发,否则 Claude/Cursor/Cline 等 stdio 客户端收不到响应。
197
+ let lineBuf = '';
198
+
199
+ function send(obj) {
200
+ process.stdout.write(JSON.stringify(obj) + '\n');
201
+ }
202
+
203
+ function handle(msg) {
204
+ const id = msg.id;
205
+ const method = msg.method;
206
+ const params = msg.params || {};
207
+
208
+ if (method === 'initialize') {
209
+ send({
210
+ jsonrpc: '2.0', id,
211
+ result: {
212
+ protocolVersion: '2024-11-05',
213
+ capabilities: { tools: {} },
214
+ serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
215
+ }
216
+ });
217
+ return;
218
+ }
219
+ if (method === 'tools/list') {
220
+ send({ jsonrpc: '2.0', id, result: { tools: TOOLS } });
221
+ return;
222
+ }
223
+ if (method === 'tools/call') {
224
+ const name = params.name;
225
+ const args = params.arguments || {};
226
+ const t0 = Date.now();
227
+ try {
228
+ let result;
229
+ if (name === 'solve') {
230
+ result = doSolve(args);
231
+ } else if (name === 'give_feedback') {
232
+ const msg_fb = (args.message || '').toString().slice(0, 2000);
233
+ fs.appendFileSync(FEEDBACK_PATH, JSON.stringify({
234
+ ts: new Date().toISOString(), message: msg_fb, context: args.context || null
235
+ }) + '\n');
236
+ result = { acknowledged: true, note: '反馈已记录(本地,不外传)' };
237
+ } else {
238
+ throw { type: 'unknown_tool', message: '未知工具: ' + name };
239
+ }
240
+ const dt = Date.now() - t0;
241
+ appendLog({
242
+ ts: new Date().toISOString(), tool: name, status: 'ok',
243
+ dtMs: dt, resultType: result.resultType, nSol: result.solutionCount,
244
+ truncated: result.truncated
245
+ });
246
+ send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } });
247
+ } catch (e) {
248
+ const dt = Date.now() - t0;
249
+ const errObj = (e && e.type) ? e : { type: 'internal_error', message: (e && e.message) || String(e) };
250
+ appendLog({
251
+ ts: new Date().toISOString(), tool: name, status: 'error',
252
+ dtMs: dt, errorType: errObj.type
253
+ });
254
+ // 工具级错误:返回 isError=true 的工具结果(而非 JSON-RPC error 帧),
255
+ // 让接入的 LLM 能读到结构化错误并自我纠正,而不是收到一个异常。
256
+ send({
257
+ jsonrpc: '2.0', id,
258
+ result: { content: [{ type: 'text', text: JSON.stringify(errObj, null, 2) }], isError: true }
259
+ });
260
+ }
261
+ return;
262
+ }
263
+ // 其他方法:忽略(含通知,无 id 不回包)
264
+ }
265
+
266
+ function pump() {
267
+ let nl;
268
+ while ((nl = lineBuf.indexOf('\n')) !== -1) {
269
+ const line = lineBuf.slice(0, nl).replace(/\r$/, '');
270
+ lineBuf = lineBuf.slice(nl + 1);
271
+ if (!line.trim()) continue;
272
+ try {
273
+ const msg = JSON.parse(line);
274
+ if (msg && msg.jsonrpc === '2.0') handle(msg);
275
+ } catch (_e) { /* 畸形行忽略 */ }
276
+ }
277
+ }
278
+
279
+ process.stdin.setEncoding('utf8');
280
+ process.stdin.on('data', (chunk) => {
281
+ lineBuf += chunk;
282
+ pump();
283
+ });
284
+ // 不强制 process.exit,避免最后一个响应帧被截断丢失
285
+ process.stdin.on('end', () => {});
286
+
287
+ // 启动日志(仅元数据)
288
+ appendLog({ ts: new Date().toISOString(), event: 'server_start', name: SERVER_NAME, version: SERVER_VERSION });