@ontrails/mcp 1.0.0-beta.2 → 1.0.0-beta.4

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.
@@ -1,3 +1,3 @@
1
1
  $ oxlint ./src
2
2
  Found 0 warnings and 0 errors.
3
- Finished in 54ms on 12 files with 93 rules using 24 threads.
3
+ Finished in 23ms on 12 files with 93 rules using 24 threads.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # @ontrails/mcp
2
2
 
3
+ ## 1.0.0-beta.4
4
+
5
+ ### Major Changes
6
+
7
+ - API simplification: unified trail model, intent enum, run, metadata.
8
+
9
+ **BREAKING CHANGES:**
10
+
11
+ - `hike()` removed — use `trail()` with optional `follow: [...]` field
12
+ - `follows` renamed to `follow` (singular, matching `ctx.follow()`)
13
+ - `topo.hikes` removed — single `topo.trails` map
14
+ - `kind: 'hike'` removed — everything is `kind: 'trail'`
15
+ - `readOnly`/`destructive` booleans replaced by `intent: 'read' | 'write' | 'destroy'`
16
+ - `implementation` field renamed to `run`
17
+ - `markers` field renamed to `metadata`
18
+ - `testHike` renamed to `testFollows`, `HikeScenario` to `FollowScenario`
19
+ - `blaze()` now returns the surface handle (`Command` for CLI, `Server` for MCP)
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies
24
+ - @ontrails/core@1.0.0-beta.4
25
+
26
+ ## 1.0.0-beta.3
27
+
28
+ ### Minor Changes
29
+
30
+ - Bug fixes across all surface packages found via parallel Codex review.
31
+
32
+ **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.
33
+
34
+ **cli**: Switch blaze to parseAsync for proper async error handling, add boolean flag negation (--no-flag), and strict number parsing that rejects partial input.
35
+
36
+ **mcp**: Align BlobRef with core (including ReadableStream support) and detect tool-name collisions after normalization.
37
+
38
+ **testing**: Include hikes in testContracts validation, with follow-context awareness.
39
+
40
+ **warden**: Collect hike detour targets, validate detour refs in hike specs, and stop implementation-returns-result from walking into nested function bodies.
41
+
42
+ ### Patch Changes
43
+
44
+ - Updated dependencies
45
+ - @ontrails/core@1.0.0-beta.3
46
+
3
47
  ## 1.0.0-beta.2
4
48
 
5
49
  ### Patch Changes
package/README.md CHANGED
@@ -11,8 +11,8 @@ import { z } from 'zod';
11
11
 
12
12
  const greet = trail('greet', {
13
13
  input: z.object({ name: z.string().describe('Who to greet') }),
14
- readOnly: true,
15
- implementation: (input) => Result.ok(`Hello, ${input.name}!`),
14
+ intent: 'read',
15
+ run: (input) => Result.ok(`Hello, ${input.name}!`),
16
16
  });
17
17
 
18
18
  const app = topo('myapp', { greet });
