@ontrails/mcp 1.0.0-beta.2 → 1.0.0-beta.3
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 +21 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +64 -27
- package/dist/build.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/build.test.ts +90 -13
- package/src/build.ts +92 -40
package/.turbo/turbo-lint.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @ontrails/mcp
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.3
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Bug fixes across all surface packages found via parallel Codex review.
|
|
8
|
+
|
|
9
|
+
**core**: Fix Result.toJson false circular detection on DAGs, deserializeError subclass round-trip, topo cross-kind ID collisions, validateTopo multi-node cycle detection, error example input validation bypass, and deriveFields array type collapse.
|
|
10
|
+
|
|
11
|
+
**cli**: Switch blaze to parseAsync for proper async error handling, add boolean flag negation (--no-flag), and strict number parsing that rejects partial input.
|
|
12
|
+
|
|
13
|
+
**mcp**: Align BlobRef with core (including ReadableStream support) and detect tool-name collisions after normalization.
|
|
14
|
+
|
|
15
|
+
**testing**: Include hikes in testContracts validation, with follow-context awareness.
|
|
16
|
+
|
|
17
|
+
**warden**: Collect hike detour targets, validate detour refs in hike specs, and stop implementation-returns-result from walking into nested function bodies.
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- Updated dependencies
|
|
22
|
+
- @ontrails/core@1.0.0-beta.3
|
|
23
|
+
|
|
3
24
|
## 1.0.0-beta.2
|
|
4
25
|
|
|
5
26
|
### Patch Changes
|
package/dist/build.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,OAAO,KAAK,EAAW,KAAK,EAAE,IAAI,EAAS,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AASvD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,aAAa,CAAC,EACnB,CAAC,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,GAC5C,SAAS,CAAC;IACd,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACvD,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACvD,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,KAAK,EAAE,GAAG,SAAS,CAAC;CAChD;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,EAAE,cAAc,GAAG,SAAS,CAAC;IACjD,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,OAAO,EAAE,CAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,KAAK,EAAE,QAAQ,KACZ,OAAO,CAAC,aAAa,CAAC,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACrD,QAAQ,CAAC,YAAY,CAAC,EAClB,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC,GACnD,SAAS,CAAC;IACd,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;CAC3C;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,CAAC;IACxC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACxC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;IAC7C,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC;AAsSD,eAAO,MAAM,aAAa,GACxB,KAAK,IAAI,EACT,UAAS,oBAAyB,KACjC,iBAAiB,EAUnB,CAAC"}
|
package/dist/build.js
CHANGED
|
@@ -5,18 +5,44 @@
|
|
|
5
5
|
* validate input, compose layers, execute the implementation, and map
|
|
6
6
|
* Results to MCP responses.
|
|
7
7
|
*/
|
|
8
|
-
import { composeLayers, createTrailContext, validateInput, zodToJsonSchema, } from '@ontrails/core';
|
|
8
|
+
import { composeLayers, createTrailContext, isBlobRef, validateInput, zodToJsonSchema, } from '@ontrails/core';
|
|
9
9
|
import { deriveAnnotations } from './annotations.js';
|
|
10
10
|
import { createMcpProgressCallback } from './progress.js';
|
|
11
11
|
import { deriveToolName } from './tool-name.js';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Internal helpers (defined before use)
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
/** Concatenate an array of Uint8Array chunks into a single Uint8Array. */
|
|
16
|
+
const concatChunks = (chunks, totalLength) => {
|
|
17
|
+
const result = new Uint8Array(totalLength);
|
|
18
|
+
let offset = 0;
|
|
19
|
+
for (const chunk of chunks) {
|
|
20
|
+
result.set(chunk, offset);
|
|
21
|
+
offset += chunk.length;
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
/** Collect a ReadableStream into a single Uint8Array. */
|
|
26
|
+
const collectStream = async (stream) => {
|
|
27
|
+
const reader = stream.getReader();
|
|
28
|
+
const chunks = [];
|
|
29
|
+
let totalLength = 0;
|
|
30
|
+
for (;;) {
|
|
31
|
+
const { done, value } = await reader.read();
|
|
32
|
+
if (done) {
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
chunks.push(value);
|
|
36
|
+
totalLength += value.length;
|
|
15
37
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
38
|
+
return concatChunks(chunks, totalLength);
|
|
39
|
+
};
|
|
40
|
+
/** Resolve BlobRef data to Uint8Array (handles ReadableStream). */
|
|
41
|
+
const resolveBlobData = (blob) => {
|
|
42
|
+
if (blob.data instanceof ReadableStream) {
|
|
43
|
+
return collectStream(blob.data);
|
|
44
|
+
}
|
|
45
|
+
return blob.data;
|
|
20
46
|
};
|
|
21
47
|
const uint8ArrayToBase64 = (bytes) => {
|
|
22
48
|
// Use btoa with manual conversion for runtime-agnostic base64
|
|
@@ -26,10 +52,11 @@ const uint8ArrayToBase64 = (bytes) => {
|
|
|
26
52
|
}
|
|
27
53
|
return btoa(binary);
|
|
28
54
|
};
|
|
29
|
-
const blobToContent = (blob) => {
|
|
55
|
+
const blobToContent = async (blob) => {
|
|
56
|
+
const bytes = await resolveBlobData(blob);
|
|
30
57
|
if (blob.mimeType.startsWith('image/')) {
|
|
31
58
|
return {
|
|
32
|
-
data: uint8ArrayToBase64(
|
|
59
|
+
data: uint8ArrayToBase64(bytes),
|
|
33
60
|
mimeType: blob.mimeType,
|
|
34
61
|
type: 'image',
|
|
35
62
|
};
|
|
@@ -37,18 +64,18 @@ const blobToContent = (blob) => {
|
|
|
37
64
|
return {
|
|
38
65
|
mimeType: blob.mimeType,
|
|
39
66
|
type: 'resource',
|
|
40
|
-
uri: `blob://${blob.name
|
|
67
|
+
uri: `blob://${blob.name}`,
|
|
41
68
|
};
|
|
42
69
|
};
|
|
43
70
|
/** Separate blob fields from non-blob fields in an object. */
|
|
44
|
-
const separateBlobFields = (obj) => {
|
|
71
|
+
const separateBlobFields = async (obj) => {
|
|
45
72
|
const blobContents = [];
|
|
46
73
|
const textFields = {};
|
|
47
74
|
let hasBlobFields = false;
|
|
48
75
|
for (const [key, val] of Object.entries(obj)) {
|
|
49
76
|
if (isBlobRef(val)) {
|
|
50
77
|
hasBlobFields = true;
|
|
51
|
-
blobContents.push(blobToContent(val));
|
|
78
|
+
blobContents.push(await blobToContent(val));
|
|
52
79
|
}
|
|
53
80
|
else {
|
|
54
81
|
textFields[key] = val;
|
|
@@ -57,8 +84,8 @@ const separateBlobFields = (obj) => {
|
|
|
57
84
|
return { blobContents, hasBlobFields, textFields };
|
|
58
85
|
};
|
|
59
86
|
/** Serialize a mixed blob/text object to MCP content. */
|
|
60
|
-
const serializeMixedObject = (obj) => {
|
|
61
|
-
const { blobContents, hasBlobFields, textFields } = separateBlobFields(obj);
|
|
87
|
+
const serializeMixedObject = async (obj) => {
|
|
88
|
+
const { blobContents, hasBlobFields, textFields } = await separateBlobFields(obj);
|
|
62
89
|
if (!hasBlobFields) {
|
|
63
90
|
return undefined;
|
|
64
91
|
}
|
|
@@ -67,12 +94,12 @@ const serializeMixedObject = (obj) => {
|
|
|
67
94
|
}
|
|
68
95
|
return blobContents;
|
|
69
96
|
};
|
|
70
|
-
const serializeOutput = (value) => {
|
|
97
|
+
const serializeOutput = async (value) => {
|
|
71
98
|
if (isBlobRef(value)) {
|
|
72
|
-
return [blobToContent(value)];
|
|
99
|
+
return [await blobToContent(value)];
|
|
73
100
|
}
|
|
74
101
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
75
|
-
const mixed = serializeMixedObject(value);
|
|
102
|
+
const mixed = await serializeMixedObject(value);
|
|
76
103
|
if (mixed) {
|
|
77
104
|
return mixed;
|
|
78
105
|
}
|
|
@@ -106,7 +133,7 @@ const executeAndMap = async (trail, validatedInput, ctx, layers) => {
|
|
|
106
133
|
try {
|
|
107
134
|
const result = await impl(validatedInput, ctx);
|
|
108
135
|
if (result.isOk()) {
|
|
109
|
-
return { content: serializeOutput(result.value) };
|
|
136
|
+
return { content: await serializeOutput(result.value) };
|
|
110
137
|
}
|
|
111
138
|
return mcpError(result.error.message);
|
|
112
139
|
}
|
|
@@ -173,17 +200,27 @@ const buildToolDefinition = (app, trail, layers, options) => {
|
|
|
173
200
|
name: deriveToolName(app.name, trail.id),
|
|
174
201
|
};
|
|
175
202
|
};
|
|
203
|
+
/** Register a trail as an MCP tool, checking for name collisions. */
|
|
204
|
+
const registerTool = (app, trailItem, layers, options, nameToTrailId, tools) => {
|
|
205
|
+
const toolName = deriveToolName(app.name, trailItem.id);
|
|
206
|
+
const existingId = nameToTrailId.get(toolName);
|
|
207
|
+
if (existingId !== undefined) {
|
|
208
|
+
throw new Error(`MCP tool-name collision: trails "${existingId}" and "${trailItem.id}" both derive the tool name "${toolName}"`);
|
|
209
|
+
}
|
|
210
|
+
nameToTrailId.set(toolName, trailItem.id);
|
|
211
|
+
tools.push(buildToolDefinition(app, trailItem, layers, options));
|
|
212
|
+
};
|
|
213
|
+
/** Filter topo items to eligible trails/hikes. */
|
|
214
|
+
const eligibleTrails = (app, options) => app
|
|
215
|
+
.list()
|
|
216
|
+
.filter((item) => (item.kind === 'trail' || item.kind === 'hike') &&
|
|
217
|
+
shouldInclude(item, options));
|
|
176
218
|
export const buildMcpTools = (app, options = {}) => {
|
|
177
219
|
const layers = options.layers ?? [];
|
|
178
220
|
const tools = [];
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
if (!shouldInclude(item, options)) {
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
tools.push(buildToolDefinition(app, item, layers, options));
|
|
221
|
+
const nameToTrailId = new Map();
|
|
222
|
+
for (const trailItem of eligibleTrails(app, options)) {
|
|
223
|
+
registerTool(app, trailItem, layers, options, nameToTrailId, tools);
|
|
187
224
|
}
|
|
188
225
|
return tools;
|
|
189
226
|
};
|
package/dist/build.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.js","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,aAAa,EACb,eAAe,GAChB,MAAM,gBAAgB,CAAC;AAIxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"build.js","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
2
|
|
|
3
|
-
import { Result, trail, topo } from '@ontrails/core';
|
|
3
|
+
import { Result, createBlobRef, trail, topo } from '@ontrails/core';
|
|
4
4
|
import type { Layer } from '@ontrails/core';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
|
|
@@ -300,12 +300,14 @@ describe('buildMcpTools', () => {
|
|
|
300
300
|
test('BlobRef output converts to image content', async () => {
|
|
301
301
|
const blobTrail = trail('blob.image', {
|
|
302
302
|
implementation: () =>
|
|
303
|
-
Result.ok(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
303
|
+
Result.ok(
|
|
304
|
+
createBlobRef({
|
|
305
|
+
data: new Uint8Array([1, 2, 3]),
|
|
306
|
+
mimeType: 'image/png',
|
|
307
|
+
name: 'test.png',
|
|
308
|
+
size: 3,
|
|
309
|
+
})
|
|
310
|
+
),
|
|
309
311
|
input: z.object({}),
|
|
310
312
|
});
|
|
311
313
|
|
|
@@ -320,12 +322,14 @@ describe('buildMcpTools', () => {
|
|
|
320
322
|
test('BlobRef output converts to resource content for non-images', async () => {
|
|
321
323
|
const blobTrail = trail('blob.file', {
|
|
322
324
|
implementation: () =>
|
|
323
|
-
Result.ok(
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
325
|
+
Result.ok(
|
|
326
|
+
createBlobRef({
|
|
327
|
+
data: new Uint8Array([1, 2, 3]),
|
|
328
|
+
mimeType: 'application/pdf',
|
|
329
|
+
name: 'doc.pdf',
|
|
330
|
+
size: 3,
|
|
331
|
+
})
|
|
332
|
+
),
|
|
329
333
|
input: z.object({}),
|
|
330
334
|
});
|
|
331
335
|
|
|
@@ -336,6 +340,79 @@ describe('buildMcpTools', () => {
|
|
|
336
340
|
expect(result?.content[0]?.uri).toBe('blob://doc.pdf');
|
|
337
341
|
expect(result?.content[0]?.mimeType).toBe('application/pdf');
|
|
338
342
|
});
|
|
343
|
+
|
|
344
|
+
test('BlobRef with ReadableStream data is collected and serialized', async () => {
|
|
345
|
+
const bytes = new Uint8Array([10, 20, 30]);
|
|
346
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
347
|
+
start(controller) {
|
|
348
|
+
controller.enqueue(bytes);
|
|
349
|
+
controller.close();
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
const blobTrail = trail('blob.stream', {
|
|
353
|
+
implementation: () =>
|
|
354
|
+
Result.ok(
|
|
355
|
+
createBlobRef({
|
|
356
|
+
data: stream,
|
|
357
|
+
mimeType: 'image/gif',
|
|
358
|
+
name: 'anim.gif',
|
|
359
|
+
size: 3,
|
|
360
|
+
})
|
|
361
|
+
),
|
|
362
|
+
input: z.object({}),
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const tool = requireOnlyTool(buildMcpTools(topo('myapp', { blobTrail })));
|
|
366
|
+
const result = await tool.handler({}, noExtra);
|
|
367
|
+
|
|
368
|
+
expect(result?.content[0]?.type).toBe('image');
|
|
369
|
+
expect(result?.content[0]?.mimeType).toBe('image/gif');
|
|
370
|
+
expect(result?.content[0]?.data).toBeDefined();
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe('tool-name collision detection', () => {
|
|
375
|
+
test('throws on trails that produce the same derived tool name', () => {
|
|
376
|
+
const dotTrail = trail('foo.bar', {
|
|
377
|
+
implementation: () => Result.ok({ ok: true }),
|
|
378
|
+
input: z.object({}),
|
|
379
|
+
});
|
|
380
|
+
const underscoreTrail = trail('foo_bar', {
|
|
381
|
+
implementation: () => Result.ok({ ok: true }),
|
|
382
|
+
input: z.object({}),
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const app = topo('myapp', { dotTrail, underscoreTrail });
|
|
386
|
+
expect(() => buildMcpTools(app)).toThrow(/tool-name collision/i);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test('throws on trails where hyphen and underscore collide', () => {
|
|
390
|
+
const hyphenTrail = trail('foo-bar', {
|
|
391
|
+
implementation: () => Result.ok({ ok: true }),
|
|
392
|
+
input: z.object({}),
|
|
393
|
+
});
|
|
394
|
+
const underscoreTrail = trail('foo_bar', {
|
|
395
|
+
implementation: () => Result.ok({ ok: true }),
|
|
396
|
+
input: z.object({}),
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const app = topo('myapp', { hyphenTrail, underscoreTrail });
|
|
400
|
+
expect(() => buildMcpTools(app)).toThrow(/tool-name collision/i);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('does not throw when trail names are distinct after normalization', () => {
|
|
404
|
+
const fooTrail = trail('foo', {
|
|
405
|
+
implementation: () => Result.ok({ ok: true }),
|
|
406
|
+
input: z.object({}),
|
|
407
|
+
});
|
|
408
|
+
const barTrail = trail('bar', {
|
|
409
|
+
implementation: () => Result.ok({ ok: true }),
|
|
410
|
+
input: z.object({}),
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const app = topo('myapp', { barTrail, fooTrail });
|
|
414
|
+
expect(() => buildMcpTools(app)).not.toThrow();
|
|
415
|
+
});
|
|
339
416
|
});
|
|
340
417
|
|
|
341
418
|
describe('end-to-end', () => {
|
package/src/build.ts
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
import {
|
|
10
10
|
composeLayers,
|
|
11
11
|
createTrailContext,
|
|
12
|
+
isBlobRef,
|
|
12
13
|
validateInput,
|
|
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';
|
|
@@ -68,23 +69,44 @@ export interface McpContent {
|
|
|
68
69
|
// Internal helpers (defined before use)
|
|
69
70
|
// ---------------------------------------------------------------------------
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
/** Concatenate an array of Uint8Array chunks into a single Uint8Array. */
|
|
73
|
+
const concatChunks = (
|
|
74
|
+
chunks: Uint8Array[],
|
|
75
|
+
totalLength: number
|
|
76
|
+
): Uint8Array => {
|
|
77
|
+
const result = new Uint8Array(totalLength);
|
|
78
|
+
let offset = 0;
|
|
79
|
+
for (const chunk of chunks) {
|
|
80
|
+
result.set(chunk, offset);
|
|
81
|
+
offset += chunk.length;
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
};
|
|
77
85
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
86
|
+
/** Collect a ReadableStream into a single Uint8Array. */
|
|
87
|
+
const collectStream = async (
|
|
88
|
+
stream: ReadableStream<Uint8Array>
|
|
89
|
+
): Promise<Uint8Array> => {
|
|
90
|
+
const reader = stream.getReader();
|
|
91
|
+
const chunks: Uint8Array[] = [];
|
|
92
|
+
let totalLength = 0;
|
|
93
|
+
for (;;) {
|
|
94
|
+
const { done, value } = await reader.read();
|
|
95
|
+
if (done) {
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
chunks.push(value);
|
|
99
|
+
totalLength += value.length;
|
|
81
100
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
)
|
|
101
|
+
return concatChunks(chunks, totalLength);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** Resolve BlobRef data to Uint8Array (handles ReadableStream). */
|
|
105
|
+
const resolveBlobData = (blob: BlobRef): Promise<Uint8Array> | Uint8Array => {
|
|
106
|
+
if (blob.data instanceof ReadableStream) {
|
|
107
|
+
return collectStream(blob.data);
|
|
108
|
+
}
|
|
109
|
+
return blob.data;
|
|
88
110
|
};
|
|
89
111
|
|
|
90
112
|
const uint8ArrayToBase64 = (bytes: Uint8Array): string => {
|
|
@@ -96,10 +118,11 @@ const uint8ArrayToBase64 = (bytes: Uint8Array): string => {
|
|
|
96
118
|
return btoa(binary);
|
|
97
119
|
};
|
|
98
120
|
|
|
99
|
-
const blobToContent = (blob: BlobRef): McpContent => {
|
|
121
|
+
const blobToContent = async (blob: BlobRef): Promise<McpContent> => {
|
|
122
|
+
const bytes = await resolveBlobData(blob);
|
|
100
123
|
if (blob.mimeType.startsWith('image/')) {
|
|
101
124
|
return {
|
|
102
|
-
data: uint8ArrayToBase64(
|
|
125
|
+
data: uint8ArrayToBase64(bytes),
|
|
103
126
|
mimeType: blob.mimeType,
|
|
104
127
|
type: 'image',
|
|
105
128
|
};
|
|
@@ -108,25 +131,25 @@ const blobToContent = (blob: BlobRef): McpContent => {
|
|
|
108
131
|
return {
|
|
109
132
|
mimeType: blob.mimeType,
|
|
110
133
|
type: 'resource',
|
|
111
|
-
uri: `blob://${blob.name
|
|
134
|
+
uri: `blob://${blob.name}`,
|
|
112
135
|
};
|
|
113
136
|
};
|
|
114
137
|
|
|
115
138
|
/** Separate blob fields from non-blob fields in an object. */
|
|
116
|
-
const separateBlobFields = (
|
|
139
|
+
const separateBlobFields = async (
|
|
117
140
|
obj: Record<string, unknown>
|
|
118
|
-
): {
|
|
141
|
+
): Promise<{
|
|
119
142
|
blobContents: McpContent[];
|
|
120
143
|
hasBlobFields: boolean;
|
|
121
144
|
textFields: Record<string, unknown>;
|
|
122
|
-
} => {
|
|
145
|
+
}> => {
|
|
123
146
|
const blobContents: McpContent[] = [];
|
|
124
147
|
const textFields: Record<string, unknown> = {};
|
|
125
148
|
let hasBlobFields = false;
|
|
126
149
|
for (const [key, val] of Object.entries(obj)) {
|
|
127
150
|
if (isBlobRef(val)) {
|
|
128
151
|
hasBlobFields = true;
|
|
129
|
-
blobContents.push(blobToContent(val));
|
|
152
|
+
blobContents.push(await blobToContent(val));
|
|
130
153
|
} else {
|
|
131
154
|
textFields[key] = val;
|
|
132
155
|
}
|
|
@@ -135,10 +158,11 @@ const separateBlobFields = (
|
|
|
135
158
|
};
|
|
136
159
|
|
|
137
160
|
/** Serialize a mixed blob/text object to MCP content. */
|
|
138
|
-
const serializeMixedObject = (
|
|
161
|
+
const serializeMixedObject = async (
|
|
139
162
|
obj: Record<string, unknown>
|
|
140
|
-
): readonly McpContent[] | undefined => {
|
|
141
|
-
const { blobContents, hasBlobFields, textFields } =
|
|
163
|
+
): Promise<readonly McpContent[] | undefined> => {
|
|
164
|
+
const { blobContents, hasBlobFields, textFields } =
|
|
165
|
+
await separateBlobFields(obj);
|
|
142
166
|
if (!hasBlobFields) {
|
|
143
167
|
return undefined;
|
|
144
168
|
}
|
|
@@ -148,12 +172,14 @@ const serializeMixedObject = (
|
|
|
148
172
|
return blobContents;
|
|
149
173
|
};
|
|
150
174
|
|
|
151
|
-
const serializeOutput = (
|
|
175
|
+
const serializeOutput = async (
|
|
176
|
+
value: unknown
|
|
177
|
+
): Promise<readonly McpContent[]> => {
|
|
152
178
|
if (isBlobRef(value)) {
|
|
153
|
-
return [blobToContent(value)];
|
|
179
|
+
return [await blobToContent(value)];
|
|
154
180
|
}
|
|
155
181
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
156
|
-
const mixed = serializeMixedObject(value as Record<string, unknown>);
|
|
182
|
+
const mixed = await serializeMixedObject(value as Record<string, unknown>);
|
|
157
183
|
if (mixed) {
|
|
158
184
|
return mixed;
|
|
159
185
|
}
|
|
@@ -202,7 +228,7 @@ const executeAndMap = async (
|
|
|
202
228
|
try {
|
|
203
229
|
const result = await impl(validatedInput, ctx);
|
|
204
230
|
if (result.isOk()) {
|
|
205
|
-
return { content: serializeOutput(result.value) };
|
|
231
|
+
return { content: await serializeOutput(result.value) };
|
|
206
232
|
}
|
|
207
233
|
return mcpError(result.error.message);
|
|
208
234
|
} catch (error: unknown) {
|
|
@@ -298,23 +324,49 @@ const buildToolDefinition = (
|
|
|
298
324
|
};
|
|
299
325
|
};
|
|
300
326
|
|
|
327
|
+
/** Register a trail as an MCP tool, checking for name collisions. */
|
|
328
|
+
const registerTool = (
|
|
329
|
+
app: Topo,
|
|
330
|
+
trailItem: Trail<unknown, unknown>,
|
|
331
|
+
layers: readonly Layer[],
|
|
332
|
+
options: BuildMcpToolsOptions,
|
|
333
|
+
nameToTrailId: Map<string, string>,
|
|
334
|
+
tools: McpToolDefinition[]
|
|
335
|
+
): void => {
|
|
336
|
+
const toolName = deriveToolName(app.name, trailItem.id);
|
|
337
|
+
const existingId = nameToTrailId.get(toolName);
|
|
338
|
+
if (existingId !== undefined) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
`MCP tool-name collision: trails "${existingId}" and "${trailItem.id}" both derive the tool name "${toolName}"`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
nameToTrailId.set(toolName, trailItem.id);
|
|
344
|
+
tools.push(buildToolDefinition(app, trailItem, layers, options));
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
/** Filter topo items to eligible trails/hikes. */
|
|
348
|
+
const eligibleTrails = (
|
|
349
|
+
app: Topo,
|
|
350
|
+
options: BuildMcpToolsOptions
|
|
351
|
+
): Trail<unknown, unknown>[] =>
|
|
352
|
+
app
|
|
353
|
+
.list()
|
|
354
|
+
.filter(
|
|
355
|
+
(item): item is Trail<unknown, unknown> =>
|
|
356
|
+
(item.kind === 'trail' || item.kind === 'hike') &&
|
|
357
|
+
shouldInclude(item as Trail<unknown, unknown>, options)
|
|
358
|
+
);
|
|
359
|
+
|
|
301
360
|
export const buildMcpTools = (
|
|
302
361
|
app: Topo,
|
|
303
362
|
options: BuildMcpToolsOptions = {}
|
|
304
363
|
): McpToolDefinition[] => {
|
|
305
364
|
const layers = options.layers ?? [];
|
|
306
365
|
const tools: McpToolDefinition[] = [];
|
|
366
|
+
const nameToTrailId = new Map<string, string>();
|
|
307
367
|
|
|
308
|
-
for (const
|
|
309
|
-
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
if (!shouldInclude(item as Trail<unknown, unknown>, options)) {
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
tools.push(
|
|
316
|
-
buildToolDefinition(app, item as Trail<unknown, unknown>, layers, options)
|
|
317
|
-
);
|
|
368
|
+
for (const trailItem of eligibleTrails(app, options)) {
|
|
369
|
+
registerTool(app, trailItem, layers, options, nameToTrailId, tools);
|
|
318
370
|
}
|
|
319
371
|
|
|
320
372
|
return tools;
|