@ontrails/mcp 1.0.0-beta.0

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 (48) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/.turbo/turbo-lint.log +3 -0
  3. package/.turbo/turbo-typecheck.log +1 -0
  4. package/CHANGELOG.md +20 -0
  5. package/README.md +161 -0
  6. package/dist/annotations.d.ts +19 -0
  7. package/dist/annotations.d.ts.map +1 -0
  8. package/dist/annotations.js +29 -0
  9. package/dist/annotations.js.map +1 -0
  10. package/dist/blaze.d.ts +36 -0
  11. package/dist/blaze.d.ts.map +1 -0
  12. package/dist/blaze.js +96 -0
  13. package/dist/blaze.js.map +1 -0
  14. package/dist/build.d.ts +40 -0
  15. package/dist/build.d.ts.map +1 -0
  16. package/dist/build.js +190 -0
  17. package/dist/build.js.map +1 -0
  18. package/dist/index.d.ts +7 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +13 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/progress.d.ts +13 -0
  23. package/dist/progress.d.ts.map +1 -0
  24. package/dist/progress.js +51 -0
  25. package/dist/progress.js.map +1 -0
  26. package/dist/stdio.d.ts +12 -0
  27. package/dist/stdio.d.ts.map +1 -0
  28. package/dist/stdio.js +15 -0
  29. package/dist/stdio.js.map +1 -0
  30. package/dist/tool-name.d.ts +15 -0
  31. package/dist/tool-name.d.ts.map +1 -0
  32. package/dist/tool-name.js +19 -0
  33. package/dist/tool-name.js.map +1 -0
  34. package/package.json +23 -0
  35. package/src/__tests__/annotations.test.ts +70 -0
  36. package/src/__tests__/blaze.test.ts +105 -0
  37. package/src/__tests__/build.test.ts +377 -0
  38. package/src/__tests__/progress.test.ts +136 -0
  39. package/src/__tests__/tool-name.test.ts +46 -0
  40. package/src/annotations.ts +51 -0
  41. package/src/blaze.ts +146 -0
  42. package/src/build.ts +321 -0
  43. package/src/index.ts +24 -0
  44. package/src/progress.ts +73 -0
  45. package/src/stdio.ts +17 -0
  46. package/src/tool-name.ts +19 -0
  47. package/tsconfig.json +9 -0
  48. package/tsconfig.tsbuildinfo +1 -0
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ // Build
2
+ export { buildMcpTools, } from './build.js';
3
+ // Tool naming
4
+ export { deriveToolName } from './tool-name.js';
5
+ // Annotations
6
+ export { deriveAnnotations } from './annotations.js';
7
+ // Progress
8
+ export { createMcpProgressCallback } from './progress.js';
9
+ // Blaze
10
+ export { blaze } from './blaze.js';
11
+ // Transport
12
+ export { connectStdio } from './stdio.js';
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,QAAQ;AACR,OAAO,EACL,aAAa,GAMd,MAAM,YAAY,CAAC;AAEpB,cAAc;AACd,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEhD,cAAc;AACd,OAAO,EAAE,iBAAiB,EAAuB,MAAM,kBAAkB,CAAC;AAE1E,WAAW;AACX,OAAO,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAE1D,QAAQ;AACR,OAAO,EAAE,KAAK,EAAwB,MAAM,YAAY,CAAC;AAEzD,YAAY;AACZ,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Bridge Trails ProgressCallback to MCP sendProgress notifications.
3
+ */
4
+ import type { ProgressCallback } from '@ontrails/core';
5
+ import type { McpExtra } from './build.js';
6
+ /**
7
+ * Create a ProgressCallback that bridges to MCP's sendProgress.
8
+ *
9
+ * Returns `undefined` if the MCP client did not provide a progressToken
10
+ * (meaning no progress reporting was requested).
11
+ */
12
+ export declare const createMcpProgressCallback: (extra: McpExtra) => ProgressCallback | undefined;
13
+ //# sourceMappingURL=progress.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"progress.d.ts","sourceRoot":"","sources":["../src/progress.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAiB,MAAM,gBAAgB,CAAC;AAEtE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AA6C3C;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,GACpC,OAAO,QAAQ,KACd,gBAAgB,GAAG,SAarB,CAAC"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Bridge Trails ProgressCallback to MCP sendProgress notifications.
3
+ */
4
+ /** Fire-and-forget a progress send, swallowing transport errors. */
5
+ const fireSend = async (send, current, total) => {
6
+ try {
7
+ await send(current, total);
8
+ }
9
+ catch {
10
+ /* Transport errors are expected and safe to ignore */
11
+ }
12
+ };
13
+ const handleProgress = (event, send) => {
14
+ if (event.current !== undefined && event.total !== undefined) {
15
+ fireSend(send, event.current, event.total);
16
+ }
17
+ else if (event.current !== undefined) {
18
+ fireSend(send, event.current, 0);
19
+ }
20
+ };
21
+ const progressHandlers = {
22
+ complete: (_event, send) => fireSend(send, 1, 1),
23
+ error: () => {
24
+ /* No progress notification for errors */
25
+ },
26
+ progress: handleProgress,
27
+ start: (_event, send) => fireSend(send, 0, 1),
28
+ };
29
+ // ---------------------------------------------------------------------------
30
+ // Factory
31
+ // ---------------------------------------------------------------------------
32
+ /**
33
+ * Create a ProgressCallback that bridges to MCP's sendProgress.
34
+ *
35
+ * Returns `undefined` if the MCP client did not provide a progressToken
36
+ * (meaning no progress reporting was requested).
37
+ */
38
+ export const createMcpProgressCallback = (extra) => {
39
+ if (extra.progressToken === undefined || extra.progressToken === null) {
40
+ return undefined;
41
+ }
42
+ if (typeof extra.sendProgress !== 'function') {
43
+ return undefined;
44
+ }
45
+ const send = extra.sendProgress;
46
+ return (event) => {
47
+ const handler = progressHandlers[event.type];
48
+ handler?.(event, send);
49
+ };
50
+ };
51
+ //# sourceMappingURL=progress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"progress.js","sourceRoot":"","sources":["../src/progress.ts"],"names":[],"mappings":"AAAA;;GAEG;AAYH,oEAAoE;AACpE,MAAM,QAAQ,GAAG,KAAK,EACpB,IAAY,EACZ,OAAe,EACf,KAAa,EACE,EAAE;IACjB,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,sDAAsD;IACxD,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,cAAc,GAAG,CAAC,KAAoB,EAAE,IAAY,EAAQ,EAAE;IAClE,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC7D,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IAC7C,CAAC;SAAM,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACvC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IACnC,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,gBAAgB,GAGlB;IACF,QAAQ,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;IAChD,KAAK,EAAE,GAAG,EAAE;QACV,yCAAyC;IAC3C,CAAC;IACD,QAAQ,EAAE,cAAc;IACxB,KAAK,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;CAC9C,CAAC;AAEF,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CACvC,KAAe,EACe,EAAE;IAChC,IAAI,KAAK,CAAC,aAAa,KAAK,SAAS,IAAI,KAAK,CAAC,aAAa,KAAK,IAAI,EAAE,CAAC;QACtE,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,OAAO,KAAK,CAAC,YAAY,KAAK,UAAU,EAAE,CAAC;QAC7C,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,CAAC,YAAY,CAAC;IAChC,OAAO,CAAC,KAAoB,EAAQ,EAAE;QACpC,MAAM,OAAO,GAAG,gBAAgB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC7C,OAAO,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACzB,CAAC,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Thin wrapper around MCP SDK's StdioServerTransport.
3
+ *
4
+ * Exists as a separate function so it can be swapped for other transports
5
+ * (SSE, streamable HTTP) without changing blaze().
6
+ */
7
+ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
8
+ /**
9
+ * Connect an MCP server to stdio transport.
10
+ */
11
+ export declare const connectStdio: (server: Server) => Promise<void>;
12
+ //# sourceMappingURL=stdio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../src/stdio.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAGxE;;GAEG;AACH,eAAO,MAAM,YAAY,GAAU,QAAQ,MAAM,KAAG,OAAO,CAAC,IAAI,CAG/D,CAAC"}
package/dist/stdio.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Thin wrapper around MCP SDK's StdioServerTransport.
3
+ *
4
+ * Exists as a separate function so it can be swapped for other transports
5
+ * (SSE, streamable HTTP) without changing blaze().
6
+ */
7
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
+ /**
9
+ * Connect an MCP server to stdio transport.
10
+ */
11
+ export const connectStdio = async (server) => {
12
+ const transport = new StdioServerTransport();
13
+ await server.connect(transport);
14
+ };
15
+ //# sourceMappingURL=stdio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdio.js","sourceRoot":"","sources":["../src/stdio.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF;;GAEG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAAE,MAAc,EAAiB,EAAE;IAClE,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Derive MCP-safe tool names from app name + trail ID.
3
+ *
4
+ * MCP tool names must be [a-z0-9_]+. We prefix with the app name,
5
+ * replace dots and hyphens with underscores, and lowercase everything.
6
+ */
7
+ /**
8
+ * Convert app name + trail ID to an MCP-safe tool name.
9
+ *
10
+ * @example
11
+ * deriveToolName("myapp", "entity.show") // "myapp_entity_show"
12
+ * deriveToolName("dispatch", "patch.search") // "dispatch_patch_search"
13
+ */
14
+ export declare const deriveToolName: (appName: string, trailId: string) => string;
15
+ //# sourceMappingURL=tool-name.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-name.d.ts","sourceRoot":"","sources":["../src/tool-name.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,MAAM,EAAE,SAAS,MAAM,KAAG,MAIjE,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Derive MCP-safe tool names from app name + trail ID.
3
+ *
4
+ * MCP tool names must be [a-z0-9_]+. We prefix with the app name,
5
+ * replace dots and hyphens with underscores, and lowercase everything.
6
+ */
7
+ /**
8
+ * Convert app name + trail ID to an MCP-safe tool name.
9
+ *
10
+ * @example
11
+ * deriveToolName("myapp", "entity.show") // "myapp_entity_show"
12
+ * deriveToolName("dispatch", "patch.search") // "dispatch_patch_search"
13
+ */
14
+ export const deriveToolName = (appName, trailId) => {
15
+ const prefix = appName.toLowerCase().replaceAll(/[.-]/g, '_');
16
+ const suffix = trailId.toLowerCase().replaceAll(/[.-]/g, '_');
17
+ return `${prefix}_${suffix}`;
18
+ };
19
+ //# sourceMappingURL=tool-name.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-name.js","sourceRoot":"","sources":["../src/tool-name.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,OAAe,EAAE,OAAe,EAAU,EAAE;IACzE,MAAM,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAC9D,MAAM,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAC9D,OAAO,GAAG,MAAM,IAAI,MAAM,EAAE,CAAC;AAC/B,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@ontrails/mcp",
3
+ "version": "1.0.0-beta.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.ts",
7
+ "./package.json": "./package.json"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc -b",
11
+ "test": "bun test",
12
+ "typecheck": "tsc --noEmit",
13
+ "lint": "oxlint ./src",
14
+ "clean": "rm -rf dist *.tsbuildinfo"
15
+ },
16
+ "dependencies": {
17
+ "@ontrails/core": "workspace:*"
18
+ },
19
+ "peerDependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.12.0",
21
+ "zod": "catalog:"
22
+ }
23
+ }
@@ -0,0 +1,70 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { deriveAnnotations } from '../annotations.js';
4
+
5
+ describe('deriveAnnotations', () => {
6
+ test('readOnly trail produces readOnlyHint', () => {
7
+ const annotations = deriveAnnotations({ readOnly: true });
8
+ expect(annotations.readOnlyHint).toBe(true);
9
+ expect(annotations.destructiveHint).toBeUndefined();
10
+ expect(annotations.idempotentHint).toBeUndefined();
11
+ });
12
+
13
+ test('destructive trail produces destructiveHint', () => {
14
+ const annotations = deriveAnnotations({ destructive: true });
15
+ expect(annotations.destructiveHint).toBe(true);
16
+ expect(annotations.readOnlyHint).toBeUndefined();
17
+ });
18
+
19
+ test('idempotent trail produces idempotentHint', () => {
20
+ const annotations = deriveAnnotations({ idempotent: true });
21
+ expect(annotations.idempotentHint).toBe(true);
22
+ });
23
+
24
+ test('multiple markers combine correctly', () => {
25
+ const annotations = deriveAnnotations({
26
+ idempotent: true,
27
+ readOnly: true,
28
+ });
29
+ expect(annotations.readOnlyHint).toBe(true);
30
+ expect(annotations.idempotentHint).toBe(true);
31
+ expect(annotations.destructiveHint).toBeUndefined();
32
+ });
33
+
34
+ test('no markers produces empty annotations', () => {
35
+ const annotations = deriveAnnotations({});
36
+ expect(annotations.readOnlyHint).toBeUndefined();
37
+ expect(annotations.destructiveHint).toBeUndefined();
38
+ expect(annotations.idempotentHint).toBeUndefined();
39
+ expect(annotations.title).toBeUndefined();
40
+ });
41
+
42
+ test('description maps to title', () => {
43
+ const annotations = deriveAnnotations({
44
+ description: 'Show entity details',
45
+ });
46
+ expect(annotations.title).toBe('Show entity details');
47
+ });
48
+
49
+ test('all markers plus description', () => {
50
+ const annotations = deriveAnnotations({
51
+ description: 'A trail',
52
+ destructive: true,
53
+ idempotent: true,
54
+ readOnly: true,
55
+ });
56
+ expect(annotations.readOnlyHint).toBe(true);
57
+ expect(annotations.destructiveHint).toBe(true);
58
+ expect(annotations.idempotentHint).toBe(true);
59
+ expect(annotations.title).toBe('A trail');
60
+ });
61
+
62
+ test('false values are not included', () => {
63
+ const annotations = deriveAnnotations({
64
+ destructive: false,
65
+ readOnly: false,
66
+ });
67
+ expect(annotations.readOnlyHint).toBeUndefined();
68
+ expect(annotations.destructiveHint).toBeUndefined();
69
+ });
70
+ });
@@ -0,0 +1,105 @@
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 { createMcpServer } from '../blaze.js';
7
+ import { buildMcpTools } from '../build.js';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Tests
11
+ // ---------------------------------------------------------------------------
12
+
13
+ const requireTool = (tools: ReturnType<typeof buildMcpTools>, name: string) => {
14
+ const tool = tools.find((entry) => entry.name === name);
15
+ expect(tool).toBeDefined();
16
+ if (!tool) {
17
+ throw new Error(`Expected tool: ${name}`);
18
+ }
19
+ return tool;
20
+ };
21
+
22
+ const createIntegrationTools = () => {
23
+ const greetTrail = trail('greet', {
24
+ description: 'Greet someone',
25
+ implementation: (input) => Result.ok({ greeting: `Hello, ${input.name}!` }),
26
+ input: z.object({ name: z.string() }),
27
+ readOnly: true,
28
+ });
29
+
30
+ const deleteTrail = trail('item.delete', {
31
+ description: 'Delete an item',
32
+ destructive: true,
33
+ implementation: (_input) => Result.ok({ deleted: true }),
34
+ input: z.object({ id: z.string() }),
35
+ });
36
+
37
+ return buildMcpTools(topo('myapp', { deleteTrail, greetTrail }));
38
+ };
39
+
40
+ describe('blaze', () => {
41
+ test('createMcpServer registers tools that can be listed', () => {
42
+ const echoTrail = trail('echo', {
43
+ description: 'Echo',
44
+ implementation: (input) => Result.ok({ reply: input.message }),
45
+ input: z.object({ message: z.string() }),
46
+ readOnly: true,
47
+ });
48
+
49
+ const app = topo('testapp', { echoTrail });
50
+ const tools = buildMcpTools(app);
51
+ const server = createMcpServer(tools, {
52
+ name: 'testapp',
53
+ version: '0.1.0',
54
+ });
55
+
56
+ // Server is created successfully
57
+ expect(server).toBeDefined();
58
+ });
59
+
60
+ test('createMcpServer handles multiple tools', () => {
61
+ const echoTrail = trail('echo', {
62
+ description: 'Echo',
63
+ implementation: (input) => Result.ok({ reply: input.message }),
64
+ input: z.object({ message: z.string() }),
65
+ });
66
+
67
+ const searchTrail = trail('search', {
68
+ description: 'Search',
69
+ implementation: (input) => Result.ok({ results: [input.query] }),
70
+ input: z.object({ query: z.string() }),
71
+ readOnly: true,
72
+ });
73
+
74
+ const app = topo('testapp', { echoTrail, searchTrail });
75
+ const tools = buildMcpTools(app);
76
+
77
+ expect(tools).toHaveLength(2);
78
+
79
+ const server = createMcpServer(tools, {
80
+ name: 'testapp',
81
+ version: '0.1.0',
82
+ });
83
+ expect(server).toBeDefined();
84
+ });
85
+
86
+ test('buildMcpTools + createMcpServer integration', () => {
87
+ const tools = createIntegrationTools();
88
+ const names = tools.map((t) => t.name);
89
+ expect(names).toContain('myapp_greet');
90
+ expect(names).toContain('myapp_item_delete');
91
+
92
+ expect(requireTool(tools, 'myapp_greet').annotations?.readOnlyHint).toBe(
93
+ true
94
+ );
95
+ expect(
96
+ requireTool(tools, 'myapp_item_delete').annotations?.destructiveHint
97
+ ).toBe(true);
98
+
99
+ const server = createMcpServer(tools, {
100
+ name: 'myapp',
101
+ version: '1.0.0',
102
+ });
103
+ expect(server).toBeDefined();
104
+ });
105
+ });