@ontrails/mcp 0.2.0

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/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # @ontrails/mcp
2
+
3
+ MCP surface adapter. One `surface()` call turns a topo into an MCP server with tool definitions, annotations, and progress bridging -- all derived from the trail contracts.
4
+
5
+ ## Usage
6
+
7
+ ```typescript
8
+ import { trail, topo, Result } from '@ontrails/core';
9
+ import { surface } from '@ontrails/mcp';
10
+ import { z } from 'zod';
11
+
12
+ const greet = trail('greet', {
13
+ input: z.object({ name: z.string().describe('Who to greet') }),
14
+ output: z.object({ greeting: z.string() }),
15
+ intent: 'read',
16
+ examples: [
17
+ {
18
+ expected: { greeting: 'Hello, Ada!' },
19
+ input: { name: 'Ada' },
20
+ name: 'Ada',
21
+ },
22
+ ],
23
+ implementation: (input) => Result.ok({ greeting: `Hello, ${input.name}!` }),
24
+ });
25
+
26
+ const graph = topo('myapp', { greet });
27
+ await surface(graph);
28
+ ```
29
+
30
+ This starts an MCP server over stdio with a `myapp_greet` tool. The tool gets `readOnlyHint: true`, JSON Schema input, JSON Schema output, and structured examples -- all derived from the trail definition.
31
+
32
+ For more control, build the tools yourself:
33
+
34
+ ```typescript
35
+ import { deriveMcpTools } from '@ontrails/mcp';
36
+
37
+ const result = deriveMcpTools(graph);
38
+ if (result.isErr()) throw result.error; // ValidationError on tool-name collision
39
+ for (const tool of result.value) {
40
+ server.registerTool(tool.name, tool.handler, {
41
+ inputSchema: tool.inputSchema,
42
+ outputSchema: tool.outputSchema,
43
+ annotations: tool.annotations,
44
+ _meta: tool._meta,
45
+ });
46
+ }
47
+ ```
48
+
49
+ `deriveMcpTools` returns `Result<McpToolDefinition[], Error>` rather than a bare array. It returns `Result.err(ValidationError)` if two trails derive the same MCP tool name. Each `McpToolDefinition` includes a `trailId` field that records which trail the tool was derived from.
50
+
51
+ ## API
52
+
53
+ | Export | What it does |
54
+ | --- | --- |
55
+ | `surface(graph, options?)` | Start an MCP server with all trails as tools |
56
+ | `deriveMcpTools(graph, options?)` | Build tool definitions without starting a server |
57
+ | `buildMcpResources(graph, tools, config?)` | Build MCP resource listings and read handlers for cold context |
58
+ | `deriveToolName(appName, trailId)` | Compute the MCP tool name from app and trail IDs |
59
+ | `deriveAnnotations(trail)` | Extract MCP annotations from trail intent, idempotency, and description |
60
+ | `createMcpProgressCallback(server)` | Bridge `ctx.progress` to MCP `notifications/progress` |
61
+
62
+ See the [API Reference](../../docs/api-reference.md) for the full list.
63
+
64
+ ## Annotations
65
+
66
+ Trail intent, idempotency, and description map directly to MCP annotations:
67
+
68
+ | Trail field | MCP annotation |
69
+ | --- | --- |
70
+ | `intent: 'read'` | `readOnlyHint: true` |
71
+ | `intent: 'destroy'` | `destructiveHint: true` |
72
+ | `idempotent: true` | `idempotentHint: true` |
73
+ | `description` | `title` |
74
+
75
+ No manual annotation definitions. The contract is the source of truth.
76
+
77
+ ## Schemas and Examples
78
+
79
+ MCP tool definitions include the trail's input schema, and trails with an `output` schema also render that schema into MCP `outputSchema`. Non-object trail outputs are wrapped in a `{ data: ... }` object because MCP structured tool results are object-shaped.
80
+
81
+ Trail examples are rendered as structured metadata under `_meta["ontrails/examples"]`. Each rendered example preserves its input, expected output or error, a success/error kind, and provenance pointing back to the authored `trail.examples` field.
82
+
83
+ ## MCP resources and deferred loading
84
+
85
+ Cold context is rendered through MCP resources, not extra Trails resources. `surface(graph)` and `createServer(graph)` expose MCP resources by default:
86
+
87
+ - `trails://surface-map` lists the resolved MCP tool rendering, including ordinary tools, trailhead tools, schemas, versions, deferred hints, and member trail IDs.
88
+ - `trails://examples/<trailId>` exposes structured examples for exposed trails that define examples.
89
+ - `trails://trail/<trailId>` exposes MCP-visible graph facts for an exposed trail when graph resources are enabled.
90
+
91
+ Disable resource rendering only when the host needs a minimal MCP capability surface:
92
+
93
+ ```typescript
94
+ await surface(graph, { mcpResources: false });
95
+ ```
96
+
97
+ Or choose a narrower resource set:
98
+
99
+ ```typescript
100
+ await surface(graph, {
101
+ mcpResources: { examples: false, graph: true, surfaceMap: true },
102
+ });
103
+ ```
104
+
105
+ Graph resources are opt-in for general MCP hosts because they widen cold context for every exposed trail. The Trails operator enables them so agents can inspect high-signal graph facts without invoking another tool.
106
+
107
+ Trailhead definitions may set `mcp: { loading: 'deferred' }`. In this release, deferred loading is a compatibility hint under `_meta["ontrails/deferred"]`; the MCP tool schema remains present so clients that do not understand deferred loading continue to work.
108
+
109
+ ## Tool naming
110
+
111
+ Trail IDs become MCP tool names with the app prefix: `entity.show` in app `myapp` becomes `myapp_entity_show`. Dots and hyphens become underscores, everything lowercase.
112
+
113
+ ## Resource resolution
114
+
115
+ Declared resources on each trail are resolved into the context before the implementation receives input.
116
+
117
+ ## Progress bridge
118
+
119
+ Implementations report progress through `ctx.progress`. On MCP, these bridge to `notifications/progress` when the client sends a `progressToken`:
120
+
121
+ ```typescript
122
+ const importTrail = trail('data.import', {
123
+ implementation: async (input, ctx) => {
124
+ for (let i = 0; i < items.length; i++) {
125
+ await processItem(items[i]);
126
+ ctx.progress?.({ type: 'progress', current: i + 1, total: items.length });
127
+ }
128
+ return Result.ok({ imported: items.length });
129
+ },
130
+ });
131
+ ```
132
+
133
+ ## Filtering
134
+
135
+ ```typescript
136
+ await surface(graph, { include: ['entity.**', 'search'] });
137
+ await surface(graph, { exclude: ['internal.debug'] });
138
+ ```
139
+
140
+ `*` matches one dotted segment and `**` matches any depth. Trails declared with `visibility: 'internal'` stay hidden unless you include their exact trail ID.
141
+
142
+ ## Installation
143
+
144
+ These commands target stable `0.2.0`. Run them after that version is published to npm.
145
+
146
+ ```bash
147
+ bun add --exact @ontrails/mcp@0.2.0
148
+ ```
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@ontrails/mcp",
3
+ "version": "0.2.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/outfitter-dev/trails.git",
7
+ "directory": "packages/mcp"
8
+ },
9
+ "files": [
10
+ "src/**/*.ts",
11
+ "!src/**/__tests__/**",
12
+ "!src/**/*.test.ts",
13
+ "!src/**/*.test-d.ts",
14
+ "README.md",
15
+ "CHANGELOG.md"
16
+ ],
17
+ "type": "module",
18
+ "exports": {
19
+ ".": "./src/index.ts",
20
+ "./package.json": "./package.json"
21
+ },
22
+ "scripts": {
23
+ "build": "tsc -b",
24
+ "test": "bun test",
25
+ "typecheck": "tsc --noEmit",
26
+ "lint": "oxlint ./src",
27
+ "clean": "rm -rf dist *.tsbuildinfo"
28
+ },
29
+ "dependencies": {
30
+ "@ontrails/core": "^0.2.0"
31
+ },
32
+ "peerDependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.28.0",
34
+ "zod": "^4.3.5"
35
+ }
36
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Derive MCP tool annotations from trail spec fields.
3
+ */
4
+
5
+ import type { Intent, Trail } from '@ontrails/core';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Types
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export interface McpAnnotations {
12
+ readonly readOnlyHint?: boolean | undefined;
13
+ readonly destructiveHint?: boolean | undefined;
14
+ readonly idempotentHint?: boolean | undefined;
15
+ readonly openWorldHint?: boolean | undefined;
16
+ readonly title?: string | undefined;
17
+ }
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Derivation
21
+ // ---------------------------------------------------------------------------
22
+
23
+ /**
24
+ * Map trail spec fields to MCP tool annotations.
25
+ *
26
+ * Only sets hints that are explicitly declared on the trail.
27
+ * Omitted hints let the MCP SDK use its defaults.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * import { deriveAnnotations } from '@ontrails/mcp';
32
+ *
33
+ * const annotations = deriveAnnotations({
34
+ * intent: 'read',
35
+ * idempotent: true,
36
+ * description: 'Show account',
37
+ * });
38
+ *
39
+ * // read intent -> readOnlyHint, idempotent -> idempotentHint, description -> title.
40
+ * annotations.readOnlyHint === true;
41
+ * annotations.idempotentHint === true;
42
+ * annotations.title === 'Show account';
43
+ *
44
+ * // destroy intent -> destructiveHint.
45
+ * const destroyAnnotations = deriveAnnotations({ intent: 'destroy' });
46
+ * destroyAnnotations.destructiveHint === true;
47
+ * ```
48
+ */
49
+ export const deriveAnnotations = (
50
+ trail: Pick<
51
+ Trail<unknown, unknown, unknown>,
52
+ 'intent' | 'idempotent' | 'description'
53
+ >
54
+ ): McpAnnotations => {
55
+ const annotations: Record<string, unknown> = {};
56
+
57
+ const intentToHint: Partial<Record<Intent, string>> = {
58
+ destroy: 'destructiveHint',
59
+ read: 'readOnlyHint',
60
+ };
61
+
62
+ const hint = intentToHint[trail.intent];
63
+ if (hint) {
64
+ annotations[hint] = true;
65
+ }
66
+ if (trail.idempotent === true) {
67
+ annotations['idempotentHint'] = true;
68
+ }
69
+ if (trail.description !== undefined) {
70
+ annotations['title'] = trail.description;
71
+ }
72
+
73
+ return annotations as McpAnnotations;
74
+ };