@agdf/mcp-server 0.0.0-stage → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,211 @@
1
+ import { fork } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { join } from 'node:path';
4
+ import { generationMessage, validGenerationMessage, generationManifest, manifestDigest, generationFailureReason, GENERATION_LIMITS as L } from './generation-protocol.js';
5
+ import { loadCockpitResource } from './cockpit-resource.js';
6
+ import { DispatchExecutionError } from './worker.js';
7
+
8
+ // One live facade, three directly owned children, no process registry or PID
9
+ // discovery. Capacity includes startup and unconfirmed cleanup, not just current.
10
+ export async function createOwnedGenerationFacade({ runtime, publisher, createControlRuntime, surface, cockpitDir, cockpitMode,
11
+ Fork = fork, qualifyMs = L.qualify, closeMs = L.close, invokeMs = L.qualify, onEvent = () => {} } = {}) {
12
+ if (![qualifyMs, closeMs, invokeMs].every(value => Number.isSafeInteger(value) && value > 0)
13
+ || qualifyMs > L.qualify || closeMs > L.close || invokeMs > L.qualify) throw Error('generation_deadline_invalid');
14
+ const epoch = randomUUID(), slots = new Map(), routes = new Map(), presentations = new Map(), operations = new Map(), receipts = new Map();
15
+ let current, nextGeneration = 0, replacing = false, closed = false, closing, actualContext = {}, nativeRequired = false;
16
+ const uiDigest = runtime.ui ? loadCockpitResource(runtime.ui).digest : undefined;
17
+ const expected = manifestDigest(generationManifest(runtime, uiDigest, publisher.initial.sdkDigest));
18
+ const event = (row, kind, reason) => onEvent({ owner_epoch: epoch, generation_id: row?.id ?? null, kind, reason: reason ?? null, occupancy: slots.size });
19
+ const failure = (reason, extra = {}) => ({ outcome: 'rejected', reason, authorizes: false, ...extra });
20
+ function status() {
21
+ return { available: true, owner_epoch: epoch, maximum: L.slots, occupancy: slots.size, current_generation: current?.id ?? null,
22
+ candidate_active: replacing, generations: [...slots.values()].map(row => ({ generation_id: row.id,
23
+ publication_id: row.package.publication_id, state: row.state, active_requests: row.inflight,
24
+ uncertain_operations: row.unknown.size, views: row.observed?.views ?? null,
25
+ confirmed_exit: row.exited, usable: row === current && row.state === 'current' && !row.exited })), authorizes: false };
26
+ }
27
+ function request(row, kind, payload, context = {}, original = null, timeout = qualifyMs) {
28
+ if (row.exited || !row.child?.connected) return Promise.reject(new DispatchExecutionError('generation_unavailable', 'not_dispatched'));
29
+ const id = randomUUID(), message = generationMessage(epoch, row.id, kind, payload, context, original, id);
30
+ if (!validGenerationMessage(message, { epoch, generation: row.id })) return Promise.reject(new DispatchExecutionError('generation_input_limit', 'not_dispatched'));
31
+ return new Promise((resolve, reject) => {
32
+ const timer = setTimeout(() => { row.pending.delete(id); reject(new DispatchExecutionError('generation_response_unknown')); }, timeout);
33
+ row.pending.set(id, { kind, original, resolve, reject, timer });
34
+ row.child.send(message, error => { if (error) { clearTimeout(timer); row.pending.delete(id); reject(new DispatchExecutionError('generation_connection_lost')); } });
35
+ });
36
+ }
37
+ async function observe(row) {
38
+ try { const reply = await request(row, 'session_inspect', {}); row.observed = reply.payload; return row.observed; }
39
+ catch { row.state = 'close_unconfirmed'; return null; }
40
+ }
41
+ async function retire(row, teardown = false) {
42
+ if (row === current || row.inflight || row.unknown.size || row.state === 'closing' || row.state === 'close_unconfirmed') return false;
43
+ const work = await observe(row);
44
+ if (!work || !teardown && work.views || work.inflight || work.executor || work.uncertain) return false;
45
+ row.state = 'closing'; event(row, 'orderly_close');
46
+ let exitTimer;
47
+ const ended = new Promise(resolve => {
48
+ if (row.exited) { resolve(false); return; }
49
+ row.end = resolve; exitTimer = setTimeout(() => { row.state = 'close_unconfirmed'; event(row, 'close_unconfirmed'); resolve(false); }, closeMs);
50
+ });
51
+ try { const ack = await request(row, 'shutdown', {}, {}, null, closeMs);
52
+ row.closeAck = ack.kind === 'closed' && !ack.payload.views && !ack.payload.inflight && !ack.payload.executor && !ack.payload.uncertain;
53
+ } catch { row.state = 'close_unconfirmed'; event(row, 'close_unconfirmed'); }
54
+ const confirmed = await ended; clearTimeout(exitTimer); return confirmed;
55
+ }
56
+ async function reserve(packageInfo, context) {
57
+ if (slots.size >= L.slots) {
58
+ const candidates = [...slots.values()].filter(row => row !== current && !row.inflight && !row.unknown.size
59
+ && !['starting', 'qualifying', 'closing', 'close_unconfirmed'].includes(row.state)).sort((a, b) => a.id - b.id);
60
+ for (const row of candidates) { if (await retire(row)) break; }
61
+ }
62
+ if (slots.size >= L.slots) throw Error('generation_capacity_protected');
63
+ const row = { id: ++nextGeneration, package: packageInfo, state: 'starting', pending: new Map(),
64
+ unknown: new Set(), inflight: 0, exited: false, closeAck: false, observed: null };
65
+ slots.set(row.id, row); event(row, 'reserved'); // Reservation precedes acquisition/load/spawn.
66
+ try {
67
+ const claim = publisher.acquire(packageInfo, { ownerEpoch: epoch, generation: row.id });
68
+ row.child = Fork(join(packageInfo.packageRoot, 'src/generation-entry.js'), [epoch, String(row.id)],
69
+ { stdio: ['ignore', 'ignore', 'ignore', 'ipc'], serialization: 'json', execArgv: [] });
70
+ claim.bindOwnedChild(row.child, { confirmedEnd: () => row.closeAck && !row.inflight && !row.unknown.size });
71
+ row.child.on('message', reply => {
72
+ if (!validGenerationMessage(reply, { epoch, generation: row.id, reply: true })) { event(row, 'invalid_reply'); return; }
73
+ const pending = row.pending.get(reply.request_id);
74
+ if (!pending || pending.original !== reply.original_operation_id) { event(row, 'unknown_reply'); return; }
75
+ const expectedKind = { qualify: 'qualified', invoke: 'result', session_inspect: 'status', shutdown: 'closed' }[pending.kind];
76
+ if (reply.kind !== expectedKind && reply.kind !== 'failure') return;
77
+ clearTimeout(pending.timer); row.pending.delete(reply.request_id);
78
+ if (reply.kind === 'failure') pending.reject(new DispatchExecutionError(reply.payload.reason));
79
+ else {
80
+ if (reply.kind === 'closed') row.closeAck = !reply.payload.views && !reply.payload.inflight && !reply.payload.executor && !reply.payload.uncertain;
81
+ pending.resolve(reply);
82
+ }
83
+ });
84
+ row.child.once('exit', () => {
85
+ row.exited = true;
86
+ for (const pending of row.pending.values()) { clearTimeout(pending.timer); pending.reject(new DispatchExecutionError('generation_connection_lost')); }
87
+ 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); }
89
+ else { row.state = 'close_unconfirmed'; event(row, 'unexpected_exit'); row.end?.(false); }
90
+ });
91
+ row.child.once('error', () => { row.state = 'close_unconfirmed'; event(row, 'spawn_failed'); });
92
+ row.state = 'qualifying'; event(row, 'qualifying');
93
+ const qualified = (await request(row, 'qualify', { surface, commandGroups: [...(runtime.trustedContext.commandGroups ?? [])],
94
+ ...(cockpitDir ? { cockpitDir } : {}), ...(cockpitMode ? { cockpitMode } : {}) }, context)).payload;
95
+ if (manifestDigest(qualified.manifest) !== expected || qualified.producer.server_digest !== packageInfo.digest
96
+ || qualified.producer.dispatcher_digest !== packageInfo.dispatcherDigest || qualified.producer.sdk_digest !== packageInfo.sdkDigest
97
+ || nativeRequired && !qualified.support.app_mediation) throw Error('generation_manifest_incompatible');
98
+ row.state = 'qualified'; event(row, 'qualified'); return row;
99
+ } catch (error) {
100
+ event(row, 'qualification_failed', generationFailureReason(error));
101
+ if (!row.child) { slots.delete(row.id); event(row, 'not_spawned'); }
102
+ else { row.state = 'draining'; await retire(row); }
103
+ throw error;
104
+ }
105
+ }
106
+ const control = {
107
+ status,
108
+ async refresh() { await Promise.all([...slots.values()].filter(row => !row.exited).map(observe)); return status(); },
109
+ inspect(id, input) { const record = receipts.get(id);
110
+ if (record && (record.input.run_id !== input.run_id || record.input.primary_target !== input.primary_target
111
+ || record.input.expected_revision_id !== input.expected_revision_id)) return { status: 'unknown', reason: 'connection_control_binding_invalid', authorizes: false };
112
+ return record ? { status: record.result ? 'recorded' : 'pending',
113
+ request_digest: record.digest, result: record.result, current: status(), authorizes: false }
114
+ : { status: 'unknown', reason: 'connection_control_record_unavailable', current: status(), authorizes: false }; },
115
+ async replace({ input, candidate, requestDigest, revalidate }) {
116
+ const prior = receipts.get(input.operation_id);
117
+ if (prior) return prior.digest !== requestDigest ? failure('operation_payload_conflict')
118
+ : prior.result ? { ...prior.result, outcome: prior.result.outcome === 'accepted' ? 'already_applied' : prior.result.outcome, historical_effect: true, current: status() }
119
+ : { outcome: 'effect_unknown', reason: 'generation_replacement_pending', current: status(), authorizes: false };
120
+ if (receipts.size >= L.receipts) return failure('connection_control_capacity');
121
+ 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;
123
+ let candidateRow;
124
+ try {
125
+ revalidate(); const packageInfo = publisher.resolve(candidate);
126
+ candidateRow = await reserve(packageInfo, actualContext); revalidate();
127
+ const previous = current; current = candidateRow; current.state = 'current';
128
+ if (previous) previous.state = previous.exited ? 'close_unconfirmed' : 'draining';
129
+ event(current, 'admitted'); record.result = { outcome: 'accepted', reason: null, operation_id: input.operation_id,
130
+ request_digest: requestDigest, effect: { runtime_replaced: true, generation_id: current.id }, current: status(), authorizes: false };
131
+ } catch (error) {
132
+ if (candidateRow && candidateRow !== current) { candidateRow.state = 'draining'; await retire(candidateRow); }
133
+ record.result = failure(generationFailureReason(error), { operation_id: input.operation_id, current: status() });
134
+ } finally { replacing = false; }
135
+ record.result.current = status(); return record.result;
136
+ },
137
+ };
138
+ const controlRuntime = createControlRuntime(control);
139
+ function operationKey(input, id = input.operation_id) { return JSON.stringify([input.primary_target, input.run_id, id]); }
140
+ async function invoke(name, args, host = {}, signal) {
141
+ if (signal?.aborted) throw new DispatchExecutionError('dispatch_cancelled', 'not_dispatched');
142
+ if (closed) throw new DispatchExecutionError('connection_closed', 'not_dispatched');
143
+ actualContext = { client: host.client, uiSupported: host.uiSupported === true };
144
+ if (args.operation_id && name === 'agdf_record' && args.command?.kind === 'runtime_generation.replace'
145
+ && operations.has(operationKey(args))) throw new DispatchExecutionError('operation_payload_conflict', 'not_dispatched');
146
+ if (args.operation_id && args.command?.kind !== 'runtime_generation.replace' && receipts.has(args.operation_id))
147
+ throw new DispatchExecutionError('operation_payload_conflict', 'not_dispatched');
148
+ if (name === 'agdf_record' && args.command?.kind === 'runtime_generation.replace'
149
+ || name === 'agdf_inspect' && args.operation === 'runtime-generations') {
150
+ const result = await controlRuntime.tool(name).execute(controlRuntime.tool(name).parse(args));
151
+ return controlRuntime.projectToolResult(name, args, name === 'agdf_inspect' && result.report?.available ? { ...result, report: await control.refresh() } : result);
152
+ }
153
+ if (name === 'agdf_inspect' && args.operation === 'operation-status' && receipts.has(args.module)) {
154
+ const validated = await controlRuntime.tool(name).execute(controlRuntime.tool(name).parse(args));
155
+ return controlRuntime.projectToolResult(name, args, validated.report?.status === 'unknown' || validated.report?.status === 'absent'
156
+ ? { ...validated, report: control.inspect(args.module, args) } : validated);
157
+ }
158
+ const knownSession = args.session_id && routes.get(args.session_id);
159
+ 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;
161
+ const key = id && operationKey(args, id), original = key && operations.get(key);
162
+ const presentationId = args.command?.payload?.presentation_id ?? args.presentation_id;
163
+ const presentationOwner = presentationId && presentations.get(operationKey(args, presentationId));
164
+ const row = knownSession ?? original ?? presentationOwner ?? current;
165
+ if (!row || row.exited) throw new DispatchExecutionError('generation_unavailable', 'not_dispatched');
166
+ if (args.operation_id && !original) {
167
+ if (operations.size >= L.routes) throw new DispatchExecutionError('generation_route_capacity', 'not_dispatched');
168
+ operations.set(key, row);
169
+ }
170
+ ++row.inflight; event(row, 'invoke', name);
171
+ try {
172
+ const result = (await request(row, 'invoke', { name, arguments: args }, actualContext, args.operation_id ?? null, invokeMs)).payload;
173
+ if (args.operation_id && result.outcome === 'effect_unknown') row.unknown.add(key);
174
+ if (name === 'agdf_inspect' && args.operation === 'operation-status' && result.report?.status === 'recorded'
175
+ && result.report.receipt?.operation_id === args.module) row.unknown.delete(key);
176
+ if (result.app_capability === 'available') nativeRequired = true;
177
+ if (result.family_result?.presentation_id && ['accepted', 'already_applied'].includes(result.outcome)) {
178
+ const presentationKey = operationKey(args, result.family_result.presentation_id);
179
+ if (!presentations.has(presentationKey) && presentations.size >= L.routes) throw Error('generation_route_capacity');
180
+ presentations.set(presentationKey, row);
181
+ }
182
+ for (const meta of [result._meta?.agdf_decision, result._meta?.agdf_cockpit]) {
183
+ for (const selector of [meta?.session_id, meta?.read_session_id]) {
184
+ if (selector) { if (routes.size >= L.routes) throw Error('generation_route_capacity'); routes.set(selector, row); }
185
+ }
186
+ }
187
+ return result;
188
+ } catch (error) { if (args.operation_id && error.dispatch !== 'not_dispatched') row.unknown.add(key); throw error; }
189
+ finally { --row.inflight; if (closed && !row.inflight && !row.unknown.size) void retire(row, true); }
190
+ }
191
+ current = await reserve(publisher.initial, {}); current.state = 'current'; event(current, 'admitted');
192
+ // Validate the public request here, but transport that exact public shape.
193
+ // Core parse results contain trusted runtime context and are executable inputs,
194
+ // not wire requests; parsing them again in a child both breaks the contract
195
+ // and copies the entire locale registry into a bounded private message.
196
+ const tools = runtime.tools.map(tool => ({ ...tool, parentOwned: true,
197
+ parse: input => { tool.parse(input); return input; }, execute: input => invoke(tool.name, input) }));
198
+ // The server supplies actual initialize/request context only at this seam.
199
+ for (const tool of tools) tool.execute = (input, signal, host) => invoke(tool.name, input, host, signal);
200
+ return Object.freeze({ ...runtime, tools, tool: name => tools.find(tool => tool.name === name),
201
+ requiresProjectedResult: true, postprocessToolResult: undefined, inspectOwnership: status, runtimeControl: control,
202
+ close() {
203
+ return closing ??= (async () => {
204
+ closed = true;
205
+ const previous = current; current = null; if (previous && !previous.exited) previous.state = 'draining';
206
+ await Promise.allSettled([...slots.values()].map(row => retire(row, true)));
207
+ return status();
208
+ })();
209
+ },
210
+ });
211
+ }
package/src/main.js ADDED
@@ -0,0 +1,56 @@
1
+ import { readFileSync } from "node:fs";
2
+ import process from "node:process";
3
+ import { createAgdfWorkerExecutor, serveAgdfStdio } from "./server.js";
4
+ import { loadCockpitResource } from './cockpit-resource.js';
5
+
6
+ export async function runMcpServer({ surface, cockpitDir, cockpitMode, runtime, executor, ownedPublication, ownedPublisher, onClosed } = {}) {
7
+ if (cockpitMode !== undefined && (cockpitMode !== 'plugin' || surface !== 'codex' || cockpitDir !== undefined)) throw Error('AGDF_COCKPIT_TARGET_INVALID');
8
+ const defaultRuntime = runtime === undefined && cockpitDir === undefined && cockpitMode === undefined;
9
+ // A supplied, bound runtime owns its resources. Do not load a second package
10
+ // composition merely to run an explicit adapter/protocol fixture.
11
+ const dispatcherRuntime = runtime === undefined ? await import('create-agdf/mcp-dispatch-runtime') : null;
12
+ runtime ??= cockpitDir !== undefined || cockpitMode !== undefined ? await dispatcherRuntime.createMcpCockpitRuntime({ surface, cockpitDir, cockpitMode }) : dispatcherRuntime.createMcpDispatchRuntime({ surface });
13
+ const serverVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
14
+ if (runtime.trustedContext.expectedVersion !== serverVersion) {
15
+ process.stderr.write("AGDF_MCP_VERSION_MISMATCH\n");
16
+ process.exitCode = 1;
17
+ return null;
18
+ }
19
+ if (runtime.trustedContext.provenanceStatus !== "matched") {
20
+ process.stderr.write("AGDF_MCP_RUNTIME_PROVENANCE_INVALID\n");
21
+ process.exitCode = 1;
22
+ return null;
23
+ }
24
+ if (defaultRuntime && surface === 'codex' && dispatcherRuntime?.createMcpDecisionRuntime) {
25
+ const ui = { uri: dispatcherRuntime.DECISION_UI_URI, mimeType: 'text/html;profile=mcp-app', limit: 4 * 1024 ** 2, kind: 'decision' };
26
+ ui.digest = loadCockpitResource(ui).digest;
27
+ // A shipped resource does not qualify native App-only mediation. Production
28
+ // actions remain disabled until exact package/host evidence is provided.
29
+ runtime = await dispatcherRuntime.createMcpDecisionRuntime({ runtime, ui });
30
+ }
31
+ if (runtime.mode === 'cockpit') loadCockpitResource(runtime.ui);
32
+ if (ownedPublication) {
33
+ const { createOwnedGenerationFacade } = await import('./generation.js');
34
+ if (!ownedPublisher) throw Error('runtime_publication_unowned');
35
+ const publisher = ownedPublisher;
36
+ runtime = await createOwnedGenerationFacade({ runtime, publisher, surface, cockpitDir, cockpitMode,
37
+ createControlRuntime: runtimeControl => dispatcherRuntime.createMcpDispatchRuntime({ surface, runtimeControl }) });
38
+ }
39
+ const dispatchExecutor = executor ?? (runtime.mode === 'cockpit' ? undefined
40
+ : createAgdfWorkerExecutor(runtime, { releaseOwnedLock: dispatcherRuntime?.releaseAbandonedOwnedLock ?? null }));
41
+ const handle = serveAgdfStdio({
42
+ runtime,
43
+ executor: dispatchExecutor,
44
+ onerror(code) {
45
+ process.stderr.write(`${code}\n`);
46
+ },
47
+ });
48
+ let closing;
49
+ const close = () => closing ??= handle.close().then(result => { onClosed?.(result); return result; });
50
+ process.once("SIGINT", close);
51
+ process.once("SIGTERM", close);
52
+ process.stdin.once("close", close);
53
+ process.stdin.once("end", close);
54
+ if (process.stdin.readableEnded || process.stdin.destroyed) void close();
55
+ return Object.freeze({ close });
56
+ }
package/src/server.js ADDED
@@ -0,0 +1,187 @@
1
+ import { McpServer, ProtocolError, ProtocolErrorCode, fromJsonSchema } from "@modelcontextprotocol/server";
2
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
3
+ import { AjvJsonSchemaValidator } from '@modelcontextprotocol/server/validators/ajv';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { DispatchExecutionError, createWorkerDispatchExecutor } from "./worker.js";
7
+ import { loadCockpitResource } from './cockpit-resource.js';
8
+ import { requestDeclaresMcpAppUi, requestMcpClientInfo, requestMcpClientCapabilities } from "./ui-capability.js";
9
+
10
+ export const SERVER_NAME = "agdf-mcp";
11
+
12
+ function toolResult(runtime, name, input, result, serialize = runtime.serialize, provider, peer, facts) {
13
+ // A generation must supply its own descriptor. Never reinterpret old results
14
+ // with the facade's current runtime. Local owners project after postprocessing.
15
+ const emit = value => {
16
+ const projected = Object.hasOwn(value._meta ?? {}, 'agdf/tool-result') || runtime.requiresProjectedResult ? value
17
+ : runtime.projectToolResult(name, input, value, facts);
18
+ const { domain, metadata, descriptor } = runtime.consumeToolResult(name, projected);
19
+ const text = serialize(domain);
20
+ const response = {
21
+ content: [{ type: 'text', text: domain.presentation_delivery?.text ?? text },
22
+ ...(descriptor.failed ? [{ type: 'text', text: descriptor.message }] : [])],
23
+ structuredContent: JSON.parse(text),
24
+ ...(descriptor.failed ? { isError: true } : {}),
25
+ ...(Object.keys(metadata).length || provider ? { _meta: { ...metadata,
26
+ ...(provider ? { 'agdf/provider': provider, 'agdf/peer': peer } : {}) } } : {}),
27
+ };
28
+ return response;
29
+ };
30
+ let response = emit(result);
31
+ if (runtime.responseLimit && Buffer.byteLength(JSON.stringify(response), 'utf8') > runtime.responseLimit) {
32
+ // The fallback itself is projected, checked and measured; no recursive call.
33
+ const limitFacts = { code: 'resource_limit', dispatch: 'unknown' };
34
+ const failure = runtime.projectToolResult(name, input, runtime.failure('resource_limit', name, input, limitFacts), limitFacts);
35
+ response = emit(failure);
36
+ if (Buffer.byteLength(JSON.stringify(response), 'utf8') > runtime.responseLimit) throw Error('tool_result_invalid');
37
+ }
38
+ return response;
39
+ }
40
+
41
+ export function buildAgdfServer({ runtime, executor } = {}) {
42
+ if (!runtime?.definition || typeof runtime.serialize !== "function") {
43
+ throw new TypeError("AGDF MCP runtime is required");
44
+ }
45
+ if (runtime.trustedContext?.provenanceStatus !== "matched") {
46
+ throw new TypeError("AGDF MCP owned runtime is required");
47
+ }
48
+ const tools = runtime.tools ?? [{ name: runtime.definition.name, definition: runtime.definition, parse: runtime.parse, execute: runtime.execute }];
49
+ const dispatchExecutor = executor ?? {
50
+ execute: async (argumentsValue, { toolName, signal } = {}) => {
51
+ const tool = tools.find((entry) => entry.name === (toolName ?? runtime.definition.name));
52
+ if (!tool) throw new DispatchExecutionError("dispatch_worker_failed");
53
+ let parsed;
54
+ try { parsed = tool.parse(argumentsValue); }
55
+ catch (error) { const facts = runtime.normalizeFailure(error, { phase: 'parse' }); throw new DispatchExecutionError(facts.code, facts.dispatch, facts.field); }
56
+ return tool.execute(parsed, signal);
57
+ },
58
+ close: async () => runtime.close?.(),
59
+ };
60
+ const server = new McpServer(
61
+ { name: SERVER_NAME, version: runtime.trustedContext.expectedVersion },
62
+ { capabilities: { tools: {}, ...(runtime.ui ? { resources: {} } : {}) } },
63
+ );
64
+ // Capability checks guard execution and resource access. Codex must first
65
+ // discover the plugin tool metadata before selecting its UI-capable route.
66
+ const gated = runtime.mode === 'cockpit' && runtime.uiCapabilityRequired === true;
67
+ const discoverBeforeUi = gated && runtime.trustedContext.surface === 'codex';
68
+ // serveStdio pins one SDK server instance for the connection's lifetime.
69
+ // This is a server-issued observation, never a guessed host namespace or approval.
70
+ const provider = gated && runtime.trustedContext.surface === 'codex' ? Object.freeze({
71
+ schema_version: 1, authorizes: false, identity_scope: 'sdk_server_instance',
72
+ connection_id: randomUUID(), server_module: fileURLToPath(import.meta.url),
73
+ version: runtime.trustedContext.expectedVersion,
74
+ }) : undefined;
75
+ const respond = (name, input, result, serialize, context, facts) => toolResult(runtime, name, input, result, serialize, provider, provider ? {
76
+ client_info: requestMcpClientInfo(server.server, context), capabilities: requestMcpClientCapabilities(server.server, context),
77
+ } : undefined, facts);
78
+ const uiAllowed = (context) => !gated || requestDeclaresMcpAppUi(server.server, context, runtime.ui.mimeType);
79
+ const supportsUi = context => !!runtime.ui && requestDeclaresMcpAppUi(server.server, context, runtime.ui.mimeType);
80
+ // Discovery needs the exact schemas, but does not execute any tool. Compile
81
+ // each validator on its first validation, using the same SDK provider/options.
82
+ // Input validation still precedes execution; successful output is still checked.
83
+ const schemaProvider = new AjvJsonSchemaValidator();
84
+ const deferredSchemas = { getValidator(schema) {
85
+ let validate;
86
+ return value => (validate ??= schemaProvider.getValidator(schema))(value);
87
+ } };
88
+ for (const tool of tools) {
89
+ const { name, definition, serialize } = tool;
90
+ server.registerTool(
91
+ name,
92
+ {
93
+ description: definition.description,
94
+ annotations: definition.annotations,
95
+ inputSchema: fromJsonSchema(definition.inputSchema, deferredSchemas),
96
+ ...(definition.outputSchema ? { outputSchema: fromJsonSchema(definition.outputSchema, deferredSchemas) } : {}),
97
+ ...(definition._meta ? { _meta: definition._meta } : {}),
98
+ },
99
+ async (argumentsValue, context) => {
100
+ const denied = !uiAllowed(context) ? runtime.unsupportedUiCode ?? 'resource_denied'
101
+ : tool.requiresUi && !supportsUi(context) ? 'host_ui_unsupported' : null;
102
+ if (denied) {
103
+ const facts = { code: denied, dispatch: 'not_dispatched' };
104
+ const failure = runtime.projectToolResult(name, argumentsValue, runtime.failure(denied, name, argumentsValue, facts), facts);
105
+ return respond(name, argumentsValue, failure, serialize ?? runtime.serialize, context, facts);
106
+ }
107
+ let phase = tool.parentOwned ? 'parse' : 'execute';
108
+ try {
109
+ let result;
110
+ if (tool.parentOwned) {
111
+ const parsed = tool.parse(argumentsValue); phase = 'execute';
112
+ result = await tool.execute(parsed, context.signal,
113
+ { client: requestMcpClientInfo(server.server, context), uiSupported: supportsUi(context) });
114
+ } else result = await dispatchExecutor.execute(argumentsValue, { signal: context.signal, toolName: name });
115
+ if (runtime.postprocessToolResult) result = await runtime.postprocessToolResult(name, argumentsValue, result,
116
+ { uiSupported: supportsUi(context), client: requestMcpClientInfo(server.server, context) });
117
+ return respond(name, argumentsValue, result, serialize ?? runtime.serialize, context);
118
+ } catch (error) {
119
+ const facts = runtime.normalizeFailure(error, { phase });
120
+ const failure = runtime.failure(facts.code, name, argumentsValue, facts);
121
+ // A local execution/delivery failure is owned here, including malformed
122
+ // producer output. It never causes re-execution or a second write.
123
+ const projected = runtime.projectToolResult(name, argumentsValue, failure, facts);
124
+ return respond(name, argumentsValue, projected, serialize ?? runtime.serialize, context, facts);
125
+ }
126
+ },
127
+ );
128
+ }
129
+ if (runtime.ui) {
130
+ const resource = loadCockpitResource(runtime.ui);
131
+ server.registerResource(runtime.mode === 'decision' ? 'AGDF Gate Decision' : runtime.ui.label ?? 'AGDF Cockpit', resource.uri, { mimeType: resource.mimeType, _meta: resource._meta },
132
+ async (_uri, context) => {
133
+ if (!uiAllowed(context) || runtime.mode === 'decision' && !supportsUi(context)) throw new ProtocolError(ProtocolErrorCode.InvalidParams, "resource_denied");
134
+ return { contents: [resource] };
135
+ });
136
+ if (gated || runtime.mode === 'decision') {
137
+ // McpServer's list handlers ignore the request context, so the gated surface lists per request.
138
+ const listedTools = tools.map(({ name, definition }) => ({
139
+ name, description: definition.description, inputSchema: server.toolInputSchemaJson(name),
140
+ annotations: definition.annotations, ...(definition._meta ? { _meta: definition._meta } : {}),
141
+ ...(definition.outputSchema ? { outputSchema: definition.outputSchema } : {}),
142
+ }));
143
+ const listedResource = { uri: resource.uri, name: runtime.ui.label ?? 'AGDF Cockpit', mimeType: resource.mimeType, _meta: resource._meta };
144
+ server.server.removeRequestHandler("tools/list");
145
+ server.server.setRequestHandler("tools/list", (_request, context) => ({ tools: !discoverBeforeUi && !uiAllowed(context) ? [] : runtime.mode === 'decision' && !supportsUi(context)
146
+ ? listedTools.filter(tool => !tools.find(entry => entry.name === tool.name)?.requiresUi) : listedTools }));
147
+ server.server.removeRequestHandler("resources/list");
148
+ server.server.setRequestHandler("resources/list", (_request, context) => ({ resources: uiAllowed(context) && (runtime.mode !== 'decision' || supportsUi(context)) ? [listedResource] : [] }));
149
+ }
150
+ }
151
+ return Object.assign(server, {
152
+ closeAgdfRuntime: async () => { await dispatchExecutor.close(); return runtime.close?.(); },
153
+ });
154
+ }
155
+
156
+ export function serveAgdfStdio({ runtime, executor, onerror } = {}) {
157
+ const instances = new Set();
158
+ const handle = serveStdio(() => {
159
+ const server = buildAgdfServer({ runtime, executor });
160
+ instances.add(server);
161
+ return server;
162
+ }, {
163
+ legacy: "serve",
164
+ onerror: () => onerror?.("AGDF_MCP_TRANSPORT_ERROR"),
165
+ });
166
+ return Object.freeze({
167
+ async close() {
168
+ // The generation facade is prepared before a client initializes. A host
169
+ // may cancel that connection without creating any SDK server instance;
170
+ // its already-owned executor/runtime must still close and prove exit.
171
+ const owners = instances.size ? [...instances].map(server => () => server.closeAgdfRuntime())
172
+ : [async () => { await executor?.close(); return runtime.close?.(); }];
173
+ const results = await Promise.allSettled(owners.map(close => close()));
174
+ await handle.close();
175
+ return { confirmed: results.length > 0 && results.every(result => result.status === 'fulfilled' && result.value?.occupancy === 0) };
176
+ },
177
+ });
178
+ }
179
+
180
+ export function createAgdfWorkerExecutor(runtime, options = {}) {
181
+ return createWorkerDispatchExecutor({
182
+ surface: runtime.trustedContext.surface,
183
+ expectedVersion: runtime.trustedContext.expectedVersion,
184
+ commandGroups: runtime.trustedContext.commandGroups,
185
+ ...options,
186
+ });
187
+ }
@@ -0,0 +1,17 @@
1
+ import { CLIENT_CAPABILITIES_META_KEY, CLIENT_INFO_META_KEY } from "@modelcontextprotocol/server";
2
+
3
+ // Extension id under which MCP Apps hosts declare UI support (@modelcontextprotocol/ext-apps EXTENSION_ID).
4
+ export const MCP_APP_UI_EXTENSION = "io.modelcontextprotocol/ui";
5
+ export const requestMcpClientInfo = (server, context) => context?.mcpReq?.envelope?.[CLIENT_INFO_META_KEY] ?? server.getClientVersion();
6
+ export const requestMcpClientCapabilities = (server, context) => context?.mcpReq?.envelope?.[CLIENT_CAPABILITIES_META_KEY] ?? server.getClientCapabilities();
7
+
8
+ export function declaresMcpAppUi(capabilities, mimeType) {
9
+ const mimeTypes = capabilities?.extensions?.[MCP_APP_UI_EXTENSION]?.mimeTypes;
10
+ return Array.isArray(mimeTypes) && mimeTypes.includes(mimeType);
11
+ }
12
+
13
+ // 2026-07-28 requests carry the capabilities in their envelope; 2025-era connections declare them once
14
+ // in initialize. The SDK does not expose envelope capabilities through getClientCapabilities() here.
15
+ export function requestDeclaresMcpAppUi(server, context, mimeType) {
16
+ return declaresMcpAppUi(requestMcpClientCapabilities(server, context), mimeType);
17
+ }
@@ -0,0 +1,39 @@
1
+ import { subscribe } from "node:diagnostics_channel";
2
+ import { parentPort, workerData } from "node:worker_threads";
3
+ import { OWNED_LOCK_CHANNEL, createMcpDispatchRuntime } from "create-agdf/mcp-dispatch-runtime";
4
+ import { ownedLockForwarder } from "./worker.js";
5
+
6
+ function send(message) {
7
+ parentPort?.postMessage(message);
8
+ }
9
+
10
+ // Forward lock ownership synchronously. If this thread is terminated while it holds
11
+ // a run lock, the executor releases exactly that lock after the thread has exited.
12
+ subscribe(OWNED_LOCK_CHANNEL, ownedLockForwarder(send));
13
+
14
+ let runtime, phase = 'setup';
15
+ try {
16
+ send({ type: 'phase', phase: 'runtime_provenance' });
17
+ runtime = createMcpDispatchRuntime({ surface: workerData.surface, ...(workerData.commandGroups !== undefined ? { commandGroups: workerData.commandGroups } : {}) });
18
+ if (runtime.trustedContext.provenanceStatus !== "matched") {
19
+ send({ type: "failure", code: "runtime_provenance_invalid", dispatch: "not_dispatched" });
20
+ } else if (runtime.trustedContext.expectedVersion !== workerData.expectedVersion) {
21
+ send({ type: "failure", code: "runtime_version_mismatch", dispatch: "not_dispatched" });
22
+ } else {
23
+ const tool = workerData.toolName ? runtime.tool(workerData.toolName) : runtime.tool(runtime.definition.name);
24
+ if (!tool) {
25
+ send({ type: "failure", code: "dispatch_worker_failed", dispatch: "not_dispatched" });
26
+ } else {
27
+ phase = 'parse';
28
+ const parsed = tool.parse(workerData.argumentsValue);
29
+ phase = 'execute';
30
+ send({ type: 'phase', phase: 'operation' });
31
+ const result = await tool.execute(parsed);
32
+ send({ type: 'phase', phase: 'result' });
33
+ send({ type: "result", result });
34
+ }
35
+ }
36
+ } catch (error) {
37
+ const facts = runtime?.normalizeFailure(error, { phase }) ?? { code: 'dispatch_worker_failed', dispatch: 'not_dispatched' };
38
+ send({ type: 'failure', ...facts });
39
+ }