@@ -42,19 +42,19 @@ for (const tool of tools) {
42
42
  | `blaze(app, options?)` | Start an MCP server with all trails as tools |
43
43
  | `buildMcpTools(app, options?)` | Build tool definitions without starting a server |
44
44
  | `deriveToolName(appName, trailId)` | Compute the MCP tool name from app and trail IDs |
45
- | `deriveAnnotations(trail)` | Extract MCP annotations from trail markers |
45
+ | `deriveAnnotations(trail)` | Extract MCP annotations from trail intent and metadata |
46
46
  | `createMcpProgressCallback(server)` | Bridge `ctx.progress` to MCP `notifications/progress` |
47
47
 
48
48
  See the [API Reference](../../docs/api-reference.md) for the full list.
49
49
 
50
50
  ## Annotations
51
51
 
52
- Trail markers map directly to MCP annotations:
52
+ Trail intent and metadata map directly to MCP annotations:
53
53
 
54
54
  | Trail field | MCP annotation |
55
55
  | --- | --- |
56
- | `readOnly: true` | `readOnlyHint: true` |
57
- | `destructive: true` | `destructiveHint: true` |
56
+ | `intent: 'read'` | `readOnlyHint: true` |
57
+ | `intent: 'destroy'` | `destructiveHint: true` |
58
58
  | `idempotent: true` | `idempotentHint: true` |
59
59
  | `description` | `title` |
60
60
 
@@ -70,7 +70,7 @@ Implementations report progress through `ctx.progress`. On MCP, these bridge to
70
70
 
71
71
  ```typescript
72
72
  const importTrail = trail('data.import', {
73
- implementation: async (input, ctx) => {
73
+ run: async (input, ctx) => {
74
74
  for (let i = 0; i < items.length; i++) {
75
75
  await processItem(items[i]);
76
76
  ctx.progress?.({ type: 'progress', current: i + 1, total: items.length });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Derive MCP tool annotations from trail spec markers.
2
+ * Derive MCP tool annotations from trail spec fields.
3
3
  */
4
4
  import type { Trail } from '@ontrails/core';
5
5
  export interface McpAnnotations {
@@ -15,5 +15,5 @@ export interface McpAnnotations {
15
15
  * Only sets hints that are explicitly declared on the trail.
16
16
  * Omitted hints let the MCP SDK use its defaults.
17
17
  */
18
- export declare const deriveAnnotations: (trail: Pick<Trail<unknown, unknown>, "readOnly" | "destructive" | "idempotent" | "description">) => McpAnnotations;
18
+ export declare const deriveAnnotations: (trail: Pick<Trail<unknown, unknown>, "intent" | "idempotent" | "description">) => McpAnnotations;
19
19
  //# sourceMappingURL=annotations.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"annotations.d.ts","sourceRoot":"","sources":["../src/annotations.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAM5C,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5C,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC/C,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAMD;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAC5B,OAAO,IAAI,CACT,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,EACvB,UAAU,GAAG,aAAa,GAAG,YAAY,GAAG,aAAa,CAC1D,KACA,cAiBF,CAAC"}
1
+ {"version":3,"file":"annotations.d.ts","sourceRoot":"","sources":["../src/annotations.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAU,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAMpD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5C,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC/C,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAMD;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAC5B,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,QAAQ,GAAG,YAAY,GAAG,aAAa,CAAC,KAC5E,cAoBF,CAAC"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Derive MCP tool annotations from trail spec markers.
2
+ * Derive MCP tool annotations from trail spec fields.
3
3
  */
4
4
  // ---------------------------------------------------------------------------
5
5
  // Derivation
@@ -12,11 +12,13 @@
12
12
  */
13
13
  export const deriveAnnotations = (trail) => {
14
14
  const annotations = {};
15
- if (trail.readOnly === true) {
16
- annotations['readOnlyHint'] = true;
17
- }
18
- if (trail.destructive === true) {
19
- annotations['destructiveHint'] = true;
15
+ const intentToHint = {
16
+ destroy: 'destructiveHint',
17
+ read: 'readOnlyHint',
18
+ };
19
+ const hint = intentToHint[trail.intent];
20
+ if (hint) {
21
+ annotations[hint] = true;
20
22
  }
21
23
  if (trail.idempotent === true) {
22
24
  annotations['idempotentHint'] = true;
@@ -1 +1 @@
1
- {"version":3,"file":"annotations.js","sourceRoot":"","sources":["../src/annotations.ts"],"names":[],"mappings":"AAAA;;GAEG;AAgBH,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,KAGC,EACe,EAAE;IAClB,MAAM,WAAW,GAA4B,EAAE,CAAC;IAEhD,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;QAC5B,WAAW,CAAC,cAAc,CAAC,GAAG,IAAI,CAAC;IACrC,CAAC;IACD,IAAI,KAAK,CAAC,WAAW,KAAK,IAAI,EAAE,CAAC;QAC/B,WAAW,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAAC;IACxC,CAAC;IACD,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;QAC9B,WAAW,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAAC;IACvC,CAAC;IACD,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACpC,WAAW,CAAC,OAAO,CAAC,GAAG,KAAK,CAAC,WAAW,CAAC;IAC3C,CAAC;IAED,OAAO,WAA6B,CAAC;AACvC,CAAC,CAAC"}
1
+ {"version":3,"file":"annotations.js","sourceRoot":"","sources":["../src/annotations.ts"],"names":[],"mappings":"AAAA;;GAEG;AAgBH,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,KAA6E,EAC7D,EAAE;IAClB,MAAM,WAAW,GAA4B,EAAE,CAAC;IAEhD,MAAM,YAAY,GAAoC;QACpD,OAAO,EAAE,iBAAiB;QAC1B,IAAI,EAAE,cAAc;KACrB,CAAC;IAEF,MAAM,IAAI,GAAG,YAAY,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACxC,IAAI,IAAI,EAAE,CAAC;QACT,WAAW,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3B,CAAC;IACD,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;QAC9B,WAAW,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAAC;IACvC,CAAC;IACD,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACpC,WAAW,CAAC,OAAO,CAAC,GAAG,KAAK,CAAC,WAAW,CAAC;IAC3C,CAAC;IAED,OAAO,WAA6B,CAAC;AACvC,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAS,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEvE,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;AA4OD,eAAO,MAAM,aAAa,GACxB,KAAK,IAAI,EACT,UAAS,oBAAyB,KACjC,iBAAiB,EAiBnB,CAAC"}
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
- const isBlobRef = (value) => {
13
- if (typeof value !== 'object' || value === null) {
14
- return false;
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
- const obj = value;
17
- return (obj['kind'] === 'blob' &&
18
- obj['data'] instanceof Uint8Array &&
19
- typeof obj['mimeType'] === 'string');
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(blob.data),
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 ?? 'unnamed'}`,
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
  }
