@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 +189 -189
- package/dist/cloudOperators.js +159 -7
- package/dist/cloudTools.js +19 -3
- package/dist/index.js +94 -35
- package/dist/prompt/compose.js +75 -75
- package/package.json +58 -58
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.
|
package/dist/cloudOperators.js
CHANGED
|
@@ -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
|
-
|
|
507
|
-
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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`,
|
package/dist/cloudTools.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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':
|
package/dist/prompt/compose.js
CHANGED
|
@@ -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.
|
|
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
|
+
}
|