@heuresis/mcp 1.0.0-rc.15 → 1.0.0-rc.17

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/README.md CHANGED
@@ -1,189 +1,189 @@
1
- # @heuresis/mcp
2
-
3
- A Model Context Protocol (MCP) server that exposes a Heuresis workspace
4
- to any MCP-capable client (Claude Desktop, Claude Code, Cursor,
5
- Windsurf, custom agents). The server logs into the user's Heuresis
6
- account, talks to the same Supabase project the webapp talks to, and
7
- respects the same RLS. Webapp and MCP are two front-ends to one cloud
8
- workspace.
9
-
10
- Current version: `1.0.0-rc.13`.
11
-
12
- ## Install
13
-
14
- ```bash
15
- npm install -g @heuresis/mcp
16
- # or on demand without installing:
17
- npx -y @heuresis/mcp
18
- ```
19
-
20
- > **Package name vs. command name.** The npm package is `@heuresis/mcp`; the
21
- > command it installs is `heuresis-mcp`. A bare `npx -y @heuresis/mcp` (no
22
- > subcommand) starts the MCP server fine, but `npx @heuresis/mcp login` can
23
- > fail with `heuresis-mcp: not found` because npx derives the command name
24
- > from the scope-stripped package name (`mcp`), which doesn't match. To run a
25
- > subcommand reliably on every npm/OS, name the binary explicitly with `-p`:
26
- >
27
- > ```bash
28
- > npx -y -p @heuresis/mcp heuresis-mcp login
29
- > ```
30
-
31
- ## Quickstart
32
-
33
- ### 1. Link this machine to your Heuresis account
34
-
35
- ```bash
36
- npx -y -p @heuresis/mcp heuresis-mcp login
37
- ```
38
-
39
- The CLI prints a device code and a one-click URL of the form
40
- `https://heuresis.app/device?code=XXXX-XXXX`. Open it in your browser,
41
- sign in if you aren't already, and confirm the device. The CLI polls
42
- in the background and writes credentials to
43
- `~/.heuresis/credentials.json` (chmod 600 on POSIX) the moment you
44
- confirm. Subsequent runs of the MCP are silent.
45
-
46
- The login flow rides three Supabase Edge Functions:
47
- `mcp-device-init`, `mcp-device-grant`, and `mcp-device-poll`.
48
-
49
- To unlink a machine: `npx -y -p @heuresis/mcp heuresis-mcp logout`, or open
50
- Settings ▸ Connected devices in the webapp to revoke remotely.
51
-
52
- `npx -y -p @heuresis/mcp heuresis-mcp whoami` confirms which account a machine
53
- is currently linked to.
54
-
55
- ### 2. Point your MCP client at it
56
-
57
- **Claude Desktop.** Edit
58
- `~/Library/Application Support/Claude/claude_desktop_config.json` on
59
- macOS, or `%APPDATA%/Claude/claude_desktop_config.json` on Windows:
60
-
61
- ```json
62
- {
63
- "mcpServers": {
64
- "heuresis": { "command": "npx", "args": ["-y", "@heuresis/mcp"] }
65
- }
66
- }
67
- ```
68
-
69
- **Claude Code / Cursor / Windsurf.** Drop a `.mcp.json` in the
70
- workspace root:
71
-
72
- ```json
73
- {
74
- "mcpServers": {
75
- "heuresis": { "command": "npx", "args": ["-y", "@heuresis/mcp"] }
76
- }
77
- }
78
- ```
79
-
80
- Restart the client. The Heuresis tools appear in the tool menu.
81
-
82
- ### 3. CLI subcommands
83
-
84
- ```bash
85
- npx -y -p @heuresis/mcp heuresis-mcp whoami # show the linked account + device
86
- npx -y -p @heuresis/mcp heuresis-mcp logout # delete the credentials file
87
- npx -y -p @heuresis/mcp heuresis-mcp --help # all options
88
- npx -y @heuresis/mcp --no-realtime # boot the server with live sync off (persisted)
89
- npx -y @heuresis/mcp --realtime # re-enable live sync
90
- ```
91
-
92
- ## Headless mode (CI, cloud agents, disposable containers)
93
-
94
- Device pairing writes a **refresh token** to disk. That works great on a
95
- personal machine, but it does **not** survive disposable/ephemeral
96
- environments (CI runners, cloud agent containers, "Claude Code on the web"):
97
- the filesystem is wiped between runs, and a Supabase refresh token is
98
- **single-use under rotation** — so a token baked into config dies after the
99
- first session.
100
-
101
- For those environments, skip pairing and let the server **sign in fresh on
102
- every boot** from your account email + password (a password is not consumed on
103
- use, so it works forever with no re-pairing). Set three env vars:
104
-
105
- ```bash
106
- HEURESIS_EMAIL=you@example.com # your Heuresis account email
107
- HEURESIS_PASSWORD=your-account-password # secret — store it in a secrets manager
108
- HEURESIS_ANON_KEY=sb_publishable_... # project anon/publishable key (public, not a secret)
109
- # optional: HEURESIS_SUPABASE_URL=... # defaults to the production project
110
- ```
111
-
112
- When `HEURESIS_EMAIL` + `HEURESIS_PASSWORD` are present they take precedence
113
- over any `credentials.json`, and the MCP server authenticates per boot — no
114
- device link required. Requirements:
115
-
116
- - Email + password sign-in must be enabled for the Supabase project, and the
117
- account must have a password set (passwordless / magic-link-only accounts
118
- need a password added first).
119
- - Treat `HEURESIS_PASSWORD` as a secret. Prefer a dedicated account if your
120
- environment can only expose env vars that are visible to its users.
121
-
122
- ## Live sync
123
-
124
- When the MCP boots in cloud mode it subscribes to the workspace over
125
- Supabase Realtime and notifies the client whenever a `nodes`, `edges`,
126
- `projects`, or `ideas` row changes. Edits made in the webapp show up
127
- in the agent's view without a manual refresh, and writes from one
128
- MCP-connected client reach any other connected client the same way.
129
- Pass `--no-realtime` to disable the subscription (useful if the
130
- chatter is noisy or the client logs every notification). The
131
- preference is saved to `~/.heuresis/config.json` so the flag only
132
- needs to be passed once.
133
-
134
- ## Tools
135
-
136
- 34 tools total: 31 data tools against the cloud workspace, plus 3
137
- operator tools that drive the same ideation operators the webapp uses.
138
-
139
- **Reads (10).** `get_workspace_summary`, `list_projects`,
140
- `get_project_graph`, `list_concepts`, `list_edges`, `get_subtree`,
141
- `get_concept`, `search_concepts`, `find_concepts`,
142
- `list_recent_decisions`. Most agent sessions start with
143
- `get_workspace_summary` or `list_projects`.
144
-
145
- **Writes (21).** Concepts: `add_concept`, `update_concept`,
146
- `bulk_add_concepts`, `set_parent`, `validate_concept`, `set_standing`,
147
- `archive_concept`, `unarchive_concept`, `star_concept`,
148
- `remove_concept`. Edges: `link_concepts`, `add_kref`. Ideas:
149
- `create_idea`, `rename_idea`, `recolor_idea`, `set_idea_members`,
150
- `add_to_idea`, `delete_idea`. Projects: `create_project`,
151
- `update_project`, `delete_project`. Every write stamps a row in
152
- `public.provenance` with `origin='mcp'` so the webapp's session log
153
- shows which surface made the change.
154
-
155
- **Operator runs (3).** `run_operator` (generate candidates with
156
- Branch / Matrix / ASIT / TRIZ / Combine / Free / Contradiction),
157
- `run_operator_and_commit` (same, plus commit the result in one
158
- round-trip), and `expand_concept` (recursive Branch, capped at depth ×
159
- breadth ≤ 60).
160
-
161
- Tool input shapes mirror their counterparts in the webapp's
162
- `src/agent/tools.ts`, so an agent that uses both surfaces sees a
163
- uniform contract.
164
-
165
- Wave-shipping: `find_in_files` (in-browser embedding search) is in the
166
- webapp but not yet on the MCP.
167
-
168
- ## Legacy snapshot mode (deprecated)
169
-
170
- The original read-only snapshot reader still works as a fallback while
171
- users migrate to cloud auth. With no `~/.heuresis/credentials.json`
172
- and the `HEURESIS_SNAPSHOT` env var set, the server reads a JSON
173
- export from disk and exposes the original read-only tool set
174
- (`get_workspace_summary`, `list_projects`, `search_concepts`,
175
- `get_concept`, `get_subtree`, `get_project_graph`,
176
- `list_recent_decisions`).
177
-
178
- ```bash
179
- export HEURESIS_SNAPSHOT="/absolute/path/to/your-export.json"
180
- npx @heuresis/mcp
181
- ```
182
-
183
- This path is deprecated and will be removed in a later release. It is
184
- here so existing setups keep working through the migration to cloud
185
- auth.
186
-
187
- ## License
188
-
189
- AGPL-3.0-or-later.
1
+ # @heuresis/mcp
2
+
3
+ A Model Context Protocol (MCP) server that exposes a Heuresis workspace
4
+ to any MCP-capable client (Claude Desktop, Claude Code, Cursor,
5
+ Windsurf, custom agents). The server logs into the user's Heuresis
6
+ account, talks to the same Supabase project the webapp talks to, and
7
+ respects the same RLS. Webapp and MCP are two front-ends to one cloud
8
+ workspace.
9
+
10
+ Current version: `1.0.0-rc.13`.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install -g @heuresis/mcp
16
+ # or on demand without installing:
17
+ npx -y @heuresis/mcp
18
+ ```
19
+
20
+ > **Package name vs. command name.** The npm package is `@heuresis/mcp`; the
21
+ > command it installs is `heuresis-mcp`. A bare `npx -y @heuresis/mcp` (no
22
+ > subcommand) starts the MCP server fine, but `npx @heuresis/mcp login` can
23
+ > fail with `heuresis-mcp: not found` because npx derives the command name
24
+ > from the scope-stripped package name (`mcp`), which doesn't match. To run a
25
+ > subcommand reliably on every npm/OS, name the binary explicitly with `-p`:
26
+ >
27
+ > ```bash
28
+ > npx -y -p @heuresis/mcp heuresis-mcp login
29
+ > ```
30
+
31
+ ## Quickstart
32
+
33
+ ### 1. Link this machine to your Heuresis account
34
+
35
+ ```bash
36
+ npx -y -p @heuresis/mcp heuresis-mcp login
37
+ ```
38
+
39
+ The CLI prints a device code and a one-click URL of the form
40
+ `https://heuresis.app/device?code=XXXX-XXXX`. Open it in your browser,
41
+ sign in if you aren't already, and confirm the device. The CLI polls
42
+ in the background and writes credentials to
43
+ `~/.heuresis/credentials.json` (chmod 600 on POSIX) the moment you
44
+ confirm. Subsequent runs of the MCP are silent.
45
+
46
+ The login flow rides three Supabase Edge Functions:
47
+ `mcp-device-init`, `mcp-device-grant`, and `mcp-device-poll`.
48
+
49
+ To unlink a machine: `npx -y -p @heuresis/mcp heuresis-mcp logout`, or open
50
+ Settings ▸ Connected devices in the webapp to revoke remotely.
51
+
52
+ `npx -y -p @heuresis/mcp heuresis-mcp whoami` confirms which account a machine
53
+ is currently linked to.
54
+
55
+ ### 2. Point your MCP client at it
56
+
57
+ **Claude Desktop.** Edit
58
+ `~/Library/Application Support/Claude/claude_desktop_config.json` on
59
+ macOS, or `%APPDATA%/Claude/claude_desktop_config.json` on Windows:
60
+
61
+ ```json
62
+ {
63
+ "mcpServers": {
64
+ "heuresis": { "command": "npx", "args": ["-y", "@heuresis/mcp"] }
65
+ }
66
+ }
67
+ ```
68
+
69
+ **Claude Code / Cursor / Windsurf.** Drop a `.mcp.json` in the
70
+ workspace root:
71
+
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "heuresis": { "command": "npx", "args": ["-y", "@heuresis/mcp"] }
76
+ }
77
+ }
78
+ ```
79
+
80
+ Restart the client. The Heuresis tools appear in the tool menu.
81
+
82
+ ### 3. CLI subcommands
83
+
84
+ ```bash
85
+ npx -y -p @heuresis/mcp heuresis-mcp whoami # show the linked account + device
86
+ npx -y -p @heuresis/mcp heuresis-mcp logout # delete the credentials file
87
+ npx -y -p @heuresis/mcp heuresis-mcp --help # all options
88
+ npx -y @heuresis/mcp --no-realtime # boot the server with live sync off (persisted)
89
+ npx -y @heuresis/mcp --realtime # re-enable live sync
90
+ ```
91
+
92
+ ## Headless mode (CI, cloud agents, disposable containers)
93
+
94
+ Device pairing writes a **refresh token** to disk. That works great on a
95
+ personal machine, but it does **not** survive disposable/ephemeral
96
+ environments (CI runners, cloud agent containers, "Claude Code on the web"):
97
+ the filesystem is wiped between runs, and a Supabase refresh token is
98
+ **single-use under rotation** — so a token baked into config dies after the
99
+ first session.
100
+
101
+ For those environments, skip pairing and let the server **sign in fresh on
102
+ every boot** from your account email + password (a password is not consumed on
103
+ use, so it works forever with no re-pairing). Set three env vars:
104
+
105
+ ```bash
106
+ HEURESIS_EMAIL=you@example.com # your Heuresis account email
107
+ HEURESIS_PASSWORD=your-account-password # secret — store it in a secrets manager
108
+ HEURESIS_ANON_KEY=sb_publishable_... # project anon/publishable key (public, not a secret)
109
+ # optional: HEURESIS_SUPABASE_URL=... # defaults to the production project
110
+ ```
111
+
112
+ When `HEURESIS_EMAIL` + `HEURESIS_PASSWORD` are present they take precedence
113
+ over any `credentials.json`, and the MCP server authenticates per boot — no
114
+ device link required. Requirements:
115
+
116
+ - Email + password sign-in must be enabled for the Supabase project, and the
117
+ account must have a password set (passwordless / magic-link-only accounts
118
+ need a password added first).
119
+ - Treat `HEURESIS_PASSWORD` as a secret. Prefer a dedicated account if your
120
+ environment can only expose env vars that are visible to its users.
121
+
122
+ ## Live sync
123
+
124
+ When the MCP boots in cloud mode it subscribes to the workspace over
125
+ Supabase Realtime and notifies the client whenever a `nodes`, `edges`,
126
+ `projects`, or `ideas` row changes. Edits made in the webapp show up
127
+ in the agent's view without a manual refresh, and writes from one
128
+ MCP-connected client reach any other connected client the same way.
129
+ Pass `--no-realtime` to disable the subscription (useful if the
130
+ chatter is noisy or the client logs every notification). The
131
+ preference is saved to `~/.heuresis/config.json` so the flag only
132
+ needs to be passed once.
133
+
134
+ ## Tools
135
+
136
+ 34 tools total: 31 data tools against the cloud workspace, plus 3
137
+ operator tools that drive the same ideation operators the webapp uses.
138
+
139
+ **Reads (10).** `get_workspace_summary`, `list_projects`,
140
+ `get_project_graph`, `list_concepts`, `list_edges`, `get_subtree`,
141
+ `get_concept`, `search_concepts`, `find_concepts`,
142
+ `list_recent_decisions`. Most agent sessions start with
143
+ `get_workspace_summary` or `list_projects`.
144
+
145
+ **Writes (21).** Concepts: `add_concept`, `update_concept`,
146
+ `bulk_add_concepts`, `set_parent`, `validate_concept`, `set_standing`,
147
+ `archive_concept`, `unarchive_concept`, `star_concept`,
148
+ `remove_concept`. Edges: `link_concepts`, `add_kref`. Ideas:
149
+ `create_idea`, `rename_idea`, `recolor_idea`, `set_idea_members`,
150
+ `add_to_idea`, `delete_idea`. Projects: `create_project`,
151
+ `update_project`, `delete_project`. Every write stamps a row in
152
+ `public.provenance` with `origin='mcp'` so the webapp's session log
153
+ shows which surface made the change.
154
+
155
+ **Operator runs (3).** `run_operator` (generate candidates with
156
+ Branch / Matrix / ASIT / TRIZ / Combine / Free / Contradiction),
157
+ `run_operator_and_commit` (same, plus commit the result in one
158
+ round-trip), and `expand_concept` (recursive Branch, capped at depth ×
159
+ breadth ≤ 60).
160
+
161
+ Tool input shapes mirror their counterparts in the webapp's
162
+ `src/agent/tools.ts`, so an agent that uses both surfaces sees a
163
+ uniform contract.
164
+
165
+ Wave-shipping: `find_in_files` (in-browser embedding search) is in the
166
+ webapp but not yet on the MCP.
167
+
168
+ ## Legacy snapshot mode (deprecated)
169
+
170
+ The original read-only snapshot reader still works as a fallback while
171
+ users migrate to cloud auth. With no `~/.heuresis/credentials.json`
172
+ and the `HEURESIS_SNAPSHOT` env var set, the server reads a JSON
173
+ export from disk and exposes the original read-only tool set
174
+ (`get_workspace_summary`, `list_projects`, `search_concepts`,
175
+ `get_concept`, `get_subtree`, `get_project_graph`,
176
+ `list_recent_decisions`).
177
+
178
+ ```bash
179
+ export HEURESIS_SNAPSHOT="/absolute/path/to/your-export.json"
180
+ npx @heuresis/mcp
181
+ ```
182
+
183
+ This path is deprecated and will be removed in a later release. It is
184
+ here so existing setups keep working through the migration to cloud
185
+ auth.
186
+
187
+ ## License
188
+
189
+ AGPL-3.0-or-later.
@@ -28,6 +28,7 @@
28
28
  // Cost preview — every run_operator response carries an `estimated_cost:
