@ontrails/mcp 1.0.0-beta.12 → 1.0.0-beta.13

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.
@@ -0,0 +1,108 @@
1
+ /**
2
+ * trailhead() -- the one-liner MCP server launcher.
3
+ *
4
+ * Three lines to expose trails as MCP tools:
5
+ *
6
+ * ```ts
7
+ * const app = topo("myapp", entity);
8
+ * await trailhead(app);
9
+ * ```
10
+ */
11
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
12
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
13
+ import { validateTopo } from '@ontrails/core';
14
+ import { buildMcpTools } from './build.js';
15
+ import { connectStdio } from './stdio.js';
16
+ // ---------------------------------------------------------------------------
17
+ // Internal: create MCP server with tool handlers
18
+ // ---------------------------------------------------------------------------
19
+ /**
20
+ * Create an MCP Server instance and register all tools.
21
+ */
22
+ export const createMcpServer = (tools, info) => {
23
+ const server = new Server({ name: info.name, version: info.version }, { capabilities: { tools: {} } });
24
+ // Build a lookup map for tool dispatch
25
+ const toolMap = new Map();
26
+ for (const tool of tools) {
27
+ toolMap.set(tool.name, tool);
28
+ }
29
+ // Register tools/list handler
30
+ // oxlint-disable-next-line require-await -- MCP SDK requires async handler
31
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
32
+ tools: tools.map((t) => ({
33
+ annotations: t.annotations,
34
+ description: t.description,
35
+ inputSchema: t.inputSchema,
36
+ name: t.name,
37
+ })),
38
+ }));
39
+ // Register tools/call handler
40
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
41
+ const tool = toolMap.get(request.params.name);
42
+ if (tool === undefined) {
43
+ return {
44
+ content: [
45
+ {
46
+ text: `Unknown tool: ${request.params.name}`,
47
+ type: 'text',
48
+ },
49
+ ],
50
+ isError: true,
51
+ };
52
+ }
53
+ const args = (request.params.arguments ?? {});
54
+ const progressToken = request.params._meta?.progressToken;
55
+ const sendProgress = progressToken === undefined
56
+ ? undefined
57
+ : async (current, total) => {
58
+ await server.notification({
59
+ method: 'notifications/progress',
60
+ params: {
61
+ progress: current,
62
+ progressToken: progressToken,
63
+ total,
64
+ },
65
+ });
66
+ };
67
+ const extra = {
68
+ abortSignal: undefined,
69
+ progressToken,
70
+ sendProgress,
71
+ };
72
+ const result = await tool.handler(args, extra);
73
+ // Spread to satisfy MCP SDK's index-signature requirement
74
+ return { ...result };
75
+ });
76
+ return server;
77
+ };
78
+ // ---------------------------------------------------------------------------
79
+ // trailhead
80
+ // ---------------------------------------------------------------------------
81
+ /**
82
+ * Build MCP tools from an App, create a server, and connect via stdio.
83
+ */
84
+ export const trailhead = async (app, options = {}) => {
85
+ if (options.validate !== false) {
86
+ const validated = validateTopo(app);
87
+ if (validated.isErr()) {
88
+ throw validated.error;
89
+ }
90
+ }
91
+ const toolsResult = buildMcpTools(app, {
92
+ configValues: options.configValues,
93
+ createContext: options.createContext,
94
+ excludeTrails: options.excludeTrails,
95
+ gates: options.gates,
96
+ includeTrails: options.includeTrails,
97
+ provisions: options.provisions,
98
+ });
99
+ if (toolsResult.isErr()) {
100
+ throw toolsResult.error;
101
+ }
102
+ const server = createMcpServer(toolsResult.value, {
103
+ name: options.serverInfo?.name ?? app.name,
104
+ version: options.serverInfo?.version ?? '0.1.0',
105
+ });
106
+ await connectStdio(server);
107
+ };
108
+ //# sourceMappingURL=trailhead.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trailhead.js","sourceRoot":"","sources":["../src/trailhead.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.12",
3
+ "version": "1.0.0-beta.13",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
@@ -2,13 +2,13 @@ import { describe, expect, test } from 'bun:test';
2
2
 
3
3
  import {
4
4
  Result,
5
- SURFACE_KEY,
5
+ TRAILHEAD_KEY,
6
6
  createBlobRef,
7
- service,
7
+ provision,
8
8
  trail,
9
9
  topo,
10
10
  } from '@ontrails/core';
11
- import type { Layer } from '@ontrails/core';
11
+ import type { Gate } from '@ontrails/core';
12
12
  import { z } from 'zod';
