@czap/mcp-server 0.1.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.
@@ -0,0 +1,169 @@
1
+ /**
2
+ * JsonRpcServer — framework-free JSON-RPC 2.0 kernel.
3
+ *
4
+ * Parses incoming wire bytes, classifies them as Request | Notification |
5
+ * Batch | InvalidRequest | ParseError, and produces responses (or null
6
+ * for notifications, which MUST NOT receive a response per §4.1).
7
+ *
8
+ * Exposed as a `pureTransform` arm capsule `mcp.jsonrpc-server` so it
9
+ * appears in the manifest and can be reused by future JSON-RPC surfaces
10
+ * beyond MCP.
11
+ *
12
+ * Conformance: JSON-RPC 2.0 specification (https://www.jsonrpc.org/specification).
13
+ * §3 — `jsonrpc: "2.0"` required.
14
+ * §4 — Request vs Notification distinguished by presence of `id`.
15
+ * §4.1 — A Notification MUST NOT receive a Response.
16
+ * §4.2 — Parse errors MUST emit a Response with code -32700, id null.
17
+ * §5 — Response is `result` XOR `error`.
18
+ * §5.1 — Standard error codes.
19
+ * §6 — Batch: array of requests/notifications. Empty array → -32600.
20
+ *
21
+ * @module
22
+ */
23
+ import { Schema } from 'effect';
24
+ import { defineCapsule } from '@czap/core';
25
+ // ---------- Standard error codes (§5.1) ----------
26
+ export const ParseError = -32700;
27
+ export const InvalidRequest = -32600;
28
+ export const MethodNotFound = -32601;
29
+ export const InvalidParams = -32602;
30
+ export const InternalError = -32603;
31
+ /**
32
+ * Parse a single JSON-RPC line. Distinguishes:
33
+ * - parse failure → `parse-error` (§4.2)
34
+ * - empty array → `invalid-request` per §6
35
+ * - non-object scalar → `invalid-request`
36
+ * - object with bad `jsonrpc`/`method` → `invalid-request`
37
+ * - object with `id` present → `request`
38
+ * - object without `id` → `notification`
39
+ * - non-empty array → `batch` with per-element outcomes
40
+ *
41
+ * Note (§4 id-vs-notification): `"id": null` is a Request with id null,
42
+ * not a notification. Only an absent id field marks a notification.
43
+ */
44
+ function _parse(line) {
45
+ let raw;
46
+ try {
47
+ raw = JSON.parse(line);
48
+ }
49
+ catch {
50
+ const parseFailure = { kind: 'parse-error' };
51
+ return parseFailure;
52
+ }
53
+ if (Array.isArray(raw)) {
54
+ if (raw.length === 0)
55
+ return { kind: 'invalid-request', id: null };
56
+ return { kind: 'batch', outcomes: raw.map(_classify) };
57
+ }
58
+ return _classify(raw);
59
+ }
60
+ function _classify(raw) {
61
+ if (typeof raw !== 'object' || raw === null) {
62
+ return { kind: 'invalid-request', id: null };
63
+ }
64
+ const obj = raw;
65
+ if (obj.jsonrpc !== '2.0' || typeof obj.method !== 'string') {
66
+ const id = typeof obj.id === 'string' || typeof obj.id === 'number' || obj.id === null
67
+ ? obj.id
68
+ : null;
69
+ return { kind: 'invalid-request', id };
70
+ }
71
+ if (!('id' in obj) || obj.id === undefined) {
72
+ return { kind: 'notification', message: obj };
73
+ }
74
+ return { kind: 'request', message: obj };
75
+ }
76
+ /** Construct a -32700 / -32600 / -32601 / -32602 / -32603 error response. */
77
+ function _errorResponse(id, code, message, data) {
78
+ return data !== undefined
79
+ ? { jsonrpc: '2.0', id, error: { code, message, data } }
80
+ : { jsonrpc: '2.0', id, error: { code, message } };
81
+ }
82
+ /** Construct a success response (§5). */
83
+ function _successResponse(id, result) {
84
+ return { jsonrpc: '2.0', id, result };
85
+ }
86
+ // Re-export pure functions for direct import sites.
87
+ export const parse = _parse;
88
+ export const errorResponse = _errorResponse;
89
+ export const successResponse = _successResponse;
90
+ // ---------- Capsule declaration (pureTransform arm) ----------
91
+ //
92
+ // Schemas are deliberately structural: the harness uses them to drive
93
+ // `Schema.decodeUnknownEffect` against `fc.anything()` inputs, so we
94
+ // only need to express enough shape for it to filter the property test.
95
+ const JsonRpcInputSchema = Schema.String;
96
+ const ParseOutcomeKindSchema = Schema.Union([
97
+ Schema.Literal('request'),
98
+ Schema.Literal('notification'),
99
+ Schema.Literal('batch'),
100
+ Schema.Literal('parse-error'),
101
+ Schema.Literal('invalid-request'),
102
+ ]);
103
+ const ParseOutcomeSchema = Schema.Struct({ kind: ParseOutcomeKindSchema });
104
+ /**
105
+ * Capsule definition for the kernel — placed in the catalog under the
106
+ * `pureTransform` arm so the factory compiler emits a generated test +
107
+ * bench pair and the manifest tracks the kernel's content address.
108
+ */
109
+ export const jsonRpcServerCapsule = defineCapsule({
110
+ _kind: 'pureTransform',
111
+ name: 'mcp.jsonrpc-server',
112
+ site: ['node', 'browser'],
113
+ capabilities: { reads: [], writes: [] },
114
+ input: JsonRpcInputSchema,
115
+ output: ParseOutcomeSchema,
116
+ budgets: { p95Ms: 1, allocClass: 'bounded' },
117
+ invariants: [
118
+ {
119
+ name: 'malformed-json-yields-parse-error',
120
+ check: (input, _output) => {
121
+ // Behavioral invariant: an input that is NOT valid JSON MUST be
122
+ // classified as parse-error. The TS union proves syntactic
123
+ // shape; this proves the parser actually rejects bad input.
124
+ try {
125
+ JSON.parse(input);
126
+ return true; // valid JSON — not the negative case we're testing
127
+ }
128
+ catch {
129
+ return _parse(input).kind === 'parse-error';
130
+ }
131
+ },
132
+ message: 'inputs that JSON.parse rejects must yield kind: parse-error',
133
+ },
134
+ {
135
+ name: 'absent-id-classifies-as-notification',
136
+ check: (input, _output) => {
137
+ // Behavioral invariant: a well-formed object with jsonrpc:'2.0'
138
+ // and method:string but NO id field MUST be a notification, not
139
+ // a request. This is the §4.1 distinction the strike force flagged.
140
+ let obj;
141
+ try {
142
+ obj = JSON.parse(input);
143
+ }
144
+ catch {
145
+ const skipAbsentIdCase = true;
146
+ return skipAbsentIdCase;
147
+ }
148
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj))
149
+ return true;
150
+ const o = obj;
151
+ if (o.jsonrpc !== '2.0' || typeof o.method !== 'string')
152
+ return true;
153
+ if ('id' in o && o.id !== undefined)
154
+ return true; // request, not the test case
155
+ return _parse(input).kind === 'notification';
156
+ },
157
+ message: 'well-formed messages without an id field must classify as notifications (§4.1)',
158
+ },
159
+ ],
160
+ run: (input) => _parse(input),
161
+ });
162
+ // ---------- Namespace surface (ADR-0001) ----------
163
+ /** Namespaced public surface of the kernel. */
164
+ export const JsonRpcServer = {
165
+ parse: _parse,
166
+ errorResponse: _errorResponse,
167
+ successResponse: _successResponse,
168
+ };
169
+ //# sourceMappingURL=jsonrpc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonrpc.js","sourceRoot":"","sources":["../src/jsonrpc.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * start — pick an MCP transport. Default is stdio; pass `{ http: ':3838' }`
3
+ * to bind HTTP instead.
4
+ *
5
+ * @module
6
+ */
7
+ /** Options for `start`. */
8
+ export interface StartOpts {
9
+ readonly http?: string;
10
+ }
11
+ /** Start the MCP server on the requested transport. */
12
+ export declare function start(opts?: StartOpts): Promise<void>;
13
+ //# sourceMappingURL=start.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"start.d.ts","sourceRoot":"","sources":["../src/start.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,2BAA2B;AAC3B,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,uDAAuD;AACvD,wBAAsB,KAAK,CAAC,IAAI,GAAE,SAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAO/D"}
package/dist/start.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * start — pick an MCP transport. Default is stdio; pass `{ http: ':3838' }`
3
+ * to bind HTTP instead.
4
+ *
5
+ * @module
6
+ */
7
+ import { runStdio } from './stdio.js';
8
+ /** Start the MCP server on the requested transport. */
9
+ export async function start(opts = {}) {
10
+ if (opts.http !== undefined) {
11
+ const { runHttp } = await import('./http.js');
12
+ await runHttp(opts.http);
13
+ return;
14
+ }
15
+ await runStdio();
16
+ }
17
+ //# sourceMappingURL=start.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"start.js","sourceRoot":"","sources":["../src/start.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAOtC,uDAAuD;AACvD,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,OAAkB,EAAE;IAC9C,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC5B,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC;QAC9C,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzB,OAAO;IACT,CAAC;IACD,MAAM,QAAQ,EAAE,CAAC;AACnB,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * MCP stdio server bootstrap. Provides the tsx direct-invoke entrypoint
3
+ * for `tests/integration/mcp/stdio-spawn.test.ts`. Excluded from
4
+ * coverage because the only way to exercise this guard is by spawning
5
+ * the script as the entrypoint of a Node process — which is what the
6
+ * integration test does. The pure read-line-write loop lives in
7
+ * `stdio.ts` (`runStdio` / `processLine`) and is fully unit-tested.
8
+ *
9
+ * @module
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=stdio-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdio-server.d.ts","sourceRoot":"","sources":["../src/stdio-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * MCP stdio server bootstrap. Provides the tsx direct-invoke entrypoint
3
+ * for `tests/integration/mcp/stdio-spawn.test.ts`. Excluded from
4
+ * coverage because the only way to exercise this guard is by spawning
5
+ * the script as the entrypoint of a Node process — which is what the
6
+ * integration test does. The pure read-line-write loop lives in
7
+ * `stdio.ts` (`runStdio` / `processLine`) and is fully unit-tested.
8
+ *
9
+ * @module
10
+ */
11
+ import { runStdio } from './stdio.js';
12
+ if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith('stdio-server.ts') || process.argv[1]?.endsWith('stdio.ts')) {
13
+ runStdio().catch((err) => {
14
+ process.stderr.write(JSON.stringify({ error: String(err) }) + '\n');
15
+ process.exit(1);
16
+ });
17
+ }
18
+ //# sourceMappingURL=stdio-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdio-server.js","sourceRoot":"","sources":["../src/stdio-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,UAAU,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,iBAAiB,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;IAC7I,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QAChC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;QACpE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * MCP stdio server — reads JSON-RPC 2.0 framed messages line-by-line from
3
+ * stdin, writes responses to stdout. Routes every line through
4
+ * `JsonRpcServer.parse` so parse errors emit -32700 (was silently dropped)
5
+ * and notifications produce no response (§4.1).
6
+ *
7
+ * @module
8
+ */
9
+ import type { Readable, Writable } from 'node:stream';
10
+ /**
11
+ * Process a single JSON-RPC stdio line and return the wire payload (a
12
+ * JSON-encoded string) or `null` when no response should be emitted
13
+ * (notification or pure-notification batch). Empty/whitespace-only lines
14
+ * are also `null` so the stdio loop can skip them silently.
15
+ *
16
+ * Exported so unit tests cover the exact line-handling logic without
17
+ * spinning up a child process and pumping stdin.
18
+ */
19
+ export declare function processLine(line: string): Promise<string | null>;
20
+ /**
21
+ * Run the MCP stdio loop until the input stream closes. Defaults to
22
+ * `process.stdin` / `process.stdout` so the production CLI bootstrap
23
+ * stays a one-liner (`runStdio()`); tests inject a pre-populated
24
+ * Readable + a sink Writable to exercise the full read-line-write loop
25
+ * without spawning a child process.
26
+ */
27
+ export declare function runStdio(input?: Readable, output?: Writable): Promise<void>;
28
+ import './stdio-server.js';
29
+ //# sourceMappingURL=stdio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../src/stdio.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAGtD;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKtE;AAED;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAC5B,KAAK,GAAE,QAAwB,EAC/B,MAAM,GAAE,QAAyB,GAChC,OAAO,CAAC,IAAI,CAAC,CAQf;AAMD,OAAO,mBAAmB,CAAC"}
package/dist/stdio.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * MCP stdio server — reads JSON-RPC 2.0 framed messages line-by-line from
3
+ * stdin, writes responses to stdout. Routes every line through
4
+ * `JsonRpcServer.parse` so parse errors emit -32700 (was silently dropped)
5
+ * and notifications produce no response (§4.1).
6
+ *
7
+ * @module
8
+ */
9
+ import { createInterface } from 'node:readline/promises';
10
+ import { handleRequest } from './http.js';
11
+ /**
12
+ * Process a single JSON-RPC stdio line and return the wire payload (a
13
+ * JSON-encoded string) or `null` when no response should be emitted
14
+ * (notification or pure-notification batch). Empty/whitespace-only lines
15
+ * are also `null` so the stdio loop can skip them silently.
16
+ *
17
+ * Exported so unit tests cover the exact line-handling logic without
18
+ * spinning up a child process and pumping stdin.
19
+ */
20
+ export async function processLine(line) {
21
+ if (!line.trim())
22
+ return null;
23
+ const response = await handleRequest(line);
24
+ if (response === null)
25
+ return null;
26
+ return JSON.stringify(response);
27
+ }
28
+ /**
29
+ * Run the MCP stdio loop until the input stream closes. Defaults to
30
+ * `process.stdin` / `process.stdout` so the production CLI bootstrap
31
+ * stays a one-liner (`runStdio()`); tests inject a pre-populated
32
+ * Readable + a sink Writable to exercise the full read-line-write loop
33
+ * without spawning a child process.
34
+ */
35
+ export async function runStdio(input = process.stdin, output = process.stdout) {
36
+ const rl = createInterface({ input });
37
+ for await (const line of rl) {
38
+ const wire = await processLine(line);
39
+ if (wire !== null) {
40
+ output.write(wire + '\n');
41
+ }
42
+ }
43
+ }
44
+ // Side-effect import installs the tsx direct-invoke guard so the integration
45
+ // spawn (`tsx packages/mcp-server/src/stdio.ts`) keeps working. Bootstrap
46
+ // lives in `./stdio-server.ts` because Windows-spawn coverage can't be
47
+ // merged back through c8 ignore (source-mapped TS line numbers don't match).
48
+ import './stdio-server.js';
49
+ //# sourceMappingURL=stdio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdio.js","sourceRoot":"","sources":["../src/stdio.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE1C;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAY;IAC5C,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;QAAE,OAAO,IAAI,CAAC;IAC9B,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC;IAC3C,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACnC,OAAO,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;AAClC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,QAAkB,OAAO,CAAC,KAAK,EAC/B,SAAmB,OAAO,CAAC,MAAM;IAEjC,MAAM,EAAE,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IACtC,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,EAAE,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC;QACrC,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClB,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC;AACH,CAAC;AAED,6EAA6E;AAC7E,0EAA0E;AAC1E,uEAAuE;AACvE,6EAA6E;AAC7E,OAAO,mBAAmB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@czap/mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "Thin MCP server over czap's capsule factory dispatch",
5
+ "license": "MIT",
6
+ "author": "Eassa Ayoub <eassa@heyoub.dev>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/heyoub/LiteShip",
10
+ "directory": "packages/mcp-server"
11
+ },
12
+ "bugs": "https://github.com/heyoub/LiteShip/issues",
13
+ "homepage": "https://github.com/heyoub/LiteShip#readme",
14
+ "type": "module",
15
+ "sideEffects": false,
16
+ "keywords": [
17
+ "czap",
18
+ "mcp",
19
+ "model-context-protocol",
20
+ "ai-tooling",
21
+ "capsule-factory",
22
+ "typescript"
23
+ ],
24
+ "main": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js",
30
+ "development": "./src/index.ts"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "src",
36
+ "LICENSE"
37
+ ],
38
+ "engines": {
39
+ "node": ">=22.0.0"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "dependencies": {
45
+ "@czap/core": "0.1.0"
46
+ },
47
+ "peerDependencies": {
48
+ "effect": ">=4.0.0-beta.32 <5",
49
+ "@czap/cli": "^0.1.0"
50
+ },
51
+ "scripts": {
52
+ "build": "tsc"
53
+ }
54
+ }
@@ -0,0 +1,3 @@
1
+ declare module '@czap/cli' {
2
+ export function run(argv: readonly string[]): Promise<number>;
3
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * MCP tool dispatch — maps tools/call params to czap CLI command
3
+ * executions. Captures CLI stdout and returns it as MCP text content.
4
+ *
5
+ * Entry point `dispatch` accepts a typed JSON-RPC `Request | Notification`
6
+ * (post-`JsonRpcServer.parse` classification) and produces a
7
+ * `JsonRpcResponse | null`. `null` is returned for notifications: per
8
+ * JSON-RPC 2.0 §4.1 the server MUST NOT send a response for them.
9
+ *
10
+ * `dispatchToolCall` remains exported for tests that exercise the CLI
11
+ * dispatch path directly without going through the JSON-RPC envelope.
12
+ *
13
+ * @module
14
+ */
15
+
16
+ import {
17
+ type JsonRpcNotification,
18
+ type JsonRpcRequest,
19
+ type JsonRpcResponse,
20
+ errorResponse,
21
+ successResponse,
22
+ MethodNotFound,
23
+ InvalidParams,
24
+ InternalError,
25
+ } from './jsonrpc.js';
26
+
27
+ /**
28
+ * Sentinel for invalid-params throws inside method invocations. Caught
29
+ * by `dispatch` and mapped to JSON-RPC 2.0 §5.1 code -32602 (the spec
30
+ * code for malformed parameters). Generic `Error`s remain -32603
31
+ * (Internal error).
32
+ */
33
+ class InvalidParamsError extends Error {
34
+ constructor(message: string, readonly detail?: unknown) {
35
+ super(message);
36
+ this.name = 'InvalidParamsError';
37
+ }
38
+ }
39
+
40
+ type RunFn = (argv: readonly string[]) => Promise<number>;
41
+
42
+ let cachedRun: RunFn | undefined;
43
+
44
+ /** Lazy-load `@czap/cli` so `@czap/mcp-server` does not declare a package dependency cycle. */
45
+ async function getRun(): Promise<RunFn> {
46
+ if (!cachedRun) {
47
+ const mod = await import('@czap/cli');
48
+ cachedRun = mod.run;
49
+ }
50
+ return cachedRun;
51
+ }
52
+
53
+ /** Shape of an MCP tools/call parameter object. */
54
+ export interface McpToolCall {
55
+ readonly name: string;
56
+ readonly arguments: Record<string, unknown>;
57
+ }
58
+
59
+ /** MCP tools/call result envelope. */
60
+ export interface McpToolResult {
61
+ readonly content: ReadonlyArray<{ type: 'text'; text: string }>;
62
+ readonly isError: boolean;
63
+ }
64
+
65
+ /**
66
+ * Route a parsed JSON-RPC message to its method handler.
67
+ *
68
+ * Returns `null` for notifications (§4.1: notifications MUST NOT receive
69
+ * a response). For requests, returns either a success or an error
70
+ * response. Internal handler exceptions are caught and surfaced as
71
+ * `-32603 Internal error` per §5.1.
72
+ */
73
+ export async function dispatch(
74
+ msg: JsonRpcRequest | JsonRpcNotification,
75
+ ): Promise<JsonRpcResponse | null> {
76
+ const isNotification = !('id' in msg);
77
+ const id = isNotification ? null : (msg as JsonRpcRequest).id;
78
+
79
+ try {
80
+ const result = await invoke(msg);
81
+ if (isNotification) return null;
82
+ if (result.kind === 'method-not-found') {
83
+ return errorResponse(id, MethodNotFound, 'method not found', { method: msg.method });
84
+ }
85
+ return successResponse(id, result.value);
86
+ } catch (err) {
87
+ if (isNotification) {
88
+ const notificationAck: null = null;
89
+ return notificationAck;
90
+ }
91
+ if (err instanceof InvalidParamsError) {
92
+ return errorResponse(id, InvalidParams, err.message, err.detail);
93
+ }
94
+ return errorResponse(id, InternalError, 'Internal error', { detail: String(err) });
95
+ }
96
+ }
97
+
98
+ /** Internal: dispatch result shape. */
99
+ type InvokeResult =
100
+ | { readonly kind: 'ok'; readonly value: unknown }
101
+ | { readonly kind: 'method-not-found' };
102
+
103
+ function ok(value: unknown): InvokeResult {
104
+ return { kind: 'ok', value };
105
+ }
106
+
107
+ async function invoke(msg: JsonRpcRequest | JsonRpcNotification): Promise<InvokeResult> {
108
+ switch (msg.method) {
109
+ case 'tools/list':
110
+ return ok({ tools: listTools() });
111
+ case 'tools/call': {
112
+ const params = msg.params as { name: string; arguments: Record<string, unknown> } | undefined;
113
+ if (!params || typeof params.name !== 'string') {
114
+ // Per §5.1, malformed params → -32602. InvalidParamsError sentinel
115
+ // is mapped to InvalidParams in dispatch's catch block.
116
+ throw new InvalidParamsError(
117
+ 'tools/call requires { name: string, arguments: object }',
118
+ { received: params },
119
+ );
120
+ }
121
+ const result = await dispatchToolCall(params);
122
+ return ok(result);
123
+ }
124
+ default:
125
+ return { kind: 'method-not-found' };
126
+ }
127
+ }
128
+
129
+ /** Translate a tools/call into argv, run the CLI, capture stdout. */
130
+ export async function dispatchToolCall(call: McpToolCall): Promise<McpToolResult> {
131
+ const args = buildArgv(call);
132
+ const originalWrite = process.stdout.write.bind(process.stdout);
133
+ let captured = '';
134
+ (process.stdout as unknown as { write: unknown }).write = ((chunk: string | Uint8Array) => {
135
+ captured += typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString();
136
+ return true;
137
+ });
138
+ try {
139
+ const run = await getRun();
140
+ const code = await run(args);
141
+ return {
142
+ content: [{ type: 'text', text: captured.trim() }],
143
+ isError: code !== 0,
144
+ };
145
+ } finally {
146
+ (process.stdout as unknown as { write: typeof originalWrite }).write = originalWrite;
147
+ }
148
+ }
149
+
150
+ function buildArgv(call: McpToolCall): string[] {
151
+ const segments = call.name.split('.');
152
+ const args: string[] = [];
153
+ for (const [k, v] of Object.entries(call.arguments)) {
154
+ if (typeof v === 'boolean') {
155
+ if (v) args.push(`--${k}`);
156
+ } else {
157
+ args.push(`--${k}=${String(v)}`);
158
+ }
159
+ }
160
+ return [...segments, ...args];
161
+ }
162
+
163
+ /** Static list of MCP tools produced by czap's CLI. */
164
+ export function listTools(): ReadonlyArray<{ name: string; description: string; inputSchema: object }> {
165
+ return [
166
+ { name: 'describe', description: 'Dump capsule catalog schema', inputSchema: { type: 'object', properties: { format: { type: 'string', enum: ['json', 'mcp'] } } } },
167
+ { name: 'scene.compile', description: 'Compile a scene capsule', inputSchema: { type: 'object', required: ['scene'], properties: { scene: { type: 'string' } } } },
168
+ { name: 'scene.render', description: 'Render scene to mp4', inputSchema: { type: 'object', required: ['scene', 'output'], properties: { scene: { type: 'string' }, output: { type: 'string' } } } },
169
+ { name: 'scene.verify', description: 'Run scene generated tests', inputSchema: { type: 'object', required: ['scene'], properties: { scene: { type: 'string' } } } },
170
+ { name: 'asset.analyze', description: 'Run cachedProjection on asset', inputSchema: { type: 'object', required: ['asset', 'projection'], properties: { asset: { type: 'string' }, projection: { type: 'string', enum: ['beat', 'onset', 'waveform'] } } } },
171
+ { name: 'asset.verify', description: 'Verify asset capsule', inputSchema: { type: 'object', required: ['asset'], properties: { asset: { type: 'string' } } } },
172
+ { name: 'capsule.inspect', description: 'Inspect a capsule manifest entry', inputSchema: { type: 'object', required: ['id'], properties: { id: { type: 'string' } } } },
173
+ { name: 'capsule.verify', description: 'Verify capsule generated tests', inputSchema: { type: 'object', required: ['id'], properties: { id: { type: 'string' } } } },
174
+ { name: 'capsule.list', description: 'List capsules filtered by kind', inputSchema: { type: 'object', properties: { kind: { type: 'string' } } } },
175
+ { name: 'gauntlet', description: 'Run the full gauntlet', inputSchema: { type: 'object', properties: { 'dry-run': { type: 'boolean' } } } },
176
+ ];
177
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * MCP HTTP server bootstrap. The pure handler logic lives in `http.ts`
3
+ * (exported as `handleRequest` / `respond`). This module owns the
4
+ * Node http server lifecycle (createServer + listen + SIGINT-await) and
5
+ * is excluded from coverage because the bootstrap path can only be
6
+ * exercised by the integration spawn at tests/integration/mcp/http.test.ts —
7
+ * Windows can't deliver SIGINT to spawned subprocesses cleanly, so a
8
+ * unit test would hang.
9
+ *
10
+ * Splitting this out lets the rest of the transport stay in coverage with
11
+ * no `c8 ignore` annotations.
12
+ *
13
+ * @module
14
+ */
15
+
16
+ import { createServer } from 'node:http';
17
+ import { handleRequest } from './http.js';
18
+
19
+ /** Run the MCP HTTP server bound to `bind` (e.g. ":3838" or "127.0.0.1:8080"). */
20
+ export async function runHttp(bind: string): Promise<void> {
21
+ const m = bind.match(/^(?:([^:]+))?:(\d+)$/);
22
+ const host = m?.[1] ?? '127.0.0.1';
23
+ const port = Number(m?.[2] ?? bind);
24
+
25
+ const server = createServer(async (req, res) => {
26
+ if (req.method !== 'POST') { res.statusCode = 405; res.end(); return; }
27
+ let body = '';
28
+ for await (const chunk of req) body += String(chunk);
29
+
30
+ const response = await handleRequest(body);
31
+
32
+ res.setHeader('content-type', 'application/json');
33
+ if (response === null) {
34
+ // §4.1: notifications produce no body. Use 204 No Content.
35
+ res.statusCode = 204;
36
+ res.end();
37
+ return;
38
+ }
39
+ res.end(JSON.stringify(response));
40
+ });
41
+
42
+ await new Promise<void>((resolve) => server.listen(port, host, () => resolve()));
43
+ // Resolve the actual bound port — when callers pass :0 they want the
44
+ // ephemeral port the OS chose, not the literal 0 they requested.
45
+ const addr = server.address();
46
+ const boundPort = typeof addr === 'object' && addr ? addr.port : port;
47
+ process.stdout.write(
48
+ JSON.stringify({
49
+ status: 'ok', command: 'mcp',
50
+ transport: 'http',
51
+ url: `http://${host}:${boundPort}/`,
52
+ }) + '\n',
53
+ );
54
+
55
+ await new Promise<void>((resolve) => {
56
+ process.on('SIGINT', () => {
57
+ server.close();
58
+ resolve();
59
+ });
60
+ });
61
+ }
62
+
63
+ if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith('http-server.ts') || process.argv[1]?.endsWith('http.ts')) {
64
+ const bind = process.argv[2] ?? ':0';
65
+ runHttp(bind).catch((err: unknown) => {
66
+ process.stderr.write(JSON.stringify({ error: String(err) }) + '\n');
67
+ process.exit(1);
68
+ });
69
+ }