@heuresis/mcp 1.0.0-rc.11 → 1.0.0-rc.13
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 -159
- package/dist/cli.js +3 -1
- package/dist/cloudClient.js +52 -22
- package/dist/gotrue.js +57 -0
- package/dist/index.js +68 -26
- package/dist/wsPolyfill.js +12 -0
- package/dist/zod-to-json-schema.js +26 -1
- package/package.json +58 -56
package/README.md
CHANGED
|
@@ -1,159 +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.
|
|
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
|
-
##
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
`
|
|
113
|
-
`
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
`
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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/cli.js
CHANGED
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
//
|
|
24
24
|
// The webapp `/device` page calls a third Edge Function `mcp-device-grant`
|
|
25
25
|
// to attach the user's identity to the pending grant row.
|
|
26
|
+
// Polyfill global WebSocket on Node < 22 before any Supabase client is built.
|
|
27
|
+
import './wsPolyfill.js';
|
|
26
28
|
import { createInterface } from 'node:readline/promises';
|
|
27
29
|
import { stdin as input, stdout as output } from 'node:process';
|
|
28
30
|
import { credentialsPath, defaultDeviceName, deleteCredentials, readCredentials, writeCredentials, } from './credentials.js';
|
|
@@ -33,7 +35,7 @@ import { ensureProxyAgent } from './proxy.js';
|
|
|
33
35
|
// allow HEURESIS_SUPABASE_URL to override which Supabase project the CLI
|
|
34
36
|
// talks to (e.g. a staging instance). Both default to production.
|
|
35
37
|
const DEFAULT_DEVICE_BASE_URL = 'https://heuresis.app';
|
|
36
|
-
const DEFAULT_SUPABASE_URL = 'https://wpgniquyuppljeqkedqh.supabase.co';
|
|
38
|
+
export const DEFAULT_SUPABASE_URL = 'https://wpgniquyuppljeqkedqh.supabase.co';
|
|
37
39
|
const POLL_INTERVAL_MS = 5_000;
|
|
38
40
|
const POLL_TIMEOUT_MS = 15 * 60 * 1_000;
|
|
39
41
|
function log(...args) {
|
package/dist/cloudClient.js
CHANGED
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
// process runs. We don't have to do anything per-tool-call.
|
|
17
17
|
// 3. If the refresh fails (revoked, expired), the bootstrap throws — the
|
|
18
18
|
// wrapper surfaces a "re-run login" message.
|
|
19
|
+
// Polyfill global WebSocket on Node < 22 before any Supabase client is built.
|
|
20
|
+
import './wsPolyfill.js';
|
|
19
21
|
import { createClient } from '@supabase/supabase-js';
|
|
20
|
-
import { exchangeRefreshToken } from './gotrue.js';
|
|
22
|
+
import { exchangeRefreshToken, signInWithPassword } from './gotrue.js';
|
|
21
23
|
let cached = null;
|
|
22
24
|
export class CloudAuthError extends Error {
|
|
23
25
|
constructor(msg) {
|
|
@@ -26,14 +28,12 @@ export class CloudAuthError extends Error {
|
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* token
|
|
31
|
+
* Create a headless Supabase client and seed it with an already-obtained
|
|
32
|
+
* GoTrue session, so every subsequent PostgREST call carries the user's JWT
|
|
33
|
+
* and supabase-js keeps the in-memory access token alive. Caches the result.
|
|
32
34
|
*/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return cached;
|
|
36
|
-
const client = createClient(creds.supabase_url, creds.anon_key, {
|
|
35
|
+
async function seedClient(supabaseUrl, anonKey, session, userId) {
|
|
36
|
+
const client = createClient(supabaseUrl, anonKey, {
|
|
37
37
|
auth: {
|
|
38
38
|
// Headless: no localStorage, no URL detection, no auto-refresh
|
|
39
39
|
// listeners writing to disk. The library still auto-refreshes the
|
|
@@ -43,28 +43,58 @@ export async function getCloudClient(creds) {
|
|
|
43
43
|
detectSessionInUrl: false,
|
|
44
44
|
},
|
|
45
45
|
});
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
const { error } = await client.auth.setSession({
|
|
47
|
+
access_token: session.access_token,
|
|
48
|
+
refresh_token: session.refresh_token,
|
|
49
|
+
});
|
|
50
|
+
if (error) {
|
|
51
|
+
throw new CloudAuthError(`Failed to seed Heuresis session: ${error.message}.`);
|
|
52
|
+
}
|
|
53
|
+
cached = { client, userId };
|
|
54
|
+
return cached;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build (or return cached) a Supabase client bound to the credentials on
|
|
58
|
+
* disk. Bootstraps by exchanging the stored (rotating) refresh token. Throws
|
|
59
|
+
* CloudAuthError if the refresh token has been revoked/rotated away.
|
|
60
|
+
*
|
|
61
|
+
* NOTE: a stored refresh token is single-use under Supabase rotation, so this
|
|
62
|
+
* path is unsuitable for ephemeral environments that reuse the same persisted
|
|
63
|
+
* credential across boots — use getCloudClientFromPassword() for those.
|
|
64
|
+
*/
|
|
65
|
+
export async function getCloudClient(creds) {
|
|
66
|
+
if (cached)
|
|
67
|
+
return cached;
|
|
50
68
|
try {
|
|
51
69
|
const session = await exchangeRefreshToken(creds.supabase_url, creds.anon_key, creds.refresh_token);
|
|
52
|
-
|
|
53
|
-
access_token: session.access_token,
|
|
54
|
-
refresh_token: session.refresh_token,
|
|
55
|
-
});
|
|
56
|
-
if (error) {
|
|
57
|
-
throw new CloudAuthError(`Failed to seed Heuresis session: ${error.message}. ` +
|
|
58
|
-
'Run `npx -y -p @heuresis/mcp heuresis-mcp login` to re-authenticate.');
|
|
59
|
-
}
|
|
70
|
+
return await seedClient(creds.supabase_url, creds.anon_key, session, creds.user_id);
|
|
60
71
|
}
|
|
61
72
|
catch (err) {
|
|
62
73
|
if (err instanceof CloudAuthError)
|
|
63
74
|
throw err;
|
|
64
75
|
throw new CloudAuthError(`Failed to refresh Heuresis session: ${err instanceof Error ? err.message : String(err)}. Run \`npx -y -p @heuresis/mcp heuresis-mcp login\` to re-authenticate.`);
|
|
65
76
|
}
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Build (or return cached) a Supabase client by signing in fresh with an
|
|
80
|
+
* email + password. Because a password is not consumed on use, this works
|
|
81
|
+
* durably across disposable/ephemeral sessions that re-authenticate on every
|
|
82
|
+
* boot — no persisted, rotating refresh token required. Throws CloudAuthError
|
|
83
|
+
* on bad credentials or if password sign-in is disabled for the project.
|
|
84
|
+
*/
|
|
85
|
+
export async function getCloudClientFromPassword(supabaseUrl, anonKey, email, password) {
|
|
86
|
+
if (cached)
|
|
87
|
+
return cached;
|
|
88
|
+
try {
|
|
89
|
+
const session = await signInWithPassword(supabaseUrl, anonKey, email, password);
|
|
90
|
+
const userId = session.user?.id ?? '(unknown)';
|
|
91
|
+
return await seedClient(supabaseUrl, anonKey, session, userId);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
if (err instanceof CloudAuthError)
|
|
95
|
+
throw err;
|
|
96
|
+
throw new CloudAuthError(`Headless email/password sign-in failed: ${err instanceof Error ? err.message : String(err)}. Check HEURESIS_EMAIL / HEURESIS_PASSWORD / HEURESIS_ANON_KEY.`);
|
|
97
|
+
}
|
|
68
98
|
}
|
|
69
99
|
/** Clear the cached client. Used after logout. */
|
|
70
100
|
export function resetCloudClient() {
|
package/dist/gotrue.js
CHANGED
|
@@ -82,3 +82,60 @@ export async function exchangeRefreshToken(supabaseUrl, anonKey, refreshToken) {
|
|
|
82
82
|
}
|
|
83
83
|
return session;
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Sign in with an email + password directly against the GoTrue token endpoint:
|
|
87
|
+
*
|
|
88
|
+
* POST `${supabaseUrl}/auth/v1/token?grant_type=password`
|
|
89
|
+
* headers: { apikey, Authorization: Bearer <anon>, Content-Type: application/json }
|
|
90
|
+
* body: { email, password }
|
|
91
|
+
*
|
|
92
|
+
* Unlike a refresh token, a password is NOT consumed on use, so this is the
|
|
93
|
+
* right primitive for headless, ephemeral environments (e.g. cloud agent
|
|
94
|
+
* containers) that re-authenticate from scratch on every boot. Returns the
|
|
95
|
+
* full session (access_token + refresh_token + user). Throws
|
|
96
|
+
* `RefreshTokenError` with an actionable message on any failure.
|
|
97
|
+
*/
|
|
98
|
+
export async function signInWithPassword(supabaseUrl, anonKey, email, password) {
|
|
99
|
+
if (!email || !password) {
|
|
100
|
+
throw new RefreshTokenError('Headless sign-in needs both an email and a password (HEURESIS_EMAIL / HEURESIS_PASSWORD).');
|
|
101
|
+
}
|
|
102
|
+
const url = `${supabaseUrl.replace(/\/$/, '')}/auth/v1/token?grant_type=password`;
|
|
103
|
+
let res;
|
|
104
|
+
try {
|
|
105
|
+
res = await fetch(url, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: {
|
|
108
|
+
apikey: anonKey,
|
|
109
|
+
Authorization: `Bearer ${anonKey}`,
|
|
110
|
+
'Content-Type': 'application/json',
|
|
111
|
+
},
|
|
112
|
+
body: JSON.stringify({ email, password }),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
throw new RefreshTokenError(`Could not reach the auth endpoint at ${url}: ${err instanceof Error ? err.message : String(err)}`);
|
|
117
|
+
}
|
|
118
|
+
let payload = null;
|
|
119
|
+
try {
|
|
120
|
+
payload = await res.json();
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
/* leave null — handled below */
|
|
124
|
+
}
|
|
125
|
+
if (!res.ok) {
|
|
126
|
+
const err = payload;
|
|
127
|
+
const detail = err?.error_description ?? err?.msg ?? err?.error ?? err?.message ?? `HTTP ${res.status}`;
|
|
128
|
+
throw new RefreshTokenError(`Email/password sign-in failed (HTTP ${res.status}): ${detail}. ` +
|
|
129
|
+
'Check HEURESIS_EMAIL / HEURESIS_PASSWORD, and that email+password sign-in is ' +
|
|
130
|
+
'enabled for the Supabase project.');
|
|
131
|
+
}
|
|
132
|
+
const session = payload;
|
|
133
|
+
if (!session || !session.access_token || !session.refresh_token) {
|
|
134
|
+
const keys = session && typeof session === 'object'
|
|
135
|
+
? Object.keys(session).join(', ') || '(empty object)'
|
|
136
|
+
: '(no JSON body)';
|
|
137
|
+
throw new RefreshTokenError(`Sign-in succeeded (HTTP ${res.status}) but the response is missing ` +
|
|
138
|
+
`access_token/refresh_token. Response keys: [${keys}].`);
|
|
139
|
+
}
|
|
140
|
+
return session;
|
|
141
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -23,9 +23,9 @@ import { HeuresisStore } from './store.js';
|
|
|
23
23
|
import { getConcept as legacyGetConcept, getConceptInput as legacyGetConceptInput, getProjectGraph as legacyGetProjectGraph, getProjectGraphInput as legacyGetProjectGraphInput, getSubtree as legacyGetSubtree, getSubtreeInput as legacyGetSubtreeInput, getWorkspaceSummary as legacyGetWorkspaceSummary, getWorkspaceSummaryInput as legacyGetWorkspaceSummaryInput, listProjects as legacyListProjects, listProjectsInput as legacyListProjectsInput, listRecentDecisions as legacyListRecentDecisions, listRecentDecisionsInput as legacyListRecentDecisionsInput, searchConcepts as legacySearchConcepts, searchConceptsInput as legacySearchConceptsInput, } from './tools.js';
|
|
24
24
|
import { CLOUD_TOOLS } from './cloudTools.js';
|
|
25
25
|
import { readCredentials } from './credentials.js';
|
|
26
|
-
import { CloudAuthError, getCloudClient } from './cloudClient.js';
|
|
26
|
+
import { CloudAuthError, getCloudClient, getCloudClientFromPassword } from './cloudClient.js';
|
|
27
27
|
import { ensureProxyAgent } from './proxy.js';
|
|
28
|
-
import { helpCommand, loginCommand, logoutCommand, whoamiCommand, } from './cli.js';
|
|
28
|
+
import { DEFAULT_SUPABASE_URL, helpCommand, loginCommand, logoutCommand, whoamiCommand, } from './cli.js';
|
|
29
29
|
import { readRealtimeFlag, resolveSubscriptionWorkspaceId, startRealtimeSubscription, stripRealtimeFlags, } from './realtime.js';
|
|
30
30
|
const VERSION = '0.2.0-alpha';
|
|
31
31
|
const MAX_RESULT_CHARS = 50_000;
|
|
@@ -46,6 +46,21 @@ function makeCloudTools(getClient, operatorTools) {
|
|
|
46
46
|
handler: async (args) => t.handler(await getClient(), args),
|
|
47
47
|
}));
|
|
48
48
|
}
|
|
49
|
+
// Phase 19.5 — try to load LLM-backed Operator tools. The module may be absent
|
|
50
|
+
// or export nothing; in that case we fall back to just the Phase 19.4 parity
|
|
51
|
+
// set. Wrapping the dynamic import in try/catch keeps the server starting
|
|
52
|
+
// cleanly either way.
|
|
53
|
+
async function loadOperatorTools() {
|
|
54
|
+
try {
|
|
55
|
+
const mod = (await import('./cloudOperators.js').catch(() => null));
|
|
56
|
+
if (mod && Array.isArray(mod.OPERATOR_TOOLS))
|
|
57
|
+
return mod.OPERATOR_TOOLS;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* fall through to empty */
|
|
61
|
+
}
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
49
64
|
function makeLegacySnapshotTools(store) {
|
|
50
65
|
// LEGACY FALLBACK — removed after 19.7. Read-only, no auth, snapshot file.
|
|
51
66
|
return [
|
|
@@ -99,38 +114,59 @@ async function runServer() {
|
|
|
99
114
|
await ensureProxyAgent(console.error);
|
|
100
115
|
const creds = await readCredentials();
|
|
101
116
|
const snapshotEnv = process.env.HEURESIS_SNAPSHOT;
|
|
117
|
+
// Headless credential (durable across ephemeral/disposable sessions): when
|
|
118
|
+
// HEURESIS_EMAIL + HEURESIS_PASSWORD are set, the server signs in fresh on
|
|
119
|
+
// every boot. Unlike a persisted refresh token — which is single-use under
|
|
120
|
+
// Supabase rotation and dies after one session — a password is not consumed,
|
|
121
|
+
// so this survives container resets with zero re-pairing. It takes
|
|
122
|
+
// precedence over a (possibly stale) credentials.json.
|
|
123
|
+
const headlessEmail = process.env.HEURESIS_EMAIL?.trim();
|
|
124
|
+
const headlessPassword = process.env.HEURESIS_PASSWORD;
|
|
102
125
|
let tools;
|
|
103
126
|
let modeBanner;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
127
|
+
// Single cloud client getter, shared by the tool handlers and the realtime
|
|
128
|
+
// subscription. null in legacy snapshot / unconfigured modes.
|
|
129
|
+
let cloudGetClient = null;
|
|
130
|
+
if (headlessEmail && headlessPassword) {
|
|
131
|
+
// CLOUD mode — headless email/password sign-in (recommended for cloud /
|
|
132
|
+
// disposable containers).
|
|
133
|
+
const supabaseUrl = process.env.HEURESIS_SUPABASE_URL?.trim() || DEFAULT_SUPABASE_URL;
|
|
134
|
+
const anonKey = process.env.HEURESIS_ANON_KEY?.trim();
|
|
135
|
+
if (!anonKey) {
|
|
136
|
+
console.error([
|
|
137
|
+
'[heuresis-mcp] HEURESIS_EMAIL/HEURESIS_PASSWORD are set but HEURESIS_ANON_KEY is missing.',
|
|
138
|
+
'Set HEURESIS_ANON_KEY to your project anon/publishable key (it is public, not a secret).',
|
|
139
|
+
].join('\n'));
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
cloudGetClient = async () => {
|
|
107
143
|
try {
|
|
108
|
-
const { client } = await
|
|
144
|
+
const { client } = await getCloudClientFromPassword(supabaseUrl, anonKey, headlessEmail, headlessPassword);
|
|
109
145
|
return client;
|
|
110
146
|
}
|
|
111
147
|
catch (err) {
|
|
112
|
-
if (err instanceof CloudAuthError)
|
|
148
|
+
if (err instanceof CloudAuthError)
|
|
113
149
|
throw new Error(err.message);
|
|
114
|
-
}
|
|
115
150
|
throw err;
|
|
116
151
|
}
|
|
117
152
|
};
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
operatorTools = mod.OPERATOR_TOOLS;
|
|
153
|
+
tools = makeCloudTools(cloudGetClient, await loadOperatorTools());
|
|
154
|
+
modeBanner = `cloud-authenticated (headless ${headlessEmail}; ${tools.length} tools)`;
|
|
155
|
+
}
|
|
156
|
+
else if (creds) {
|
|
157
|
+
// CLOUD mode — persisted device credential (refresh-token bootstrap).
|
|
158
|
+
cloudGetClient = async () => {
|
|
159
|
+
try {
|
|
160
|
+
const { client } = await getCloudClient(creds);
|
|
161
|
+
return client;
|
|
128
162
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
163
|
+
catch (err) {
|
|
164
|
+
if (err instanceof CloudAuthError)
|
|
165
|
+
throw new Error(err.message);
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
tools = makeCloudTools(cloudGetClient, await loadOperatorTools());
|
|
134
170
|
modeBanner = `cloud-authenticated (user_id ${creds.user_id}, device ${creds.device_name}; ${tools.length} tools)`;
|
|
135
171
|
}
|
|
136
172
|
else if (snapshotEnv || hasDefaultSnapshot()) {
|
|
@@ -144,9 +180,15 @@ async function runServer() {
|
|
|
144
180
|
console.error([
|
|
145
181
|
'[heuresis-mcp] Not configured.',
|
|
146
182
|
'',
|
|
147
|
-
'To use cloud mode (
|
|
183
|
+
'To use cloud mode on a personal machine (device pairing):',
|
|
148
184
|
' npx -y -p @heuresis/mcp heuresis-mcp login',
|
|
149
185
|
'',
|
|
186
|
+
'To use cloud mode headlessly (CI / cloud agents / disposable containers),',
|
|
187
|
+
'set these env vars so the server signs in fresh on every boot:',
|
|
188
|
+
' HEURESIS_EMAIL your Heuresis account email',
|
|
189
|
+
' HEURESIS_PASSWORD your Heuresis account password',
|
|
190
|
+
' HEURESIS_ANON_KEY your project anon/publishable key (public, not a secret)',
|
|
191
|
+
'',
|
|
150
192
|
'To use legacy snapshot mode (deprecated, removed after 19.7):',
|
|
151
193
|
' HEURESIS_SNAPSHOT=/path/to/export.json npx @heuresis/mcp',
|
|
152
194
|
'',
|
|
@@ -210,7 +252,7 @@ async function runServer() {
|
|
|
210
252
|
console.error(`[heuresis-mcp ${VERSION}] ready - ${modeBanner}`);
|
|
211
253
|
// Phase 19.8 - Supabase Realtime CDC subscription. Cloud mode only; legacy
|
|
212
254
|
// snapshot mode has no live source to subscribe to.
|
|
213
|
-
if (
|
|
255
|
+
if (cloudGetClient) {
|
|
214
256
|
const realtimeOn = await readRealtimeFlag();
|
|
215
257
|
if (!realtimeOn) {
|
|
216
258
|
console.error('[heuresis-mcp] realtime: disabled (--no-realtime or config).');
|
|
@@ -220,7 +262,7 @@ async function runServer() {
|
|
|
220
262
|
// client (Supabase) is not reachable, the error surfaces on stderr.
|
|
221
263
|
void (async () => {
|
|
222
264
|
try {
|
|
223
|
-
const
|
|
265
|
+
const client = await cloudGetClient();
|
|
224
266
|
const wsId = await resolveSubscriptionWorkspaceId(client);
|
|
225
267
|
if (!wsId) {
|
|
226
268
|
console.error('[heuresis-mcp] realtime: no workspace visible; skipping subscription.');
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Node < 22 ships no global `WebSocket`, but @supabase/realtime-js requires
|
|
2
|
+
// one: a Supabase client builds a RealtimeClient in its constructor (even when
|
|
3
|
+
// realtime is never used), which throws "Node.js 20 detected without native
|
|
4
|
+
// WebSocket support" when no global WebSocket exists. Install the `ws`
|
|
5
|
+
// implementation as the global so createClient works on Node 18/20. No-op on
|
|
6
|
+
// Node 22+, where WebSocket is native. Imported for its side effect by every
|
|
7
|
+
// module that builds a Supabase client, before the first createClient call.
|
|
8
|
+
import WebSocket from 'ws';
|
|
9
|
+
const g = globalThis;
|
|
10
|
+
if (typeof g.WebSocket === 'undefined') {
|
|
11
|
+
g.WebSocket = WebSocket;
|
|
12
|
+
}
|
|
@@ -69,7 +69,32 @@ function leafSchema(schema) {
|
|
|
69
69
|
return out;
|
|
70
70
|
}
|
|
71
71
|
export function zodToJsonSchema(schema) {
|
|
72
|
-
|
|
72
|
+
// Peel wrappers to reach the underlying object whose `.shape` we can
|
|
73
|
+
// introspect: `.refine()`/`.transform()`/`.superRefine()` produce a
|
|
74
|
+
// ZodEffects (no `.shape`), and optional/default/nullable wrap the root too.
|
|
75
|
+
// Without this, a tool whose inputSchema is a ZodEffects (e.g. expand_concept)
|
|
76
|
+
// makes `Object.entries(undefined)` throw — which previously took down the
|
|
77
|
+
// ENTIRE tools/list response and caused MCP clients to drop the server.
|
|
78
|
+
let root = schema;
|
|
79
|
+
while (root && root._def) {
|
|
80
|
+
if (root instanceof z.ZodEffects) {
|
|
81
|
+
root = root._def.schema;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (root instanceof z.ZodOptional ||
|
|
85
|
+
root instanceof z.ZodDefault ||
|
|
86
|
+
root instanceof z.ZodNullable) {
|
|
87
|
+
root = root._def.innerType;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
const shape = root?.shape;
|
|
93
|
+
if (!shape || typeof shape !== 'object') {
|
|
94
|
+
// Not an object schema we can introspect — expose a permissive object so
|
|
95
|
+
// the tool still lists and still accepts its arguments.
|
|
96
|
+
return { type: 'object', additionalProperties: true };
|
|
97
|
+
}
|
|
73
98
|
const properties = {};
|
|
74
99
|
const required = [];
|
|
75
100
|
for (const [key, value] of Object.entries(shape)) {
|
package/package.json
CHANGED
|
@@ -1,56 +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
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@heuresis/mcp",
|
|
3
|
+
"version": "1.0.0-rc.13",
|
|
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
|
+
}
|