@ontrails/mcp 1.0.0-beta.11 → 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.11",
3
+ "version": "1.0.0-beta.13",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
@@ -14,7 +14,7 @@
14
14
  "clean": "rm -rf dist *.tsbuildinfo"
15
15
  },
16
16
  "dependencies": {
17
- "@ontrails/core": "^1.0.0-beta.8"
17
+ "@ontrails/core": "^1.0.0-beta.12"
18
18
  },
19
19
  "peerDependencies": {
20
20
  "@modelcontextprotocol/sdk": "^1.12.0",
@@ -1,7 +1,14 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
2
 
3
- import { Result, createBlobRef, service, trail, topo } from '@ontrails/core';
4
- import type { Layer } from '@ontrails/core';
3
+ import {
4
+ Result,
5
+ TRAILHEAD_KEY,
6
+ createBlobRef,
7
+ provision,
8
+ trail,
9
+ topo,
10
+ } from '@ontrails/core';
11
+ import type { Gate } from '@ontrails/core';
5
12
  import { z } from 'zod';
6
13
 
7
14
  import { buildMcpTools } from '../build.js';
@@ -12,27 +19,28 @@ import type { McpExtra, McpToolDefinition } from '../build.js';
12
19
  // ---------------------------------------------------------------------------
13
20
 
14
21
  const echoTrail = trail('echo', {
22
+ blaze: (input) => Result.ok({ reply: input.message }),
15
23
  description: 'Echo a message back',
16
24
  input: z.object({ message: z.string() }),
17
25
  intent: 'read',
18
26
  output: z.object({ reply: z.string() }),
19
- run: (input) => Result.ok({ reply: input.message }),
20
27
  });
21
28
 
22
29
  const deleteTrail = trail('item.delete', {
30
+ blaze: (_input) => Result.ok({ deleted: true }),
23
31
  description: 'Delete an item',
24
32
  input: z.object({ id: z.string() }),
25
33
  intent: 'destroy',
26
- run: (_input) => Result.ok({ deleted: true }),
27
34
  });
28
35
 
29
36
  const failTrail = trail('fail', {
37
+ blaze: (input) => Result.err(new Error(input.reason)),
30
38
  description: 'Always fails',
31
39
  input: z.object({ reason: z.string() }),
32
- run: (input) => Result.err(new Error(input.reason)),
33
40
  });
34
41
 
35
42
  const exampleTrail = trail('with.examples', {
43
+ blaze: (input) => Result.ok({ greeting: `hello ${input.name}` }),
36
44
  description: 'A trail with examples',
37
45
  examples: [
38
46
  {
@@ -42,10 +50,9 @@ const exampleTrail = trail('with.examples', {
42
50
  },
43
51
  ],
44
52
  input: z.object({ name: z.string() }),
45
- run: (input) => Result.ok({ greeting: `hello ${input.name}` }),
46
53
  });
47
54
 
48
- const dbService = service('db.main', {
55
+ const dbProvision = provision('db.main', {
49
56
  create: () =>
50
57
  Result.ok({
51
58
  source: 'factory',
@@ -75,7 +82,7 @@ const requireOnlyTool = (tools: McpToolDefinition[]) => {
75
82
 
76
83
  /**
77
84
  * Unwrap buildMcpTools result for success-path tests.
78
- * 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.
79
86
  */
80
87
  const buildTools = (
81
88
  ...args: Parameters<typeof buildMcpTools>
@@ -196,10 +203,10 @@ describe('buildMcpTools', () => {
196
203
 
197
204
  test('handler catches thrown exceptions', async () => {
198
205
  const throwTrail = trail('throw', {
199
- input: z.object({}),
200
- run: () => {
206
+ blaze: () => {
201
207
  throw new Error('unexpected crash');
202
208
  },
209
+ input: z.object({}),
203
210
  });
204
211
 
205
212
  const tool = requireOnlyTool(buildTools(topo('myapp', { throwTrail })));
@@ -247,11 +254,11 @@ describe('buildMcpTools', () => {
247
254
  });
248
255
 
249
256
  describe('composition', () => {
250
- test('layers compose and execute around the implementation', async () => {
257
+ test('gates compose and execute around the implementation', async () => {
251
258
  const calls: string[] = [];
252
259
 
253
- const testLayer: Layer = {
254
- name: 'test-layer',
260
+ const testGate: Gate = {
261
+ name: 'test-gate',
255
262
  wrap(_trail, impl) {
256
263
  return async (input, ctx) => {
257
264
  calls.push('before');
@@ -263,7 +270,7 @@ describe('buildMcpTools', () => {
263
270
  };
264
271
 
265
272
  const app = topo('myapp', { echoTrail });
266
- const tool = requireOnlyTool(buildTools(app, { layers: [testLayer] }));
273
+ const tool = requireOnlyTool(buildTools(app, { gates: [testGate] }));
267
274
 
268
275
  await tool.handler({ message: 'hi' }, noExtra);
269
276
  expect(calls).toEqual(['before', 'after']);
@@ -273,17 +280,17 @@ describe('buildMcpTools', () => {
273
280
  let capturedSignal: AbortSignal | undefined;
274
281
 
275
282
  const signalTrail = trail('signal.check', {
276
- input: z.object({}),
277
- run: (_input, ctx) => {
278
- capturedSignal = ctx.signal;
283
+ blaze: (_input, ctx) => {
284
+ capturedSignal = ctx.abortSignal;
279
285
  return Result.ok({ ok: true });
280
286
  },
287
+ input: z.object({}),
281
288
  });
282
289
 
283
290
  const controller = new AbortController();
284
291
  const tool = requireOnlyTool(buildTools(topo('myapp', { signalTrail })));
285
292
 
286
- await tool.handler({}, { signal: controller.signal });
293
+ await tool.handler({}, { abortSignal: controller.signal });
287
294
  expect(capturedSignal).toBe(controller.signal);
288
295
  });
289
296
 
@@ -297,42 +304,45 @@ describe('buildMcpTools', () => {
297
304
 
298
305
  test('custom createContext is used when provided', async () => {
299
306
  let contextUsed = false;
307
+ let trailheadMarkerUsed = false;
300
308
 
301
309
  const ctxTrail = trail('ctx.check', {
302
- input: z.object({}),
303
- run: (_input, ctx) => {
310
+ blaze: (_input, ctx) => {
304
311
  contextUsed = ctx.extensions?.['custom'] === true;
312
+ trailheadMarkerUsed = ctx.extensions?.[TRAILHEAD_KEY] === 'mcp';
305
313
  return Result.ok({ ok: true });
306
314
  },
315
+ input: z.object({}),
307
316
  });
308
317
 
309
318
  const app = topo('myapp', { ctxTrail });
310
319
  const tool = requireOnlyTool(
311
320
  buildTools(app, {
312
321
  createContext: () => ({
322
+ abortSignal: new AbortController().signal,
313
323
  extensions: { custom: true },
314
324
  requestId: 'test-id',
315
- signal: new AbortController().signal,
316
325
  }),
317
326
  })
318
327
  );
319
328
 
320
329
  await tool.handler({}, noExtra);
321
330
  expect(contextUsed).toBe(true);
331
+ expect(trailheadMarkerUsed).toBe(true);
322
332
  });
323
333
 
324
- test('service overrides are forwarded to executeTrail', async () => {
325
- 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 }),
326
338
  input: z.object({}),
327
339
  output: z.object({ source: z.string() }),
328
- run: (_input, ctx) =>
329
- Result.ok({ source: dbService.from(ctx).source as string }),
330
- services: [dbService],
340
+ provisions: [dbProvision],
331
341
  });
332
342
 
333
343
  const tool = requireOnlyTool(
334
- buildTools(topo('myapp', { serviceTrail }), {
335
- services: { 'db.main': { source: 'override' } },
344
+ buildTools(topo('myapp', { provisionTrail }), {
345
+ provisions: { 'db.main': { source: 'override' } },
336
346
  })
337
347
  );
338
348
 
@@ -347,8 +357,7 @@ describe('buildMcpTools', () => {
347
357
  describe('blob outputs', () => {
348
358
  test('BlobRef output converts to image content', async () => {
349
359
  const blobTrail = trail('blob.image', {
350
- input: z.object({}),
351
- run: () =>
360
+ blaze: () =>
352
361
  Result.ok(
353
362
  createBlobRef({
354
363
  data: new Uint8Array([1, 2, 3]),
@@ -357,6 +366,7 @@ describe('buildMcpTools', () => {
357
366
  size: 3,
358
367
  })
359
368
  ),
369
+ input: z.object({}),
360
370
  });
361
371
 
362
372
  const tool = requireOnlyTool(buildTools(topo('myapp', { blobTrail })));
@@ -369,8 +379,7 @@ describe('buildMcpTools', () => {
369
379
 
370
380
  test('BlobRef output converts to resource content for non-images', async () => {
371
381
  const blobTrail = trail('blob.file', {
372
- input: z.object({}),
373
- run: () =>
382
+ blaze: () =>
374
383
  Result.ok(
375
384
  createBlobRef({
376
385
  data: new Uint8Array([1, 2, 3]),
@@ -379,6 +388,7 @@ describe('buildMcpTools', () => {
379
388
  size: 3,
380
389
  })
381
390
  ),
391
+ input: z.object({}),
382
392
  });
383
393
 
384
394
  const tool = requireOnlyTool(buildTools(topo('myapp', { blobTrail })));
@@ -398,8 +408,7 @@ describe('buildMcpTools', () => {
398
408
  },
399
409
  });
400
410
  const blobTrail = trail('blob.stream', {
401
- input: z.object({}),
402
- run: () =>
411
+ blaze: () =>
403
412
  Result.ok(
404
413
  createBlobRef({
405
414
  data: stream,
@@ -408,6 +417,7 @@ describe('buildMcpTools', () => {
408
417
  size: 3,
409
418
  })
410
419
  ),
420
+ input: z.object({}),
411
421
  });
412
422
 
413
423
  const tool = requireOnlyTool(buildTools(topo('myapp', { blobTrail })));
@@ -422,12 +432,12 @@ describe('buildMcpTools', () => {
422
432
  describe('tool-name collision detection', () => {
423
433
  test('returns Err on trails that produce the same derived tool name', () => {
424
434
  const dotTrail = trail('foo.bar', {
435
+ blaze: () => Result.ok({ ok: true }),
425
436
  input: z.object({}),
426
- run: () => Result.ok({ ok: true }),
427
437
  });
428
438
  const underscoreTrail = trail('foo_bar', {
439
+ blaze: () => Result.ok({ ok: true }),
429
440
  input: z.object({}),
430
- run: () => Result.ok({ ok: true }),
431
441
  });
432
442
 
433
443
  const app = topo('myapp', { dotTrail, underscoreTrail });
@@ -438,12 +448,12 @@ describe('buildMcpTools', () => {
438
448
 
439
449
  test('returns Err on trails where hyphen and underscore collide', () => {
440
450
  const hyphenTrail = trail('foo-bar', {
451
+ blaze: () => Result.ok({ ok: true }),
441
452
  input: z.object({}),
442
- run: () => Result.ok({ ok: true }),
443
453
  });
444
454
  const underscoreTrail = trail('foo_bar', {
455
+ blaze: () => Result.ok({ ok: true }),
445
456
  input: z.object({}),
446
- run: () => Result.ok({ ok: true }),
447
457
  });
448
458
 
449
459
  const app = topo('myapp', { hyphenTrail, underscoreTrail });
@@ -454,12 +464,12 @@ describe('buildMcpTools', () => {
454
464
 
455
465
  test('returns Ok when trail names are distinct after normalization', () => {
456
466
  const fooTrail = trail('foo', {
467
+ blaze: () => Result.ok({ ok: true }),
457
468
  input: z.object({}),
458
- run: () => Result.ok({ ok: true }),
459
469
  });
460
470
  const barTrail = trail('bar', {
471
+ blaze: () => Result.ok({ ok: true }),
461
472
  input: z.object({}),
462
- run: () => Result.ok({ ok: true }),
463
473
  });
464
474
 
465
475
  const app = topo('myapp', { barTrail, fooTrail });
@@ -471,12 +481,12 @@ describe('buildMcpTools', () => {
471
481
  describe('end-to-end', () => {
472
482
  test('full pipeline from trail to MCP response', async () => {
473
483
  const greetTrail = trail('greet', {
484
+ blaze: (input) => Result.ok({ greeting: `Hello, ${input.name}!` }),
474
485
  description: 'Greet someone',
475
486
  idempotent: true,
476
487
  input: z.object({ name: z.string() }),
477
488
  intent: 'read',
478
489
  output: z.object({ greeting: z.string() }),
479
- run: (input) => Result.ok({ greeting: `Hello, ${input.name}!` }),
480
490
  });
481
491
 
482
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 });