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

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +36 -17
  3. package/package.json +11 -3
  4. package/src/annotations.ts +4 -1
  5. package/src/build.ts +689 -127
  6. package/src/index.ts +14 -4
  7. package/src/stdio.ts +1 -1
  8. package/src/surface.ts +215 -0
  9. package/.turbo/turbo-build.log +0 -1
  10. package/.turbo/turbo-lint.log +0 -3
  11. package/.turbo/turbo-typecheck.log +0 -1
  12. package/dist/annotations.d.ts +0 -19
  13. package/dist/annotations.d.ts.map +0 -1
  14. package/dist/annotations.js +0 -31
  15. package/dist/annotations.js.map +0 -1
  16. package/dist/blaze.d.ts +0 -41
  17. package/dist/blaze.d.ts.map +0 -1
  18. package/dist/blaze.js +0 -108
  19. package/dist/blaze.js.map +0 -1
  20. package/dist/build.d.ts +0 -48
  21. package/dist/build.d.ts.map +0 -1
  22. package/dist/build.js +0 -229
  23. package/dist/build.js.map +0 -1
  24. package/dist/index.d.ts +0 -7
  25. package/dist/index.d.ts.map +0 -1
  26. package/dist/index.js +0 -13
  27. package/dist/index.js.map +0 -1
  28. package/dist/progress.d.ts +0 -13
  29. package/dist/progress.d.ts.map +0 -1
  30. package/dist/progress.js +0 -51
  31. package/dist/progress.js.map +0 -1
  32. package/dist/stdio.d.ts +0 -12
  33. package/dist/stdio.d.ts.map +0 -1
  34. package/dist/stdio.js +0 -15
  35. package/dist/stdio.js.map +0 -1
  36. package/dist/tool-name.d.ts +0 -15
  37. package/dist/tool-name.d.ts.map +0 -1
  38. package/dist/tool-name.js +0 -19
  39. package/dist/tool-name.js.map +0 -1
  40. package/dist/trailhead.d.ts +0 -41
  41. package/dist/trailhead.d.ts.map +0 -1
  42. package/dist/trailhead.js +0 -109
  43. package/dist/trailhead.js.map +0 -1
  44. package/src/__tests__/annotations.test.ts +0 -63
  45. package/src/__tests__/build.test.ts +0 -529
  46. package/src/__tests__/progress.test.ts +0 -136
  47. package/src/__tests__/tool-name.test.ts +0 -46
  48. package/src/__tests__/trailhead.test.ts +0 -158
  49. package/src/trailhead.ts +0 -173
  50. package/tsconfig.json +0 -9
  51. package/tsconfig.tsbuildinfo +0 -1
