@heuresis/mcp 1.0.0-rc.14 → 1.0.0-rc.15
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/dist/cloudTools.js +22 -7
- package/dist/index.js +52 -2
- package/dist/zod-to-json-schema.js +10 -0
- package/package.json +1 -1
package/dist/cloudTools.js
CHANGED
|
@@ -545,18 +545,29 @@ export async function linkConcepts(client, args) {
|
|
|
545
545
|
if (args.fromId === args.toId) {
|
|
546
546
|
return { error: 'Self-loop edges are not allowed.' };
|
|
547
547
|
}
|
|
548
|
-
|
|
548
|
+
// These are .maybeSingle() lookups whose null result is MEANINGFUL ("not
|
|
549
|
+
// found" / "no duplicate") — do NOT wrap them in unwrap(). unwrap() throws on
|
|
550
|
+
// a null `data`, so the dup-check below blew up with "Empty result from
|
|
551
|
+
// cloud" on EVERY first-time link, meaning no non-partition edge (derived-
|
|
552
|
+
// from / k-ref / semantic-adjacency, incl. add_kref) could ever be created.
|
|
553
|
+
const fromRes = await client
|
|
549
554
|
.from('nodes')
|
|
550
555
|
.select('id, workspace_id')
|
|
551
556
|
.eq('id', args.fromId)
|
|
552
|
-
.maybeSingle()
|
|
557
|
+
.maybeSingle();
|
|
558
|
+
if (fromRes.error)
|
|
559
|
+
throw new Error(fromRes.error.message);
|
|
560
|
+
const from = fromRes.data;
|
|
553
561
|
if (!from)
|
|
554
562
|
return { error: `No concept with id ${args.fromId}` };
|
|
555
|
-
const
|
|
563
|
+
const toRes = await client
|
|
556
564
|
.from('nodes')
|
|
557
565
|
.select('id, workspace_id')
|
|
558
566
|
.eq('id', args.toId)
|
|
559
|
-
.maybeSingle()
|
|
567
|
+
.maybeSingle();
|
|
568
|
+
if (toRes.error)
|
|
569
|
+
throw new Error(toRes.error.message);
|
|
570
|
+
const to = toRes.data;
|
|
560
571
|
if (!to)
|
|
561
572
|
return { error: `No concept with id ${args.toId}` };
|
|
562
573
|
if (from.workspace_id !== to.workspace_id) {
|
|
@@ -564,14 +575,18 @@ export async function linkConcepts(client, args) {
|
|
|
564
575
|
error: 'Cannot link concepts from different workspaces.',
|
|
565
576
|
};
|
|
566
577
|
}
|
|
567
|
-
// Reject duplicate edges of the same kind on the same pair.
|
|
568
|
-
|
|
578
|
+
// Reject duplicate edges of the same kind on the same pair. A null here is
|
|
579
|
+
// the normal "no existing edge" case — handle it directly, never unwrap().
|
|
580
|
+
const dupRes = await client
|
|
569
581
|
.from('edges')
|
|
570
582
|
.select('id')
|
|
571
583
|
.eq('from_id', from.id)
|
|
572
584
|
.eq('to_id', to.id)
|
|
573
585
|
.eq('kind', args.kind)
|
|
574
|
-
.maybeSingle()
|
|
586
|
+
.maybeSingle();
|
|
587
|
+
if (dupRes.error)
|
|
588
|
+
throw new Error(dupRes.error.message);
|
|
589
|
+
const dup = dupRes.data;
|
|
575
590
|
if (dup) {
|
|
576
591
|
return { id: dup.id, fromId: from.id, toId: to.id, kind: args.kind, duplicate: true };
|
|
577
592
|
}
|
package/dist/index.js
CHANGED
|
@@ -209,7 +209,22 @@ async function runServer() {
|
|
|
209
209
|
inputSchema: zodToJsonSchema(t.inputSchema),
|
|
210
210
|
})),
|
|
211
211
|
}));
|
|
212
|
-
|
|
212
|
+
// The heavy operator/LLM tools are serialized: several fired in parallel
|
|
213
|
+
// overwhelm the backend and trip the 60s timeout (observed: parallel
|
|
214
|
+
// expand_concept runs timing out). Read/write tools still run concurrently.
|
|
215
|
+
const OPERATOR_TOOLS = new Set([
|
|
216
|
+
'run_operator',
|
|
217
|
+
'run_operator_and_commit',
|
|
218
|
+
'expand_concept',
|
|
219
|
+
]);
|
|
220
|
+
let operatorChain = Promise.resolve();
|
|
221
|
+
const runExclusive = (fn) => {
|
|
222
|
+
const result = operatorChain.then(fn, fn);
|
|
223
|
+
// Keep the chain alive regardless of this call's outcome.
|
|
224
|
+
operatorChain = result.then(() => undefined, () => undefined);
|
|
225
|
+
return result;
|
|
226
|
+
};
|
|
227
|
+
server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
|
|
213
228
|
const tool = tools.find((t) => t.name === req.params.name);
|
|
214
229
|
if (!tool) {
|
|
215
230
|
return {
|
|
@@ -219,8 +234,39 @@ async function runServer() {
|
|
|
219
234
|
],
|
|
220
235
|
};
|
|
221
236
|
}
|
|
237
|
+
// Heartbeat — emit progress notifications so MCP clients that honor them
|
|
238
|
+
// keep resetting their request timeout. Operator/LLM runs routinely exceed
|
|
239
|
+
// the 60s default, where the response would time out even though the work
|
|
240
|
+
// already committed (which also drove duplicate retries). No-op when the
|
|
241
|
+
// client supplied no progressToken.
|
|
242
|
+
const progressToken = req.params._meta?.progressToken;
|
|
243
|
+
let heartbeat;
|
|
244
|
+
if (progressToken !== undefined) {
|
|
245
|
+
let ticks = 0;
|
|
246
|
+
heartbeat = setInterval(() => {
|
|
247
|
+
ticks += 1;
|
|
248
|
+
try {
|
|
249
|
+
void extra
|
|
250
|
+
.sendNotification({
|
|
251
|
+
method: 'notifications/progress',
|
|
252
|
+
params: {
|
|
253
|
+
progressToken,
|
|
254
|
+
progress: ticks,
|
|
255
|
+
message: `still working… (~${ticks * 15}s)`,
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
.catch(() => { });
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
/* a heartbeat failure must never break the call */
|
|
262
|
+
}
|
|
263
|
+
}, 15_000);
|
|
264
|
+
}
|
|
222
265
|
try {
|
|
223
|
-
const
|
|
266
|
+
const invoke = () => tool.handler(req.params.arguments ?? {});
|
|
267
|
+
const result = OPERATOR_TOOLS.has(tool.name)
|
|
268
|
+
? await runExclusive(invoke)
|
|
269
|
+
: await invoke();
|
|
224
270
|
const text = JSON.stringify(result, null, 2);
|
|
225
271
|
if (text.length > MAX_RESULT_CHARS) {
|
|
226
272
|
return {
|
|
@@ -246,6 +292,10 @@ async function runServer() {
|
|
|
246
292
|
content: [{ type: 'text', text: `Error: ${msg}` }],
|
|
247
293
|
};
|
|
248
294
|
}
|
|
295
|
+
finally {
|
|
296
|
+
if (heartbeat)
|
|
297
|
+
clearInterval(heartbeat);
|
|
298
|
+
}
|
|
249
299
|
});
|
|
250
300
|
const transport = new StdioServerTransport();
|
|
251
301
|
await server.connect(transport);
|
|
@@ -59,6 +59,16 @@ function leafSchema(schema) {
|
|
|
59
59
|
// Nested object — recurse via the public path.
|
|
60
60
|
return zodToJsonSchema(cur);
|
|
61
61
|
}
|
|
62
|
+
else if (cur instanceof z.ZodRecord) {
|
|
63
|
+
// Open-ended string→value map (e.g. an operator's `args`). It MUST declare
|
|
64
|
+
// type:object — otherwise MCP clients don't JSON-parse the value, they send
|
|
65
|
+
// it as a raw string, and the server's validator rejects it ("Expected
|
|
66
|
+
// object, received string"). That silently disabled every parameterized
|
|
67
|
+
// operator (run_operator / run_operator_and_commit: free-text angle,
|
|
68
|
+
// contradiction improving/worsening, combine combineWithIds).
|
|
69
|
+
out.type = 'object';
|
|
70
|
+
out.additionalProperties = true;
|
|
71
|
+
}
|
|
62
72
|
else {
|
|
63
73
|
// Unknown / unsupported — fall back to "any".
|
|
64
74
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heuresis/mcp",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.15",
|
|
4
4
|
"mcpName": "io.github.ToremLabs/heuresis",
|
|
5
5
|
"description": "Cloud-authenticated Model Context Protocol server for a Heuresis workspace. Logs into the user's Heuresis account and lets any MCP client (Claude Desktop, Claude Code, Cursor, custom agents) read and write the same workspace the webapp uses. 31 data tools, 3 operator tools (Branch/Matrix/C-K/ASIT/TRIZ/Free/Combine/Explore), and live Realtime change subscriptions.",
|
|
6
6
|
"type": "module",
|