@ontrails/mcp 1.0.0-beta.1 → 1.0.0-beta.11
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/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +146 -0
- package/README.md +16 -9
- package/dist/annotations.d.ts +2 -2
- package/dist/annotations.d.ts.map +1 -1
- package/dist/annotations.js +8 -6
- package/dist/annotations.js.map +1 -1
- package/dist/blaze.d.ts +5 -2
- package/dist/blaze.d.ts.map +1 -1
- package/dist/blaze.js +13 -2
- package/dist/blaze.js.map +1 -1
- package/dist/build.d.ts +7 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +80 -62
- package/dist/build.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/annotations.test.ts +15 -22
- package/src/__tests__/blaze.test.ts +67 -14
- package/src/__tests__/build.test.ts +188 -61
- package/src/annotations.ts +11 -11
- package/src/blaze.ts +25 -4
- package/src/build.ts +129 -92
package/src/build.ts
CHANGED
|
@@ -7,12 +7,20 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
Result,
|
|
11
|
+
ValidationError,
|
|
12
|
+
executeTrail,
|
|
13
|
+
isBlobRef,
|
|
13
14
|
zodToJsonSchema,
|
|
14
15
|
} from '@ontrails/core';
|
|
15
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
BlobRef,
|
|
18
|
+
Layer,
|
|
19
|
+
ServiceOverrideMap,
|
|
20
|
+
Topo,
|
|
21
|
+
Trail,
|
|
22
|
+
TrailContextInit,
|
|
23
|
+
} from '@ontrails/core';
|
|
16
24
|
|
|
17
25
|
import type { McpAnnotations } from './annotations.js';
|
|
18
26
|
import { deriveAnnotations } from './annotations.js';
|
|
@@ -25,11 +33,12 @@ import { deriveToolName } from './tool-name.js';
|
|
|
25
33
|
|
|
26
34
|
export interface BuildMcpToolsOptions {
|
|
27
35
|
readonly createContext?:
|
|
28
|
-
| (() =>
|
|
36
|
+
| (() => TrailContextInit | Promise<TrailContextInit>)
|
|
29
37
|
| undefined;
|
|
30
38
|
readonly excludeTrails?: readonly string[] | undefined;
|
|
31
39
|
readonly includeTrails?: readonly string[] | undefined;
|
|
32
40
|
readonly layers?: readonly Layer[] | undefined;
|
|
41
|
+
readonly services?: ServiceOverrideMap | undefined;
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
export interface McpToolDefinition {
|
|
@@ -41,6 +50,8 @@ export interface McpToolDefinition {
|
|
|
41
50
|
) => Promise<McpToolResult>;
|
|
42
51
|
readonly inputSchema: Record<string, unknown>;
|
|
43
52
|
readonly name: string;
|
|
53
|
+
/** The trail ID this tool was derived from. */
|
|
54
|
+
readonly trailId: string;
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
export interface McpExtra {
|
|
@@ -68,23 +79,44 @@ export interface McpContent {
|
|
|
68
79
|
// Internal helpers (defined before use)
|
|
69
80
|
// ---------------------------------------------------------------------------
|
|
70
81
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
82
|
+
/** Concatenate an array of Uint8Array chunks into a single Uint8Array. */
|
|
83
|
+
const concatChunks = (
|
|
84
|
+
chunks: Uint8Array[],
|
|
85
|
+
totalLength: number
|
|
86
|
+
): Uint8Array => {
|
|
87
|
+
const result = new Uint8Array(totalLength);
|
|
88
|
+
let offset = 0;
|
|
89
|
+
for (const chunk of chunks) {
|
|
90
|
+
result.set(chunk, offset);
|
|
91
|
+
offset += chunk.length;
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
};
|
|
77
95
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
96
|
+
/** Collect a ReadableStream into a single Uint8Array. */
|
|
97
|
+
const collectStream = async (
|
|
98
|
+
stream: ReadableStream<Uint8Array>
|
|
99
|
+
): Promise<Uint8Array> => {
|
|
100
|
+
const reader = stream.getReader();
|
|
101
|
+
const chunks: Uint8Array[] = [];
|
|
102
|
+
let totalLength = 0;
|
|
103
|
+
for (;;) {
|
|
104
|
+
const { done, value } = await reader.read();
|
|
105
|
+
if (done) {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
chunks.push(value);
|
|
109
|
+
totalLength += value.length;
|
|
81
110
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
)
|
|
111
|
+
return concatChunks(chunks, totalLength);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** Resolve BlobRef data to Uint8Array (handles ReadableStream). */
|
|
115
|
+
const resolveBlobData = (blob: BlobRef): Promise<Uint8Array> | Uint8Array => {
|
|
116
|
+
if (blob.data instanceof ReadableStream) {
|
|
117
|
+
return collectStream(blob.data);
|
|
118
|
+
}
|
|
119
|
+
return blob.data;
|
|
88
120
|
};
|
|
89
121
|
|
|
90
122
|
const uint8ArrayToBase64 = (bytes: Uint8Array): string => {
|
|
@@ -96,10 +128,11 @@ const uint8ArrayToBase64 = (bytes: Uint8Array): string => {
|
|
|
96
128
|
return btoa(binary);
|
|
97
129
|
};
|
|
98
130
|
|
|
99
|
-
const blobToContent = (blob: BlobRef): McpContent => {
|
|
131
|
+
const blobToContent = async (blob: BlobRef): Promise<McpContent> => {
|
|
132
|
+
const bytes = await resolveBlobData(blob);
|
|
100
133
|
if (blob.mimeType.startsWith('image/')) {
|
|
101
134
|
return {
|
|
102
|
-
data: uint8ArrayToBase64(
|
|
135
|
+
data: uint8ArrayToBase64(bytes),
|
|
103
136
|
mimeType: blob.mimeType,
|
|
104
137
|
type: 'image',
|
|
105
138
|
};
|
|
@@ -108,25 +141,25 @@ const blobToContent = (blob: BlobRef): McpContent => {
|
|
|
108
141
|
return {
|
|
109
142
|
mimeType: blob.mimeType,
|
|
110
143
|
type: 'resource',
|
|
111
|
-
uri: `blob://${blob.name
|
|
144
|
+
uri: `blob://${blob.name}`,
|
|
112
145
|
};
|
|
113
146
|
};
|
|
114
147
|
|
|
115
148
|
/** Separate blob fields from non-blob fields in an object. */
|
|
116
|
-
const separateBlobFields = (
|
|
149
|
+
const separateBlobFields = async (
|
|
117
150
|
obj: Record<string, unknown>
|
|
118
|
-
): {
|
|
151
|
+
): Promise<{
|
|
119
152
|
blobContents: McpContent[];
|
|
120
153
|
hasBlobFields: boolean;
|
|
121
154
|
textFields: Record<string, unknown>;
|
|
122
|
-
} => {
|
|
155
|
+
}> => {
|
|
123
156
|
const blobContents: McpContent[] = [];
|
|
124
157
|
const textFields: Record<string, unknown> = {};
|
|
125
158
|
let hasBlobFields = false;
|
|
126
159
|
for (const [key, val] of Object.entries(obj)) {
|
|
127
160
|
if (isBlobRef(val)) {
|
|
128
161
|
hasBlobFields = true;
|
|
129
|
-
blobContents.push(blobToContent(val));
|
|
162
|
+
blobContents.push(await blobToContent(val));
|
|
130
163
|
} else {
|
|
131
164
|
textFields[key] = val;
|
|
132
165
|
}
|
|
@@ -135,10 +168,11 @@ const separateBlobFields = (
|
|
|
135
168
|
};
|
|
136
169
|
|
|
137
170
|
/** Serialize a mixed blob/text object to MCP content. */
|
|
138
|
-
const serializeMixedObject = (
|
|
171
|
+
const serializeMixedObject = async (
|
|
139
172
|
obj: Record<string, unknown>
|
|
140
|
-
): readonly McpContent[] | undefined => {
|
|
141
|
-
const { blobContents, hasBlobFields, textFields } =
|
|
173
|
+
): Promise<readonly McpContent[] | undefined> => {
|
|
174
|
+
const { blobContents, hasBlobFields, textFields } =
|
|
175
|
+
await separateBlobFields(obj);
|
|
142
176
|
if (!hasBlobFields) {
|
|
143
177
|
return undefined;
|
|
144
178
|
}
|
|
@@ -148,12 +182,14 @@ const serializeMixedObject = (
|
|
|
148
182
|
return blobContents;
|
|
149
183
|
};
|
|
150
184
|
|
|
151
|
-
const serializeOutput = (
|
|
185
|
+
const serializeOutput = async (
|
|
186
|
+
value: unknown
|
|
187
|
+
): Promise<readonly McpContent[]> => {
|
|
152
188
|
if (isBlobRef(value)) {
|
|
153
|
-
return [blobToContent(value)];
|
|
189
|
+
return [await blobToContent(value)];
|
|
154
190
|
}
|
|
155
191
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
156
|
-
const mixed = serializeMixedObject(value as Record<string, unknown>);
|
|
192
|
+
const mixed = await serializeMixedObject(value as Record<string, unknown>);
|
|
157
193
|
if (mixed) {
|
|
158
194
|
return mixed;
|
|
159
195
|
}
|
|
@@ -171,48 +207,9 @@ const mcpError = (message: string): McpToolResult => ({
|
|
|
171
207
|
isError: true,
|
|
172
208
|
});
|
|
173
209
|
|
|
174
|
-
/** Build a TrailContext from options and MCP extra. */
|
|
175
|
-
const buildTrailContext = async (
|
|
176
|
-
options: BuildMcpToolsOptions,
|
|
177
|
-
extra: McpExtra
|
|
178
|
-
): Promise<TrailContext> => {
|
|
179
|
-
const baseContext =
|
|
180
|
-
options.createContext !== undefined && options.createContext !== null
|
|
181
|
-
? await options.createContext()
|
|
182
|
-
: createTrailContext();
|
|
183
|
-
|
|
184
|
-
const signal = extra.signal ?? baseContext.signal;
|
|
185
|
-
const progressCb = createMcpProgressCallback(extra);
|
|
186
|
-
|
|
187
|
-
return {
|
|
188
|
-
...baseContext,
|
|
189
|
-
signal,
|
|
190
|
-
...(progressCb === undefined ? {} : { progress: progressCb }),
|
|
191
|
-
};
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
/** Execute a trail and map the result to an MCP response. */
|
|
195
|
-
const executeAndMap = async (
|
|
196
|
-
trail: Trail<unknown, unknown>,
|
|
197
|
-
validatedInput: unknown,
|
|
198
|
-
ctx: TrailContext,
|
|
199
|
-
layers: readonly Layer[]
|
|
200
|
-
): Promise<McpToolResult> => {
|
|
201
|
-
const impl = composeLayers([...layers], trail, trail.implementation);
|
|
202
|
-
try {
|
|
203
|
-
const result = await impl(validatedInput, ctx);
|
|
204
|
-
if (result.isOk()) {
|
|
205
|
-
return { content: serializeOutput(result.value) };
|
|
206
|
-
}
|
|
207
|
-
return mcpError(result.error.message);
|
|
208
|
-
} catch (error: unknown) {
|
|
209
|
-
return mcpError(error instanceof Error ? error.message : String(error));
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
|
|
213
210
|
const createHandler =
|
|
214
211
|
(
|
|
215
|
-
|
|
212
|
+
t: Trail<unknown, unknown>,
|
|
216
213
|
layers: readonly Layer[],
|
|
217
214
|
options: BuildMcpToolsOptions
|
|
218
215
|
): ((
|
|
@@ -220,12 +217,18 @@ const createHandler =
|
|
|
220
217
|
extra: McpExtra
|
|
221
218
|
) => Promise<McpToolResult>) =>
|
|
222
219
|
async (args, extra): Promise<McpToolResult> => {
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
220
|
+
const progressCb = createMcpProgressCallback(extra);
|
|
221
|
+
const result = await executeTrail(t, args, {
|
|
222
|
+
createContext: options.createContext,
|
|
223
|
+
ctx: progressCb === undefined ? undefined : { progress: progressCb },
|
|
224
|
+
layers,
|
|
225
|
+
services: options.services,
|
|
226
|
+
signal: extra.signal,
|
|
227
|
+
});
|
|
228
|
+
if (result.isOk()) {
|
|
229
|
+
return { content: await serializeOutput(result.value) };
|
|
226
230
|
}
|
|
227
|
-
|
|
228
|
-
return executeAndMap(trail, validated.value, ctx, layers);
|
|
231
|
+
return mcpError(result.error.message);
|
|
229
232
|
};
|
|
230
233
|
|
|
231
234
|
// ---------------------------------------------------------------------------
|
|
@@ -238,15 +241,15 @@ const createHandler =
|
|
|
238
241
|
* Each trail in the topo becomes an McpToolDefinition with:
|
|
239
242
|
* - A derived tool name (app-prefixed, underscore-delimited)
|
|
240
243
|
* - JSON Schema input from zodToJsonSchema
|
|
241
|
-
* - MCP annotations from trail
|
|
244
|
+
* - MCP annotations from trail metadata
|
|
242
245
|
* - A handler that validates, composes layers, executes, and maps results
|
|
243
246
|
*/
|
|
244
|
-
/** Check if a trail should be included based on
|
|
247
|
+
/** Check if a trail should be included based on metadata and filters. */
|
|
245
248
|
const shouldInclude = (
|
|
246
249
|
trail: Trail<unknown, unknown>,
|
|
247
250
|
options: BuildMcpToolsOptions
|
|
248
251
|
): boolean => {
|
|
249
|
-
if (trail.
|
|
252
|
+
if (trail.metadata?.['internal'] === true) {
|
|
250
253
|
return false;
|
|
251
254
|
}
|
|
252
255
|
if (options.includeTrails !== undefined && options.includeTrails.length > 0) {
|
|
@@ -295,27 +298,61 @@ const buildToolDefinition = (
|
|
|
295
298
|
handler: createHandler(trail, layers, options),
|
|
296
299
|
inputSchema: zodToJsonSchema(trail.input),
|
|
297
300
|
name: deriveToolName(app.name, trail.id),
|
|
301
|
+
trailId: trail.id,
|
|
298
302
|
};
|
|
299
303
|
};
|
|
300
304
|
|
|
305
|
+
/** Register a trail as an MCP tool, checking for name collisions. */
|
|
306
|
+
const registerTool = (
|
|
307
|
+
app: Topo,
|
|
308
|
+
trailItem: Trail<unknown, unknown>,
|
|
309
|
+
layers: readonly Layer[],
|
|
310
|
+
options: BuildMcpToolsOptions,
|
|
311
|
+
nameToTrailId: Map<string, string>,
|
|
312
|
+
tools: McpToolDefinition[]
|
|
313
|
+
): Result<void, Error> => {
|
|
314
|
+
const toolName = deriveToolName(app.name, trailItem.id);
|
|
315
|
+
const existingId = nameToTrailId.get(toolName);
|
|
316
|
+
if (existingId !== undefined) {
|
|
317
|
+
return Result.err(
|
|
318
|
+
new ValidationError(
|
|
319
|
+
`MCP tool-name collision: trails "${existingId}" and "${trailItem.id}" both derive the tool name "${toolName}"`
|
|
320
|
+
)
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
nameToTrailId.set(toolName, trailItem.id);
|
|
324
|
+
tools.push(buildToolDefinition(app, trailItem, layers, options));
|
|
325
|
+
return Result.ok();
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
/** Filter topo items to eligible trails. */
|
|
329
|
+
const eligibleTrails = (
|
|
330
|
+
app: Topo,
|
|
331
|
+
options: BuildMcpToolsOptions
|
|
332
|
+
): Trail<unknown, unknown>[] =>
|
|
333
|
+
app.list().filter((trail) => shouldInclude(trail, options));
|
|
334
|
+
|
|
301
335
|
export const buildMcpTools = (
|
|
302
336
|
app: Topo,
|
|
303
337
|
options: BuildMcpToolsOptions = {}
|
|
304
|
-
): McpToolDefinition[] => {
|
|
338
|
+
): Result<McpToolDefinition[], Error> => {
|
|
305
339
|
const layers = options.layers ?? [];
|
|
306
340
|
const tools: McpToolDefinition[] = [];
|
|
341
|
+
const nameToTrailId = new Map<string, string>();
|
|
307
342
|
|
|
308
|
-
for (const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
buildToolDefinition(app, item as Trail<unknown, unknown>, layers, options)
|
|
343
|
+
for (const trailItem of eligibleTrails(app, options)) {
|
|
344
|
+
const registered = registerTool(
|
|
345
|
+
app,
|
|
346
|
+
trailItem,
|
|
347
|
+
layers,
|
|
348
|
+
options,
|
|
349
|
+
nameToTrailId,
|
|
350
|
+
tools
|
|
317
351
|
);
|
|
352
|
+
if (registered.isErr()) {
|
|
353
|
+
return registered;
|
|
354
|
+
}
|
|
318
355
|
}
|
|
319
356
|
|
|
320
|
-
return tools;
|
|
357
|
+
return Result.ok(tools);
|
|
321
358
|
};
|