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