@hediet/linkrpc-mcp 0.0.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 ADDED
@@ -0,0 +1,82 @@
1
+ # @hediet/linkrpc-mcp
2
+
3
+ Stdio MCP server that lets an LLM drive a [linkrpc](../linkrpc) hub.
4
+
5
+ ## Tools
6
+
7
+ ### `runLinkRpcScript`
8
+
9
+ Evaluates a JS function inside a [QuickJS](https://github.com/justjake/quickjs-emscripten) sandbox (~5 s CPU, ~32 MB memory) with these globals in scope:
10
+
11
+ | Name | Description |
12
+ | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `con` | Live hub connection — see the `connection.d.ts` resource for the full signature. Covers `con.call`, `con.notify`, `con.callRaw`, `con.notifyRaw`, **and `con.explore(...)`** for service discovery. |
14
+ | `lastResultVal` | The previous `runLinkRpcScript` result against the same hub. JSON round-tripped. `undefined` on the first call. |
15
+ | `mcp` | Optional result-presentation helpers: `raw`, `text`, `image`, `audio`, `resource`, `resourceLink`, `content`, and `result`. |
16
+ | `console.log/warn/error` | Captured and returned alongside the result. |
17
+
18
+ `code` must evaluate to a function:
19
+
20
+ ```js
21
+ ({ con }) => con.call("vscode", "vscode.window",
22
+ "showInformationMessage",
23
+ { message: "hello world" })
24
+ ```
25
+
26
+ Set `connection` to a hub endpoint URI (e.g. `unix:/path?token=…`, `npipe://./pipe/…?token=…`, or `wss://host?token=…`) to target a specific hub, or omit it to use the `LINKRPC_ENDPOINT` / `LINKRPC_TOKEN` env vars (set by the team-tools extension on every terminal). The token may be embedded in the URI or supplied via `LINKRPC_TOKEN`.
27
+
28
+ #### Result presentation
29
+
30
+ Return values remain unchanged while the script runs, when passed to another hub
31
+ service, and when stored in `lastResultVal`. At the final MCP boundary, the server
32
+ automatically recognizes:
33
+
34
+ - MCP text, image, audio, embedded-resource, and resource-link blocks;
35
+ - `data:` URLs;
36
+ - common image/audio/document base64 signatures;
37
+ - `{ data|base64|blob, mimeType }` and `{ text, mimeType }` objects.
38
+
39
+ Recognized values become native MCP content blocks. Their base64 is replaced in the
40
+ JSON text and `structuredContent` fallbacks by a small descriptor. Set
41
+ `presentation: "raw"` on `runLinkRpcScript`, `awaitLinkRpcTask`, or
42
+ `cancelLinkRpcTask` to expose the original JSON, or selectively return
43
+ `mcp.raw(value)`.
44
+
45
+ Explicit content needs little ceremony:
46
+
47
+ ```js
48
+ ({ mcp }) => mcp.result({
49
+ value: { width: 800, height: 600 },
50
+ content: [
51
+ mcp.image(pngBase64, "image/png"),
52
+ mcp.resourceLink({
53
+ uri: "file:///report.pdf",
54
+ name: "report.pdf",
55
+ mimeType: "application/pdf",
56
+ }),
57
+ ],
58
+ })
59
+ ```
60
+
61
+ The `mcp.*` values are presentation markers intended to be returned from the
62
+ script. Keep using the original value for intermediate hub calls.
63
+
64
+ ## Resources
65
+
66
+ ### `linkrpc-mcp://docs/connection.d.ts`
67
+
68
+ TypeScript declarations for the in-sandbox API (`con`, `console`, `lastResultVal`). Read this once before authoring `runLinkRpcScript` calls.
69
+
70
+ ## Running locally
71
+
72
+ ```jsonc
73
+ // .vscode/mcp.json
74
+ {
75
+ "servers": {
76
+ "linkrpc": {
77
+ "command": "npx",
78
+ "args": ["@hediet/linkrpc-mcp"]
79
+ }
80
+ }
81
+ }
82
+ ```
@@ -0,0 +1,248 @@
1
+ import { IMessageTransport, IRequestSender, SignedCapability, SigningCallCtx } from "@hediet/linkrpc";
2
+ import { ResolvedEndpoint } from "@hediet/linkrpc/node";
3
+ import { HubAccessRequest, HubAccessResult } from "@hediet/linkrpc/hub/common";
4
+ import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
5
+ import { HubSigningSender } from "@hediet/linkrpc/hub/client";
6
+ //#region src/connectionPool.d.ts
7
+ /** Receives one trace line at a time. */
8
+ type TraceListener = (line: string) => void;
9
+ /**
10
+ * Minimal hub-access surface the MCP tools consume from a connection: read the
11
+ * current grants and request more. Satisfied by both a CLI {@link SigningSession}
12
+ * (endpoint pool) and a {@link import('@hediet/linkrpc/hub/client').HubSigningSender}
13
+ * (consumer-provided sender).
14
+ */
15
+ interface HubAccess {
16
+ listGrants(): readonly SignedCapability[];
17
+ requestAccess(req: HubAccessRequest): Promise<HubAccessResult>;
18
+ }
19
+ /**
20
+ * One MCP-server-wide live linkrpc connection. We keep a single connection per
21
+ * unique endpoint URI (path/url + token) so repeated tool calls reuse the same
22
+ * socket and the per-connection `lastResultVal` survives across runs.
23
+ */
24
+ interface PooledConnection {
25
+ readonly channel: IRequestSender<SigningCallCtx>;
26
+ /** Redacted endpoint URI of this connection, for labels and tool results. */
27
+ readonly endpoint: string;
28
+ /**
29
+ * Canonical endpoint URI (token revealed) — uniquely identifies this
30
+ * connection. Used to key per-connection background tasks so that a new
31
+ * request on the same connection supersedes the previous task.
32
+ */
33
+ readonly key: string;
34
+ /**
35
+ * Hub signing session: signs every outbound call with the connection's
36
+ * identity and exposes the grant surface ({@link HubAccess}). For the
37
+ * endpoint pool this is a CLI `SigningSession`; for a consumer-provided
38
+ * sender it is the sender itself.
39
+ */
40
+ readonly session: HubAccess;
41
+ /** Most recent value returned by `runLinkRpcScript`. Updated after each run. */
42
+ lastResultVal: unknown;
43
+ /**
44
+ * Subscribe `listener` to every JSON-RPC trace line produced while
45
+ * the subscription is live (in addition to the always-on stderr
46
+ * mirror). Returns a disposer that removes the listener. Multiple
47
+ * concurrent listeners are supported — each sees every line —
48
+ * which means concurrent runs on the same pooled connection will
49
+ * see each other's traces. That matches the channel's own
50
+ * concurrency model.
51
+ */
52
+ addTraceListener(listener: TraceListener): () => void;
53
+ /** Emit a trace line on this connection (also writes to stderr). */
54
+ trace(line: string): void;
55
+ /** Close the underlying socket and drop the entry from the pool. */
56
+ dispose(): void;
57
+ }
58
+ /**
59
+ * Subset of {@link ConnectionPool} used by the MCP server, extracted so tests
60
+ * can inject a fake pool without standing up a real hub connection.
61
+ */
62
+ interface IConnectionPool {
63
+ resolve(endpointUri: string | undefined): Promise<PooledConnection>;
64
+ dispose(): void;
65
+ }
66
+ /** Plain JSON-RPC connection descriptor sent by the MCP client. */
67
+ interface ConnectionPoolOptions {
68
+ /**
69
+ * Endpoint used when a tool call does not specify a `connection`. When
70
+ * neither the caller nor a default is supplied, the pool falls back to the
71
+ * `LINKRPC_ENDPOINT` / `LINKRPC_TOKEN` environment variables.
72
+ */
73
+ readonly defaultEndpoint?: ResolvedEndpoint;
74
+ /**
75
+ * Opens the **default** connection (the one used when a tool call supplies
76
+ * no `connection` argument) over an in-process transport instead of dialing
77
+ * a socket — typically a leg into an in-process hub participant. When set it
78
+ * takes precedence over {@link defaultEndpoint} / env vars for the
79
+ * no-argument case; explicit `connection` endpoint URIs still dial normally.
80
+ *
81
+ * Called lazily on first use; the returned {@link DefaultTransport.dispose}
82
+ * runs when the pooled connection is disposed.
83
+ */
84
+ readonly defaultTransport?: () => DefaultTransport;
85
+ }
86
+ /**
87
+ * An in-process transport supplying the pool's default connection, plus
88
+ * presentation/teardown hooks. The pool wraps {@link transport} in a managed
89
+ * signing connection ({@link connectViaTransport} + `setupSigning`), exactly as
90
+ * it would a dialed socket.
91
+ */
92
+ interface DefaultTransport {
93
+ /** In-memory transport whose peer serves the hub (incl. `identity::*`). */
94
+ readonly transport: IMessageTransport;
95
+ /** Human-readable label for traces / tool results. Defaults to `"inproc"`. */
96
+ readonly label?: string;
97
+ /** Extra teardown run when the pooled connection is disposed (after `cli.close`). */
98
+ readonly dispose?: () => void;
99
+ }
100
+ /**
101
+ * Maintains live hub connections keyed by the canonical endpoint URI (path/url
102
+ * + token). Connections are created lazily on first use and reused for every
103
+ * subsequent call against the same endpoint. Setting up the hub signing session
104
+ * (which may surface a consent modal the first time) happens once per endpoint
105
+ * and the resulting session is cached on the pool entry.
106
+ */
107
+ declare class ConnectionPool implements IConnectionPool {
108
+ private readonly _entries;
109
+ private readonly _defaultEndpoint;
110
+ private readonly _defaultTransport;
111
+ /** Pool key for the in-process default connection (see {@link ConnectionPoolOptions.defaultTransport}). */
112
+ private static readonly _DEFAULT_INPROC_KEY;
113
+ constructor(options?: ConnectionPoolOptions);
114
+ /**
115
+ * Resolve a pooled connection for `endpointUri` (a strict endpoint URI such
116
+ * as `unix:/path?token=…`, `npipe://./pipe/…?token=…`, or
117
+ * `wss://host?token=…`). When omitted, an in-process default transport (if
118
+ * configured) is used; otherwise it falls back to the configured default
119
+ * endpoint, then to the `LINKRPC_ENDPOINT` / `LINKRPC_TOKEN` env vars. A token
120
+ * absent from the URI is filled in from `LINKRPC_TOKEN` when present.
121
+ */
122
+ resolve(endpointUri: string | undefined): Promise<PooledConnection>;
123
+ /**
124
+ * Shared cache-or-open: returns the in-flight/cached entry for `key`, or
125
+ * starts `open(key)` and caches the promise so concurrent callers share the
126
+ * same signing setup. On failure the entry is evicted so the next call
127
+ * retries cleanly.
128
+ */
129
+ private _resolveCached;
130
+ private _resolveSpec;
131
+ private _open;
132
+ private _openInProc;
133
+ /**
134
+ * Wrap an already-open {@link CliConnection} in a managed signing session
135
+ * and build the {@link PooledConnection}. Shared by the dialed-socket and
136
+ * in-process default paths — both sign as a managed principal and bootstrap
137
+ * the `hubAccess` cap, the only difference being how the transport was
138
+ * obtained. `extraDispose` runs on disposal after the connection is closed.
139
+ */
140
+ private _finishOpen;
141
+ dispose(): void;
142
+ }
143
+ //#endregion
144
+ //#region src/senderProvider.d.ts
145
+ /** Identifies the MCP session a connection is opened for (consumer-provided mode). */
146
+ interface McpSessionInfo {
147
+ readonly sessionId?: string;
148
+ readonly authorization?: string;
149
+ }
150
+ /**
151
+ * Resolves a {@link HubSigningSender} for an MCP session and an optional
152
+ * `connection` endpoint argument (`undefined` → the provider's default). This is
153
+ * the single injection seam for both identity-provision modes:
154
+ * - package-provisioned: the provider dials an endpoint and signs (managed).
155
+ * - consumer-provisioned: the embedder returns a sender bound to a per-session
156
+ * identity it owns (e.g. an in-process hub participant).
157
+ */
158
+ type HubSenderProvider = (session: McpSessionInfo | undefined, endpoint: string | undefined) => Promise<HubSigningSender>;
159
+ /**
160
+ * {@link IConnectionPool} backed by a {@link HubSenderProvider}: caches one
161
+ * {@link HubSigningSender} per distinct `connection` argument and adapts it to
162
+ * the {@link PooledConnection} shape the MCP tools consume. Holds the
163
+ * MCP-layer-only `lastResultVal` and trace fan-out so the sender stays pure.
164
+ */
165
+ declare class ProviderPool implements IConnectionPool {
166
+ private readonly _provider;
167
+ private readonly _session?;
168
+ private readonly _entries;
169
+ constructor(_provider: HubSenderProvider, _session?: McpSessionInfo | undefined);
170
+ resolve(endpointUri: string | undefined): Promise<PooledConnection>;
171
+ private _open;
172
+ dispose(): void;
173
+ }
174
+ //#endregion
175
+ //#region src/server.d.ts
176
+ interface McpToolCall {
177
+ readonly name: string;
178
+ readonly arguments: unknown;
179
+ readonly result: unknown;
180
+ }
181
+ type McpExploreCall = {
182
+ readonly arguments: unknown;
183
+ readonly result: unknown;
184
+ } | {
185
+ readonly arguments: unknown;
186
+ readonly error: string;
187
+ };
188
+ interface LinkRpcMcpServerOptions {
189
+ /**
190
+ * Endpoint used when the tool call does not supply a `connection`. When
191
+ * omitted, the server falls back to the `LINKRPC_ENDPOINT` / `LINKRPC_TOKEN`
192
+ * environment variables (the stdio CLI mode).
193
+ */
194
+ readonly defaultEndpoint?: ResolvedEndpoint;
195
+ /**
196
+ * Connection pool to use. Defaults to a real {@link ConnectionPool}.
197
+ * Injectable so tests can supply a fake pool without a live hub.
198
+ */
199
+ readonly pool?: IConnectionPool;
200
+ /**
201
+ * Resolves a signing sender per MCP session / `connection` argument instead
202
+ * of dialing an endpoint. When set (and no explicit `pool` is given), the
203
+ * server uses a {@link ProviderPool} — the consumer-provisioned identity
204
+ * mode. Mutually exclusive with `defaultEndpoint`.
205
+ */
206
+ readonly provider?: HubSenderProvider;
207
+ /**
208
+ * Supplies the **default** connection (used when a tool call omits
209
+ * `connection`) over an in-process transport — typically a leg into an
210
+ * in-process hub participant — instead of dialing a socket. Explicit
211
+ * `connection` endpoint URIs still dial normally. Combine with
212
+ * {@link defaultEndpoint} to additionally set a dialed fallback; mutually
213
+ * exclusive with `pool` / `provider`.
214
+ */
215
+ readonly defaultConnection?: () => DefaultTransport;
216
+ /** Observes completed MCP tool calls exactly as their result is returned to the client. */
217
+ readonly onToolCall?: (call: McpToolCall) => void;
218
+ /** Observes every sandbox `con.explore` call, including calls that fail. */
219
+ readonly onExploreCall?: (call: McpExploreCall) => void;
220
+ }
221
+ /**
222
+ * MCP server exposing the `runLinkRpcScript` tool plus task-management helpers.
223
+ *
224
+ * Documentation for the sandbox API (`con`) is reachable from inside the
225
+ * sandbox via `con.getDocs()` rather than an MCP resource — that way the
226
+ * model can pull it in by issuing a one-line `runLinkRpcScript` call.
227
+ *
228
+ * Can be hosted over stdio (CLI mode, via {@link startStdio}) or over any
229
+ * MCP {@link Transport} (e.g. Streamable HTTP, used by the VS Code Team
230
+ * Tools extension which embeds this server in-process).
231
+ */
232
+ declare class LinkRpcMcpServer {
233
+ static startStdio(options?: LinkRpcMcpServerOptions): Promise<LinkRpcMcpServer>;
234
+ private readonly _mcp;
235
+ private readonly _pool;
236
+ private readonly _tasks;
237
+ private readonly _onToolCall;
238
+ private readonly _onExploreCall;
239
+ constructor(options?: LinkRpcMcpServerOptions);
240
+ connect(transport: Transport): Promise<void>;
241
+ dispose(): void;
242
+ private _registerTools;
243
+ private _observeToolCall;
244
+ private _observeExploreCall;
245
+ }
246
+ //#endregion
247
+ export { HubSenderProvider as a, ConnectionPool as c, HubAccess as d, IConnectionPool as f, McpToolCall as i, ConnectionPoolOptions as l, TraceListener as m, LinkRpcMcpServerOptions as n, McpSessionInfo as o, PooledConnection as p, McpExploreCall as r, ProviderPool as s, LinkRpcMcpServer as t, DefaultTransport as u };
248
+ //# sourceMappingURL=server-Bquqk9Mk.d.ts.map