@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/src/http.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP HTTP transport — POST /, body is a JSON-RPC 2.0 request.
|
|
3
|
+
* Pure JSON-RPC handler logic lives here. The Node `createServer` +
|
|
4
|
+
* `listen` + `SIGINT`-await bootstrap lives in `./http-server.ts`,
|
|
5
|
+
* excluded from coverage because Windows can't deliver SIGINT to
|
|
6
|
+
* spawned subprocesses cleanly.
|
|
7
|
+
*
|
|
8
|
+
* Routes incoming bodies through `JsonRpcServer.parse` for the same
|
|
9
|
+
* conformance properties as the stdio transport: parse errors → -32700,
|
|
10
|
+
* notifications produce no body, batches handled per §6.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { dispatch } from './dispatch.js';
|
|
16
|
+
import {
|
|
17
|
+
JsonRpcServer,
|
|
18
|
+
type JsonRpcResponse,
|
|
19
|
+
type ParseOutcome,
|
|
20
|
+
errorResponse,
|
|
21
|
+
ParseError,
|
|
22
|
+
InvalidRequest,
|
|
23
|
+
} from './jsonrpc.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve a parse outcome to its wire response, or `null` if the spec
|
|
27
|
+
* requires no response (notification, or pure-notification batch).
|
|
28
|
+
*
|
|
29
|
+
* Exported so unit tests can exercise every branch without spinning up a
|
|
30
|
+
* real HTTP server (Windows can't deliver SIGINT to subprocess for the
|
|
31
|
+
* full integration path).
|
|
32
|
+
*/
|
|
33
|
+
export async function respond(
|
|
34
|
+
outcome: ParseOutcome,
|
|
35
|
+
): Promise<JsonRpcResponse | readonly JsonRpcResponse[] | null> {
|
|
36
|
+
switch (outcome.kind) {
|
|
37
|
+
case 'parse-error':
|
|
38
|
+
return errorResponse(null, ParseError, 'Parse error');
|
|
39
|
+
case 'invalid-request':
|
|
40
|
+
return errorResponse(outcome.id, InvalidRequest, 'Invalid Request');
|
|
41
|
+
case 'notification':
|
|
42
|
+
await dispatch(outcome.message);
|
|
43
|
+
return null;
|
|
44
|
+
case 'request':
|
|
45
|
+
return dispatch(outcome.message);
|
|
46
|
+
case 'batch': {
|
|
47
|
+
const responses: JsonRpcResponse[] = [];
|
|
48
|
+
for (const sub of outcome.outcomes) {
|
|
49
|
+
const r = await respond(sub);
|
|
50
|
+
if (r === null) continue;
|
|
51
|
+
if (Array.isArray(r)) responses.push(...r);
|
|
52
|
+
else responses.push(r as JsonRpcResponse);
|
|
53
|
+
}
|
|
54
|
+
return responses.length > 0 ? responses : null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Pure wire handler — accepts a JSON-RPC body string, returns the response
|
|
61
|
+
* envelope (or null for notification-only batches). Drives the HTTP server's
|
|
62
|
+
* request path; extracted so unit tests cover every parse-outcome branch
|
|
63
|
+
* without spawning a server process.
|
|
64
|
+
*/
|
|
65
|
+
export async function handleRequest(
|
|
66
|
+
body: string,
|
|
67
|
+
): Promise<JsonRpcResponse | readonly JsonRpcResponse[] | null> {
|
|
68
|
+
const outcome = JsonRpcServer.parse(body);
|
|
69
|
+
return respond(outcome);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Re-export the bootstrap so callers (start.ts) can keep using `import { runHttp } from './http.js'`.
|
|
73
|
+
// The bootstrap module also installs a top-level direct-invoke guard for
|
|
74
|
+
// the integration spawn entrypoint (`tsx packages/mcp-server/src/http.ts ...`).
|
|
75
|
+
// We import that module for its side effect so the spawn keeps working.
|
|
76
|
+
import './http-server.js';
|
|
77
|
+
|
|
78
|
+
export { runHttp } from './http-server.js';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** `@czap/mcp-server` — MCP bridge for **LiteShip**; forwards tools to the `czap` CLI + capsule factory. */
|
|
2
|
+
|
|
3
|
+
export { start } from './start.js';
|
|
4
|
+
export type { StartOpts } from './start.js';
|
|
5
|
+
export { listTools, dispatchToolCall, dispatch } from './dispatch.js';
|
|
6
|
+
export type { McpToolCall, McpToolResult } from './dispatch.js';
|
|
7
|
+
export { runStdio } from './stdio.js';
|
|
8
|
+
export { runHttp } from './http.js';
|
|
9
|
+
|
|
10
|
+
// JSON-RPC 2.0 kernel — reusable beyond MCP.
|
|
11
|
+
export {
|
|
12
|
+
JsonRpcServer,
|
|
13
|
+
jsonRpcServerCapsule,
|
|
14
|
+
parse,
|
|
15
|
+
errorResponse,
|
|
16
|
+
successResponse,
|
|
17
|
+
ParseError,
|
|
18
|
+
InvalidRequest,
|
|
19
|
+
MethodNotFound,
|
|
20
|
+
InvalidParams,
|
|
21
|
+
InternalError,
|
|
22
|
+
} from './jsonrpc.js';
|
|
23
|
+
export type {
|
|
24
|
+
JsonRpcId,
|
|
25
|
+
JsonRpcRequest,
|
|
26
|
+
JsonRpcNotification,
|
|
27
|
+
JsonRpcResponse,
|
|
28
|
+
JsonRpcSuccess,
|
|
29
|
+
JsonRpcErrorResponse,
|
|
30
|
+
ParseOutcome,
|
|
31
|
+
} from './jsonrpc.js';
|
package/src/jsonrpc.ts
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
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
|
+
|
|
24
|
+
import { Schema } from 'effect';
|
|
25
|
+
import { defineCapsule } from '@czap/core';
|
|
26
|
+
|
|
27
|
+
// ---------- JSON-RPC 2.0 types (wire-shape) ----------
|
|
28
|
+
|
|
29
|
+
/** Per §4: `id` is string, number, or null. Absent = notification. */
|
|
30
|
+
export type JsonRpcId = string | number | null;
|
|
31
|
+
|
|
32
|
+
/** A JSON-RPC 2.0 request (has `id`). */
|
|
33
|
+
export interface JsonRpcRequest {
|
|
34
|
+
readonly jsonrpc: '2.0';
|
|
35
|
+
readonly id: JsonRpcId;
|
|
36
|
+
readonly method: string;
|
|
37
|
+
readonly params?: readonly unknown[] | Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A JSON-RPC 2.0 notification (no `id`). Per §4.1 MUST NOT be responded to. */
|
|
41
|
+
export interface JsonRpcNotification {
|
|
42
|
+
readonly jsonrpc: '2.0';
|
|
43
|
+
readonly method: string;
|
|
44
|
+
readonly params?: readonly unknown[] | Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Successful response per §5. */
|
|
48
|
+
export interface JsonRpcSuccess {
|
|
49
|
+
readonly jsonrpc: '2.0';
|
|
50
|
+
readonly id: JsonRpcId;
|
|
51
|
+
readonly result: unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Error response per §5 + §5.1. */
|
|
55
|
+
export interface JsonRpcErrorResponse {
|
|
56
|
+
readonly jsonrpc: '2.0';
|
|
57
|
+
readonly id: JsonRpcId;
|
|
58
|
+
readonly error: { readonly code: number; readonly message: string; readonly data?: unknown };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Either a success or error response. */
|
|
62
|
+
export type JsonRpcResponse = JsonRpcSuccess | JsonRpcErrorResponse;
|
|
63
|
+
|
|
64
|
+
// ---------- Standard error codes (§5.1) ----------
|
|
65
|
+
|
|
66
|
+
export const ParseError = -32700 as const;
|
|
67
|
+
export const InvalidRequest = -32600 as const;
|
|
68
|
+
export const MethodNotFound = -32601 as const;
|
|
69
|
+
export const InvalidParams = -32602 as const;
|
|
70
|
+
export const InternalError = -32603 as const;
|
|
71
|
+
|
|
72
|
+
// ---------- Parser output classification ----------
|
|
73
|
+
|
|
74
|
+
/** Discriminated union of every parse outcome the kernel produces. */
|
|
75
|
+
export type ParseOutcome =
|
|
76
|
+
| { readonly kind: 'request'; readonly message: JsonRpcRequest }
|
|
77
|
+
| { readonly kind: 'notification'; readonly message: JsonRpcNotification }
|
|
78
|
+
| { readonly kind: 'batch'; readonly outcomes: readonly ParseOutcome[] }
|
|
79
|
+
| { readonly kind: 'parse-error' }
|
|
80
|
+
| { readonly kind: 'invalid-request'; readonly id: JsonRpcId };
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parse a single JSON-RPC line. Distinguishes:
|
|
84
|
+
* - parse failure → `parse-error` (§4.2)
|
|
85
|
+
* - empty array → `invalid-request` per §6
|
|
86
|
+
* - non-object scalar → `invalid-request`
|
|
87
|
+
* - object with bad `jsonrpc`/`method` → `invalid-request`
|
|
88
|
+
* - object with `id` present → `request`
|
|
89
|
+
* - object without `id` → `notification`
|
|
90
|
+
* - non-empty array → `batch` with per-element outcomes
|
|
91
|
+
*
|
|
92
|
+
* Note (§4 id-vs-notification): `"id": null` is a Request with id null,
|
|
93
|
+
* not a notification. Only an absent id field marks a notification.
|
|
94
|
+
*/
|
|
95
|
+
function _parse(line: string): ParseOutcome {
|
|
96
|
+
let raw: unknown;
|
|
97
|
+
try {
|
|
98
|
+
raw = JSON.parse(line);
|
|
99
|
+
} catch {
|
|
100
|
+
const parseFailure: ParseOutcome = { kind: 'parse-error' };
|
|
101
|
+
return parseFailure;
|
|
102
|
+
}
|
|
103
|
+
if (Array.isArray(raw)) {
|
|
104
|
+
if (raw.length === 0) return { kind: 'invalid-request', id: null };
|
|
105
|
+
return { kind: 'batch', outcomes: raw.map(_classify) };
|
|
106
|
+
}
|
|
107
|
+
return _classify(raw);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function _classify(raw: unknown): ParseOutcome {
|
|
111
|
+
if (typeof raw !== 'object' || raw === null) {
|
|
112
|
+
return { kind: 'invalid-request', id: null };
|
|
113
|
+
}
|
|
114
|
+
const obj = raw as Record<string, unknown>;
|
|
115
|
+
if (obj.jsonrpc !== '2.0' || typeof obj.method !== 'string') {
|
|
116
|
+
const id =
|
|
117
|
+
typeof obj.id === 'string' || typeof obj.id === 'number' || obj.id === null
|
|
118
|
+
? (obj.id as JsonRpcId)
|
|
119
|
+
: null;
|
|
120
|
+
return { kind: 'invalid-request', id };
|
|
121
|
+
}
|
|
122
|
+
if (!('id' in obj) || obj.id === undefined) {
|
|
123
|
+
return { kind: 'notification', message: obj as unknown as JsonRpcNotification };
|
|
124
|
+
}
|
|
125
|
+
return { kind: 'request', message: obj as unknown as JsonRpcRequest };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Construct a -32700 / -32600 / -32601 / -32602 / -32603 error response. */
|
|
129
|
+
function _errorResponse(
|
|
130
|
+
id: JsonRpcId,
|
|
131
|
+
code: number,
|
|
132
|
+
message: string,
|
|
133
|
+
data?: unknown,
|
|
134
|
+
): JsonRpcErrorResponse {
|
|
135
|
+
return data !== undefined
|
|
136
|
+
? { jsonrpc: '2.0', id, error: { code, message, data } }
|
|
137
|
+
: { jsonrpc: '2.0', id, error: { code, message } };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Construct a success response (§5). */
|
|
141
|
+
function _successResponse(id: JsonRpcId, result: unknown): JsonRpcSuccess {
|
|
142
|
+
return { jsonrpc: '2.0', id, result };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Re-export pure functions for direct import sites.
|
|
146
|
+
export const parse = _parse;
|
|
147
|
+
export const errorResponse = _errorResponse;
|
|
148
|
+
export const successResponse = _successResponse;
|
|
149
|
+
|
|
150
|
+
// ---------- Capsule declaration (pureTransform arm) ----------
|
|
151
|
+
//
|
|
152
|
+
// Schemas are deliberately structural: the harness uses them to drive
|
|
153
|
+
// `Schema.decodeUnknownEffect` against `fc.anything()` inputs, so we
|
|
154
|
+
// only need to express enough shape for it to filter the property test.
|
|
155
|
+
const JsonRpcInputSchema = Schema.String;
|
|
156
|
+
const ParseOutcomeKindSchema = Schema.Union([
|
|
157
|
+
Schema.Literal('request'),
|
|
158
|
+
Schema.Literal('notification'),
|
|
159
|
+
Schema.Literal('batch'),
|
|
160
|
+
Schema.Literal('parse-error'),
|
|
161
|
+
Schema.Literal('invalid-request'),
|
|
162
|
+
]);
|
|
163
|
+
const ParseOutcomeSchema = Schema.Struct({ kind: ParseOutcomeKindSchema });
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Capsule definition for the kernel — placed in the catalog under the
|
|
167
|
+
* `pureTransform` arm so the factory compiler emits a generated test +
|
|
168
|
+
* bench pair and the manifest tracks the kernel's content address.
|
|
169
|
+
*/
|
|
170
|
+
export const jsonRpcServerCapsule = defineCapsule({
|
|
171
|
+
_kind: 'pureTransform',
|
|
172
|
+
name: 'mcp.jsonrpc-server',
|
|
173
|
+
site: ['node', 'browser'],
|
|
174
|
+
capabilities: { reads: [], writes: [] },
|
|
175
|
+
input: JsonRpcInputSchema,
|
|
176
|
+
output: ParseOutcomeSchema,
|
|
177
|
+
budgets: { p95Ms: 1, allocClass: 'bounded' },
|
|
178
|
+
invariants: [
|
|
179
|
+
{
|
|
180
|
+
name: 'malformed-json-yields-parse-error',
|
|
181
|
+
check: (input: string, _output): boolean => {
|
|
182
|
+
// Behavioral invariant: an input that is NOT valid JSON MUST be
|
|
183
|
+
// classified as parse-error. The TS union proves syntactic
|
|
184
|
+
// shape; this proves the parser actually rejects bad input.
|
|
185
|
+
try {
|
|
186
|
+
JSON.parse(input);
|
|
187
|
+
return true; // valid JSON — not the negative case we're testing
|
|
188
|
+
} catch {
|
|
189
|
+
return _parse(input).kind === 'parse-error';
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
message: 'inputs that JSON.parse rejects must yield kind: parse-error',
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'absent-id-classifies-as-notification',
|
|
196
|
+
check: (input: string, _output): boolean => {
|
|
197
|
+
// Behavioral invariant: a well-formed object with jsonrpc:'2.0'
|
|
198
|
+
// and method:string but NO id field MUST be a notification, not
|
|
199
|
+
// a request. This is the §4.1 distinction the strike force flagged.
|
|
200
|
+
let obj: unknown;
|
|
201
|
+
try {
|
|
202
|
+
obj = JSON.parse(input);
|
|
203
|
+
} catch {
|
|
204
|
+
const skipAbsentIdCase = true;
|
|
205
|
+
return skipAbsentIdCase;
|
|
206
|
+
}
|
|
207
|
+
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) return true;
|
|
208
|
+
const o = obj as Record<string, unknown>;
|
|
209
|
+
if (o.jsonrpc !== '2.0' || typeof o.method !== 'string') return true;
|
|
210
|
+
if ('id' in o && o.id !== undefined) return true; // request, not the test case
|
|
211
|
+
return _parse(input).kind === 'notification';
|
|
212
|
+
},
|
|
213
|
+
message: 'well-formed messages without an id field must classify as notifications (§4.1)',
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
run: (input: string): { kind: string } => _parse(input),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// ---------- Namespace surface (ADR-0001) ----------
|
|
220
|
+
|
|
221
|
+
/** Namespaced public surface of the kernel. */
|
|
222
|
+
export const JsonRpcServer = {
|
|
223
|
+
parse: _parse,
|
|
224
|
+
errorResponse: _errorResponse,
|
|
225
|
+
successResponse: _successResponse,
|
|
226
|
+
} as const;
|
|
227
|
+
|
|
228
|
+
export declare namespace JsonRpcServer {
|
|
229
|
+
/** Discriminated parse outcome. */
|
|
230
|
+
export type Outcome = ParseOutcome;
|
|
231
|
+
/** Wire-shape request (§4). */
|
|
232
|
+
export type Request = JsonRpcRequest;
|
|
233
|
+
/** Wire-shape notification (§4.1). */
|
|
234
|
+
export type Notification = JsonRpcNotification;
|
|
235
|
+
/** Wire-shape response (§5). */
|
|
236
|
+
export type Response = JsonRpcResponse;
|
|
237
|
+
/** Wire-shape success response. */
|
|
238
|
+
export type Success = JsonRpcSuccess;
|
|
239
|
+
/** Wire-shape error response. */
|
|
240
|
+
export type Error = JsonRpcErrorResponse;
|
|
241
|
+
/** Id type per §4. */
|
|
242
|
+
export type Id = JsonRpcId;
|
|
243
|
+
}
|
package/src/start.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* start — pick an MCP transport. Default is stdio; pass `{ http: ':3838' }`
|
|
3
|
+
* to bind HTTP instead.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { runStdio } from './stdio.js';
|
|
9
|
+
|
|
10
|
+
/** Options for `start`. */
|
|
11
|
+
export interface StartOpts {
|
|
12
|
+
readonly http?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Start the MCP server on the requested transport. */
|
|
16
|
+
export async function start(opts: StartOpts = {}): Promise<void> {
|
|
17
|
+
if (opts.http !== undefined) {
|
|
18
|
+
const { runHttp } = await import('./http.js');
|
|
19
|
+
await runHttp(opts.http);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
await runStdio();
|
|
23
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
|
|
12
|
+
import { runStdio } from './stdio.js';
|
|
13
|
+
|
|
14
|
+
if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith('stdio-server.ts') || process.argv[1]?.endsWith('stdio.ts')) {
|
|
15
|
+
runStdio().catch((err: unknown) => {
|
|
16
|
+
process.stderr.write(JSON.stringify({ error: String(err) }) + '\n');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
});
|
|
19
|
+
}
|
package/src/stdio.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
|
|
10
|
+
import { createInterface } from 'node:readline/promises';
|
|
11
|
+
import type { Readable, Writable } from 'node:stream';
|
|
12
|
+
import { handleRequest } from './http.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Process a single JSON-RPC stdio line and return the wire payload (a
|
|
16
|
+
* JSON-encoded string) or `null` when no response should be emitted
|
|
17
|
+
* (notification or pure-notification batch). Empty/whitespace-only lines
|
|
18
|
+
* are also `null` so the stdio loop can skip them silently.
|
|
19
|
+
*
|
|
20
|
+
* Exported so unit tests cover the exact line-handling logic without
|
|
21
|
+
* spinning up a child process and pumping stdin.
|
|
22
|
+
*/
|
|
23
|
+
export async function processLine(line: string): Promise<string | null> {
|
|
24
|
+
if (!line.trim()) return null;
|
|
25
|
+
const response = await handleRequest(line);
|
|
26
|
+
if (response === null) return null;
|
|
27
|
+
return JSON.stringify(response);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Run the MCP stdio loop until the input stream closes. Defaults to
|
|
32
|
+
* `process.stdin` / `process.stdout` so the production CLI bootstrap
|
|
33
|
+
* stays a one-liner (`runStdio()`); tests inject a pre-populated
|
|
34
|
+
* Readable + a sink Writable to exercise the full read-line-write loop
|
|
35
|
+
* without spawning a child process.
|
|
36
|
+
*/
|
|
37
|
+
export async function runStdio(
|
|
38
|
+
input: Readable = process.stdin,
|
|
39
|
+
output: Writable = process.stdout,
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
const rl = createInterface({ input });
|
|
42
|
+
for await (const line of rl) {
|
|
43
|
+
const wire = await processLine(line);
|
|
44
|
+
if (wire !== null) {
|
|
45
|
+
output.write(wire + '\n');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Side-effect import installs the tsx direct-invoke guard so the integration
|
|
51
|
+
// spawn (`tsx packages/mcp-server/src/stdio.ts`) keeps working. Bootstrap
|
|
52
|
+
// lives in `./stdio-server.ts` because Windows-spawn coverage can't be
|
|
53
|
+
// merged back through c8 ignore (source-mapped TS line numbers don't match).
|
|
54
|
+
import './stdio-server.js';
|