29
29
  // { credits, dollars }` chip from the rate card in `docs/credits.md` §2.
30
30
  // Informational only; BYO-key runs don't bill against managed credits.
31
+ import { randomUUID } from 'node:crypto';
31
32
  import { z } from 'zod';
32
33
  import { unwrap } from './cloudClient.js';
33
34
  import { ASIT_OPERATORS } from './operators/asit.js';
@@ -205,6 +206,10 @@ export const runOperatorInput = z
205
206
  .record(z.unknown())
206
207
  .optional()
207
208
  .describe("Family-specific extras: { angle?: string } for free/explore/combine, { improving: number, worsening: number } for contradiction, { combineWithIds: string[] } for combine. Optional { provider: 'anthropic' | 'openai' | 'openrouter' | 'google' } overrides the default provider."),
209
+ force: z
210
+ .boolean()
211
+ .optional()
212
+ .describe('Bypass idempotent reuse and force a fresh run even if an identical run is already in flight or recently finished.'),
208
213
  })
209
214
  .strict();
210
215
  export async function runOperator(client, args) {
@@ -502,28 +507,175 @@ export async function expandConcept(client, args) {
502
507
  },
503
508
  };
504
509
  }
505
- // ---------------------------------------------------------------------------
506
- // OPERATOR_TOOLS export index.ts splices this onto CLOUD_TOOLS and wires
507
- // each entry's `handler(client, args)` to the lazy auth handshake.
508
- // ---------------------------------------------------------------------------
510
+ const FAST_PATH_MS = 8_000; // return inline if the run settles this fast
511
+ const REUSE_MS = 10 * 60_000; // idempotent-reuse window for completed runs
512
+ const MAX_RUNS = 200; // cap the in-memory registry
513
+ const MAX_CONCURRENT_OPS = 2; // heavy LLM bodies allowed in flight at once
514
+ const runsById = new Map();
515
+ const runIdByOpKey = new Map();
516
+ let activeOps = 0;
517
+ const opQueue = [];
518
+ function acquireOpSlot() {
519
+ if (activeOps < MAX_CONCURRENT_OPS) {
520
+ activeOps += 1;
521
+ return Promise.resolve();
522
+ }
523
+ return new Promise((resolve) => opQueue.push(resolve));
524
+ }
525
+ function releaseOpSlot() {
526
+ const next = opQueue.shift();
527
+ if (next)
528
+ next();
529
+ else
530
+ activeOps -= 1;
531
+ }
532
+ function stableKey(v) {
533
+ if (Array.isArray(v))
534
+ return v.map(stableKey);
535
+ if (v && typeof v === 'object') {
536
+ const o = v;
537
+ return Object.keys(o)
538
+ .sort()
539
+ .reduce((acc, k) => {
540
+ if (k !== 'force')
541
+ acc[k] = stableKey(o[k]); // `force` never affects identity
542
+ return acc;
543
+ }, {});
544
+ }
545
+ return v;
546
+ }
547
+ function makeOpKey(tool, args) {
548
+ return JSON.stringify([tool, stableKey(args)]);
549
+ }
550
+ function startOperatorRun(tool, args, work, force = false) {
551
+ const opKey = makeOpKey(tool, args);
552
+ if (!force) {
553
+ const prevId = runIdByOpKey.get(opKey);
554
+ const prev = prevId ? runsById.get(prevId) : undefined;
555
+ if (prev &&
556
+ (prev.status === 'running' ||
557
+ (prev.status === 'done' &&
558
+ prev.finishedAt !== undefined &&
559
+ Date.now() - prev.finishedAt < REUSE_MS))) {
560
+ return prev; // idempotent reuse — no duplicate run/commit
561
+ }
562
+ }
563
+ const run = {
564
+ runId: randomUUID(),
565
+ opKey,
566
+ tool,
567
+ status: 'running',
568
+ startedAt: Date.now(),
569
+ };
570
+ runsById.set(run.runId, run);
571
+ runIdByOpKey.set(opKey, run.runId);
572
+ if (runsById.size > MAX_RUNS) {
573
+ let oldest;
574
+ for (const r of runsById.values())
575
+ if (!oldest || r.startedAt < oldest.startedAt)
576
+ oldest = r;
577
+ if (oldest) {
578
+ runsById.delete(oldest.runId);
579
+ if (runIdByOpKey.get(oldest.opKey) === oldest.runId)
580
+ runIdByOpKey.delete(oldest.opKey);
581
+ }
582
+ }
583
+ void (async () => {
584
+ await acquireOpSlot();
585
+ try {
586
+ run.result = await work();
587
+ run.status = 'done';
588
+ }
589
+ catch (err) {
590
+ run.status = 'error';
591
+ run.error = err instanceof Error ? err.message : String(err);
592
+ }
593
+ finally {
594
+ run.finishedAt = Date.now();
595
+ releaseOpSlot();
596
+ }
597
+ })();
598
+ return run;
599
+ }
600
+ async function settleWithin(run, ms) {
601
+ const deadline = Date.now() + ms;
602
+ while (run.status === 'running' && Date.now() < deadline) {
603
+ await new Promise((r) => setTimeout(r, 200));
604
+ }
605
+ }
606
+ function runView(run) {
607
+ if (run.status === 'done')
608
+ return { runId: run.runId, status: 'done', result: run.result };
609
+ if (run.status === 'error')
610
+ return { runId: run.runId, status: 'error', error: run.error };
611
+ return {
612
+ runId: run.runId,
613
+ status: 'running',
614
+ hint: `Operator still running (a heavy model can take 1-2 min). Call get_run with runId "${run.runId}" — it returns the result the moment status is "done".`,
615
+ };
616
+ }
617
+ /**
618
+ * Run an operator asynchronously: start it (or reuse an identical in-flight /
619
+ * recent run), return inline if it settles within FAST_PATH_MS, otherwise hand
620
+ * back a runId for the agent to poll via get_run.
621
+ */
622
+ async function runAsync(tool, args, work) {
623
+ const force = Boolean(args?.force);
624
+ const run = startOperatorRun(tool, args, work, force);
625
+ await settleWithin(run, FAST_PATH_MS);
626
+ return runView(run);
627
+ }
628
+ export const getRunInput = z
629
+ .object({
630
+ runId: z
631
+ .string()
632
+ .min(1)
633
+ .describe('The runId returned by run_operator / run_operator_and_commit / expand_concept.'),
634
+ })
635
+ .strict();
509
636
  export const OPERATOR_TOOLS = [
510
637
  {
511
638
  name: 'run_operator',
512
639
  description: "Run an ASIT / TRIZ / Contradiction / Free / Combine / Explore operator against one concept (the anchor). Returns CANDIDATE concepts WITHOUT committing them — call `bulk_add_concepts` (or `run_operator_and_commit`) to persist. Use this when you want the agent to vet the candidates before writing. The anchor must already live in a project; the operator pulls in ancestry + validated knowledge in the same project to ground the prompt.",
513
640
  inputSchema: runOperatorInput,
514
- handler: async (client, raw) => runOperator(client, runOperatorInput.parse(raw)),
641
+ handler: async (client, raw) => {
642
+ const args = runOperatorInput.parse(raw);
643
+ return runAsync('run_operator', args, () => runOperator(client, args));
644
+ },
515
645
  },
516
646
  {
517
647
  name: 'run_operator_and_commit',
518
648
  description: "Same as `run_operator`, but immediately writes every top-level candidate as a child of the anchor with a partition edge and a provenance row stamped origin='mcp'. Use when the agent is confident the candidates should land directly on the canvas (e.g. inside an autonomous expand loop). Children-of-children proposed in `partitions[].children` are NOT auto-committed — request them via a separate run.",
519
649
  inputSchema: runOperatorAndCommitInput,
520
- handler: async (client, raw) => runOperatorAndCommit(client, runOperatorAndCommitInput.parse(raw)),
650
+ handler: async (client, raw) => {
651
+ const args = runOperatorAndCommitInput.parse(raw);
652
+ return runAsync('run_operator_and_commit', args, () => runOperatorAndCommit(client, args));
653
+ },
521
654
  },
522
655
  {
523
656
  name: 'expand_concept',
524
657
  description: 'Recursive Branch — expand a concept by `breadth` children at each of `depth` levels (depth*breadth ≤ 60). Commits each level immediately so the webapp shows partial results as the run progresses. Optionally takes an `angle` hint that biases the underlying EXPLORE operator. Best when the anchor has few or no children yet; on a dense subtree consider `run_operator(family="explore")` so the operator can read existing children and propose complementary partitions.',
525
658
  inputSchema: expandConceptInput,
526
- handler: async (client, raw) => expandConcept(client, expandConceptInput.parse(raw)),
659
+ handler: async (client, raw) => {
660
+ const args = expandConceptInput.parse(raw);
661
+ return runAsync('expand_concept', args, () => expandConcept(client, args));
662
+ },
663
+ },
664
+ {
665
+ name: 'get_run',
666
+ description: 'Fetch an async operator run by its runId. Returns { status: "running" | "done" | "error" }; when "done", the `result` field holds the operator output (candidates / committedIds). Waits up to ~8s for completion before returning, so a simple poll loop converges fast. run_operator / run_operator_and_commit / expand_concept hand back a runId whenever their work exceeds the ~8s fast-path window.',
667
+ inputSchema: getRunInput,
668
+ handler: async (_client, raw) => {
669
+ const { runId } = getRunInput.parse(raw);
670
+ const run = runsById.get(runId);
671
+ if (!run) {
672
+ return {
673
+ error: `No run with id ${runId}. Operator runs are kept in memory for the session; it may have been evicted (200-run cap) or the server restarted.`,
674
+ };
675
+ }
676
+ await settleWithin(run, FAST_PATH_MS);
677
+ return runView(run);
678
+ },
527
679
  },
528
680
  ];
