@heuresis/mcp 1.0.0-rc.14 → 1.0.0-rc.16
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 +22 -7
- package/dist/index.js +36 -1
- package/dist/prompt/compose.js +75 -75
- package/dist/zod-to-json-schema.js +10 -0
- 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
|
@@ -545,18 +545,29 @@ export async function linkConcepts(client, args) {
|
|
|
545
545
|
if (args.fromId === args.toId) {
|
|
546
546
|
return { error: 'Self-loop edges are not allowed.' };
|
|
547
547
|
}
|
|
548
|
-
|
|
548
|
+
// These are .maybeSingle() lookups whose null result is MEANINGFUL ("not
|
|
549
|
+
// found" / "no duplicate") — do NOT wrap them in unwrap(). unwrap() throws on
|
|
550
|
+
// a null `data`, so the dup-check below blew up with "Empty result from
|
|
551
|
+
// cloud" on EVERY first-time link, meaning no non-partition edge (derived-
|
|
552
|
+
// from / k-ref / semantic-adjacency, incl. add_kref) could ever be created.
|
|
553
|
+
const fromRes = await client
|
|
549
554
|
.from('nodes')
|
|
550
555
|
.select('id, workspace_id')
|
|
551
556
|
.eq('id', args.fromId)
|
|
552
|
-
.maybeSingle()
|
|
557
|
+
.maybeSingle();
|
|
558
|
+
if (fromRes.error)
|
|
559
|
+
throw new Error(fromRes.error.message);
|
|
560
|
+
const from = fromRes.data;
|
|
553
561
|
if (!from)
|
|
554
562
|
return { error: `No concept with id ${args.fromId}` };
|
|
555
|
-
const
|
|
563
|
+
const toRes = await client
|
|
556
564
|
.from('nodes')
|
|
557
565
|
.select('id, workspace_id')
|
|
558
566
|
.eq('id', args.toId)
|
|
559
|
-
.maybeSingle()
|
|
567
|
+
.maybeSingle();
|
|
568
|
+
if (toRes.error)
|
|
569
|
+
throw new Error(toRes.error.message);
|
|
570
|
+
const to = toRes.data;
|
|
560
571
|
if (!to)
|
|
561
572
|
return { error: `No concept with id ${args.toId}` };
|
|
562
573
|
if (from.workspace_id !== to.workspace_id) {
|
|
@@ -564,14 +575,18 @@ export async function linkConcepts(client, args) {
|
|
|
564
575
|
error: 'Cannot link concepts from different workspaces.',
|
|
565
576
|
};
|
|
566
577
|
}
|
|
567
|
-
// Reject duplicate edges of the same kind on the same pair.
|
|
568
|
-
|
|
578
|
+
// Reject duplicate edges of the same kind on the same pair. A null here is
|
|
579
|
+
// the normal "no existing edge" case — handle it directly, never unwrap().
|
|
580
|
+
const dupRes = await client
|
|
569
581
|
.from('edges')
|
|
570
582
|
.select('id')
|
|
571
583
|
.eq('from_id', from.id)
|
|
572
584
|
.eq('to_id', to.id)
|
|
573
585
|
.eq('kind', args.kind)
|
|
574
|
-
.maybeSingle()
|
|
586
|
+
.maybeSingle();
|
|
587
|
+
if (dupRes.error)
|
|
588
|
+
throw new Error(dupRes.error.message);
|
|
589
|
+
const dup = dupRes.data;
|
|
575
590
|
if (dup) {
|
|
576
591
|
return { id: dup.id, fromId: from.id, toId: to.id, kind: args.kind, duplicate: true };
|
|
577
592
|
}
|
package/dist/index.js
CHANGED
|
@@ -209,7 +209,7 @@ async function runServer() {
|
|
|
209
209
|
inputSchema: zodToJsonSchema(t.inputSchema),
|
|
210
210
|
})),
|
|
211
211
|
}));
|
|
212
|
-
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
212
|
+
server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
|
|
213
213
|
const tool = tools.find((t) => t.name === req.params.name);
|
|
214
214
|
if (!tool) {
|
|
215
215
|
return {
|
|
@@ -219,7 +219,38 @@ async function runServer() {
|
|
|
219
219
|
],
|
|
220
220
|
};
|
|
221
221
|
}
|
|
222
|
+
// Heartbeat — emit progress notifications so MCP clients that honor them
|
|
223
|
+
// keep resetting their request timeout. Operator/LLM runs routinely exceed
|
|
224
|
+
// the 60s default, where the response would time out even though the work
|
|
225
|
+
// already committed (which also drove duplicate retries). No-op when the
|
|
226
|
+
// client supplied no progressToken.
|
|
227
|
+
const progressToken = req.params._meta?.progressToken;
|
|
228
|
+
let heartbeat;
|
|
229
|
+
if (progressToken !== undefined) {
|
|
230
|
+
let ticks = 0;
|
|
231
|
+
heartbeat = setInterval(() => {
|
|
232
|
+
ticks += 1;
|
|
233
|
+
try {
|
|
234
|
+
void extra
|
|
235
|
+
.sendNotification({
|
|
236
|
+
method: 'notifications/progress',
|
|
237
|
+
params: {
|
|
238
|
+
progressToken,
|
|
239
|
+
progress: ticks,
|
|
240
|
+
message: `still working… (~${ticks * 15}s)`,
|
|
241
|
+
},
|
|
242
|
+
})
|
|
243
|
+
.catch(() => { });
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
/* a heartbeat failure must never break the call */
|
|
247
|
+
}
|
|
248
|
+
}, 15_000);
|
|
249
|
+
}
|
|
222
250
|
try {
|
|
251
|
+
// Operator tools are async now: they return a runId fast and run in the
|
|
252
|
+
// background with their own concurrency control (cloudOperators.ts), so no
|
|
253
|
+
// per-call serialization is needed here.
|
|
223
254
|
const result = await tool.handler(req.params.arguments ?? {});
|
|
224
255
|
const text = JSON.stringify(result, null, 2);
|
|
225
256
|
if (text.length > MAX_RESULT_CHARS) {
|
|
@@ -246,6 +277,10 @@ async function runServer() {
|
|
|
246
277
|
content: [{ type: 'text', text: `Error: ${msg}` }],
|
|
247
278
|
};
|
|
248
279
|
}
|
|
280
|
+
finally {
|
|
281
|
+
if (heartbeat)
|
|
282
|
+
clearInterval(heartbeat);
|
|
283
|
+
}
|
|
249
284
|
});
|
|
250
285
|
const transport = new StdioServerTransport();
|
|
251
286
|
await server.connect(transport);
|
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
|
}
|
|
@@ -59,6 +59,16 @@ function leafSchema(schema) {
|
|
|
59
59
|
// Nested object — recurse via the public path.
|
|
60
60
|
return zodToJsonSchema(cur);
|
|
61
61
|
}
|
|
62
|
+
else if (cur instanceof z.ZodRecord) {
|
|
63
|
+
// Open-ended string→value map (e.g. an operator's `args`). It MUST declare
|
|
64
|
+
// type:object — otherwise MCP clients don't JSON-parse the value, they send
|
|
65
|
+
// it as a raw string, and the server's validator rejects it ("Expected
|
|
66
|
+
// object, received string"). That silently disabled every parameterized
|
|
67
|
+
// operator (run_operator / run_operator_and_commit: free-text angle,
|
|
68
|
+
// contradiction improving/worsening, combine combineWithIds).
|
|
69
|
+
out.type = 'object';
|
|
70
|
+
out.additionalProperties = true;
|
|
71
|
+
}
|
|
62
72
|
else {
|
|
63
73
|
// Unknown / unsupported — fall back to "any".
|
|
64
74
|
}
|
package/package.json
CHANGED
|
@@ -1,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.16",
|
|
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
|
+
}
|