13
13
 
14
14
  import { buildMcpTools } from '../build.js';
@@ -19,27 +19,28 @@ import type { McpExtra, McpToolDefinition } from '../build.js';
19
19
  // ---------------------------------------------------------------------------
20
20
 
21
21
  const echoTrail = trail('echo', {
22
+ blaze: (input) => Result.ok({ reply: input.message }),
22
23
  description: 'Echo a message back',
23
24
  input: z.object({ message: z.string() }),
24
25
  intent: 'read',
25
26
  output: z.object({ reply: z.string() }),
26
- run: (input) => Result.ok({ reply: input.message }),
27
27
  });
28
28
 
29
29
  const deleteTrail = trail('item.delete', {
30
+ blaze: (_input) => Result.ok({ deleted: true }),
30
31
  description: 'Delete an item',
31
32
  input: z.object({ id: z.string() }),
32
33
  intent: 'destroy',
33
- run: (_input) => Result.ok({ deleted: true }),
34
34
  });
35
35
 
36
36
  const failTrail = trail('fail', {
37
+ blaze: (input) => Result.err(new Error(input.reason)),
37
38
  description: 'Always fails',
38
39
  input: z.object({ reason: z.string() }),
39
- run: (input) => Result.err(new Error(input.reason)),
40
40
  });
41
41
 
