@nwire/mcp 0.9.2 → 0.10.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 +52 -61
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -2
- package/dist/inspect.d.ts +0 -1
- package/dist/inspect.js +0 -1
- package/dist/internal/command-router.d.ts +39 -0
- package/dist/internal/command-router.js +112 -0
- package/dist/internal/event-bus.d.ts +72 -0
- package/dist/internal/event-bus.js +34 -0
- package/dist/internal/index.d.ts +10 -0
- package/dist/internal/index.js +10 -0
- package/dist/internal/kernel.d.ts +24 -0
- package/dist/internal/kernel.js +53 -0
- package/dist/mcp-adapter.d.ts +35 -0
- package/dist/mcp-adapter.js +89 -0
- package/dist/write-tools.d.ts +1 -2
- package/dist/write-tools.js +1 -2
- package/package.json +7 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/inspect.d.ts.map +0 -1
- package/dist/inspect.js.map +0 -1
- package/dist/write-tools.d.ts.map +0 -1
- package/dist/write-tools.js.map +0 -1
- package/src/__tests__/inspect-tools.test.ts +0 -341
- package/src/__tests__/mcp-io.test.ts +0 -182
- package/src/__tests__/mcp.test.ts +0 -48
- package/src/__tests__/write-tools.test.ts +0 -356
- package/src/index.ts +0 -348
- package/src/inspect.ts +0 -329
- package/src/write-tools.ts +0 -247
package/README.md
CHANGED
|
@@ -1,36 +1,65 @@
|
|
|
1
1
|
# @nwire/mcp
|
|
2
2
|
|
|
3
|
-
> Model Context Protocol
|
|
3
|
+
> Model Context Protocol — Nwire wires as MCP tools.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
```bash
|
|
6
|
+
pnpm add @nwire/mcp @nwire/app @nwire/endpoint @nwire/wires
|
|
7
|
+
```
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
2. **Inspect tools** — read-only introspection over `/__nwire/*` (falls back to `.nwire/` cache).
|
|
11
|
-
3. **Write tools** — mutating actions that POST to `/__nwire/*` on a live wire.
|
|
9
|
+
## As an adopter
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
The 0.10 adopter consumes wires whose `binding.$adapter === "mcp"` and
|
|
12
|
+
exposes them as MCP tools — same wire, same handler, served alongside
|
|
13
|
+
HTTP and queue under one endpoint:
|
|
14
14
|
|
|
15
|
-
```
|
|
16
|
-
|
|
15
|
+
```ts
|
|
16
|
+
import { createApp } from "@nwire/app";
|
|
17
|
+
import { endpoint } from "@nwire/endpoint";
|
|
18
|
+
import { tool } from "@nwire/wires/mcp";
|
|
19
|
+
import { httpKoa } from "@nwire/koa";
|
|
20
|
+
import { mcpAdapter } from "@nwire/mcp";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
|
|
23
|
+
const app = createApp({ appName: "tasks" });
|
|
24
|
+
|
|
25
|
+
app.wire(
|
|
26
|
+
tool("create-task", {
|
|
27
|
+
description: "Create a task",
|
|
28
|
+
input: z.object({ title: z.string() }),
|
|
29
|
+
}),
|
|
30
|
+
async (input) => ({ id: createId(), title: input.title }),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const mcp = mcpAdapter();
|
|
34
|
+
await endpoint("tasks", { port: 3000 })
|
|
35
|
+
.use(httpKoa({ prefix: "/api" }))
|
|
36
|
+
.use(mcp)
|
|
37
|
+
.mount(app)
|
|
38
|
+
.run();
|
|
39
|
+
|
|
40
|
+
// In-process introspection — no stdio server required:
|
|
41
|
+
mcp.list(); // [{ name, description, inputSchema }]
|
|
42
|
+
await mcp.call("create-task", { title: "x" });
|
|
17
43
|
```
|
|
18
44
|
|
|
19
|
-
##
|
|
45
|
+
## Stdio server (legacy serveMcp)
|
|
46
|
+
|
|
47
|
+
The original `serveMcp()` entry — JSON-RPC 2.0 over stdin/stdout, suitable
|
|
48
|
+
for direct Claude Desktop / Cursor / IDE plugin integration — stays
|
|
49
|
+
available:
|
|
20
50
|
|
|
21
51
|
```ts
|
|
22
52
|
import { serveMcp } from "@nwire/mcp";
|
|
23
|
-
import { createKernel } from "
|
|
53
|
+
import { createKernel } from "./kernel";
|
|
24
54
|
|
|
25
55
|
const kernel = createKernel();
|
|
26
56
|
kernel.router.register("dev", devHandler);
|
|
27
|
-
kernel.router.register("ls", listHandler);
|
|
28
57
|
|
|
29
58
|
await serveMcp({ kernel, serverName: "my-app" });
|
|
30
|
-
// Process
|
|
59
|
+
// Process speaks MCP over stdin/stdout; stderr is for logs.
|
|
31
60
|
```
|
|
32
61
|
|
|
33
|
-
|
|
62
|
+
Claude Desktop config:
|
|
34
63
|
|
|
35
64
|
```json
|
|
36
65
|
{ "mcpServers": { "my-app": { "command": "node", "args": ["./mcp.js"] } } }
|
|
@@ -38,52 +67,14 @@ Then point Claude Desktop at the binary in `claude_desktop_config.json`:
|
|
|
38
67
|
|
|
39
68
|
## Surface
|
|
40
69
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
### Built-in tools
|
|
47
|
-
|
|
48
|
-
Beyond the kernel's CommandRouter, the server exposes these tools:
|
|
49
|
-
|
|
50
|
-
**Inspect (read-only)** — proxy `/__nwire/*` on a running wire, fall back to `.nwire/*.json` on disk:
|
|
51
|
-
|
|
52
|
-
| Tool | Reads |
|
|
53
|
-
| ------------------ | -------------------------------- |
|
|
54
|
-
| `manifest` | `.nwire/manifest.json` |
|
|
55
|
-
| `list_actions` | `.nwire/actions.json` |
|
|
56
|
-
| `list_events` | `.nwire/events.json` |
|
|
57
|
-
| `list_hooks` | `.nwire/hooks.json` |
|
|
58
|
-
| `list_plugins` | `.nwire/plugins.json` |
|
|
59
|
-
| `recent_events` | Live `/__nwire/events/recent` |
|
|
60
|
-
| `recent_telemetry` | Live `/__nwire/telemetry/recent` |
|
|
61
|
-
|
|
62
|
-
**Write (mutating)** — POST to a live wire:
|
|
63
|
-
|
|
64
|
-
| Tool | Posts to |
|
|
65
|
-
| ----------------- | -------------------------------------------------------- |
|
|
66
|
-
| `dispatch_action` | `/__nwire/dispatch` — fire any registered action. |
|
|
67
|
-
| `run_command` | `/__nwire/commands/run` — invoke a `defineCommand`. |
|
|
68
|
-
| `replay_trace` | `/__nwire/replay` — replay a recording against the wire. |
|
|
69
|
-
|
|
70
|
-
Write tools gracefully gap-flag when the target endpoint isn't mounted
|
|
71
|
-
(e.g., MCP running standalone with no wire up).
|
|
72
|
-
|
|
73
|
-
### Progress notifications
|
|
74
|
-
|
|
75
|
-
For kernel router commands, the server forwards `command.log` and
|
|
76
|
-
`command.progress` events as MCP `notifications/progress` so the AI
|
|
77
|
-
client streams output as the command runs. Inspect + write tools are
|
|
78
|
-
synchronous and don't notify.
|
|
70
|
+
- `mcpAdapter(config?)` — 0.10 adopter
|
|
71
|
+
- `serveMcp(options)` — stdio JSON-RPC server (legacy)
|
|
72
|
+
- `inspectTools`, `findInspectTool` — read-only introspection tools
|
|
73
|
+
- `writeTools`, `findWriteTool` — mutating tools that drive `/__nwire/*`
|
|
79
74
|
|
|
80
75
|
## Related
|
|
81
76
|
|
|
82
|
-
- `@nwire/
|
|
83
|
-
- `@nwire/
|
|
84
|
-
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
## Status
|
|
88
|
-
|
|
89
|
-
v0.x — `initialize`, `tools/list`, `tools/call` work end-to-end. Resources, prompts, and sampling are sketched but not implemented yet.
|
|
77
|
+
- [`@nwire/wires`](../core-wires) — `tool()` binding helper
|
|
78
|
+
- [`@nwire/endpoint`](../core-endpoint) — adopter lifecycle host
|
|
79
|
+
- [`examples/multi-transport`](../../examples/multi-transport) — MCP alongside
|
|
80
|
+
HTTP + queue
|
package/dist/index.d.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* `serveMcp` switch as TODOs but not yet implemented. The hot path
|
|
24
24
|
* (AI client invokes an nwire command) works end-to-end.
|
|
25
25
|
*/
|
|
26
|
-
import { type Kernel } from "
|
|
26
|
+
import { type Kernel } from "./internal/index.js";
|
|
27
27
|
interface JsonRpcRequest {
|
|
28
28
|
readonly jsonrpc: "2.0";
|
|
29
29
|
readonly id?: string | number | null;
|
|
@@ -79,4 +79,4 @@ export type { JsonRpcRequest, JsonRpcResponse, JsonRpcNotification };
|
|
|
79
79
|
export { inspectTools, findInspectTool, resetInspectDiscoveryCache } from "./inspect.js";
|
|
80
80
|
export type { InspectToolDef } from "./inspect.js";
|
|
81
81
|
export { writeTools, findWriteTool } from "./write-tools.js";
|
|
82
|
-
|
|
82
|
+
export { mcpAdapter, type McpAdapter, type McpAdapterConfig, type McpToolDescriptor } from "./mcp-adapter.js";
|
package/dist/index.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* `serveMcp` switch as TODOs but not yet implemented. The hot path
|
|
24
24
|
* (AI client invokes an nwire command) works end-to-end.
|
|
25
25
|
*/
|
|
26
|
-
import { createKernel } from "
|
|
26
|
+
import { createKernel } from "./internal/index.js";
|
|
27
27
|
import { inspectTools, findInspectTool } from "./inspect.js";
|
|
28
28
|
import { writeTools, findWriteTool } from "./write-tools.js";
|
|
29
29
|
const ERR_PARSE = -32700;
|
|
@@ -272,4 +272,5 @@ function readLines(stream, onLine) {
|
|
|
272
272
|
}
|
|
273
273
|
export { inspectTools, findInspectTool, resetInspectDiscoveryCache } from "./inspect.js";
|
|
274
274
|
export { writeTools, findWriteTool } from "./write-tools.js";
|
|
275
|
-
|
|
275
|
+
// 0.10 adopter shape — consumed by endpoint().use(mcpAdapter()).mount(app).
|
|
276
|
+
export { mcpAdapter } from "./mcp-adapter.js";
|
package/dist/inspect.d.ts
CHANGED
package/dist/inspect.js
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommandRouter — one dispatch table for every named kernel command.
|
|
3
|
+
*
|
|
4
|
+
* The CLI registers handlers ("dev", "test", "build", ...). The CLI, Studio,
|
|
5
|
+
* and MCP all *invoke* them via `kernel.run(name, args)`. Every handler
|
|
6
|
+
* receives a CommandContext with helpers that publish through the same
|
|
7
|
+
* EventBus, so the live UI feels identical regardless of which surface
|
|
8
|
+
* fired the command.
|
|
9
|
+
*/
|
|
10
|
+
import type { EventBus, KernelEvent } from "./event-bus.js";
|
|
11
|
+
export interface CommandContext {
|
|
12
|
+
readonly commandId: string;
|
|
13
|
+
readonly bus: EventBus;
|
|
14
|
+
readonly signal: AbortSignal;
|
|
15
|
+
log(line: string, stream?: "stdout" | "stderr"): void;
|
|
16
|
+
progress(message: string, pct?: number): void;
|
|
17
|
+
}
|
|
18
|
+
export type CommandHandler<TArgs = unknown, TResult = unknown> = (ctx: CommandContext, args: TArgs) => Promise<TResult>;
|
|
19
|
+
export interface CommandHandle<TResult = unknown> {
|
|
20
|
+
readonly commandId: string;
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly promise: Promise<TResult>;
|
|
23
|
+
abort(reason?: string): void;
|
|
24
|
+
on(handler: (event: KernelEvent) => void): () => void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Holds the named-handler table + creates handles when commands run.
|
|
28
|
+
* Pure routing — execution is the handler's job; this class only wires up
|
|
29
|
+
* the context, lifecycle events, and abort plumbing.
|
|
30
|
+
*/
|
|
31
|
+
export declare class CommandRouter {
|
|
32
|
+
private readonly bus;
|
|
33
|
+
private readonly handlers;
|
|
34
|
+
constructor(bus: EventBus);
|
|
35
|
+
register<TArgs, TResult>(name: string, handler: CommandHandler<TArgs, TResult>): void;
|
|
36
|
+
has(name: string): boolean;
|
|
37
|
+
list(): string[];
|
|
38
|
+
run<TArgs = unknown, TResult = unknown>(name: string, args: TArgs): CommandHandle<TResult>;
|
|
39
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommandRouter — one dispatch table for every named kernel command.
|
|
3
|
+
*
|
|
4
|
+
* The CLI registers handlers ("dev", "test", "build", ...). The CLI, Studio,
|
|
5
|
+
* and MCP all *invoke* them via `kernel.run(name, args)`. Every handler
|
|
6
|
+
* receives a CommandContext with helpers that publish through the same
|
|
7
|
+
* EventBus, so the live UI feels identical regardless of which surface
|
|
8
|
+
* fired the command.
|
|
9
|
+
*/
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
/**
|
|
12
|
+
* Holds the named-handler table + creates handles when commands run.
|
|
13
|
+
* Pure routing — execution is the handler's job; this class only wires up
|
|
14
|
+
* the context, lifecycle events, and abort plumbing.
|
|
15
|
+
*/
|
|
16
|
+
export class CommandRouter {
|
|
17
|
+
bus;
|
|
18
|
+
handlers = new Map();
|
|
19
|
+
constructor(bus) {
|
|
20
|
+
this.bus = bus;
|
|
21
|
+
}
|
|
22
|
+
register(name, handler) {
|
|
23
|
+
if (this.handlers.has(name)) {
|
|
24
|
+
throw new Error(`command "${name}" is already registered`);
|
|
25
|
+
}
|
|
26
|
+
this.handlers.set(name, handler);
|
|
27
|
+
}
|
|
28
|
+
has(name) {
|
|
29
|
+
return this.handlers.has(name);
|
|
30
|
+
}
|
|
31
|
+
list() {
|
|
32
|
+
return [...this.handlers.keys()].sort();
|
|
33
|
+
}
|
|
34
|
+
run(name, args) {
|
|
35
|
+
const handler = this.handlers.get(name);
|
|
36
|
+
if (!handler) {
|
|
37
|
+
throw new Error(`unknown command "${name}"`);
|
|
38
|
+
}
|
|
39
|
+
const commandId = randomUUID();
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const startedAt = Date.now();
|
|
42
|
+
const ctx = {
|
|
43
|
+
commandId,
|
|
44
|
+
bus: this.bus,
|
|
45
|
+
signal: controller.signal,
|
|
46
|
+
log: (line, stream = "stdout") => {
|
|
47
|
+
this.bus.emit({
|
|
48
|
+
kind: "command.log",
|
|
49
|
+
commandId,
|
|
50
|
+
stream,
|
|
51
|
+
line,
|
|
52
|
+
at: new Date().toISOString(),
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
progress: (message, pct) => {
|
|
56
|
+
this.bus.emit({
|
|
57
|
+
kind: "command.progress",
|
|
58
|
+
commandId,
|
|
59
|
+
message,
|
|
60
|
+
pct,
|
|
61
|
+
at: new Date().toISOString(),
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
this.bus.emit({
|
|
66
|
+
kind: "command.started",
|
|
67
|
+
commandId,
|
|
68
|
+
name,
|
|
69
|
+
args,
|
|
70
|
+
at: new Date(startedAt).toISOString(),
|
|
71
|
+
});
|
|
72
|
+
// Defer the handler so subscribers can attach via `handle.on(...)`
|
|
73
|
+
// before any `ctx.log` / `ctx.progress` fires. Without this, every
|
|
74
|
+
// sync log call inside the handler races the caller's subscription.
|
|
75
|
+
const promise = Promise.resolve()
|
|
76
|
+
.then(() => handler(ctx, args))
|
|
77
|
+
.then((result) => {
|
|
78
|
+
this.bus.emit({
|
|
79
|
+
kind: "command.done",
|
|
80
|
+
commandId,
|
|
81
|
+
result,
|
|
82
|
+
durationMs: Date.now() - startedAt,
|
|
83
|
+
at: new Date().toISOString(),
|
|
84
|
+
});
|
|
85
|
+
return result;
|
|
86
|
+
})
|
|
87
|
+
.catch((err) => {
|
|
88
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
89
|
+
this.bus.emit({
|
|
90
|
+
kind: "command.error",
|
|
91
|
+
commandId,
|
|
92
|
+
message,
|
|
93
|
+
durationMs: Date.now() - startedAt,
|
|
94
|
+
at: new Date().toISOString(),
|
|
95
|
+
});
|
|
96
|
+
throw err;
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
commandId,
|
|
100
|
+
name,
|
|
101
|
+
promise,
|
|
102
|
+
abort: (reason) => controller.abort(reason),
|
|
103
|
+
on: (handler) => {
|
|
104
|
+
return this.bus.onAny((event) => {
|
|
105
|
+
if ("commandId" in event && event.commandId === commandId) {
|
|
106
|
+
handler(event);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventBus — typed pub/sub for kernel-level events.
|
|
3
|
+
*
|
|
4
|
+
* Every command and every supervised process emits events here. The CLI
|
|
5
|
+
* renders them in ink, Studio mirrors them over SSE, MCP forwards them to
|
|
6
|
+
* the model. One emitter, many subscribers — that's the whole point.
|
|
7
|
+
*/
|
|
8
|
+
import type { LogLine } from "@nwire/supervisor";
|
|
9
|
+
export type KernelEvent = {
|
|
10
|
+
kind: "command.started";
|
|
11
|
+
commandId: string;
|
|
12
|
+
name: string;
|
|
13
|
+
args: unknown;
|
|
14
|
+
at: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: "command.log";
|
|
17
|
+
commandId: string;
|
|
18
|
+
stream: "stdout" | "stderr";
|
|
19
|
+
line: string;
|
|
20
|
+
at: string;
|
|
21
|
+
} | {
|
|
22
|
+
kind: "command.progress";
|
|
23
|
+
commandId: string;
|
|
24
|
+
message: string;
|
|
25
|
+
pct?: number;
|
|
26
|
+
at: string;
|
|
27
|
+
} | {
|
|
28
|
+
kind: "command.done";
|
|
29
|
+
commandId: string;
|
|
30
|
+
result: unknown;
|
|
31
|
+
durationMs: number;
|
|
32
|
+
at: string;
|
|
33
|
+
} | {
|
|
34
|
+
kind: "command.error";
|
|
35
|
+
commandId: string;
|
|
36
|
+
message: string;
|
|
37
|
+
durationMs: number;
|
|
38
|
+
at: string;
|
|
39
|
+
} | {
|
|
40
|
+
kind: "process.started";
|
|
41
|
+
processId: string;
|
|
42
|
+
topology: string;
|
|
43
|
+
port?: number;
|
|
44
|
+
at: string;
|
|
45
|
+
} | {
|
|
46
|
+
kind: "process.log";
|
|
47
|
+
processId: string;
|
|
48
|
+
line: LogLine;
|
|
49
|
+
} | {
|
|
50
|
+
kind: "process.exited";
|
|
51
|
+
processId: string;
|
|
52
|
+
code: number | null;
|
|
53
|
+
signal: NodeJS.Signals | null;
|
|
54
|
+
at: string;
|
|
55
|
+
};
|
|
56
|
+
export type KernelEventKind = KernelEvent["kind"];
|
|
57
|
+
type EventByKind<K extends KernelEventKind> = Extract<KernelEvent, {
|
|
58
|
+
kind: K;
|
|
59
|
+
}>;
|
|
60
|
+
/**
|
|
61
|
+
* Tiny in-process bus. EventEmitter under the hood; the typed wrappers
|
|
62
|
+
* are the only public surface so consumers can't subscribe to unknown
|
|
63
|
+
* event names.
|
|
64
|
+
*/
|
|
65
|
+
export declare class EventBus {
|
|
66
|
+
private readonly emitter;
|
|
67
|
+
constructor();
|
|
68
|
+
emit(event: KernelEvent): void;
|
|
69
|
+
on<K extends KernelEventKind>(kind: K, handler: (event: EventByKind<K>) => void): () => void;
|
|
70
|
+
onAny(handler: (event: KernelEvent) => void): () => void;
|
|
71
|
+
}
|
|
72
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventBus — typed pub/sub for kernel-level events.
|
|
3
|
+
*
|
|
4
|
+
* Every command and every supervised process emits events here. The CLI
|
|
5
|
+
* renders them in ink, Studio mirrors them over SSE, MCP forwards them to
|
|
6
|
+
* the model. One emitter, many subscribers — that's the whole point.
|
|
7
|
+
*/
|
|
8
|
+
import { EventEmitter } from "node:events";
|
|
9
|
+
const WILDCARD = "__any__";
|
|
10
|
+
/**
|
|
11
|
+
* Tiny in-process bus. EventEmitter under the hood; the typed wrappers
|
|
12
|
+
* are the only public surface so consumers can't subscribe to unknown
|
|
13
|
+
* event names.
|
|
14
|
+
*/
|
|
15
|
+
export class EventBus {
|
|
16
|
+
emitter = new EventEmitter();
|
|
17
|
+
constructor() {
|
|
18
|
+
// Plenty of headroom — Studio + ink + MCP can each add several listeners.
|
|
19
|
+
this.emitter.setMaxListeners(100);
|
|
20
|
+
}
|
|
21
|
+
emit(event) {
|
|
22
|
+
this.emitter.emit(event.kind, event);
|
|
23
|
+
this.emitter.emit(WILDCARD, event);
|
|
24
|
+
}
|
|
25
|
+
on(kind, handler) {
|
|
26
|
+
const listener = handler;
|
|
27
|
+
this.emitter.on(kind, listener);
|
|
28
|
+
return () => this.emitter.off(kind, listener);
|
|
29
|
+
}
|
|
30
|
+
onAny(handler) {
|
|
31
|
+
this.emitter.on(WILDCARD, handler);
|
|
32
|
+
return () => this.emitter.off(WILDCARD, handler);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal command-routing substrate for the MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Was `@nwire/app` in 0.9.x — moved in-tree as the kernel package was
|
|
5
|
+
* repurposed for the runtime substrate. MCP keeps its own dispatch table
|
|
6
|
+
* + event bus until the MCP server itself becomes a wire adapter.
|
|
7
|
+
*/
|
|
8
|
+
export { createKernel, type Kernel } from "./kernel.js";
|
|
9
|
+
export { CommandRouter, type CommandHandler, type CommandContext, type CommandHandle, } from "./command-router.js";
|
|
10
|
+
export { EventBus, type KernelEvent, type KernelEventKind } from "./event-bus.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal command-routing substrate for the MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Was `@nwire/app` in 0.9.x — moved in-tree as the kernel package was
|
|
5
|
+
* repurposed for the runtime substrate. MCP keeps its own dispatch table
|
|
6
|
+
* + event bus until the MCP server itself becomes a wire adapter.
|
|
7
|
+
*/
|
|
8
|
+
export { createKernel } from "./kernel.js";
|
|
9
|
+
export { CommandRouter, } from "./command-router.js";
|
|
10
|
+
export { EventBus } from "./event-bus.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kernel — composes the three primitives into the one object the CLI,
|
|
3
|
+
* Studio, and MCP all hold.
|
|
4
|
+
*
|
|
5
|
+
* const kernel = createKernel();
|
|
6
|
+
* kernel.router.register("dev", devHandler);
|
|
7
|
+
* kernel.run("dev", { wire: "main" }); // delegates to router
|
|
8
|
+
* kernel.bus.on("command.log", ...); // anyone can listen
|
|
9
|
+
* kernel.supervisor.start(...); // long-lived processes
|
|
10
|
+
*
|
|
11
|
+
* That's the whole surface. New commands plug into the router; new live
|
|
12
|
+
* surfaces plug into the bus; new long-running processes plug into the
|
|
13
|
+
* supervisor.
|
|
14
|
+
*/
|
|
15
|
+
import { RunnerSupervisor } from "@nwire/supervisor";
|
|
16
|
+
import { CommandRouter, type CommandHandle } from "./command-router.js";
|
|
17
|
+
import { EventBus } from "./event-bus.js";
|
|
18
|
+
export interface Kernel {
|
|
19
|
+
readonly bus: EventBus;
|
|
20
|
+
readonly router: CommandRouter;
|
|
21
|
+
readonly supervisor: RunnerSupervisor;
|
|
22
|
+
run<TArgs = unknown, TResult = unknown>(name: string, args: TArgs): CommandHandle<TResult>;
|
|
23
|
+
}
|
|
24
|
+
export declare function createKernel(): Kernel;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kernel — composes the three primitives into the one object the CLI,
|
|
3
|
+
* Studio, and MCP all hold.
|
|
4
|
+
*
|
|
5
|
+
* const kernel = createKernel();
|
|
6
|
+
* kernel.router.register("dev", devHandler);
|
|
7
|
+
* kernel.run("dev", { wire: "main" }); // delegates to router
|
|
8
|
+
* kernel.bus.on("command.log", ...); // anyone can listen
|
|
9
|
+
* kernel.supervisor.start(...); // long-lived processes
|
|
10
|
+
*
|
|
11
|
+
* That's the whole surface. New commands plug into the router; new live
|
|
12
|
+
* surfaces plug into the bus; new long-running processes plug into the
|
|
13
|
+
* supervisor.
|
|
14
|
+
*/
|
|
15
|
+
import { RunnerSupervisor } from "@nwire/supervisor";
|
|
16
|
+
import { CommandRouter } from "./command-router.js";
|
|
17
|
+
import { EventBus } from "./event-bus.js";
|
|
18
|
+
export function createKernel() {
|
|
19
|
+
const bus = new EventBus();
|
|
20
|
+
const router = new CommandRouter(bus);
|
|
21
|
+
const supervisor = new RunnerSupervisor();
|
|
22
|
+
// Re-publish supervisor events on the kernel bus so a single subscriber
|
|
23
|
+
// sees the whole picture without listening on two emitters.
|
|
24
|
+
supervisor.on("log", (processId, line) => {
|
|
25
|
+
bus.emit({ kind: "process.log", processId, line });
|
|
26
|
+
});
|
|
27
|
+
supervisor.on("status", (processId, status, proc) => {
|
|
28
|
+
if (status === "running") {
|
|
29
|
+
bus.emit({
|
|
30
|
+
kind: "process.started",
|
|
31
|
+
processId,
|
|
32
|
+
topology: proc.topology,
|
|
33
|
+
port: proc.port,
|
|
34
|
+
at: new Date().toISOString(),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
else if (status === "exited" || status === "crashed") {
|
|
38
|
+
bus.emit({
|
|
39
|
+
kind: "process.exited",
|
|
40
|
+
processId,
|
|
41
|
+
code: proc.exitCode ?? null,
|
|
42
|
+
signal: (proc.signal ?? null),
|
|
43
|
+
at: new Date().toISOString(),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
bus,
|
|
49
|
+
router,
|
|
50
|
+
supervisor,
|
|
51
|
+
run: (name, args) => router.run(name, args),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/mcp` — Adapter shape for 0.10.
|
|
3
|
+
*
|
|
4
|
+
* import { mcpAdapter } from "@nwire/mcp";
|
|
5
|
+
*
|
|
6
|
+
* const mcp = mcpAdapter();
|
|
7
|
+
* await endpoint("svc", { port: 3000 })
|
|
8
|
+
* .use(httpKoa({ prefix: "/api" }))
|
|
9
|
+
* .use(mcp)
|
|
10
|
+
* .mount(app)
|
|
11
|
+
* .run();
|
|
12
|
+
*
|
|
13
|
+
* const tools = mcp.list(); // [{ name, description, input }]
|
|
14
|
+
* const result = await mcp.call("create-task", { title: "x" });
|
|
15
|
+
*
|
|
16
|
+
* Consumes wires whose `binding.$adapter === "mcp"` + `binding.kind === "tool"`.
|
|
17
|
+
* In-process introspection (`list`, `call`) makes the adapter testable without
|
|
18
|
+
* spinning up an MCP stdio server. The legacy `serveMcp(...)` stdio entry
|
|
19
|
+
* stays available via the package's main export.
|
|
20
|
+
*/
|
|
21
|
+
import type { Adapter } from "@nwire/endpoint";
|
|
22
|
+
import { type Logger } from "@nwire/logger";
|
|
23
|
+
export interface McpAdapterConfig {
|
|
24
|
+
readonly logger?: Logger;
|
|
25
|
+
}
|
|
26
|
+
export interface McpToolDescriptor {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly description?: string;
|
|
29
|
+
readonly inputSchema?: unknown;
|
|
30
|
+
}
|
|
31
|
+
export interface McpAdapter extends Adapter {
|
|
32
|
+
list(): readonly McpToolDescriptor[];
|
|
33
|
+
call(toolName: string, input: unknown): Promise<unknown>;
|
|
34
|
+
}
|
|
35
|
+
export declare function mcpAdapter(config?: McpAdapterConfig): McpAdapter;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/mcp` — Adapter shape for 0.10.
|
|
3
|
+
*
|
|
4
|
+
* import { mcpAdapter } from "@nwire/mcp";
|
|
5
|
+
*
|
|
6
|
+
* const mcp = mcpAdapter();
|
|
7
|
+
* await endpoint("svc", { port: 3000 })
|
|
8
|
+
* .use(httpKoa({ prefix: "/api" }))
|
|
9
|
+
* .use(mcp)
|
|
10
|
+
* .mount(app)
|
|
11
|
+
* .run();
|
|
12
|
+
*
|
|
13
|
+
* const tools = mcp.list(); // [{ name, description, input }]
|
|
14
|
+
* const result = await mcp.call("create-task", { title: "x" });
|
|
15
|
+
*
|
|
16
|
+
* Consumes wires whose `binding.$adapter === "mcp"` + `binding.kind === "tool"`.
|
|
17
|
+
* In-process introspection (`list`, `call`) makes the adapter testable without
|
|
18
|
+
* spinning up an MCP stdio server. The legacy `serveMcp(...)` stdio entry
|
|
19
|
+
* stays available via the package's main export.
|
|
20
|
+
*/
|
|
21
|
+
import { dummyContainer } from "@nwire/container";
|
|
22
|
+
import { ConsoleLogger } from "@nwire/logger";
|
|
23
|
+
function isToolBinding(b) {
|
|
24
|
+
return (typeof b === "object" &&
|
|
25
|
+
b !== null &&
|
|
26
|
+
b.$adapter === "mcp" &&
|
|
27
|
+
b.kind === "tool" &&
|
|
28
|
+
typeof b.tool === "string");
|
|
29
|
+
}
|
|
30
|
+
export function mcpAdapter(config = {}) {
|
|
31
|
+
const logger = config.logger ?? new ConsoleLogger();
|
|
32
|
+
let tools = new Map();
|
|
33
|
+
let bootCtx;
|
|
34
|
+
return {
|
|
35
|
+
$kind: "adapter",
|
|
36
|
+
kind: "mcp",
|
|
37
|
+
list() {
|
|
38
|
+
return [...tools.values()].map(({ binding }) => ({
|
|
39
|
+
name: binding.tool,
|
|
40
|
+
description: binding.description,
|
|
41
|
+
inputSchema: binding.input,
|
|
42
|
+
}));
|
|
43
|
+
},
|
|
44
|
+
async call(toolName, input) {
|
|
45
|
+
const entry = tools.get(toolName);
|
|
46
|
+
if (!entry)
|
|
47
|
+
throw new Error(`unknown MCP tool: ${toolName}`);
|
|
48
|
+
if (!bootCtx)
|
|
49
|
+
throw new Error("mcpAdapter not booted");
|
|
50
|
+
const { wire, binding } = entry;
|
|
51
|
+
let parsedInput = input;
|
|
52
|
+
if (binding.input) {
|
|
53
|
+
parsedInput = binding.input.parse(input);
|
|
54
|
+
}
|
|
55
|
+
const parentContainer = bootCtx.containerOf(wire) ?? dummyContainer();
|
|
56
|
+
const reqContainer = parentContainer.createScope();
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
58
|
+
const wireApp = wire.app;
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
60
|
+
const runtimeExecute = wireApp?.runtime?.execute;
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
62
|
+
const hasRunMethod = typeof wire.handler?.run === "function";
|
|
63
|
+
if (runtimeExecute && hasRunMethod) {
|
|
64
|
+
return await runtimeExecute.call(wireApp.runtime, wire.handler, parsedInput, {});
|
|
65
|
+
}
|
|
66
|
+
const handlerCtx = {
|
|
67
|
+
resolve: (name) => reqContainer.resolve(name),
|
|
68
|
+
logger,
|
|
69
|
+
};
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
71
|
+
const fn = wire.handler.run ?? wire.handler;
|
|
72
|
+
return await fn(parsedInput, handlerCtx);
|
|
73
|
+
},
|
|
74
|
+
async boot(ctx) {
|
|
75
|
+
bootCtx = ctx;
|
|
76
|
+
tools = new Map();
|
|
77
|
+
for (const wire of ctx.wires) {
|
|
78
|
+
if (!isToolBinding(wire.binding))
|
|
79
|
+
continue;
|
|
80
|
+
tools.set(wire.binding.tool, { wire, binding: wire.binding });
|
|
81
|
+
}
|
|
82
|
+
ctx.addCheck({ name: "mcp", check: () => undefined });
|
|
83
|
+
},
|
|
84
|
+
async shutdown() {
|
|
85
|
+
tools = new Map();
|
|
86
|
+
bootCtx = undefined;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|