@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 +82 -0
- package/dist/chunks/server-Bquqk9Mk.d.ts +248 -0
- package/dist/chunks/server-C4ZM6bfK.js +2530 -0
- package/dist/chunks/server-C4ZM6bfK.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +23 -0
- package/dist/cli.js.map +1 -0
- package/dist/guestMain.d.ts +1 -0
- package/dist/index.d.ts +229 -0
- package/dist/index.js +2 -0
- package/dist/node.d.ts +121 -0
- package/dist/node.js +163 -0
- package/dist/node.js.map +1 -0
- package/dist/src-guest-guestMain.js +168 -0
- package/dist/src-guest-guestMain.js.map +1 -0
- package/package.json +49 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { a as HubSenderProvider, c as ConnectionPool, d as HubAccess, f as IConnectionPool, i as McpToolCall, l as ConnectionPoolOptions, m as TraceListener, n as LinkRpcMcpServerOptions, o as McpSessionInfo, p as PooledConnection, r as McpExploreCall, s as ProviderPool, t as LinkRpcMcpServer, u as DefaultTransport } from "./chunks/server-Bquqk9Mk.js";
|
|
2
|
+
import { IRequestSender, LinkRpcInterfaceSchema, SigningCallCtx } from "@hediet/linkrpc";
|
|
3
|
+
//#region src/explore.d.ts
|
|
4
|
+
interface ExploreCommonArgs {
|
|
5
|
+
/** Exact directory-level filters, applied before browsing or searching. */
|
|
6
|
+
readonly serviceId?: string;
|
|
7
|
+
readonly interfaceId?: string;
|
|
8
|
+
/** Include reflection plumbing such as `hubrpc.directory` and `hubrpc.schemas`. */
|
|
9
|
+
readonly includeInternal?: boolean;
|
|
10
|
+
/** Request one broad reflection grant when a gated directory is encountered. */
|
|
11
|
+
readonly requestPermission?: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface ExploreBrowseArgs extends ExploreCommonArgs {
|
|
14
|
+
readonly kind: 'browse';
|
|
15
|
+
/** Number of interfaces to return. Defaults to 20; maximum 100. */
|
|
16
|
+
readonly limit?: number;
|
|
17
|
+
/** Opaque continuation cursor returned by a previous browse call. */
|
|
18
|
+
readonly cursor?: string;
|
|
19
|
+
}
|
|
20
|
+
interface ExploreGrepArgs extends ExploreCommonArgs {
|
|
21
|
+
readonly kind: 'grep';
|
|
22
|
+
/** Pattern searched against the generated `defineInterface` source, one line at a time. */
|
|
23
|
+
readonly pattern: string;
|
|
24
|
+
/** Defaults to `regex`. Both modes are case-insensitive. */
|
|
25
|
+
readonly syntax?: 'regex' | 'literal';
|
|
26
|
+
/** Number of matching virtual documents to return. Defaults to 20; maximum 100. */
|
|
27
|
+
readonly limit?: number;
|
|
28
|
+
/** Opaque continuation cursor returned by a previous grep call. */
|
|
29
|
+
readonly cursor?: string;
|
|
30
|
+
/** Source lines included before and after each match. Defaults to 1; maximum 5. */
|
|
31
|
+
readonly contextLines?: number;
|
|
32
|
+
}
|
|
33
|
+
interface ExploreInspectArgs extends ExploreCommonArgs {
|
|
34
|
+
readonly kind: 'inspect';
|
|
35
|
+
readonly serviceId: string;
|
|
36
|
+
readonly interfaceId: string;
|
|
37
|
+
/** Generated `defineInterface` source by default; use `schema` for the raw wire schema. */
|
|
38
|
+
readonly format?: 'source' | 'schema';
|
|
39
|
+
}
|
|
40
|
+
type ExploreArgs = ExploreBrowseArgs | ExploreGrepArgs | ExploreInspectArgs;
|
|
41
|
+
interface ExploreDeps {
|
|
42
|
+
requestReflectionAccess(): Promise<boolean>;
|
|
43
|
+
}
|
|
44
|
+
interface ExploreListing {
|
|
45
|
+
readonly serviceId: string;
|
|
46
|
+
readonly serviceDescription?: string;
|
|
47
|
+
readonly interfaceId: string;
|
|
48
|
+
readonly interfaceHash: string;
|
|
49
|
+
readonly documentId: string;
|
|
50
|
+
}
|
|
51
|
+
interface ExploreGrepMatch {
|
|
52
|
+
/** Matching source plus the requested surrounding context, joined with newlines. */
|
|
53
|
+
readonly searchResult: string;
|
|
54
|
+
/** Inclusive, 1-based line range of `searchResult` in the virtual document. */
|
|
55
|
+
readonly lineRange: readonly [start: number, end: number];
|
|
56
|
+
/** LinkRPC member containing every matched line in this chunk, when unambiguous. */
|
|
57
|
+
readonly member?: string;
|
|
58
|
+
}
|
|
59
|
+
interface ExploreGrepEntry extends ExploreListing {
|
|
60
|
+
readonly matches: readonly ExploreGrepMatch[];
|
|
61
|
+
/** True when this document contains more matches than were returned. */
|
|
62
|
+
readonly matchesTruncated?: boolean;
|
|
63
|
+
}
|
|
64
|
+
interface ExploreDocumentError {
|
|
65
|
+
readonly serviceId: string;
|
|
66
|
+
readonly interfaceId: string;
|
|
67
|
+
readonly error: string;
|
|
68
|
+
}
|
|
69
|
+
interface ExploreInaccessible {
|
|
70
|
+
readonly serviceId: string;
|
|
71
|
+
readonly reason: string;
|
|
72
|
+
readonly hint: string;
|
|
73
|
+
}
|
|
74
|
+
interface ExploreBrowseResult {
|
|
75
|
+
readonly kind: 'browse';
|
|
76
|
+
readonly total: number;
|
|
77
|
+
readonly entries: readonly ExploreListing[];
|
|
78
|
+
readonly nextCursor?: string;
|
|
79
|
+
readonly inaccessible?: readonly ExploreInaccessible[];
|
|
80
|
+
}
|
|
81
|
+
interface ExploreGrepResult {
|
|
82
|
+
readonly kind: 'grep';
|
|
83
|
+
readonly pattern: string;
|
|
84
|
+
readonly syntax: 'regex' | 'literal';
|
|
85
|
+
readonly total: number;
|
|
86
|
+
readonly entries: readonly ExploreGrepEntry[];
|
|
87
|
+
readonly nextCursor?: string;
|
|
88
|
+
readonly documentErrors?: readonly ExploreDocumentError[];
|
|
89
|
+
readonly inaccessible?: readonly ExploreInaccessible[];
|
|
90
|
+
}
|
|
91
|
+
interface ExploreInspectResult extends ExploreListing {
|
|
92
|
+
readonly kind: 'inspect';
|
|
93
|
+
readonly format: 'source' | 'schema';
|
|
94
|
+
readonly source?: string;
|
|
95
|
+
readonly schema?: LinkRpcInterfaceSchema;
|
|
96
|
+
readonly inaccessible?: readonly ExploreInaccessible[];
|
|
97
|
+
}
|
|
98
|
+
type ExploreResult = ExploreBrowseResult | ExploreGrepResult | ExploreInspectResult;
|
|
99
|
+
/**
|
|
100
|
+
* Browse the reflected directory, grep generated interface source, or inspect
|
|
101
|
+
* one exact virtual document. Generated source is cached by the directory
|
|
102
|
+
* route and interface hash; every call still walks the live directory.
|
|
103
|
+
*/
|
|
104
|
+
declare function explore(channel: IRequestSender<SigningCallCtx>, args: ExploreArgs, deps?: ExploreDeps): Promise<ExploreResult>;
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/sandbox.d.ts
|
|
107
|
+
/**
|
|
108
|
+
* Async host functions exposed to the guest. Each returns either:
|
|
109
|
+
* - a JSON-stringified value (the guest will `JSON.parse` it), or
|
|
110
|
+
* - the empty string for `undefined` results (used for `notify`).
|
|
111
|
+
*
|
|
112
|
+
* Host rejections become guest `Error`s with a bounded snapshot of every
|
|
113
|
+
* serializable own data property preserved.
|
|
114
|
+
*/
|
|
115
|
+
interface SandboxHostApi {
|
|
116
|
+
call(method: string, paramsJson: string, optsJson: string, onStreamMessage: (payloadJson: string) => void, signal: AbortSignal): Promise<string>;
|
|
117
|
+
notify(method: string, paramsJson: string): Promise<string>;
|
|
118
|
+
explore(argsJson: string): Promise<string>;
|
|
119
|
+
requestAccess(argsJson: string): Promise<string>;
|
|
120
|
+
grants(argsJson: string): Promise<string>;
|
|
121
|
+
}
|
|
122
|
+
type SandboxLog = {
|
|
123
|
+
level: "log" | "warn" | "error";
|
|
124
|
+
text: string;
|
|
125
|
+
};
|
|
126
|
+
/** Legacy options for {@link runSandboxed} (run-to-completion, no parking). */
|
|
127
|
+
interface SandboxOptions {
|
|
128
|
+
/** Foreground budget in ms. Defaults to {@link DEFAULT_FOREGROUND_MS}. */
|
|
129
|
+
readonly timeoutMs?: number;
|
|
130
|
+
readonly memoryLimitBytes?: number;
|
|
131
|
+
}
|
|
132
|
+
/** Options for {@link startSandbox} (parking-capable). */
|
|
133
|
+
interface StartSandboxOptions {
|
|
134
|
+
/**
|
|
135
|
+
* How long the call may run synchronously before it is parked as a
|
|
136
|
+
* background task. Defaults to {@link DEFAULT_FOREGROUND_MS}.
|
|
137
|
+
*/
|
|
138
|
+
readonly foregroundMs?: number;
|
|
139
|
+
/**
|
|
140
|
+
* Absolute lifetime cap for a parked task. After this the task is
|
|
141
|
+
* soft-aborted and settled with an error. Defaults to
|
|
142
|
+
* {@link DEFAULT_MAX_LIFETIME_MS}. Must be `> foregroundMs` for parking
|
|
143
|
+
* to be possible.
|
|
144
|
+
*/
|
|
145
|
+
readonly maxLifetimeMs?: number;
|
|
146
|
+
readonly memoryLimitBytes?: number;
|
|
147
|
+
/**
|
|
148
|
+
* Human-readable debug name for the parked task. When omitted, the name
|
|
149
|
+
* is inferred from the in-flight RPC(s) at park time, falling back to the
|
|
150
|
+
* first line of `code`.
|
|
151
|
+
*/
|
|
152
|
+
readonly label?: string;
|
|
153
|
+
}
|
|
154
|
+
/** Legacy result shape for {@link runSandboxed}. */
|
|
155
|
+
interface SandboxRunResult {
|
|
156
|
+
readonly resultJson: string;
|
|
157
|
+
readonly logs: readonly SandboxLog[];
|
|
158
|
+
}
|
|
159
|
+
/** Terminal state of a parked task. */
|
|
160
|
+
type TaskOutcome = {
|
|
161
|
+
readonly status: "completed";
|
|
162
|
+
readonly result: unknown;
|
|
163
|
+
readonly logs: readonly SandboxLog[];
|
|
164
|
+
} | {
|
|
165
|
+
readonly status: "error";
|
|
166
|
+
readonly error: string;
|
|
167
|
+
readonly logs: readonly SandboxLog[];
|
|
168
|
+
} | {
|
|
169
|
+
readonly status: "cancelled";
|
|
170
|
+
readonly logs: readonly SandboxLog[];
|
|
171
|
+
};
|
|
172
|
+
/** Result of the foreground phase of {@link startSandbox}. */
|
|
173
|
+
type SandboxOutcome = {
|
|
174
|
+
readonly status: "completed";
|
|
175
|
+
readonly result: unknown;
|
|
176
|
+
readonly logs: readonly SandboxLog[];
|
|
177
|
+
} | {
|
|
178
|
+
readonly status: "error";
|
|
179
|
+
readonly error: string;
|
|
180
|
+
readonly logs: readonly SandboxLog[];
|
|
181
|
+
} | {
|
|
182
|
+
readonly status: "parked";
|
|
183
|
+
readonly debugName: string;
|
|
184
|
+
readonly task: ParkedTask;
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* A live sandbox execution that did not settle within the foreground budget
|
|
188
|
+
* and is now pumping in the background until its in-flight work resolves, it
|
|
189
|
+
* is cancelled, or it hits the max-lifetime guard.
|
|
190
|
+
*/
|
|
191
|
+
interface ParkedTask {
|
|
192
|
+
/** Labels of the RPC(s) currently in flight (snapshot). */
|
|
193
|
+
inFlight(): string[];
|
|
194
|
+
/** Live view of logs captured so far. */
|
|
195
|
+
readonly logs: readonly SandboxLog[];
|
|
196
|
+
/** Resolves when the task reaches a terminal state. Never rejects. */
|
|
197
|
+
readonly done: Promise<TaskOutcome>;
|
|
198
|
+
/**
|
|
199
|
+
* Soft-abort the task and settle it. Resolves with the terminal outcome
|
|
200
|
+
* (`cancelled`, or `completed`/`error` if the guest settled cleanly
|
|
201
|
+
* within its grace window).
|
|
202
|
+
*/
|
|
203
|
+
cancel(): Promise<TaskOutcome>;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Run-to-completion entry point preserved for callers that just want a result
|
|
207
|
+
* and treat the timeout as a hard wall (the original behaviour). Throws on
|
|
208
|
+
* guest error or deadline; never parks.
|
|
209
|
+
*/
|
|
210
|
+
declare function runSandboxed(userCode: string, host: SandboxHostApi, lastResultVal: unknown, options?: SandboxOptions): Promise<SandboxRunResult>;
|
|
211
|
+
/**
|
|
212
|
+
* Parking-capable entry point. Runs `userCode` for up to `foregroundMs`; if it
|
|
213
|
+
* settles in that window the result is returned inline, otherwise the live VM
|
|
214
|
+
* is detached into a {@link ParkedTask} that keeps pumping in the background.
|
|
215
|
+
*/
|
|
216
|
+
declare function startSandbox(userCode: string, host: SandboxHostApi, lastResultVal: unknown, options?: StartSandboxOptions): Promise<SandboxOutcome>;
|
|
217
|
+
//#endregion
|
|
218
|
+
//#region src/connectionDts.d.ts
|
|
219
|
+
/**
|
|
220
|
+
* Documentation that the MCP server exposes as a resource. The text is the
|
|
221
|
+
* single source of truth for what `runLinkRpcScript` sees inside the QuickJS
|
|
222
|
+
* sandbox — it lives as the real declaration file `connection.d.ts` and is
|
|
223
|
+
* embedded here verbatim. Keep `connection.d.ts` in sync with `sandbox.ts`
|
|
224
|
+
* and `explore.ts`.
|
|
225
|
+
*/
|
|
226
|
+
declare const CONNECTION_DTS: string;
|
|
227
|
+
//#endregion
|
|
228
|
+
export { CONNECTION_DTS, ConnectionPool, ConnectionPoolOptions, DefaultTransport, ExploreArgs, ExploreBrowseArgs, ExploreBrowseResult, ExploreDeps, ExploreDocumentError, ExploreGrepArgs, ExploreGrepEntry, ExploreGrepMatch, ExploreGrepResult, ExploreInaccessible, ExploreInspectArgs, ExploreInspectResult, ExploreListing, ExploreResult, HubAccess, HubSenderProvider, IConnectionPool, LinkRpcMcpServer, LinkRpcMcpServerOptions, McpExploreCall, McpSessionInfo, McpToolCall, ParkedTask, PooledConnection, ProviderPool, SandboxHostApi, SandboxLog, SandboxOptions, SandboxOutcome, SandboxRunResult, StartSandboxOptions, TaskOutcome, TraceListener, explore, runSandboxed, startSandbox };
|
|
229
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as CONNECTION_DTS, i as startSandbox, n as explore, o as ProviderPool, r as runSandboxed, s as ConnectionPool, t as LinkRpcMcpServer } from "./chunks/server-C4ZM6bfK.js";
|
|
2
|
+
export { CONNECTION_DTS, ConnectionPool, LinkRpcMcpServer, ProviderPool, explore, runSandboxed, startSandbox };
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { n as LinkRpcMcpServerOptions } from "./chunks/server-Bquqk9Mk.js";
|
|
2
|
+
//#region src/socketHost.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Per-session wiring the host asks its owner to provide. Created once per MCP
|
|
5
|
+
* `initialize`, keyed by `sessionId`. The owner decides how that session reaches
|
|
6
|
+
* the hub (e.g. an in-process `defaultConnection`) and what to tear down when the
|
|
7
|
+
* session closes.
|
|
8
|
+
*/
|
|
9
|
+
interface McpSession {
|
|
10
|
+
/** Options for this session's {@link LinkRpcMcpServer} (e.g. a `defaultConnection`). */
|
|
11
|
+
readonly serverOptions: LinkRpcMcpServerOptions;
|
|
12
|
+
/**
|
|
13
|
+
* Teardown for resources scoped to this session, run after the session's
|
|
14
|
+
* server is disposed. Note: this fires on MCP *session* close — do **not**
|
|
15
|
+
* wipe long-lived identity here if you want it to survive a client reload.
|
|
16
|
+
*/
|
|
17
|
+
readonly dispose?: () => void;
|
|
18
|
+
}
|
|
19
|
+
interface McpSocketHostOptions {
|
|
20
|
+
/** Display name used only for log lines. */
|
|
21
|
+
readonly label?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Mints per-session wiring. Called once per MCP `initialize`, before the
|
|
24
|
+
* client's session id is acknowledged. The `sessionId` is the id the host
|
|
25
|
+
* will report back to the client, so the owner can key identity on it (the
|
|
26
|
+
* consumer-provisioned mode).
|
|
27
|
+
*/
|
|
28
|
+
readonly createSession: (ctx: {
|
|
29
|
+
readonly sessionId: string;
|
|
30
|
+
}) => Promise<McpSession>;
|
|
31
|
+
/** Optional structured logger; defaults to a no-op. */
|
|
32
|
+
readonly log?: (message: string) => void;
|
|
33
|
+
/**
|
|
34
|
+
* Where the host listens:
|
|
35
|
+
* - `"socket"` (default): a local Unix domain socket / named pipe. Most
|
|
36
|
+
* private (no TCP port at all), but some MCP clients — notably the Copilot
|
|
37
|
+
* harness — don't support `unix`/`pipe` transports yet.
|
|
38
|
+
* - `"http"`: a loopback (`127.0.0.1`) TCP port with a random bearer token.
|
|
39
|
+
* Use this until the socket transport is supported everywhere.
|
|
40
|
+
*/
|
|
41
|
+
readonly transport?: "socket" | "http";
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Describes where the host is listening, as transport-level facts — not as any
|
|
45
|
+
* particular client's URI encoding. A consumer (e.g. the VS Code extension)
|
|
46
|
+
* translates this into whatever its MCP client expects; for VS Code that means a
|
|
47
|
+
* `unix`/`pipe` URI with the socket path in `uri.path` and {@link requestPath}
|
|
48
|
+
* in `uri.fragment`, but that encoding is the consumer's concern, not ours.
|
|
49
|
+
*/
|
|
50
|
+
type McpSocketEndpoint = {
|
|
51
|
+
/**
|
|
52
|
+
* `unixSocket` on posix, `namedPipe` on Windows. Discriminates how
|
|
53
|
+
* {@link McpSocketEndpoint.path} is interpreted by the OS / an HTTP
|
|
54
|
+
* client's `socketPath`.
|
|
55
|
+
*/
|
|
56
|
+
readonly kind: "unixSocket" | "namedPipe";
|
|
57
|
+
/**
|
|
58
|
+
* Exact OS path the server listens on and that an HTTP client passes to
|
|
59
|
+
* `socketPath`: a filesystem path for `unixSocket`, a `\\.\pipe\…` path
|
|
60
|
+
* for `namedPipe`. Note a `namedPipe` path contains backslashes and is
|
|
61
|
+
* therefore not representable as a URL string — keep it as-is rather
|
|
62
|
+
* than round-tripping through `URL`/`Uri.parse`.
|
|
63
|
+
*/
|
|
64
|
+
readonly path: string;
|
|
65
|
+
/** HTTP request path the host serves (defaults to `/mcp`). */
|
|
66
|
+
readonly requestPath: string;
|
|
67
|
+
/** Bearer token the client must send in the `Authorization` header. */
|
|
68
|
+
readonly authorizationToken: string;
|
|
69
|
+
} | {
|
|
70
|
+
/** A loopback TCP port, reachable over plain `http://`. */
|
|
71
|
+
readonly kind: "tcp";
|
|
72
|
+
/** Host the server is bound to (always `127.0.0.1`). */
|
|
73
|
+
readonly host: string;
|
|
74
|
+
/** TCP port the server listens on. */
|
|
75
|
+
readonly port: number;
|
|
76
|
+
/** HTTP request path the host serves (defaults to `/mcp`). */
|
|
77
|
+
readonly requestPath: string;
|
|
78
|
+
/** Bearer token the client must send in the `Authorization` header. */
|
|
79
|
+
readonly authorizationToken: string;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Hosts one or more {@link LinkRpcMcpServer} sessions over a local endpoint —
|
|
83
|
+
* by default a Unix domain socket on posix / a named pipe on Windows, or, when
|
|
84
|
+
* `transport: "http"` is set, a loopback (`127.0.0.1`) TCP port. The `http`
|
|
85
|
+
* transport exists for clients that don't yet support the `unix`/`pipe`
|
|
86
|
+
* transport (e.g. the Copilot harness).
|
|
87
|
+
*
|
|
88
|
+
* The host owns the generic plumbing: the socket/pipe or TCP port, bearer-token
|
|
89
|
+
* auth, and the per-session Streamable HTTP transport lifecycle. *How* each
|
|
90
|
+
* session reaches the hub — and what identity it signs as — is delegated
|
|
91
|
+
* entirely to {@link McpSocketHostOptions.createSession}, so the same host serves
|
|
92
|
+
* both the server-provisioned and consumer-provisioned identity modes.
|
|
93
|
+
*/
|
|
94
|
+
declare class McpSocketHost {
|
|
95
|
+
static start(options: McpSocketHostOptions): Promise<McpSocketHost>;
|
|
96
|
+
private readonly _options;
|
|
97
|
+
private readonly _http;
|
|
98
|
+
private readonly _token;
|
|
99
|
+
private readonly _transport;
|
|
100
|
+
private readonly _kind;
|
|
101
|
+
/** Set for the socket/pipe transport; the OS path we listen on. */
|
|
102
|
+
private readonly _path;
|
|
103
|
+
/** Loopback host for the TCP (`http`) transport. */
|
|
104
|
+
private readonly _host;
|
|
105
|
+
/** Bound port for the TCP (`http`) transport; filled in after `_listen`. */
|
|
106
|
+
private _port;
|
|
107
|
+
private readonly _transports;
|
|
108
|
+
private readonly _active;
|
|
109
|
+
private _disposed;
|
|
110
|
+
private constructor();
|
|
111
|
+
/** The advertised endpoint (socket/pipe path or TCP host:port + bearer token). */
|
|
112
|
+
get endpoint(): McpSocketEndpoint;
|
|
113
|
+
dispose(): void;
|
|
114
|
+
private _log;
|
|
115
|
+
private _listen;
|
|
116
|
+
private _handleHttp;
|
|
117
|
+
private _openSession;
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
export { McpSession, McpSocketEndpoint, McpSocketHost, McpSocketHostOptions };
|
|
121
|
+
//# sourceMappingURL=node.d.ts.map
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { t as LinkRpcMcpServer } from "./chunks/server-C4ZM6bfK.js";
|
|
2
|
+
import { randomBytes, randomUUID } from "crypto";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as http from "http";
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
8
|
+
//#region src/socketHost.ts
|
|
9
|
+
const REQUEST_PATH = "/mcp";
|
|
10
|
+
/**
|
|
11
|
+
* Hosts one or more {@link LinkRpcMcpServer} sessions over a local endpoint —
|
|
12
|
+
* by default a Unix domain socket on posix / a named pipe on Windows, or, when
|
|
13
|
+
* `transport: "http"` is set, a loopback (`127.0.0.1`) TCP port. The `http`
|
|
14
|
+
* transport exists for clients that don't yet support the `unix`/`pipe`
|
|
15
|
+
* transport (e.g. the Copilot harness).
|
|
16
|
+
*
|
|
17
|
+
* The host owns the generic plumbing: the socket/pipe or TCP port, bearer-token
|
|
18
|
+
* auth, and the per-session Streamable HTTP transport lifecycle. *How* each
|
|
19
|
+
* session reaches the hub — and what identity it signs as — is delegated
|
|
20
|
+
* entirely to {@link McpSocketHostOptions.createSession}, so the same host serves
|
|
21
|
+
* both the server-provisioned and consumer-provisioned identity modes.
|
|
22
|
+
*/
|
|
23
|
+
var McpSocketHost = class McpSocketHost {
|
|
24
|
+
static async start(options) {
|
|
25
|
+
const host = new McpSocketHost(options);
|
|
26
|
+
await host._listen();
|
|
27
|
+
return host;
|
|
28
|
+
}
|
|
29
|
+
_options;
|
|
30
|
+
_http;
|
|
31
|
+
_token = randomBytes(24).toString("base64url");
|
|
32
|
+
_transport;
|
|
33
|
+
_kind;
|
|
34
|
+
/** Set for the socket/pipe transport; the OS path we listen on. */
|
|
35
|
+
_path;
|
|
36
|
+
/** Loopback host for the TCP (`http`) transport. */
|
|
37
|
+
_host = "127.0.0.1";
|
|
38
|
+
/** Bound port for the TCP (`http`) transport; filled in after `_listen`. */
|
|
39
|
+
_port = 0;
|
|
40
|
+
_transports = /* @__PURE__ */ new Map();
|
|
41
|
+
_active = /* @__PURE__ */ new Map();
|
|
42
|
+
_disposed = false;
|
|
43
|
+
constructor(options) {
|
|
44
|
+
this._options = options;
|
|
45
|
+
this._http = http.createServer((req, res) => void this._handleHttp(req, res));
|
|
46
|
+
this._transport = options.transport ?? "socket";
|
|
47
|
+
if (this._transport === "http") {
|
|
48
|
+
this._kind = "tcp";
|
|
49
|
+
this._path = void 0;
|
|
50
|
+
} else {
|
|
51
|
+
this._kind = process.platform === "win32" ? "namedPipe" : "unixSocket";
|
|
52
|
+
this._path = process.platform === "win32" ? `\\\\.\\pipe\\linkrpc-mcp-${randomUUID()}` : path.join(os.tmpdir(), `linkrpc-mcp-${randomUUID()}.sock`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** The advertised endpoint (socket/pipe path or TCP host:port + bearer token). */
|
|
56
|
+
get endpoint() {
|
|
57
|
+
if (this._kind === "tcp") return {
|
|
58
|
+
kind: "tcp",
|
|
59
|
+
host: this._host,
|
|
60
|
+
port: this._port,
|
|
61
|
+
requestPath: REQUEST_PATH,
|
|
62
|
+
authorizationToken: this._token
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
kind: this._kind,
|
|
66
|
+
path: this._path,
|
|
67
|
+
requestPath: REQUEST_PATH,
|
|
68
|
+
authorizationToken: this._token
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
dispose() {
|
|
72
|
+
if (this._disposed) return;
|
|
73
|
+
this._disposed = true;
|
|
74
|
+
for (const { server, session } of this._active.values()) {
|
|
75
|
+
server.dispose();
|
|
76
|
+
session.dispose?.();
|
|
77
|
+
}
|
|
78
|
+
this._active.clear();
|
|
79
|
+
this._transports.clear();
|
|
80
|
+
this._http.close();
|
|
81
|
+
if (this._kind === "unixSocket" && this._path) fs.rm(this._path, { force: true }, () => {});
|
|
82
|
+
}
|
|
83
|
+
_log(message) {
|
|
84
|
+
this._options.log?.(this._options.label ? `[${this._options.label}] ${message}` : message);
|
|
85
|
+
}
|
|
86
|
+
_listen() {
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
this._http.once("error", reject);
|
|
89
|
+
const onListening = () => {
|
|
90
|
+
this._http.removeListener("error", reject);
|
|
91
|
+
if (this._kind === "tcp") {
|
|
92
|
+
const addr = this._http.address();
|
|
93
|
+
if (!addr || typeof addr === "string") {
|
|
94
|
+
reject(/* @__PURE__ */ new Error("linkrpc-mcp: failed to bind loopback TCP port"));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this._port = addr.port;
|
|
98
|
+
this._log(`listening on tcp ${this._host}:${this._port}`);
|
|
99
|
+
} else this._log(`listening on ${this._kind} ${this._path}`);
|
|
100
|
+
resolve();
|
|
101
|
+
};
|
|
102
|
+
if (this._kind === "tcp") this._http.listen(0, this._host, onListening);
|
|
103
|
+
else this._http.listen(this._path, onListening);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
async _handleHttp(req, res) {
|
|
107
|
+
if (!req.url || !req.url.startsWith(REQUEST_PATH)) {
|
|
108
|
+
res.statusCode = 404;
|
|
109
|
+
res.end();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (req.headers.authorization !== `Bearer ${this._token}`) {
|
|
113
|
+
this._log(`${req.method} ${req.url} → 401 (bad/missing auth)`);
|
|
114
|
+
res.statusCode = 401;
|
|
115
|
+
res.end();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const sessionHeader = req.headers["mcp-session-id"];
|
|
119
|
+
const sessionId = Array.isArray(sessionHeader) ? sessionHeader[0] : sessionHeader;
|
|
120
|
+
let transport = sessionId ? this._transports.get(sessionId) : void 0;
|
|
121
|
+
if (!transport) {
|
|
122
|
+
if (req.method !== "POST") {
|
|
123
|
+
this._log(`${req.method} without known session id → 400 (only POST initialize starts a session)`);
|
|
124
|
+
res.statusCode = 400;
|
|
125
|
+
res.end();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
transport = await this._openSession();
|
|
129
|
+
}
|
|
130
|
+
await transport.handleRequest(req, res);
|
|
131
|
+
}
|
|
132
|
+
async _openSession() {
|
|
133
|
+
const sessionId = randomUUID();
|
|
134
|
+
const session = await this._options.createSession({ sessionId });
|
|
135
|
+
const server = new LinkRpcMcpServer(session.serverOptions);
|
|
136
|
+
const transport = new StreamableHTTPServerTransport({
|
|
137
|
+
sessionIdGenerator: () => sessionId,
|
|
138
|
+
onsessioninitialized: (sid) => {
|
|
139
|
+
this._transports.set(sid, transport);
|
|
140
|
+
this._log(`session initialized sid=${sid}`);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
this._active.set(transport, {
|
|
144
|
+
server,
|
|
145
|
+
session
|
|
146
|
+
});
|
|
147
|
+
transport.onclose = () => {
|
|
148
|
+
if (transport.sessionId) {
|
|
149
|
+
this._transports.delete(transport.sessionId);
|
|
150
|
+
this._log(`session closed sid=${transport.sessionId}`);
|
|
151
|
+
}
|
|
152
|
+
this._active.delete(transport);
|
|
153
|
+
server.dispose();
|
|
154
|
+
session.dispose?.();
|
|
155
|
+
};
|
|
156
|
+
await server.connect(transport);
|
|
157
|
+
return transport;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
//#endregion
|
|
161
|
+
export { McpSocketHost };
|
|
162
|
+
|
|
163
|
+
//# sourceMappingURL=node.js.map
|
package/dist/node.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.js","names":[],"sources":["../src/socketHost.ts"],"sourcesContent":["import * as fs from \"fs\";\nimport * as http from \"http\";\nimport * as os from \"os\";\nimport * as path from \"path\";\nimport { randomBytes, randomUUID } from \"crypto\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { LinkRpcMcpServer, type LinkRpcMcpServerOptions } from \"./server\";\n\n/**\n * Per-session wiring the host asks its owner to provide. Created once per MCP\n * `initialize`, keyed by `sessionId`. The owner decides how that session reaches\n * the hub (e.g. an in-process `defaultConnection`) and what to tear down when the\n * session closes.\n */\nexport interface McpSession {\n /** Options for this session's {@link LinkRpcMcpServer} (e.g. a `defaultConnection`). */\n readonly serverOptions: LinkRpcMcpServerOptions;\n /**\n * Teardown for resources scoped to this session, run after the session's\n * server is disposed. Note: this fires on MCP *session* close — do **not**\n * wipe long-lived identity here if you want it to survive a client reload.\n */\n readonly dispose?: () => void;\n}\n\nexport interface McpSocketHostOptions {\n /** Display name used only for log lines. */\n readonly label?: string;\n /**\n * Mints per-session wiring. Called once per MCP `initialize`, before the\n * client's session id is acknowledged. The `sessionId` is the id the host\n * will report back to the client, so the owner can key identity on it (the\n * consumer-provisioned mode).\n */\n readonly createSession: (ctx: { readonly sessionId: string }) => Promise<McpSession>;\n /** Optional structured logger; defaults to a no-op. */\n readonly log?: (message: string) => void;\n /**\n * Where the host listens:\n * - `\"socket\"` (default): a local Unix domain socket / named pipe. Most\n * private (no TCP port at all), but some MCP clients — notably the Copilot\n * harness — don't support `unix`/`pipe` transports yet.\n * - `\"http\"`: a loopback (`127.0.0.1`) TCP port with a random bearer token.\n * Use this until the socket transport is supported everywhere.\n */\n readonly transport?: \"socket\" | \"http\";\n}\n\n/**\n * Describes where the host is listening, as transport-level facts — not as any\n * particular client's URI encoding. A consumer (e.g. the VS Code extension)\n * translates this into whatever its MCP client expects; for VS Code that means a\n * `unix`/`pipe` URI with the socket path in `uri.path` and {@link requestPath}\n * in `uri.fragment`, but that encoding is the consumer's concern, not ours.\n */\nexport type McpSocketEndpoint =\n | {\n /**\n * `unixSocket` on posix, `namedPipe` on Windows. Discriminates how\n * {@link McpSocketEndpoint.path} is interpreted by the OS / an HTTP\n * client's `socketPath`.\n */\n readonly kind: \"unixSocket\" | \"namedPipe\";\n /**\n * Exact OS path the server listens on and that an HTTP client passes to\n * `socketPath`: a filesystem path for `unixSocket`, a `\\\\.\\pipe\\…` path\n * for `namedPipe`. Note a `namedPipe` path contains backslashes and is\n * therefore not representable as a URL string — keep it as-is rather\n * than round-tripping through `URL`/`Uri.parse`.\n */\n readonly path: string;\n /** HTTP request path the host serves (defaults to `/mcp`). */\n readonly requestPath: string;\n /** Bearer token the client must send in the `Authorization` header. */\n readonly authorizationToken: string;\n }\n | {\n /** A loopback TCP port, reachable over plain `http://`. */\n readonly kind: \"tcp\";\n /** Host the server is bound to (always `127.0.0.1`). */\n readonly host: string;\n /** TCP port the server listens on. */\n readonly port: number;\n /** HTTP request path the host serves (defaults to `/mcp`). */\n readonly requestPath: string;\n /** Bearer token the client must send in the `Authorization` header. */\n readonly authorizationToken: string;\n };\n\nconst REQUEST_PATH = \"/mcp\";\n\ninterface ActiveSession {\n readonly server: LinkRpcMcpServer;\n readonly session: McpSession;\n}\n\n/**\n * Hosts one or more {@link LinkRpcMcpServer} sessions over a local endpoint —\n * by default a Unix domain socket on posix / a named pipe on Windows, or, when\n * `transport: \"http\"` is set, a loopback (`127.0.0.1`) TCP port. The `http`\n * transport exists for clients that don't yet support the `unix`/`pipe`\n * transport (e.g. the Copilot harness).\n *\n * The host owns the generic plumbing: the socket/pipe or TCP port, bearer-token\n * auth, and the per-session Streamable HTTP transport lifecycle. *How* each\n * session reaches the hub — and what identity it signs as — is delegated\n * entirely to {@link McpSocketHostOptions.createSession}, so the same host serves\n * both the server-provisioned and consumer-provisioned identity modes.\n */\nexport class McpSocketHost {\n public static async start(options: McpSocketHostOptions): Promise<McpSocketHost> {\n const host = new McpSocketHost(options);\n await host._listen();\n return host;\n }\n\n private readonly _options: McpSocketHostOptions;\n private readonly _http: http.Server;\n private readonly _token = randomBytes(24).toString(\"base64url\");\n private readonly _transport: \"socket\" | \"http\";\n private readonly _kind: McpSocketEndpoint[\"kind\"];\n /** Set for the socket/pipe transport; the OS path we listen on. */\n private readonly _path: string | undefined;\n /** Loopback host for the TCP (`http`) transport. */\n private readonly _host = \"127.0.0.1\";\n /** Bound port for the TCP (`http`) transport; filled in after `_listen`. */\n private _port = 0;\n private readonly _transports = new Map<string, StreamableHTTPServerTransport>();\n private readonly _active = new Map<StreamableHTTPServerTransport, ActiveSession>();\n private _disposed = false;\n\n private constructor(options: McpSocketHostOptions) {\n this._options = options;\n this._http = http.createServer((req, res) => void this._handleHttp(req, res));\n this._transport = options.transport ?? \"socket\";\n if (this._transport === \"http\") {\n this._kind = \"tcp\";\n this._path = undefined;\n } else {\n this._kind = process.platform === \"win32\" ? \"namedPipe\" : \"unixSocket\";\n this._path =\n process.platform === \"win32\"\n ? `\\\\\\\\.\\\\pipe\\\\linkrpc-mcp-${randomUUID()}`\n : path.join(os.tmpdir(), `linkrpc-mcp-${randomUUID()}.sock`);\n }\n }\n\n /** The advertised endpoint (socket/pipe path or TCP host:port + bearer token). */\n public get endpoint(): McpSocketEndpoint {\n if (this._kind === \"tcp\") {\n return {\n kind: \"tcp\",\n host: this._host,\n port: this._port,\n requestPath: REQUEST_PATH,\n authorizationToken: this._token,\n };\n }\n return {\n kind: this._kind,\n path: this._path!,\n requestPath: REQUEST_PATH,\n authorizationToken: this._token,\n };\n }\n\n public dispose(): void {\n if (this._disposed) {\n return;\n }\n this._disposed = true;\n for (const { server, session } of this._active.values()) {\n server.dispose();\n session.dispose?.();\n }\n this._active.clear();\n this._transports.clear();\n this._http.close();\n // Node unlinks the Unix socket on graceful close; remove it best-effort\n // in case the server never fully started. Named pipes / TCP need no cleanup.\n if (this._kind === \"unixSocket\" && this._path) {\n fs.rm(this._path, { force: true }, () => {});\n }\n }\n\n private _log(message: string): void {\n this._options.log?.(this._options.label ? `[${this._options.label}] ${message}` : message);\n }\n\n private _listen(): Promise<void> {\n return new Promise((resolve, reject) => {\n this._http.once(\"error\", reject);\n const onListening = () => {\n this._http.removeListener(\"error\", reject);\n if (this._kind === \"tcp\") {\n const addr = this._http.address();\n if (!addr || typeof addr === \"string\") {\n reject(new Error(\"linkrpc-mcp: failed to bind loopback TCP port\"));\n return;\n }\n this._port = addr.port;\n this._log(`listening on tcp ${this._host}:${this._port}`);\n } else {\n this._log(`listening on ${this._kind} ${this._path}`);\n }\n resolve();\n };\n if (this._kind === \"tcp\") {\n this._http.listen(0, this._host, onListening);\n } else {\n this._http.listen(this._path!, onListening);\n }\n });\n }\n\n private async _handleHttp(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {\n if (!req.url || !req.url.startsWith(REQUEST_PATH)) {\n res.statusCode = 404;\n res.end();\n return;\n }\n if (req.headers.authorization !== `Bearer ${this._token}`) {\n this._log(`${req.method} ${req.url} → 401 (bad/missing auth)`);\n res.statusCode = 401;\n res.end();\n return;\n }\n\n const sessionHeader = req.headers[\"mcp-session-id\"];\n const sessionId = Array.isArray(sessionHeader) ? sessionHeader[0] : sessionHeader;\n let transport = sessionId ? this._transports.get(sessionId) : undefined;\n\n if (!transport) {\n if (req.method !== \"POST\") {\n this._log(`${req.method} without known session id → 400 (only POST initialize starts a session)`);\n res.statusCode = 400;\n res.end();\n return;\n }\n transport = await this._openSession();\n }\n\n await transport.handleRequest(req, res);\n }\n\n private async _openSession(): Promise<StreamableHTTPServerTransport> {\n // The session id is fixed up front so `createSession` can key identity on\n // it (the consumer-provisioned mode) and the transport reports it back.\n const sessionId = randomUUID();\n const session = await this._options.createSession({ sessionId });\n const server = new LinkRpcMcpServer(session.serverOptions);\n\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => sessionId,\n onsessioninitialized: (sid) => {\n this._transports.set(sid, transport);\n this._log(`session initialized sid=${sid}`);\n },\n });\n this._active.set(transport, { server, session });\n\n transport.onclose = () => {\n if (transport.sessionId) {\n this._transports.delete(transport.sessionId);\n this._log(`session closed sid=${transport.sessionId}`);\n }\n this._active.delete(transport);\n // Dispose the server (tears down its connection pool / in-process leg)\n // before the session's own teardown.\n server.dispose();\n session.dispose?.();\n };\n\n await server.connect(transport);\n return transport;\n }\n}\n"],"mappings":"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"}
|