@agdf/mcp-server 0.30.0 → 0.31.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 +26 -2
- package/package.json +2 -2
- package/src/generation-runtime.js +11 -2
- package/src/main.js +1 -0
- package/src/next-action.js +227 -0
- package/src/prepared-call-transport.js +306 -0
- package/src/safe-read-continuation.js +62 -0
- package/src/server.js +139 -35
- package/ui/cockpit.html +2 -2
- package/ui/decision-manifest.json +3 -3
- package/ui/decision.html +2 -2
- package/ui/manifest.json +3 -3
package/src/server.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { createPreparedCallTransport, preparedTransportSchema } from './prepared-call-transport.js';
|
|
2
|
+
import { appendSafeReadContinuation, nextActionOutputSchema } from './safe-read-continuation.js';
|
|
3
|
+
import { createNextActionProjection } from './next-action.js';
|
|
1
4
|
import { boundStdioOutput } from './stdio-output.js';
|
|
2
5
|
import { McpServer, ProtocolError, ProtocolErrorCode, fromJsonSchema } from "@modelcontextprotocol/server";
|
|
3
6
|
import { serveStdio, StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
@@ -11,14 +14,40 @@ import { requestDeclaresMcpAppUi, requestMcpClientInfo, requestMcpClientCapabili
|
|
|
11
14
|
|
|
12
15
|
export const SERVER_NAME = "agdf-mcp";
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
// A folded follow-up read is a convenience. Hosts move large tool output into
|
|
18
|
+
// files (Claude Code from roughly 80 KB of text), which costs the model more
|
|
19
|
+
// round trips than a separate read, so a read is inlined only below this size.
|
|
20
|
+
const INLINE_READ_TEXT_BYTES = 48 * 1024;
|
|
21
|
+
const byteLength = value => Buffer.byteLength(typeof value === 'string' ? value : JSON.stringify(value), 'utf8');
|
|
22
|
+
|
|
23
|
+
function omittedFollowup(nextRead) {
|
|
24
|
+
return { ...nextRead, result: null, failure: {
|
|
25
|
+
code: 'read_followup_output_limit', phase: 'wire_budget', original_effect_unchanged: true,
|
|
26
|
+
instruction: 'The original result and receipt are unchanged. Its follow-up read was not inlined to keep this response small; run only that supplied read call separately.',
|
|
27
|
+
} };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function toolResult(runtime, name, input, result, { serialize = runtime.serialize, consume = runtime.consumeToolResult,
|
|
31
|
+
provider, peer, facts, preparedCalls, actions, nextRead } = {}) {
|
|
15
32
|
// A generation must supply its own descriptor. Never reinterpret old results
|
|
16
33
|
// with the facade's current runtime. Local owners project after postprocessing.
|
|
17
|
-
const emit = value => {
|
|
34
|
+
const emit = (value, { followup, decorate = true } = {}) => {
|
|
18
35
|
const projected = Object.hasOwn(value._meta ?? {}, 'agdf/tool-result') || runtime.requiresProjectedResult ? value
|
|
19
36
|
: runtime.projectToolResult(name, input, value, facts);
|
|
20
|
-
const { domain, metadata, descriptor } =
|
|
21
|
-
|
|
37
|
+
const { domain, metadata, descriptor } = consume(name, projected);
|
|
38
|
+
// Validate and bound the canonical domain with its existing owner first.
|
|
39
|
+
// Additive MCP navigation has its own explicit wire projection; it is not
|
|
40
|
+
// passed back into a closed Core result serializer as canonical state.
|
|
41
|
+
const canonical = JSON.parse(serialize(domain));
|
|
42
|
+
const navigable = followup ? { ...canonical, next_read: followup } : canonical;
|
|
43
|
+
const decorated = decorate && preparedCalls ? preparedCalls.decorate(navigable) : navigable;
|
|
44
|
+
if (decorated.next_read?.result?.report?.material_transport?.instructions) {
|
|
45
|
+
const transport = decorated.next_read.result.report.material_transport;
|
|
46
|
+
transport.instructions = transport.instructions.replaceAll('response.structuredContent.report',
|
|
47
|
+
'response.structuredContent.next_read.result.report').replaceAll('JSON.parse(response.content[0].text).report',
|
|
48
|
+
'JSON.parse(response.content[0].text).next_read.result.report');
|
|
49
|
+
}
|
|
50
|
+
const text = JSON.stringify(actions ? actions.project(decorated) : decorated);
|
|
22
51
|
const response = {
|
|
23
52
|
content: [{ type: 'text', text: domain.presentation_delivery?.text ?? text },
|
|
24
53
|
...(descriptor.failed ? [{ type: 'text', text: descriptor.message }] : [])],
|
|
@@ -29,18 +58,29 @@ function toolResult(runtime, name, input, result, serialize = runtime.serialize,
|
|
|
29
58
|
};
|
|
30
59
|
return response;
|
|
31
60
|
};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
61
|
+
const withinLimit = response => !runtime.responseLimit || byteLength(response) <= runtime.responseLimit;
|
|
62
|
+
// Optional additions are dropped before the result itself is touched: first the
|
|
63
|
+
// inlined read, then the transport decoration (whose recovery copies can double
|
|
64
|
+
// large forms). Only a canonical result that alone exceeds the limit becomes
|
|
65
|
+
// resource_limit, so navigation can never turn a committed write into unknown.
|
|
66
|
+
const variants = [
|
|
67
|
+
...(nextRead?.result ? [{ followup: nextRead, inlineRead: true }] : []),
|
|
68
|
+
...(nextRead ? [{ followup: nextRead.result ? omittedFollowup(nextRead) : nextRead }] : []),
|
|
69
|
+
{}, { decorate: false },
|
|
70
|
+
];
|
|
71
|
+
for (const { inlineRead, ...variant } of variants) {
|
|
72
|
+
const response = emit(result, variant);
|
|
73
|
+
if (withinLimit(response) && (!inlineRead || byteLength(response.content[0].text) <= INLINE_READ_TEXT_BYTES)) return response;
|
|
39
74
|
}
|
|
75
|
+
// The fallback itself is projected, checked and measured; no recursive call.
|
|
76
|
+
const limitFacts = { code: 'resource_limit', dispatch: 'unknown' };
|
|
77
|
+
const failure = runtime.projectToolResult(name, input, runtime.failure('resource_limit', name, input, limitFacts), limitFacts);
|
|
78
|
+
const response = emit(failure);
|
|
79
|
+
if (!withinLimit(response)) throw Error('tool_result_invalid');
|
|
40
80
|
return response;
|
|
41
81
|
}
|
|
42
82
|
|
|
43
|
-
export function buildAgdfServer({ runtime, executor, era } = {}) {
|
|
83
|
+
export function buildAgdfServer({ runtime, executor, era, preparedCallStore } = {}) {
|
|
44
84
|
if (!runtime?.definition || typeof runtime.serialize !== "function") {
|
|
45
85
|
throw new TypeError("AGDF MCP runtime is required");
|
|
46
86
|
}
|
|
@@ -48,6 +88,15 @@ export function buildAgdfServer({ runtime, executor, era } = {}) {
|
|
|
48
88
|
throw new TypeError("AGDF MCP owned runtime is required");
|
|
49
89
|
}
|
|
50
90
|
const tools = runtime.tools ?? [{ name: runtime.definition.name, definition: runtime.definition, parse: runtime.parse, execute: runtime.execute }];
|
|
91
|
+
// Discovery and retained reads share the original SDK validators. Compile
|
|
92
|
+
// lazily; constructing the transport never validates or executes a request.
|
|
93
|
+
const schemaProvider = new AjvJsonSchemaValidator();
|
|
94
|
+
const deferredSchemas = { getValidator(schema) {
|
|
95
|
+
let validate;
|
|
96
|
+
return value => (validate ??= schemaProvider.getValidator(schema))(value);
|
|
97
|
+
} };
|
|
98
|
+
const inspectTool = tools.find(tool => tool.name === 'agdf_inspect');
|
|
99
|
+
const validateInspect = inspectTool && deferredSchemas.getValidator(inspectTool.definition.inputSchema);
|
|
51
100
|
const dispatchExecutor = executor ?? {
|
|
52
101
|
execute: async (argumentsValue, { toolName, signal } = {}) => {
|
|
53
102
|
const tool = tools.find((entry) => entry.name === (toolName ?? runtime.definition.name));
|
|
@@ -59,7 +108,14 @@ export function buildAgdfServer({ runtime, executor, era } = {}) {
|
|
|
59
108
|
},
|
|
60
109
|
close: async () => runtime.close?.(),
|
|
61
110
|
};
|
|
62
|
-
const registrations = new Map()
|
|
111
|
+
const registrations = new Map(), preparedCalls = createPreparedCallTransport({
|
|
112
|
+
store: preparedCallStore,
|
|
113
|
+
// A read reference retains a supplied canonical request, never an inferred
|
|
114
|
+
// target. Validate the unwrapped arguments with the original SDK schema.
|
|
115
|
+
validateRead: call => inspectTool?.definition.annotations?.readOnlyHint === true
|
|
116
|
+
&& validateInspect(call.arguments).valid,
|
|
117
|
+
}),
|
|
118
|
+
actions = createNextActionProjection({ isAvailable: call => preparedCalls.isAvailable(call) });
|
|
63
119
|
class AgdfServer extends McpServer {
|
|
64
120
|
async connect(transport) {
|
|
65
121
|
return super.connect(capabilityTransport(transport, message => modernToolUiDenial(message,
|
|
@@ -68,7 +124,8 @@ export function buildAgdfServer({ runtime, executor, era } = {}) {
|
|
|
68
124
|
}
|
|
69
125
|
const server = new AgdfServer(
|
|
70
126
|
{ name: SERVER_NAME, version: runtime.trustedContext.expectedVersion },
|
|
71
|
-
{ capabilities: { tools: {}, ...(runtime.ui ? { resources: {} } : {}) }
|
|
127
|
+
{ capabilities: { tools: {}, ...(runtime.ui ? { resources: {} } : {}) },
|
|
128
|
+
instructions: 'Use the returned next_action.call as the normal path for reads and writes. Fill only its named inputs from actual authored content, review or a new deliberate reply; ready calls retain inputs:{} and are forwarded unchanged. The adapter retains the exact target, Run, revision, selectors and original operation identity. Do not reconstruct raw reads or status requests, retype references or use cloning helpers. Owned plugin references retain their original lifetime across reconnects at the same runtime; other connections use explicit recovery_only instructions and the saved original request. A safe inspect may already be included as next_read.result; consume it without repeating it. next_read.tool/arguments describe that executed read; next_read.call is its supplied retry only. A terminal action stops work. No reference or continuation grants authority or approval.' },
|
|
72
129
|
);
|
|
73
130
|
// Capability checks guard execution and resource access. Codex must first
|
|
74
131
|
// discover the plugin tool metadata before selecting its UI-capable route.
|
|
@@ -81,26 +138,61 @@ export function buildAgdfServer({ runtime, executor, era } = {}) {
|
|
|
81
138
|
connection_id: randomUUID(), server_module: fileURLToPath(import.meta.url),
|
|
82
139
|
version: runtime.trustedContext.expectedVersion,
|
|
83
140
|
}) : undefined;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
141
|
+
// The handler validates a result before choosing its follow-up read; the
|
|
142
|
+
// response variants then reuse that validation instead of repeating it.
|
|
143
|
+
const consumed = new WeakMap();
|
|
144
|
+
const consume = (name, value) => {
|
|
145
|
+
if (!consumed.has(value)) consumed.set(value, runtime.consumeToolResult(name, value));
|
|
146
|
+
return consumed.get(value);
|
|
147
|
+
};
|
|
148
|
+
const respond = (name, input, result, serialize, context, facts, nextRead) => toolResult(runtime, name, input, result, {
|
|
149
|
+
serialize, consume, provider, facts, preparedCalls, actions, nextRead, peer: provider ? {
|
|
150
|
+
client_info: requestMcpClientInfo(server.server, context), capabilities: requestMcpClientCapabilities(server.server, context),
|
|
151
|
+
} : undefined });
|
|
152
|
+
// Companion events (SD SDD-006): only the process that executed a call journals its final result.
|
|
153
|
+
// Behind the generation facade (projected results) the generation has journaled every result it
|
|
154
|
+
// produced; a failure thrown here (lost connection, unknown response, unavailable generation,
|
|
155
|
+
// expired session) never reached a generation result, so the facade journals it itself.
|
|
156
|
+
const journaling = () => !runtime.requiresProjectedResult && typeof runtime.observeOutcome === 'function';
|
|
157
|
+
const journalingFailures = () => typeof runtime.observeOutcome === 'function';
|
|
158
|
+
const journal = (name, input, projected) => {
|
|
159
|
+
try { runtime.observeOutcome(name, input, projected); } catch { /* notification only */ }
|
|
160
|
+
};
|
|
87
161
|
const uiAllowed = (context) => !gated || requestDeclaresMcpAppUi(server.server, context, runtime.ui.mimeType);
|
|
88
162
|
const supportsUi = context => !!runtime.ui && requestDeclaresMcpAppUi(server.server, context, runtime.ui.mimeType);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
163
|
+
const nextRead = async (result, context) => appendSafeReadContinuation(result, {
|
|
164
|
+
allowed: !context.signal?.aborted && !!inspectTool && inspectTool.definition.annotations?.readOnlyHint === true
|
|
165
|
+
&& uiAllowed(context) && (!inspectTool.requiresUi || supportsUi(context)),
|
|
166
|
+
validate: call => ['working_directory', 'presentation_language', 'primary_target', 'target_source']
|
|
167
|
+
.every(key => typeof call.arguments[key] === 'string' && call.arguments[key].length)
|
|
168
|
+
&& validateInspect(call.arguments).valid,
|
|
169
|
+
execute: async call => {
|
|
170
|
+
const name = call.tool;
|
|
171
|
+
const input = runtime.prepareInput?.(name, call.arguments) ?? call.arguments;
|
|
172
|
+
const parentOwned = typeof inspectTool.parentOwned === 'function' ? inspectTool.parentOwned(input) : inspectTool.parentOwned;
|
|
173
|
+
let observed = parentOwned
|
|
174
|
+
? await inspectTool.execute(inspectTool.parse(input), context.signal,
|
|
175
|
+
{ client: requestMcpClientInfo(server.server, context), uiSupported: supportsUi(context) })
|
|
176
|
+
: await dispatchExecutor.execute(input, { signal: context.signal, toolName: name,
|
|
177
|
+
trusted: runtime.prepareDispatch?.(name, input) });
|
|
178
|
+
if (runtime.postprocessToolResult) observed = await runtime.postprocessToolResult(name, input, observed,
|
|
179
|
+
{ uiSupported: supportsUi(context), client: requestMcpClientInfo(server.server, context) });
|
|
180
|
+
const projected = runtime.projectToolResult(name, input, observed);
|
|
181
|
+
const { domain } = runtime.consumeToolResult(name, projected);
|
|
182
|
+
return JSON.parse((inspectTool.serialize ?? runtime.serialize)(domain));
|
|
183
|
+
},
|
|
184
|
+
});
|
|
97
185
|
for (const tool of tools) {
|
|
98
186
|
const { name, definition, serialize } = tool;
|
|
99
|
-
const schema = fromJsonSchema(definition.inputSchema, { getValidator(inputSchema) {
|
|
187
|
+
const schema = fromJsonSchema(preparedTransportSchema(name, definition.inputSchema), { getValidator(inputSchema) {
|
|
100
188
|
const validate = deferredSchemas.getValidator(inputSchema);
|
|
101
189
|
return input => {
|
|
102
190
|
const checked = validate(input);
|
|
103
191
|
if (checked.valid) return checked;
|
|
192
|
+
const preparedHint = preparedCalls.validationHint(name, input);
|
|
193
|
+
// Validation has already failed. The exact transport correction is more
|
|
194
|
+
// useful here than every unrelated alternative in the raw-command union.
|
|
195
|
+
if (preparedHint) return { ...checked, errorMessage: preparedHint };
|
|
104
196
|
const hint = runtime.inputValidationHint?.(name, input);
|
|
105
197
|
return hint ? { ...checked, errorMessage: `${hint} Schema: ${checked.errorMessage}` } : checked;
|
|
106
198
|
};
|
|
@@ -111,7 +203,7 @@ export function buildAgdfServer({ runtime, executor, era } = {}) {
|
|
|
111
203
|
description: definition.description,
|
|
112
204
|
annotations: definition.annotations,
|
|
113
205
|
inputSchema: schema,
|
|
114
|
-
...(definition.outputSchema ? { outputSchema: fromJsonSchema(definition.outputSchema, deferredSchemas) } : {}),
|
|
206
|
+
...(definition.outputSchema ? { outputSchema: fromJsonSchema(nextActionOutputSchema(name, definition.outputSchema), deferredSchemas) } : {}),
|
|
115
207
|
...(definition._meta ? { _meta: definition._meta } : {}),
|
|
116
208
|
},
|
|
117
209
|
async (argumentsValue, context) => {
|
|
@@ -123,10 +215,12 @@ export function buildAgdfServer({ runtime, executor, era } = {}) {
|
|
|
123
215
|
const failure = runtime.projectToolResult(name, argumentsValue, runtime.failure(denied, name, argumentsValue, facts), facts);
|
|
124
216
|
return respond(name, argumentsValue, failure, serialize ?? runtime.serialize, context, facts);
|
|
125
217
|
}
|
|
126
|
-
|
|
127
|
-
let phase =
|
|
218
|
+
let parentOwned;
|
|
219
|
+
let phase = 'parse';
|
|
128
220
|
try {
|
|
129
221
|
phase = 'parse';
|
|
222
|
+
argumentsValue = preparedCalls.resolve(name, argumentsValue);
|
|
223
|
+
parentOwned = typeof tool.parentOwned === 'function' ? tool.parentOwned(argumentsValue) : tool.parentOwned;
|
|
130
224
|
argumentsValue = runtime.prepareInput?.(name, argumentsValue) ?? argumentsValue;
|
|
131
225
|
phase = parentOwned ? 'parse' : 'execute';
|
|
132
226
|
let result;
|
|
@@ -138,13 +232,23 @@ export function buildAgdfServer({ runtime, executor, era } = {}) {
|
|
|
138
232
|
trusted: runtime.prepareDispatch?.(name, argumentsValue) });
|
|
139
233
|
if (runtime.postprocessToolResult) result = await runtime.postprocessToolResult(name, argumentsValue, result,
|
|
140
234
|
{ uiSupported: supportsUi(context), client: requestMcpClientInfo(server.server, context) });
|
|
141
|
-
|
|
235
|
+
// Validate the producer and its canonical domain before consuming any
|
|
236
|
+
// supplied continuation. Malformed output must not start another read.
|
|
237
|
+
const checked = Object.hasOwn(result._meta ?? {}, 'agdf/tool-result') || runtime.requiresProjectedResult ? result
|
|
238
|
+
: runtime.projectToolResult(name, argumentsValue, result);
|
|
239
|
+
const { domain } = consume(name, checked);
|
|
240
|
+
if (journaling()) journal(name, argumentsValue, checked);
|
|
241
|
+
const continued = await nextRead(domain, context);
|
|
242
|
+
return respond(name, argumentsValue, checked, serialize ?? runtime.serialize, context, undefined, continued.next_read);
|
|
142
243
|
} catch (error) {
|
|
143
244
|
const facts = runtime.normalizeFailure(error, { phase });
|
|
144
|
-
const
|
|
245
|
+
const help = phase === 'parse' ? preparedCalls.validationHint(name, argumentsValue) : '';
|
|
246
|
+
const failure = { ...runtime.failure(facts.code, name, argumentsValue, { ...facts, elapsed_ms: performance.now() - started }),
|
|
247
|
+
...(help ? { prepared_call_help: help } : {}) };
|
|
145
248
|
// A local execution/delivery failure is owned here, including malformed
|
|
146
249
|
// producer output. It never causes re-execution or a second write.
|
|
147
250
|
const projected = runtime.projectToolResult(name, argumentsValue, failure, facts);
|
|
251
|
+
if (journalingFailures()) journal(name, argumentsValue, projected);
|
|
148
252
|
return respond(name, argumentsValue, projected, serialize ?? runtime.serialize, context, facts);
|
|
149
253
|
}
|
|
150
254
|
},
|
|
@@ -166,7 +270,7 @@ export function buildAgdfServer({ runtime, executor, era } = {}) {
|
|
|
166
270
|
const listedTools = tools.map(({ name, definition }) => ({
|
|
167
271
|
name, description: definition.description, inputSchema: server.toolInputSchemaJson(name),
|
|
168
272
|
annotations: definition.annotations, ...(definition._meta ? { _meta: definition._meta } : {}),
|
|
169
|
-
...(definition.outputSchema ? { outputSchema: definition.outputSchema } : {}),
|
|
273
|
+
...(definition.outputSchema ? { outputSchema: nextActionOutputSchema(name, definition.outputSchema) } : {}),
|
|
170
274
|
}));
|
|
171
275
|
const listedResource = { uri: resource.uri, name: runtime.ui.label ?? 'AGDF Cockpit', mimeType: resource.mimeType, _meta: resource._meta };
|
|
172
276
|
server.server.removeRequestHandler("tools/list");
|
|
@@ -177,14 +281,14 @@ export function buildAgdfServer({ runtime, executor, era } = {}) {
|
|
|
177
281
|
}
|
|
178
282
|
}
|
|
179
283
|
return Object.assign(server, {
|
|
180
|
-
closeAgdfRuntime: async () => { await dispatchExecutor.close(); return runtime.close?.(); },
|
|
284
|
+
closeAgdfRuntime: async () => { preparedCalls.close(); actions.close(); await dispatchExecutor.close(); return runtime.close?.(); },
|
|
181
285
|
});
|
|
182
286
|
}
|
|
183
287
|
|
|
184
|
-
export function serveAgdfStdio({ runtime, executor, onerror } = {}) {
|
|
288
|
+
export function serveAgdfStdio({ runtime, executor, onerror, preparedCallStore } = {}) {
|
|
185
289
|
const instances = new Set();
|
|
186
290
|
const handle = serveStdio(({ era }) => {
|
|
187
|
-
const server = buildAgdfServer({ runtime, executor, era });
|
|
291
|
+
const server = buildAgdfServer({ runtime, executor, era, preparedCallStore });
|
|
188
292
|
instances.add(server);
|
|
189
293
|
return server;
|
|
190
294
|
}, {
|