@xenolithengine/graph-mcp-server 0.7.0-beta.0
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 +167 -0
- package/dist/bridge.d.ts +37 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +118 -0
- package/dist/bridge.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +38 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +138 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +65 -0
- package/dist/protocol.js.map +1 -0
- package/dist/resources.d.ts +14 -0
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +22 -0
- package/dist/resources.js.map +1 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +58 -0
- package/dist/server.js.map +1 -0
- package/dist/tools.d.ts +452 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +193 -0
- package/dist/tools.js.map +1 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 strelok2012
|
|
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
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# @xenolithengine/graph-mcp-server
|
|
2
|
+
|
|
3
|
+
[](https://github.com/XenolithEngine/xenolith-graph#status)
|
|
4
|
+
[](https://github.com/XenolithEngine/xenolith-graph/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
MCP (Model Context Protocol) server for XenolithGraph. Lets Claude Desktop, Cursor, or any MCP client drive a live editor in your browser over a localhost WebSocket bridge.
|
|
7
|
+
|
|
8
|
+
> **Beta** — public API in `STABLE-API.md` is the surface we plan to freeze, but it is **NOT frozen yet** — breaking changes can land at any point before v1.0. If you adopt now, pin an exact version.
|
|
9
|
+
|
|
10
|
+
## Architecture (Scenario A — desktop dev)
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
Claude Desktop ──stdio JSON-RPC──▶ xenolith-mcp (Node)
|
|
14
|
+
│
|
|
15
|
+
▼
|
|
16
|
+
WebSocket bridge (127.0.0.1:7777)
|
|
17
|
+
│
|
|
18
|
+
▼
|
|
19
|
+
Browser editor (editor.connectMCP)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- MCP client speaks stdio JSON-RPC to the CLI.
|
|
23
|
+
- The CLI hosts a localhost WS bridge.
|
|
24
|
+
- Your XenolithGraph editor opens a WS to the bridge and registers itself as the "current" world.
|
|
25
|
+
- Tool calls from Claude → CLI → WS → editor → result → back to Claude.
|
|
26
|
+
|
|
27
|
+
## Resources exposed
|
|
28
|
+
|
|
29
|
+
Read-only context the MCP client can **attach** to its prompt without spending a tool call. In Claude Desktop / Cursor, these show up in the attachment picker next to local files.
|
|
30
|
+
|
|
31
|
+
| URI | What |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `graph://current` | The live graph as `xenolith.v1` JSON. Attach this to ask the AI to summarise / refactor / explain the current state. |
|
|
34
|
+
| `schema://types` | Every registered node type with pin & widget specs. Attach this so the AI doesn't have to call `list_node_types` every session — saves a roundtrip and 10× tokens against large registries (Comfy 60k+ types). |
|
|
35
|
+
|
|
36
|
+
Resources route to the same WS bridge as tools; if no editor is connected they return an error blob instead of failing the MCP request.
|
|
37
|
+
|
|
38
|
+
## Tools exposed
|
|
39
|
+
|
|
40
|
+
| Tool | What it does |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `list_node_types` | Lists every registered node type (so the LLM picks a real name). |
|
|
43
|
+
| `get_graph` | Returns the current graph as `xenolith.v1` JSON. |
|
|
44
|
+
| `add_node` | Inserts a node by type at `(x, y)`. Returns its id. |
|
|
45
|
+
| `connect_pins` | Connects two pins by node-id + pin-id. Returns edge id. |
|
|
46
|
+
| `fit_view` | Fits the whole graph into the viewport. |
|
|
47
|
+
|
|
48
|
+
All mutations route through the editor's CommandBus, so undo/redo and events fire normally.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## How to test (full walkthrough)
|
|
53
|
+
|
|
54
|
+
### 1. Build the workspace
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pnpm install
|
|
58
|
+
pnpm --filter @xenolithengine/graph-editor build
|
|
59
|
+
pnpm --filter @xenolithengine/graph-mcp-server build
|
|
60
|
+
chmod +x packages/mcp-server/dist/cli.js
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 2. Start the MCP server (separate terminal)
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
node packages/mcp-server/dist/cli.js --port 7777 --token devtoken
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
It prints to stderr:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
[xenolith-mcp] bridge listening on ws://127.0.0.1:7777
|
|
73
|
+
|
|
74
|
+
XenolithGraph MCP server ready.
|
|
75
|
+
Editor URL → ws://127.0.0.1:7777?token=devtoken
|
|
76
|
+
In the browser console of your XenolithGraph host, run:
|
|
77
|
+
editor.connectMCP('ws://127.0.0.1:7777?token=devtoken')
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
stdout is reserved for the MCP JSON-RPC framing — never log there.
|
|
81
|
+
|
|
82
|
+
> The CLI is **always-on stdio**. When you run it manually like above without an MCP client attached, it just sits waiting. Ctrl+C to stop.
|
|
83
|
+
|
|
84
|
+
### 3. Wire the editor in the browser
|
|
85
|
+
|
|
86
|
+
Open the playground (`pnpm playground`) or any host that mounts a XenolithGraph editor. In DevTools console:
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
await window.__xenoEditor.connectMCP('ws://127.0.0.1:7777?token=devtoken', {
|
|
90
|
+
onStatus: (s) => console.log('[mcp]', s),
|
|
91
|
+
})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
You should see `[mcp] connecting` → `[mcp] open` and the CLI stderr prints `editor connected (e1); 1 total`.
|
|
95
|
+
|
|
96
|
+
### 4. Drive it from a temporary local MCP client (no Claude Desktop needed)
|
|
97
|
+
|
|
98
|
+
Quick smoke without going through Claude — use the MCP SDK as a client:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
cat > /tmp/mcp-smoke.mjs <<'EOF'
|
|
102
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
103
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
|
104
|
+
|
|
105
|
+
const transport = new StdioClientTransport({
|
|
106
|
+
command: 'node',
|
|
107
|
+
args: ['packages/mcp-server/dist/cli.js', '--port', '7777', '--token', 'devtoken'],
|
|
108
|
+
})
|
|
109
|
+
const client = new Client({ name: 'smoke', version: '0' })
|
|
110
|
+
await client.connect(transport)
|
|
111
|
+
|
|
112
|
+
console.log('tools:', (await client.listTools()).tools.map((t) => t.name))
|
|
113
|
+
console.log('add_node →', await client.callTool({ name: 'add_node', arguments: { type: 'Box', x: 100, y: 100 } }))
|
|
114
|
+
console.log('graph →', await client.callTool({ name: 'get_graph', arguments: {} }))
|
|
115
|
+
|
|
116
|
+
await client.close()
|
|
117
|
+
EOF
|
|
118
|
+
cd /Users/vitaliyry/PET_PROJECTS/xenolith-graph && node /tmp/mcp-smoke.mjs
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
> Note: this client spawns its OWN CLI process, so kill the manual one from step 2 first (or pass a different `--port`). The browser editor must already be connected to whichever bridge port the client uses.
|
|
122
|
+
|
|
123
|
+
If everything works you'll see a `Box` node appear in the browser playground and the tool result JSON in the terminal.
|
|
124
|
+
|
|
125
|
+
### 5. Hook into Claude Desktop
|
|
126
|
+
|
|
127
|
+
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"mcpServers": {
|
|
132
|
+
"xenolith": {
|
|
133
|
+
"command": "node",
|
|
134
|
+
"args": [
|
|
135
|
+
"/Users/vitaliyry/PET_PROJECTS/xenolith-graph/packages/mcp-server/dist/cli.js",
|
|
136
|
+
"--port", "7777",
|
|
137
|
+
"--token", "devtoken"
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Restart Claude Desktop. In a new chat you'll see a tools icon (🔧). Ask:
|
|
145
|
+
|
|
146
|
+
> "Use the xenolith add_node tool to add a Box node at (200, 200)."
|
|
147
|
+
|
|
148
|
+
Claude will call the tool, the bridge forwards it, and the node pops into your browser editor.
|
|
149
|
+
|
|
150
|
+
### 6. Troubleshooting
|
|
151
|
+
|
|
152
|
+
- **`no editor connected`** — the browser tab didn't run `connectMCP` (or it disconnected). Re-run in console.
|
|
153
|
+
- **`bad token`** — the URL you pasted has a different token than the CLI was started with.
|
|
154
|
+
- **port already in use** — kill the previous process or pick `--port 7778`. Update the editor URL to match.
|
|
155
|
+
- **Claude doesn't see the tools** — Claude Desktop only re-reads the config on restart; quit it fully (Cmd+Q), not just close the window.
|
|
156
|
+
- **Tool times out** — defaults to 5s. The editor probably threw or you're paused in DevTools.
|
|
157
|
+
|
|
158
|
+
## Programmatic embedding
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { EditorBridge, createMcpServer, startStdio } from '@xenolithengine/graph-mcp-server'
|
|
162
|
+
|
|
163
|
+
const bridge = new EditorBridge({ port: 7777, token: 'mytoken' })
|
|
164
|
+
await bridge.start()
|
|
165
|
+
const mcp = createMcpServer(bridge)
|
|
166
|
+
await startStdio(mcp)
|
|
167
|
+
```
|
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
import { PendingCalls } from './protocol.js';
|
|
3
|
+
/** A single editor connection — owns a WebSocket and its in-flight call ledger. */
|
|
4
|
+
export declare class EditorConnection {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
private readonly ws;
|
|
7
|
+
readonly pending: PendingCalls;
|
|
8
|
+
constructor(id: string, ws: WebSocket, defaultTimeoutMs: number);
|
|
9
|
+
call(tool: string, args?: unknown, timeoutMs?: number): Promise<unknown>;
|
|
10
|
+
close(reason: string): void;
|
|
11
|
+
}
|
|
12
|
+
export interface BridgeOptions {
|
|
13
|
+
port?: number;
|
|
14
|
+
/** When set, incoming connections must include `?token=...` matching this value. */
|
|
15
|
+
token?: string;
|
|
16
|
+
/** Default per-call timeout in ms. */
|
|
17
|
+
callTimeoutMs?: number;
|
|
18
|
+
/** Optional log sink (defaults to stderr — never stdout, that's reserved for MCP stdio). */
|
|
19
|
+
log?: (msg: string) => void;
|
|
20
|
+
}
|
|
21
|
+
/** WebSocket hub: accepts editor connections, exposes `current()` for tool handlers. */
|
|
22
|
+
export declare class EditorBridge {
|
|
23
|
+
#private;
|
|
24
|
+
readonly opts: {
|
|
25
|
+
port: number;
|
|
26
|
+
callTimeoutMs: number;
|
|
27
|
+
token?: string;
|
|
28
|
+
log: (msg: string) => void;
|
|
29
|
+
};
|
|
30
|
+
constructor(opts?: BridgeOptions);
|
|
31
|
+
start(): Promise<number>;
|
|
32
|
+
/** Return the currently-active editor connection, or throw if none is connected. */
|
|
33
|
+
current(): EditorConnection;
|
|
34
|
+
hasEditor(): boolean;
|
|
35
|
+
stop(): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,SAAS,EAAE,MAAM,IAAI,CAAA;AAE/C,OAAO,EAAE,YAAY,EAAqB,MAAM,eAAe,CAAA;AAE/D,mFAAmF;AACnF,qBAAa,gBAAgB;aAEC,EAAE,EAAE,MAAM;IAAE,OAAO,CAAC,QAAQ,CAAC,EAAE;IAD3D,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;gBACF,EAAE,EAAE,MAAM,EAAmB,EAAE,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM;IAGhG,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAGxE,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;CAI5B;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,oFAAoF;IACpF,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,sCAAsC;IACtC,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,4FAA4F;IAC5F,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAC5B;AAED,wFAAwF;AACxF,qBAAa,YAAY;;IAKvB,QAAQ,CAAC,IAAI,EAAE;QACb,IAAI,EAAE,MAAM,CAAA;QACZ,aAAa,EAAE,MAAM,CAAA;QACrB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;KAC3B,CAAA;gBAEW,IAAI,GAAE,aAAkB;IASpC,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAiDxB,oFAAoF;IACpF,OAAO,IAAI,gBAAgB;IAK3B,SAAS,IAAI,OAAO;IAEd,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAO5B"}
|
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
2
|
+
import { IncomingMessage } from 'node:http';
|
|
3
|
+
import { PendingCalls, WSCall, WSMessage } from './protocol.js';
|
|
4
|
+
/** A single editor connection — owns a WebSocket and its in-flight call ledger. */
|
|
5
|
+
export class EditorConnection {
|
|
6
|
+
id;
|
|
7
|
+
ws;
|
|
8
|
+
pending;
|
|
9
|
+
constructor(id, ws, defaultTimeoutMs) {
|
|
10
|
+
this.id = id;
|
|
11
|
+
this.ws = ws;
|
|
12
|
+
this.pending = new PendingCalls({ send: (m) => this.ws.send(JSON.stringify(m)) }, defaultTimeoutMs);
|
|
13
|
+
}
|
|
14
|
+
call(tool, args, timeoutMs) {
|
|
15
|
+
return this.pending.call(tool, args, timeoutMs);
|
|
16
|
+
}
|
|
17
|
+
close(reason) {
|
|
18
|
+
this.pending.abortAll(reason);
|
|
19
|
+
try {
|
|
20
|
+
this.ws.close();
|
|
21
|
+
}
|
|
22
|
+
catch { /* noop */ }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** WebSocket hub: accepts editor connections, exposes `current()` for tool handlers. */
|
|
26
|
+
export class EditorBridge {
|
|
27
|
+
#wss = null;
|
|
28
|
+
#connections = new Map();
|
|
29
|
+
#current = null;
|
|
30
|
+
#counter = 0;
|
|
31
|
+
opts;
|
|
32
|
+
constructor(opts = {}) {
|
|
33
|
+
this.opts = {
|
|
34
|
+
port: opts.port ?? 7777,
|
|
35
|
+
callTimeoutMs: opts.callTimeoutMs ?? 5000,
|
|
36
|
+
log: opts.log ?? ((m) => process.stderr.write(`[xenolith-mcp] ${m}\n`)),
|
|
37
|
+
...(opts.token !== undefined ? { token: opts.token } : {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
start() {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const wss = new WebSocketServer({ port: this.opts.port, host: '127.0.0.1' });
|
|
43
|
+
wss.on('error', reject);
|
|
44
|
+
wss.on('listening', () => {
|
|
45
|
+
const addr = wss.address();
|
|
46
|
+
const port = typeof addr === 'object' && addr ? addr.port : this.opts.port;
|
|
47
|
+
this.opts.log(`bridge listening on ws://127.0.0.1:${port}`);
|
|
48
|
+
resolve(port);
|
|
49
|
+
});
|
|
50
|
+
wss.on('connection', (ws, req) => this.#onConnect(ws, req));
|
|
51
|
+
this.#wss = wss;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
#onConnect(ws, req) {
|
|
55
|
+
if (this.opts.token) {
|
|
56
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
57
|
+
if (url.searchParams.get('token') !== this.opts.token) {
|
|
58
|
+
this.opts.log('rejected connection: bad/missing token');
|
|
59
|
+
ws.close(1008, 'bad token');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const id = `e${++this.#counter}`;
|
|
64
|
+
const conn = new EditorConnection(id, ws, this.opts.callTimeoutMs);
|
|
65
|
+
this.#connections.set(id, conn);
|
|
66
|
+
this.#current = conn;
|
|
67
|
+
this.opts.log(`editor connected (${id}); ${this.#connections.size} total`);
|
|
68
|
+
ws.on('message', (raw) => this.#onMessage(conn, raw.toString()));
|
|
69
|
+
ws.on('close', () => {
|
|
70
|
+
this.#connections.delete(id);
|
|
71
|
+
conn.close('socket closed');
|
|
72
|
+
if (this.#current === conn)
|
|
73
|
+
this.#current = [...this.#connections.values()].pop() ?? null;
|
|
74
|
+
this.opts.log(`editor disconnected (${id}); ${this.#connections.size} remain`);
|
|
75
|
+
});
|
|
76
|
+
ws.on('error', (err) => this.opts.log(`socket error (${id}): ${err.message}`));
|
|
77
|
+
}
|
|
78
|
+
#onMessage(conn, raw) {
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(raw);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
this.opts.log(`malformed json from ${conn.id}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const msg = parseMessage(parsed);
|
|
88
|
+
if (!msg) {
|
|
89
|
+
this.opts.log(`unknown message shape from ${conn.id}`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (msg.kind === 'result')
|
|
93
|
+
conn.pending.resolve(msg);
|
|
94
|
+
else if (msg.kind === 'hello')
|
|
95
|
+
this.opts.log(`hello from ${conn.id} (editor ${msg.editorVersion ?? '?'})`);
|
|
96
|
+
// editor never sends 'call' in scenario A — server is the caller.
|
|
97
|
+
}
|
|
98
|
+
/** Return the currently-active editor connection, or throw if none is connected. */
|
|
99
|
+
current() {
|
|
100
|
+
if (!this.#current)
|
|
101
|
+
throw new Error('no editor connected — open the playground and call editor.connectMCP() first');
|
|
102
|
+
return this.#current;
|
|
103
|
+
}
|
|
104
|
+
hasEditor() { return this.#current !== null; }
|
|
105
|
+
async stop() {
|
|
106
|
+
for (const c of this.#connections.values())
|
|
107
|
+
c.close('server stopping');
|
|
108
|
+
this.#connections.clear();
|
|
109
|
+
this.#current = null;
|
|
110
|
+
await new Promise((res) => { this.#wss ? this.#wss.close(() => res()) : res(); });
|
|
111
|
+
this.#wss = null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function parseMessage(parsed) {
|
|
115
|
+
const r = WSMessage.safeParse(parsed);
|
|
116
|
+
return r.success ? r.data : null;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { EditorBridge } from './bridge.js';
|
|
4
|
+
import { createMcpServer, startStdio } from './server.js';
|
|
5
|
+
/** xenolith-mcp — CLI entrypoint.
|
|
6
|
+
* Reads --port (default 7777) and --token (default = random). Spawns the WS bridge and MCP server.
|
|
7
|
+
* Logs everything to STDERR; stdout is reserved for the MCP JSON-RPC framing.
|
|
8
|
+
* On startup prints the editor URL (with token) so the user can paste it into
|
|
9
|
+
* `editor.connectMCP('ws://...')` in the browser console. */
|
|
10
|
+
async function main() {
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const port = Number(arg(args, '--port') ?? 7777);
|
|
13
|
+
const token = arg(args, '--token') ?? randomBytes(8).toString('hex');
|
|
14
|
+
const bridge = new EditorBridge({ port, token });
|
|
15
|
+
const listeningPort = await bridge.start();
|
|
16
|
+
process.stderr.write([
|
|
17
|
+
'',
|
|
18
|
+
' XenolithGraph MCP server ready.',
|
|
19
|
+
` Editor URL → ws://127.0.0.1:${listeningPort}?token=${token}`,
|
|
20
|
+
' In the browser console of your XenolithGraph host, run:',
|
|
21
|
+
` editor.connectMCP('ws://127.0.0.1:${listeningPort}?token=${token}')`,
|
|
22
|
+
'',
|
|
23
|
+
].join('\n'));
|
|
24
|
+
const mcp = createMcpServer(bridge);
|
|
25
|
+
await startStdio(mcp);
|
|
26
|
+
const stop = async () => { await bridge.stop(); process.exit(0); };
|
|
27
|
+
process.on('SIGINT', stop);
|
|
28
|
+
process.on('SIGTERM', stop);
|
|
29
|
+
}
|
|
30
|
+
function arg(args, flag) {
|
|
31
|
+
const i = args.indexOf(flag);
|
|
32
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
33
|
+
}
|
|
34
|
+
main().catch((err) => {
|
|
35
|
+
process.stderr.write(`[xenolith-mcp] fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
});
|
|
38
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAEzD;;;;8DAI8D;AAC9D,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IAClC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,IAAI,CAAC,CAAA;IAChD,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;IAEpE,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;IAChD,MAAM,aAAa,GAAG,MAAM,MAAM,CAAC,KAAK,EAAE,CAAA;IAC1C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;QACnB,EAAE;QACF,mCAAmC;QACnC,mCAAmC,aAAa,UAAU,KAAK,EAAE;QACjE,2DAA2D;QAC3D,2CAA2C,aAAa,UAAU,KAAK,IAAI;QAC3E,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IAEb,MAAM,GAAG,GAAG,eAAe,CAAC,MAAM,CAAC,CAAA;IACnC,MAAM,UAAU,CAAC,GAAG,CAAC,CAAA;IAErB,MAAM,IAAI,GAAG,KAAK,IAAmB,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA,CAAC,CAAC,CAAA;IAChF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAC1B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;AAC7B,CAAC;AAED,SAAS,GAAG,CAAC,IAAc,EAAE,IAAY;IACvC,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AACzC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,yBAAyB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IACnG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC,CAAC,CAAA"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { EditorBridge, EditorConnection, type BridgeOptions } from './bridge.js';
|
|
2
|
+
export { PendingCalls, WSCall, WSResult, WSMessage, type CallSink } from './protocol.js';
|
|
3
|
+
export { TOOLS, TOOL_NAMES, type ToolName, type ToolDef } from './tools.js';
|
|
4
|
+
export { RESOURCES, type ResourceDef } from './resources.js';
|
|
5
|
+
export { createMcpServer, startStdio } from './server.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAA;AAChF,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxF,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,QAAQ,EAAE,KAAK,OAAO,EAAE,MAAM,YAAY,CAAA;AAC3E,OAAO,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5D,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { EditorBridge, EditorConnection } from './bridge.js';
|
|
2
|
+
export { PendingCalls, WSCall, WSResult, WSMessage } from './protocol.js';
|
|
3
|
+
export { TOOLS, TOOL_NAMES } from './tools.js';
|
|
4
|
+
export { RESOURCES } from './resources.js';
|
|
5
|
+
export { createMcpServer, startStdio } from './server.js';
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAsB,MAAM,aAAa,CAAA;AAChF,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAiB,MAAM,eAAe,CAAA;AACxF,OAAO,EAAE,KAAK,EAAE,UAAU,EAA+B,MAAM,YAAY,CAAA;AAC3E,OAAO,EAAE,SAAS,EAAoB,MAAM,gBAAgB,CAAA;AAC5D,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Wire protocol between the MCP server (Node) and the editor (browser) over WebSocket.
|
|
3
|
+
* All messages are JSON envelopes with a correlation `id`. The server initiates `call`;
|
|
4
|
+
* the editor replies with one `result` carrying ok/data or ok=false/error. */
|
|
5
|
+
export declare const WSCall: z.ZodObject<{
|
|
6
|
+
id: z.ZodString;
|
|
7
|
+
kind: z.ZodLiteral<"call">;
|
|
8
|
+
tool: z.ZodString;
|
|
9
|
+
args: z.ZodOptional<z.ZodUnknown>;
|
|
10
|
+
}, "strip", z.ZodTypeAny, {
|
|
11
|
+
id: string;
|
|
12
|
+
kind: "call";
|
|
13
|
+
tool: string;
|
|
14
|
+
args?: unknown;
|
|
15
|
+
}, {
|
|
16
|
+
id: string;
|
|
17
|
+
kind: "call";
|
|
18
|
+
tool: string;
|
|
19
|
+
args?: unknown;
|
|
20
|
+
}>;
|
|
21
|
+
export type WSCall = z.infer<typeof WSCall>;
|
|
22
|
+
export declare const WSResult: z.ZodDiscriminatedUnion<"ok", [z.ZodObject<{
|
|
23
|
+
id: z.ZodString;
|
|
24
|
+
kind: z.ZodLiteral<"result">;
|
|
25
|
+
ok: z.ZodLiteral<true>;
|
|
26
|
+
data: z.ZodOptional<z.ZodUnknown>;
|
|
27
|
+
}, "strip", z.ZodTypeAny, {
|
|
28
|
+
id: string;
|
|
29
|
+
kind: "result";
|
|
30
|
+
ok: true;
|
|
31
|
+
data?: unknown;
|
|
32
|
+
}, {
|
|
33
|
+
id: string;
|
|
34
|
+
kind: "result";
|
|
35
|
+
ok: true;
|
|
36
|
+
data?: unknown;
|
|
37
|
+
}>, z.ZodObject<{
|
|
38
|
+
id: z.ZodString;
|
|
39
|
+
kind: z.ZodLiteral<"result">;
|
|
40
|
+
ok: z.ZodLiteral<false>;
|
|
41
|
+
error: z.ZodString;
|
|
42
|
+
}, "strip", z.ZodTypeAny, {
|
|
43
|
+
id: string;
|
|
44
|
+
kind: "result";
|
|
45
|
+
ok: false;
|
|
46
|
+
error: string;
|
|
47
|
+
}, {
|
|
48
|
+
id: string;
|
|
49
|
+
kind: "result";
|
|
50
|
+
ok: false;
|
|
51
|
+
error: string;
|
|
52
|
+
}>]>;
|
|
53
|
+
export type WSResult = z.infer<typeof WSResult>;
|
|
54
|
+
export declare const WSHello: z.ZodObject<{
|
|
55
|
+
kind: z.ZodLiteral<"hello">;
|
|
56
|
+
editorVersion: z.ZodOptional<z.ZodString>;
|
|
57
|
+
}, "strip", z.ZodTypeAny, {
|
|
58
|
+
kind: "hello";
|
|
59
|
+
editorVersion?: string | undefined;
|
|
60
|
+
}, {
|
|
61
|
+
kind: "hello";
|
|
62
|
+
editorVersion?: string | undefined;
|
|
63
|
+
}>;
|
|
64
|
+
export type WSHello = z.infer<typeof WSHello>;
|
|
65
|
+
export declare const WSMessage: z.ZodUnion<[z.ZodObject<{
|
|
66
|
+
id: z.ZodString;
|
|
67
|
+
kind: z.ZodLiteral<"call">;
|
|
68
|
+
tool: z.ZodString;
|
|
69
|
+
args: z.ZodOptional<z.ZodUnknown>;
|
|
70
|
+
}, "strip", z.ZodTypeAny, {
|
|
71
|
+
id: string;
|
|
72
|
+
kind: "call";
|
|
73
|
+
tool: string;
|
|
74
|
+
args?: unknown;
|
|
75
|
+
}, {
|
|
76
|
+
id: string;
|
|
77
|
+
kind: "call";
|
|
78
|
+
tool: string;
|
|
79
|
+
args?: unknown;
|
|
80
|
+
}>, z.ZodDiscriminatedUnion<"ok", [z.ZodObject<{
|
|
81
|
+
id: z.ZodString;
|
|
82
|
+
kind: z.ZodLiteral<"result">;
|
|
83
|
+
ok: z.ZodLiteral<true>;
|
|
84
|
+
data: z.ZodOptional<z.ZodUnknown>;
|
|
85
|
+
}, "strip", z.ZodTypeAny, {
|
|
86
|
+
id: string;
|
|
87
|
+
kind: "result";
|
|
88
|
+
ok: true;
|
|
89
|
+
data?: unknown;
|
|
90
|
+
}, {
|
|
91
|
+
id: string;
|
|
92
|
+
kind: "result";
|
|
93
|
+
ok: true;
|
|
94
|
+
data?: unknown;
|
|
95
|
+
}>, z.ZodObject<{
|
|
96
|
+
id: z.ZodString;
|
|
97
|
+
kind: z.ZodLiteral<"result">;
|
|
98
|
+
ok: z.ZodLiteral<false>;
|
|
99
|
+
error: z.ZodString;
|
|
100
|
+
}, "strip", z.ZodTypeAny, {
|
|
101
|
+
id: string;
|
|
102
|
+
kind: "result";
|
|
103
|
+
ok: false;
|
|
104
|
+
error: string;
|
|
105
|
+
}, {
|
|
106
|
+
id: string;
|
|
107
|
+
kind: "result";
|
|
108
|
+
ok: false;
|
|
109
|
+
error: string;
|
|
110
|
+
}>]>, z.ZodObject<{
|
|
111
|
+
kind: z.ZodLiteral<"hello">;
|
|
112
|
+
editorVersion: z.ZodOptional<z.ZodString>;
|
|
113
|
+
}, "strip", z.ZodTypeAny, {
|
|
114
|
+
kind: "hello";
|
|
115
|
+
editorVersion?: string | undefined;
|
|
116
|
+
}, {
|
|
117
|
+
kind: "hello";
|
|
118
|
+
editorVersion?: string | undefined;
|
|
119
|
+
}>]>;
|
|
120
|
+
export type WSMessage = z.infer<typeof WSMessage>;
|
|
121
|
+
export interface CallSink {
|
|
122
|
+
send(msg: WSCall): void;
|
|
123
|
+
}
|
|
124
|
+
/** Tracks in-flight `call` ids and resolves them when the matching `result` arrives.
|
|
125
|
+
* Times out after `timeoutMs` so a dead editor doesn't hang the MCP server. */
|
|
126
|
+
export declare class PendingCalls {
|
|
127
|
+
#private;
|
|
128
|
+
private readonly sink;
|
|
129
|
+
private readonly defaultTimeoutMs;
|
|
130
|
+
constructor(sink: CallSink, defaultTimeoutMs?: number);
|
|
131
|
+
call(tool: string, args?: unknown, timeoutMs?: number): Promise<unknown>;
|
|
132
|
+
/** Match an incoming result to a pending call. Returns true if it was handled. */
|
|
133
|
+
resolve(msg: WSResult): boolean;
|
|
134
|
+
/** Reject every in-flight call (e.g. when the editor disconnects). */
|
|
135
|
+
abortAll(reason: string): void;
|
|
136
|
+
get pendingCount(): number;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB;;+EAE+E;AAE/E,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;EAKjB,CAAA;AACF,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAA;AAE3C,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAGnB,CAAA;AACF,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAA;AAE/C,eAAO,MAAM,OAAO;;;;;;;;;EAGlB,CAAA;AACF,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,OAAO,CAAC,CAAA;AAE7C,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAAuC,CAAA;AAC7D,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAA;AAEjD,MAAM,WAAW,QAAQ;IACvB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED;gFACgF;AAChF,qBAAa,YAAY;;IAIX,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAY,OAAO,CAAC,QAAQ,CAAC,gBAAgB;gBAAjD,IAAI,EAAE,QAAQ,EAAmB,gBAAgB,SAAO;IAErF,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,SAAS,SAAwB,GAAG,OAAO,CAAC,OAAO,CAAC;IAYvF,kFAAkF;IAClF,OAAO,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO;IAU/B,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAK9B,IAAI,YAAY,IAAI,MAAM,CAA8B;CACzD"}
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Wire protocol between the MCP server (Node) and the editor (browser) over WebSocket.
|
|
3
|
+
* All messages are JSON envelopes with a correlation `id`. The server initiates `call`;
|
|
4
|
+
* the editor replies with one `result` carrying ok/data or ok=false/error. */
|
|
5
|
+
export const WSCall = z.object({
|
|
6
|
+
id: z.string(),
|
|
7
|
+
kind: z.literal('call'),
|
|
8
|
+
tool: z.string(),
|
|
9
|
+
args: z.unknown().optional(),
|
|
10
|
+
});
|
|
11
|
+
export const WSResult = z.discriminatedUnion('ok', [
|
|
12
|
+
z.object({ id: z.string(), kind: z.literal('result'), ok: z.literal(true), data: z.unknown().optional() }),
|
|
13
|
+
z.object({ id: z.string(), kind: z.literal('result'), ok: z.literal(false), error: z.string() }),
|
|
14
|
+
]);
|
|
15
|
+
export const WSHello = z.object({
|
|
16
|
+
kind: z.literal('hello'),
|
|
17
|
+
editorVersion: z.string().optional(),
|
|
18
|
+
});
|
|
19
|
+
export const WSMessage = z.union([WSCall, WSResult, WSHello]);
|
|
20
|
+
/** Tracks in-flight `call` ids and resolves them when the matching `result` arrives.
|
|
21
|
+
* Times out after `timeoutMs` so a dead editor doesn't hang the MCP server. */
|
|
22
|
+
export class PendingCalls {
|
|
23
|
+
sink;
|
|
24
|
+
defaultTimeoutMs;
|
|
25
|
+
#counter = 0;
|
|
26
|
+
#pending = new Map();
|
|
27
|
+
constructor(sink, defaultTimeoutMs = 5000) {
|
|
28
|
+
this.sink = sink;
|
|
29
|
+
this.defaultTimeoutMs = defaultTimeoutMs;
|
|
30
|
+
}
|
|
31
|
+
call(tool, args, timeoutMs = this.defaultTimeoutMs) {
|
|
32
|
+
const id = `c${++this.#counter}`;
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
this.#pending.delete(id);
|
|
36
|
+
reject(new Error(`mcp tool '${tool}' timed out after ${timeoutMs}ms (is the editor connected?)`));
|
|
37
|
+
}, timeoutMs);
|
|
38
|
+
this.#pending.set(id, { resolve, reject, timer });
|
|
39
|
+
this.sink.send({ id, kind: 'call', tool, args });
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** Match an incoming result to a pending call. Returns true if it was handled. */
|
|
43
|
+
resolve(msg) {
|
|
44
|
+
const entry = this.#pending.get(msg.id);
|
|
45
|
+
if (!entry)
|
|
46
|
+
return false;
|
|
47
|
+
clearTimeout(entry.timer);
|
|
48
|
+
this.#pending.delete(msg.id);
|
|
49
|
+
if (msg.ok)
|
|
50
|
+
entry.resolve(msg.data);
|
|
51
|
+
else
|
|
52
|
+
entry.reject(new Error(msg.error));
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
/** Reject every in-flight call (e.g. when the editor disconnects). */
|
|
56
|
+
abortAll(reason) {
|
|
57
|
+
for (const [, entry] of this.#pending) {
|
|
58
|
+
clearTimeout(entry.timer);
|
|
59
|
+
entry.reject(new Error(reason));
|
|
60
|
+
}
|
|
61
|
+
this.#pending.clear();
|
|
62
|
+
}
|
|
63
|
+
get pendingCount() { return this.#pending.size; }
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
|