@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.
- package/LICENSE +21 -0
- package/README.md +13 -0
- package/dist/dispatch.d.ts +46 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +141 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/http-server.d.ts +17 -0
- package/dist/http-server.d.ts.map +1 -0
- package/dist/http-server.js +65 -0
- package/dist/http-server.js.map +1 -0
- package/dist/http.d.ts +33 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +66 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/jsonrpc.d.ts +129 -0
- package/dist/jsonrpc.d.ts.map +1 -0
- package/dist/jsonrpc.js +169 -0
- package/dist/jsonrpc.js.map +1 -0
- package/dist/start.d.ts +13 -0
- package/dist/start.d.ts.map +1 -0
- package/dist/start.js +17 -0
- package/dist/start.js.map +1 -0
- package/dist/stdio-server.d.ts +12 -0
- package/dist/stdio-server.d.ts.map +1 -0
- package/dist/stdio-server.js +18 -0
- package/dist/stdio-server.js.map +1 -0
- package/dist/stdio.d.ts +29 -0
- package/dist/stdio.d.ts.map +1 -0
- package/dist/stdio.js +49 -0
- package/dist/stdio.js.map +1 -0
- package/package.json +54 -0
- package/src/czap-cli-shim.d.ts +3 -0
- package/src/dispatch.ts +177 -0
- package/src/http-server.ts +69 -0
- package/src/http.ts +78 -0
- package/src/index.ts +31 -0
- package/src/jsonrpc.ts +243 -0
- package/src/start.ts +23 -0
- package/src/stdio-server.ts +19 -0
- package/src/stdio.ts +54 -0
package/dist/jsonrpc.js
ADDED
|
@@ -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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"}
|
package/dist/start.d.ts
ADDED
|
@@ -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"}
|
package/dist/stdio.d.ts
ADDED
|
@@ -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
|
+
}
|
package/src/dispatch.ts
ADDED
|
@@ -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
|
+
}
|