@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/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
- 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 });
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
- revalidate(); const packageInfo = publisher.resolve(candidate);
126
- candidateRow = await reserve(packageInfo, actualContext); revalidate();
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
- const controlRuntime = createControlRuntime(control);
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 (args.operation_id && !original) {
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 ?? null, invokeMs)).payload;
173
- if (args.operation_id && result.outcome === 'effect_unknown') row.unknown.add(key);
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 === args.module) row.unknown.delete(key);
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 (args.operation_id && error.dispatch !== 'not_dispatched') row.unknown.add(key); throw error; }
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 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 });
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 (defaultRuntime && surface === 'codex' && dispatcherRuntime?.createMcpDecisionRuntime) {
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
- ui.digest = loadCockpitResource(ui).digest;
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.createMcpDecisionRuntime({ runtime, ui });
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
- runtime = await createOwnedGenerationFacade({ runtime, publisher, surface, cockpitDir, cockpitMode,
37
- createControlRuntime: runtimeControl => dispatcherRuntime.createMcpDispatchRuntime({ surface, runtimeControl }) });
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, createWorkerDispatchExecutor } from "./worker.js";
7
+ import { DispatchExecutionError } from "./worker.js";
7
8
  import { loadCockpitResource } from './cockpit-resource.js';
8
- import { requestDeclaresMcpAppUi, requestMcpClientInfo, requestMcpClientCapabilities } from "./ui-capability.js";
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 server = new McpServer(
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
- server.registerTool(
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: fromJsonSchema(definition.inputSchema, deferredSchemas),
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
- let phase = tool.parentOwned ? 'parse' : 'execute';
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 (tool.parentOwned) {
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 (!uiAllowed(context) || runtime.mode === 'decision' && !supportsUi(context)) throw new ProtocolError(ProtocolErrorCode.InvalidParams, "resource_denied");
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 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
- }
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
+ }
@@ -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
 
@@ -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 "create-agdf/mcp-dispatch-runtime";
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) {