@aventara/mcp 0.0.0-stage → 1.0.0-rc.1
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/CHANGELOG.md +25 -0
- package/LICENSE +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +144 -2
- package/dist/av-mcp.d.ts +5 -0
- package/dist/av-mcp.js +1 -0
- package/dist/endpoint-access.policy.d.ts +33 -0
- package/dist/endpoint-access.policy.js +89 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1 -0
- package/dist/json-text.writer.d.ts +7 -0
- package/dist/json-text.writer.js +44 -0
- package/dist/mcp.binder.d.ts +73 -0
- package/dist/mcp.binder.js +256 -0
- package/dist/tool-call.translator.d.ts +32 -0
- package/dist/tool-call.translator.js +169 -0
- package/dist/tool-catalog.compiler.d.ts +44 -0
- package/dist/tool-catalog.compiler.js +210 -0
- package/dist/tool-result.encoder.d.ts +27 -0
- package/dist/tool-result.encoder.js +29 -0
- package/package.json +38 -3
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { mcpFailureReporterOf } 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 { authorizedAccess, jsonAnswer, loopbackAccess, metadataAnswer, metadataPathOf, } from "./endpoint-access.policy.js";
|
|
7
|
+
import { translateOperationCall, translateTransactionCall, } from "./tool-call.translator.js";
|
|
8
|
+
import { CONTRACT_RESOURCE_URI, compileToolCatalog, OPERATION_FAMILIES, TRANSACTION_TOOL, } from "./tool-catalog.compiler.js";
|
|
9
|
+
import { internalFailureResult, refusedCallResult, toolResultOf, } from "./tool-result.encoder.js";
|
|
10
|
+
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.`;
|
|
11
|
+
export function bind(framework) {
|
|
12
|
+
const endpoint = framework.mcp;
|
|
13
|
+
if (endpoint === undefined) {
|
|
14
|
+
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.");
|
|
15
|
+
}
|
|
16
|
+
const agent = framework.contracts.agent;
|
|
17
|
+
const protocol = AvProtocol.bind(framework, { origin: "agent" });
|
|
18
|
+
const bound = {
|
|
19
|
+
endpoint,
|
|
20
|
+
protocol,
|
|
21
|
+
identity: { version: agent.protocol.version, hash: agent.protocol.hash },
|
|
22
|
+
tools: compileToolCatalog({
|
|
23
|
+
surface: protocol.surface(),
|
|
24
|
+
maxTransactionOperations: agent.limits.maxTransactionOperations,
|
|
25
|
+
}),
|
|
26
|
+
contractDocument: protocol.encodeContract({
|
|
27
|
+
method: "GET",
|
|
28
|
+
path: "/_contract",
|
|
29
|
+
headers: {},
|
|
30
|
+
body: { kind: "raw", content: "" },
|
|
31
|
+
}).body,
|
|
32
|
+
maxRequestBytes: agent.limits.maxRequestBytes,
|
|
33
|
+
reportFailure: mcpFailureReporterOf(framework),
|
|
34
|
+
};
|
|
35
|
+
const routes = Object.freeze([
|
|
36
|
+
Object.freeze({
|
|
37
|
+
kind: "endpoint",
|
|
38
|
+
path: endpoint.path === "" ? "/" : endpoint.path,
|
|
39
|
+
methods: Object.freeze(["POST", "GET", "DELETE"]),
|
|
40
|
+
}),
|
|
41
|
+
...(endpoint.authorization === undefined
|
|
42
|
+
? []
|
|
43
|
+
: [
|
|
44
|
+
Object.freeze({
|
|
45
|
+
kind: "metadata",
|
|
46
|
+
path: metadataPathOf(endpoint.path),
|
|
47
|
+
methods: Object.freeze(["GET"]),
|
|
48
|
+
}),
|
|
49
|
+
]),
|
|
50
|
+
]);
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
surface: () => routes,
|
|
53
|
+
maxRequestBytes: bound.maxRequestBytes,
|
|
54
|
+
handleEndpoint: (request, options) => answerEndpoint(bound, request, options?.state),
|
|
55
|
+
encodeMetadata: (request) => answerMetadata(bound.endpoint, request),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function headerReader(headers) {
|
|
59
|
+
const lowered = new Map();
|
|
60
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
61
|
+
const values = typeof value === "string" ? [value] : (value ?? []);
|
|
62
|
+
if (values.length > 0) {
|
|
63
|
+
const key = name.toLowerCase();
|
|
64
|
+
const before = lowered.get(key);
|
|
65
|
+
lowered.set(key, before === undefined
|
|
66
|
+
? values.join(", ")
|
|
67
|
+
: `${before}, ${values.join(", ")}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return (name) => lowered.get(name.toLowerCase());
|
|
71
|
+
}
|
|
72
|
+
function urlOf(request) {
|
|
73
|
+
try {
|
|
74
|
+
return new URL(request.url);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const BAD_URL = () => jsonAnswer(400, {
|
|
81
|
+
error: "invalid_request",
|
|
82
|
+
error_description: "The request URL is not absolute.",
|
|
83
|
+
});
|
|
84
|
+
function answerMetadata(endpoint, request) {
|
|
85
|
+
const url = urlOf(request);
|
|
86
|
+
if (url === undefined) {
|
|
87
|
+
return BAD_URL();
|
|
88
|
+
}
|
|
89
|
+
if (endpoint.authorization === undefined) {
|
|
90
|
+
return jsonAnswer(404, {
|
|
91
|
+
error: "not_found",
|
|
92
|
+
error_description: "This MCP endpoint has no authorization.",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return metadataAnswer(request.method, url.origin, endpoint.path, endpoint.authorization);
|
|
96
|
+
}
|
|
97
|
+
const rpcRefusal = (status, message, headers = {}) => jsonAnswer(status, { jsonrpc: "2.0", error: { code: -32000, message }, id: null }, headers);
|
|
98
|
+
async function answerEndpoint(bound, request, state) {
|
|
99
|
+
try {
|
|
100
|
+
const url = urlOf(request);
|
|
101
|
+
if (url === undefined) {
|
|
102
|
+
return BAD_URL();
|
|
103
|
+
}
|
|
104
|
+
const header = headerReader(request.headers);
|
|
105
|
+
const { authorization } = bound.endpoint;
|
|
106
|
+
const access = authorization === undefined
|
|
107
|
+
? loopbackAccess(request.remoteAddress, header)
|
|
108
|
+
: await authorizedAccess(header("authorization"), url.origin, bound.endpoint.path, authorization);
|
|
109
|
+
if (!access.ok) {
|
|
110
|
+
return access.response;
|
|
111
|
+
}
|
|
112
|
+
if (request.method !== "POST") {
|
|
113
|
+
return rpcRefusal(405, "Method not allowed: this endpoint is stateless; use POST.", {
|
|
114
|
+
Allow: "POST",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const coding = header("content-encoding")?.trim().toLowerCase();
|
|
118
|
+
if (coding !== undefined && coding !== "" && coding !== "identity") {
|
|
119
|
+
return rpcRefusal(415, "Unsupported Content-Encoding.");
|
|
120
|
+
}
|
|
121
|
+
return await answerMessage(bound, request, url, seedOf(state, access.caller));
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
bound.reportFailure(error);
|
|
125
|
+
return rpcRefusal(500, "Internal error.");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function seedOf(state, caller) {
|
|
129
|
+
if (state === undefined && caller === undefined) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
return async () => {
|
|
133
|
+
const seeded = state === undefined ? {} : await state();
|
|
134
|
+
return caller === undefined
|
|
135
|
+
? seeded
|
|
136
|
+
: { ...seeded, mcp: { claims: caller.claims } };
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const DROPPED_HEADERS = new Set([
|
|
140
|
+
"host",
|
|
141
|
+
"connection",
|
|
142
|
+
"keep-alive",
|
|
143
|
+
"transfer-encoding",
|
|
144
|
+
"upgrade",
|
|
145
|
+
"content-length",
|
|
146
|
+
"content-encoding",
|
|
147
|
+
]);
|
|
148
|
+
function webRequestOf(request, url) {
|
|
149
|
+
const headers = new Headers();
|
|
150
|
+
for (const [name, value] of Object.entries(request.headers)) {
|
|
151
|
+
if (DROPPED_HEADERS.has(name.toLowerCase()) || value === undefined) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
for (const item of typeof value === "string" ? [value] : value) {
|
|
155
|
+
headers.append(name, item);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return new Request(url, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers,
|
|
161
|
+
body: typeof request.body === "string"
|
|
162
|
+
? request.body
|
|
163
|
+
: new Uint8Array(request.body),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async function answerMessage(bound, request, url, state) {
|
|
167
|
+
const server = serverFor(bound, request.headers, state);
|
|
168
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
169
|
+
enableJsonResponse: true,
|
|
170
|
+
maxRequestBodySize: bound.maxRequestBytes,
|
|
171
|
+
});
|
|
172
|
+
try {
|
|
173
|
+
await server.connect(transport);
|
|
174
|
+
const response = await transport.handleRequest(webRequestOf(request, url));
|
|
175
|
+
const headers = {};
|
|
176
|
+
response.headers.forEach((value, name) => {
|
|
177
|
+
headers[name] = value;
|
|
178
|
+
});
|
|
179
|
+
return { status: response.status, headers, body: await response.text() };
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
await server.close();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function serverFor(bound, headers, state) {
|
|
186
|
+
const server = new Server({ name: "aventara", title: "Aventara", version: bound.identity.hash }, {
|
|
187
|
+
capabilities: { tools: {}, resources: {} },
|
|
188
|
+
instructions: SERVER_INSTRUCTIONS,
|
|
189
|
+
});
|
|
190
|
+
const options = state === undefined ? {} : { state };
|
|
191
|
+
server.setRequestHandler(ListToolsRequestSchema, guarded(bound, () => ({ tools: bound.tools })));
|
|
192
|
+
server.setRequestHandler(CallToolRequestSchema, async (call) => {
|
|
193
|
+
const name = call.params.name;
|
|
194
|
+
if (!bound.tools.some((tool) => tool.name === name)) {
|
|
195
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${name}`);
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
return await callTool(bound, name, call.params.arguments, headers, options);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
return internalFailureResult(bound.reportFailure(error));
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
server.setRequestHandler(ListResourcesRequestSchema, guarded(bound, () => ({
|
|
205
|
+
resources: [
|
|
206
|
+
{
|
|
207
|
+
uri: CONTRACT_RESOURCE_URI,
|
|
208
|
+
name: "contract",
|
|
209
|
+
title: "Agent Contract",
|
|
210
|
+
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.",
|
|
211
|
+
mimeType: "application/json",
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
})));
|
|
215
|
+
server.setRequestHandler(ReadResourceRequestSchema, guarded(bound, (read) => {
|
|
216
|
+
if (read.params.uri !== CONTRACT_RESOURCE_URI) {
|
|
217
|
+
throw new McpError(-32002, `Resource not found: ${read.params.uri}`);
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
contents: [
|
|
221
|
+
{
|
|
222
|
+
uri: CONTRACT_RESOURCE_URI,
|
|
223
|
+
mimeType: "application/json",
|
|
224
|
+
text: bound.contractDocument,
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
};
|
|
228
|
+
}));
|
|
229
|
+
return server;
|
|
230
|
+
}
|
|
231
|
+
function guarded(bound, handler) {
|
|
232
|
+
return async (...args) => {
|
|
233
|
+
try {
|
|
234
|
+
return await handler(...args);
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
if (error instanceof McpError) {
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
throw new McpError(ErrorCode.InternalError, bound.reportFailure(error).cause.message);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
async function callTool(bound, name, input, headers, options) {
|
|
245
|
+
if (name === TRANSACTION_TOOL) {
|
|
246
|
+
const translated = await translateTransactionCall(input, headers, bound.identity);
|
|
247
|
+
return (translated.ok
|
|
248
|
+
? toolResultOf(await bound.protocol.handleTransaction(translated.request, options))
|
|
249
|
+
: refusedCallResult(translated.message));
|
|
250
|
+
}
|
|
251
|
+
const family = OPERATION_FAMILIES.find((candidate) => candidate === name);
|
|
252
|
+
const translated = translateOperationCall(family, input, headers, bound.identity);
|
|
253
|
+
return (translated.ok
|
|
254
|
+
? toolResultOf(await bound.protocol.handleOperation(translated.request, options))
|
|
255
|
+
: refusedCallResult(translated.message));
|
|
256
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
export 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>;
|
|
@@ -0,0 +1,169 @@
|
|
|
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
|
+
export function translateOperationCall(family, input, received, identity) {
|
|
36
|
+
if (!isRecord(input)) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
message: `${family}: the arguments must be an object.`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const { resource, variant, args } = input;
|
|
43
|
+
if (typeof resource !== "string" || typeof variant !== "string") {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
message: `${family}: \`resource\` and \`variant\` must be strings (read ${CONTRACT_RESOURCE_URI}).`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const path = `/_resources/${[resource, family, variant].map(encodeURIComponent).join("/")}`;
|
|
50
|
+
return {
|
|
51
|
+
ok: true,
|
|
52
|
+
kind: "operation",
|
|
53
|
+
request: protocolRequest(path, args === undefined ? {} : args, headersOf(received, identity)),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export async function translateTransactionCall(input, received, identity) {
|
|
57
|
+
if (!isRecord(input) || !Array.isArray(input.steps)) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
message: `${TRANSACTION_TOOL}: the arguments must be { steps: [{ resource, family, variant, args }] }.`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const fingerprints = [];
|
|
64
|
+
const operations = [];
|
|
65
|
+
for (const [index, step] of input.steps.entries()) {
|
|
66
|
+
if (!isRecord(step) ||
|
|
67
|
+
typeof step.resource !== "string" ||
|
|
68
|
+
!isFamily(step.family) ||
|
|
69
|
+
typeof step.variant !== "string") {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
message: `${TRANSACTION_TOOL}: step ${index} must be { resource, family, variant, args } with family one of ${OPERATION_FAMILIES.join(", ")}.`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const args = referenced(step.args === undefined ? {} : step.args, fingerprints);
|
|
76
|
+
const node = {
|
|
77
|
+
resource: step.resource,
|
|
78
|
+
family: step.family,
|
|
79
|
+
variant: step.variant,
|
|
80
|
+
args,
|
|
81
|
+
};
|
|
82
|
+
const fingerprint = await fingerprintOf(node);
|
|
83
|
+
fingerprints.push(fingerprint ?? "");
|
|
84
|
+
operations.push(fingerprint === undefined ? node : { ...node, fingerprint });
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
ok: true,
|
|
88
|
+
kind: "transaction",
|
|
89
|
+
request: protocolRequest("/_transactions", { operations }, headersOf(received, identity)),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function fingerprintOf(node) {
|
|
93
|
+
try {
|
|
94
|
+
return await computeOperationFingerprint(node);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function referenced(value, fingerprints) {
|
|
101
|
+
const root = {};
|
|
102
|
+
const stack = [
|
|
103
|
+
{
|
|
104
|
+
source: value,
|
|
105
|
+
assign: (copy) => {
|
|
106
|
+
root.value = copy;
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
while (stack.length > 0) {
|
|
111
|
+
const { source, assign } = stack.pop();
|
|
112
|
+
if (Array.isArray(source)) {
|
|
113
|
+
const copy = new Array(source.length);
|
|
114
|
+
assign(copy);
|
|
115
|
+
for (let index = source.length - 1; index >= 0; index -= 1) {
|
|
116
|
+
stack.push({
|
|
117
|
+
source: source[index],
|
|
118
|
+
assign: (item) => {
|
|
119
|
+
copy[index] = item;
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!isRecord(source)) {
|
|
126
|
+
assign(source);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const reference = withFingerprint(source, fingerprints);
|
|
130
|
+
if (reference !== undefined) {
|
|
131
|
+
assign(reference);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const copy = {};
|
|
135
|
+
assign(copy);
|
|
136
|
+
const keys = Object.keys(source);
|
|
137
|
+
for (const key of keys) {
|
|
138
|
+
Object.defineProperty(copy, key, {
|
|
139
|
+
value: undefined,
|
|
140
|
+
writable: true,
|
|
141
|
+
enumerable: true,
|
|
142
|
+
configurable: true,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
for (let index = keys.length - 1; index >= 0; index -= 1) {
|
|
146
|
+
const key = keys[index];
|
|
147
|
+
stack.push({
|
|
148
|
+
source: source[key],
|
|
149
|
+
assign: (member) => {
|
|
150
|
+
copy[key] = member;
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return root.value;
|
|
156
|
+
}
|
|
157
|
+
function withFingerprint(value, fingerprints) {
|
|
158
|
+
const reference = value.$ref;
|
|
159
|
+
if (Object.keys(value).length !== 1 || !isRecord(reference)) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
const operation = reference.operation;
|
|
163
|
+
const fingerprint = typeof operation === "number" && Number.isInteger(operation)
|
|
164
|
+
? fingerprints[operation]
|
|
165
|
+
: undefined;
|
|
166
|
+
return fingerprint === undefined || fingerprint === ""
|
|
167
|
+
? value
|
|
168
|
+
: { $ref: { ...reference, fingerprint } };
|
|
169
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
/** Every tool name the catalog can offer. */
|
|
9
|
+
export type McpToolName = OperationFamily | typeof TRANSACTION_TOOL;
|
|
10
|
+
/** An MCP tool's behaviour hints. */
|
|
11
|
+
export type McpToolAnnotations = {
|
|
12
|
+
readonly readOnlyHint: boolean;
|
|
13
|
+
readonly destructiveHint?: boolean;
|
|
14
|
+
readonly openWorldHint: boolean;
|
|
15
|
+
};
|
|
16
|
+
/** A JSON Schema object, as MCP carries it. */
|
|
17
|
+
export type JsonSchemaObject = {
|
|
18
|
+
readonly type: "object";
|
|
19
|
+
readonly properties: Readonly<Record<string, object>>;
|
|
20
|
+
readonly required?: readonly string[];
|
|
21
|
+
readonly additionalProperties?: boolean;
|
|
22
|
+
};
|
|
23
|
+
/** One tool as `tools/list` answers it. */
|
|
24
|
+
export type McpToolDefinition = {
|
|
25
|
+
readonly name: McpToolName;
|
|
26
|
+
readonly title: string;
|
|
27
|
+
readonly description: string;
|
|
28
|
+
readonly inputSchema: JsonSchemaObject;
|
|
29
|
+
readonly outputSchema: JsonSchemaObject;
|
|
30
|
+
readonly annotations: McpToolAnnotations;
|
|
31
|
+
};
|
|
32
|
+
/** The URI of the MCP resource that holds the Agent Contract. */
|
|
33
|
+
export declare const CONTRACT_RESOURCE_URI = "aventara://contract";
|
|
34
|
+
/** What the catalog is compiled from: the agent binding's surface, and its plan limit. */
|
|
35
|
+
export type CatalogSource = {
|
|
36
|
+
readonly surface: ReturnType<AvProtocolBinding["surface"]>;
|
|
37
|
+
readonly maxTransactionOperations: number;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The tools the agent binding's surface supports: one per family it offers, in
|
|
41
|
+
* the protocol's family order, then `transaction` when the surface has the
|
|
42
|
+
* transactions route and at least one operation.
|
|
43
|
+
*/
|
|
44
|
+
export declare function compileToolCatalog(source: CatalogSource): readonly McpToolDefinition[];
|