@ontrails/mcp 1.0.0-beta.14 → 1.0.0-beta.15

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.
@@ -3,14 +3,22 @@ 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 { trailhead, createMcpServer } from '../trailhead.js';
7
- import { buildMcpTools } from '../build.js';
6
+ import { createServer, surface } from '../surface.js';
7
+ import { deriveMcpTools } from '../build.js';
8
8
  import type { McpToolDefinition } from '../build.js';
9
9
 
10
10
  // ---------------------------------------------------------------------------
11
11
  // Tests
12
12
  // ---------------------------------------------------------------------------
13
13
 
14
+ const unwrapOk = <T>(result: Result<T, Error>): T =>
15
+ result.match({
16
+ err: (error) => {
17
+ throw error;
18
+ },
19
+ ok: (value) => value,
20
+ });
21
+
14
22
  const requireTool = (tools: McpToolDefinition[], name: string) => {
15
23
  const tool = tools.find((entry) => entry.name === name);
16
24
  expect(tool).toBeDefined();
@@ -21,19 +29,19 @@ const requireTool = (tools: McpToolDefinition[], name: string) => {
21
29
  };
22
30
 
23
31
  /**
24
- * Unwrap buildMcpTools result, throwing on error so test failures show up clearly.
32
+ * Unwrap deriveMcpTools result, throwing on error so test failures show up clearly.
25
33
  */
26
- const buildTools = (
27
- ...args: Parameters<typeof buildMcpTools>
34
+ const deriveTools = (
35
+ ...args: Parameters<typeof deriveMcpTools>
28
36
  ): McpToolDefinition[] => {
29
- const result = buildMcpTools(...args);
37
+ const result = deriveMcpTools(...args);
30
38
  if (result.isErr()) {
31
39
  throw result.error;
32
40
  }
33
41
  return result.value;
34
42
  };
35
43
 
36
- const createIntegrationTools = () => {
44
+ const createIntegrationFixtures = () => {
37
45
  const greetTrail = trail('greet', {
38
46
  blaze: (input) => Result.ok({ greeting: `Hello, ${input.name}!` }),
39
47
  description: 'Greet someone',
@@ -48,11 +56,12 @@ const createIntegrationTools = () => {
48
56
  intent: 'destroy',
49
57
  });
50
58
 
51
- return buildTools(topo('myapp', { deleteTrail, greetTrail }));
59
+ const app = topo('myapp', { deleteTrail, greetTrail });
60
+ return { app, tools: deriveTools(app) };
52
61
  };
53
62
 
54
- describe('trailhead', () => {
55
- test('trailhead throws on invalid topo', async () => {
63
+ describe('surface', () => {
64
+ test('surface throws on invalid topo', async () => {
56
65
  const t = trail('broken', {
57
66
  blaze: () => Result.ok({}),
58
67
  crosses: ['nonexistent.trail'],
@@ -60,10 +69,10 @@ describe('trailhead', () => {
60
69
  output: z.object({}),
61
70
  });
62
71
  const app = topo('test', { t });
63
- await expect(trailhead(app)).rejects.toThrow(/validation/i);
72
+ await expect(surface(app)).rejects.toThrow(/validation/i);
64
73
  });
65
74
 
66
- test('trailhead skips validation when validate: false', async () => {
75
+ test('surface skips validation when validate: false', async () => {
67
76
  const t = trail('broken', {
68
77
  blaze: () => Result.ok({}),
69
78
  crosses: ['nonexistent.trail'],
@@ -72,7 +81,7 @@ describe('trailhead', () => {
72
81
  });
73
82
  const app = topo('test', { t });
74
83
  const result = await Promise.race([
75
- trailhead(app, { validate: false }).then(() => 'resolved' as const),
84
+ surface(app, { validate: false }).then(() => 'resolved' as const),
76
85
  // oxlint-disable-next-line avoid-new -- Promise constructor needed for setTimeout-based timeout
77
86
  new Promise<'timeout'>((resolve) => {
78
87
  setTimeout(() => {
@@ -83,15 +92,22 @@ describe('trailhead', () => {
83
92
  expect(['resolved', 'timeout']).toContain(result);
84
93
  });
85
94
 
86
- test('TrailheadMcpOptions accepts provision overrides', () => {
87
- const opts: Parameters<typeof trailhead>[1] = {
88
- provisions: {},
95
+ test('CreateServerOptions accepts flattened identity and resource fields', () => {
96
+ const opts: Parameters<typeof surface>[1] = {
97
+ description: 'Test MCP server',
98
+ name: 'testapp',
99
+ resources: {},
89
100
  validate: false,
101
+ version: '1.2.3',
90
102
  };
91
- expect(opts.provisions).toEqual({});
103
+ expect(opts.description).toBe('Test MCP server');
104
+ expect(opts.name).toBe('testapp');
105
+ expect(opts.resources).toEqual({});
106
+ expect(opts.validate).toBe(false);
107
+ expect(opts.version).toBe('1.2.3');
92
108
  });
93
109
 
94
- test('createMcpServer registers tools that can be listed', () => {
110
+ test('createServer registers tools that can be listed', () => {
95
111
  const echoTrail = trail('echo', {
96
112
  blaze: (input) => Result.ok({ reply: input.message }),
97
113
  description: 'Echo',
@@ -100,8 +116,7 @@ describe('trailhead', () => {
100
116
  });
101
117
 
102
118
  const app = topo('testapp', { echoTrail });
103
- const tools = buildTools(app);
104
- const server = createMcpServer(tools, {
119
+ const server = createServer(app, {
105
120
  name: 'testapp',
106
121
  version: '0.1.0',
107
122
  });
@@ -110,7 +125,7 @@ describe('trailhead', () => {
110
125
  expect(server).toBeDefined();
111
126
  });
112
127
 
113
- test('createMcpServer handles multiple tools', () => {
128
+ test('createServer handles multiple tools', () => {
114
129
  const echoTrail = trail('echo', {
115
130
  blaze: (input) => Result.ok({ reply: input.message }),
116
131
  description: 'Echo',
@@ -125,19 +140,37 @@ describe('trailhead', () => {
125
140
  });
126
141
 
127
142
  const app = topo('testapp', { echoTrail, searchTrail });
128
- const tools = buildTools(app);
143
+ const tools = deriveTools(app);
129
144
 
130
145
  expect(tools).toHaveLength(2);
131
146
 
132
- const server = createMcpServer(tools, {
147
+ const server = createServer(app, {
133
148
  name: 'testapp',
134
149
  version: '0.1.0',
135
150
  });
136
151
  expect(server).toBeDefined();
137
152
  });
138
153
 
139
- test('buildMcpTools + createMcpServer integration', () => {
140
- const tools = createIntegrationTools();
154
+ test('deriveMcpTools returns tools and createServer materializes the server', () => {
155
+ const echoTrail = trail('echo', {
156
+ blaze: (input) => Result.ok({ reply: input.message }),
157
+ description: 'Echo',
158
+ input: z.object({ message: z.string() }),
159
+ });
160
+ const app = topo('surface-api', { echoTrail });
161
+
162
+ const tools = unwrapOk(deriveMcpTools(app));
163
+ expect(tools).toHaveLength(1);
164
+
165
+ const server = createServer(app, {
166
+ description: 'Surface API smoke',
167
+ version: '2.0.0',
168
+ });
169
+ expect(server).toBeDefined();
170
+ });
171
+
172
+ test('deriveMcpTools + createServer integration', () => {
173
+ const { app, tools } = createIntegrationFixtures();
141
174
  const names = tools.map((t) => t.name);
142
175
  expect(names).toContain('myapp_greet');
143
176
  expect(names).toContain('myapp_item_delete');
@@ -149,7 +182,7 @@ describe('trailhead', () => {
149
182
  requireTool(tools, 'myapp_item_delete').annotations?.destructiveHint
150
183
  ).toBe(true);
151
184
 
152
- const server = createMcpServer(tools, {
185
+ const server = createServer(app, {
153
186
  name: 'myapp',
154
187
  version: '1.0.0',
155
188
  });
@@ -27,7 +27,10 @@ export interface McpAnnotations {
27
27
  * Omitted hints let the MCP SDK use its defaults.
28
28
  */
29
29
  export const deriveAnnotations = (
30
- trail: Pick<Trail<unknown, unknown>, 'intent' | 'idempotent' | 'description'>
30
+ trail: Pick<
31
+ Trail<unknown, unknown, unknown>,
32
+ 'intent' | 'idempotent' | 'description'
33
+ >
31
34
  ): McpAnnotations => {
32
35
  const annotations: Record<string, unknown> = {};
33
36
 
package/src/build.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Build MCP tool definitions from a Trails App.
2
+ * Build MCP tool definitions from a Trails graph.
3
3
  *
4
4
  * Iterates the topo, generates McpToolDefinition[] with handlers that
5
- * validate input, compose gates, execute the implementation, and map
5
+ * validate input, compose layers, execute the implementation, and map
6
6
  * Results to MCP responses.
7
7
  */
8
8
 
@@ -11,14 +11,16 @@ import {
11
11
  TRAILHEAD_KEY,
12
12
  ValidationError,
13
13
  executeTrail,
14
+ filterSurfaceTrails,
14
15
  isBlobRef,
15
16
  validateEstablishedTopo,
16
17
  zodToJsonSchema,
17
18
  } from '@ontrails/core';
18
19
  import type {
19
20
  BlobRef,
20
- Gate,
21
- ProvisionOverrideMap,
21
+ Intent,
22
+ Layer,
23
+ ResourceOverrideMap,
22
24
  Topo,
23
25
  Trail,
24
26
  TrailContextInit,
@@ -33,18 +35,19 @@ import { deriveToolName } from './tool-name.js';
33
35
  // Public types
34
36
  // ---------------------------------------------------------------------------
35
37
 
36
- export interface BuildMcpToolsOptions {
37
- /** Config values for provisions that declare a `config` schema, keyed by provision ID. */
38
+ export interface DeriveMcpToolsOptions {
39
+ /** Config values for resources that declare a `config` schema, keyed by resource ID. */
38
40
  readonly configValues?:
39
41
  | Readonly<Record<string, Record<string, unknown>>>
40
42
  | undefined;
41
43
  readonly createContext?:
42
44
  | (() => TrailContextInit | Promise<TrailContextInit>)
43
45
  | undefined;
44
- readonly excludeTrails?: readonly string[] | undefined;
45
- readonly includeTrails?: readonly string[] | undefined;
46
- readonly gates?: readonly Gate[] | undefined;
47
- readonly provisions?: ProvisionOverrideMap | undefined;
46
+ readonly exclude?: readonly string[] | undefined;
47
+ readonly include?: readonly string[] | undefined;
48
+ readonly intent?: readonly Intent[] | undefined;
49
+ readonly layers?: readonly Layer[] | undefined;
50
+ readonly resources?: ResourceOverrideMap | undefined;
48
51
  /** Set to `false` to skip topo validation while building tools. */
49
52
  readonly validate?: boolean | undefined;
50
53
  }
@@ -227,9 +230,10 @@ const withMcpTrailhead = (
227
230
 
228
231
  const createHandler =
229
232
  (
230
- t: Trail<unknown, unknown>,
231
- gates: readonly Gate[],
232
- options: BuildMcpToolsOptions
233
+ graph: Topo,
234
+ t: Trail<unknown, unknown, unknown>,
235
+ layers: readonly Layer[],
236
+ options: DeriveMcpToolsOptions
233
237
  ): ((
234
238
  args: Record<string, unknown>,
235
239
  extra: McpExtra
@@ -241,8 +245,9 @@ const createHandler =
241
245
  configValues: options.configValues,
242
246
  createContext: options.createContext,
243
247
  ctx: withMcpTrailhead(progressCb),
244
- gates,
245
- provisions: options.provisions,
248
+ layers,
249
+ resources: options.resources,
250
+ topo: graph,
246
251
  });
247
252
  if (result.isOk()) {
248
253
  return { content: await serializeOutput(result.value) };
@@ -255,37 +260,18 @@ const createHandler =
255
260
  // ---------------------------------------------------------------------------
256
261
 
257
262
  /**
258
- * Build MCP tool definitions from an App's topology.
263
+ * Build MCP tool definitions from a graph's topology.
259
264
  *
260
265
  * Each trail in the topo becomes an McpToolDefinition with:
261
- * - A derived tool name (app-prefixed, underscore-delimited)
266
+ * - A derived tool name (topo-name-prefixed, underscore-delimited)
262
267
  * - JSON Schema input from zodToJsonSchema
263
268
  * - MCP annotations from trail meta
264
- * - A handler that validates, composes gates, executes, and maps results
269
+ * - A handler that validates, composes layers, executes, and maps results
265
270
  */
266
- /** Check if a trail should be included based on meta and filters. */
267
- const shouldInclude = (
268
- trail: Trail<unknown, unknown>,
269
- options: BuildMcpToolsOptions
270
- ): boolean => {
271
- if (trail.meta?.['internal'] === true) {
272
- return false;
273
- }
274
- if (options.includeTrails !== undefined && options.includeTrails.length > 0) {
275
- return options.includeTrails.includes(trail.id);
276
- }
277
- if (
278
- options.excludeTrails !== undefined &&
279
- options.excludeTrails.includes(trail.id)
280
- ) {
281
- return false;
282
- }
283
- return true;
284
- };
285
271
 
286
272
  /** Build a description with optional example input appended. */
287
273
  const buildDescription = (
288
- trail: Trail<unknown, unknown>
274
+ trail: Trail<unknown, unknown, unknown>
289
275
  ): string | undefined => {
290
276
  let { description } = trail;
291
277
  if (
@@ -303,10 +289,10 @@ const buildDescription = (
303
289
 
304
290
  /** Build a single MCP tool definition from a trail. */
305
291
  const buildToolDefinition = (
306
- app: Topo,
307
- trail: Trail<unknown, unknown>,
308
- gates: readonly Gate[],
309
- options: BuildMcpToolsOptions
292
+ graph: Topo,
293
+ trail: Trail<unknown, unknown, unknown>,
294
+ layers: readonly Layer[],
295
+ options: DeriveMcpToolsOptions
310
296
  ): McpToolDefinition => {
311
297
  const rawAnnotations = deriveAnnotations(trail);
312
298
  const annotations =
@@ -314,23 +300,23 @@ const buildToolDefinition = (
314
300
  return {
315
301
  annotations,
316
302
  description: buildDescription(trail),
317
- handler: createHandler(trail, gates, options),
303
+ handler: createHandler(graph, trail, layers, options),
318
304
  inputSchema: zodToJsonSchema(trail.input),
319
- name: deriveToolName(app.name, trail.id),
305
+ name: deriveToolName(graph.name, trail.id),
320
306
  trailId: trail.id,
321
307
  };
322
308
  };
323
309
 
324
310
  /** Register a trail as an MCP tool, checking for name collisions. */
325
311
  const registerTool = (
326
- app: Topo,
327
- trailItem: Trail<unknown, unknown>,
328
- gates: readonly Gate[],
329
- options: BuildMcpToolsOptions,
312
+ graph: Topo,
313
+ trailItem: Trail<unknown, unknown, unknown>,
314
+ layers: readonly Layer[],
315
+ options: DeriveMcpToolsOptions,
330
316
  nameToTrailId: Map<string, string>,
331
317
  tools: McpToolDefinition[]
332
318
  ): Result<void, Error> => {
333
- const toolName = deriveToolName(app.name, trailItem.id);
319
+ const toolName = deriveToolName(graph.name, trailItem.id);
334
320
  const existingId = nameToTrailId.get(toolName);
335
321
  if (existingId !== undefined) {
336
322
  return Result.err(
@@ -340,42 +326,46 @@ const registerTool = (
340
326
  );
341
327
  }
342
328
  nameToTrailId.set(toolName, trailItem.id);
343
- tools.push(buildToolDefinition(app, trailItem, gates, options));
329
+ tools.push(buildToolDefinition(graph, trailItem, layers, options));
344
330
  return Result.ok();
345
331
  };
346
332
 
347
333
  /** Filter topo items to eligible trails. */
348
334
  const eligibleTrails = (
349
- app: Topo,
350
- options: BuildMcpToolsOptions
351
- ): Trail<unknown, unknown>[] =>
352
- app.list().filter((trail) => shouldInclude(trail, options));
335
+ graph: Topo,
336
+ options: DeriveMcpToolsOptions
337
+ ): Trail<unknown, unknown, unknown>[] =>
338
+ filterSurfaceTrails(graph.list(), {
339
+ exclude: options.exclude,
340
+ include: options.include,
341
+ intent: options.intent,
342
+ });
353
343
 
354
344
  const validateToolBuild = (
355
- app: Topo,
356
- options: BuildMcpToolsOptions
345
+ graph: Topo,
346
+ options: DeriveMcpToolsOptions
357
347
  ): Result<void, Error> => {
358
348
  if (options.validate === false) {
359
349
  return Result.ok();
360
350
  }
361
351
 
362
- const validated = validateEstablishedTopo(app);
352
+ const validated = validateEstablishedTopo(graph);
363
353
  return validated.isErr() ? Result.err(validated.error) : Result.ok();
364
354
  };
365
355
 
366
356
  const registerTools = (
367
- app: Topo,
368
- options: BuildMcpToolsOptions,
369
- gates: readonly Gate[]
357
+ graph: Topo,
358
+ options: DeriveMcpToolsOptions,
359
+ layers: readonly Layer[]
370
360
  ): Result<McpToolDefinition[], Error> => {
371
361
  const tools: McpToolDefinition[] = [];
372
362
  const nameToTrailId = new Map<string, string>();
373
363
 
374
- for (const trailItem of eligibleTrails(app, options)) {
364
+ for (const trailItem of eligibleTrails(graph, options)) {
375
365
  const registered = registerTool(
376
- app,
366
+ graph,
377
367
  trailItem,
378
- gates,
368
+ layers,
379
369
  options,
380
370
  nameToTrailId,
381
371
  tools
@@ -388,14 +378,14 @@ const registerTools = (
388
378
  return Result.ok(tools);
389
379
  };
390
380
 
391
- export const buildMcpTools = (
392
- app: Topo,
393
- options: BuildMcpToolsOptions = {}
381
+ export const deriveMcpTools = (
382
+ graph: Topo,
383
+ options: DeriveMcpToolsOptions = {}
394
384
  ): Result<McpToolDefinition[], Error> => {
395
- const validation = validateToolBuild(app, options);
385
+ const validation = validateToolBuild(graph, options);
396
386
  if (validation.isErr()) {
397
387
  return validation;
398
388
  }
399
389
 
400
- return registerTools(app, options, options.gates ?? []);
390
+ return registerTools(graph, options, options.layers ?? []);
401
391
  };
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // Build
2
2
  export {
3
- buildMcpTools,
4
- type BuildMcpToolsOptions,
3
+ deriveMcpTools,
4
+ type DeriveMcpToolsOptions,
5
5
  type McpToolDefinition,
6
6
  type McpToolResult,
7
7
  type McpContent,
@@ -17,8 +17,13 @@ export { deriveAnnotations, type McpAnnotations } from './annotations.js';
17
17
  // Progress
18
18
  export { createMcpProgressCallback } from './progress.js';
19
19
 
20
- // Trailhead
21
- export { trailhead, type TrailheadMcpOptions } from './trailhead.js';
20
+ // Surface
21
+ export {
22
+ createServer,
23
+ surface,
24
+ type CreateServerOptions,
25
+ type SurfaceMcpResult,
26
+ } from './surface.js';
22
27
 
23
28
  // Transport
24
29
  export { connectStdio } from './stdio.js';
package/src/stdio.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Thin wrapper around MCP SDK's StdioServerTransport.
3
3
  *
4
4
  * Exists as a separate function so it can be swapped for other transports
5
- * (SSE, streamable HTTP) without changing trailhead().
5
+ * (SSE, streamable HTTP) without changing surface().
6
6
  */
7
7
 
8
8
  import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
@@ -1,12 +1,5 @@
1
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
- * ```
2
+ * Surface helpers for exposing a topo over MCP.
10
3
  */
11
4
 
12
5
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
@@ -15,42 +8,43 @@ import {
15
8
  ListToolsRequestSchema,
16
9
  } from '@modelcontextprotocol/sdk/types.js';
17
10
  import type {
18
- Gate,
19
- ProvisionOverrideMap,
11
+ Intent,
12
+ Layer,
13
+ ResourceOverrideMap,
20
14
  Topo,
21
15
  TrailContextInit,
22
16
  } from '@ontrails/core';
23
- import { validateEstablishedTopo } from '@ontrails/core';
24
17
 
25
18
  import type { McpToolDefinition } from './build.js';
26
- import { buildMcpTools } from './build.js';
19
+ import { deriveMcpTools } from './build.js';
27
20
  import { connectStdio } from './stdio.js';
28
21
 
29
22
  // ---------------------------------------------------------------------------
30
23
  // Options
31
24
  // ---------------------------------------------------------------------------
32
25
 
33
- export interface TrailheadMcpOptions {
34
- /** Config values for provisions that declare a `config` schema, keyed by provision ID. */
26
+ export interface CreateServerOptions {
27
+ /** Config values for resources that declare a `config` schema, keyed by resource ID. */
35
28
  readonly configValues?:
36
29
  | Readonly<Record<string, Record<string, unknown>>>
37
30
  | undefined;
38
31
  readonly createContext?:
39
32
  | (() => TrailContextInit | Promise<TrailContextInit>)
40
33
  | undefined;
41
- readonly excludeTrails?: readonly string[] | undefined;
42
- readonly includeTrails?: readonly string[] | undefined;
43
- readonly gates?: readonly Gate[] | undefined;
44
- readonly provisions?: ProvisionOverrideMap | undefined;
45
- readonly serverInfo?:
46
- | {
47
- readonly name?: string | undefined;
48
- readonly version?: string | undefined;
49
- }
50
- | undefined;
51
- readonly transport?: 'stdio' | undefined;
34
+ readonly description?: string | undefined;
35
+ readonly exclude?: readonly string[] | undefined;
36
+ readonly include?: readonly string[] | undefined;
37
+ readonly intent?: readonly Intent[] | undefined;
38
+ readonly layers?: readonly Layer[] | undefined;
39
+ readonly name?: string | undefined;
40
+ readonly resources?: ResourceOverrideMap | undefined;
52
41
  /** Set to `false` to skip topo validation at startup. Defaults to `true`. */
53
42
  readonly validate?: boolean | undefined;
43
+ readonly version?: string | undefined;
44
+ }
45
+
46
+ export interface SurfaceMcpResult {
47
+ readonly close: () => Promise<void>;
54
48
  }
55
49
 
56
50
  // ---------------------------------------------------------------------------
@@ -59,14 +53,27 @@ export interface TrailheadMcpOptions {
59
53
 
60
54
  /**
61
55
  * Create an MCP Server instance and register all tools.
56
+ *
57
+ * When provided, `info.description` is forwarded to the MCP SDK as the
58
+ * server's `instructions` field — the SDK's documented channel for
59
+ * "optional instructions describing how to use the server and its features."
62
60
  */
63
- export const createMcpServer = (
61
+ const createMcpServer = (
64
62
  tools: McpToolDefinition[],
65
- info: { readonly name: string; readonly version: string }
63
+ info: {
64
+ readonly name: string;
65
+ readonly version: string;
66
+ readonly description?: string | undefined;
67
+ }
66
68
  ): Server => {
67
69
  const server = new Server(
68
70
  { name: info.name, version: info.version },
69
- { capabilities: { tools: {} } }
71
+ {
72
+ capabilities: { tools: {} },
73
+ ...(info.description === undefined
74
+ ? {}
75
+ : { instructions: info.description }),
76
+ }
70
77
  );
71
78
 
72
79
  // Build a lookup map for tool dispatch
@@ -133,30 +140,24 @@ export const createMcpServer = (
133
140
  };
134
141
 
135
142
  // ---------------------------------------------------------------------------
136
- // trailhead
143
+ // createServer
137
144
  // ---------------------------------------------------------------------------
138
145
 
139
146
  /**
140
- * Build MCP tools from an App, create a server, and connect via stdio.
147
+ * Build MCP tools from a topo and create an MCP server.
141
148
  */
142
- export const trailhead = async (
143
- app: Topo,
144
- options: TrailheadMcpOptions = {}
145
- ): Promise<void> => {
146
- if (options.validate !== false) {
147
- const validated = validateEstablishedTopo(app);
148
- if (validated.isErr()) {
149
- throw validated.error;
150
- }
151
- }
152
-
153
- const toolsResult = buildMcpTools(app, {
149
+ export const createServer = (
150
+ graph: Topo,
151
+ options: CreateServerOptions = {}
152
+ ): Server => {
153
+ const toolsResult = deriveMcpTools(graph, {
154
154
  configValues: options.configValues,
155
155
  createContext: options.createContext,
156
- excludeTrails: options.excludeTrails,
157
- gates: options.gates,
158
- includeTrails: options.includeTrails,
159
- provisions: options.provisions,
156
+ exclude: options.exclude,
157
+ include: options.include,
158
+ intent: options.intent,
159
+ layers: options.layers,
160
+ resources: options.resources,
160
161
  validate: options.validate,
161
162
  });
162
163
 
@@ -164,10 +165,33 @@ export const trailhead = async (
164
165
  throw toolsResult.error;
165
166
  }
166
167
 
167
- const server = createMcpServer(toolsResult.value, {
168
- name: options.serverInfo?.name ?? app.name,
169
- version: options.serverInfo?.version ?? '0.1.0',
168
+ return createMcpServer(toolsResult.value, {
169
+ description: options.description ?? graph.description,
170
+ name: options.name ?? graph.name,
171
+ version: options.version ?? graph.version ?? '0.1.0',
170
172
  });
173
+ };
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // surface
177
+ // ---------------------------------------------------------------------------
171
178
 
179
+ /**
180
+ * Build MCP tools from a topo, create a server, and connect via stdio.
181
+ *
182
+ * @remarks Opens the MCP server on stdio. For custom transports, use
183
+ * `createServer(graph)` with `connectStdio` or your own connector.
184
+ */
185
+ export const surface = async (
186
+ graph: Topo,
187
+ options: CreateServerOptions = {}
188
+ ): Promise<SurfaceMcpResult> => {
189
+ const server = createServer(graph, options);
172
190
  await connectStdio(server);
191
+
192
+ return {
193
+ close: async () => {
194
+ await server.close();
195
+ },
196
+ };
173
197
  };
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "rootDir": "./src",
6
+ "types": ["bun"]
7
+ },
8
+ "include": ["src/**/*.test.ts", "src/__tests__/**/*.ts"],
9
+ "exclude": []
10
+ }
@@ -1 +1 @@
1
- {"root":["./src/annotations.ts","./src/build.ts","./src/index.ts","./src/progress.ts","./src/stdio.ts","./src/tool-name.ts","./src/trailhead.ts"],"version":"5.9.3"}
1
+ {"root":["./src/annotations.ts","./src/build.ts","./src/index.ts","./src/progress.ts","./src/stdio.ts","./src/surface.ts","./src/tool-name.ts"],"version":"5.9.3"}