@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.
@@ -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
- const from = unwrap(await client
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 to = unwrap(await client
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
- const dup = unwrap(await client
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
- server.setRequestHandler(CallToolRequestSchema, async (req) => {
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 result = await tool.handler(req.params.arguments ?? {});
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.14",
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",