529
681
  // Re-exports kept narrow on purpose: index.ts only needs `makeOperatorTools`,
@@ -188,11 +188,25 @@ function shapeProject(p) {
188
188
  // ---------------------------------------------------------------------------
189
189
  export const getSubtreeInput = z
190
190
  .object({
191
- rootId: z.string(),
191
+ rootId: z.string().min(1).optional(),
192
+ id: z
193
+ .string()
194
+ .min(1)
195
+ .optional()
196
+ .describe('Alias for rootId — the concept to root the subtree at (use either).'),
192
197
  depth: z.number().int().min(0).max(6).default(3),
193
198
  detail: detailArg,
194
199
  })
195
- .strict();
200
+ .strict()
201
+ .refine((a) => Boolean(a.rootId ?? a.id), {
202
+ message: 'Provide `id` (or `rootId`) — the concept to get the subtree for.',
203
+ path: ['id'],
204
+ })
205
+ .transform((a) => ({
206
+ rootId: (a.rootId ?? a.id),
207
+ depth: a.depth,
208
+ detail: a.detail,
209
+ }));
196
210
  export async function getSubtree(client, args) {
197
211
  const rootRes = await client.from('nodes').select('*').eq('id', args.rootId).maybeSingle();
198
212
  if (rootRes.error)
@@ -1554,7 +1568,9 @@ export const CLOUD_TOOLS = [
1554
1568
  },
1555
1569
  {
1556
1570
  name: 'get_subtree',
1557
- description: 'A node and its descendants up to a given depth. Use when the user asks "tell me about everything under X".',
1571
+ description: 'A node and its descendants up to a given depth. Pass the concept as `id` (or `rootId`). Use when the user asks "tell me about everything under X".',
1572
+ // ZodEffects (refine+transform for the id/rootId alias) — cast like the
1573
+ // operator tools; zodToJsonSchema unwraps it for the served schema.
1558
1574
  inputSchema: getSubtreeInput,
1559
1575
  handler: async (client, args) => getSubtree(client, getSubtreeInput.parse(args)),
1560
1576
  },
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@
14
14
  //
15
15
  // Subcommands (one-shot, never start the MCP server):
16
16
  // login | logout | whoami | --help
17
- import { existsSync } from 'node:fs';
17
+ import { existsSync, readFileSync } from 'node:fs';
18
18
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
19
19
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
20
20
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
@@ -27,8 +27,87 @@ import { CloudAuthError, getCloudClient, getCloudClientFromPassword } from './cl
27
27
  import { ensureProxyAgent } from './proxy.js';
28
28
  import { DEFAULT_SUPABASE_URL, helpCommand, loginCommand, logoutCommand, whoamiCommand, } from './cli.js';
29
29
  import { readRealtimeFlag, resolveSubscriptionWorkspaceId, startRealtimeSubscription, stripRealtimeFlags, } from './realtime.js';
30
- const VERSION = '0.2.0-alpha';
30
+ // Report the real published version (dist/index.js → ../package.json) so
31
+ // `heuresis-mcp --version` and the startup banner never lie about what's loaded.
32
+ const VERSION = (() => {
33
+ try {
34
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
35
+ return pkg.version ?? '0.0.0';
36
+ }
37
+ catch {
38
+ return '0.0.0';
39
+ }
40
+ })();
31
41
  const MAX_RESULT_CHARS = 50_000;
42
+ // Fields worth preserving when a node/edge-bearing result is degraded to fit
43
+ // the size cap (everything else — descriptions, rationale, tags — is dropped).
44
+ const SKELETON_KEYS = [
45
+ 'id',
46
+ 'label',
47
+ 'name',
48
+ 'parentId',
49
+ 'parent_id',
50
+ 'kind',
51
+ 'from',
52
+ 'to',
53
+ 'status',
54
+ 'standing',
55
+ 'starred',
56
+ 'projectId',
57
+ 'rootNodeId',
58
+ ];
59
+ function skeletonize(item) {
60
+ if (item && typeof item === 'object' && !Array.isArray(item)) {
61
+ const o = item;
62
+ const keep = {};
63
+ for (const k of SKELETON_KEYS)
64
+ if (k in o)
65
+ keep[k] = o[k];
66
+ return Object.keys(keep).length > 0 ? keep : item;
67
+ }
68
+ return item;
69
+ }
70
+ // Never hard-fail a read on size. If a result blows the cap, first drop verbose
71
+ // fields from its array elements (keep id/label/parent/kind…), then, if still
72
+ // too big, slice the largest array — always tagging `_truncated` + `_note` so
73
+ // the agent knows to narrow (limit/depth/scope) or fetch detail via get_concept.
74
+ function fitResultToCap(result, cap) {
75
+ if (JSON.stringify(result, null, 2).length <= cap)
76
+ return result;
77
+ if (!result || typeof result !== 'object' || Array.isArray(result)) {
78
+ const s = JSON.stringify(result, null, 2);
79
+ return {
80
+ _truncated: true,
81
+ _note: `Result exceeded ${cap} chars and could not be structurally reduced; showing a prefix. Narrow the query.`,
82
+ preview: s.slice(0, Math.max(0, cap - 200)),
83
+ };
84
+ }
85
+ const obj = { ...result };
86
+ const arrayKeys = Object.keys(obj).filter((k) => Array.isArray(obj[k]) && obj[k].some((x) => x && typeof x === 'object'));
87
+ for (const k of arrayKeys)
88
+ obj[k] = obj[k].map(skeletonize);
89
+ obj._truncated = true;
90
+ obj._note =
91
+ 'Result exceeded the size cap; verbose fields were dropped (id/label/parent/kind kept). Use get_concept(id) for full detail, or narrow with limit/depth/scope.';
92
+ let guard = 0;
93
+ while (JSON.stringify(obj, null, 2).length > cap && guard++ < 100) {
94
+ let biggestKey;
95
+ let biggestLen = 0;
96
+ for (const k of arrayKeys) {
97
+ const len = obj[k].length;
98
+ if (len > biggestLen) {
99
+ biggestLen = len;
100
+ biggestKey = k;
101
+ }
102
+ }
103
+ if (!biggestKey || biggestLen <= 1)
104
+ break;
105
+ const arr = obj[biggestKey];
106
+ obj[biggestKey] = arr.slice(0, Math.max(1, Math.floor(arr.length * 0.8)));
107
+ obj._note = `Result exceeded the size cap; showing a truncated, skeletonized view (some "${biggestKey}" omitted). Narrow with limit/depth/scope, or fetch specifics with get_concept.`;
108
+ }
109
+ return obj;
110
+ }
32
111
  function makeCloudTools(getClient, operatorTools) {
33
112
  // Lazy: defer the actual auth handshake until the first tool call so the
34
113
  // MCP server boots fast.
@@ -209,21 +288,6 @@ async function runServer() {
209
288
  inputSchema: zodToJsonSchema(t.inputSchema),
210
289
  })),
211
290
  }));
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
291
  server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
