@ontrails/mcp 1.0.0-beta.13 → 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,13 +11,16 @@ import {
11
11
  TRAILHEAD_KEY,
12
12
  ValidationError,
13
13
  executeTrail,
14
+ filterSurfaceTrails,
14
15
  isBlobRef,
16
+ validateEstablishedTopo,
15
17
  zodToJsonSchema,
16
18
  } from '@ontrails/core';
17
19
  import type {
18
20
  BlobRef,
19
- Gate,
20
- ProvisionOverrideMap,
21
+ Intent,
22
+ Layer,
23
+ ResourceOverrideMap,
21
24
  Topo,
22
25
  Trail,
23
26
  TrailContextInit,
@@ -32,18 +35,21 @@ import { deriveToolName } from './tool-name.js';
32
35
  // Public types
33
36
  // ---------------------------------------------------------------------------
34
37
 
35
- export interface BuildMcpToolsOptions {
36
- /** 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. */
37
40
  readonly configValues?:
38
41
  | Readonly<Record<string, Record<string, unknown>>>
39
42
  | undefined;
40
43
  readonly createContext?:
41
44
  | (() => TrailContextInit | Promise<TrailContextInit>)
42
45
  | undefined;
43
- readonly excludeTrails?: readonly string[] | undefined;
44
- readonly includeTrails?: readonly string[] | undefined;
45
- readonly gates?: readonly Gate[] | undefined;
46
- 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;
51
+ /** Set to `false` to skip topo validation while building tools. */
52
+ readonly validate?: boolean | undefined;
47
53
  }
48
54
 
49
55
  export interface McpToolDefinition {
@@ -224,9 +230,10 @@ const withMcpTrailhead = (
224
230
 
225
231
  const createHandler =
226
232
  (
227
- t: Trail<unknown, unknown>,
228
- gates: readonly Gate[],
229
- options: BuildMcpToolsOptions
233
+ graph: Topo,
234
+ t: Trail<unknown, unknown, unknown>,
235
+ layers: readonly Layer[],
236
+ options: DeriveMcpToolsOptions
230
237
  ): ((
231
238
  args: Record<string, unknown>,
232
239
  extra: McpExtra
@@ -238,8 +245,9 @@ const createHandler =
238
245
  configValues: options.configValues,
239
246
  createContext: options.createContext,
240
247
  ctx: withMcpTrailhead(progressCb),
241
- gates,
242
- provisions: options.provisions,
248
+ layers,
249
+ resources: options.resources,
250
+ topo: graph,
243
251
  });
244
252
  if (result.isOk()) {
245
253
  return { content: await serializeOutput(result.value) };
@@ -252,37 +260,18 @@ const createHandler =
252
260
  // ---------------------------------------------------------------------------
253
261
 
254
262
  /**
255
- * Build MCP tool definitions from an App's topology.
263
+ * Build MCP tool definitions from a graph's topology.
256
264
  *
257
265
  * Each trail in the topo becomes an McpToolDefinition with:
258
- * - A derived tool name (app-prefixed, underscore-delimited)
266
+ * - A derived tool name (topo-name-prefixed, underscore-delimited)
259
267
  * - JSON Schema input from zodToJsonSchema
260
268
  * - MCP annotations from trail meta
261
- * - A handler that validates, composes gates, executes, and maps results
269
+ * - A handler that validates, composes layers, executes, and maps results
262
270
  */
263
- /** Check if a trail should be included based on meta and filters. */
264
- const shouldInclude = (
265
- trail: Trail<unknown, unknown>,
266
- options: BuildMcpToolsOptions
267
- ): boolean => {
268
- if (trail.meta?.['internal'] === true) {
269
- return false;
270
- }
271
- if (options.includeTrails !== undefined && options.includeTrails.length > 0) {
272
- return options.includeTrails.includes(trail.id);
273
- }
274
- if (
275
- options.excludeTrails !== undefined &&
276
- options.excludeTrails.includes(trail.id)
277
- ) {
278
- return false;
279
- }
280
- return true;
281
- };
282
271
 
283
272
  /** Build a description with optional example input appended. */
284
273
  const buildDescription = (
285
- trail: Trail<unknown, unknown>
274
+ trail: Trail<unknown, unknown, unknown>
286
275
  ): string | undefined => {
287
276
  let { description } = trail;
288
277
  if (
@@ -300,10 +289,10 @@ const buildDescription = (
300
289
 
301
290
  /** Build a single MCP tool definition from a trail. */
302
291
  const buildToolDefinition = (
303
- app: Topo,
304
- trail: Trail<unknown, unknown>,
305
- gates: readonly Gate[],
306
- options: BuildMcpToolsOptions
292
+ graph: Topo,
293
+ trail: Trail<unknown, unknown, unknown>,
294
+ layers: readonly Layer[],
295
+ options: DeriveMcpToolsOptions
307
296
  ): McpToolDefinition => {
308
297
  const rawAnnotations = deriveAnnotations(trail);
309
298
  const annotations =
@@ -311,23 +300,23 @@ const buildToolDefinition = (
311
300
  return {
312
301
  annotations,
313
302
  description: buildDescription(trail),
314
- handler: createHandler(trail, gates, options),
303
+ handler: createHandler(graph, trail, layers, options),
315
304
  inputSchema: zodToJsonSchema(trail.input),
316
- name: deriveToolName(app.name, trail.id),
305
+ name: deriveToolName(graph.name, trail.id),
317
306
  trailId: trail.id,
318
307
  };
319
308
  };
320
309
 
321
310
  /** Register a trail as an MCP tool, checking for name collisions. */
322
311
  const registerTool = (
323
- app: Topo,
324
- trailItem: Trail<unknown, unknown>,
325
- gates: readonly Gate[],
326
- options: BuildMcpToolsOptions,
312
+ graph: Topo,
313
+ trailItem: Trail<unknown, unknown, unknown>,
314
+ layers: readonly Layer[],
315
+ options: DeriveMcpToolsOptions,
327
316
  nameToTrailId: Map<string, string>,
328
317
  tools: McpToolDefinition[]
329
318
  ): Result<void, Error> => {
330
- const toolName = deriveToolName(app.name, trailItem.id);
319
+ const toolName = deriveToolName(graph.name, trailItem.id);
331
320
  const existingId = nameToTrailId.get(toolName);
332
321
  if (existingId !== undefined) {
333
322
  return Result.err(
@@ -337,30 +326,46 @@ const registerTool = (
337
326
  );
338
327
  }
339
328
  nameToTrailId.set(toolName, trailItem.id);
340
- tools.push(buildToolDefinition(app, trailItem, gates, options));
329
+ tools.push(buildToolDefinition(graph, trailItem, layers, options));
341
330
  return Result.ok();
342
331
  };
343
332
 
344
333
  /** Filter topo items to eligible trails. */
345
334
  const eligibleTrails = (
346
- app: Topo,
347
- options: BuildMcpToolsOptions
348
- ): Trail<unknown, unknown>[] =>
349
- app.list().filter((trail) => shouldInclude(trail, options));
350
-
351
- export const buildMcpTools = (
352
- app: Topo,
353
- options: BuildMcpToolsOptions = {}
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
+ });
343
+
344
+ const validateToolBuild = (
345
+ graph: Topo,
346
+ options: DeriveMcpToolsOptions
347
+ ): Result<void, Error> => {
348
+ if (options.validate === false) {
349
+ return Result.ok();
350
+ }
351
+
352
+ const validated = validateEstablishedTopo(graph);
353
+ return validated.isErr() ? Result.err(validated.error) : Result.ok();
354
+ };
355
+
356
+ const registerTools = (
357
+ graph: Topo,
358
+ options: DeriveMcpToolsOptions,
359
+ layers: readonly Layer[]
354
360
  ): Result<McpToolDefinition[], Error> => {
355
- const gates = options.gates ?? [];
356
361
  const tools: McpToolDefinition[] = [];
357
362
  const nameToTrailId = new Map<string, string>();
358
363
 
359
- for (const trailItem of eligibleTrails(app, options)) {
364
+ for (const trailItem of eligibleTrails(graph, options)) {
360
365
  const registered = registerTool(
361
- app,
366
+ graph,
362
367
  trailItem,
363
- gates,
368
+ layers,
364
369
  options,
365
370
  nameToTrailId,
366
371
  tools
@@ -372,3 +377,15 @@ export const buildMcpTools = (
372
377
 
373
378
  return Result.ok(tools);
374
379
  };
380
+
381
+ export const deriveMcpTools = (
382
+ graph: Topo,
383
+ options: DeriveMcpToolsOptions = {}
384
+ ): Result<McpToolDefinition[], Error> => {
385
+ const validation = validateToolBuild(graph, options);
386
+ if (validation.isErr()) {
387
+ return validation;
388
+ }
389
+
390
+ return registerTools(graph, options, options.layers ?? []);
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 { validateTopo } 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,40 +140,58 @@ 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 = validateTopo(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,
161
+ validate: options.validate,
160
162
  });
161
163
 
162
164
  if (toolsResult.isErr()) {
163
165
  throw toolsResult.error;
164
166
  }
165
167
 
166
- const server = createMcpServer(toolsResult.value, {
167
- name: options.serverInfo?.name ?? app.name,
168
- 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',
169
172
  });
173
+ };
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // surface
177
+ // ---------------------------------------------------------------------------
170
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);
171
190
  await connectStdio(server);
191
+
192
+ return {
193
+ close: async () => {
194
+ await server.close();
195
+ },
196
+ };
172
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"}