@aventara/mcp 0.0.0-stage → 1.0.0-rc.10

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,44 @@
1
+ export function jsonTextOf(value) {
2
+ const output = [];
3
+ const stack = [
4
+ { value },
5
+ ];
6
+ while (stack.length > 0) {
7
+ const task = stack.pop();
8
+ if ("text" in task) {
9
+ output.push(task.text);
10
+ continue;
11
+ }
12
+ const current = task.value;
13
+ if (typeof current !== "object" || current === null) {
14
+ output.push(JSON.stringify(current) ?? "null");
15
+ continue;
16
+ }
17
+ if (Array.isArray(current)) {
18
+ output.push("[");
19
+ stack.push({ text: "]" });
20
+ for (let index = current.length - 1; index >= 0; index -= 1) {
21
+ stack.push({ value: current[index] });
22
+ if (index > 0) {
23
+ stack.push({ text: "," });
24
+ }
25
+ }
26
+ continue;
27
+ }
28
+ const record = current;
29
+ const keys = Object.keys(record).filter((key) => hasJsonForm(record[key]));
30
+ output.push("{");
31
+ stack.push({ text: "}" });
32
+ for (let index = keys.length - 1; index >= 0; index -= 1) {
33
+ const key = keys[index];
34
+ stack.push({ value: record[key] });
35
+ stack.push({ text: `${index > 0 ? "," : ""}${JSON.stringify(key)}:` });
36
+ }
37
+ }
38
+ return output.join("");
39
+ }
40
+ function hasJsonForm(value) {
41
+ return (value !== undefined &&
42
+ typeof value !== "function" &&
43
+ typeof value !== "symbol");
44
+ }
@@ -0,0 +1,74 @@
1
+ import type { AvFramework, AvState } from "@aventara/core";
2
+ import { type AvProtocolResponse } from "@aventara/core/protocol";
3
+ /**
4
+ * One HTTP request to the MCP endpoint or its metadata, as a host hands it to
5
+ * the binding.
6
+ */
7
+ export type AvMcpRequest = {
8
+ /** The HTTP method as received. */
9
+ readonly method: string;
10
+ /**
11
+ * The request's absolute URL as the host sees it (scheme, host, path,
12
+ * query), its proxy settings applied. The OAuth metadata names the endpoint
13
+ * by its origin.
14
+ */
15
+ readonly url: string;
16
+ /** The request headers as received; a repeated header as its list of values. */
17
+ readonly headers: Readonly<Record<string, string | readonly string[] | undefined>>;
18
+ /** The body as received: at most `maxRequestBytes + 1` bytes are needed. */
19
+ readonly body: string | Uint8Array;
20
+ /**
21
+ * The socket peer's address. An endpoint without `authorization` answers
22
+ * loopback callers only.
23
+ */
24
+ readonly remoteAddress?: string | undefined;
25
+ };
26
+ /** The routes a host mounts: the endpoint, then its OAuth metadata when it has `authorization`. */
27
+ type McpRoute = {
28
+ /** `endpoint`: answered by `handleEndpoint`; `metadata`: by `encodeMetadata`. */
29
+ readonly kind: "endpoint" | "metadata";
30
+ /** The absolute path. */
31
+ readonly path: string;
32
+ /** The methods to mount on it; the binding answers each, a refusal included. */
33
+ readonly methods: readonly ("GET" | "POST" | "DELETE")[];
34
+ };
35
+ /** The MCP server bound to one Framework, as `AvMcp.bind` returns it. */
36
+ export type AvMcpBinding = {
37
+ /**
38
+ * The routes to mount: the endpoint at `framework.mcp.path` (`/` at the
39
+ * root) for `POST`, `GET` and `DELETE` (the Streamable HTTP methods; this
40
+ * stateless endpoint answers the last two `405`), and, when the
41
+ * configuration has `mcp.authorization`, the protected-resource metadata at
42
+ * `/.well-known/oauth-protected-resource` followed by the endpoint's path,
43
+ * for `GET`.
44
+ */
45
+ readonly surface: () => readonly McpRoute[];
46
+ /** The largest request body the endpoint reads: the Agent Contract's `maxRequestBytes`. */
47
+ readonly maxRequestBytes: number;
48
+ /**
49
+ * Answers one request to the endpoint: the caller checked (a bearer token
50
+ * through `verify`, or, with no `authorization`, a caller on this machine),
51
+ * then the MCP message answered as JSON, statelessly. A tool call runs as an
52
+ * agent-origin protocol request. Never rejects: a failure of its own (the MCP
53
+ * SDK's) is answered `500` with no detail and reported to the Framework's
54
+ * diagnostics sink as an `AvMcpDiagnostic`; a `verify` that throws is
55
+ * answered `401` and reported the same way.
56
+ */
57
+ readonly handleEndpoint: (request: AvMcpRequest, options?: {
58
+ /**
59
+ * Seeds each tool call's `ctx.$state`, as for a protocol request. With
60
+ * `authorization`, the state also carries `mcp: { claims }`: what
61
+ * `verify` resolved the token to.
62
+ */
63
+ readonly state?: () => AvState | Promise<AvState>;
64
+ }) => Promise<AvProtocolResponse>;
65
+ /** Answers the protected-resource metadata route (RFC 9728). */
66
+ readonly encodeMetadata: (request: AvMcpRequest) => AvProtocolResponse;
67
+ };
68
+ /**
69
+ * `AvMcp.bind(framework)`: the MCP server for a Framework whose configuration
70
+ * has `mcp`, bound once at startup. Its tools and its contract resource are read
71
+ * from the Agent Contract now.
72
+ */
73
+ export declare function bind(framework: AvFramework): AvMcpBinding;
74
+ export {};
@@ -0,0 +1,295 @@
1
+ import { accessProviderOf, mcpFailureReporterOf, projectJsonValue, } from "@aventara/core/internal";
2
+ import { AvProtocol, } from "@aventara/core/protocol";
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
5
+ import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
6
+ import { AjvJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/ajv";
7
+ import { authorizedAccess, jsonAnswer, loopbackAccess, metadataAnswer, metadataPathOf, } from "./endpoint-access.policy.js";
8
+ import { translateOperationCall, translateTransactionCall, } from "./tool-call.translator.js";
9
+ import { CONTRACT_RESOURCE_URI, compileToolCatalog, ME_TOOL, OPERATION_FAMILIES, TRANSACTION_TOOL, } from "./tool-catalog.compiler.js";
10
+ import { internalFailureResult, refusedCallResult, toolResultOf, } from "./tool-result.encoder.js";
11
+ const SERVER_INSTRUCTIONS = `This server reads and writes an application's data through Aventara. Read the resource ${CONTRACT_RESOURCE_URI} first: it describes every resource, its fields and the operations it offers. Then call a tool with { resource, variant, args }. A failed call answers isError with the outcome code and the issues to fix.`;
12
+ export function bind(framework) {
13
+ const endpoint = framework.mcp;
14
+ if (endpoint === undefined) {
15
+ throw new Error("AvMcp.bind: this Framework's configuration has no `mcp`, so it serves no MCP endpoint. Add `mcp: '/mcp'` (or `{ path, authorization }`) to the configuration.");
16
+ }
17
+ const agent = framework.contracts.agent;
18
+ const protocol = AvProtocol.bind(framework, { origin: "agent" });
19
+ const me = accessProviderOf(framework)?.client?.verbs.me;
20
+ const bound = {
21
+ endpoint,
22
+ protocol,
23
+ identity: { version: agent.protocol.version, hash: agent.protocol.hash },
24
+ tools: compileToolCatalog({
25
+ surface: protocol.surface(),
26
+ maxTransactionOperations: agent.limits.maxTransactionOperations,
27
+ me: me !== undefined,
28
+ }),
29
+ contractDocument: protocol.encodeContract({
30
+ method: "GET",
31
+ path: "/_contract",
32
+ headers: {},
33
+ body: { kind: "raw", content: "" },
34
+ }).body,
35
+ maxRequestBytes: agent.limits.maxRequestBytes,
36
+ reportFailure: mcpFailureReporterOf(framework),
37
+ schemaValidator: new AjvJsonSchemaValidator(),
38
+ me,
39
+ };
40
+ const routes = Object.freeze([
41
+ Object.freeze({
42
+ kind: "endpoint",
43
+ path: endpoint.path === "" ? "/" : endpoint.path,
44
+ methods: Object.freeze(["POST", "GET", "DELETE"]),
45
+ }),
46
+ ...(endpoint.authorization === undefined
47
+ ? []
48
+ : [
49
+ Object.freeze({
50
+ kind: "metadata",
51
+ path: metadataPathOf(endpoint.path),
52
+ methods: Object.freeze(["GET"]),
53
+ }),
54
+ ]),
55
+ ]);
56
+ return Object.freeze({
57
+ surface: () => routes,
58
+ maxRequestBytes: bound.maxRequestBytes,
59
+ handleEndpoint: (request, options) => answerEndpoint(bound, request, options?.state),
60
+ encodeMetadata: (request) => answerMetadata(bound.endpoint, request),
61
+ });
62
+ }
63
+ function headerReader(headers) {
64
+ const lowered = new Map();
65
+ for (const [name, value] of Object.entries(headers)) {
66
+ const values = typeof value === "string" ? [value] : (value ?? []);
67
+ if (values.length > 0) {
68
+ const key = name.toLowerCase();
69
+ const before = lowered.get(key);
70
+ lowered.set(key, before === undefined
71
+ ? values.join(", ")
72
+ : `${before}, ${values.join(", ")}`);
73
+ }
74
+ }
75
+ return (name) => lowered.get(name.toLowerCase());
76
+ }
77
+ function urlOf(request) {
78
+ try {
79
+ return new URL(request.url);
80
+ }
81
+ catch {
82
+ return undefined;
83
+ }
84
+ }
85
+ const BAD_URL = () => jsonAnswer(400, {
86
+ error: "invalid_request",
87
+ error_description: "The request URL is not absolute.",
88
+ });
89
+ function answerMetadata(endpoint, request) {
90
+ const url = urlOf(request);
91
+ if (url === undefined) {
92
+ return BAD_URL();
93
+ }
94
+ if (endpoint.authorization === undefined) {
95
+ return jsonAnswer(404, {
96
+ error: "not_found",
97
+ error_description: "This MCP endpoint has no authorization.",
98
+ });
99
+ }
100
+ return metadataAnswer(request.method, url.origin, endpoint.path, endpoint.authorization);
101
+ }
102
+ const rpcRefusal = (status, message, headers = {}) => jsonAnswer(status, { jsonrpc: "2.0", error: { code: -32000, message }, id: null }, headers);
103
+ async function answerEndpoint(bound, request, state) {
104
+ try {
105
+ const url = urlOf(request);
106
+ if (url === undefined) {
107
+ return BAD_URL();
108
+ }
109
+ const header = headerReader(request.headers);
110
+ const { authorization } = bound.endpoint;
111
+ const access = authorization === undefined
112
+ ? loopbackAccess(request.remoteAddress, header)
113
+ : await authorizedAccess(header("authorization"), url.origin, bound.endpoint.path, authorization, bound.reportFailure);
114
+ if (!access.ok) {
115
+ return access.response;
116
+ }
117
+ if (request.method !== "POST") {
118
+ return rpcRefusal(405, "Method not allowed: this endpoint is stateless; use POST.", {
119
+ Allow: "POST",
120
+ });
121
+ }
122
+ const coding = header("content-encoding")?.trim().toLowerCase();
123
+ if (coding !== undefined && coding !== "" && coding !== "identity") {
124
+ return rpcRefusal(415, "Unsupported Content-Encoding.");
125
+ }
126
+ return await answerMessage(bound, request, url, seedOf(state, access.caller));
127
+ }
128
+ catch (error) {
129
+ bound.reportFailure(error);
130
+ return rpcRefusal(500, "Internal error.");
131
+ }
132
+ }
133
+ function seedOf(state, caller) {
134
+ if (state === undefined && caller === undefined) {
135
+ return undefined;
136
+ }
137
+ return async () => {
138
+ const seeded = state === undefined ? {} : await state();
139
+ return caller === undefined
140
+ ? seeded
141
+ : { ...seeded, mcp: { claims: caller.claims } };
142
+ };
143
+ }
144
+ const DROPPED_HEADERS = new Set([
145
+ "host",
146
+ "connection",
147
+ "keep-alive",
148
+ "transfer-encoding",
149
+ "upgrade",
150
+ "content-length",
151
+ "content-encoding",
152
+ ]);
153
+ function webRequestOf(request, url) {
154
+ const headers = new Headers();
155
+ for (const [name, value] of Object.entries(request.headers)) {
156
+ if (DROPPED_HEADERS.has(name.toLowerCase()) || value === undefined) {
157
+ continue;
158
+ }
159
+ for (const item of typeof value === "string" ? [value] : value) {
160
+ headers.append(name, item);
161
+ }
162
+ }
163
+ return new Request(url, {
164
+ method: "POST",
165
+ headers,
166
+ body: typeof request.body === "string"
167
+ ? request.body
168
+ : new Uint8Array(request.body),
169
+ });
170
+ }
171
+ async function answerMessage(bound, request, url, state) {
172
+ const server = serverFor(bound, request.headers, state);
173
+ const transport = new WebStandardStreamableHTTPServerTransport({
174
+ enableJsonResponse: true,
175
+ maxRequestBodySize: bound.maxRequestBytes,
176
+ });
177
+ try {
178
+ await server.connect(transport);
179
+ const response = await transport.handleRequest(webRequestOf(request, url));
180
+ const headers = {};
181
+ response.headers.forEach((value, name) => {
182
+ headers[name] = value;
183
+ });
184
+ return { status: response.status, headers, body: await response.text() };
185
+ }
186
+ finally {
187
+ await server.close();
188
+ }
189
+ }
190
+ function serverFor(bound, headers, state) {
191
+ const server = new Server({ name: "aventara", title: "Aventara", version: bound.identity.hash }, {
192
+ capabilities: { tools: {}, resources: {} },
193
+ instructions: SERVER_INSTRUCTIONS,
194
+ jsonSchemaValidator: bound.schemaValidator,
195
+ });
196
+ const options = state === undefined ? {} : { state };
197
+ server.setRequestHandler(ListToolsRequestSchema, guarded(bound, () => ({ tools: bound.tools })));
198
+ server.setRequestHandler(CallToolRequestSchema, async (call) => {
199
+ const name = call.params.name;
200
+ if (!bound.tools.some((tool) => tool.name === name)) {
201
+ throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${name}`);
202
+ }
203
+ try {
204
+ return await callTool(bound, name, call.params.arguments, headers, options);
205
+ }
206
+ catch (error) {
207
+ return internalFailureResult(bound.reportFailure(error));
208
+ }
209
+ });
210
+ server.setRequestHandler(ListResourcesRequestSchema, guarded(bound, () => ({
211
+ resources: [
212
+ {
213
+ uri: CONTRACT_RESOURCE_URI,
214
+ name: "contract",
215
+ title: "Agent Contract",
216
+ description: "Every resource the tools reach: its fields and their types, what can be filtered, selected, created and updated, the operations it offers, and the request limits.",
217
+ mimeType: "application/json",
218
+ },
219
+ ],
220
+ })));
221
+ server.setRequestHandler(ReadResourceRequestSchema, guarded(bound, (read) => {
222
+ if (read.params.uri !== CONTRACT_RESOURCE_URI) {
223
+ throw new McpError(-32002, `Resource not found: ${read.params.uri}`);
224
+ }
225
+ return {
226
+ contents: [
227
+ {
228
+ uri: CONTRACT_RESOURCE_URI,
229
+ mimeType: "application/json",
230
+ text: bound.contractDocument,
231
+ },
232
+ ],
233
+ };
234
+ }));
235
+ return server;
236
+ }
237
+ function guarded(bound, handler) {
238
+ return async (...args) => {
239
+ try {
240
+ return await handler(...args);
241
+ }
242
+ catch (error) {
243
+ if (error instanceof McpError) {
244
+ throw error;
245
+ }
246
+ throw new McpError(ErrorCode.InternalError, bound.reportFailure(error).cause.message);
247
+ }
248
+ };
249
+ }
250
+ async function callTool(bound, name, input, headers, options) {
251
+ if (name === ME_TOOL) {
252
+ return (await answerMe(bound, input, headers));
253
+ }
254
+ if (name === TRANSACTION_TOOL) {
255
+ const translated = await translateTransactionCall(input, headers, bound.identity);
256
+ return (translated.ok
257
+ ? toolResultOf(await bound.protocol.handleTransaction(translated.request, options))
258
+ : refusedCallResult(translated.message));
259
+ }
260
+ const family = OPERATION_FAMILIES.find((candidate) => candidate === name);
261
+ const translated = translateOperationCall(family, input, headers, bound.identity);
262
+ return (translated.ok
263
+ ? toolResultOf(await bound.protocol.handleOperation(translated.request, options))
264
+ : refusedCallResult(translated.message));
265
+ }
266
+ async function answerMe(bound, input, headers) {
267
+ if (input !== undefined &&
268
+ (typeof input !== "object" ||
269
+ input === null ||
270
+ Array.isArray(input) ||
271
+ Object.keys(input).length > 0)) {
272
+ return refusedCallResult("me takes no arguments: call it with {}.");
273
+ }
274
+ const me = bound.me;
275
+ if (me === undefined) {
276
+ throw new Error("The me tool was called on a binding that offers none.");
277
+ }
278
+ const identity = await me.call({
279
+ args: undefined,
280
+ headers: webHeadersOf(headers),
281
+ });
282
+ const data = identity === null || identity === undefined
283
+ ? null
284
+ : projectJsonValue(identity, me.result);
285
+ return toolResultOf(jsonAnswer(200, { code: "A1000", data, cause: null }));
286
+ }
287
+ function webHeadersOf(headers) {
288
+ const web = new Headers();
289
+ for (const [name, value] of Object.entries(headers)) {
290
+ for (const item of typeof value === "string" ? [value] : (value ?? [])) {
291
+ web.append(name, item);
292
+ }
293
+ }
294
+ return web;
295
+ }
@@ -0,0 +1,33 @@
1
+ import { type AvProtocolRequest } from "@aventara/core/protocol";
2
+ import { type OperationFamily } from "./tool-catalog.compiler.js";
3
+ /** The Agent Contract's identity, sent as the protocol's identity headers. */
4
+ export type AgentIdentity = {
5
+ readonly version: number;
6
+ readonly hash: string;
7
+ };
8
+ /** A tool call as the protocol request that runs it, or why it cannot be one. */
9
+ type TranslatedCall = {
10
+ readonly ok: true;
11
+ readonly kind: "operation" | "transaction";
12
+ readonly request: AvProtocolRequest;
13
+ } | {
14
+ readonly ok: false;
15
+ readonly message: string;
16
+ };
17
+ /**
18
+ * A family tool's call: `{ resource, variant, args? }` as the operation route
19
+ * `/_resources/<resource>/<family>/<variant>` with `args` (or `{}`) as its body.
20
+ * An unknown Resource or variant is the protocol's to refuse (`A2001`, `A2002`);
21
+ * only a call that names none as a string is refused here.
22
+ */
23
+ export declare function translateOperationCall(family: OperationFamily, input: unknown, received: Readonly<Record<string, string | readonly string[] | undefined>>, identity: AgentIdentity): TranslatedCall;
24
+ /**
25
+ * The transaction tool's call: `{ steps: [{ resource, family, variant, args? }] }`
26
+ * as the `/_transactions` plan `{ operations: [...] }`, each step carrying the
27
+ * fingerprint the protocol requires and each `{ $ref: { operation, path } }`
28
+ * the fingerprint of the step it names — computed here, since an agent cannot,
29
+ * by the protocol's own algorithm. A reference that names no earlier step is
30
+ * left as written, for the protocol to refuse.
31
+ */
32
+ export declare function translateTransactionCall(input: unknown, received: Readonly<Record<string, string | readonly string[] | undefined>>, identity: AgentIdentity): Promise<TranslatedCall>;
33
+ export {};
@@ -0,0 +1,172 @@
1
+ import { computeOperationFingerprint } from "@aventara/core/internal";
2
+ import { AvProtocol } from "@aventara/core/protocol";
3
+ import { jsonTextOf } from "./json-text.writer.js";
4
+ import { CONTRACT_RESOURCE_URI, OPERATION_FAMILIES, TRANSACTION_TOOL, } from "./tool-catalog.compiler.js";
5
+ const REPLACED_HEADERS = new Set([
6
+ "content-type",
7
+ "content-length",
8
+ "content-encoding",
9
+ "transfer-encoding",
10
+ AvProtocol.headers.protocolVersion.toLowerCase(),
11
+ AvProtocol.headers.contractHash.toLowerCase(),
12
+ ]);
13
+ function headersOf(received, identity) {
14
+ const headers = {};
15
+ for (const [name, value] of Object.entries(received)) {
16
+ if (!REPLACED_HEADERS.has(name.toLowerCase())) {
17
+ headers[name] = value;
18
+ }
19
+ }
20
+ headers["content-type"] = "application/json";
21
+ headers[AvProtocol.headers.protocolVersion] = String(identity.version);
22
+ headers[AvProtocol.headers.contractHash] = identity.hash;
23
+ return headers;
24
+ }
25
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
26
+ const isFamily = (value) => OPERATION_FAMILIES.includes(value);
27
+ function protocolRequest(path, body, headers) {
28
+ return {
29
+ method: "POST",
30
+ path,
31
+ headers,
32
+ body: { kind: "raw", content: jsonTextOf(body) },
33
+ };
34
+ }
35
+ function routeSegmentOf(name) {
36
+ return encodeURIComponent(name.replace(/\p{Surrogate}/gu, "\uFFFD"));
37
+ }
38
+ export function translateOperationCall(family, input, received, identity) {
39
+ if (!isRecord(input)) {
40
+ return {
41
+ ok: false,
42
+ message: `${family}: the arguments must be an object.`,
43
+ };
44
+ }
45
+ const { resource, variant, args } = input;
46
+ if (typeof resource !== "string" || typeof variant !== "string") {
47
+ return {
48
+ ok: false,
49
+ message: `${family}: \`resource\` and \`variant\` must be strings (read ${CONTRACT_RESOURCE_URI}).`,
50
+ };
51
+ }
52
+ const path = `/_resources/${[resource, family, variant].map(routeSegmentOf).join("/")}`;
53
+ return {
54
+ ok: true,
55
+ kind: "operation",
56
+ request: protocolRequest(path, args === undefined ? {} : args, headersOf(received, identity)),
57
+ };
58
+ }
59
+ export async function translateTransactionCall(input, received, identity) {
60
+ if (!isRecord(input) || !Array.isArray(input.steps)) {
61
+ return {
62
+ ok: false,
63
+ message: `${TRANSACTION_TOOL}: the arguments must be { steps: [{ resource, family, variant, args }] }.`,
64
+ };
65
+ }
66
+ const fingerprints = [];
67
+ const operations = [];
68
+ for (const [index, step] of input.steps.entries()) {
69
+ if (!isRecord(step) ||
70
+ typeof step.resource !== "string" ||
71
+ !isFamily(step.family) ||
72
+ typeof step.variant !== "string") {
73
+ return {
74
+ ok: false,
75
+ message: `${TRANSACTION_TOOL}: step ${index} must be { resource, family, variant, args } with family one of ${OPERATION_FAMILIES.join(", ")}.`,
76
+ };
77
+ }
78
+ const args = referenced(step.args === undefined ? {} : step.args, fingerprints);
79
+ const node = {
80
+ resource: step.resource,
81
+ family: step.family,
82
+ variant: step.variant,
83
+ args,
84
+ };
85
+ const fingerprint = await fingerprintOf(node);
86
+ fingerprints.push(fingerprint);
87
+ operations.push({ ...node, fingerprint });
88
+ }
89
+ return {
90
+ ok: true,
91
+ kind: "transaction",
92
+ request: protocolRequest("/_transactions", { operations }, headersOf(received, identity)),
93
+ };
94
+ }
95
+ async function fingerprintOf(node) {
96
+ try {
97
+ return await computeOperationFingerprint(node);
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ }
103
+ function referenced(value, fingerprints) {
104
+ const root = {};
105
+ const stack = [
106
+ {
107
+ source: value,
108
+ assign: (copy) => {
109
+ root.value = copy;
110
+ },
111
+ },
112
+ ];
113
+ while (stack.length > 0) {
114
+ const { source, assign } = stack.pop();
115
+ if (Array.isArray(source)) {
116
+ const copy = new Array(source.length);
117
+ assign(copy);
118
+ for (let index = source.length - 1; index >= 0; index -= 1) {
119
+ stack.push({
120
+ source: source[index],
121
+ assign: (item) => {
122
+ copy[index] = item;
123
+ },
124
+ });
125
+ }
126
+ continue;
127
+ }
128
+ if (!isRecord(source)) {
129
+ assign(source);
130
+ continue;
131
+ }
132
+ const reference = withFingerprint(source, fingerprints);
133
+ if (reference !== undefined) {
134
+ assign(reference);
135
+ continue;
136
+ }
137
+ const copy = {};
138
+ assign(copy);
139
+ const keys = Object.keys(source);
140
+ for (const key of keys) {
141
+ Object.defineProperty(copy, key, {
142
+ value: undefined,
143
+ writable: true,
144
+ enumerable: true,
145
+ configurable: true,
146
+ });
147
+ }
148
+ for (let index = keys.length - 1; index >= 0; index -= 1) {
149
+ const key = keys[index];
150
+ stack.push({
151
+ source: source[key],
152
+ assign: (member) => {
153
+ copy[key] = member;
154
+ },
155
+ });
156
+ }
157
+ }
158
+ return root.value;
159
+ }
160
+ function withFingerprint(value, fingerprints) {
161
+ const reference = value.$ref;
162
+ if (Object.keys(value).length !== 1 || !isRecord(reference)) {
163
+ return undefined;
164
+ }
165
+ const operation = reference.operation;
166
+ const fingerprint = typeof operation === "number" && Number.isInteger(operation)
167
+ ? fingerprints[operation]
168
+ : undefined;
169
+ return fingerprint === undefined || fingerprint === null
170
+ ? value
171
+ : { $ref: { ...reference, fingerprint } };
172
+ }
@@ -0,0 +1,49 @@
1
+ import type { AvProtocolBinding } from "@aventara/core/protocol";
2
+ /** The operation families, in the order the protocol defines them. */
3
+ export declare const OPERATION_FAMILIES: readonly ["find", "create", "update", "delete", "upsert"];
4
+ /** One operation family: the name of its MCP tool. */
5
+ export type OperationFamily = (typeof OPERATION_FAMILIES)[number];
6
+ /** The name of the tool that runs a plan of operations. */
7
+ export declare const TRANSACTION_TOOL = "transaction";
8
+ /** The name of the tool that answers who the agent acts for. */
9
+ export declare const ME_TOOL = "me";
10
+ /** Every tool name the catalog can offer. */
11
+ export type McpToolName = OperationFamily | typeof TRANSACTION_TOOL | typeof ME_TOOL;
12
+ /** An MCP tool's behaviour hints. */
13
+ export type McpToolAnnotations = {
14
+ readonly readOnlyHint: boolean;
15
+ readonly destructiveHint?: boolean;
16
+ readonly openWorldHint: boolean;
17
+ };
18
+ /** A JSON Schema object, as MCP carries it. */
19
+ export type JsonSchemaObject = {
20
+ readonly type: "object";
21
+ readonly properties: Readonly<Record<string, object>>;
22
+ readonly required?: readonly string[];
23
+ readonly additionalProperties?: boolean;
24
+ };
25
+ /** One tool as `tools/list` answers it. */
26
+ export type McpToolDefinition = {
27
+ readonly name: McpToolName;
28
+ readonly title: string;
29
+ readonly description: string;
30
+ readonly inputSchema: JsonSchemaObject;
31
+ readonly outputSchema: JsonSchemaObject;
32
+ readonly annotations: McpToolAnnotations;
33
+ };
34
+ /** The URI of the MCP resource that holds the Agent Contract. */
35
+ export declare const CONTRACT_RESOURCE_URI = "aventara://contract";
36
+ /** What the catalog is compiled from: the agent binding's surface, its plan limit, and whether `me` is offered. */
37
+ export type CatalogSource = {
38
+ readonly surface: ReturnType<AvProtocolBinding["surface"]>;
39
+ readonly maxTransactionOperations: number;
40
+ /** The access provider states its `client`, so `me` has an answer. */
41
+ readonly me?: boolean;
42
+ };
43
+ /**
44
+ * The tools the agent binding's surface supports: one per family it offers, in
45
+ * the protocol's family order, then `transaction` when the surface has the
46
+ * transactions route and at least one operation, then `me` when the access
47
+ * provider states its `client`.
48
+ */
49
+ export declare function compileToolCatalog(source: CatalogSource): readonly McpToolDefinition[];