@@ -1,136 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
-
3
- import type { McpExtra } from '../build.js';
4
- import { createMcpProgressCallback } from '../progress.js';
5
-
6
- describe('createMcpProgressCallback', () => {
7
- test('returns undefined when no progressToken', () => {
8
- const extra: McpExtra = {};
9
- expect(createMcpProgressCallback(extra)).toBeUndefined();
10
- });
11
-
12
- test('returns undefined when no sendProgress function', () => {
13
- const extra: McpExtra = { progressToken: 'tok-1' };
14
- expect(createMcpProgressCallback(extra)).toBeUndefined();
15
- });
16
-
17
- test('sends 0/1 for start events', () => {
18
- const calls: [number, number][] = [];
19
- const extra: McpExtra = {
20
- progressToken: 'tok-1',
21
- sendProgress: (current, total) => {
22
- calls.push([current, total]);
23
- return Promise.resolve();
24
- },
25
- };
26
-
27
- const cb = createMcpProgressCallback(extra);
28
- expect(cb).toBeDefined();
29
-
30
- const callback = cb as NonNullable<typeof cb>;
31
- // oxlint-disable-next-line prefer-await-to-callbacks -- not a node callback
32
- callback({
33
- ts: new Date().toISOString(),
34
- type: 'start',
35
- });
36
-
37
- expect(calls).toEqual([[0, 1]]);
38
- });
39
-
40
- test('sends progress notification for progress events', () => {
41
- const calls: [number, number][] = [];
42
- const extra: McpExtra = {
43
- progressToken: 'tok-1',
44
- sendProgress: (current, total) => {
45
- calls.push([current, total]);
46
- return Promise.resolve();
47
- },
48
- };
49
-
50
- const cb = createMcpProgressCallback(extra);
51
- expect(cb).toBeDefined();
52
- const callback = cb as NonNullable<typeof cb>;
53
-
54
- // oxlint-disable-next-line prefer-await-to-callbacks -- not a node callback
55
- callback({
56
- current: 5,
57
- total: 10,
58
- ts: new Date().toISOString(),
59
- type: 'progress',
60
- });
61
-
62
- expect(calls).toEqual([[5, 10]]);
63
- });
64
-
65
- test('sends 1/1 for complete events', () => {
66
- const calls: [number, number][] = [];
67
- const extra: McpExtra = {
68
- progressToken: 'tok-1',
69
- sendProgress: (current, total) => {
70
- calls.push([current, total]);
71
- return Promise.resolve();
72
- },
73
- };
74
-
75
- const cb = createMcpProgressCallback(extra);
76
- expect(cb).toBeDefined();
77
- const callback = cb as NonNullable<typeof cb>;
78
-
79
- // oxlint-disable-next-line prefer-await-to-callbacks -- not a node callback
80
- callback({
81
- ts: new Date().toISOString(),
82
- type: 'complete',
83
- });
84
-
85
- expect(calls).toEqual([[1, 1]]);
86
- });
87
-
88
- test('does not send for error events', () => {
89
- const calls: [number, number][] = [];
90
- const extra: McpExtra = {
91
- progressToken: 'tok-1',
92
- sendProgress: (current, total) => {
93
- calls.push([current, total]);
94
- return Promise.resolve();
95
- },
96
- };
97
-
98
- const cb = createMcpProgressCallback(extra);
99
- expect(cb).toBeDefined();
100
- const callback = cb as NonNullable<typeof cb>;
101
-
102
- // oxlint-disable-next-line prefer-await-to-callbacks -- not a node callback
103
- callback({
104
- message: 'something broke',
105
- ts: new Date().toISOString(),
106
- type: 'error',
107
- });
108
-
109
- expect(calls).toEqual([]);
110
- });
111
-
112
- test('handles missing total gracefully', () => {
113
- const calls: [number, number][] = [];
114
- const extra: McpExtra = {
115
- progressToken: 'tok-1',
116
- sendProgress: (current, total) => {
117
- calls.push([current, total]);
118
- return Promise.resolve();
119
- },
120
- };
121
-
122
- const cb = createMcpProgressCallback(extra);
123
- expect(cb).toBeDefined();
124
- const callback = cb as NonNullable<typeof cb>;
125
-
126
- // oxlint-disable-next-line prefer-await-to-callbacks -- not a node callback
127
- callback({
128
- current: 3,
129
- ts: new Date().toISOString(),
130
- type: 'progress',
131
- });
132
-
133
- // Sends current with 0 as indeterminate total
134
- expect(calls).toEqual([[3, 0]]);
135
- });
136
- });
@@ -1,46 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
-
3
- import { deriveToolName } from '../tool-name.js';
4
-
5
- describe('deriveToolName', () => {
6
- test('replaces dots with underscores', () => {
7
- expect(deriveToolName('myapp', 'entity.show')).toBe('myapp_entity_show');
8
- });
9
-
10
- test('prefixes with app name', () => {
11
- expect(deriveToolName('myapp', 'search')).toBe('myapp_search');
12
- });
13
-
14
- test('handles multiple dots', () => {
15
- expect(deriveToolName('myapp', 'entity.onboard')).toBe(
16
- 'myapp_entity_onboard'
17
- );
18
- });
19
-
20
- test('replaces hyphens with underscores', () => {
21
- expect(deriveToolName('my-app', 'some-trail')).toBe('my_app_some_trail');
22
- });
23
-
24
- test('lowercases everything', () => {
25
- expect(deriveToolName('MyApp', 'Entity.Show')).toBe('myapp_entity_show');
26
- });
27
-
28
- test('handles single-segment trail IDs', () => {
29
- expect(deriveToolName('dispatch', 'search')).toBe('dispatch_search');
30
- });
31
-
32
- test('handles dots in app name', () => {
33
- expect(deriveToolName('my.app', 'trail')).toBe('my_app_trail');
34
- });
35
-
36
- test('matches spec examples', () => {
37
- expect(deriveToolName('myapp', 'entity.show')).toBe('myapp_entity_show');
38
- expect(deriveToolName('myapp', 'search')).toBe('myapp_search');
39
- expect(deriveToolName('myapp', 'entity.onboard')).toBe(
40
- 'myapp_entity_onboard'
41
- );
42
- expect(deriveToolName('dispatch', 'patch.search')).toBe(
43
- 'dispatch_patch_search'
44
- );
45
- });
46
- });
@@ -1,158 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
-
3
- import { Result, trail, topo } from '@ontrails/core';
4
- import { z } from 'zod';
5
-
6
- import { trailhead, createMcpServer } from '../trailhead.js';
7
- import { buildMcpTools } from '../build.js';
8
- import type { McpToolDefinition } from '../build.js';
9
-
10
- // ---------------------------------------------------------------------------
11
- // Tests
12
- // ---------------------------------------------------------------------------
13
-
14
- const requireTool = (tools: McpToolDefinition[], name: string) => {
15
- const tool = tools.find((entry) => entry.name === name);
16
- expect(tool).toBeDefined();
17
- if (!tool) {
18
- throw new Error(`Expected tool: ${name}`);
19
- }
20
- return tool;
21
- };
22
-
23
- /**
24
- * Unwrap buildMcpTools result, throwing on error so test failures show up clearly.
25
- */
26
- const buildTools = (
27
- ...args: Parameters<typeof buildMcpTools>
28
- ): McpToolDefinition[] => {
29
- const result = buildMcpTools(...args);
30
- if (result.isErr()) {
31
- throw result.error;
32
- }
33
- return result.value;
34
- };
35
-
36
- const createIntegrationTools = () => {
37
- const greetTrail = trail('greet', {
38
- blaze: (input) => Result.ok({ greeting: `Hello, ${input.name}!` }),
39
- description: 'Greet someone',
40
- input: z.object({ name: z.string() }),
41
- intent: 'read',
42
- });
43
-
44
- const deleteTrail = trail('item.delete', {
45
- blaze: (_input) => Result.ok({ deleted: true }),
46
- description: 'Delete an item',
47
- input: z.object({ id: z.string() }),
48
- intent: 'destroy',
49
- });
50
-
51
- return buildTools(topo('myapp', { deleteTrail, greetTrail }));
52
- };
53
-
54
- describe('trailhead', () => {
55
- test('trailhead throws on invalid topo', async () => {
56
- const t = trail('broken', {
57
- blaze: () => Result.ok({}),
58
- crosses: ['nonexistent.trail'],
59
- input: z.object({}),
60
- output: z.object({}),
61
- });
62
- const app = topo('test', { t });
63
- await expect(trailhead(app)).rejects.toThrow(/validation/i);
64
- });
65
-
66
- test('trailhead skips validation when validate: false', async () => {
67
- const t = trail('broken', {
68
- blaze: () => Result.ok({}),
69
- crosses: ['nonexistent.trail'],
70
- input: z.object({}),
71
- output: z.object({}),
72
- });
73
- const app = topo('test', { t });
74
- const result = await Promise.race([
75
- trailhead(app, { validate: false }).then(() => 'resolved' as const),
76
- // oxlint-disable-next-line avoid-new -- Promise constructor needed for setTimeout-based timeout
77
- new Promise<'timeout'>((resolve) => {
78
- setTimeout(() => {
79
- resolve('timeout');
80
- }, 50);
81
- }),
82
- ]);
83
- expect(['resolved', 'timeout']).toContain(result);
84
- });
85
-
86
- test('TrailheadMcpOptions accepts provision overrides', () => {
87
- const opts: Parameters<typeof trailhead>[1] = {
88
- provisions: {},
89
- validate: false,
90
- };
91
- expect(opts.provisions).toEqual({});
92
- });
93
-
94
- test('createMcpServer registers tools that can be listed', () => {
95
- const echoTrail = trail('echo', {
96
- blaze: (input) => Result.ok({ reply: input.message }),
97
- description: 'Echo',
98
- input: z.object({ message: z.string() }),
99
- intent: 'read',
100
- });
101
-
102
- const app = topo('testapp', { echoTrail });
103
- const tools = buildTools(app);
104
- const server = createMcpServer(tools, {
105
- name: 'testapp',
106
- version: '0.1.0',
107
- });
108
-
109
- // Server is created successfully
110
- expect(server).toBeDefined();
111
- });
112
-
113
- test('createMcpServer handles multiple tools', () => {
114
- const echoTrail = trail('echo', {
115
- blaze: (input) => Result.ok({ reply: input.message }),
116
- description: 'Echo',
117
- input: z.object({ message: z.string() }),
118
- });
119
-
120
- const searchTrail = trail('search', {
121
- blaze: (input) => Result.ok({ results: [input.query] }),
122
- description: 'Search',
123
- input: z.object({ query: z.string() }),
124
- intent: 'read',
125
- });
126
-
127
- const app = topo('testapp', { echoTrail, searchTrail });
128
- const tools = buildTools(app);
129
-
130
- expect(tools).toHaveLength(2);
131
-
132
- const server = createMcpServer(tools, {
133
- name: 'testapp',
134
- version: '0.1.0',
135
- });
136
- expect(server).toBeDefined();
137
- });
138
-
139
- test('buildMcpTools + createMcpServer integration', () => {
140
- const tools = createIntegrationTools();
141
- const names = tools.map((t) => t.name);
142
- expect(names).toContain('myapp_greet');
143
- expect(names).toContain('myapp_item_delete');
144
-
145
- expect(requireTool(tools, 'myapp_greet').annotations?.readOnlyHint).toBe(
146
- true
147
- );
148
- expect(
149
- requireTool(tools, 'myapp_item_delete').annotations?.destructiveHint
150
- ).toBe(true);
151
-
152
- const server = createMcpServer(tools, {
153
- name: 'myapp',
154
- version: '1.0.0',
155
- });
156
- expect(server).toBeDefined();
157
- });
158
- });
package/src/trailhead.ts DELETED
@@ -1,173 +0,0 @@
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
-
12
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
13
- import {
14
- CallToolRequestSchema,
15
- ListToolsRequestSchema,
16
- } from '@modelcontextprotocol/sdk/types.js';
17
- import type {
18
- Gate,
19
- ProvisionOverrideMap,
20
- Topo,
21
- TrailContextInit,
22
- } from '@ontrails/core';
23
- import { validateEstablishedTopo } from '@ontrails/core';
24
-
25
- import type { McpToolDefinition } from './build.js';
26
- import { buildMcpTools } from './build.js';
27
- import { connectStdio } from './stdio.js';
28
-
29
- // ---------------------------------------------------------------------------
30
- // Options
31
- // ---------------------------------------------------------------------------
32
-
33
- export interface TrailheadMcpOptions {
34
- /** Config values for provisions that declare a `config` schema, keyed by provision ID. */
35
- readonly configValues?:
36
- | Readonly<Record<string, Record<string, unknown>>>
37
- | undefined;
38
- readonly createContext?:
39
- | (() => TrailContextInit | Promise<TrailContextInit>)
40
- | 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;
52
- /** Set to `false` to skip topo validation at startup. Defaults to `true`. */
53
- readonly validate?: boolean | undefined;
54
- }
55
-
56
- // ---------------------------------------------------------------------------
57
- // Internal: create MCP server with tool handlers
58
- // ---------------------------------------------------------------------------
59
-
60
- /**
61
- * Create an MCP Server instance and register all tools.
62
- */
63
- export const createMcpServer = (
64
- tools: McpToolDefinition[],
65
- info: { readonly name: string; readonly version: string }
66
- ): Server => {
67
- const server = new Server(
68
- { name: info.name, version: info.version },
69
- { capabilities: { tools: {} } }
70
- );
71
-
72
- // Build a lookup map for tool dispatch
73
- const toolMap = new Map<string, McpToolDefinition>();
74
- for (const tool of tools) {
75
- toolMap.set(tool.name, tool);
76
- }
77
-
78
- // Register tools/list handler
79
- // oxlint-disable-next-line require-await -- MCP SDK requires async handler
80
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
81
- tools: tools.map((t) => ({
82
- annotations: t.annotations,
83
- description: t.description,
84
- inputSchema: t.inputSchema,
85
- name: t.name,
86
- })),
87
- }));
88
-
89
- // Register tools/call handler
90
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
91
- const tool = toolMap.get(request.params.name);
92
- if (tool === undefined) {
93
- return {
94
- content: [
95
- {
96
- text: `Unknown tool: ${request.params.name}`,
97
- type: 'text' as const,
98
- },
99
- ],
100
- isError: true,
101
- } as Record<string, unknown>;
102
- }
103
-
104
- const args = (request.params.arguments ?? {}) as Record<string, unknown>;
105
- const progressToken = request.params._meta?.progressToken;
106
-
107
- const sendProgress =
108
- progressToken === undefined
109
- ? undefined
110
- : async (current: number, total: number) => {
111
- await server.notification({
112
- method: 'notifications/progress',
113
- params: {
114
- progress: current,
115
- progressToken: progressToken,
116
- total,
117
- },
118
- });
119
- };
120
-
121
- const extra = {
122
- abortSignal: undefined as AbortSignal | undefined,
123
- progressToken,
124
- sendProgress,
125
- };
126
-
127
- const result = await tool.handler(args, extra);
128
- // Spread to satisfy MCP SDK's index-signature requirement
129
- return { ...result } as Record<string, unknown>;
130
- });
131
-
132
- return server;
133
- };
134
-
135
- // ---------------------------------------------------------------------------
136
- // trailhead
137
- // ---------------------------------------------------------------------------
138
-
139
- /**
140
- * Build MCP tools from an App, create a server, and connect via stdio.
141
- */
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, {
154
- configValues: options.configValues,
155
- createContext: options.createContext,
156
- excludeTrails: options.excludeTrails,
157
- gates: options.gates,
158
- includeTrails: options.includeTrails,
159
- provisions: options.provisions,
160
- validate: options.validate,
161
- });
162
-
163
- if (toolsResult.isErr()) {
164
- throw toolsResult.error;
165
- }
166
-
167
- const server = createMcpServer(toolsResult.value, {
168
- name: options.serverInfo?.name ?? app.name,
169
- version: options.serverInfo?.version ?? '0.1.0',
170
- });
171
-
172
- await connectStdio(server);
173
- };
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "src"
6
- },
7
- "include": ["src"],
8
- "exclude": ["**/__tests__/**", "**/*.test.ts", "dist"]
9
- }
@@ -1 +0,0 @@
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"}