@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
@@ -0,0 +1,377 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { Result, trail, topo } from '@ontrails/core';
4
+ import type { Layer } from '@ontrails/core';
5
+ import { z } from 'zod';
6
+
7
+ import { buildMcpTools } from '../build.js';
8
+ import type { McpExtra } from '../build.js';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Helpers
12
+ // ---------------------------------------------------------------------------
13
+
14
+ const echoTrail = trail('echo', {
15
+ description: 'Echo a message back',
16
+ implementation: (input) => Result.ok({ reply: input.message }),
17
+ input: z.object({ message: z.string() }),
18
+ output: z.object({ reply: z.string() }),
19
+ readOnly: true,
20
+ });
21
+
22
+ const deleteTrail = trail('item.delete', {
23
+ description: 'Delete an item',
24
+ destructive: true,
25
+ implementation: (_input) => Result.ok({ deleted: true }),
26
+ input: z.object({ id: z.string() }),
27
+ });
28
+
29
+ const failTrail = trail('fail', {
30
+ description: 'Always fails',
31
+ implementation: (input) => Result.err(new Error(input.reason)),
32
+ input: z.object({ reason: z.string() }),
33
+ });
34
+
35
+ const exampleTrail = trail('with.examples', {
36
+ description: 'A trail with examples',
37
+ examples: [
38
+ {
39
+ expected: { greeting: 'hello world' },
40
+ input: { name: 'world' },
41
+ name: 'basic',
42
+ },
43
+ ],
44
+ implementation: (input) => Result.ok({ greeting: `hello ${input.name}` }),
45
+ input: z.object({ name: z.string() }),
46
+ });
47
+
48
+ const noExtra: McpExtra = {};
49
+
50
+ const requireTool = (tools: ReturnType<typeof buildMcpTools>, name: string) => {
51
+ const tool = tools.find((entry) => entry.name === name);
52
+ expect(tool).toBeDefined();
53
+ if (!tool) {
54
+ throw new Error(`Expected tool: ${name}`);
55
+ }
56
+ return tool;
57
+ };
58
+
59
+ const requireOnlyTool = (tools: ReturnType<typeof buildMcpTools>) => {
60
+ expect(tools).toHaveLength(1);
61
+ const [tool] = tools;
62
+ expect(tool).toBeDefined();
63
+ if (!tool) {
64
+ throw new Error('Expected one MCP tool');
65
+ }
66
+ return tool;
67
+ };
68
+
69
+ const parseJsonContent = (
70
+ content: { readonly text?: string | undefined } | undefined
71
+ ): unknown => {
72
+ expect(content?.text).toBeDefined();
73
+ return JSON.parse(content?.text ?? 'null');
74
+ };
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Tests
78
+ // ---------------------------------------------------------------------------
79
+
80
+ describe('buildMcpTools', () => {
81
+ describe('discovery', () => {
82
+ test('builds tools from a single-trail app', () => {
83
+ const app = topo('myapp', { echoTrail });
84
+ const tools = buildMcpTools(app);
85
+
86
+ expect(tools).toHaveLength(1);
87
+ expect(requireOnlyTool(tools).name).toBe('myapp_echo');
88
+ });
89
+
90
+ test('builds tools from a multi-trail app', () => {
91
+ const app = topo('myapp', { deleteTrail, echoTrail, failTrail });
92
+ const tools = buildMcpTools(app);
93
+
94
+ expect(tools).toHaveLength(3);
95
+ const names = tools.map((t) => t.name);
96
+ expect(names).toContain('myapp_echo');
97
+ expect(names).toContain('myapp_item_delete');
98
+ expect(names).toContain('myapp_fail');
99
+ });
100
+
101
+ test('tool names follow derivation rules', () => {
102
+ const app = topo('myapp', { deleteTrail });
103
+ const tools = buildMcpTools(app);
104
+
105
+ expect(requireOnlyTool(tools).name).toBe('myapp_item_delete');
106
+ });
107
+
108
+ test('input schema is valid JSON Schema', () => {
109
+ const app = topo('myapp', { echoTrail });
110
+ const schema = requireOnlyTool(buildMcpTools(app)).inputSchema;
111
+
112
+ expect(schema['type']).toBe('object');
113
+ expect(schema['properties']).toBeDefined();
114
+ const props = schema['properties'] as Record<string, unknown>;
115
+ expect(props['message']).toEqual({ type: 'string' });
116
+ });
117
+
118
+ test('annotations are correctly derived', () => {
119
+ const app = topo('myapp', { deleteTrail, echoTrail });
120
+ const tools = buildMcpTools(app);
121
+
122
+ expect(requireTool(tools, 'myapp_echo').annotations?.readOnlyHint).toBe(
123
+ true
124
+ );
125
+ expect(requireTool(tools, 'myapp_echo').annotations?.title).toBe(
126
+ 'Echo a message back'
127
+ );
128
+ expect(
129
+ requireTool(tools, 'myapp_item_delete').annotations?.destructiveHint
130
+ ).toBe(true);
131
+ });
132
+ });
133
+
134
+ describe('handler execution', () => {
135
+ test('handler validates input and returns isError on invalid', async () => {
136
+ const app = topo('myapp', { echoTrail });
137
+ const tool = requireOnlyTool(buildMcpTools(app));
138
+
139
+ const result = await tool.handler({ notMessage: 123 }, noExtra);
140
+ expect(result?.isError).toBe(true);
141
+ expect(result?.content[0]?.type).toBe('text');
142
+ expect(result?.content[0]?.text).toBeDefined();
143
+ });
144
+
145
+ test('handler calls implementation and returns result as text content', async () => {
146
+ const app = topo('myapp', { echoTrail });
147
+ const tool = requireOnlyTool(buildMcpTools(app));
148
+
149
+ const result = await tool.handler({ message: 'hello' }, noExtra);
150
+ expect(result?.isError).toBeUndefined();
151
+ expect(result?.content[0]?.type).toBe('text');
152
+ expect(parseJsonContent(result?.content[0])).toEqual({
153
+ reply: 'hello',
154
+ });
155
+ });
156
+
157
+ test('handler maps errors to isError content', async () => {
158
+ const app = topo('myapp', { failTrail });
159
+ const tool = requireOnlyTool(buildMcpTools(app));
160
+
161
+ const result = await tool.handler({ reason: 'broken' }, noExtra);
162
+ expect(result?.isError).toBe(true);
163
+ expect(result?.content[0]?.text).toBe('broken');
164
+ });
165
+
166
+ test('handler catches thrown exceptions', async () => {
167
+ const throwTrail = trail('throw', {
168
+ implementation: () => {
169
+ throw new Error('unexpected crash');
170
+ },
171
+ input: z.object({}),
172
+ });
173
+
174
+ const tool = requireOnlyTool(
175
+ buildMcpTools(topo('myapp', { throwTrail }))
176
+ );
177
+ const result = await tool.handler({}, noExtra);
178
+
179
+ expect(result?.isError).toBe(true);
180
+ expect(result?.content[0]?.text).toBe('unexpected crash');
181
+ });
182
+ });
183
+
184
+ describe('filters', () => {
185
+ test('include filter limits which trails become tools', () => {
186
+ const app = topo('myapp', { deleteTrail, echoTrail, failTrail });
187
+ const tools = buildMcpTools(app, {
188
+ includeTrails: ['echo'],
189
+ });
190
+
191
+ expect(tools).toHaveLength(1);
192
+ expect(requireOnlyTool(tools).name).toBe('myapp_echo');
193
+ });
194
+
195
+ test('exclude filter removes specific trails', () => {
196
+ const app = topo('myapp', { deleteTrail, echoTrail, failTrail });
197
+ const tools = buildMcpTools(app, {
198
+ excludeTrails: ['fail'],
199
+ });
200
+
201
+ expect(tools).toHaveLength(2);
202
+ const names = tools.map((t) => t.name);
203
+ expect(names).not.toContain('myapp_fail');
204
+ });
205
+
206
+ test('include takes precedence over exclude', () => {
207
+ const app = topo('myapp', { deleteTrail, echoTrail, failTrail });
208
+ const tools = buildMcpTools(app, {
209
+ excludeTrails: ['fail'],
210
+ includeTrails: ['echo', 'fail'],
211
+ });
212
+
213
+ expect(tools).toHaveLength(2);
214
+ const names = tools.map((t) => t.name);
215
+ expect(names).toContain('myapp_echo');
216
+ expect(names).toContain('myapp_fail');
217
+ });
218
+ });
219
+
220
+ describe('composition', () => {
221
+ test('layers compose and execute around the implementation', async () => {
222
+ const calls: string[] = [];
223
+
224
+ const testLayer: Layer = {
225
+ name: 'test-layer',
226
+ wrap(_trail, impl) {
227
+ return async (input, ctx) => {
228
+ calls.push('before');
229
+ const result = await impl(input, ctx);
230
+ calls.push('after');
231
+ return result;
232
+ };
233
+ },
234
+ };
235
+
236
+ const app = topo('myapp', { echoTrail });
237
+ const tool = requireOnlyTool(buildMcpTools(app, { layers: [testLayer] }));
238
+
239
+ await tool.handler({ message: 'hi' }, noExtra);
240
+ expect(calls).toEqual(['before', 'after']);
241
+ });
242
+
243
+ test('AbortSignal propagates from MCP extra to TrailContext', async () => {
244
+ let capturedSignal: AbortSignal | undefined;
245
+
246
+ const signalTrail = trail('signal.check', {
247
+ implementation: (_input, ctx) => {
248
+ capturedSignal = ctx.signal;
249
+ return Result.ok({ ok: true });
250
+ },
251
+ input: z.object({}),
252
+ });
253
+
254
+ const controller = new AbortController();
255
+ const tool = requireOnlyTool(
256
+ buildMcpTools(topo('myapp', { signalTrail }))
257
+ );
258
+
259
+ await tool.handler({}, { signal: controller.signal });
260
+ expect(capturedSignal).toBe(controller.signal);
261
+ });
262
+
263
+ test('description includes first example input when present', () => {
264
+ const app = topo('myapp', { exampleTrail });
265
+ const tool = requireOnlyTool(buildMcpTools(app));
266
+
267
+ expect(tool.description).toContain('A trail with examples');
268
+ expect(tool.description).toContain('"name":"world"');
269
+ });
270
+
271
+ test('custom createContext is used when provided', async () => {
272
+ let contextUsed = false;
273
+
274
+ const ctxTrail = trail('ctx.check', {
275
+ implementation: (_input, ctx) => {
276
+ const ctxRecord = ctx as Record<string, unknown>;
277
+ contextUsed = ctxRecord['custom'] === true;
278
+ return Result.ok({ ok: true });
279
+ },
280
+ input: z.object({}),
281
+ });
282
+
283
+ const app = topo('myapp', { ctxTrail });
284
+ const tool = requireOnlyTool(
285
+ buildMcpTools(app, {
286
+ createContext: () => ({
287
+ custom: true,
288
+ requestId: 'test-id',
289
+ signal: new AbortController().signal,
290
+ }),
291
+ })
292
+ );
293
+
294
+ await tool.handler({}, noExtra);
295
+ expect(contextUsed).toBe(true);
296
+ });
297
+ });
298
+
299
+ describe('blob outputs', () => {
300
+ test('BlobRef output converts to image content', async () => {
301
+ const blobTrail = trail('blob.image', {
302
+ implementation: () =>
303
+ Result.ok({
304
+ data: new Uint8Array([1, 2, 3]),
305
+ kind: 'blob' as const,
306
+ mimeType: 'image/png',
307
+ name: 'test.png',
308
+ }),
309
+ input: z.object({}),
310
+ });
311
+
312
+ const tool = requireOnlyTool(buildMcpTools(topo('myapp', { blobTrail })));
313
+ const result = await tool.handler({}, noExtra);
314
+
315
+ expect(result?.content[0]?.type).toBe('image');
316
+ expect(result?.content[0]?.mimeType).toBe('image/png');
317
+ expect(result?.content[0]?.data).toBeDefined();
318
+ });
319
+
320
+ test('BlobRef output converts to resource content for non-images', async () => {
321
+ const blobTrail = trail('blob.file', {
322
+ implementation: () =>
323
+ Result.ok({
324
+ data: new Uint8Array([1, 2, 3]),
325
+ kind: 'blob' as const,
326
+ mimeType: 'application/pdf',
327
+ name: 'doc.pdf',
328
+ }),
329
+ input: z.object({}),
330
+ });
331
+
332
+ const tool = requireOnlyTool(buildMcpTools(topo('myapp', { blobTrail })));
333
+ const result = await tool.handler({}, noExtra);
334
+
335
+ expect(result?.content[0]?.type).toBe('resource');
336
+ expect(result?.content[0]?.uri).toBe('blob://doc.pdf');
337
+ expect(result?.content[0]?.mimeType).toBe('application/pdf');
338
+ });
339
+ });
340
+
341
+ describe('end-to-end', () => {
342
+ test('full pipeline from trail to MCP response', async () => {
343
+ const greetTrail = trail('greet', {
344
+ description: 'Greet someone',
345
+ idempotent: true,
346
+ implementation: (input) =>
347
+ Result.ok({ greeting: `Hello, ${input.name}!` }),
348
+ input: z.object({ name: z.string() }),
349
+ output: z.object({ greeting: z.string() }),
350
+ readOnly: true,
351
+ });
352
+
353
+ const tool = requireOnlyTool(
354
+ buildMcpTools(topo('testapp', { greetTrail }))
355
+ );
356
+
357
+ expect(tool).toMatchObject({
358
+ annotations: {
359
+ idempotentHint: true,
360
+ readOnlyHint: true,
361
+ },
362
+ description: 'Greet someone',
363
+ name: 'testapp_greet',
364
+ });
365
+ expect(tool.inputSchema['type']).toBe('object');
366
+
367
+ const successResult = await tool.handler({ name: 'World' }, noExtra);
368
+ expect(successResult?.isError).toBeUndefined();
369
+ expect(parseJsonContent(successResult?.content[0])).toEqual({
370
+ greeting: 'Hello, World!',
371
+ });
372
+
373
+ const errorResult = await tool.handler({}, noExtra);
374
+ expect(errorResult?.isError).toBe(true);
375
+ });
376
+ });
377
+ });
@@ -0,0 +1,136 @@
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
+ });
@@ -0,0 +1,46 @@
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
+ });
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Derive MCP tool annotations from trail spec markers.
3
+ */
4
+
5
+ import type { Trail } from '@ontrails/core';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Types
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export interface McpAnnotations {
12
+ readonly readOnlyHint?: boolean | undefined;
13
+ readonly destructiveHint?: boolean | undefined;
14
+ readonly idempotentHint?: boolean | undefined;
15
+ readonly openWorldHint?: boolean | undefined;
16
+ readonly title?: string | undefined;
17
+ }
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Derivation
21
+ // ---------------------------------------------------------------------------
22
+
23
+ /**
24
+ * Map trail spec fields to MCP tool annotations.
25
+ *
26
+ * Only sets hints that are explicitly declared on the trail.
27
+ * Omitted hints let the MCP SDK use its defaults.
28
+ */
29
+ export const deriveAnnotations = (
30
+ trail: Pick<
31
+ Trail<unknown, unknown>,
32
+ 'readOnly' | 'destructive' | 'idempotent' | 'description'
33
+ >
34
+ ): McpAnnotations => {
35
+ const annotations: Record<string, unknown> = {};
36
+
37
+ if (trail.readOnly === true) {
38
+ annotations['readOnlyHint'] = true;
39
+ }
40
+ if (trail.destructive === true) {
41
+ annotations['destructiveHint'] = true;
42
+ }
43
+ if (trail.idempotent === true) {
44
+ annotations['idempotentHint'] = true;
45
+ }
46
+ if (trail.description !== undefined) {
47
+ annotations['title'] = trail.description;
48
+ }
49
+
50
+ return annotations as McpAnnotations;
51
+ };