@nwire/mcp 0.7.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alex Gefter / 200apps Ltd.
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,71 @@
1
+ # @nwire/mcp
2
+
3
+ > Model Context Protocol server — exposes the kernel's CommandRouter as MCP tools over stdio.
4
+
5
+ ## What it does
6
+
7
+ Wraps the Nwire kernel's `CommandRouter` as a Model Context Protocol server. AI clients (Claude Desktop, Cursor, IDE plugins) speak JSON-RPC 2.0 over stdio per the MCP spec; this server routes their `tools/list` and `tools/call` to the same command table the CLI and Studio use. One transport, three surfaces.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pnpm add @nwire/mcp
13
+ ```
14
+
15
+ ## Quick start
16
+
17
+ ```ts
18
+ import { serveMcp } from "@nwire/mcp";
19
+ import { createKernel } from "@nwire/kernel";
20
+
21
+ const kernel = createKernel();
22
+ kernel.router.register("dev", devHandler);
23
+ kernel.router.register("ls", listHandler);
24
+
25
+ await serveMcp({ kernel, serverName: "my-app" });
26
+ // Process now speaks MCP over stdin/stdout; stderr is for logs.
27
+ ```
28
+
29
+ Then point Claude Desktop at the binary in `claude_desktop_config.json`:
30
+
31
+ ```json
32
+ { "mcpServers": { "my-app": { "command": "node", "args": ["./mcp.js"] } } }
33
+ ```
34
+
35
+ ## API surface
36
+
37
+ - `serveMcp({ kernel?, serverName?, serverVersion? })` — boot the server on stdio; resolves when stdin closes.
38
+ - `JsonRpcRequest` / `JsonRpcResponse` / `JsonRpcNotification` — exported for callers writing alternative transports (WebSocket, etc.) that reuse dispatch.
39
+
40
+ ## When to use
41
+
42
+ When you want AI assistants to invoke your Nwire commands directly. Pairs naturally with `@nwire/cli` (same commands) and Studio (same router, browser surface). Fits L4 and up.
43
+
44
+ ## Standalone use
45
+
46
+ For developers using `@nwire/mcp` **without the rest of Nwire** — pair it with any TypeScript project, any container, any HTTP framework.
47
+
48
+ ```ts
49
+ // See the package's main entry (src/) for the standalone surface.
50
+ // The exports below work without @nwire/app or @nwire/forge.
51
+ import {} from /* ...standalone exports... */ "@nwire/mcp";
52
+ ```
53
+
54
+ ## Within nwire-app
55
+
56
+ For developers using this package as part of the Nwire stack — register it via `app.use(...)` or it auto-wires when you compose `createApp({ modules })`.
57
+
58
+ ```ts
59
+ import { createApp } from "@nwire/forge";
60
+
61
+ const app = createApp({
62
+ /* ...config... */
63
+ });
64
+ // Adapter/plugin wiring happens here when applicable.
65
+ ```
66
+
67
+ ## See also
68
+
69
+ - [Architecture sketch §05 — Interfaces tier](../../architecture-sketch.html#packages)
70
+ - [Model Context Protocol spec](https://spec.modelcontextprotocol.io/)
71
+ - Sibling packages: [@nwire/kernel](../nwire-kernel), [@nwire/cli](../nwire-cli), [@nwire/studio](../nwire-studio)
@@ -0,0 +1,67 @@
1
+ /**
2
+ * `@nwire/mcp` — Model Context Protocol server exposing the kernel's
3
+ * `CommandRouter` as MCP tools over stdio.
4
+ *
5
+ * One transport, three surfaces: the CLI calls `kernel.router.run(name, args)`
6
+ * directly; Studio dispatches through `/__nwire/run/exec`; AI clients (Claude
7
+ * Desktop, Cursor, IDE plugins) speak JSON-RPC 2.0 over stdio per the MCP
8
+ * spec and the server routes their `tools/call` to the same command table.
9
+ *
10
+ * The protocol surface this server implements (per
11
+ * https://spec.modelcontextprotocol.io/):
12
+ *
13
+ * initialize — client handshake, returns server capabilities
14
+ * tools/list — returns one tool per registered command
15
+ * tools/call — invokes the named command, streams output as
16
+ * progress notifications, returns the final result
17
+ *
18
+ * Transport: stdio. JSON-RPC framed by newline-delimited messages over
19
+ * stdin/stdout. Stderr is for server logs (never JSON-RPC).
20
+ *
21
+ * NOTE: This is a focused MVP — initialize + tools/list + tools/call.
22
+ * Resources, prompts, sampling, and notifications are sketched in the
23
+ * `serveMcp` switch as TODOs but not yet implemented. The hot path
24
+ * (AI client invokes an nwire command) works end-to-end.
25
+ */
26
+ import { type Kernel } from "@nwire/kernel";
27
+ interface JsonRpcRequest {
28
+ readonly jsonrpc: "2.0";
29
+ readonly id?: string | number | null;
30
+ readonly method: string;
31
+ readonly params?: Record<string, unknown>;
32
+ }
33
+ interface JsonRpcResponse {
34
+ readonly jsonrpc: "2.0";
35
+ readonly id: string | number | null;
36
+ readonly result?: unknown;
37
+ readonly error?: {
38
+ code: number;
39
+ message: string;
40
+ data?: unknown;
41
+ };
42
+ }
43
+ interface JsonRpcNotification {
44
+ readonly jsonrpc: "2.0";
45
+ readonly method: string;
46
+ readonly params?: Record<string, unknown>;
47
+ }
48
+ export interface ServeMcpOptions {
49
+ /** Kernel instance to drive. Defaults to a fresh one. */
50
+ readonly kernel?: Kernel;
51
+ /** Server name reported on `initialize`. Default `"nwire"`. */
52
+ readonly serverName?: string;
53
+ /** Server version reported on `initialize`. Default `"0.1.0"`. */
54
+ readonly serverVersion?: string;
55
+ }
56
+ /**
57
+ * Start the MCP server on stdio. Blocks the process until stdin closes
58
+ * (the client disconnected). Returns the kernel for callers who want
59
+ * to register commands before serving:
60
+ *
61
+ * const k = createKernel();
62
+ * k.router.register("dev", devHandler);
63
+ * await serveMcp({ kernel: k });
64
+ */
65
+ export declare function serveMcp(options?: ServeMcpOptions): Promise<void>;
66
+ export type { JsonRpcRequest, JsonRpcResponse, JsonRpcNotification };
67
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAoC,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAI9E,UAAU,cAAc;IACtB,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;IACxB,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3C;AAED,UAAU,eAAe;IACvB,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CACpE;AAED,UAAU,mBAAmB;IAC3B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3C;AAkBD,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,+DAA+D;IAC/D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;;;GAQG;AACH,wBAAsB,QAAQ,CAAC,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwB3E;AA8JD,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,mBAAmB,EAAE,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,212 @@
1
+ /**
2
+ * `@nwire/mcp` — Model Context Protocol server exposing the kernel's
3
+ * `CommandRouter` as MCP tools over stdio.
4
+ *
5
+ * One transport, three surfaces: the CLI calls `kernel.router.run(name, args)`
6
+ * directly; Studio dispatches through `/__nwire/run/exec`; AI clients (Claude
7
+ * Desktop, Cursor, IDE plugins) speak JSON-RPC 2.0 over stdio per the MCP
8
+ * spec and the server routes their `tools/call` to the same command table.
9
+ *
10
+ * The protocol surface this server implements (per
11
+ * https://spec.modelcontextprotocol.io/):
12
+ *
13
+ * initialize — client handshake, returns server capabilities
14
+ * tools/list — returns one tool per registered command
15
+ * tools/call — invokes the named command, streams output as
16
+ * progress notifications, returns the final result
17
+ *
18
+ * Transport: stdio. JSON-RPC framed by newline-delimited messages over
19
+ * stdin/stdout. Stderr is for server logs (never JSON-RPC).
20
+ *
21
+ * NOTE: This is a focused MVP — initialize + tools/list + tools/call.
22
+ * Resources, prompts, sampling, and notifications are sketched in the
23
+ * `serveMcp` switch as TODOs but not yet implemented. The hot path
24
+ * (AI client invokes an nwire command) works end-to-end.
25
+ */
26
+ import { createKernel } from "@nwire/kernel";
27
+ const ERR_PARSE = -32700;
28
+ const ERR_INVALID_REQ = -32600;
29
+ const ERR_NOT_FOUND = -32601;
30
+ const ERR_INVALID_PARAMS = -32602;
31
+ const ERR_INTERNAL = -32603;
32
+ function send(msg) {
33
+ process.stdout.write(JSON.stringify(msg) + "\n");
34
+ }
35
+ function log(text) {
36
+ process.stderr.write(`[nwire-mcp] ${text}\n`);
37
+ }
38
+ /**
39
+ * Start the MCP server on stdio. Blocks the process until stdin closes
40
+ * (the client disconnected). Returns the kernel for callers who want
41
+ * to register commands before serving:
42
+ *
43
+ * const k = createKernel();
44
+ * k.router.register("dev", devHandler);
45
+ * await serveMcp({ kernel: k });
46
+ */
47
+ export async function serveMcp(options = {}) {
48
+ const kernel = options.kernel ?? createKernel();
49
+ const serverName = options.serverName ?? "nwire";
50
+ const serverVersion = options.serverVersion ?? "0.1.0";
51
+ log(`server starting (${kernel.router.list().length} commands registered)`);
52
+ await readLines(async (line) => {
53
+ if (!line.trim())
54
+ return;
55
+ let request;
56
+ try {
57
+ request = JSON.parse(line);
58
+ }
59
+ catch (err) {
60
+ send({
61
+ jsonrpc: "2.0",
62
+ id: null,
63
+ error: { code: ERR_PARSE, message: err.message },
64
+ });
65
+ return;
66
+ }
67
+ await handle(request, { kernel, serverName, serverVersion });
68
+ });
69
+ log("stdin closed — server exiting");
70
+ }
71
+ async function handle(req, ctx) {
72
+ const id = req.id ?? null;
73
+ try {
74
+ switch (req.method) {
75
+ case "initialize":
76
+ send({
77
+ jsonrpc: "2.0",
78
+ id,
79
+ result: {
80
+ protocolVersion: "2024-11-05",
81
+ serverInfo: { name: ctx.serverName, version: ctx.serverVersion },
82
+ capabilities: { tools: { listChanged: false } },
83
+ },
84
+ });
85
+ return;
86
+ case "tools/list":
87
+ send({
88
+ jsonrpc: "2.0",
89
+ id,
90
+ result: {
91
+ tools: ctx.kernel.router.list().map((name) => ({
92
+ name,
93
+ description: `Nwire command "${name}"`,
94
+ inputSchema: { type: "object", additionalProperties: true },
95
+ })),
96
+ },
97
+ });
98
+ return;
99
+ case "tools/call":
100
+ await callTool(req, id, ctx);
101
+ return;
102
+ default:
103
+ send({
104
+ jsonrpc: "2.0",
105
+ id,
106
+ error: { code: ERR_NOT_FOUND, message: `unknown method "${req.method}"` },
107
+ });
108
+ }
109
+ }
110
+ catch (err) {
111
+ send({
112
+ jsonrpc: "2.0",
113
+ id,
114
+ error: { code: ERR_INTERNAL, message: err.message },
115
+ });
116
+ }
117
+ }
118
+ async function callTool(req, id, ctx) {
119
+ const name = req.params?.name;
120
+ const args = (req.params?.arguments ?? {});
121
+ if (!name) {
122
+ send({
123
+ jsonrpc: "2.0",
124
+ id,
125
+ error: { code: ERR_INVALID_PARAMS, message: "tools/call requires `name`" },
126
+ });
127
+ return;
128
+ }
129
+ if (!ctx.kernel.router.has(name)) {
130
+ send({
131
+ jsonrpc: "2.0",
132
+ id,
133
+ error: { code: ERR_INVALID_PARAMS, message: `unknown tool "${name}"` },
134
+ });
135
+ return;
136
+ }
137
+ // Forward command lifecycle events as MCP progress notifications.
138
+ const handle = ctx.kernel.run(name, args);
139
+ const unsubscribe = handle.on((event) => {
140
+ if (event.kind === "command.log") {
141
+ send({
142
+ jsonrpc: "2.0",
143
+ method: "notifications/progress",
144
+ params: {
145
+ progressToken: handle.commandId,
146
+ message: event.line,
147
+ stream: event.stream,
148
+ },
149
+ });
150
+ }
151
+ else if (event.kind === "command.progress") {
152
+ send({
153
+ jsonrpc: "2.0",
154
+ method: "notifications/progress",
155
+ params: {
156
+ progressToken: handle.commandId,
157
+ message: event.message,
158
+ pct: event.pct,
159
+ },
160
+ });
161
+ }
162
+ });
163
+ try {
164
+ const result = await handle.promise;
165
+ send({
166
+ jsonrpc: "2.0",
167
+ id,
168
+ result: {
169
+ content: [{ type: "text", text: JSON.stringify(result) }],
170
+ isError: false,
171
+ },
172
+ });
173
+ }
174
+ catch (err) {
175
+ send({
176
+ jsonrpc: "2.0",
177
+ id,
178
+ result: {
179
+ content: [{ type: "text", text: err.message }],
180
+ isError: true,
181
+ },
182
+ });
183
+ }
184
+ finally {
185
+ unsubscribe();
186
+ }
187
+ }
188
+ /**
189
+ * Read newline-delimited messages from stdin. Resolves when stdin
190
+ * closes. Tolerates partial chunks (buffers + splits on \n).
191
+ */
192
+ function readLines(onLine) {
193
+ return new Promise((resolve) => {
194
+ let buffer = "";
195
+ process.stdin.on("data", async (chunk) => {
196
+ buffer += chunk.toString("utf8");
197
+ const lines = buffer.split("\n");
198
+ buffer = lines.pop() ?? "";
199
+ for (const line of lines) {
200
+ try {
201
+ await onLine(line);
202
+ }
203
+ catch (err) {
204
+ log(`onLine threw: ${err.message}`);
205
+ }
206
+ }
207
+ });
208
+ process.stdin.on("end", () => resolve());
209
+ process.stdin.on("close", () => resolve());
210
+ });
211
+ }
212
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@nwire/mcp",
3
+ "version": "0.7.0",
4
+ "private": false,
5
+ "description": "Nwire MCP (Model Context Protocol) server — exposes the kernel's CommandRouter as MCP tools over stdio. AI clients (Claude, Cursor, …) drive nwire commands the same way the CLI and Studio do.",
6
+ "files": [
7
+ "dist",
8
+ "src"
9
+ ],
10
+ "type": "module",
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./dist/index.js",
16
+ "types": "./dist/index.d.ts"
17
+ }
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "dependencies": {
23
+ "@nwire/kernel": "0.7.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^22.19.9",
27
+ "typescript": "^5.9.3"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc && node ../../scripts/fix-dist-extensions.mjs dist",
31
+ "typecheck": "tsc --noEmit"
32
+ }
33
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * MCP server smoke tests — exercise the dispatch logic without going
3
+ * through stdio. We import the internal handle/callTool surface by
4
+ * proxying through a fake JSON-RPC client: write a request to the
5
+ * server's input, capture stdout.
6
+ *
7
+ * For the MVP we test the dispatch shape via the CommandRouter directly
8
+ * — the JSON-RPC framing is straightforward enough that a full stdio
9
+ * dance isn't worth the harness complexity right now.
10
+ */
11
+
12
+ import { describe, it, expect } from "vitest";
13
+ import { createKernel } from "@nwire/kernel";
14
+
15
+ describe("@nwire/mcp", () => {
16
+ it("kernel.router exposes registered commands by name", async () => {
17
+ const k = createKernel();
18
+ k.router.register("greet", async () => ({ hello: "world" }));
19
+ expect(k.router.list()).toEqual(["greet"]);
20
+ expect(k.router.has("greet")).toBe(true);
21
+ });
22
+
23
+ it("kernel.run dispatches and returns the handler's result", async () => {
24
+ const k = createKernel();
25
+ k.router.register("double", async (_ctx, args: { n: number }) => args.n * 2);
26
+ const handle = k.run<{ n: number }, number>("double", { n: 21 });
27
+ await expect(handle.promise).resolves.toBe(42);
28
+ });
29
+
30
+ it("command.log events stream through the handle's `on` subscription", async () => {
31
+ const k = createKernel();
32
+ k.router.register("noisy", async (ctx) => {
33
+ ctx.log("line 1");
34
+ ctx.log("line 2", "stderr");
35
+ return "done";
36
+ });
37
+ const handle = k.run("noisy", {});
38
+ const logs: Array<{ stream: string; line: string }> = [];
39
+ handle.on((e) => {
40
+ if (e.kind === "command.log") logs.push({ stream: e.stream, line: e.line });
41
+ });
42
+ await handle.promise;
43
+ expect(logs).toEqual([
44
+ { stream: "stdout", line: "line 1" },
45
+ { stream: "stderr", line: "line 2" },
46
+ ]);
47
+ });
48
+ });
package/src/index.ts ADDED
@@ -0,0 +1,267 @@
1
+ /**
2
+ * `@nwire/mcp` — Model Context Protocol server exposing the kernel's
3
+ * `CommandRouter` as MCP tools over stdio.
4
+ *
5
+ * One transport, three surfaces: the CLI calls `kernel.router.run(name, args)`
6
+ * directly; Studio dispatches through `/__nwire/run/exec`; AI clients (Claude
7
+ * Desktop, Cursor, IDE plugins) speak JSON-RPC 2.0 over stdio per the MCP
8
+ * spec and the server routes their `tools/call` to the same command table.
9
+ *
10
+ * The protocol surface this server implements (per
11
+ * https://spec.modelcontextprotocol.io/):
12
+ *
13
+ * initialize — client handshake, returns server capabilities
14
+ * tools/list — returns one tool per registered command
15
+ * tools/call — invokes the named command, streams output as
16
+ * progress notifications, returns the final result
17
+ *
18
+ * Transport: stdio. JSON-RPC framed by newline-delimited messages over
19
+ * stdin/stdout. Stderr is for server logs (never JSON-RPC).
20
+ *
21
+ * NOTE: This is a focused MVP — initialize + tools/list + tools/call.
22
+ * Resources, prompts, sampling, and notifications are sketched in the
23
+ * `serveMcp` switch as TODOs but not yet implemented. The hot path
24
+ * (AI client invokes an nwire command) works end-to-end.
25
+ */
26
+
27
+ import { createKernel, type CommandHandle, type Kernel } from "@nwire/kernel";
28
+
29
+ // ─── JSON-RPC framing over stdio ───────────────────────────────────
30
+
31
+ interface JsonRpcRequest {
32
+ readonly jsonrpc: "2.0";
33
+ readonly id?: string | number | null;
34
+ readonly method: string;
35
+ readonly params?: Record<string, unknown>;
36
+ }
37
+
38
+ interface JsonRpcResponse {
39
+ readonly jsonrpc: "2.0";
40
+ readonly id: string | number | null;
41
+ readonly result?: unknown;
42
+ readonly error?: { code: number; message: string; data?: unknown };
43
+ }
44
+
45
+ interface JsonRpcNotification {
46
+ readonly jsonrpc: "2.0";
47
+ readonly method: string;
48
+ readonly params?: Record<string, unknown>;
49
+ }
50
+
51
+ const ERR_PARSE = -32700;
52
+ const ERR_INVALID_REQ = -32600;
53
+ const ERR_NOT_FOUND = -32601;
54
+ const ERR_INVALID_PARAMS = -32602;
55
+ const ERR_INTERNAL = -32603;
56
+
57
+ function send(msg: JsonRpcResponse | JsonRpcNotification): void {
58
+ process.stdout.write(JSON.stringify(msg) + "\n");
59
+ }
60
+
61
+ function log(text: string): void {
62
+ process.stderr.write(`[nwire-mcp] ${text}\n`);
63
+ }
64
+
65
+ // ─── Server ────────────────────────────────────────────────────────
66
+
67
+ export interface ServeMcpOptions {
68
+ /** Kernel instance to drive. Defaults to a fresh one. */
69
+ readonly kernel?: Kernel;
70
+ /** Server name reported on `initialize`. Default `"nwire"`. */
71
+ readonly serverName?: string;
72
+ /** Server version reported on `initialize`. Default `"0.1.0"`. */
73
+ readonly serverVersion?: string;
74
+ }
75
+
76
+ /**
77
+ * Start the MCP server on stdio. Blocks the process until stdin closes
78
+ * (the client disconnected). Returns the kernel for callers who want
79
+ * to register commands before serving:
80
+ *
81
+ * const k = createKernel();
82
+ * k.router.register("dev", devHandler);
83
+ * await serveMcp({ kernel: k });
84
+ */
85
+ export async function serveMcp(options: ServeMcpOptions = {}): Promise<void> {
86
+ const kernel = options.kernel ?? createKernel();
87
+ const serverName = options.serverName ?? "nwire";
88
+ const serverVersion = options.serverVersion ?? "0.1.0";
89
+
90
+ log(`server starting (${kernel.router.list().length} commands registered)`);
91
+
92
+ await readLines(async (line) => {
93
+ if (!line.trim()) return;
94
+ let request: JsonRpcRequest;
95
+ try {
96
+ request = JSON.parse(line) as JsonRpcRequest;
97
+ } catch (err) {
98
+ send({
99
+ jsonrpc: "2.0",
100
+ id: null,
101
+ error: { code: ERR_PARSE, message: (err as Error).message },
102
+ });
103
+ return;
104
+ }
105
+ await handle(request, { kernel, serverName, serverVersion });
106
+ });
107
+
108
+ log("stdin closed — server exiting");
109
+ }
110
+
111
+ interface HandleContext {
112
+ readonly kernel: Kernel;
113
+ readonly serverName: string;
114
+ readonly serverVersion: string;
115
+ }
116
+
117
+ async function handle(req: JsonRpcRequest, ctx: HandleContext): Promise<void> {
118
+ const id = req.id ?? null;
119
+ try {
120
+ switch (req.method) {
121
+ case "initialize":
122
+ send({
123
+ jsonrpc: "2.0",
124
+ id,
125
+ result: {
126
+ protocolVersion: "2024-11-05",
127
+ serverInfo: { name: ctx.serverName, version: ctx.serverVersion },
128
+ capabilities: { tools: { listChanged: false } },
129
+ },
130
+ });
131
+ return;
132
+
133
+ case "tools/list":
134
+ send({
135
+ jsonrpc: "2.0",
136
+ id,
137
+ result: {
138
+ tools: ctx.kernel.router.list().map((name) => ({
139
+ name,
140
+ description: `Nwire command "${name}"`,
141
+ inputSchema: { type: "object", additionalProperties: true },
142
+ })),
143
+ },
144
+ });
145
+ return;
146
+
147
+ case "tools/call":
148
+ await callTool(req, id, ctx);
149
+ return;
150
+
151
+ default:
152
+ send({
153
+ jsonrpc: "2.0",
154
+ id,
155
+ error: { code: ERR_NOT_FOUND, message: `unknown method "${req.method}"` },
156
+ });
157
+ }
158
+ } catch (err) {
159
+ send({
160
+ jsonrpc: "2.0",
161
+ id,
162
+ error: { code: ERR_INTERNAL, message: (err as Error).message },
163
+ });
164
+ }
165
+ }
166
+
167
+ async function callTool(
168
+ req: JsonRpcRequest,
169
+ id: string | number | null,
170
+ ctx: HandleContext,
171
+ ): Promise<void> {
172
+ const name = req.params?.name as string | undefined;
173
+ const args = (req.params?.arguments ?? {}) as Record<string, unknown>;
174
+ if (!name) {
175
+ send({
176
+ jsonrpc: "2.0",
177
+ id,
178
+ error: { code: ERR_INVALID_PARAMS, message: "tools/call requires `name`" },
179
+ });
180
+ return;
181
+ }
182
+ if (!ctx.kernel.router.has(name)) {
183
+ send({
184
+ jsonrpc: "2.0",
185
+ id,
186
+ error: { code: ERR_INVALID_PARAMS, message: `unknown tool "${name}"` },
187
+ });
188
+ return;
189
+ }
190
+
191
+ // Forward command lifecycle events as MCP progress notifications.
192
+ const handle: CommandHandle = ctx.kernel.run(name, args);
193
+ const unsubscribe = handle.on((event) => {
194
+ if (event.kind === "command.log") {
195
+ send({
196
+ jsonrpc: "2.0",
197
+ method: "notifications/progress",
198
+ params: {
199
+ progressToken: handle.commandId,
200
+ message: event.line,
201
+ stream: event.stream,
202
+ },
203
+ });
204
+ } else if (event.kind === "command.progress") {
205
+ send({
206
+ jsonrpc: "2.0",
207
+ method: "notifications/progress",
208
+ params: {
209
+ progressToken: handle.commandId,
210
+ message: event.message,
211
+ pct: event.pct,
212
+ },
213
+ });
214
+ }
215
+ });
216
+
217
+ try {
218
+ const result = await handle.promise;
219
+ send({
220
+ jsonrpc: "2.0",
221
+ id,
222
+ result: {
223
+ content: [{ type: "text", text: JSON.stringify(result) }],
224
+ isError: false,
225
+ },
226
+ });
227
+ } catch (err) {
228
+ send({
229
+ jsonrpc: "2.0",
230
+ id,
231
+ result: {
232
+ content: [{ type: "text", text: (err as Error).message }],
233
+ isError: true,
234
+ },
235
+ });
236
+ } finally {
237
+ unsubscribe();
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Read newline-delimited messages from stdin. Resolves when stdin
243
+ * closes. Tolerates partial chunks (buffers + splits on \n).
244
+ */
245
+ function readLines(onLine: (line: string) => Promise<void>): Promise<void> {
246
+ return new Promise((resolve) => {
247
+ let buffer = "";
248
+ process.stdin.on("data", async (chunk: Buffer) => {
249
+ buffer += chunk.toString("utf8");
250
+ const lines = buffer.split("\n");
251
+ buffer = lines.pop() ?? "";
252
+ for (const line of lines) {
253
+ try {
254
+ await onLine(line);
255
+ } catch (err) {
256
+ log(`onLine threw: ${(err as Error).message}`);
257
+ }
258
+ }
259
+ });
260
+ process.stdin.on("end", () => resolve());
261
+ process.stdin.on("close", () => resolve());
262
+ });
263
+ }
264
+
265
+ // Also export the JSON-RPC types so consumers can write their own
266
+ // transport adapters (e.g. WebSocket) reusing the dispatch logic.
267
+ export type { JsonRpcRequest, JsonRpcResponse, JsonRpcNotification };