frontmcp 1.2.1 → 1.4.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/README.md +38 -29
- package/package.json +4 -4
- package/src/commands/build/exec/bin-meta.d.ts +49 -0
- package/src/commands/build/exec/bin-meta.js +68 -0
- package/src/commands/build/exec/bin-meta.js.map +1 -0
- package/src/commands/build/exec/cli-runtime/generate-cli-entry.js +195 -3
- package/src/commands/build/exec/cli-runtime/generate-cli-entry.js.map +1 -1
- package/src/commands/build/exec/cli-runtime/plugin-emitter.d.ts +160 -0
- package/src/commands/build/exec/cli-runtime/plugin-emitter.js +512 -0
- package/src/commands/build/exec/cli-runtime/plugin-emitter.js.map +1 -0
- package/src/commands/build/exec/cli-runtime/schema-extractor.d.ts +13 -1
- package/src/commands/build/exec/cli-runtime/schema-extractor.js +29 -3
- package/src/commands/build/exec/cli-runtime/schema-extractor.js.map +1 -1
- package/src/commands/build/exec/cli-runtime/skill-md-compose.d.ts +25 -0
- package/src/commands/build/exec/cli-runtime/skill-md-compose.js +63 -0
- package/src/commands/build/exec/cli-runtime/skill-md-compose.js.map +1 -0
- package/src/commands/build/exec/index.js +26 -0
- package/src/commands/build/exec/index.js.map +1 -1
- package/src/commands/build/exec/runner-script.js +16 -4
- package/src/commands/build/exec/runner-script.js.map +1 -1
- package/src/commands/dev/bridge/child-supervisor.d.ts +48 -0
- package/src/commands/dev/bridge/child-supervisor.js +228 -0
- package/src/commands/dev/bridge/child-supervisor.js.map +1 -0
- package/src/commands/dev/bridge/errors.d.ts +23 -0
- package/src/commands/dev/bridge/errors.js +34 -0
- package/src/commands/dev/bridge/errors.js.map +1 -0
- package/src/commands/dev/bridge/index.d.ts +30 -0
- package/src/commands/dev/bridge/index.js +220 -0
- package/src/commands/dev/bridge/index.js.map +1 -0
- package/src/commands/dev/bridge/log.d.ts +29 -0
- package/src/commands/dev/bridge/log.js +82 -0
- package/src/commands/dev/bridge/log.js.map +1 -0
- package/src/commands/dev/bridge/state-machine.d.ts +56 -0
- package/src/commands/dev/bridge/state-machine.js +245 -0
- package/src/commands/dev/bridge/state-machine.js.map +1 -0
- package/src/commands/dev/bridge/stdio-framer.d.ts +47 -0
- package/src/commands/dev/bridge/stdio-framer.js +128 -0
- package/src/commands/dev/bridge/stdio-framer.js.map +1 -0
- package/src/commands/dev/bridge/upstream-client.d.ts +49 -0
- package/src/commands/dev/bridge/upstream-client.js +159 -0
- package/src/commands/dev/bridge/upstream-client.js.map +1 -0
- package/src/commands/dev/bridge/watcher.d.ts +30 -0
- package/src/commands/dev/bridge/watcher.js +87 -0
- package/src/commands/dev/bridge/watcher.js.map +1 -0
- package/src/commands/dev/dev.d.ts +34 -1
- package/src/commands/dev/dev.js +168 -14
- package/src/commands/dev/dev.js.map +1 -1
- package/src/commands/dev/inspector.d.ts +13 -1
- package/src/commands/dev/inspector.js +77 -3
- package/src/commands/dev/inspector.js.map +1 -1
- package/src/commands/dev/port.d.ts +23 -0
- package/src/commands/dev/port.js +87 -0
- package/src/commands/dev/port.js.map +1 -0
- package/src/commands/dev/register.d.ts +1 -1
- package/src/commands/dev/register.js +28 -4
- package/src/commands/dev/register.js.map +1 -1
- package/src/commands/dev/test.d.ts +26 -1
- package/src/commands/dev/test.js +181 -64
- package/src/commands/dev/test.js.map +1 -1
- package/src/commands/eject/mcp-client.d.ts +25 -0
- package/src/commands/eject/mcp-client.js +74 -0
- package/src/commands/eject/mcp-client.js.map +1 -0
- package/src/commands/eject/register.d.ts +9 -0
- package/src/commands/eject/register.js +56 -0
- package/src/commands/eject/register.js.map +1 -0
- package/src/commands/install/install-claude-plugin.d.ts +13 -0
- package/src/commands/install/install-claude-plugin.js +327 -0
- package/src/commands/install/install-claude-plugin.js.map +1 -0
- package/src/commands/install/register.d.ts +16 -0
- package/src/commands/install/register.js +70 -0
- package/src/commands/install/register.js.map +1 -0
- package/src/commands/scaffold/create.js +52 -8
- package/src/commands/scaffold/create.js.map +1 -1
- package/src/commands/skills/from-entry.d.ts +31 -0
- package/src/commands/skills/from-entry.js +68 -0
- package/src/commands/skills/from-entry.js.map +1 -0
- package/src/commands/skills/install.d.ts +12 -0
- package/src/commands/skills/install.js +173 -8
- package/src/commands/skills/install.js.map +1 -1
- package/src/commands/skills/register.js +7 -3
- package/src/commands/skills/register.js.map +1 -1
- package/src/config/frontmcp-config.loader.d.ts +28 -0
- package/src/config/frontmcp-config.loader.js +146 -67
- package/src/config/frontmcp-config.loader.js.map +1 -1
- package/src/config/frontmcp-config.resolve.d.ts +67 -0
- package/src/config/frontmcp-config.resolve.js +118 -0
- package/src/config/frontmcp-config.resolve.js.map +1 -0
- package/src/config/frontmcp-config.schema.d.ts +207 -0
- package/src/config/frontmcp-config.schema.js +217 -1
- package/src/config/frontmcp-config.schema.js.map +1 -1
- package/src/config/frontmcp-config.types.d.ts +133 -0
- package/src/config/frontmcp-config.types.js.map +1 -1
- package/src/config/index.d.ts +2 -1
- package/src/config/index.js +3 -1
- package/src/config/index.js.map +1 -1
- package/src/core/args.d.ts +13 -0
- package/src/core/args.js.map +1 -1
- package/src/core/bridge.js +39 -0
- package/src/core/bridge.js.map +1 -1
- package/src/core/cli.d.ts +0 -6
- package/src/core/cli.js +23 -3
- package/src/core/cli.js.map +1 -1
- package/src/core/help.d.ts +1 -1
- package/src/core/help.js +27 -6
- package/src/core/help.js.map +1 -1
- package/src/core/program.d.ts +1 -1
- package/src/core/program.js +56 -12
- package/src/core/program.js.map +1 -1
- package/src/core/project-commands.d.ts +44 -0
- package/src/core/project-commands.js +216 -0
- package/src/core/project-commands.js.map +1 -0
- package/src/core/tsconfig.d.ts +20 -0
- package/src/core/tsconfig.js +41 -2
- package/src/core/tsconfig.js.map +1 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Bridge state machine (issue #399).
|
|
4
|
+
*
|
|
5
|
+
* Idle → Booting → Ready ⇄ Reloading
|
|
6
|
+
* ↓ deadline
|
|
7
|
+
* Degraded
|
|
8
|
+
*
|
|
9
|
+
* Owns the request buffer that absorbs inbound JSON-RPC frames while the
|
|
10
|
+
* upstream child is mid-restart. When buffered requests exceed the
|
|
11
|
+
* configured cap (default 8), the FSM synthesises an immediate
|
|
12
|
+
* `dev_buffer_full` response so clients never silently lose frames.
|
|
13
|
+
*
|
|
14
|
+
* The FSM is transport-agnostic — `child-supervisor` and `upstream-client`
|
|
15
|
+
* call into it; this module never touches sockets, child processes, or
|
|
16
|
+
* file descriptors.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.createBridgeStateMachine = createBridgeStateMachine;
|
|
20
|
+
const errors_1 = require("./errors");
|
|
21
|
+
function createBridgeStateMachine(options) {
|
|
22
|
+
const { log, bufferSize, reloadDeadlineMs, respond, forward } = options;
|
|
23
|
+
let state = 'Idle';
|
|
24
|
+
const buffer = [];
|
|
25
|
+
const inflight = new Map();
|
|
26
|
+
let reloadTimer;
|
|
27
|
+
// Monotonic token bumped every time the FSM leaves Ready. The async
|
|
28
|
+
// drain loop started in onChildReady() captures the token at start
|
|
29
|
+
// and bails out as soon as it changes — without this guard a watcher
|
|
30
|
+
// event mid-drain would let buffered frames forward into the old
|
|
31
|
+
// child (now being killed), producing stranded inflight entries and
|
|
32
|
+
// potentially duplicate responses.
|
|
33
|
+
let readyGen = 0;
|
|
34
|
+
function transition(next, info) {
|
|
35
|
+
if (state === next)
|
|
36
|
+
return;
|
|
37
|
+
log.info('state-transition', { from: state, to: next, ...info });
|
|
38
|
+
state = next;
|
|
39
|
+
}
|
|
40
|
+
function clearReloadTimer() {
|
|
41
|
+
if (reloadTimer) {
|
|
42
|
+
clearTimeout(reloadTimer);
|
|
43
|
+
reloadTimer = undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function flushBufferAsResponses(code, reason, data) {
|
|
47
|
+
while (buffer.length > 0) {
|
|
48
|
+
const f = buffer.shift();
|
|
49
|
+
if (!f)
|
|
50
|
+
break;
|
|
51
|
+
const id = f.id ?? null;
|
|
52
|
+
// Notifications (no id) can't get a response — drop with a log line.
|
|
53
|
+
if (id === null) {
|
|
54
|
+
log.warn('drop-notification', { method: f.method, reason });
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
await respond((0, errors_1.makeDevError)(id, code, { reason, ...data }));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function failInflightAsResponses(reason, data) {
|
|
61
|
+
for (const [id, req] of inflight) {
|
|
62
|
+
const idVal = typeof id === 'number' ? id : id;
|
|
63
|
+
await respond((0, errors_1.makeDevError)(idVal, errors_1.DEV_SERVER_UNREACHABLE, { reason, ...data, method: req.frame.method }));
|
|
64
|
+
}
|
|
65
|
+
inflight.clear();
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
get state() {
|
|
69
|
+
return state;
|
|
70
|
+
},
|
|
71
|
+
bufferDepth: () => buffer.length,
|
|
72
|
+
onBootStart() {
|
|
73
|
+
transition('Booting');
|
|
74
|
+
},
|
|
75
|
+
onChildReady() {
|
|
76
|
+
clearReloadTimer();
|
|
77
|
+
transition('Ready', { bufferDepth: buffer.length });
|
|
78
|
+
// Drain buffered requests in FIFO; preserve order on the wire.
|
|
79
|
+
const drain = [...buffer];
|
|
80
|
+
buffer.length = 0;
|
|
81
|
+
const drainGen = readyGen;
|
|
82
|
+
void (async () => {
|
|
83
|
+
for (const f of drain) {
|
|
84
|
+
// Bail out if the FSM left Ready (watcher event, child exit,
|
|
85
|
+
// or stop()) — otherwise this drain would forward into a
|
|
86
|
+
// child that's already being killed and duplicate responses
|
|
87
|
+
// already sent by `failInflightAsResponses`.
|
|
88
|
+
if (readyGen !== drainGen)
|
|
89
|
+
return;
|
|
90
|
+
const requestId = typeof f.id === 'string' || typeof f.id === 'number' ? f.id : null;
|
|
91
|
+
// Re-mark as inflight when forwarding; relayUpstream will clear it.
|
|
92
|
+
if (requestId !== null) {
|
|
93
|
+
inflight.set(requestId, { frame: f, forwardedAt: Date.now() });
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
await forward(f);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
log.error('forward-failed', { error: err.message });
|
|
100
|
+
// Mirror the live-enqueue failure path: a request that never
|
|
101
|
+
// reached the child must still resolve on the wire, otherwise
|
|
102
|
+
// the MCP client sits on `Calling…` forever. Notifications
|
|
103
|
+
// (`requestId === null`) have no caller waiting on a response.
|
|
104
|
+
// Only respond if we still own this generation — otherwise
|
|
105
|
+
// failInflightAsResponses() already sent the error.
|
|
106
|
+
if (requestId !== null && readyGen === drainGen) {
|
|
107
|
+
inflight.delete(requestId);
|
|
108
|
+
await respond((0, errors_1.makeDevError)(requestId, errors_1.DEV_SERVER_UNREACHABLE, { reason: 'forward_failed' }));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
})();
|
|
113
|
+
},
|
|
114
|
+
onChildExit(reason) {
|
|
115
|
+
log.warn('child-exit', { reason, state });
|
|
116
|
+
if (state === 'Stopping')
|
|
117
|
+
return; // expected
|
|
118
|
+
// Treat as a reload trigger if we were Ready; otherwise stay where we are.
|
|
119
|
+
if (state === 'Ready' || state === 'Booting') {
|
|
120
|
+
// Invalidate any in-progress drain that captured the prior readyGen.
|
|
121
|
+
readyGen++;
|
|
122
|
+
transition('Reloading', { trigger: 'child-exit', reason });
|
|
123
|
+
// Inflight requests will never get a real response — synthesise one.
|
|
124
|
+
void failInflightAsResponses('child_exit', { reason });
|
|
125
|
+
scheduleReloadDeadline();
|
|
126
|
+
}
|
|
127
|
+
else if (state === 'Reloading') {
|
|
128
|
+
// Two exits in a row — keep waiting for boot
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
onWatcherEvent(trigger) {
|
|
132
|
+
if (state === 'Stopping' || state === 'Degraded')
|
|
133
|
+
return;
|
|
134
|
+
log.reloadEvent('start', { trigger });
|
|
135
|
+
// Invalidate any in-progress drain so it stops forwarding into
|
|
136
|
+
// the child that's about to be killed.
|
|
137
|
+
readyGen++;
|
|
138
|
+
transition('Reloading', { trigger });
|
|
139
|
+
// Inflight requests will likely be killed when supervisor kills the
|
|
140
|
+
// child. Respond now so the client spinner clears immediately.
|
|
141
|
+
void failInflightAsResponses('reload', { trigger });
|
|
142
|
+
scheduleReloadDeadline();
|
|
143
|
+
},
|
|
144
|
+
onReloadDeadline() {
|
|
145
|
+
log.error('reload-deadline-elapsed', { bufferDepth: buffer.length });
|
|
146
|
+
transition('Degraded', { reason: 'reload_deadline' });
|
|
147
|
+
// Deadline path → DEV_RELOAD_DEADLINE (not DEV_SERVER_UNREACHABLE).
|
|
148
|
+
// The two map to distinct public error codes so clients can
|
|
149
|
+
// distinguish "watcher reload took too long" from "child crashed".
|
|
150
|
+
void flushBufferAsResponses(errors_1.DEV_RELOAD_DEADLINE, 'deadline', { deadlineMs: reloadDeadlineMs });
|
|
151
|
+
},
|
|
152
|
+
async enqueue(frame) {
|
|
153
|
+
const isRequest = frame.id !== undefined && frame.id !== null;
|
|
154
|
+
if (state === 'Ready') {
|
|
155
|
+
if (isRequest) {
|
|
156
|
+
inflight.set(frame.id, { frame, forwardedAt: Date.now() });
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
await forward(frame);
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
log.error('forward-failed', { error: err.message });
|
|
163
|
+
if (isRequest) {
|
|
164
|
+
inflight.delete(frame.id);
|
|
165
|
+
await respond((0, errors_1.makeDevError)(frame.id ?? null, errors_1.DEV_SERVER_UNREACHABLE, { reason: 'forward_failed' }));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (state === 'Degraded' || state === 'Stopping') {
|
|
171
|
+
// Both states are terminal for new traffic — Degraded is post-
|
|
172
|
+
// deadline (user code is broken), Stopping means SIGINT/SIGTERM
|
|
173
|
+
// already fired. Buffering here is wrong because nothing will
|
|
174
|
+
// drain it. Reject so the client spinner clears immediately.
|
|
175
|
+
if (isRequest) {
|
|
176
|
+
await respond((0, errors_1.makeDevError)(frame.id ?? null, errors_1.DEV_SERVER_UNREACHABLE, {
|
|
177
|
+
reason: state === 'Stopping' ? 'stopping' : 'degraded',
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
log.warn('drop-notification', {
|
|
182
|
+
method: frame.method,
|
|
183
|
+
reason: state === 'Stopping' ? 'stopping' : 'degraded',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// Idle / Booting / Reloading → buffer (will drain on onChildReady)
|
|
189
|
+
if (buffer.length >= bufferSize) {
|
|
190
|
+
if (isRequest) {
|
|
191
|
+
await respond((0, errors_1.makeDevError)(frame.id ?? null, errors_1.DEV_BUFFER_FULL, { capacity: bufferSize }));
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
log.warn('drop-notification', { method: frame.method, reason: 'buffer_full' });
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
buffer.push(frame);
|
|
199
|
+
},
|
|
200
|
+
async relayUpstream(frame) {
|
|
201
|
+
// Clear inflight if this is a response to a known id
|
|
202
|
+
if (frame.id !== undefined && frame.id !== null && (frame.result !== undefined || frame.error !== undefined)) {
|
|
203
|
+
inflight.delete(frame.id);
|
|
204
|
+
}
|
|
205
|
+
await respond(frame);
|
|
206
|
+
},
|
|
207
|
+
async stop() {
|
|
208
|
+
// Invalidate any in-progress drain — same reason as the watcher /
|
|
209
|
+
// child-exit paths: avoid forwarding into a child we're tearing
|
|
210
|
+
// down.
|
|
211
|
+
readyGen++;
|
|
212
|
+
transition('Stopping');
|
|
213
|
+
clearReloadTimer();
|
|
214
|
+
// Inflight + buffered: respond once so the client's pending RPCs don't
|
|
215
|
+
// dangle past the bridge exit.
|
|
216
|
+
await failInflightAsResponses('stopping');
|
|
217
|
+
await flushBufferAsResponses(errors_1.DEV_SERVER_UNREACHABLE, 'stopping');
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
function scheduleReloadDeadline() {
|
|
221
|
+
clearReloadTimer();
|
|
222
|
+
reloadTimer = setTimeout(() => {
|
|
223
|
+
if (state === 'Reloading' || state === 'Booting') {
|
|
224
|
+
// Mark deadline reached — supervisor stays alive, watcher retries.
|
|
225
|
+
log.error('reload-deadline-fired');
|
|
226
|
+
transition('Degraded', { reason: 'reload_deadline' });
|
|
227
|
+
// Drain buffer with deadline-shaped error.
|
|
228
|
+
void (async () => {
|
|
229
|
+
while (buffer.length > 0) {
|
|
230
|
+
const f = buffer.shift();
|
|
231
|
+
if (!f)
|
|
232
|
+
break;
|
|
233
|
+
const id = f.id ?? null;
|
|
234
|
+
if (id === null) {
|
|
235
|
+
log.warn('drop-notification', { method: f.method, reason: 'deadline' });
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
await respond((0, errors_1.makeDevError)(id, errors_1.DEV_RELOAD_DEADLINE, { deadlineMs: reloadDeadlineMs }));
|
|
239
|
+
}
|
|
240
|
+
})();
|
|
241
|
+
}
|
|
242
|
+
}, reloadDeadlineMs).unref();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=state-machine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state-machine.js","sourceRoot":"","sources":["../../../../../src/commands/dev/bridge/state-machine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Bridge state machine (issue #399).\n *\n * Idle → Booting → Ready ⇄ Reloading\n * ↓ deadline\n * Degraded\n *\n * Owns the request buffer that absorbs inbound JSON-RPC frames while the\n * upstream child is mid-restart. When buffered requests exceed the\n * configured cap (default 8), the FSM synthesises an immediate\n * `dev_buffer_full` response so clients never silently lose frames.\n *\n * The FSM is transport-agnostic — `child-supervisor` and `upstream-client`\n * call into it; this module never touches sockets, child processes, or\n * file descriptors.\n */\n\nimport { DEV_BUFFER_FULL, DEV_RELOAD_DEADLINE, DEV_SERVER_UNREACHABLE, makeDevError } from './errors';\nimport type { BridgeLogger } from './log';\nimport type { JsonRpcFrame } from './stdio-framer';\n\nexport type BridgeState = 'Idle' | 'Booting' | 'Ready' | 'Reloading' | 'Degraded' | 'Stopping';\n\nexport interface InflightRequest {\n /** Original frame (kept for replay if the child dies before responding). */\n frame: JsonRpcFrame;\n /** Wall-clock when the bridge forwarded the frame to upstream. */\n forwardedAt: number;\n}\n\nexport interface BridgeStateMachineOptions {\n log: BridgeLogger;\n bufferSize: number;\n reloadDeadlineMs: number;\n /** Send a JSON-RPC frame back to the client (stdio out). */\n respond(frame: JsonRpcFrame): void | Promise<void>;\n /** Forward a request frame to the upstream child. Implemented by the supervisor. */\n forward(frame: JsonRpcFrame): void | Promise<void>;\n}\n\nexport interface BridgeStateMachine {\n readonly state: BridgeState;\n /** Bridge has begun launching the child — buffer inbound frames. */\n onBootStart(): void;\n /** Child reports ready — drain the buffer through `forward`. */\n onChildReady(): void;\n /** Child exited unexpectedly. If we were Ready, transition to Reloading. */\n onChildExit(reason: string): void;\n /** Watcher fired. Buffer inbound, start reload timer. */\n onWatcherEvent(trigger: string): void;\n /** Reload deadline elapsed without a ready signal. */\n onReloadDeadline(): void;\n /** Inbound JSON-RPC frame from stdin. Routes to `forward` or buffers. */\n enqueue(frame: JsonRpcFrame): Promise<void>;\n /** Outbound JSON-RPC frame from upstream — relay to client. */\n relayUpstream(frame: JsonRpcFrame): Promise<void>;\n /** SIGTERM/SIGINT — flush buffer with `dev_server_unreachable` and stop. */\n stop(): Promise<void>;\n /** Number of frames currently buffered (testing hook). */\n bufferDepth(): number;\n}\n\nexport function createBridgeStateMachine(options: BridgeStateMachineOptions): BridgeStateMachine {\n const { log, bufferSize, reloadDeadlineMs, respond, forward } = options;\n let state: BridgeState = 'Idle';\n const buffer: JsonRpcFrame[] = [];\n const inflight = new Map<string | number, InflightRequest>();\n let reloadTimer: NodeJS.Timeout | undefined;\n // Monotonic token bumped every time the FSM leaves Ready. The async\n // drain loop started in onChildReady() captures the token at start\n // and bails out as soon as it changes — without this guard a watcher\n // event mid-drain would let buffered frames forward into the old\n // child (now being killed), producing stranded inflight entries and\n // potentially duplicate responses.\n let readyGen = 0;\n\n function transition(next: BridgeState, info?: Record<string, unknown>): void {\n if (state === next) return;\n log.info('state-transition', { from: state, to: next, ...info });\n state = next;\n }\n\n function clearReloadTimer(): void {\n if (reloadTimer) {\n clearTimeout(reloadTimer);\n reloadTimer = undefined;\n }\n }\n\n async function flushBufferAsResponses(code: number, reason: string, data?: Record<string, unknown>): Promise<void> {\n while (buffer.length > 0) {\n const f = buffer.shift();\n if (!f) break;\n const id = f.id ?? null;\n // Notifications (no id) can't get a response — drop with a log line.\n if (id === null) {\n log.warn('drop-notification', { method: f.method, reason });\n continue;\n }\n await respond(makeDevError(id, code, { reason, ...data }));\n }\n }\n\n async function failInflightAsResponses(reason: string, data?: Record<string, unknown>): Promise<void> {\n for (const [id, req] of inflight) {\n const idVal = typeof id === 'number' ? id : (id as string);\n await respond(makeDevError(idVal, DEV_SERVER_UNREACHABLE, { reason, ...data, method: req.frame.method }));\n }\n inflight.clear();\n }\n\n return {\n get state() {\n return state;\n },\n bufferDepth: () => buffer.length,\n\n onBootStart() {\n transition('Booting');\n },\n\n onChildReady() {\n clearReloadTimer();\n transition('Ready', { bufferDepth: buffer.length });\n // Drain buffered requests in FIFO; preserve order on the wire.\n const drain = [...buffer];\n buffer.length = 0;\n const drainGen = readyGen;\n void (async () => {\n for (const f of drain) {\n // Bail out if the FSM left Ready (watcher event, child exit,\n // or stop()) — otherwise this drain would forward into a\n // child that's already being killed and duplicate responses\n // already sent by `failInflightAsResponses`.\n if (readyGen !== drainGen) return;\n const requestId = typeof f.id === 'string' || typeof f.id === 'number' ? f.id : null;\n // Re-mark as inflight when forwarding; relayUpstream will clear it.\n if (requestId !== null) {\n inflight.set(requestId, { frame: f, forwardedAt: Date.now() });\n }\n try {\n await forward(f);\n } catch (err) {\n log.error('forward-failed', { error: (err as Error).message });\n // Mirror the live-enqueue failure path: a request that never\n // reached the child must still resolve on the wire, otherwise\n // the MCP client sits on `Calling…` forever. Notifications\n // (`requestId === null`) have no caller waiting on a response.\n // Only respond if we still own this generation — otherwise\n // failInflightAsResponses() already sent the error.\n if (requestId !== null && readyGen === drainGen) {\n inflight.delete(requestId);\n await respond(makeDevError(requestId, DEV_SERVER_UNREACHABLE, { reason: 'forward_failed' }));\n }\n }\n }\n })();\n },\n\n onChildExit(reason) {\n log.warn('child-exit', { reason, state });\n if (state === 'Stopping') return; // expected\n // Treat as a reload trigger if we were Ready; otherwise stay where we are.\n if (state === 'Ready' || state === 'Booting') {\n // Invalidate any in-progress drain that captured the prior readyGen.\n readyGen++;\n transition('Reloading', { trigger: 'child-exit', reason });\n // Inflight requests will never get a real response — synthesise one.\n void failInflightAsResponses('child_exit', { reason });\n scheduleReloadDeadline();\n } else if (state === 'Reloading') {\n // Two exits in a row — keep waiting for boot\n }\n },\n\n onWatcherEvent(trigger) {\n if (state === 'Stopping' || state === 'Degraded') return;\n log.reloadEvent('start', { trigger });\n // Invalidate any in-progress drain so it stops forwarding into\n // the child that's about to be killed.\n readyGen++;\n transition('Reloading', { trigger });\n // Inflight requests will likely be killed when supervisor kills the\n // child. Respond now so the client spinner clears immediately.\n void failInflightAsResponses('reload', { trigger });\n scheduleReloadDeadline();\n },\n\n onReloadDeadline() {\n log.error('reload-deadline-elapsed', { bufferDepth: buffer.length });\n transition('Degraded', { reason: 'reload_deadline' });\n // Deadline path → DEV_RELOAD_DEADLINE (not DEV_SERVER_UNREACHABLE).\n // The two map to distinct public error codes so clients can\n // distinguish \"watcher reload took too long\" from \"child crashed\".\n void flushBufferAsResponses(DEV_RELOAD_DEADLINE, 'deadline', { deadlineMs: reloadDeadlineMs });\n },\n\n async enqueue(frame: JsonRpcFrame) {\n const isRequest = frame.id !== undefined && frame.id !== null;\n\n if (state === 'Ready') {\n if (isRequest) {\n inflight.set(frame.id as string | number, { frame, forwardedAt: Date.now() });\n }\n try {\n await forward(frame);\n } catch (err) {\n log.error('forward-failed', { error: (err as Error).message });\n if (isRequest) {\n inflight.delete(frame.id as string | number);\n await respond(makeDevError(frame.id ?? null, DEV_SERVER_UNREACHABLE, { reason: 'forward_failed' }));\n }\n }\n return;\n }\n\n if (state === 'Degraded' || state === 'Stopping') {\n // Both states are terminal for new traffic — Degraded is post-\n // deadline (user code is broken), Stopping means SIGINT/SIGTERM\n // already fired. Buffering here is wrong because nothing will\n // drain it. Reject so the client spinner clears immediately.\n if (isRequest) {\n await respond(\n makeDevError(frame.id ?? null, DEV_SERVER_UNREACHABLE, {\n reason: state === 'Stopping' ? 'stopping' : 'degraded',\n }),\n );\n } else {\n log.warn('drop-notification', {\n method: frame.method,\n reason: state === 'Stopping' ? 'stopping' : 'degraded',\n });\n }\n return;\n }\n\n // Idle / Booting / Reloading → buffer (will drain on onChildReady)\n if (buffer.length >= bufferSize) {\n if (isRequest) {\n await respond(makeDevError(frame.id ?? null, DEV_BUFFER_FULL, { capacity: bufferSize }));\n } else {\n log.warn('drop-notification', { method: frame.method, reason: 'buffer_full' });\n }\n return;\n }\n buffer.push(frame);\n },\n\n async relayUpstream(frame: JsonRpcFrame) {\n // Clear inflight if this is a response to a known id\n if (frame.id !== undefined && frame.id !== null && (frame.result !== undefined || frame.error !== undefined)) {\n inflight.delete(frame.id);\n }\n await respond(frame);\n },\n\n async stop() {\n // Invalidate any in-progress drain — same reason as the watcher /\n // child-exit paths: avoid forwarding into a child we're tearing\n // down.\n readyGen++;\n transition('Stopping');\n clearReloadTimer();\n // Inflight + buffered: respond once so the client's pending RPCs don't\n // dangle past the bridge exit.\n await failInflightAsResponses('stopping');\n await flushBufferAsResponses(DEV_SERVER_UNREACHABLE, 'stopping');\n },\n };\n\n function scheduleReloadDeadline(): void {\n clearReloadTimer();\n reloadTimer = setTimeout(() => {\n if (state === 'Reloading' || state === 'Booting') {\n // Mark deadline reached — supervisor stays alive, watcher retries.\n log.error('reload-deadline-fired');\n transition('Degraded', { reason: 'reload_deadline' });\n // Drain buffer with deadline-shaped error.\n void (async () => {\n while (buffer.length > 0) {\n const f = buffer.shift();\n if (!f) break;\n const id = f.id ?? null;\n if (id === null) {\n log.warn('drop-notification', { method: f.method, reason: 'deadline' });\n continue;\n }\n await respond(makeDevError(id, DEV_RELOAD_DEADLINE, { deadlineMs: reloadDeadlineMs }));\n }\n })();\n }\n }, reloadDeadlineMs).unref();\n }\n}\n"]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newline-delimited JSON-RPC framer for the dev bridge (issue #399).
|
|
3
|
+
*
|
|
4
|
+
* Reads `process.stdin` and yields complete JSON-RPC frames. Writes
|
|
5
|
+
* complete frames to `process.stdout` (atomic per `\n` boundary). Parser
|
|
6
|
+
* state is preserved across chunks so frames split arbitrarily on the
|
|
7
|
+
* wire still parse.
|
|
8
|
+
*
|
|
9
|
+
* MCP stdio framing per spec is newline-delimited JSON (`\n`-terminated
|
|
10
|
+
* UTF-8). We do NOT implement LSP-style `Content-Length` framing — it's
|
|
11
|
+
* not in the MCP spec and adding it would silently shadow real JSON
|
|
12
|
+
* bodies that happen to start with `C`.
|
|
13
|
+
*/
|
|
14
|
+
import type { Readable, Writable } from 'node:stream';
|
|
15
|
+
import type { BridgeLogger } from './log';
|
|
16
|
+
export interface JsonRpcFrame {
|
|
17
|
+
jsonrpc: '2.0';
|
|
18
|
+
id?: string | number | null;
|
|
19
|
+
method?: string;
|
|
20
|
+
params?: unknown;
|
|
21
|
+
result?: unknown;
|
|
22
|
+
error?: {
|
|
23
|
+
code: number;
|
|
24
|
+
message: string;
|
|
25
|
+
data?: unknown;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export interface StdioFramerOptions {
|
|
29
|
+
input: Readable;
|
|
30
|
+
output: Writable;
|
|
31
|
+
log: BridgeLogger;
|
|
32
|
+
onFrame: (frame: JsonRpcFrame) => void | Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export interface StdioFramer {
|
|
35
|
+
start(): void;
|
|
36
|
+
/** Write a single frame as a newline-terminated JSON line. Resolves on `'drain'` when backpressure kicks in. */
|
|
37
|
+
write(frame: JsonRpcFrame): Promise<void>;
|
|
38
|
+
stop(): void;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build a newline-delimited JSON framer bound to the supplied streams.
|
|
42
|
+
*
|
|
43
|
+
* Parse errors (malformed JSON between newlines) emit a `-32700` Parse
|
|
44
|
+
* error response back on the output stream and continue — a malformed
|
|
45
|
+
* frame must not kill the bridge.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createStdioFramer(options: StdioFramerOptions): StdioFramer;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Newline-delimited JSON-RPC framer for the dev bridge (issue #399).
|
|
4
|
+
*
|
|
5
|
+
* Reads `process.stdin` and yields complete JSON-RPC frames. Writes
|
|
6
|
+
* complete frames to `process.stdout` (atomic per `\n` boundary). Parser
|
|
7
|
+
* state is preserved across chunks so frames split arbitrarily on the
|
|
8
|
+
* wire still parse.
|
|
9
|
+
*
|
|
10
|
+
* MCP stdio framing per spec is newline-delimited JSON (`\n`-terminated
|
|
11
|
+
* UTF-8). We do NOT implement LSP-style `Content-Length` framing — it's
|
|
12
|
+
* not in the MCP spec and adding it would silently shadow real JSON
|
|
13
|
+
* bodies that happen to start with `C`.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.createStdioFramer = createStdioFramer;
|
|
17
|
+
const errors_1 = require("./errors");
|
|
18
|
+
/**
|
|
19
|
+
* Build a newline-delimited JSON framer bound to the supplied streams.
|
|
20
|
+
*
|
|
21
|
+
* Parse errors (malformed JSON between newlines) emit a `-32700` Parse
|
|
22
|
+
* error response back on the output stream and continue — a malformed
|
|
23
|
+
* frame must not kill the bridge.
|
|
24
|
+
*/
|
|
25
|
+
function createStdioFramer(options) {
|
|
26
|
+
const { input, output, log, onFrame } = options;
|
|
27
|
+
let buffer = '';
|
|
28
|
+
// True while `output.write` is signalling backpressure. Read by `onData`
|
|
29
|
+
// to pause the inbound stream so we don't keep buffering frames when the
|
|
30
|
+
// outbound side can't keep up.
|
|
31
|
+
let paused = false;
|
|
32
|
+
let drainResolvers = [];
|
|
33
|
+
function flushBuffer() {
|
|
34
|
+
let nl;
|
|
35
|
+
while ((nl = buffer.indexOf('\n')) >= 0) {
|
|
36
|
+
const raw = buffer.slice(0, nl).replace(/\r$/, '');
|
|
37
|
+
buffer = buffer.slice(nl + 1);
|
|
38
|
+
if (raw.length === 0)
|
|
39
|
+
continue;
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(raw);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
log.warn('parse-error', { raw: raw.slice(0, 200), error: err.message });
|
|
46
|
+
// `makeDevError` returns a strict JSON-RPC error shape that
|
|
47
|
+
// structurally satisfies `JsonRpcFrame`; the dev bridge owns both
|
|
48
|
+
// sides of this constructor, so write() trusts the payload.
|
|
49
|
+
void write((0, errors_1.makeDevError)(null, -32700, { reason: 'parse_error' }));
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
53
|
+
// Per JSON-RPC 2.0, every frame must be an object. A bare `42` or
|
|
54
|
+
// `"string"` line is just as invalid as garbage JSON — emit the
|
|
55
|
+
// same -32700 envelope so the client doesn't get inconsistent
|
|
56
|
+
// behaviour depending on whether the parser stage or the shape
|
|
57
|
+
// check rejected the input.
|
|
58
|
+
log.warn('parse-error', { raw: raw.slice(0, 200), reason: 'not_object' });
|
|
59
|
+
void write((0, errors_1.makeDevError)(null, -32700, { reason: 'not_object' }));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
// Don't drop the onFrame promise. `onFrame` is user-supplied (the
|
|
63
|
+
// state machine in the production wiring) and can reject — if we
|
|
64
|
+
// void the rejection, Node bubbles it as an unhandledRejection and
|
|
65
|
+
// crashes the bridge. Funnel it through the logger instead.
|
|
66
|
+
void Promise.resolve(onFrame(parsed)).catch((err) => {
|
|
67
|
+
log.error('on-frame-error', {
|
|
68
|
+
error: err instanceof Error ? err.message : String(err),
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function onData(chunk) {
|
|
74
|
+
buffer += typeof chunk === 'string' ? chunk : chunk.toString('utf-8');
|
|
75
|
+
flushBuffer();
|
|
76
|
+
}
|
|
77
|
+
function write(frame) {
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
const line = JSON.stringify(frame) + '\n';
|
|
80
|
+
const ok = output.write(line);
|
|
81
|
+
if (ok)
|
|
82
|
+
return resolve();
|
|
83
|
+
// Backpressure: pause inbound parsing AND the input stream so we
|
|
84
|
+
// stop accumulating frames in `buffer` until drain. `input.pause()`
|
|
85
|
+
// is a no-op on streams that don't support flow-mode pausing.
|
|
86
|
+
if (!paused) {
|
|
87
|
+
paused = true;
|
|
88
|
+
input.pause?.();
|
|
89
|
+
}
|
|
90
|
+
drainResolvers.push(resolve);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function onDrain() {
|
|
94
|
+
if (paused) {
|
|
95
|
+
paused = false;
|
|
96
|
+
input.resume?.();
|
|
97
|
+
}
|
|
98
|
+
const resolvers = drainResolvers;
|
|
99
|
+
drainResolvers = [];
|
|
100
|
+
for (const r of resolvers)
|
|
101
|
+
r();
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
start: () => {
|
|
105
|
+
input.setEncoding?.('utf-8');
|
|
106
|
+
input.on('data', onData);
|
|
107
|
+
output.on('drain', onDrain);
|
|
108
|
+
},
|
|
109
|
+
write,
|
|
110
|
+
stop: () => {
|
|
111
|
+
input.off('data', onData);
|
|
112
|
+
output.off('drain', onDrain);
|
|
113
|
+
// Settle any queued write() promises so callers blocked on
|
|
114
|
+
// backpressure don't hang past shutdown. Resume the input stream
|
|
115
|
+
// symmetrically (we may have paused it on backpressure) so the
|
|
116
|
+
// caller doesn't inherit a paused stdin.
|
|
117
|
+
if (paused) {
|
|
118
|
+
paused = false;
|
|
119
|
+
input.resume?.();
|
|
120
|
+
}
|
|
121
|
+
const resolvers = drainResolvers;
|
|
122
|
+
drainResolvers = [];
|
|
123
|
+
for (const r of resolvers)
|
|
124
|
+
r();
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=stdio-framer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio-framer.js","sourceRoot":"","sources":["../../../../../src/commands/dev/bridge/stdio-framer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Newline-delimited JSON-RPC framer for the dev bridge (issue #399).\n *\n * Reads `process.stdin` and yields complete JSON-RPC frames. Writes\n * complete frames to `process.stdout` (atomic per `\\n` boundary). Parser\n * state is preserved across chunks so frames split arbitrarily on the\n * wire still parse.\n *\n * MCP stdio framing per spec is newline-delimited JSON (`\\n`-terminated\n * UTF-8). We do NOT implement LSP-style `Content-Length` framing — it's\n * not in the MCP spec and adding it would silently shadow real JSON\n * bodies that happen to start with `C`.\n */\n\nimport type { Readable, Writable } from 'node:stream';\n\nimport { makeDevError } from './errors';\nimport type { BridgeLogger } from './log';\n\nexport interface JsonRpcFrame {\n jsonrpc: '2.0';\n id?: string | number | null;\n method?: string;\n params?: unknown;\n result?: unknown;\n error?: { code: number; message: string; data?: unknown };\n}\n\nexport interface StdioFramerOptions {\n input: Readable;\n output: Writable;\n log: BridgeLogger;\n onFrame: (frame: JsonRpcFrame) => void | Promise<void>;\n}\n\nexport interface StdioFramer {\n start(): void;\n /** Write a single frame as a newline-terminated JSON line. Resolves on `'drain'` when backpressure kicks in. */\n write(frame: JsonRpcFrame): Promise<void>;\n stop(): void;\n}\n\n/**\n * Build a newline-delimited JSON framer bound to the supplied streams.\n *\n * Parse errors (malformed JSON between newlines) emit a `-32700` Parse\n * error response back on the output stream and continue — a malformed\n * frame must not kill the bridge.\n */\nexport function createStdioFramer(options: StdioFramerOptions): StdioFramer {\n const { input, output, log, onFrame } = options;\n let buffer = '';\n // True while `output.write` is signalling backpressure. Read by `onData`\n // to pause the inbound stream so we don't keep buffering frames when the\n // outbound side can't keep up.\n let paused = false;\n let drainResolvers: Array<() => void> = [];\n\n function flushBuffer(): void {\n let nl: number;\n while ((nl = buffer.indexOf('\\n')) >= 0) {\n const raw = buffer.slice(0, nl).replace(/\\r$/, '');\n buffer = buffer.slice(nl + 1);\n if (raw.length === 0) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n log.warn('parse-error', { raw: raw.slice(0, 200), error: (err as Error).message });\n // `makeDevError` returns a strict JSON-RPC error shape that\n // structurally satisfies `JsonRpcFrame`; the dev bridge owns both\n // sides of this constructor, so write() trusts the payload.\n void write(makeDevError(null, -32700, { reason: 'parse_error' }));\n continue;\n }\n if (!parsed || typeof parsed !== 'object') {\n // Per JSON-RPC 2.0, every frame must be an object. A bare `42` or\n // `\"string\"` line is just as invalid as garbage JSON — emit the\n // same -32700 envelope so the client doesn't get inconsistent\n // behaviour depending on whether the parser stage or the shape\n // check rejected the input.\n log.warn('parse-error', { raw: raw.slice(0, 200), reason: 'not_object' });\n void write(makeDevError(null, -32700, { reason: 'not_object' }));\n continue;\n }\n // Don't drop the onFrame promise. `onFrame` is user-supplied (the\n // state machine in the production wiring) and can reject — if we\n // void the rejection, Node bubbles it as an unhandledRejection and\n // crashes the bridge. Funnel it through the logger instead.\n void Promise.resolve(onFrame(parsed as JsonRpcFrame)).catch((err: unknown) => {\n log.error('on-frame-error', {\n error: err instanceof Error ? err.message : String(err),\n });\n });\n }\n }\n\n function onData(chunk: Buffer | string): void {\n buffer += typeof chunk === 'string' ? chunk : chunk.toString('utf-8');\n flushBuffer();\n }\n\n function write(frame: JsonRpcFrame): Promise<void> {\n return new Promise<void>((resolve) => {\n const line = JSON.stringify(frame) + '\\n';\n const ok = output.write(line);\n if (ok) return resolve();\n // Backpressure: pause inbound parsing AND the input stream so we\n // stop accumulating frames in `buffer` until drain. `input.pause()`\n // is a no-op on streams that don't support flow-mode pausing.\n if (!paused) {\n paused = true;\n input.pause?.();\n }\n drainResolvers.push(resolve);\n });\n }\n\n function onDrain(): void {\n if (paused) {\n paused = false;\n input.resume?.();\n }\n const resolvers = drainResolvers;\n drainResolvers = [];\n for (const r of resolvers) r();\n }\n\n return {\n start: () => {\n input.setEncoding?.('utf-8');\n input.on('data', onData);\n output.on('drain', onDrain);\n },\n write,\n stop: () => {\n input.off('data', onData);\n output.off('drain', onDrain);\n // Settle any queued write() promises so callers blocked on\n // backpressure don't hang past shutdown. Resume the input stream\n // symmetrically (we may have paused it on backpressure) so the\n // caller doesn't inherit a paused stdin.\n if (paused) {\n paused = false;\n input.resume?.();\n }\n const resolvers = drainResolvers;\n drainResolvers = [];\n for (const r of resolvers) r();\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upstream MCP client for the dev bridge (issue #399).
|
|
3
|
+
*
|
|
4
|
+
* Two transport variants:
|
|
5
|
+
*
|
|
6
|
+
* - **HTTP mode**: speaks streamable-HTTP JSON-RPC to the child's HTTP
|
|
7
|
+
* listener at `http://127.0.0.1:<port>/`. Carries the bridge-pinned
|
|
8
|
+
* `mcp-session-id` header on every request so session continuity
|
|
9
|
+
* survives reload.
|
|
10
|
+
*
|
|
11
|
+
* - **Pipe mode (--serve)**: writes/reads newline-delimited JSON-RPC
|
|
12
|
+
* frames on a pair of FDs paired with the child via `child_process`
|
|
13
|
+
* IPC. The child opens these FDs because `FRONTMCP_DEV_STDIO_FD` is
|
|
14
|
+
* set; the bridge writes requests, reads responses, no HTTP layer.
|
|
15
|
+
*/
|
|
16
|
+
import type { ChildProcess } from 'node:child_process';
|
|
17
|
+
import type { BridgeLogger } from './log';
|
|
18
|
+
import type { JsonRpcFrame } from './stdio-framer';
|
|
19
|
+
export interface UpstreamClient {
|
|
20
|
+
send(frame: JsonRpcFrame): Promise<void>;
|
|
21
|
+
/** Stop background tasks (SSE listener, pipe parser). */
|
|
22
|
+
close(): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export interface UpstreamClientOptions {
|
|
25
|
+
log: BridgeLogger;
|
|
26
|
+
/** Called for every frame the upstream child sends back. */
|
|
27
|
+
onFrame: (frame: JsonRpcFrame) => void | Promise<void>;
|
|
28
|
+
/** Session id pinned by the bridge for HTTP mode. */
|
|
29
|
+
sessionId?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface HttpUpstreamOptions extends UpstreamClientOptions {
|
|
32
|
+
/** Loopback URL of the user-code HTTP server. */
|
|
33
|
+
url: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function createHttpUpstream(options: HttpUpstreamOptions): UpstreamClient;
|
|
36
|
+
export interface PipeUpstreamOptions extends UpstreamClientOptions {
|
|
37
|
+
/** The forked child; we write to / read from the IPC pipe (FD 3). */
|
|
38
|
+
child: ChildProcess;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Pipe mode: the child speaks JSON-RPC on FD 3 (set via
|
|
42
|
+
* `FRONTMCP_DEV_STDIO_FD=3`). We use Node's IPC channel for the same wire
|
|
43
|
+
* — `child.send(...)` forwards a structured message and `child.on('message', …)`
|
|
44
|
+
* yields whatever the child writes back.
|
|
45
|
+
*
|
|
46
|
+
* (Node's IPC wraps JSON over a pipe internally; the framing is consistent
|
|
47
|
+
* with what `runStdio` would write if it were pointed at FD 3.)
|
|
48
|
+
*/
|
|
49
|
+
export declare function createPipeUpstream(options: PipeUpstreamOptions): UpstreamClient;
|