42
42
  const exampleTrail = trail('with.examples', {
43
+ blaze: (input) => Result.ok({ greeting: `hello ${input.name}` }),
43
44
  description: 'A trail with examples',
44
45
  examples: [
45
46
  {
@@ -49,10 +50,9 @@ const exampleTrail = trail('with.examples', {
49
50
  },
50
51
  ],
51
52
  input: z.object({ name: z.string() }),
52
- run: (input) => Result.ok({ greeting: `hello ${input.name}` }),
53
53
  });
54
54
 
55
- const dbService = service('db.main', {
55
+ const dbProvision = provision('db.main', {
56
56
  create: () =>
57
57
  Result.ok({
58
58
  source: 'factory',
@@ -82,7 +82,7 @@ const requireOnlyTool = (tools: McpToolDefinition[]) => {
82
82
 
83
83
  /**
84
84
  * Unwrap buildMcpTools result for success-path tests.
85
- * Throws if the result is an error so test failures surface clearly.
85
+ * Throws if the result is an error so test failures show up clearly.
86
86
  */
87
87
  const buildTools = (
88
88
  ...args: Parameters<typeof buildMcpTools>
@@ -203,10 +203,10 @@ describe('buildMcpTools', () => {
203
203
 
204
204
  test('handler catches thrown exceptions', async () => {
205
205
  const throwTrail = trail('throw', {
206
- input: z.object({}),
207
- run: () => {
206
+ blaze: () => {
208
207
  throw new Error('unexpected crash');
209
208
  },
209
+ input: z.object({}),
210
210
  });
211
211
 
212
212
  const tool = requireOnlyTool(buildTools(topo('myapp', { throwTrail })));
@@ -254,11 +254,11 @@ describe('buildMcpTools', () => {
254
254
  });
255
255
 
256
256
  describe('composition', () => {
257
- test('layers compose and execute around the implementation', async () => {
257
+ test('gates compose and execute around the implementation', async () => {
258
258
  const calls: string[] = [];
259
259
 
260
- const testLayer: Layer = {
261
- name: 'test-layer',
260
+ const testGate: Gate = {
261
+ name: 'test-gate',
262
262
  wrap(_trail, impl) {
263
263
  return async (input, ctx) => {
264
264
  calls.push('before');
@@ -270,7 +270,7 @@ describe('buildMcpTools', () => {
270
270
  };
271
271
 
272
272
  const app = topo('myapp', { echoTrail });
273
- const tool = requireOnlyTool(buildTools(app, { layers: [testLayer] }));
273
+ const tool = requireOnlyTool(buildTools(app, { gates: [testGate] }));
274
274
 
275
275
  await tool.handler({ message: 'hi' }, noExtra);
276
276
  expect(calls).toEqual(['before', 'after']);
@@ -280,17 +280,17 @@ describe('buildMcpTools', () => {
280
280
  let capturedSignal: AbortSignal | undefined;
281
281
 
282
282
  const signalTrail = trail('signal.check', {
283
- input: z.object({}),
284
- run: (_input, ctx) => {
285
- capturedSignal = ctx.signal;
283
+ blaze: (_input, ctx) => {
284
+ capturedSignal = ctx.abortSignal;
286
285
  return Result.ok({ ok: true });
287
286
  },
287
+ input: z.object({}),
288
288
  });
289
289
 
290
290
  const controller = new AbortController();
291
291
  const tool = requireOnlyTool(buildTools(topo('myapp', { signalTrail })));
292
292
 
293
- await tool.handler({}, { signal: controller.signal });
293
+ await tool.handler({}, { abortSignal: controller.signal });
294
294
  expect(capturedSignal).toBe(controller.signal);
295
295
  });
296
296
 
@@ -304,45 +304,45 @@ describe('buildMcpTools', () => {
304
304
 
305
305
  test('custom createContext is used when provided', async () => {
306
306
  let contextUsed = false;
307
- let surfaceUsed = false;
307
+ let trailheadMarkerUsed = false;
308
308
 
309
309
  const ctxTrail = trail('ctx.check', {
310
- input: z.object({}),
311
- run: (_input, ctx) => {
310
+ blaze: (_input, ctx) => {
312
311
  contextUsed = ctx.extensions?.['custom'] === true;
313
- surfaceUsed = ctx.extensions?.[SURFACE_KEY] === 'mcp';
312
+ trailheadMarkerUsed = ctx.extensions?.[TRAILHEAD_KEY] === 'mcp';
314
313
  return Result.ok({ ok: true });
315
314
  },
315
+ input: z.object({}),
316
316
  });
317
317
 
318
318
  const app = topo('myapp', { ctxTrail });
319
319
  const tool = requireOnlyTool(
320
320
  buildTools(app, {
321
321
  createContext: () => ({
322
+ abortSignal: new AbortController().signal,
322
323
  extensions: { custom: true },
323
324
  requestId: 'test-id',
324
- signal: new AbortController().signal,
325
325
  }),
326
326
  })
327
327
  );
328
328
 
329
329
  await tool.handler({}, noExtra);
330
330
  expect(contextUsed).toBe(true);
331
- expect(surfaceUsed).toBe(true);
331
+ expect(trailheadMarkerUsed).toBe(true);
332
332
  });
333
333
 
334
- test('service overrides are forwarded to executeTrail', async () => {
335
- const serviceTrail = trail('service.check', {
334
+ test('provision overrides are forwarded to executeTrail', async () => {
335
+ const provisionTrail = trail('provision.check', {
336
+ blaze: (_input, ctx) =>
337
+ Result.ok({ source: dbProvision.from(ctx).source as string }),
336
338
  input: z.object({}),
337
339
  output: z.object({ source: z.string() }),
338
- run: (_input, ctx) =>
339
- Result.ok({ source: dbService.from(ctx).source as string }),
340
- services: [dbService],
340
+ provisions: [dbProvision],
341
341
  });
342
342
 
343
343
  const tool = requireOnlyTool(
344
- buildTools(topo('myapp', { serviceTrail }), {
345
- services: { 'db.main': { source: 'override' } },
344
+ buildTools(topo('myapp', { provisionTrail }), {
345
+ provisions: { 'db.main': { source: 'override' } },
346
346
  })
347
347
  );
348
348
 
@@ -357,8 +357,7 @@ describe('buildMcpTools', () => {
357
357
  describe('blob outputs', () => {
358
358
  test('BlobRef output converts to image content', async () => {
359
359
  const blobTrail = trail('blob.image', {
360
- input: z.object({}),
361
- run: () =>
360
+ blaze: () =>
362
361
  Result.ok(
363
362
  createBlobRef({
364
363
  data: new Uint8Array([1, 2, 3]),
@@ -367,6 +366,7 @@ describe('buildMcpTools', () => {
367
366
  size: 3,
368
367
  })
369
368
  ),
369
+ input: z.object({}),
370
370
  });
371
371
 
372
372
  const tool = requireOnlyTool(buildTools(topo('myapp', { blobTrail })));
@@ -379,8 +379,7 @@ describe('buildMcpTools', () => {
379
379
 
380
380
  test('BlobRef output converts to resource content for non-images', async () => {
381
381
  const blobTrail = trail('blob.file', {
382
- input: z.object({}),
383
- run: () =>
382
+ blaze: () =>
384
383
  Result.ok(
385
384
  createBlobRef({
386
385
  data: new Uint8Array([1, 2, 3]),
@@ -389,6 +388,7 @@ describe('buildMcpTools', () => {
389
388
  size: 3,
390
389
  })
391
390
  ),
391
+ input: z.object({}),
392
392
  });
393
393
 
394
394
  const tool = requireOnlyTool(buildTools(topo('myapp', { blobTrail })));
@@ -408,8 +408,7 @@ describe('buildMcpTools', () => {
408
408
  },
409
409
  });
410
410
  const blobTrail = trail('blob.stream', {
411
- input: z.object({}),
412
- run: () =>
411
+ blaze: () =>
413
412
  Result.ok(
414
413
  createBlobRef({
415
414
  data: stream,
@@ -418,6 +417,7 @@ describe('buildMcpTools', () => {
418
417
  size: 3,
419
418
  })
420
419
  ),
420
+ input: z.object({}),
421
421
  });
422
422
 
423
423
  const tool = requireOnlyTool(buildTools(topo('myapp', { blobTrail })));
@@ -432,12 +432,12 @@ describe('buildMcpTools', () => {
432
432
  describe('tool-name collision detection', () => {
433
433
  test('returns Err on trails that produce the same derived tool name', () => {
434
434
  const dotTrail = trail('foo.bar', {
435
+ blaze: () => Result.ok({ ok: true }),
435
436
  input: z.object({}),
436
- run: () => Result.ok({ ok: true }),
437
437
  });
438
438
  const underscoreTrail = trail('foo_bar', {
439
+ blaze: () => Result.ok({ ok: true }),
439
440
  input: z.object({}),
440
- run: () => Result.ok({ ok: true }),
441
441
  });
442
442
 
443
443
  const app = topo('myapp', { dotTrail, underscoreTrail });
@@ -448,12 +448,12 @@ describe('buildMcpTools', () => {
448
448
 
449
449
  test('returns Err on trails where hyphen and underscore collide', () => {
450
450
  const hyphenTrail = trail('foo-bar', {
451
+ blaze: () => Result.ok({ ok: true }),
451
452
  input: z.object({}),
452
- run: () => Result.ok({ ok: true }),
453
453
  });
454
454
  const underscoreTrail = trail('foo_bar', {
455
+ blaze: () => Result.ok({ ok: true }),
455
456
  input: z.object({}),
456
- run: () => Result.ok({ ok: true }),
457
457
  });
458
458
 
459
459
  const app = topo('myapp', { hyphenTrail, underscoreTrail });
@@ -464,12 +464,12 @@ describe('buildMcpTools', () => {
464
464
 
465
465
  test('returns Ok when trail names are distinct after normalization', () => {
466
466
  const fooTrail = trail('foo', {
467
+ blaze: () => Result.ok({ ok: true }),
467
468
  input: z.object({}),
468
- run: () => Result.ok({ ok: true }),
469
469
  });
470
470
  const barTrail = trail('bar', {
471
+ blaze: () => Result.ok({ ok: true }),
471
472
  input: z.object({}),
472
- run: () => Result.ok({ ok: true }),
473
473
  });
474
474
 
475
475
  const app = topo('myapp', { barTrail, fooTrail });
@@ -481,12 +481,12 @@ describe('buildMcpTools', () => {
481
481
  describe('end-to-end', () => {
482
482
  test('full pipeline from trail to MCP response', async () => {
483
483
  const greetTrail = trail('greet', {
484
+ blaze: (input) => Result.ok({ greeting: `Hello, ${input.name}!` }),
484
485
  description: 'Greet someone',
485
486
  idempotent: true,
486
487
  input: z.object({ name: z.string() }),
487
488
  intent: 'read',
488
489
  output: z.object({ greeting: z.string() }),
489
- run: (input) => Result.ok({ greeting: `Hello, ${input.name}!` }),
490
490
  });
491
491
 
492
492
  const tool = requireOnlyTool(buildTools(topo('testapp', { greetTrail })));
@@ -3,7 +3,7 @@ import { describe, expect, test } from 'bun:test';
3
3
  import { Result, trail, topo } from '@ontrails/core';
4
4
  import { z } from 'zod';
5
5
 
6
- import { blaze, createMcpServer } from '../blaze.js';
6
+ import { trailhead, createMcpServer } from '../trailhead.js';
7
7
  import { buildMcpTools } from '../build.js';
8
8
  import type { McpToolDefinition } from '../build.js';
9
9
 
@@ -21,7 +21,7 @@ const requireTool = (tools: McpToolDefinition[], name: string) => {
21
21
  };
22
22
 
23
23
  /**
24
- * Unwrap buildMcpTools result, throwing on error so test failures surface clearly.
24
+ * Unwrap buildMcpTools result, throwing on error so test failures show up clearly.
25
25
  */
26
26
  const buildTools = (
27
27
  ...args: Parameters<typeof buildMcpTools>
@@ -35,44 +35,44 @@ const buildTools = (
35
35
 
36
36
  const createIntegrationTools = () => {
37
37
  const greetTrail = trail('greet', {
38
+ blaze: (input) => Result.ok({ greeting: `Hello, ${input.name}!` }),
38
39
  description: 'Greet someone',
39
40
  input: z.object({ name: z.string() }),
40
41
  intent: 'read',
41
- run: (input) => Result.ok({ greeting: `Hello, ${input.name}!` }),
42
42
  });
43
43
 
44
44
  const deleteTrail = trail('item.delete', {
45
+ blaze: (_input) => Result.ok({ deleted: true }),
45
46
  description: 'Delete an item',
46
47
  input: z.object({ id: z.string() }),
47
48
  intent: 'destroy',
48
- run: (_input) => Result.ok({ deleted: true }),
49
49
  });
50
50
 
51
51
  return buildTools(topo('myapp', { deleteTrail, greetTrail }));
52
52
  };
53
53
 
54
- describe('blaze', () => {
55
- test('blaze throws on invalid topo', async () => {
54
+ describe('trailhead', () => {
55
+ test('trailhead throws on invalid topo', async () => {
56
56
  const t = trail('broken', {
57
- follow: ['nonexistent.trail'],
57
+ blaze: () => Result.ok({}),
58
+ crosses: ['nonexistent.trail'],
58
59
  input: z.object({}),
59
60
  output: z.object({}),
60
- run: () => Result.ok({}),
61
61
  });
62
62
  const app = topo('test', { t });
63
- await expect(blaze(app)).rejects.toThrow(/validation/i);
63
+ await expect(trailhead(app)).rejects.toThrow(/validation/i);
64
64
  });
65
65
 
66
- test('blaze skips validation when validate: false', async () => {
66
+ test('trailhead skips validation when validate: false', async () => {
67
67
  const t = trail('broken', {
68
- follow: ['nonexistent.trail'],
68
+ blaze: () => Result.ok({}),
69
+ crosses: ['nonexistent.trail'],
69
70
  input: z.object({}),
70
71
  output: z.object({}),
71
- run: () => Result.ok({}),
72
72
  });
73
73
  const app = topo('test', { t });
74
74
  const result = await Promise.race([
75
- blaze(app, { validate: false }).then(() => 'resolved' as const),
75
+ trailhead(app, { validate: false }).then(() => 'resolved' as const),
76
76
  // oxlint-disable-next-line avoid-new -- Promise constructor needed for setTimeout-based timeout
77
77
  new Promise<'timeout'>((resolve) => {
78
78
  setTimeout(() => {
@@ -83,20 +83,20 @@ describe('blaze', () => {
83
83
  expect(['resolved', 'timeout']).toContain(result);
84
84
  });
85
85
 
86
- test('BlazeMcpOptions accepts service overrides', () => {
87
- const opts: Parameters<typeof blaze>[1] = {
88
- services: {},
86
+ test('TrailheadMcpOptions accepts provision overrides', () => {
87
+ const opts: Parameters<typeof trailhead>[1] = {
88
+ provisions: {},
89
89
  validate: false,
90
90
  };
91
- expect(opts.services).toEqual({});
91
+ expect(opts.provisions).toEqual({});
92
92
  });
93
93
 
94
94
  test('createMcpServer registers tools that can be listed', () => {
95
95
  const echoTrail = trail('echo', {
96
+ blaze: (input) => Result.ok({ reply: input.message }),
96
97
  description: 'Echo',
97
98
  input: z.object({ message: z.string() }),
98
99
  intent: 'read',
99
- run: (input) => Result.ok({ reply: input.message }),
100
100
  });
101
101
 
102
102
  const app = topo('testapp', { echoTrail });
@@ -112,16 +112,16 @@ describe('blaze', () => {
112
112
 
113
113
  test('createMcpServer handles multiple tools', () => {
114
114
  const echoTrail = trail('echo', {
115
+ blaze: (input) => Result.ok({ reply: input.message }),
115
116
  description: 'Echo',
116
117
  input: z.object({ message: z.string() }),
117
- run: (input) => Result.ok({ reply: input.message }),
118
118
  });
119
119
 
120
120
  const searchTrail = trail('search', {
121
+ blaze: (input) => Result.ok({ results: [input.query] }),
121
122
  description: 'Search',
122
123
  input: z.object({ query: z.string() }),
123
124
  intent: 'read',
124
- run: (input) => Result.ok({ results: [input.query] }),
125
125
  });
126
126
 
127
127
  const app = topo('testapp', { echoTrail, searchTrail });