228
292
  const tool = tools.find((t) => t.name === req.params.name);
229
293
  if (!tool) {
@@ -263,24 +327,14 @@ async function runServer() {
263
327
  }, 15_000);
264
328
  }
265
329
  try {
266
- const invoke = () => tool.handler(req.params.arguments ?? {});
267
- const result = OPERATOR_TOOLS.has(tool.name)
268
- ? await runExclusive(invoke)
269
- : await invoke();
270
- const text = JSON.stringify(result, null, 2);
271
- if (text.length > MAX_RESULT_CHARS) {
272
- return {
273
- isError: true,
274
- content: [
275
- {
276
- type: 'text',
277
- text: `Result too large (${text.length} chars, limit ${MAX_RESULT_CHARS}). ` +
278
- `Narrow the query: lower 'limit'/'depth', keep detail='compact', ` +
279
- `or fetch individual nodes with get_concept.`,
280
- },
281
- ],
282
- };
283
- }
330
+ // Operator tools are async now: they return a runId fast and run in the
331
+ // background with their own concurrency control (cloudOperators.ts), so no
332
+ // per-call serialization is needed here.
333
+ const result = await tool.handler(req.params.arguments ?? {});
334
+ // Never hard-fail on size: auto-degrade node/edge-bearing results
335
+ // (skeletonize slice) so the agent always gets actionable structure
336
+ // back instead of an error it has to recover from.
337
+ const text = JSON.stringify(fitResultToCap(result, MAX_RESULT_CHARS), null, 2);
284
338
  return {
285
339
  content: [{ type: 'text', text }],
286
340
  };
@@ -373,6 +427,11 @@ async function main() {
373
427
  case 'whoami':
374
428
  await whoamiCommand();
375
429
  return;
430
+ case '-v':
431
+ case '--version':
432
+ case 'version':
433
+ console.log(VERSION);
434
+ return;
376
435
  case '-h':
377
436
  case '--help':
378
437
  case 'help':
@@ -8,31 +8,31 @@
8
8
  // target, knowledge pool, operator, plus the operator-specific inputs block.
9
9
  // File-context retrieval is a separate tool (find_in_files) that ships in
10
10
  // Agent B's tool-parity wave; not folded in here.
11
- const RESPONSE_TEMPLATE = `{
12
- "partitions": [
13
- {
14
- "label": "STANDALONE concept title — 2–5 words, ≤ 60 chars, NO parent prefix, no trailing period",
15
- "description": "1–2 sentences, ≤ 280 chars",
16
- "partitionAttribute": "≤ 5 words for the distinguishing attribute",
17
- "rationale": "1–3 sentences citing the operator and any K used",
18
- "kReferences": ["k_id_or_empty"],
19
- "selfCritique": "main weakness or assumption",
20
- "children": [
21
- {
22
- "label": "STANDALONE sub-concept title — same rules; do NOT prefix with this partition's label either",
23
- "description": "1–2 sentences, ≤ 280 chars",
24
- "partitionAttribute": "≤ 5 words",
25
- "rationale": "1–3 sentences",
26
- "kReferences": [],
27
- "selfCritique": "main weakness or assumption"
28
- }
29
- ]
30
- }
31
- ],
32
- "newKnowledgeProposed": [
33
- { "title": "fact title", "body": "1–2 sentences", "tags": ["tag1"] }
34
- ],
35
- "operatorNotes": "one line on how the operator fit (optional)"
11
+ const RESPONSE_TEMPLATE = `{
12
+ "partitions": [
13
+ {
14
+ "label": "STANDALONE concept title — 2–5 words, ≤ 60 chars, NO parent prefix, no trailing period",
15
+ "description": "1–2 sentences, ≤ 280 chars",
16
+ "partitionAttribute": "≤ 5 words for the distinguishing attribute",
17
+ "rationale": "1–3 sentences citing the operator and any K used",
18
+ "kReferences": ["k_id_or_empty"],
19
+ "selfCritique": "main weakness or assumption",
20
+ "children": [
21
+ {
22
+ "label": "STANDALONE sub-concept title — same rules; do NOT prefix with this partition's label either",
23
+ "description": "1–2 sentences, ≤ 280 chars",
24
+ "partitionAttribute": "≤ 5 words",
25
+ "rationale": "1–3 sentences",
26
+ "kReferences": [],
27
+ "selfCritique": "main weakness or assumption"
28
+ }
29
+ ]
30
+ }
31
+ ],
32
+ "newKnowledgeProposed": [
33
+ { "title": "fact title", "body": "1–2 sentences", "tags": ["tag1"] }
34
+ ],
35
+ "operatorNotes": "one line on how the operator fit (optional)"
36
36
  }`;
37
37
  function pathBlock(path) {
38
38
  return path
@@ -70,11 +70,11 @@ function contradictionBlock(c) {
70
70
  const principles = c.principles
71
71
  .map((p) => ` - #${p.num} ${p.name}: ${p.doctrine}`)
72
72
  .join('\n');
73
- return `<contradiction>
74
- improving: ${c.improvingName}
75
- worsening: ${c.worseningName}
76
- matrix_principles:
77
- ${principles}
73
+ return `<contradiction>
74
+ improving: ${c.improvingName}
75
+ worsening: ${c.worseningName}
76
+ matrix_principles:
77
+ ${principles}
78
78
  </contradiction>`;
79
79
  }
80
80
  export function composePrompt(input) {
@@ -92,50 +92,50 @@ export function composePrompt(input) {
92
92
  const contradictionXml = operator.family === 'CONTRADICTION' && contradiction
93
93
  ? `\n${contradictionBlock(contradiction)}\n`
94
94
  : '';
95
- return `You are assisting an inventive design session structured by C-K theory. The user is growing a graph of concepts (C) drawing on a pool of validated knowledge (K). You will generate a set of new partitions of the TARGET concept by applying the requested operator from ASIT/TRIZ.
96
-
97
- <brief>
98
- ${project.brief}
99
- </brief>
100
-
101
- <concept_path_root_to_target>
102
- ${pathBlock(ancestry)}
103
- </concept_path_root_to_target>
104
-
105
- <target_concept>
106
- id: ${target.id}
107
- label: ${target.label}
108
- description: ${target.description || '(no description)'}
109
- notes: ${target.notes || '(none)'}
110
- </target_concept>
111
-
112
- <knowledge_pool>
113
- ${knowledgeBlock(knowledge)}
114
- </knowledge_pool>
115
-
116
- <operator>
117
- family: ${operator.family}
118
- key: ${operator.key}
119
- name: ${operator.name}
120
- doctrine: ${operator.doctrine}
121
- </operator>
122
- ${inputsXml}${branchXml}${contradictionXml}${angleBlock}
123
- <instructions>
124
- ${operator.promptFragment}
125
-
126
- Rules:
127
- - Produce 3–5 partitions at the top level, each genuinely distinct, each adding a clear new attribute to the TARGET concept. (The optional \`children\` array below adds depth-2 nodes; it does NOT count toward the 3–5 top-level requirement.)
128
- - Labels MUST be STANDALONE concept titles. Do NOT prefix labels with the parent concept's label. For example, if the parent is "Test", do NOT write labels like "Test by destruction" or "Test for X" — just write "Destruction" or "X". The label should make sense on its own; the parent context is implicit from the graph structure. This rule applies to EVERY label in the response, including children (a child's label must not contain its immediate parent partition's label either).
129
- - Labels MUST be short: 2–5 words, ≤ 60 characters, no trailing punctuation. The label is a concept title, not a sentence. Put long-form prose in description/rationale, not in label.
130
- - Each partition MAY optionally include a \`children\` array of 1–4 sub-partitions, when the partition naturally decomposes further into a clearly distinct sub-axis. Children follow the same shape (label, description, partitionAttribute, rationale, kReferences, selfCritique). Do NOT nest beyond one level — a child must NEVER have its own \`children\` array. Omit \`children\` entirely when no useful sub-decomposition exists; do not pad.
131
- - Stay faithful to the operator's doctrine. If the operator forbids alien components (ASIT closed-world), do not introduce them.
132
- - For each partition, cite by id any knowledge item from <knowledge_pool> you actually used in kReferences. Empty array if none.
133
- - Use selfCritique to surface the strongest assumption or risk in that partition (do not flatter the idea).
134
- - If you needed a fact you did not have, propose it via newKnowledgeProposed (1–3 items max). Do NOT invent specific numbers as facts; phrase as questions to verify.
135
- - Output ONLY a single JSON object, matching this shape exactly. No prose before or after, no markdown fences.
136
- </instructions>
137
-
138
- <response_shape>
139
- ${RESPONSE_TEMPLATE}
95
+ return `You are assisting an inventive design session structured by C-K theory. The user is growing a graph of concepts (C) drawing on a pool of validated knowledge (K). You will generate a set of new partitions of the TARGET concept by applying the requested operator from ASIT/TRIZ.
96
+
97
+ <brief>
98
+ ${project.brief}
99
+ </brief>
100
+
101
+ <concept_path_root_to_target>
102
+ ${pathBlock(ancestry)}
103
+ </concept_path_root_to_target>
104
+
105
+ <target_concept>
106
+ id: ${target.id}
107
+ label: ${target.label}
108
+ description: ${target.description || '(no description)'}
109
+ notes: ${target.notes || '(none)'}
110
+ </target_concept>
111
+
112
+ <knowledge_pool>
113
+ ${knowledgeBlock(knowledge)}
114
+ </knowledge_pool>
115
+
116
+ <operator>
117
+ family: ${operator.family}
118
+ key: ${operator.key}
119
+ name: ${operator.name}
120
+ doctrine: ${operator.doctrine}
121
+ </operator>
122
+ ${inputsXml}${branchXml}${contradictionXml}${angleBlock}
123
+ <instructions>
124
+ ${operator.promptFragment}
125
+
126
+ Rules:
127
+ - Produce 3–5 partitions at the top level, each genuinely distinct, each adding a clear new attribute to the TARGET concept. (The optional \`children\` array below adds depth-2 nodes; it does NOT count toward the 3–5 top-level requirement.)
128
+ - Labels MUST be STANDALONE concept titles. Do NOT prefix labels with the parent concept's label. For example, if the parent is "Test", do NOT write labels like "Test by destruction" or "Test for X" — just write "Destruction" or "X". The label should make sense on its own; the parent context is implicit from the graph structure. This rule applies to EVERY label in the response, including children (a child's label must not contain its immediate parent partition's label either).
129
+ - Labels MUST be short: 2–5 words, ≤ 60 characters, no trailing punctuation. The label is a concept title, not a sentence. Put long-form prose in description/rationale, not in label.
130
+ - Each partition MAY optionally include a \`children\` array of 1–4 sub-partitions, when the partition naturally decomposes further into a clearly distinct sub-axis. Children follow the same shape (label, description, partitionAttribute, rationale, kReferences, selfCritique). Do NOT nest beyond one level — a child must NEVER have its own \`children\` array. Omit \`children\` entirely when no useful sub-decomposition exists; do not pad.
131
+ - Stay faithful to the operator's doctrine. If the operator forbids alien components (ASIT closed-world), do not introduce them.
132
+ - For each partition, cite by id any knowledge item from <knowledge_pool> you actually used in kReferences. Empty array if none.
133
+ - Use selfCritique to surface the strongest assumption or risk in that partition (do not flatter the idea).
134
+ - If you needed a fact you did not have, propose it via newKnowledgeProposed (1–3 items max). Do NOT invent specific numbers as facts; phrase as questions to verify.
135
+ - Output ONLY a single JSON object, matching this shape exactly. No prose before or after, no markdown fences.
136
+ </instructions>
137
+
138
+ <response_shape>
139
+ ${RESPONSE_TEMPLATE}
140
140
  </response_shape>`;
141
141
  }
package/package.json CHANGED
@@ -1,58 +1,58 @@
1
- {
2
- "name": "@heuresis/mcp",
3
- "version": "1.0.0-rc.15",
4
- "mcpName": "io.github.ToremLabs/heuresis",
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
- "type": "module",
7
- "bin": {
8
- "heuresis-mcp": "dist/index.js"
9
- },
10
- "files": [
11
- "dist",
12
- "README.md"
13
- ],
14
- "scripts": {
15
- "build": "tsc",
16
- "start": "node dist/index.js",
17
- "dev": "tsc --watch",
18
- "prepublishOnly": "npm run build"
19
- },
20
- "publishConfig": {
21
- "access": "public"
22
- },
23
- "homepage": "https://heuresis.app/mcp",
24
- "repository": {
25
- "type": "git",
26
- "url": "git+https://github.com/ToremLabs/Heuresis.git",
27
- "directory": "mcp-server"
28
- },
29
- "keywords": [
30
- "mcp",
31
- "model-context-protocol",
32
- "heuresis",
33
- "ideation",
34
- "knowledge-graph",
35
- "claude-code",
36
- "claude-desktop",
37
- "cursor"
38
- ],
39
- "dependencies": {
40
- "@anthropic-ai/sdk": "^0.40.0",
41
- "@google/generative-ai": "^0.21.0",
42
- "@modelcontextprotocol/sdk": "^1.0.0",
43
- "@supabase/supabase-js": "^2.45.0",
44
- "openai": "^4.71.0",
45
- "undici": "^6.25.0",
46
- "ws": "^8.18.0",
47
- "zod": "^3.23.0"
48
- },
49
- "devDependencies": {
50
- "@types/node": "^22.0.0",
51
- "@types/ws": "^8.5.13",
52
- "typescript": "^5.6.0"
53
- },
54
- "engines": {
55
- "node": ">=18"
56
- },
57
- "license": "AGPL-3.0-or-later"
58
- }
1
+ {
2
+ "name": "@heuresis/mcp",
3
+ "version": "1.0.0-rc.17",
4
+ "mcpName": "io.github.ToremLabs/heuresis",
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
+ "type": "module",
7
+ "bin": {
8
+ "heuresis-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "start": "node dist/index.js",
17
+ "dev": "tsc --watch",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "homepage": "https://heuresis.app/mcp",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/ToremLabs/Heuresis.git",
27
+ "directory": "mcp-server"
28
+ },
29
+ "keywords": [
30
+ "mcp",
31
+ "model-context-protocol",
32
+ "heuresis",
33
+ "ideation",
34
+ "knowledge-graph",
35
+ "claude-code",
36
+ "claude-desktop",
37
+ "cursor"
38
+ ],
39
+ "dependencies": {
40
+ "@anthropic-ai/sdk": "^0.40.0",
41
+ "@google/generative-ai": "^0.21.0",
42
+ "@modelcontextprotocol/sdk": "^1.0.0",
43
+ "@supabase/supabase-js": "^2.45.0",
44
+ "openai": "^4.71.0",
45
+ "undici": "^6.25.0",
46
+ "ws": "^8.18.0",
47
+ "zod": "^3.23.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^22.0.0",
51
+ "@types/ws": "^8.5.13",
52
+ "typescript": "^5.6.0"
53
+ },
54
+ "engines": {
55
+ "node": ">=18"
56
+ },
57
+ "license": "AGPL-3.0-or-later"
58
+ }