@@ -102,11 +129,11 @@ const buildTrailContext = async (options, extra) => {
102
129
  };
103
130
  /** Execute a trail and map the result to an MCP response. */
104
131
  const executeAndMap = async (trail, validatedInput, ctx, layers) => {
105
- const impl = composeLayers([...layers], trail, trail.implementation);
132
+ const impl = composeLayers([...layers], trail, trail.run);
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
  }
@@ -131,12 +158,12 @@ const createHandler = (trail, layers, options) => async (args, extra) => {
131
158
  * Each trail in the topo becomes an McpToolDefinition with:
132
159
  * - A derived tool name (app-prefixed, underscore-delimited)
133
160
  * - JSON Schema input from zodToJsonSchema
134
- * - MCP annotations from trail markers
161
+ * - MCP annotations from trail metadata
135
162
  * - A handler that validates, composes layers, executes, and maps results
136
163
  */
137
- /** Check if a trail should be included based on markers and filters. */
164
+ /** Check if a trail should be included based on metadata and filters. */
138
165
  const shouldInclude = (trail, options) => {
139
- if (trail.markers?.['internal'] === true) {
166
+ if (trail.metadata?.['internal'] === true) {
140
167
  return false;
141
168
  }
142
169
  if (options.includeTrails !== undefined && options.includeTrails.length > 0) {
@@ -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. */
214
+ const eligibleTrails = (app, options) => app
215
+ .list()
216
+ .filter((item) => item.kind === 'trail' &&
217
+ shouldInclude(item, options));
176
218
  export const buildMcpTools = (app, options = {}) => {
177
219
  const layers = options.layers ?? [];
178
220
  const tools = [];
179
- for (const item of app.list()) {
180
- if (item.kind !== 'trail' && item.kind !== 'hike') {
181
- continue;
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":"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"}
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
  {
2
2
  "name": "@ontrails/mcp",
3
- "version": "1.0.0-beta.2",
3
+ "version": "1.0.0-beta.4",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
@@ -3,36 +3,39 @@ import { describe, expect, test } from 'bun:test';
3
3
  import { deriveAnnotations } from '../annotations.js';
4
4
 
5
5
  describe('deriveAnnotations', () => {
6
- test('readOnly trail produces readOnlyHint', () => {
7
- const annotations = deriveAnnotations({ readOnly: true });
6
+ test('read intent produces readOnlyHint', () => {
7
+ const annotations = deriveAnnotations({ intent: 'read' });
8
8
  expect(annotations.readOnlyHint).toBe(true);
9
9
  expect(annotations.destructiveHint).toBeUndefined();
10
10
  expect(annotations.idempotentHint).toBeUndefined();
11
11
  });
12
12
 
13
- test('destructive trail produces destructiveHint', () => {
14
- const annotations = deriveAnnotations({ destructive: true });
13
+ test('destroy intent produces destructiveHint', () => {
14
+ const annotations = deriveAnnotations({ intent: 'destroy' });
15
15
  expect(annotations.destructiveHint).toBe(true);
16
16
  expect(annotations.readOnlyHint).toBeUndefined();
17
17
  });
18
18
 
19
19
  test('idempotent trail produces idempotentHint', () => {
20
- const annotations = deriveAnnotations({ idempotent: true });
20
+ const annotations = deriveAnnotations({
21
+ idempotent: true,
22
+ intent: 'write',
23
+ });
21
24
  expect(annotations.idempotentHint).toBe(true);
22
25
  });
23
26
 
24
- test('multiple markers combine correctly', () => {
27
+ test('read intent with idempotent combines correctly', () => {
25
28
  const annotations = deriveAnnotations({
26
29
  idempotent: true,
27
- readOnly: true,
30
+ intent: 'read',
28
31
  });
29
32
  expect(annotations.readOnlyHint).toBe(true);
30
33
  expect(annotations.idempotentHint).toBe(true);
31
34
  expect(annotations.destructiveHint).toBeUndefined();
32
35
  });
33
36
 
34
- test('no markers produces empty annotations', () => {
35
- const annotations = deriveAnnotations({});
37
+ test('write intent produces empty annotations', () => {
38
+ const annotations = deriveAnnotations({ intent: 'write' });
36
39
  expect(annotations.readOnlyHint).toBeUndefined();
37
40
  expect(annotations.destructiveHint).toBeUndefined();
38
41
  expect(annotations.idempotentHint).toBeUndefined();
@@ -42,29 +45,19 @@ describe('deriveAnnotations', () => {
42
45
  test('description maps to title', () => {
43
46
  const annotations = deriveAnnotations({
44
47
  description: 'Show entity details',
48
+ intent: 'write',
45
49
  });
46
50
  expect(annotations.title).toBe('Show entity details');
47
51
  });
48
52
 
49
- test('all markers plus description', () => {
53
+ test('all hints plus description', () => {
50
54
  const annotations = deriveAnnotations({
51
55
  description: 'A trail',
52
- destructive: true,
53
56
  idempotent: true,
54
- readOnly: true,
57
+ intent: 'destroy',
55
58
  });
56
- expect(annotations.readOnlyHint).toBe(true);
57
59
  expect(annotations.destructiveHint).toBe(true);
58
60
  expect(annotations.idempotentHint).toBe(true);
59
61
  expect(annotations.title).toBe('A trail');
60
62
  });
61
-
62
- test('false values are not included', () => {
63
- const annotations = deriveAnnotations({
64
- destructive: false,
65
- readOnly: false,
66
- });
67
- expect(annotations.readOnlyHint).toBeUndefined();
68
- expect(annotations.destructiveHint).toBeUndefined();
69
- });
70
63
  });
@@ -22,16 +22,16 @@ const requireTool = (tools: ReturnType<typeof buildMcpTools>, name: string) => {
22
22
  const createIntegrationTools = () => {
23
23
  const greetTrail = trail('greet', {
24
24
  description: 'Greet someone',
25
- implementation: (input) => Result.ok({ greeting: `Hello, ${input.name}!` }),
26
25
  input: z.object({ name: z.string() }),
27
- readOnly: true,
26
+ intent: 'read',
27
+ run: (input) => Result.ok({ greeting: `Hello, ${input.name}!` }),
28
28
  });
29
29
 
30
30
  const deleteTrail = trail('item.delete', {
31
31
  description: 'Delete an item',
32
- destructive: true,
33
- implementation: (_input) => Result.ok({ deleted: true }),
34
32
  input: z.object({ id: z.string() }),
33
+ intent: 'destroy',
34
+ run: (_input) => Result.ok({ deleted: true }),
35
35
  });
36
36
 
37
37
  return buildMcpTools(topo('myapp', { deleteTrail, greetTrail }));
@@ -41,9 +41,9 @@ describe('blaze', () => {
41
41
  test('createMcpServer registers tools that can be listed', () => {
42
42
  const echoTrail = trail('echo', {
43
43
  description: 'Echo',
44
- implementation: (input) => Result.ok({ reply: input.message }),
45
44
  input: z.object({ message: z.string() }),
46
- readOnly: true,
45
+ intent: 'read',
46
+ run: (input) => Result.ok({ reply: input.message }),
47
47
  });
48
48
 
49
49
  const app = topo('testapp', { echoTrail });
@@ -60,15 +60,15 @@ describe('blaze', () => {
60
60
  test('createMcpServer handles multiple tools', () => {
61
61
  const echoTrail = trail('echo', {
62
62
  description: 'Echo',
63
- implementation: (input) => Result.ok({ reply: input.message }),
64
63
  input: z.object({ message: z.string() }),
64
+ run: (input) => Result.ok({ reply: input.message }),
65
65
  });
66
66
 
67
67
  const searchTrail = trail('search', {
68
68
  description: 'Search',
69
- implementation: (input) => Result.ok({ results: [input.query] }),
70
69
  input: z.object({ query: z.string() }),
71
- readOnly: true,
70
+ intent: 'read',
71
+ run: (input) => Result.ok({ results: [input.query] }),
72
72
  });
73
73
 
74
74
  const app = topo('testapp', { echoTrail, searchTrail });
@@ -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
 
@@ -13,23 +13,23 @@ import type { McpExtra } from '../build.js';
13
13
 
14
14
  const echoTrail = trail('echo', {
15
15
  description: 'Echo a message back',
16
- implementation: (input) => Result.ok({ reply: input.message }),
17
16
  input: z.object({ message: z.string() }),
17
+ intent: 'read',
18
18
  output: z.object({ reply: z.string() }),
19
- readOnly: true,
19
+ run: (input) => Result.ok({ reply: input.message }),
20
20
  });
21
21
 
22
22
  const deleteTrail = trail('item.delete', {
23
23
  description: 'Delete an item',
24
- destructive: true,
25
- implementation: (_input) => Result.ok({ deleted: true }),
26
24
  input: z.object({ id: z.string() }),
25
+ intent: 'destroy',
26
+ run: (_input) => Result.ok({ deleted: true }),
27
27
  });
28
28
 
29
29
  const failTrail = trail('fail', {
30
30
  description: 'Always fails',
31
- implementation: (input) => Result.err(new Error(input.reason)),
32
31
  input: z.object({ reason: z.string() }),
32
+ run: (input) => Result.err(new Error(input.reason)),
33
33
  });
34
34
 
35
35
  const exampleTrail = trail('with.examples', {
@@ -41,8 +41,8 @@ const exampleTrail = trail('with.examples', {
41
41
  name: 'basic',
42
42
  },
43
43
  ],
44
- implementation: (input) => Result.ok({ greeting: `hello ${input.name}` }),
45
44
  input: z.object({ name: z.string() }),
45
+ run: (input) => Result.ok({ greeting: `hello ${input.name}` }),
46
46
  });
47
47
 
48
48
  const noExtra: McpExtra = {};
@@ -165,10 +165,10 @@ describe('buildMcpTools', () => {
165
165
 
166
166
  test('handler catches thrown exceptions', async () => {
167
167
  const throwTrail = trail('throw', {
168
- implementation: () => {
168
+ input: z.object({}),
169
+ run: () => {
169
170
  throw new Error('unexpected crash');
170
171
  },
171
- input: z.object({}),
172
172
  });
173
173
 
174
174
  const tool = requireOnlyTool(
@@ -244,11 +244,11 @@ describe('buildMcpTools', () => {
244
244
  let capturedSignal: AbortSignal | undefined;
245
245
 
246
246
  const signalTrail = trail('signal.check', {
247
- implementation: (_input, ctx) => {
247
+ input: z.object({}),
248
+ run: (_input, ctx) => {
248
249
  capturedSignal = ctx.signal;
249
250
  return Result.ok({ ok: true });
250
251
  },
251
- input: z.object({}),
252
252
  });
253
253
 
254
254
  const controller = new AbortController();
@@ -272,12 +272,12 @@ describe('buildMcpTools', () => {
272
272
  let contextUsed = false;
273
273
 
274
274
  const ctxTrail = trail('ctx.check', {
275
- implementation: (_input, ctx) => {
275
+ input: z.object({}),
276
+ run: (_input, ctx) => {
276
277
  const ctxRecord = ctx as Record<string, unknown>;
277
278
  contextUsed = ctxRecord['custom'] === true;
278
279
  return Result.ok({ ok: true });
279
280
  },
280
- input: z.object({}),
281
281
  });
282
282
 
283
283
  const app = topo('myapp', { ctxTrail });
@@ -299,14 +299,16 @@ describe('buildMcpTools', () => {
299
299
  describe('blob outputs', () => {
300
300
  test('BlobRef output converts to image content', async () => {
301
301
  const blobTrail = trail('blob.image', {
302
- implementation: () =>
303
- Result.ok({
304
- data: new Uint8Array([1, 2, 3]),
305
- kind: 'blob' as const,
306
- mimeType: 'image/png',
307
- name: 'test.png',
308
- }),
309
302
  input: z.object({}),
303
+ run: () =>
304
+ Result.ok(
305
+ createBlobRef({
306
+ data: new Uint8Array([1, 2, 3]),
307
+ mimeType: 'image/png',
308
+ name: 'test.png',
309
+ size: 3,
310
+ })
311
+ ),
310
312
  });
311
313
 
312
314
  const tool = requireOnlyTool(buildMcpTools(topo('myapp', { blobTrail })));
@@ -319,14 +321,16 @@ describe('buildMcpTools', () => {
319
321
 
320
322
  test('BlobRef output converts to resource content for non-images', async () => {
321
323
  const blobTrail = trail('blob.file', {
322
- implementation: () =>
323
- Result.ok({
324
- data: new Uint8Array([1, 2, 3]),
325
- kind: 'blob' as const,
326
- mimeType: 'application/pdf',
327
- name: 'doc.pdf',
328
- }),
329
324
  input: z.object({}),
325
+ run: () =>
326
+ Result.ok(
327
+ createBlobRef({
328
+ data: new Uint8Array([1, 2, 3]),
329
+ mimeType: 'application/pdf',
330
+ name: 'doc.pdf',
331
+ size: 3,
332
+ })
333
+ ),
330
334
  });
331
335
 
332
336
  const tool = requireOnlyTool(buildMcpTools(topo('myapp', { blobTrail })));
@@ -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
+ input: z.object({}),
354
+ run: () =>
355
+ Result.ok(
356
+ createBlobRef({
357
+ data: stream,
358
+ mimeType: 'image/gif',
359
+ name: 'anim.gif',
360
+ size: 3,
361
+ })
362
+ ),
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
+ input: z.object({}),
378
+ run: () => Result.ok({ ok: true }),
379
+ });
380
+ const underscoreTrail = trail('foo_bar', {
381
+ input: z.object({}),
382
+ run: () => Result.ok({ ok: true }),
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
+ input: z.object({}),
392
+ run: () => Result.ok({ ok: true }),
393
+ });
394
+ const underscoreTrail = trail('foo_bar', {
395
+ input: z.object({}),
396
+ run: () => Result.ok({ ok: true }),
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
+ input: z.object({}),
406
+ run: () => Result.ok({ ok: true }),
407
+ });
408
+ const barTrail = trail('bar', {
409
+ input: z.object({}),
410
+ run: () => Result.ok({ ok: true }),
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', () => {
@@ -343,11 +420,10 @@ describe('buildMcpTools', () => {
343
420
  const greetTrail = trail('greet', {
344
421
  description: 'Greet someone',
345
422
  idempotent: true,
346
- implementation: (input) =>
347
- Result.ok({ greeting: `Hello, ${input.name}!` }),
348
423
  input: z.object({ name: z.string() }),
424
+ intent: 'read',
349
425
  output: z.object({ greeting: z.string() }),
350
- readOnly: true,
426
+ run: (input) => Result.ok({ greeting: `Hello, ${input.name}!` }),
351
427
  });
352
428
 
353
429
  const tool = requireOnlyTool(
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Derive MCP tool annotations from trail spec markers.
2
+ * Derive MCP tool annotations from trail spec fields.
3
3
  */
4
4
 
5
- import type { Trail } from '@ontrails/core';
5
+ import type { Intent, Trail } from '@ontrails/core';
6
6
 
7
7
  // ---------------------------------------------------------------------------
8
8
  // Types
@@ -27,18 +27,18 @@ export interface McpAnnotations {
27
27
  * Omitted hints let the MCP SDK use its defaults.
28
28
  */
29
29
  export const deriveAnnotations = (
30
- trail: Pick<
31
- Trail<unknown, unknown>,
32
- 'readOnly' | 'destructive' | 'idempotent' | 'description'
33
- >
30
+ trail: Pick<Trail<unknown, unknown>, 'intent' | 'idempotent' | 'description'>
34
31
  ): McpAnnotations => {
35
32
  const annotations: Record<string, unknown> = {};
36
33
 
37
- if (trail.readOnly === true) {
38
- annotations['readOnlyHint'] = true;
39
- }
40
- if (trail.destructive === true) {
41
- annotations['destructiveHint'] = true;
34
+ const intentToHint: Partial<Record<Intent, string>> = {
35
+ destroy: 'destructiveHint',
36
+ read: 'readOnlyHint',
37
+ };
38
+
39
+ const hint = intentToHint[trail.intent];
40
+ if (hint) {
41
+ annotations[hint] = true;
42
42
  }
43
43
  if (trail.idempotent === true) {
44
44
  annotations['idempotentHint'] = true;
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
- interface BlobRef {
72
- readonly data: Uint8Array;
73
- readonly kind: 'blob';
74
- readonly mimeType: string;
75
- readonly name?: string | undefined;
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
- const isBlobRef = (value: unknown): value is BlobRef => {
79
- if (typeof value !== 'object' || value === null) {
80
- return false;
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
- const obj = value as Record<string, unknown>;
83
- return (
84
- obj['kind'] === 'blob' &&
85
- obj['data'] instanceof Uint8Array &&
86
- typeof obj['mimeType'] === 'string'
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(blob.data),
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 ?? 'unnamed'}`,
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 } = separateBlobFields(obj);
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 = (value: unknown): readonly McpContent[] => {
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
  }
@@ -198,11 +224,11 @@ const executeAndMap = async (
198
224
  ctx: TrailContext,
199
225
  layers: readonly Layer[]
200
226
  ): Promise<McpToolResult> => {
201
- const impl = composeLayers([...layers], trail, trail.implementation);
227
+ const impl = composeLayers([...layers], trail, trail.run);
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) {
@@ -238,15 +264,15 @@ const createHandler =
238
264
  * Each trail in the topo becomes an McpToolDefinition with:
239
265
  * - A derived tool name (app-prefixed, underscore-delimited)
240
266
  * - JSON Schema input from zodToJsonSchema
241
- * - MCP annotations from trail markers
267
+ * - MCP annotations from trail metadata
242
268
  * - A handler that validates, composes layers, executes, and maps results
243
269
  */
244
- /** Check if a trail should be included based on markers and filters. */
270
+ /** Check if a trail should be included based on metadata and filters. */
245
271
  const shouldInclude = (
246
272
  trail: Trail<unknown, unknown>,
247
273
  options: BuildMcpToolsOptions
248
274
  ): boolean => {
249
- if (trail.markers?.['internal'] === true) {
275
+ if (trail.metadata?.['internal'] === true) {
250
276
  return false;
251
277
  }
252
278
  if (options.includeTrails !== undefined && options.includeTrails.length > 0) {
@@ -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. */
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' &&
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 item of app.list()) {
309
- if (item.kind !== 'trail' && item.kind !== 'hike') {
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;