getclaros 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +253 -2
- package/dist/bridge.js +72 -0
- package/dist/cli.js +456 -0
- package/dist/clients.js +428 -0
- package/dist/config.js +77 -0
- package/dist/peers.js +104 -0
- package/dist/query.js +39 -0
- package/dist/setup.js +636 -0
- package/dist/ui.js +67 -0
- package/package.json +26 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Timur Monasypov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,254 @@
|
|
|
1
|
-
#
|
|
1
|
+
# getclaros guard
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A local stdio MCP proxy. It wraps an app's MCP server and checks every write tool call against the company's
|
|
4
|
+
expert-approved Work Maps (`POST /v1/check`) before the call reaches the app. A call that breaks a rule is never
|
|
5
|
+
forwarded; the agent gets a tool error with the rule, the expert's words and who to ask.
|
|
6
|
+
|
|
7
|
+
The package name (`getclaros`) is a placeholder and lives only in `package.json`.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
npx getclaros init
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
To try a local build, run `node dist/cli.js init` with `GETCLAROS_LAUNCHER="node /abs/path/to/dist/cli.js"` so the
|
|
16
|
+
configs launch your build instead of `npx -y getclaros`.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
claros” getclaros 0.1.1
|
|
20
|
+
|
|
21
|
+
Sign in opening your browser
|
|
22
|
+
› https://claros-eta.vercel.app/device?code=ZQRC-ZCTP
|
|
23
|
+
Code ZQRC-ZCTP approve only if the page shows this code
|
|
24
|
+
|
|
25
|
+
✓ Signed in to workspace "Acme" key clr_7f3a91c2_…
|
|
26
|
+
<claros server> · key saved in ~/.config/getclaros/config.json, readable only by you
|
|
27
|
+
|
|
28
|
+
MCP servers
|
|
29
|
+
|
|
30
|
+
Claude Code ~/.claude.json
|
|
31
|
+
1 ● erpnext npx -y frappe-mcp-server Work Map "Process a supplier invoice"
|
|
32
|
+
2 ○ github docker run -i …
|
|
33
|
+
3 ○ playwright (project ~/proj) npx @playwright/mcp@latest
|
|
34
|
+
|
|
35
|
+
Cursor ~/.cursor/mcp.json
|
|
36
|
+
4 ● erpnext npx frappe-mcp-server Work Map "Process a supplier invoice"
|
|
37
|
+
|
|
38
|
+
· 3 remote servers need OAuth — guard them later: linear, notion, figma
|
|
39
|
+
|
|
40
|
+
● matches a Work Map: recommended
|
|
41
|
+
Guard which? Enter = 1,4 (recommended) · numbers like 1,3 · all · none ›
|
|
42
|
+
|
|
43
|
+
✓ Guarded erpnext in Claude Code, Cursor
|
|
44
|
+
✓ Added the Claros MCP server in Claude Code, Cursor
|
|
45
|
+
every file backed up first: ~/.config/getclaros/backups/2026-10-24T01-34-53-159Z
|
|
46
|
+
|
|
47
|
+
Next restart Claude Code, Cursor, then run npx getclaros test
|
|
48
|
+
undo anytime: npx getclaros undo
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
On a terminal the output is coloured in Claros's inks (stop magenta, ask green, nudge vermilion) with a spinner while
|
|
52
|
+
it waits for the browser. Piped, it is plain text with no escape codes and no header; `NO_COLOR` turns colour off,
|
|
53
|
+
`FORCE_COLOR` turns it on. Old Windows consoles get ASCII marks.
|
|
54
|
+
|
|
55
|
+
`init` signs you in through the browser (RFC 8628 device flow): it prints a URL and a code (XXXX-XXXX), opens the
|
|
56
|
+
URL (`--no-browser` to skip), and once you approve on Claros's `/device` page and pick a workspace it receives an
|
|
57
|
+
`agent` key for that workspace. A key from `--key` / `$CLAROS_KEY` / the saved config wins; `--paste` (or a Claros
|
|
58
|
+
without device sign-in) asks for a hidden paste instead. It checks the key, finds your agent clients' MCP configs, lists their servers and
|
|
59
|
+
asks which to guard. Servers whose name matches an app with a Work Map are preselected. `--yes` takes that default,
|
|
60
|
+
`--servers a,b` picks by name, `--project <dir>` sets where project configs are read (default: the current
|
|
61
|
+
directory), `--no-mcp` skips the Claros MCP entry. Restart the agent client afterwards.
|
|
62
|
+
|
|
63
|
+
| Client | Files read |
|
|
64
|
+
|---|---|
|
|
65
|
+
| Claude Code | `<project>/.mcp.json`; `~/.claude.json` (`mcpServers` and each `projects[path].mcpServers`) |
|
|
66
|
+
| Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json`, `%APPDATA%\Claude\…`, `~/.config/Claude/…` |
|
|
67
|
+
| Cursor | `~/.cursor/mcp.json`, `<project>/.cursor/mcp.json` |
|
|
68
|
+
| VS Code | the profile's `mcp.json` (`~/Library/Application Support/Code/User/mcp.json`, …), `<project>/.vscode/mcp.json` |
|
|
69
|
+
| Codex | `~/.codex/config.toml` (`$CODEX_HOME`), `<project>/.codex/config.toml`: `[mcp_servers.<name>]` tables |
|
|
70
|
+
|
|
71
|
+
What it changes, in files that already exist (it never creates a client config):
|
|
72
|
+
|
|
73
|
+
- Each picked stdio server: `command` becomes `npx`, and `-y getclaros guard --server <name> --` plus the original
|
|
74
|
+
command go in front of the original `args`. `env`, `cwd`, comments and everything else stay. Already-guarded
|
|
75
|
+
servers are left alone, so running `init` again changes nothing.
|
|
76
|
+
- Each picked remote (`url`) server with static headers: the entry becomes a stdio one running
|
|
77
|
+
`npx -y getclaros guard --server <name> --remote <url> --header "K: V"…`, which reaches the server over Streamable
|
|
78
|
+
HTTP. The headers stay in that file and are never sent to Claros. Servers that need OAuth (no static header), take
|
|
79
|
+
their token from an env var or helper, or speak legacy SSE are listed as not guarded yet.
|
|
80
|
+
- A `claros` server (`npx -y getclaros mcp`, a stdio bridge to Claros's `/mcp`) so agents can read the Work Map
|
|
81
|
+
up front with `get_workflow`; in each client's user config (or its project config when it has no user config).
|
|
82
|
+
- Every file is backed up before it changes (`~/.config/getclaros/backups/<time>/`) and written atomically.
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
npx getclaros test # a made-up record that breaks one of your rules → prints the stop (rule, expert's words, who to ask)
|
|
86
|
+
npx getclaros status # which servers are guarded, Claros reachable, key valid
|
|
87
|
+
npx getclaros undo # undo the last init; --all: every init, plus any guard wrapping it finds
|
|
88
|
+
npx getclaros export <workflow_id> [--format agents|design|skill] > AGENTS.md # a map's rules as markdown
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`export` prints a Work Map's rules (`GET /v1/maps/<id>/export`) as an AGENTS.md section, a DESIGN.md-style file or a
|
|
92
|
+
SKILL.md: an advisory copy for tools that read rules files; Claros still enforces the rules when the agent acts.
|
|
93
|
+
Exit codes: 0 printed, 1 no such map, 2 not signed in / bad usage, 3 Claros unreachable or key rejected.
|
|
94
|
+
|
|
95
|
+
`override <check_id> --rule <id> --reason "…"` tells the rule's expert a check got it wrong
|
|
96
|
+
(`POST /v1/checks/<id>/override`; `--verdict wrong_stop` by default, or `wrong_nudge | missed | right`). Every stop
|
|
97
|
+
message ends with this command filled in. Nothing changes until the expert confirms a suggested change on the map's
|
|
98
|
+
rules page. Exit codes: 0 sent, 1 no such check / not valid, 2 bad usage, 3 unreachable or key rejected.
|
|
99
|
+
|
|
100
|
+
`test` asks Claros for a record that breaks a precise rule of a Work Map for one of your guarded servers
|
|
101
|
+
(`GET /v1/check/example?app=…`), checks it with `POST /v1/check` and prints the stop. Nothing is sent to the app.
|
|
102
|
+
With no guarded server matching a map it uses the workspace's newest map and ends with a `Demo:` note.
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
◉ Stopped in 1.3 s Work Map "Process a supplier invoice" · check chk_0b14cb5eef
|
|
106
|
+
|
|
107
|
+
◉ stop Equipment over 5,000 is capex: book it to the capital equipment account, never to expense.
|
|
108
|
+
“Equipment above five thousand is an asset, it goes to capex.” — AP lead
|
|
109
|
+
Who to ask: Controller
|
|
110
|
+
|
|
111
|
+
Test action for erpnext, made up, only checked: items.0.item_group = Machinery, items.0.amount = 7200
|
|
112
|
+
Your agent gets the rule as the tool's error. Nothing reached erpnext.
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Exit codes: 0 stopped, 1 not stopped
|
|
116
|
+
or no map to test, 2 not signed in / bad usage, 3 Claros unreachable or key rejected (`status` uses 0 / 2 / 3 too).
|
|
117
|
+
|
|
118
|
+
`undo` restores a file from its backup when it is byte-for-byte what `init` wrote; if anything changed it since, it
|
|
119
|
+
unwraps the guarded servers and removes the `claros` entry in place instead, keeping the other edits, and says which
|
|
120
|
+
it did. What `undo` overwrites is backed up as well.
|
|
121
|
+
|
|
122
|
+
Security model:
|
|
123
|
+
|
|
124
|
+
- The key is stored once, in `~/.config/getclaros/config.json` (`$XDG_CONFIG_HOME`), mode 0600 in a 0700 directory,
|
|
125
|
+
never in a client config (project files such as `.mcp.json` are often committed). The guard and the bridge read it
|
|
126
|
+
from there (`--key` / `$CLAROS_KEY` still win). The key is never printed; output shows only its id
|
|
127
|
+
(`clr_<prefix>_…`).
|
|
128
|
+
- Backups hold the full previous config files (which can contain other servers' tokens): 0600 files in a 0700
|
|
129
|
+
directory. Delete `~/.config/getclaros/backups` once you're happy.
|
|
130
|
+
- The guard adds no credentials to the wrapped server: its `env` is passed through as before.
|
|
131
|
+
|
|
132
|
+
## Usage
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
export CLAROS_KEY=clr_... # a key with the `agent` scope
|
|
136
|
+
getclaros guard --server erpnext -- npx -y some-erpnext-mcp --site https://erp.example.com
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
In an MCP client config, replace the server's `command`/`args` with the guard in front of them:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{ "erpnext": { "command": "getclaros",
|
|
143
|
+
"args": ["guard", "--server", "erpnext", "--", "npx", "-y", "some-erpnext-mcp"],
|
|
144
|
+
"env": { "CLAROS_KEY": "clr_..." } } }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
| Flag | Default | |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| `--server <name>` | required | Sent as `app`; helps route the call to the right Work Map |
|
|
150
|
+
| `--url <base>` | `$CLAROS_URL`, else the URL `init` saved, else the one the web app names | Claros server base URL |
|
|
151
|
+
| `--web <origin>` | `$CLAROS_WEB`, else the web origin `init` saved, else `https://claros-eta.vercel.app` | Where to ask for the server address (`/.well-known/claros.json`) |
|
|
152
|
+
| `--key <key>` | `$CLAROS_KEY`, else the key `init` saved | Prefer the env var: flags show up in process lists |
|
|
153
|
+
| `--timeout <ms>` | `5000` | How long to wait for a check |
|
|
154
|
+
| `--fail-open` | off | Forward writes when Claros can't be reached (default: block them) |
|
|
155
|
+
|
|
156
|
+
Server address: `--url` / `$CLAROS_URL` win and are never changed. Otherwise the CLI uses the URL `init` saved, or asks
|
|
157
|
+
the web app (`GET <web>/.well-known/claros.json` → `{"api": "<server>"}`) and saves the answer with the web origin.
|
|
158
|
+
When the saved server doesn't answer at all (a connection error, not an HTTP error), it asks the web app once more and
|
|
159
|
+
follows a moved server, updating the saved config: no reinstall. A config made with `init --url` alone stays pinned to
|
|
160
|
+
that URL. `--help` / `-h` on any command prints the usage.
|
|
161
|
+
|
|
162
|
+
## What it does
|
|
163
|
+
|
|
164
|
+
- Relays newline-delimited JSON-RPC both ways byte for byte; only `tools/call` is inspected.
|
|
165
|
+
- Every `tools/list` response is passed on unchanged, then its tools are registered with Claros
|
|
166
|
+
(`POST /v1/tools {app, tools:[{name, description, inputSchema, annotations}]}`) so Claros can map their arguments
|
|
167
|
+
to a Work Map's fields once per tool. Only new tools or tools whose definition changed are sent. Registration is
|
|
168
|
+
fire and forget: a failure is logged, retried on the next `tools/list`, and never delays or blocks a call.
|
|
169
|
+
- Read tools pass without a check. Claros's classification (`GET /v1/tools/kinds`: LLM-proposed, expert-overridable)
|
|
170
|
+
wins once known; until then `annotations.readOnlyHint === true`, or a name starting with get / list / search / read /
|
|
171
|
+
find / describe / fetch / check_ / run_ / export_, or exactly version / ping. Everything else is a write.
|
|
172
|
+
Kinds are refreshed after registration, on every `tools/list` and at most every 30 s on writes.
|
|
173
|
+
- Writes: `POST /v1/check` with `{app, tool, action: tool, state, arguments}`. `state` is the arguments flattened
|
|
174
|
+
(`{"items.0.amount": 7200, "supplier": "..."}`); the server checks each array row as its own record. When the
|
|
175
|
+
tool has a mapping (proposed by Claros, confirmed by the expert), the server reads `arguments` through it instead,
|
|
176
|
+
so argument names don't have to match the map (`debit_to`, `items[].amount`).
|
|
177
|
+
- Record resolution: when the tool's mapping references a record (a submit by name, a partial doc) or needs values the
|
|
178
|
+
app fills on save (an item's group), Claros answers `need: [{key, tool, arguments}]`. The guard calls those read
|
|
179
|
+
tools on the wrapped server itself, with ids under a per-process prefix that are never relayed to the agent, waits
|
|
180
|
+
at most `--timeout`, takes the record from `structuredContent` or the JSON in the text, and checks again with
|
|
181
|
+
`fetched`. A failed or missing read blocks the write (or forwards it with `--fail-open`). A `need` naming a tool the
|
|
182
|
+
guard doesn't consider a read is refused. Lookups (`cache: true`) are cached for 60 s; the record itself is read
|
|
183
|
+
every time.
|
|
184
|
+
- Cross-server lookups: a `need` with `app` (another guarded server's read tool) goes to that server's guard over a
|
|
185
|
+
Unix socket, `<config dir>/run/<hash of the server name>.sock` (0600, dir 0700). Each guard serves its own server's
|
|
186
|
+
read tools there to guards that present the same key (sha256), and only read tools. No guard for that app → the
|
|
187
|
+
read fails → the write is blocked. Not on Windows yet.
|
|
188
|
+
- Allowed → forwarded unchanged. Stopped (`stop` or `ask`) → an `isError` result under the same id; the app never sees the call.
|
|
189
|
+
Nudged (a rule broken whose response is `nudge`, e.g. a taste rule) → forwarded; the app's result comes back with
|
|
190
|
+
one more text item, "Claros nudge: …" (rule, the expert's words, the suggested fix), so the agent can correct itself.
|
|
191
|
+
A call several Work Maps govern is checked against each (milestone 7); the stop names every map whose rule it
|
|
192
|
+
breaks, and a rule taken from a written policy is quoted as "In the written policy (file § section)".
|
|
193
|
+
- Claros unreachable, timed out or rejecting the key → writes are blocked with "ask a person"; reads still pass.
|
|
194
|
+
- A JSON-RPC batch containing a write `tools/call` is rejected.
|
|
195
|
+
- One log line per decision on stderr (stdout carries the protocol).
|
|
196
|
+
|
|
197
|
+
## How a stop looks
|
|
198
|
+
|
|
199
|
+
What the agent receives as the tool result (`isError: true`):
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
Claros stopped this action: it breaks a rule from the expert-approved Work Map "wf_ap_invoice". Do not retry it as is.
|
|
203
|
+
|
|
204
|
+
Rule: Equipment over 5,000 is capex: book to the capital equipment account, never to expense.
|
|
205
|
+
In the expert's words (AP lead): "Equipment above five thousand is an asset, it goes to capex."
|
|
206
|
+
Who to ask: AP lead
|
|
207
|
+
Next: change the record so the rule holds, or ask AP lead to approve an exception. Tell the user why.
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
An ask-first rule is held the same way (never run), but opens with "Claros held this action: a rule from the expert-approved Work Map … says to ask first." and its Next line says who to ask.
|
|
211
|
+
|
|
212
|
+
A nudge, appended to the app's own result (not an error):
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
Claros nudge: the call went through, but it doesn't follow a rule your team holds. Fix it if you still can, and follow it from now on.
|
|
216
|
+
|
|
217
|
+
Rule: <the rule>
|
|
218
|
+
In the expert's words (<expert>): "<their words>"
|
|
219
|
+
Suggested fix: <the judge's fix, or the rule's fix template filled from the record>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
And on stderr:
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
[getclaros guard] erpnext submit_purchase_invoice → stop wf_ap_invoice g1 31ms chk_0b14cb5eef
|
|
226
|
+
[getclaros guard] erpnext call_method → stop wf_ap_invoice g1 12ms (1 read) chk_8187eaa79f
|
|
227
|
+
[getclaros guard] erpnext create_purchase_invoice → nudge wf_ap_invoice g1 2ms chk_25cd79b34a
|
|
228
|
+
[getclaros guard] nudge: <the rule> (fix: <the fix>)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Develop
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
npm install
|
|
235
|
+
npm test # tsc + node:test (fake MCP server + fake Claros; init/test/undo against a temp HOME)
|
|
236
|
+
npm run smoke # starts its own Claros on a free port with a temp DB, seeds wf_ap_invoice; create_purchase_invoice
|
|
237
|
+
# (supplier_name, debit_to, items[].amount) passes unmapped, is stopped once the expert maps it;
|
|
238
|
+
# update_record {doctype, name, values: {docstatus: 1}} is stopped after the guard reads the record
|
|
239
|
+
# and the item (fetch recipe + derived amount + lookup)
|
|
240
|
+
# then smoke-init: empty temp HOME with a Claude Code config → init --yes → test (timed) → status,
|
|
241
|
+
# the Claros MCP bridge against the real /mcp, undo
|
|
242
|
+
cd e2e && npm run e2e # against a real ERPNext through frappe-mcp-server (docs/GATEWAY_E2E_ERPNEXT.md)
|
|
243
|
+
cd e2e && npm run horizontal # four maps made from written policies, guards around mcp-sqlite (helpdesk, crm),
|
|
244
|
+
# mcp-server-git, the filesystem server and (when it answers) the local Zammad; the real
|
|
245
|
+
# LLM mapper, router and judge; prints mapper / routing / per-case tables (docs/HORIZONTAL.md)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The smoke runs the server with `CLAROS_TOOL_MAPPER=off` (no LLM proposals, offline) and maps the tool the way an
|
|
249
|
+
expert would, through `POST /v1/tools/{id}/mapping`.
|
|
250
|
+
|
|
251
|
+
Limits: until a tool is mapped, only argument names that match the map's field names or aliases are understood, and
|
|
252
|
+
an unmapped write nothing matches is allowed (logged `mapping_status: unmapped`). The record is read just before the
|
|
253
|
+
write, not locked: it can change in between. No Windows `.cmd` shims (spawn
|
|
254
|
+
without a shell). Fuzzy rules (`judge_yourself`) are not enforced yet (milestone 5).
|
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// getclaros mcp: the Claros MCP server (get_workflow, check_action, …) for any client, as a stdio server.
|
|
2
|
+
// Relays each newline-delimited JSON-RPC message to <url>/mcp (streamable HTTP) with the saved key, and the answer
|
|
3
|
+
// (JSON, or the `data:` events of an SSE answer) back on stdout. Every client speaks stdio, and the key stays in
|
|
4
|
+
// ~/.config/getclaros instead of each client's config.
|
|
5
|
+
import { relocate, resolve } from "./config.js";
|
|
6
|
+
export async function bridge() {
|
|
7
|
+
const log = (s) => process.stderr.write(`[getclaros mcp] ${s}\n`);
|
|
8
|
+
let at;
|
|
9
|
+
try {
|
|
10
|
+
at = await resolve();
|
|
11
|
+
}
|
|
12
|
+
catch (e) {
|
|
13
|
+
log(e.message);
|
|
14
|
+
return process.exit(2);
|
|
15
|
+
}
|
|
16
|
+
const key = at.key;
|
|
17
|
+
if (!key)
|
|
18
|
+
log("no key: run `npx getclaros init` (or set CLAROS_KEY)");
|
|
19
|
+
let session, version, chain = Promise.resolve();
|
|
20
|
+
const out = (m) => process.stdout.write(m.trim() + "\n");
|
|
21
|
+
async function send(line) {
|
|
22
|
+
let msg;
|
|
23
|
+
try {
|
|
24
|
+
msg = JSON.parse(line);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const post = () => fetch(`${at.url}/mcp`, {
|
|
30
|
+
method: "POST", body: line, signal: AbortSignal.timeout(30_000),
|
|
31
|
+
headers: { "content-type": "application/json", accept: "application/json, text/event-stream",
|
|
32
|
+
...(key ? { authorization: `Bearer ${key}` } : {}), ...(session ? { "mcp-session-id": session } : {}),
|
|
33
|
+
...(version ? { "mcp-protocol-version": version } : {}) },
|
|
34
|
+
});
|
|
35
|
+
try {
|
|
36
|
+
// a server that doesn't answer may have moved: ask the web app once, then retry there
|
|
37
|
+
const r = await post().catch(async (e) => {
|
|
38
|
+
if (e.name === "TimeoutError" || !(await relocate(at)))
|
|
39
|
+
throw e;
|
|
40
|
+
log(`Claros moved: now ${at.url}`);
|
|
41
|
+
return post();
|
|
42
|
+
});
|
|
43
|
+
session = r.headers.get("mcp-session-id") ?? session;
|
|
44
|
+
const text = await r.text();
|
|
45
|
+
if (!r.ok)
|
|
46
|
+
throw new Error(`HTTP ${r.status}`);
|
|
47
|
+
const msgs = (r.headers.get("content-type") ?? "").includes("event-stream")
|
|
48
|
+
? text.split(/\r?\n\r?\n/).map((ev) => ev.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).join("\n"))
|
|
49
|
+
: [text];
|
|
50
|
+
for (const m of msgs.filter((x) => x.trim())) {
|
|
51
|
+
version ??= JSON.parse(m)?.result?.protocolVersion;
|
|
52
|
+
out(m);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
if (msg?.id !== undefined) {
|
|
57
|
+
out(JSON.stringify({ jsonrpc: "2.0", id: msg.id, error: { code: -32000, message: `Claros MCP at ${at.url}: ${e.message}` } }));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
let buf = "";
|
|
62
|
+
process.stdin.setEncoding("utf8");
|
|
63
|
+
process.stdin.on("data", (c) => {
|
|
64
|
+
buf += c;
|
|
65
|
+
for (let i; (i = buf.indexOf("\n")) >= 0; buf = buf.slice(i + 1)) {
|
|
66
|
+
const line = buf.slice(0, i);
|
|
67
|
+
if (line.trim())
|
|
68
|
+
chain = chain.then(() => send(line)); // in order; the server is stateless, so this is just simple
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
process.stdin.on("end", () => chain.then(() => process.exit(0)));
|
|
72
|
+
}
|