@agdf/mcp-server 0.20.1 → 0.30.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 +124 -12
- package/package.json +8 -3
- package/src/generation-entry.js +3 -85
- package/src/generation-protocol.js +2 -44
- package/src/generation-runtime.js +94 -0
- package/src/generation-thread-entry.js +8 -0
- package/src/generation.js +64 -20
- package/src/main.js +42 -10
- package/src/server.js +44 -22
- package/src/stdio-output.js +42 -0
- package/src/ui-capability.js +49 -1
- package/src/worker-entry.js +2 -2
- package/src/worker.js +53 -7
- package/ui/cockpit.html +31 -31
- package/ui/decision-manifest.json +3 -3
- package/ui/decision.html +36 -36
- package/ui/manifest.json +4 -4
package/src/generation.js
CHANGED
|
@@ -8,7 +8,7 @@ import { DispatchExecutionError } from './worker.js';
|
|
|
8
8
|
// One live facade, three directly owned children, no process registry or PID
|
|
9
9
|
// discovery. Capacity includes startup and unconfirmed cleanup, not just current.
|
|
10
10
|
export async function createOwnedGenerationFacade({ runtime, publisher, createControlRuntime, surface, cockpitDir, cockpitMode,
|
|
11
|
-
Fork = fork, qualifyMs = L.qualify, closeMs = L.close, invokeMs = L.qualify, onEvent = () => {} } = {}) {
|
|
11
|
+
generationTransport, Fork = fork, qualifyMs = L.qualify, closeMs = L.close, invokeMs = L.qualify, onEvent = () => {} } = {}) {
|
|
12
12
|
if (![qualifyMs, closeMs, invokeMs].every(value => Number.isSafeInteger(value) && value > 0)
|
|
13
13
|
|| qualifyMs > L.qualify || closeMs > L.close || invokeMs > L.qualify) throw Error('generation_deadline_invalid');
|
|
14
14
|
const epoch = randomUUID(), slots = new Map(), routes = new Map(), presentations = new Map(), operations = new Map(), receipts = new Map();
|
|
@@ -64,10 +64,16 @@ export async function createOwnedGenerationFacade({ runtime, publisher, createCo
|
|
|
64
64
|
unknown: new Set(), inflight: 0, exited: false, closeAck: false, observed: null };
|
|
65
65
|
slots.set(row.id, row); event(row, 'reserved'); // Reservation precedes acquisition/load/spawn.
|
|
66
66
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
if (generationTransport) {
|
|
68
|
+
// This is a logical private connection. Actual Worker and claim ownership
|
|
69
|
+
// stay in the detached owner; no ChildProcess/Worker impersonation.
|
|
70
|
+
row.child = await generationTransport.open({ packageInfo, epoch, generation: row.id });
|
|
71
|
+
} else {
|
|
72
|
+
const claim = publisher.acquire(packageInfo, { ownerEpoch: epoch, generation: row.id });
|
|
73
|
+
row.child = Fork(join(packageInfo.packageRoot, 'src/generation-entry.js'), [epoch, String(row.id)],
|
|
74
|
+
{ stdio: ['ignore', 'ignore', 'ignore', 'ipc'], serialization: 'json', execArgv: [] });
|
|
75
|
+
claim.bindOwnedChild(row.child, { confirmedEnd: () => row.closeAck && !row.inflight && !row.unknown.size });
|
|
76
|
+
}
|
|
71
77
|
row.child.on('message', reply => {
|
|
72
78
|
if (!validGenerationMessage(reply, { epoch, generation: row.id, reply: true })) { event(row, 'invalid_reply'); return; }
|
|
73
79
|
const pending = row.pending.get(reply.request_id);
|
|
@@ -81,13 +87,18 @@ export async function createOwnedGenerationFacade({ runtime, publisher, createCo
|
|
|
81
87
|
pending.resolve(reply);
|
|
82
88
|
}
|
|
83
89
|
});
|
|
84
|
-
row.child.once('exit', () => {
|
|
90
|
+
row.child.once('exit', (code, signal) => {
|
|
85
91
|
row.exited = true;
|
|
86
92
|
for (const pending of row.pending.values()) { clearTimeout(pending.timer); pending.reject(new DispatchExecutionError('generation_connection_lost')); }
|
|
87
93
|
row.pending.clear();
|
|
88
|
-
if (row.closeAck && !row.inflight && !row.unknown.size) { slots.delete(row.id); row.state = 'closed'; event(row, 'confirmed_exit'); row.end?.(true); }
|
|
94
|
+
if (code === 0 && !signal && row.closeAck && !row.inflight && !row.unknown.size) { slots.delete(row.id); row.state = 'closed'; event(row, 'confirmed_exit'); row.end?.(true); }
|
|
89
95
|
else { row.state = 'close_unconfirmed'; event(row, 'unexpected_exit'); row.end?.(false); }
|
|
90
96
|
});
|
|
97
|
+
row.child.once('disconnect', () => {
|
|
98
|
+
row.state = 'close_unconfirmed'; event(row, 'connection_lost');
|
|
99
|
+
for (const pending of row.pending.values()) { clearTimeout(pending.timer); pending.reject(new DispatchExecutionError('generation_connection_lost')); }
|
|
100
|
+
row.pending.clear();
|
|
101
|
+
});
|
|
91
102
|
row.child.once('error', () => { row.state = 'close_unconfirmed'; event(row, 'spawn_failed'); });
|
|
92
103
|
row.state = 'qualifying'; event(row, 'qualifying');
|
|
93
104
|
const qualified = (await request(row, 'qualify', { surface, commandGroups: [...(runtime.trustedContext.commandGroups ?? [])],
|
|
@@ -105,6 +116,11 @@ export async function createOwnedGenerationFacade({ runtime, publisher, createCo
|
|
|
105
116
|
}
|
|
106
117
|
const control = {
|
|
107
118
|
status,
|
|
119
|
+
originalInput(input) {
|
|
120
|
+
const prior = receipts.get(input.operation_id);
|
|
121
|
+
if (!prior) return null;
|
|
122
|
+
return { known: true, resolution: prior.resolution ?? null };
|
|
123
|
+
},
|
|
108
124
|
async refresh() { await Promise.all([...slots.values()].filter(row => !row.exited).map(observe)); return status(); },
|
|
109
125
|
inspect(id, input) { const record = receipts.get(id);
|
|
110
126
|
if (record && (record.input.run_id !== input.run_id || record.input.primary_target !== input.primary_target
|
|
@@ -112,18 +128,23 @@ export async function createOwnedGenerationFacade({ runtime, publisher, createCo
|
|
|
112
128
|
return record ? { status: record.result ? 'recorded' : 'pending',
|
|
113
129
|
request_digest: record.digest, result: record.result, current: status(), authorizes: false }
|
|
114
130
|
: { status: 'unknown', reason: 'connection_control_record_unavailable', current: status(), authorizes: false }; },
|
|
115
|
-
async replace({ input, candidate, requestDigest, revalidate }) {
|
|
131
|
+
async replace({ input, candidate, requestDigest, resolution, revalidate }) {
|
|
116
132
|
const prior = receipts.get(input.operation_id);
|
|
117
133
|
if (prior) return prior.digest !== requestDigest ? failure('operation_payload_conflict')
|
|
118
134
|
: prior.result ? { ...prior.result, outcome: prior.result.outcome === 'accepted' ? 'already_applied' : prior.result.outcome, historical_effect: true, current: status() }
|
|
119
135
|
: { outcome: 'effect_unknown', reason: 'generation_replacement_pending', current: status(), authorizes: false };
|
|
120
136
|
if (receipts.size >= L.receipts) return failure('connection_control_capacity');
|
|
121
137
|
if (closed || replacing) return failure(closed ? 'connection_closed' : 'generation_replacement_busy');
|
|
122
|
-
const record = { digest: requestDigest, input, result: null }; receipts.set(input.operation_id, record); replacing = true;
|
|
138
|
+
const record = { digest: requestDigest, input, resolution, result: null }; receipts.set(input.operation_id, record); replacing = true;
|
|
123
139
|
let candidateRow;
|
|
124
140
|
try {
|
|
125
|
-
|
|
126
|
-
|
|
141
|
+
const recheck = () => {
|
|
142
|
+
if (closed) throw new DispatchExecutionError('connection_closed', 'not_dispatched');
|
|
143
|
+
controlRuntimeInstance?.verifyProducer();
|
|
144
|
+
revalidate();
|
|
145
|
+
};
|
|
146
|
+
recheck(); const packageInfo = publisher.resolve(candidate);
|
|
147
|
+
candidateRow = await reserve(packageInfo, actualContext); recheck();
|
|
127
148
|
const previous = current; current = candidateRow; current.state = 'current';
|
|
128
149
|
if (previous) previous.state = previous.exited ? 'close_unconfirmed' : 'draining';
|
|
129
150
|
event(current, 'admitted'); record.result = { outcome: 'accepted', reason: null, operation_id: input.operation_id,
|
|
@@ -135,44 +156,66 @@ export async function createOwnedGenerationFacade({ runtime, publisher, createCo
|
|
|
135
156
|
record.result.current = status(); return record.result;
|
|
136
157
|
},
|
|
137
158
|
};
|
|
138
|
-
|
|
159
|
+
// Ordinary requests belong to the fully qualified generation. Only the rare
|
|
160
|
+
// facade control branches need another Core instance; share one initialization.
|
|
161
|
+
let controlRuntimePromise, controlRuntimeInstance;
|
|
162
|
+
const getControlRuntime = () => controlRuntimePromise ??= Promise.resolve().then(() => createControlRuntime(control)).then(async created => {
|
|
163
|
+
const producer = created?.producerIdentity;
|
|
164
|
+
const code = created?.trustedContext?.expectedVersion !== runtime.trustedContext.expectedVersion ? 'runtime_version_mismatch'
|
|
165
|
+
: created?.trustedContext?.provenanceStatus !== 'matched'
|
|
166
|
+
|| !producer || producer.server_digest !== publisher.initial.digest || producer.dispatcher_digest !== publisher.initial.dispatcherDigest
|
|
167
|
+
|| producer.sdk_digest !== publisher.initial.sdkDigest || typeof created.verifyProducer !== 'function'
|
|
168
|
+
? 'runtime_provenance_invalid' : null;
|
|
169
|
+
if (code) {
|
|
170
|
+
await created?.close?.(); throw new DispatchExecutionError(code, 'not_dispatched');
|
|
171
|
+
}
|
|
172
|
+
controlRuntimeInstance = created;
|
|
173
|
+
return created;
|
|
174
|
+
}).catch(error => { throw new DispatchExecutionError(generationFailureReason(error), 'not_dispatched'); });
|
|
139
175
|
function operationKey(input, id = input.operation_id) { return JSON.stringify([input.primary_target, input.run_id, id]); }
|
|
140
176
|
async function invoke(name, args, host = {}, signal) {
|
|
141
177
|
if (signal?.aborted) throw new DispatchExecutionError('dispatch_cancelled', 'not_dispatched');
|
|
142
178
|
if (closed) throw new DispatchExecutionError('connection_closed', 'not_dispatched');
|
|
143
179
|
actualContext = { client: host.client, uiSupported: host.uiSupported === true };
|
|
180
|
+
const canonical = Boolean(args.operation_id) && !runtime.isEphemeralControlCommand?.(name,args);
|
|
144
181
|
if (args.operation_id && name === 'agdf_record' && args.command?.kind === 'runtime_generation.replace'
|
|
145
182
|
&& operations.has(operationKey(args))) throw new DispatchExecutionError('operation_payload_conflict', 'not_dispatched');
|
|
146
183
|
if (args.operation_id && args.command?.kind !== 'runtime_generation.replace' && receipts.has(args.operation_id))
|
|
147
184
|
throw new DispatchExecutionError('operation_payload_conflict', 'not_dispatched');
|
|
148
185
|
if (name === 'agdf_record' && args.command?.kind === 'runtime_generation.replace'
|
|
149
186
|
|| name === 'agdf_inspect' && args.operation === 'runtime-generations') {
|
|
187
|
+
const controlRuntime = await getControlRuntime();
|
|
188
|
+
if (closed || signal?.aborted) throw new DispatchExecutionError(closed ? 'connection_closed' : 'dispatch_cancelled', 'not_dispatched');
|
|
189
|
+
try { controlRuntime.verifyProducer(); } catch (error) { throw new DispatchExecutionError(generationFailureReason(error), 'not_dispatched'); }
|
|
150
190
|
const result = await controlRuntime.tool(name).execute(controlRuntime.tool(name).parse(args));
|
|
151
191
|
return controlRuntime.projectToolResult(name, args, name === 'agdf_inspect' && result.report?.available ? { ...result, report: await control.refresh() } : result);
|
|
152
192
|
}
|
|
153
193
|
if (name === 'agdf_inspect' && args.operation === 'operation-status' && receipts.has(args.module)) {
|
|
194
|
+
const controlRuntime = await getControlRuntime();
|
|
195
|
+
if (closed || signal?.aborted) throw new DispatchExecutionError(closed ? 'connection_closed' : 'dispatch_cancelled', 'not_dispatched');
|
|
196
|
+
try { controlRuntime.verifyProducer(); } catch (error) { throw new DispatchExecutionError(generationFailureReason(error), 'not_dispatched'); }
|
|
154
197
|
const validated = await controlRuntime.tool(name).execute(controlRuntime.tool(name).parse(args));
|
|
155
198
|
return controlRuntime.projectToolResult(name, args, validated.report?.status === 'unknown' || validated.report?.status === 'absent'
|
|
156
199
|
? { ...validated, report: control.inspect(args.module, args) } : validated);
|
|
157
200
|
}
|
|
158
201
|
const knownSession = args.session_id && routes.get(args.session_id);
|
|
159
202
|
if (args.session_id && !knownSession) throw new DispatchExecutionError('session_expired', 'not_dispatched');
|
|
160
|
-
const id = name === 'agdf_inspect' && args.operation === 'operation-status' ? args.module : args.operation_id;
|
|
203
|
+
const id = name === 'agdf_inspect' && args.operation === 'operation-status' ? args.module.split('/').at(-1) : canonical ? args.operation_id : null;
|
|
161
204
|
const key = id && operationKey(args, id), original = key && operations.get(key);
|
|
162
205
|
const presentationId = args.command?.payload?.presentation_id ?? args.presentation_id;
|
|
163
206
|
const presentationOwner = presentationId && presentations.get(operationKey(args, presentationId));
|
|
164
207
|
const row = knownSession ?? original ?? presentationOwner ?? current;
|
|
165
208
|
if (!row || row.exited) throw new DispatchExecutionError('generation_unavailable', 'not_dispatched');
|
|
166
|
-
if (
|
|
209
|
+
if (canonical && !original) {
|
|
167
210
|
if (operations.size >= L.routes) throw new DispatchExecutionError('generation_route_capacity', 'not_dispatched');
|
|
168
211
|
operations.set(key, row);
|
|
169
212
|
}
|
|
170
213
|
++row.inflight; event(row, 'invoke', name);
|
|
171
214
|
try {
|
|
172
|
-
const result = (await request(row, 'invoke', { name, arguments: args }, actualContext, args.operation_id
|
|
173
|
-
if (
|
|
215
|
+
const result = (await request(row, 'invoke', { name, arguments: args }, actualContext, canonical ? args.operation_id : null, invokeMs)).payload;
|
|
216
|
+
if (canonical && result.outcome === 'effect_unknown') row.unknown.add(key);
|
|
174
217
|
if (name === 'agdf_inspect' && args.operation === 'operation-status' && result.report?.status === 'recorded'
|
|
175
|
-
&& result.report.receipt?.operation_id
|
|
218
|
+
&& (result.report.receipt?.operation_id ?? result.report.receipt?.operation?.operation_id) === id) row.unknown.delete(key);
|
|
176
219
|
if (result.app_capability === 'available') nativeRequired = true;
|
|
177
220
|
if (result.family_result?.presentation_id && ['accepted', 'already_applied'].includes(result.outcome)) {
|
|
178
221
|
const presentationKey = operationKey(args, result.family_result.presentation_id);
|
|
@@ -185,7 +228,7 @@ export async function createOwnedGenerationFacade({ runtime, publisher, createCo
|
|
|
185
228
|
}
|
|
186
229
|
}
|
|
187
230
|
return result;
|
|
188
|
-
} catch (error) { if (
|
|
231
|
+
} catch (error) { if (canonical && error.dispatch !== 'not_dispatched') row.unknown.add(key); throw error; }
|
|
189
232
|
finally { --row.inflight; if (closed && !row.inflight && !row.unknown.size) void retire(row, true); }
|
|
190
233
|
}
|
|
191
234
|
current = await reserve(publisher.initial, {}); current.state = 'current'; event(current, 'admitted');
|
|
@@ -194,16 +237,17 @@ export async function createOwnedGenerationFacade({ runtime, publisher, createCo
|
|
|
194
237
|
// not wire requests; parsing them again in a child both breaks the contract
|
|
195
238
|
// and copies the entire locale registry into a bounded private message.
|
|
196
239
|
const tools = runtime.tools.map(tool => ({ ...tool, parentOwned: true,
|
|
197
|
-
parse: input => { tool.parse(input); return input; }, execute: input => invoke(tool.name, input) }));
|
|
240
|
+
parse: input => { if (runtime.validatePublicInput?.(tool.name, input) !== true) tool.parse(input); return input; }, execute: input => invoke(tool.name, input) }));
|
|
198
241
|
// The server supplies actual initialize/request context only at this seam.
|
|
199
242
|
for (const tool of tools) tool.execute = (input, signal, host) => invoke(tool.name, input, host, signal);
|
|
200
243
|
return Object.freeze({ ...runtime, tools, tool: name => tools.find(tool => tool.name === name),
|
|
201
|
-
requiresProjectedResult: true, postprocessToolResult: undefined, inspectOwnership: status, runtimeControl: control,
|
|
244
|
+
requiresProjectedResult: true, prepareInput: undefined, postprocessToolResult: undefined, inspectOwnership: status, runtimeControl: control,
|
|
202
245
|
close() {
|
|
203
246
|
return closing ??= (async () => {
|
|
204
247
|
closed = true;
|
|
205
248
|
const previous = current; current = null; if (previous && !previous.exited) previous.state = 'draining';
|
|
206
249
|
await Promise.allSettled([...slots.values()].map(row => retire(row, true)));
|
|
250
|
+
if (controlRuntimePromise) await controlRuntimePromise.then(controlRuntime => controlRuntime.close?.(), () => {});
|
|
207
251
|
return status();
|
|
208
252
|
})();
|
|
209
253
|
},
|
package/src/main.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import process from "node:process";
|
|
3
|
-
import { createAgdfWorkerExecutor, serveAgdfStdio } from "./server.js";
|
|
4
3
|
import { loadCockpitResource } from './cockpit-resource.js';
|
|
5
4
|
|
|
6
|
-
export async function runMcpServer({ surface, cockpitDir, cockpitMode, runtime, executor, ownedPublication, ownedPublisher, onClosed } = {}) {
|
|
5
|
+
export async function runMcpServer({ surface, cockpitDir, cockpitMode, runtime, executor, ownedPublication, ownedPublisher, generationTransport, onClosed } = {}) {
|
|
7
6
|
if (cockpitMode !== undefined && (cockpitMode !== 'plugin' || surface !== 'codex' || cockpitDir !== undefined)) throw Error('AGDF_COCKPIT_TARGET_INVALID');
|
|
8
7
|
const defaultRuntime = runtime === undefined && cockpitDir === undefined && cockpitMode === undefined;
|
|
9
8
|
// A supplied, bound runtime owns its resources. Do not load a second package
|
|
10
9
|
// composition merely to run an explicit adapter/protocol fixture.
|
|
11
|
-
const
|
|
12
|
-
|
|
10
|
+
const thin = defaultRuntime && !!ownedPublication;
|
|
11
|
+
const dispatcherRuntime = runtime === undefined ? await import(thin ? 'create-agdf/mcp-protocol-runtime' : 'create-agdf/mcp-dispatch-runtime') : null;
|
|
12
|
+
runtime ??= cockpitDir !== undefined || cockpitMode !== undefined ? await dispatcherRuntime.createMcpCockpitRuntime({ surface, cockpitDir, cockpitMode })
|
|
13
|
+
: thin ? dispatcherRuntime.createMcpTransportRuntime({ surface }) : dispatcherRuntime.createMcpDispatchRuntime({ surface });
|
|
13
14
|
const serverVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
14
15
|
if (runtime.trustedContext.expectedVersion !== serverVersion) {
|
|
15
16
|
process.stderr.write("AGDF_MCP_VERSION_MISMATCH\n");
|
|
@@ -21,21 +22,52 @@ export async function runMcpServer({ surface, cockpitDir, cockpitMode, runtime,
|
|
|
21
22
|
process.exitCode = 1;
|
|
22
23
|
return null;
|
|
23
24
|
}
|
|
24
|
-
if (
|
|
25
|
+
if (ownedPublication) {
|
|
26
|
+
// The serving protocol factory freshly inspects all selected source and SDK
|
|
27
|
+
// bytes. Bind that actual producer to the parent's claimed expectation;
|
|
28
|
+
// never treat a cross-thread/async observation as fresh inspection evidence.
|
|
29
|
+
const producer = runtime.producerIdentity;
|
|
30
|
+
if (!producer || producer.server_digest !== ownedPublication.digest
|
|
31
|
+
|| producer.dispatcher_digest !== ownedPublication.dispatcherDigest || producer.sdk_digest !== ownedPublication.sdkDigest)
|
|
32
|
+
throw Error('AGDF_MCP_FACADE_IDENTITY_INVALID');
|
|
33
|
+
}
|
|
34
|
+
if (defaultRuntime && surface === 'codex') {
|
|
25
35
|
const ui = { uri: dispatcherRuntime.DECISION_UI_URI, mimeType: 'text/html;profile=mcp-app', limit: 4 * 1024 ** 2, kind: 'decision' };
|
|
26
|
-
|
|
36
|
+
// The thin front has no session/qualification consumer. Its facade loads
|
|
37
|
+
// the resource for the real generation manifest; full runtimes need it here.
|
|
38
|
+
if (!thin) ui.digest = loadCockpitResource(ui).digest;
|
|
27
39
|
// A shipped resource does not qualify native App-only mediation. Production
|
|
28
40
|
// actions remain disabled until exact package/host evidence is provided.
|
|
29
|
-
runtime = await dispatcherRuntime.
|
|
41
|
+
runtime = thin ? await dispatcherRuntime.createMcpDecisionTransportRuntime({ runtime, ui })
|
|
42
|
+
: await dispatcherRuntime.createMcpDecisionRuntime({ runtime, ui });
|
|
30
43
|
}
|
|
31
44
|
if (runtime.mode === 'cockpit') loadCockpitResource(runtime.ui);
|
|
45
|
+
let transport;
|
|
32
46
|
if (ownedPublication) {
|
|
33
47
|
const { createOwnedGenerationFacade } = await import('./generation.js');
|
|
34
48
|
if (!ownedPublisher) throw Error('runtime_publication_unowned');
|
|
35
49
|
const publisher = ownedPublisher;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
let beginTransport;
|
|
51
|
+
const claimed = new Promise(resolve => { beginTransport = resolve; });
|
|
52
|
+
const starting = createOwnedGenerationFacade({ runtime, publisher, generationTransport, surface, cockpitDir, cockpitMode,
|
|
53
|
+
onEvent: event => { if (event.kind === 'qualifying') beginTransport(); },
|
|
54
|
+
createControlRuntime: async runtimeControl => {
|
|
55
|
+
const full = thin ? await import('create-agdf/mcp-dispatch-runtime') : dispatcherRuntime;
|
|
56
|
+
return full.createMcpDispatchRuntime({ surface, runtimeControl });
|
|
57
|
+
} });
|
|
58
|
+
// Only a claimed private-generation start releases the SDK import. A
|
|
59
|
+
// pre-claim failure rejects starting without loading transport code. Its full
|
|
60
|
+
// qualification and the facade SDK must both finish before public service.
|
|
61
|
+
// Observe both promises so a transport failure cannot abandon a live child.
|
|
62
|
+
const transportStarting = Promise.race([claimed, starting]).then(() => import('./server.js'));
|
|
63
|
+
const [generation, loaded] = await Promise.allSettled([starting, transportStarting]);
|
|
64
|
+
if (generation.status === 'rejected' || loaded.status === 'rejected') {
|
|
65
|
+
if (generation.status === 'fulfilled') await generation.value.close();
|
|
66
|
+
throw generation.status === 'rejected' ? generation.reason : loaded.reason;
|
|
67
|
+
}
|
|
68
|
+
runtime = generation.value; transport = loaded.value;
|
|
69
|
+
} else transport = await import('./server.js');
|
|
70
|
+
const { createAgdfWorkerExecutor, serveAgdfStdio } = transport;
|
|
39
71
|
const dispatchExecutor = executor ?? (runtime.mode === 'cockpit' ? undefined
|
|
40
72
|
: createAgdfWorkerExecutor(runtime, { releaseOwnedLock: dispatcherRuntime?.releaseAbandonedOwnedLock ?? null }));
|
|
41
73
|
const handle = serveAgdfStdio({
|
package/src/server.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import { boundStdioOutput } from './stdio-output.js';
|
|
1
2
|
import { McpServer, ProtocolError, ProtocolErrorCode, fromJsonSchema } from "@modelcontextprotocol/server";
|
|
2
|
-
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
3
|
+
import { serveStdio, StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
3
4
|
import { AjvJsonSchemaValidator } from '@modelcontextprotocol/server/validators/ajv';
|
|
4
5
|
import { randomUUID } from 'node:crypto';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { DispatchExecutionError
|
|
7
|
+
import { DispatchExecutionError } from "./worker.js";
|
|
7
8
|
import { loadCockpitResource } from './cockpit-resource.js';
|
|
8
|
-
import { requestDeclaresMcpAppUi, requestMcpClientInfo, requestMcpClientCapabilities
|
|
9
|
+
import { requestDeclaresMcpAppUi, requestMcpClientInfo, requestMcpClientCapabilities, requiresMcpAppUi,
|
|
10
|
+
missingMcpAppUi, capabilityTransport, modernToolUiDenial } from "./ui-capability.js";
|
|
9
11
|
|
|
10
12
|
export const SERVER_NAME = "agdf-mcp";
|
|
11
13
|
|
|
@@ -38,7 +40,7 @@ function toolResult(runtime, name, input, result, serialize = runtime.serialize,
|
|
|
38
40
|
return response;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
export function buildAgdfServer({ runtime, executor } = {}) {
|
|
43
|
+
export function buildAgdfServer({ runtime, executor, era } = {}) {
|
|
42
44
|
if (!runtime?.definition || typeof runtime.serialize !== "function") {
|
|
43
45
|
throw new TypeError("AGDF MCP runtime is required");
|
|
44
46
|
}
|
|
@@ -57,7 +59,14 @@ export function buildAgdfServer({ runtime, executor } = {}) {
|
|
|
57
59
|
},
|
|
58
60
|
close: async () => runtime.close?.(),
|
|
59
61
|
};
|
|
60
|
-
const
|
|
62
|
+
const registrations = new Map();
|
|
63
|
+
class AgdfServer extends McpServer {
|
|
64
|
+
async connect(transport) {
|
|
65
|
+
return super.connect(capabilityTransport(transport, message => modernToolUiDenial(message,
|
|
66
|
+
{ era: era ?? (this.server.getNegotiatedProtocolVersion() === '2026-07-28' ? 'modern' : 'legacy'), runtime, registrations })));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const server = new AgdfServer(
|
|
61
70
|
{ name: SERVER_NAME, version: runtime.trustedContext.expectedVersion },
|
|
62
71
|
{ capabilities: { tools: {}, ...(runtime.ui ? { resources: {} } : {}) } },
|
|
63
72
|
);
|
|
@@ -87,16 +96,26 @@ export function buildAgdfServer({ runtime, executor } = {}) {
|
|
|
87
96
|
} };
|
|
88
97
|
for (const tool of tools) {
|
|
89
98
|
const { name, definition, serialize } = tool;
|
|
90
|
-
|
|
99
|
+
const schema = fromJsonSchema(definition.inputSchema, { getValidator(inputSchema) {
|
|
100
|
+
const validate = deferredSchemas.getValidator(inputSchema);
|
|
101
|
+
return input => {
|
|
102
|
+
const checked = validate(input);
|
|
103
|
+
if (checked.valid) return checked;
|
|
104
|
+
const hint = runtime.inputValidationHint?.(name, input);
|
|
105
|
+
return hint ? { ...checked, errorMessage: `${hint} Schema: ${checked.errorMessage}` } : checked;
|
|
106
|
+
};
|
|
107
|
+
} });
|
|
108
|
+
const registered = server.registerTool(
|
|
91
109
|
name,
|
|
92
110
|
{
|
|
93
111
|
description: definition.description,
|
|
94
112
|
annotations: definition.annotations,
|
|
95
|
-
inputSchema:
|
|
113
|
+
inputSchema: schema,
|
|
96
114
|
...(definition.outputSchema ? { outputSchema: fromJsonSchema(definition.outputSchema, deferredSchemas) } : {}),
|
|
97
115
|
...(definition._meta ? { _meta: definition._meta } : {}),
|
|
98
116
|
},
|
|
99
117
|
async (argumentsValue, context) => {
|
|
118
|
+
const started = performance.now();
|
|
100
119
|
const denied = !uiAllowed(context) ? runtime.unsupportedUiCode ?? 'resource_denied'
|
|
101
120
|
: tool.requiresUi && !supportsUi(context) ? 'host_ui_unsupported' : null;
|
|
102
121
|
if (denied) {
|
|
@@ -104,20 +123,25 @@ export function buildAgdfServer({ runtime, executor } = {}) {
|
|
|
104
123
|
const failure = runtime.projectToolResult(name, argumentsValue, runtime.failure(denied, name, argumentsValue, facts), facts);
|
|
105
124
|
return respond(name, argumentsValue, failure, serialize ?? runtime.serialize, context, facts);
|
|
106
125
|
}
|
|
107
|
-
|
|
126
|
+
const parentOwned = typeof tool.parentOwned === 'function' ? tool.parentOwned(argumentsValue) : tool.parentOwned;
|
|
127
|
+
let phase = parentOwned ? 'parse' : 'execute';
|
|
108
128
|
try {
|
|
129
|
+
phase = 'parse';
|
|
130
|
+
argumentsValue = runtime.prepareInput?.(name, argumentsValue) ?? argumentsValue;
|
|
131
|
+
phase = parentOwned ? 'parse' : 'execute';
|
|
109
132
|
let result;
|
|
110
|
-
if (
|
|
133
|
+
if (parentOwned) {
|
|
111
134
|
const parsed = tool.parse(argumentsValue); phase = 'execute';
|
|
112
135
|
result = await tool.execute(parsed, context.signal,
|
|
113
136
|
{ client: requestMcpClientInfo(server.server, context), uiSupported: supportsUi(context) });
|
|
114
|
-
} else result = await dispatchExecutor.execute(argumentsValue, { signal: context.signal, toolName: name
|
|
137
|
+
} else result = await dispatchExecutor.execute(argumentsValue, { signal: context.signal, toolName: name,
|
|
138
|
+
trusted: runtime.prepareDispatch?.(name, argumentsValue) });
|
|
115
139
|
if (runtime.postprocessToolResult) result = await runtime.postprocessToolResult(name, argumentsValue, result,
|
|
116
140
|
{ uiSupported: supportsUi(context), client: requestMcpClientInfo(server.server, context) });
|
|
117
141
|
return respond(name, argumentsValue, result, serialize ?? runtime.serialize, context);
|
|
118
142
|
} catch (error) {
|
|
119
143
|
const facts = runtime.normalizeFailure(error, { phase });
|
|
120
|
-
const failure = runtime.failure(facts.code, name, argumentsValue, facts);
|
|
144
|
+
const failure = runtime.failure(facts.code, name, argumentsValue, { ...facts, elapsed_ms: performance.now() - started });
|
|
121
145
|
// A local execution/delivery failure is owned here, including malformed
|
|
122
146
|
// producer output. It never causes re-execution or a second write.
|
|
123
147
|
const projected = runtime.projectToolResult(name, argumentsValue, failure, facts);
|
|
@@ -125,12 +149,16 @@ export function buildAgdfServer({ runtime, executor } = {}) {
|
|
|
125
149
|
}
|
|
126
150
|
},
|
|
127
151
|
);
|
|
152
|
+
registrations.set(name, { tool, schema, registered });
|
|
128
153
|
}
|
|
129
154
|
if (runtime.ui) {
|
|
130
155
|
const resource = loadCockpitResource(runtime.ui);
|
|
131
156
|
server.registerResource(runtime.mode === 'decision' ? 'AGDF Gate Decision' : runtime.ui.label ?? 'AGDF Cockpit', resource.uri, { mimeType: resource.mimeType, _meta: resource._meta },
|
|
132
157
|
async (_uri, context) => {
|
|
133
|
-
if (
|
|
158
|
+
if (requiresMcpAppUi(runtime, { requiresUi: runtime.mode === 'decision' }) && !supportsUi(context)) {
|
|
159
|
+
if ((era ?? (server.server.getNegotiatedProtocolVersion() === '2026-07-28' ? 'modern' : 'legacy')) === 'modern') throw missingMcpAppUi(runtime.ui.mimeType);
|
|
160
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, "resource_denied");
|
|
161
|
+
}
|
|
134
162
|
return { contents: [resource] };
|
|
135
163
|
});
|
|
136
164
|
if (gated || runtime.mode === 'decision') {
|
|
@@ -155,12 +183,13 @@ export function buildAgdfServer({ runtime, executor } = {}) {
|
|
|
155
183
|
|
|
156
184
|
export function serveAgdfStdio({ runtime, executor, onerror } = {}) {
|
|
157
185
|
const instances = new Set();
|
|
158
|
-
const handle = serveStdio(() => {
|
|
159
|
-
const server = buildAgdfServer({ runtime, executor });
|
|
186
|
+
const handle = serveStdio(({ era }) => {
|
|
187
|
+
const server = buildAgdfServer({ runtime, executor, era });
|
|
160
188
|
instances.add(server);
|
|
161
189
|
return server;
|
|
162
190
|
}, {
|
|
163
191
|
legacy: "serve",
|
|
192
|
+
transport: boundStdioOutput(new StdioServerTransport()),
|
|
164
193
|
onerror: () => onerror?.("AGDF_MCP_TRANSPORT_ERROR"),
|
|
165
194
|
});
|
|
166
195
|
return Object.freeze({
|
|
@@ -177,11 +206,4 @@ export function serveAgdfStdio({ runtime, executor, onerror } = {}) {
|
|
|
177
206
|
});
|
|
178
207
|
}
|
|
179
208
|
|
|
180
|
-
export
|
|
181
|
-
return createWorkerDispatchExecutor({
|
|
182
|
-
surface: runtime.trustedContext.surface,
|
|
183
|
-
expectedVersion: runtime.trustedContext.expectedVersion,
|
|
184
|
-
commandGroups: runtime.trustedContext.commandGroups,
|
|
185
|
-
...options,
|
|
186
|
-
});
|
|
187
|
-
}
|
|
209
|
+
export { createAgdfWorkerExecutor } from './worker.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { OWNER_LIMITS } from './generation-protocol.js';
|
|
2
|
+
|
|
3
|
+
// One entry-owned SDK wire, one active write. Concurrent SDK responses must not
|
|
4
|
+
// attach one drain/error listener per result to a blocked native Worker stream.
|
|
5
|
+
// Capture exact JSON at admission. Existing result/resource owners bound the
|
|
6
|
+
// one active message; the additional queue has its own strict byte/frame cap.
|
|
7
|
+
// A legal large UI response must still work without lowering its resource cap.
|
|
8
|
+
export function boundStdioOutput(transport) {
|
|
9
|
+
const originalSend = transport.send.bind(transport), originalClose = transport.close.bind(transport);
|
|
10
|
+
const queue = [];
|
|
11
|
+
let active, queuedBytes = 0, closed = false, closing;
|
|
12
|
+
const capacityError = () => Error('AGDF_MCP_STDIO_CAPACITY');
|
|
13
|
+
function pump() {
|
|
14
|
+
if (active || closed || !queue.length) return;
|
|
15
|
+
const entry = active = queue.shift(); queuedBytes -= entry.bytes;
|
|
16
|
+
Promise.resolve().then(() => originalSend(entry.message)).then(entry.resolve, entry.reject).finally(() => {
|
|
17
|
+
active = undefined; pump();
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
transport.send = message => {
|
|
21
|
+
if (closed) return Promise.reject(Error('AGDF_MCP_STDIO_CLOSED'));
|
|
22
|
+
let snapshot, size;
|
|
23
|
+
try {
|
|
24
|
+
const json = JSON.stringify(message); size = Buffer.byteLength(json) + 1;
|
|
25
|
+
if (queue.length + (active ? 1 : 0) >= OWNER_LIMITS.frames || active && queuedBytes + size > OWNER_LIMITS.bytes) return Promise.reject(capacityError());
|
|
26
|
+
snapshot = JSON.parse(json); // Preserve SDK JSON semantics and call-time bytes.
|
|
27
|
+
} catch (error) { return Promise.reject(error); }
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
queuedBytes += size; queue.push({ message: snapshot, bytes: size, resolve, reject }); pump();
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
transport.close = () => {
|
|
33
|
+
if (closing) return closing;
|
|
34
|
+
closed = true;
|
|
35
|
+
for (const entry of queue.splice(0)) { queuedBytes -= entry.bytes; entry.reject(Error('AGDF_MCP_STDIO_CLOSED')); }
|
|
36
|
+
// Close does not prove that an active write drained. Its SDK promise stays
|
|
37
|
+
// observed; genuine Worker exit is still required by the lifecycle owner.
|
|
38
|
+
active?.reject(Error('AGDF_MCP_STDIO_CLOSED'));
|
|
39
|
+
closing = Promise.resolve().then(originalClose); return closing;
|
|
40
|
+
};
|
|
41
|
+
return transport;
|
|
42
|
+
}
|
package/src/ui-capability.js
CHANGED
|
@@ -1,7 +1,55 @@
|
|
|
1
|
-
import { CLIENT_CAPABILITIES_META_KEY, CLIENT_INFO_META_KEY } from "@modelcontextprotocol/server";
|
|
1
|
+
import { CLIENT_CAPABILITIES_META_KEY, CLIENT_INFO_META_KEY, MissingRequiredClientCapabilityError, classifyInboundRequest } from "@modelcontextprotocol/server";
|
|
2
2
|
|
|
3
3
|
// Extension id under which MCP Apps hosts declare UI support (@modelcontextprotocol/ext-apps EXTENSION_ID).
|
|
4
4
|
export const MCP_APP_UI_EXTENSION = "io.modelcontextprotocol/ui";
|
|
5
|
+
export const requiresMcpAppUi = (runtime, tool) => runtime.mode === 'cockpit' && runtime.uiCapabilityRequired === true || tool?.requiresUi === true;
|
|
6
|
+
export const missingMcpAppUi = mimeType => new MissingRequiredClientCapabilityError({
|
|
7
|
+
requiredCapabilities: { extensions: { [MCP_APP_UI_EXTENSION]: { mimeTypes: [mimeType] } } },
|
|
8
|
+
}, 'resource_denied');
|
|
9
|
+
|
|
10
|
+
// One narrow Transport decorator; framing, request dispatch, validation and
|
|
11
|
+
// pending-id/drain accounting remain with the original SDK connection.
|
|
12
|
+
export function capabilityTransport(transport, denial) {
|
|
13
|
+
const wrapped = {
|
|
14
|
+
onmessage: transport.onmessage,
|
|
15
|
+
onerror: transport.onerror,
|
|
16
|
+
onclose: transport.onclose,
|
|
17
|
+
start: () => transport.start(),
|
|
18
|
+
send: (message, options) => transport.send(message, options),
|
|
19
|
+
close: () => transport.close(),
|
|
20
|
+
get sessionId() { return transport.sessionId; },
|
|
21
|
+
set sessionId(value) { transport.sessionId = value; },
|
|
22
|
+
get hasPerRequestStream() { return transport.hasPerRequestStream; },
|
|
23
|
+
};
|
|
24
|
+
for (const method of ['setProtocolVersion', 'setSupportedProtocolVersions']) {
|
|
25
|
+
if (transport[method]) wrapped[method] = (...args) => transport[method](...args);
|
|
26
|
+
}
|
|
27
|
+
transport.onerror = error => wrapped.onerror?.(error);
|
|
28
|
+
transport.onclose = () => wrapped.onclose?.();
|
|
29
|
+
transport.onmessage = (message, extra) => {
|
|
30
|
+
const error = denial(message);
|
|
31
|
+
if (!error) { wrapped.onmessage?.(message, extra); return; }
|
|
32
|
+
// Sending through the original channel settles its pending request id.
|
|
33
|
+
Promise.resolve().then(() => transport.send({ jsonrpc: '2.0', id: message.id,
|
|
34
|
+
error: { code: error.code, message: error.message, data: error.data } }))
|
|
35
|
+
.catch(cause => wrapped.onerror?.(cause));
|
|
36
|
+
};
|
|
37
|
+
return wrapped;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function modernToolUiDenial(message, { era, runtime, registrations }) {
|
|
41
|
+
if (era !== 'modern' || message?.method !== 'tools/call') return null;
|
|
42
|
+
const classified = classifyInboundRequest({ httpMethod: 'POST', body: message });
|
|
43
|
+
if (classified.kind !== 'modern' || classified.messageKind !== 'request') return null;
|
|
44
|
+
const entry = registrations.get(message.params?.name);
|
|
45
|
+
if (!entry?.registered.enabled || !requiresMcpAppUi(runtime, entry.tool)
|
|
46
|
+
|| declaresMcpAppUi(message.params?._meta?.[CLIENT_CAPABILITIES_META_KEY], runtime.ui.mimeType)) return null;
|
|
47
|
+
const checked = entry.schema['~standard'].validate(message.params.arguments ?? {});
|
|
48
|
+
// The pinned fromJsonSchema provider is synchronous. Never treat an unknown
|
|
49
|
+
// asynchronous provider as proof that arguments passed validation.
|
|
50
|
+
if (checked?.then || checked.issues?.length) return null;
|
|
51
|
+
return missingMcpAppUi(runtime.ui.mimeType);
|
|
52
|
+
}
|
|
5
53
|
export const requestMcpClientInfo = (server, context) => context?.mcpReq?.envelope?.[CLIENT_INFO_META_KEY] ?? server.getClientVersion();
|
|
6
54
|
export const requestMcpClientCapabilities = (server, context) => context?.mcpReq?.envelope?.[CLIENT_CAPABILITIES_META_KEY] ?? server.getClientCapabilities();
|
|
7
55
|
|
package/src/worker-entry.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { subscribe } from "node:diagnostics_channel";
|
|
2
2
|
import { parentPort, workerData } from "node:worker_threads";
|
|
3
|
-
import { OWNED_LOCK_CHANNEL, createMcpDispatchRuntime } from "
|
|
3
|
+
import { OWNED_LOCK_CHANNEL, createMcpDispatchRuntime } from "#agdf-dispatch-worker";
|
|
4
4
|
import { ownedLockForwarder } from "./worker.js";
|
|
5
5
|
|
|
6
6
|
function send(message) {
|
|
@@ -14,7 +14,7 @@ subscribe(OWNED_LOCK_CHANNEL, ownedLockForwarder(send));
|
|
|
14
14
|
let runtime, phase = 'setup';
|
|
15
15
|
try {
|
|
16
16
|
send({ type: 'phase', phase: 'runtime_provenance' });
|
|
17
|
-
runtime = createMcpDispatchRuntime({ surface: workerData.surface, ...(workerData.commandGroups !== undefined ? { commandGroups: workerData.commandGroups } : {}) });
|
|
17
|
+
runtime = createMcpDispatchRuntime({ surface: workerData.surface, materialGrants: workerData.materialGrants, inputContext: workerData.inputContext, ...(workerData.commandGroups !== undefined ? { commandGroups: workerData.commandGroups } : {}) });
|
|
18
18
|
if (runtime.trustedContext.provenanceStatus !== "matched") {
|
|
19
19
|
send({ type: "failure", code: "runtime_provenance_invalid", dispatch: "not_dispatched" });
|
|
20
20
|
} else if (runtime.trustedContext.expectedVersion !== workerData.expectedVersion) {
|