@heuresis/mcp 1.0.0-rc.1
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 +152 -0
- package/dist/cli.js +276 -0
- package/dist/cloudClient.js +71 -0
- package/dist/cloudOperators.js +530 -0
- package/dist/cloudTools.js +1727 -0
- package/dist/cloudTypes.js +8 -0
- package/dist/credentials.js +97 -0
- package/dist/index.js +294 -0
- package/dist/llm/client.js +155 -0
- package/dist/llm/cost.js +65 -0
- package/dist/operators/asit.js +50 -0
- package/dist/operators/combine.js +10 -0
- package/dist/operators/contradiction.js +13 -0
- package/dist/operators/explore.js +14 -0
- package/dist/operators/triz-matrix.js +1964 -0
- package/dist/operators/triz.js +23 -0
- package/dist/operators/types.js +10 -0
- package/dist/prompt/compose.js +141 -0
- package/dist/prompt/parse.js +99 -0
- package/dist/prompt/schema.js +30 -0
- package/dist/realtime.js +192 -0
- package/dist/store.js +128 -0
- package/dist/tools.js +264 -0
- package/dist/types.js +5 -0
- package/dist/zod-to-json-schema.js +89 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# @heuresis/mcp
|
|
2
|
+
|
|
3
|
+
A Model Context Protocol (MCP) server that turns the user's Heuresis
|
|
4
|
+
workspace into a thinking substrate any MCP-capable agent (Claude
|
|
5
|
+
Desktop, Claude Code, Cursor, Windsurf, custom agents) can read and
|
|
6
|
+
write — the webapp and the MCP become two front-ends to one cloud
|
|
7
|
+
workspace. The MCP logs into the user's Heuresis account, talks to the
|
|
8
|
+
same Supabase project the webapp talks to, and respects the same RLS.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install -g @heuresis/mcp
|
|
14
|
+
# or run on demand without installing:
|
|
15
|
+
npx -y @heuresis/mcp@latest
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
> Not yet published. While in alpha you can run it locally:
|
|
19
|
+
>
|
|
20
|
+
> ```bash
|
|
21
|
+
> cd mcp-server
|
|
22
|
+
> npm install
|
|
23
|
+
> npm run build
|
|
24
|
+
> node dist/index.js --help
|
|
25
|
+
> ```
|
|
26
|
+
|
|
27
|
+
## Quickstart
|
|
28
|
+
|
|
29
|
+
### 1. Link this machine to your Heuresis account
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx @heuresis/mcp login
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The CLI prints a short device code (`XXXX-XXXX`) and a URL. Open the URL in your browser, sign into Heuresis if you aren't already, paste the code, and confirm the device. The CLI polls in the background and writes your credentials to `~/.heuresis/credentials.json` (chmod 600 on POSIX) the moment you confirm. Subsequent runs of the MCP are silent.
|
|
36
|
+
|
|
37
|
+
To unlink a machine, run `npx @heuresis/mcp logout` locally, or open Settings ▸ Connected devices in the webapp to revoke remotely.
|
|
38
|
+
|
|
39
|
+
`npx @heuresis/mcp whoami` confirms which account a machine is currently linked to.
|
|
40
|
+
|
|
41
|
+
### 2. Point your MCP client at it
|
|
42
|
+
|
|
43
|
+
**Claude Desktop** — edit `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
44
|
+
on macOS, or `%APPDATA%/Claude/claude_desktop_config.json` on Windows:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"heuresis": { "command": "npx", "args": ["-y", "@heuresis/mcp"] }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Claude Code / Cursor / Windsurf** — drop a `.mcp.json` in the
|
|
55
|
+
workspace root:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"mcpServers": {
|
|
60
|
+
"heuresis": { "command": "npx", "args": ["-y", "@heuresis/mcp"] }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Restart the client. The Heuresis tools appear in the tool menu.
|
|
66
|
+
|
|
67
|
+
### 3. Optional CLI subcommands
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx @heuresis/mcp whoami # show the linked account + device
|
|
71
|
+
npx @heuresis/mcp logout # delete the credentials file
|
|
72
|
+
npx @heuresis/mcp --help # all options
|
|
73
|
+
npx @heuresis/mcp --no-realtime # boot once with live sync turned off (persisted)
|
|
74
|
+
npx @heuresis/mcp --realtime # re-enable live sync
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Live sync
|
|
78
|
+
|
|
79
|
+
When the MCP boots in cloud mode it subscribes to your workspace over
|
|
80
|
+
Supabase Realtime and notifies the client whenever a `nodes`, `edges`,
|
|
81
|
+
`projects`, or `ideas` row changes. Edits you make in the webapp show
|
|
82
|
+
up in your agent's view without a manual refresh, and writes from one
|
|
83
|
+
MCP-connected client reach any other connected client the same way.
|
|
84
|
+
Pass `--no-realtime` to disable the subscription (useful if the chatter
|
|
85
|
+
is noisy or your client logs every notification). The preference is
|
|
86
|
+
saved to `~/.heuresis/config.json` so you only have to pass the flag
|
|
87
|
+
once.
|
|
88
|
+
|
|
89
|
+
## Tools
|
|
90
|
+
|
|
91
|
+
This is the **Phase 19.1** tool surface. The remaining tools from
|
|
92
|
+
`docs/mcp-cloud.md` §4 ship in rolling waves under Phase 19.4.
|
|
93
|
+
|
|
94
|
+
### Reads
|
|
95
|
+
|
|
96
|
+
| Tool | Input |
|
|
97
|
+
| ------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
98
|
+
| `list_projects` | `{}` — every project in the workspace with brief, direction, lifecycle, and node count. |
|
|
99
|
+
| `get_project_graph` | `{ projectId, includeArchived?, detail? }` — every node + edge inside one project. |
|
|
100
|
+
| `get_subtree` | `{ rootId, depth?, detail? }` — a node and its descendants up to `depth` generations. |
|
|
101
|
+
| `get_concept` | `{ id, includeAncestry?, includeChildren?, includeIdeaMemberships? }` — one concept with full detail. |
|
|
102
|
+
| `search_concepts` | `{ query, limit?, projectId?, status?, detail? }` — substring search across label / description / partition / tags. Text-only; semantic search lands in 19.4. |
|
|
103
|
+
|
|
104
|
+
### Writes
|
|
105
|
+
|
|
106
|
+
| Tool | Input |
|
|
107
|
+
| ---------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
108
|
+
| `add_concept` | `{ label, description?, parentId?, projectId?, tags? }` — create a node; partition edge auto-created if parented. |
|
|
109
|
+
| `update_concept` | `{ id, label?, description?, tags?, partitionAttribute?, rationale?, status? }` — patch a node. |
|
|
110
|
+
| `link_concepts` | `{ fromId, toId, kind }` where `kind ∈ { 'k-ref', 'derived-from', 'semantic-adjacency' }` — create a non-partition edge. |
|
|
111
|
+
|
|
112
|
+
Each tool's input shape mirrors its counterpart in the webapp's
|
|
113
|
+
`src/agent/tools.ts`, so an agent that uses both surfaces sees a
|
|
114
|
+
uniform contract.
|
|
115
|
+
|
|
116
|
+
## Status
|
|
117
|
+
|
|
118
|
+
**Cloud-authenticated alpha (v0.2.0-alpha, Phase 19.1).**
|
|
119
|
+
|
|
120
|
+
- The 8 tools above hit Supabase live, against the same workspace your
|
|
121
|
+
webapp sees, under the same RLS.
|
|
122
|
+
- Full tool parity (the other 19 tools — operators, k-ref bulk ops,
|
|
123
|
+
validation, standing, ideas, projects-as-targets) ships in waves
|
|
124
|
+
under Phase 19.4 and is **not** included in this build.
|
|
125
|
+
- The `mcp-device-poll` / `mcp-device-grant` Edge Functions that make
|
|
126
|
+
`login` automatic ship in Phase 19.3. Until then the `login`
|
|
127
|
+
subcommand uses the manual paste workaround described above.
|
|
128
|
+
- The Settings ▸ Account ▸ Connected devices UI (where you'd revoke
|
|
129
|
+
this device) ships in Phase 19.6. Until then, revoke at the database
|
|
130
|
+
level.
|
|
131
|
+
|
|
132
|
+
## Legacy snapshot mode (deprecated)
|
|
133
|
+
|
|
134
|
+
The v0 read-only snapshot reader still works as a fallback while users
|
|
135
|
+
migrate. With no `~/.heuresis/credentials.json` and the
|
|
136
|
+
`HEURESIS_SNAPSHOT` env var set, the server reads a JSON export from
|
|
137
|
+
disk and exposes the v0 read-only tool set (`get_workspace_summary`,
|
|
138
|
+
`list_projects`, `search_concepts`, `get_concept`, `get_subtree`,
|
|
139
|
+
`get_project_graph`, `list_recent_decisions`).
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
export HEURESIS_SNAPSHOT="/absolute/path/to/your-export.json"
|
|
143
|
+
npx @heuresis/mcp
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
This path is **deprecated** and will be removed after Phase 19.7
|
|
147
|
+
(cloud-auth mandatory). It's here so the current install path keeps
|
|
148
|
+
working through the migration.
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
AGPL-3.0-or-later.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// Heuresis MCP — CLI subcommand handlers.
|
|
2
|
+
//
|
|
3
|
+
// `npx @heuresis/mcp` with no subcommand → start the MCP stdio server
|
|
4
|
+
// (run by Claude Desktop / Claude Code / etc.). With a subcommand it's a
|
|
5
|
+
// one-shot CLI:
|
|
6
|
+
// login — pair this machine with the user's Heuresis account
|
|
7
|
+
// logout — delete ~/.heuresis/credentials.json
|
|
8
|
+
// whoami — print the linked email + workspace
|
|
9
|
+
// --help — usage
|
|
10
|
+
//
|
|
11
|
+
// AUTH UX (Phase 19.3 — device-code poll flow).
|
|
12
|
+
// ----------------------------------------------------------
|
|
13
|
+
// 1. POST to the `mcp-device-init` Edge Function with the chosen device name.
|
|
14
|
+
// Receive a short XXXX-XXXX code + an expiry.
|
|
15
|
+
// 2. Tell the user to open https://heuresis.app/device (overridable via
|
|
16
|
+
// HEURESIS_DEVICE_BASE_URL for staging / self-hosted setups) and enter
|
|
17
|
+
// the code.
|
|
18
|
+
// 3. Poll `mcp-device-poll` every 5s until status: ok (claim accepted) or
|
|
19
|
+
// 410 (expired / already-used), or 15-minute timeout.
|
|
20
|
+
// 4. On success, write ~/.heuresis/credentials.json with the returned
|
|
21
|
+
// refresh_token + supabase_url + anon_key + user_id + device_name and
|
|
22
|
+
// print "Linked to <email>".
|
|
23
|
+
//
|
|
24
|
+
// The webapp `/device` page calls a third Edge Function `mcp-device-grant`
|
|
25
|
+
// to attach the user's identity to the pending grant row.
|
|
26
|
+
import { createInterface } from 'node:readline/promises';
|
|
27
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
28
|
+
import { createClient } from '@supabase/supabase-js';
|
|
29
|
+
import { credentialsPath, defaultDeviceName, deleteCredentials, readCredentials, writeCredentials, } from './credentials.js';
|
|
30
|
+
// Where the device pairing UI lives. Production default; can be overridden
|
|
31
|
+
// for staging / self-hosted deploys via HEURESIS_DEVICE_BASE_URL. We also
|
|
32
|
+
// allow HEURESIS_SUPABASE_URL to override which Supabase project the CLI
|
|
33
|
+
// talks to (e.g. a staging instance). Both default to production.
|
|
34
|
+
const DEFAULT_DEVICE_BASE_URL = 'https://heuresis.app';
|
|
35
|
+
const DEFAULT_SUPABASE_URL = 'https://heuresis.supabase.co';
|
|
36
|
+
const POLL_INTERVAL_MS = 5_000;
|
|
37
|
+
const POLL_TIMEOUT_MS = 15 * 60 * 1_000;
|
|
38
|
+
function log(...args) {
|
|
39
|
+
// Use stderr so we don't confuse MCP-client stdout parsers when this CLI
|
|
40
|
+
// is misconfigured into the MCP slot. stderr is always safe.
|
|
41
|
+
console.error(...args);
|
|
42
|
+
}
|
|
43
|
+
function printHelp() {
|
|
44
|
+
log([
|
|
45
|
+
'heuresis-mcp — Heuresis MCP server (cloud-authenticated, alpha)',
|
|
46
|
+
'',
|
|
47
|
+
'Usage:',
|
|
48
|
+
' npx @heuresis/mcp Start the MCP stdio server (run by Claude Desktop, Cursor, etc.)',
|
|
49
|
+
' npx @heuresis/mcp login Link this machine to your Heuresis account',
|
|
50
|
+
' --device-name <name> Override the default device name (hostname-shortRand).',
|
|
51
|
+
' npx @heuresis/mcp logout Remove the saved credentials',
|
|
52
|
+
' npx @heuresis/mcp whoami Show the linked account',
|
|
53
|
+
' npx @heuresis/mcp --help Show this message',
|
|
54
|
+
'',
|
|
55
|
+
'Credentials are stored at:',
|
|
56
|
+
` ${credentialsPath()}`,
|
|
57
|
+
'',
|
|
58
|
+
'Environment overrides:',
|
|
59
|
+
' HEURESIS_DEVICE_BASE_URL Webapp origin (default https://heuresis.app)',
|
|
60
|
+
' HEURESIS_SUPABASE_URL Supabase project URL (default the heuresis.app project)',
|
|
61
|
+
'',
|
|
62
|
+
'Legacy snapshot mode (deprecated, removed after 19.7):',
|
|
63
|
+
' HEURESIS_SNAPSHOT=/path/to/export.json npx @heuresis/mcp',
|
|
64
|
+
' …falls back to read-only behavior against a JSON export.',
|
|
65
|
+
].join('\n'));
|
|
66
|
+
}
|
|
67
|
+
async function prompt(question) {
|
|
68
|
+
const rl = createInterface({ input, output, terminal: true });
|
|
69
|
+
try {
|
|
70
|
+
return (await rl.question(question)).trim();
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
rl.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Parse `npx @heuresis/mcp login [--device-name <name>]`. */
|
|
77
|
+
function parseLoginFlags(argv) {
|
|
78
|
+
const opts = {};
|
|
79
|
+
for (let i = 0; i < argv.length; i++) {
|
|
80
|
+
const flag = argv[i];
|
|
81
|
+
if (flag === '--device-name' || flag === '--device') {
|
|
82
|
+
const v = argv[i + 1];
|
|
83
|
+
if (!v) {
|
|
84
|
+
log(`Missing value for ${flag}`);
|
|
85
|
+
process.exit(2);
|
|
86
|
+
}
|
|
87
|
+
opts.deviceName = v;
|
|
88
|
+
i++;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
log(`Unknown flag: ${flag}`);
|
|
92
|
+
process.exit(2);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return opts;
|
|
96
|
+
}
|
|
97
|
+
/** Sleep `ms` milliseconds. Resolves only — no rejection path. */
|
|
98
|
+
function sleep(ms) {
|
|
99
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
100
|
+
}
|
|
101
|
+
async function postJson(url, body) {
|
|
102
|
+
const res = await fetch(url, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: { 'Content-Type': 'application/json' },
|
|
105
|
+
body: JSON.stringify(body),
|
|
106
|
+
});
|
|
107
|
+
let data = null;
|
|
108
|
+
try {
|
|
109
|
+
data = await res.json();
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
/* leave null */
|
|
113
|
+
}
|
|
114
|
+
return { status: res.status, data };
|
|
115
|
+
}
|
|
116
|
+
export async function loginCommand(argv = []) {
|
|
117
|
+
const opts = parseLoginFlags(argv);
|
|
118
|
+
const deviceName = opts.deviceName ?? defaultDeviceName();
|
|
119
|
+
const deviceBaseUrl = process.env.HEURESIS_DEVICE_BASE_URL ?? DEFAULT_DEVICE_BASE_URL;
|
|
120
|
+
const supabaseUrl = process.env.HEURESIS_SUPABASE_URL ?? DEFAULT_SUPABASE_URL;
|
|
121
|
+
log('');
|
|
122
|
+
log('Heuresis MCP — device link (19.3)');
|
|
123
|
+
log('─'.repeat(50));
|
|
124
|
+
// 1. Init — allocate a pairing code.
|
|
125
|
+
const initUrl = `${supabaseUrl}/functions/v1/mcp-device-init`;
|
|
126
|
+
let initRes;
|
|
127
|
+
try {
|
|
128
|
+
initRes = await postJson(initUrl, { device_name: deviceName });
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
log('');
|
|
132
|
+
log(`Could not reach Heuresis at ${initUrl}.`);
|
|
133
|
+
log(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
134
|
+
log('If you are on a private / staging Supabase project, set HEURESIS_SUPABASE_URL.');
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
if (initRes.status !== 200) {
|
|
138
|
+
log('');
|
|
139
|
+
log(`Failed to start the pairing flow (HTTP ${initRes.status}).`);
|
|
140
|
+
const data = initRes.data;
|
|
141
|
+
if (data?.error)
|
|
142
|
+
log(` ${data.error}${data.detail ? ` — ${data.detail}` : ''}`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
const init = initRes.data;
|
|
146
|
+
if (!init?.code) {
|
|
147
|
+
log('Pairing init returned no code. Aborting.');
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
// 2. Tell the user where to go.
|
|
151
|
+
log('');
|
|
152
|
+
log(`1. Open this page in your browser:`);
|
|
153
|
+
log(` ${deviceBaseUrl}/device`);
|
|
154
|
+
log('');
|
|
155
|
+
log(`2. Enter this code (case-insensitive):`);
|
|
156
|
+
log(` ${init.code}`);
|
|
157
|
+
log('');
|
|
158
|
+
log(`3. This window will finish on its own once you confirm. The code`);
|
|
159
|
+
log(` expires in 15 minutes.`);
|
|
160
|
+
log('');
|
|
161
|
+
log(`Waiting for confirmation…`);
|
|
162
|
+
// 3. Poll until ok / 410 / timeout.
|
|
163
|
+
const pollUrl = `${supabaseUrl}/functions/v1/mcp-device-poll`;
|
|
164
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
165
|
+
let success = null;
|
|
166
|
+
while (Date.now() < deadline) {
|
|
167
|
+
await sleep(POLL_INTERVAL_MS);
|
|
168
|
+
let pollRes;
|
|
169
|
+
try {
|
|
170
|
+
pollRes = await postJson(pollUrl, { code: init.code });
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
// Transient network errors don't kill the loop — log once and keep polling.
|
|
174
|
+
log(` (network blip: ${err instanceof Error ? err.message : String(err)}; retrying)`);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (pollRes.status === 410) {
|
|
178
|
+
log('');
|
|
179
|
+
log('That code expired or was already used. Run `npx @heuresis/mcp login` again to start over.');
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
if (pollRes.status === 202) {
|
|
183
|
+
// Still pending — wait for the next tick.
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (pollRes.status === 200) {
|
|
187
|
+
const data = pollRes.data;
|
|
188
|
+
if (data && data.status === 'ok') {
|
|
189
|
+
success = data;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
// Unexpected 200 shape — keep trying until timeout rather than fail
|
|
193
|
+
// catastrophically; the next poll will likely clarify.
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
// Anything else: log and keep polling. The function may transiently 5xx.
|
|
197
|
+
log(` (poll returned HTTP ${pollRes.status}; retrying)`);
|
|
198
|
+
}
|
|
199
|
+
if (!success) {
|
|
200
|
+
log('');
|
|
201
|
+
log('Timed out waiting for confirmation. Run `npx @heuresis/mcp login` again.');
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
// 4. Verify the refresh token works + fetch the user's email to print.
|
|
205
|
+
const client = createClient(success.supabase_url, success.anon_key, {
|
|
206
|
+
auth: {
|
|
207
|
+
persistSession: false,
|
|
208
|
+
autoRefreshToken: false,
|
|
209
|
+
detectSessionInUrl: false,
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
let email = '(no email on record)';
|
|
213
|
+
try {
|
|
214
|
+
const { data, error } = await client.auth.setSession({
|
|
215
|
+
access_token: '',
|
|
216
|
+
refresh_token: success.refresh_token,
|
|
217
|
+
});
|
|
218
|
+
if (error || !data.session || !data.user) {
|
|
219
|
+
log('');
|
|
220
|
+
log(`Pairing returned a token, but it failed to refresh: ${error?.message ?? 'no session'}`);
|
|
221
|
+
log('Run `npx @heuresis/mcp login` again to retry.');
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
email = data.user.email ?? email;
|
|
225
|
+
// Use whatever supabase-js handed us back — it may have already rotated
|
|
226
|
+
// the refresh token at the first refresh.
|
|
227
|
+
success.refresh_token = data.session.refresh_token ?? success.refresh_token;
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
log(`Verification of the new refresh token failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
const creds = {
|
|
234
|
+
supabase_url: success.supabase_url,
|
|
235
|
+
anon_key: success.anon_key,
|
|
236
|
+
refresh_token: success.refresh_token,
|
|
237
|
+
user_id: success.user_id,
|
|
238
|
+
device_name: success.device_name || deviceName,
|
|
239
|
+
created_at: new Date().toISOString(),
|
|
240
|
+
};
|
|
241
|
+
const path = await writeCredentials(creds);
|
|
242
|
+
log('');
|
|
243
|
+
log(`Linked to ${email} as device "${creds.device_name}".`);
|
|
244
|
+
log(`Credentials saved to ${path} (chmod 600 on POSIX).`);
|
|
245
|
+
log('You can now point Claude Desktop / Claude Code at @heuresis/mcp.');
|
|
246
|
+
log('');
|
|
247
|
+
}
|
|
248
|
+
export async function logoutCommand() {
|
|
249
|
+
const removed = await deleteCredentials();
|
|
250
|
+
if (removed) {
|
|
251
|
+
log('Heuresis credentials removed.');
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
log('No Heuresis credentials were found.');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
export async function whoamiCommand() {
|
|
258
|
+
const creds = await readCredentials();
|
|
259
|
+
if (!creds) {
|
|
260
|
+
log('Not linked. Run `npx @heuresis/mcp login` to pair this machine.');
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
log(`Heuresis MCP — linked`);
|
|
264
|
+
log(` device: ${creds.device_name}`);
|
|
265
|
+
log(` user_id: ${creds.user_id}`);
|
|
266
|
+
log(` supabase_url: ${creds.supabase_url}`);
|
|
267
|
+
log(` created_at: ${creds.created_at}`);
|
|
268
|
+
log(` credentials: ${credentialsPath()}`);
|
|
269
|
+
}
|
|
270
|
+
export function helpCommand() {
|
|
271
|
+
printHelp();
|
|
272
|
+
}
|
|
273
|
+
// The unused `prompt` helper would only be used if we re-introduced any
|
|
274
|
+
// interactive form; export it so future subcommands can pick it up without
|
|
275
|
+
// re-implementing readline plumbing.
|
|
276
|
+
export { prompt };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Heuresis MCP — Supabase client wrapper.
|
|
2
|
+
//
|
|
3
|
+
// One SupabaseClient per MCP process. We DON'T let supabase-js persist the
|
|
4
|
+
// session to localStorage (no such thing in Node) — instead we manage the
|
|
5
|
+
// session manually:
|
|
6
|
+
//
|
|
7
|
+
// 1. At process start: read ~/.heuresis/credentials.json → call
|
|
8
|
+
// `client.auth.setSession({ access_token: '', refresh_token })`.
|
|
9
|
+
// supabase-js immediately refreshes the access token from the refresh
|
|
10
|
+
// token and stores both in memory.
|
|
11
|
+
// 2. supabase-js handles silent re-refresh in the background while the
|
|
12
|
+
// process runs. We don't have to do anything per-tool-call.
|
|
13
|
+
// 3. If the refresh fails (revoked, expired), tool calls 401 — the wrapper
|
|
14
|
+
// catches that and surfaces a "re-run `npx @heuresis/mcp login`" message.
|
|
15
|
+
//
|
|
16
|
+
// Phase 19.3 will swap the refresh-token issuance to come from the
|
|
17
|
+
// `mcp-device-poll` Edge Function, but the client-side shape doesn't change.
|
|
18
|
+
import { createClient } from '@supabase/supabase-js';
|
|
19
|
+
let cached = null;
|
|
20
|
+
export class CloudAuthError extends Error {
|
|
21
|
+
constructor(msg) {
|
|
22
|
+
super(msg);
|
|
23
|
+
this.name = 'CloudAuthError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build (or return cached) a Supabase client bound to the credentials on
|
|
28
|
+
* disk. Throws CloudAuthError if no credentials exist or if the refresh
|
|
29
|
+
* token has been revoked.
|
|
30
|
+
*/
|
|
31
|
+
export async function getCloudClient(creds) {
|
|
32
|
+
if (cached)
|
|
33
|
+
return cached;
|
|
34
|
+
const client = createClient(creds.supabase_url, creds.anon_key, {
|
|
35
|
+
auth: {
|
|
36
|
+
// Headless: no localStorage, no URL detection, no auto-refresh
|
|
37
|
+
// listeners writing to disk. The library still auto-refreshes the
|
|
38
|
+
// in-memory access token from the refresh token, which is all we want.
|
|
39
|
+
persistSession: false,
|
|
40
|
+
autoRefreshToken: true,
|
|
41
|
+
detectSessionInUrl: false,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
// Bootstrap the session from the refresh token. setSession with an empty
|
|
45
|
+
// access_token forces an immediate refresh against the refresh_token.
|
|
46
|
+
const { data, error } = await client.auth.setSession({
|
|
47
|
+
access_token: '',
|
|
48
|
+
refresh_token: creds.refresh_token,
|
|
49
|
+
});
|
|
50
|
+
if (error || !data.session) {
|
|
51
|
+
throw new CloudAuthError(`Failed to refresh Heuresis session: ${error?.message ?? 'no session returned'}. Run \`npx @heuresis/mcp login\` to re-authenticate.`);
|
|
52
|
+
}
|
|
53
|
+
cached = { client, userId: creds.user_id };
|
|
54
|
+
return cached;
|
|
55
|
+
}
|
|
56
|
+
/** Clear the cached client. Used after logout. */
|
|
57
|
+
export function resetCloudClient() {
|
|
58
|
+
cached = null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Convenience wrapper that surfaces Postgres / auth errors as a readable
|
|
62
|
+
* MCP tool error. supabase-js returns `{ data, error }` everywhere; this
|
|
63
|
+
* unwraps it.
|
|
64
|
+
*/
|
|
65
|
+
export function unwrap(res) {
|
|
66
|
+
if (res.error)
|
|
67
|
+
throw new Error(res.error.message);
|
|
68
|
+
if (res.data === null)
|
|
69
|
+
throw new Error('Empty result from cloud.');
|
|
70
|
+
return res.data;
|
|
71
|
+
}
|