@ontrails/mcp 0.2.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.
- package/CHANGELOG.md +661 -0
- package/README.md +148 -0
- package/package.json +36 -0
- package/src/annotations.ts +74 -0
- package/src/build.ts +1572 -0
- package/src/index.ts +52 -0
- package/src/progress.ts +95 -0
- package/src/resources.ts +336 -0
- package/src/stdio.ts +25 -0
- package/src/surface.ts +295 -0
- package/src/tool-name.ts +18 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Build
|
|
2
|
+
export {
|
|
3
|
+
MCP_TOOL_ERROR_META_KEY,
|
|
4
|
+
MCP_TOOL_EXAMPLES_META_KEY,
|
|
5
|
+
MCP_TOOL_DEFERRED_META_KEY,
|
|
6
|
+
MCP_TOOL_TRAILHEAD_META_KEY,
|
|
7
|
+
deriveMcpTools,
|
|
8
|
+
type DeriveMcpToolsOptions,
|
|
9
|
+
type McpSurfaceTrailheadDefinition,
|
|
10
|
+
type McpSurfaceTrailheadMap,
|
|
11
|
+
type McpSurfaceTrailheadTrailSelector,
|
|
12
|
+
type McpToolDefinition,
|
|
13
|
+
type McpToolResult,
|
|
14
|
+
type McpToolErrorMeta,
|
|
15
|
+
type McpContent,
|
|
16
|
+
type McpExtra,
|
|
17
|
+
type ResolveMcpPermit,
|
|
18
|
+
type ResolveMcpPermitInput,
|
|
19
|
+
} from './build.js';
|
|
20
|
+
|
|
21
|
+
// MCP resources
|
|
22
|
+
export {
|
|
23
|
+
MCP_EXAMPLES_RESOURCE_PREFIX,
|
|
24
|
+
MCP_SURFACE_MAP_RESOURCE_URI,
|
|
25
|
+
MCP_TRAIL_RESOURCE_PREFIX,
|
|
26
|
+
buildMcpResources,
|
|
27
|
+
isMcpTrailheadTool,
|
|
28
|
+
type BuiltMcpResources,
|
|
29
|
+
type McpResourceContent,
|
|
30
|
+
type McpResourceDefinition,
|
|
31
|
+
type McpResourcesConfig,
|
|
32
|
+
} from './resources.js';
|
|
33
|
+
|
|
34
|
+
// Tool naming
|
|
35
|
+
export { deriveToolName } from './tool-name.js';
|
|
36
|
+
|
|
37
|
+
// Annotations
|
|
38
|
+
export { deriveAnnotations, type McpAnnotations } from './annotations.js';
|
|
39
|
+
|
|
40
|
+
// Progress
|
|
41
|
+
export { createMcpProgressCallback } from './progress.js';
|
|
42
|
+
|
|
43
|
+
// Surface
|
|
44
|
+
export {
|
|
45
|
+
createServer,
|
|
46
|
+
surface,
|
|
47
|
+
type CreateServerOptions,
|
|
48
|
+
type SurfaceMcpResult,
|
|
49
|
+
} from './surface.js';
|
|
50
|
+
|
|
51
|
+
// Transport
|
|
52
|
+
export { connectStdio } from './stdio.js';
|
package/src/progress.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge Trails ProgressCallback to MCP sendProgress notifications.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ProgressCallback, ProgressEvent } from '@ontrails/core';
|
|
6
|
+
|
|
7
|
+
import type { McpExtra } from './build.js';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Event handlers
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
type SendFn = (current: number, total: number) => Promise<void>;
|
|
14
|
+
|
|
15
|
+
/** Fire-and-forget a progress send, swallowing transport errors. */
|
|
16
|
+
const fireSend = async (
|
|
17
|
+
send: SendFn,
|
|
18
|
+
current: number,
|
|
19
|
+
total: number
|
|
20
|
+
): Promise<void> => {
|
|
21
|
+
try {
|
|
22
|
+
await send(current, total);
|
|
23
|
+
} catch {
|
|
24
|
+
/* Transport errors are expected and safe to ignore */
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const handleProgress = (event: ProgressEvent, send: SendFn): void => {
|
|
29
|
+
if (event.current !== undefined && event.total !== undefined) {
|
|
30
|
+
fireSend(send, event.current, event.total);
|
|
31
|
+
} else if (event.current !== undefined) {
|
|
32
|
+
fireSend(send, event.current, 0);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const progressHandlers: Record<
|
|
37
|
+
string,
|
|
38
|
+
(event: ProgressEvent, send: SendFn) => void
|
|
39
|
+
> = {
|
|
40
|
+
complete: (_event, send) => fireSend(send, 1, 1),
|
|
41
|
+
error: () => {
|
|
42
|
+
/* No progress notification for errors */
|
|
43
|
+
},
|
|
44
|
+
progress: handleProgress,
|
|
45
|
+
start: (_event, send) => fireSend(send, 0, 1),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Factory
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a ProgressCallback that bridges to MCP's sendProgress.
|
|
54
|
+
*
|
|
55
|
+
* Returns `undefined` if the MCP client did not provide a progressToken
|
|
56
|
+
* (meaning no progress reporting was requested).
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* import { createMcpProgressCallback, type McpExtra } from '@ontrails/mcp';
|
|
61
|
+
*
|
|
62
|
+
* const extra: McpExtra = {
|
|
63
|
+
* progressToken: 'token-123',
|
|
64
|
+
* sendProgress: async (current, total) => {
|
|
65
|
+
* console.log(`progress ${current}/${total}`);
|
|
66
|
+
* },
|
|
67
|
+
* };
|
|
68
|
+
*
|
|
69
|
+
* const progress = createMcpProgressCallback(extra);
|
|
70
|
+
* if (progress !== undefined) {
|
|
71
|
+
* progress({
|
|
72
|
+
* current: 0,
|
|
73
|
+
* ts: new Date().toISOString(),
|
|
74
|
+
* total: 1,
|
|
75
|
+
* type: 'start',
|
|
76
|
+
* });
|
|
77
|
+
* }
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export const createMcpProgressCallback = (
|
|
81
|
+
extra: McpExtra
|
|
82
|
+
): ProgressCallback | undefined => {
|
|
83
|
+
if (extra.progressToken === undefined || extra.progressToken === null) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
if (typeof extra.sendProgress !== 'function') {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const send = extra.sendProgress;
|
|
91
|
+
return (event: ProgressEvent): void => {
|
|
92
|
+
const handler = progressHandlers[event.type];
|
|
93
|
+
handler?.(event, send);
|
|
94
|
+
};
|
|
95
|
+
};
|
package/src/resources.ts
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP resource rendering for cold Trails context.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { deriveStructuredTrailExamples } from '@ontrails/core';
|
|
6
|
+
import type { Topo, Trail } from '@ontrails/core';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
MCP_TOOL_DEFERRED_META_KEY,
|
|
10
|
+
MCP_TOOL_TRAILHEAD_META_KEY,
|
|
11
|
+
} from './build.js';
|
|
12
|
+
import type { McpToolDefinition } from './build.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resource URI used for the resolved MCP surface map.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { MCP_SURFACE_MAP_RESOURCE_URI } from '@ontrails/mcp';
|
|
20
|
+
*
|
|
21
|
+
* const surfaceMap = resources.read(MCP_SURFACE_MAP_RESOURCE_URI);
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export const MCP_SURFACE_MAP_RESOURCE_URI = 'trails://surface-map';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Prefix used for trail example resources exposed through MCP.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* import { MCP_EXAMPLES_RESOURCE_PREFIX } from '@ontrails/mcp';
|
|
32
|
+
*
|
|
33
|
+
* const uri = `${MCP_EXAMPLES_RESOURCE_PREFIX}${encodeURIComponent('tasks.create')}`;
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export const MCP_EXAMPLES_RESOURCE_PREFIX = 'trails://examples/';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Prefix used for trail graph fact resources exposed through MCP.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* import { MCP_TRAIL_RESOURCE_PREFIX } from '@ontrails/mcp';
|
|
44
|
+
*
|
|
45
|
+
* const uri = `${MCP_TRAIL_RESOURCE_PREFIX}${encodeURIComponent('tasks.create')}`;
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export const MCP_TRAIL_RESOURCE_PREFIX = 'trails://trail/';
|
|
49
|
+
|
|
50
|
+
export interface McpResourceDefinition {
|
|
51
|
+
readonly uri: string;
|
|
52
|
+
readonly mimeType: string;
|
|
53
|
+
readonly name: string;
|
|
54
|
+
readonly description?: string | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface McpResourceContent {
|
|
58
|
+
readonly uri: string;
|
|
59
|
+
readonly mimeType: string;
|
|
60
|
+
readonly text: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface McpResourcesConfig {
|
|
64
|
+
readonly surfaceMap?: boolean | undefined;
|
|
65
|
+
readonly examples?: boolean | undefined;
|
|
66
|
+
readonly graph?: boolean | undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface BuiltMcpResources {
|
|
70
|
+
readonly list: readonly McpResourceDefinition[];
|
|
71
|
+
readonly read: (uri: string) => McpResourceContent | undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface McpSurfaceMapTool {
|
|
75
|
+
readonly annotations: McpToolDefinition['annotations'];
|
|
76
|
+
readonly description: string | undefined;
|
|
77
|
+
readonly trailheadId?: string | undefined;
|
|
78
|
+
readonly inputSchema: Record<string, unknown>;
|
|
79
|
+
readonly memberTrailIds?: readonly string[] | undefined;
|
|
80
|
+
readonly name: string;
|
|
81
|
+
readonly outputSchema?: Record<string, unknown> | undefined;
|
|
82
|
+
readonly trailId?: string | undefined;
|
|
83
|
+
readonly versions?: McpToolDefinition['versions'];
|
|
84
|
+
readonly deferred?: true | undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface McpSurfaceMap {
|
|
88
|
+
readonly surface: 'mcp';
|
|
89
|
+
readonly tools: readonly McpSurfaceMapTool[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface McpTrailResource {
|
|
93
|
+
readonly trailId: string;
|
|
94
|
+
readonly description?: string | undefined;
|
|
95
|
+
readonly intent: Trail<unknown, unknown, unknown>['intent'];
|
|
96
|
+
readonly visibility: Trail<unknown, unknown, unknown>['visibility'];
|
|
97
|
+
readonly composes: readonly string[];
|
|
98
|
+
readonly resources: readonly string[];
|
|
99
|
+
readonly signals: {
|
|
100
|
+
readonly fires: readonly string[];
|
|
101
|
+
readonly on: readonly string[];
|
|
102
|
+
};
|
|
103
|
+
readonly surface: 'mcp';
|
|
104
|
+
readonly tools: readonly McpSurfaceMapTool[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const asJson = (value: unknown): string =>
|
|
108
|
+
`${JSON.stringify(value, null, 2)}\n`;
|
|
109
|
+
|
|
110
|
+
const renderSurfaceMapTool = (tool: McpToolDefinition): McpSurfaceMapTool => ({
|
|
111
|
+
annotations: tool.annotations,
|
|
112
|
+
description: tool.description,
|
|
113
|
+
inputSchema: tool.inputSchema,
|
|
114
|
+
name: tool.name,
|
|
115
|
+
...(tool.trailheadId === undefined ? {} : { trailheadId: tool.trailheadId }),
|
|
116
|
+
...(tool.memberTrailIds === undefined
|
|
117
|
+
? {}
|
|
118
|
+
: { memberTrailIds: tool.memberTrailIds }),
|
|
119
|
+
...(tool.outputSchema === undefined
|
|
120
|
+
? {}
|
|
121
|
+
: { outputSchema: tool.outputSchema }),
|
|
122
|
+
...(tool.trailId === undefined ? {} : { trailId: tool.trailId }),
|
|
123
|
+
...(tool.versions === undefined ? {} : { versions: tool.versions }),
|
|
124
|
+
...(tool._meta?.[MCP_TOOL_DEFERRED_META_KEY] === true
|
|
125
|
+
? { deferred: true }
|
|
126
|
+
: {}),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const buildSurfaceMap = (
|
|
130
|
+
tools: readonly McpToolDefinition[]
|
|
131
|
+
): McpSurfaceMap => ({
|
|
132
|
+
surface: 'mcp',
|
|
133
|
+
tools: tools.map(renderSurfaceMapTool),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const exposedTrailIds = (
|
|
137
|
+
tools: readonly McpToolDefinition[]
|
|
138
|
+
): ReadonlySet<string> =>
|
|
139
|
+
new Set(
|
|
140
|
+
tools.flatMap((tool) => [
|
|
141
|
+
...(tool.trailId === undefined ? [] : [tool.trailId]),
|
|
142
|
+
...(tool.memberTrailIds ?? []),
|
|
143
|
+
])
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const examplesUriForTrail = (trailId: string): string =>
|
|
147
|
+
`${MCP_EXAMPLES_RESOURCE_PREFIX}${encodeURIComponent(trailId)}`;
|
|
148
|
+
|
|
149
|
+
const trailUriForTrail = (trailId: string): string =>
|
|
150
|
+
`${MCP_TRAIL_RESOURCE_PREFIX}${encodeURIComponent(trailId)}`;
|
|
151
|
+
|
|
152
|
+
const buildExampleResource = (
|
|
153
|
+
trailItem: Trail<unknown, unknown, unknown>
|
|
154
|
+
):
|
|
155
|
+
| {
|
|
156
|
+
readonly content: McpResourceContent;
|
|
157
|
+
readonly listing: McpResourceDefinition;
|
|
158
|
+
}
|
|
159
|
+
| undefined => {
|
|
160
|
+
const examples = deriveStructuredTrailExamples(trailItem.examples);
|
|
161
|
+
if (examples === undefined) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const uri = examplesUriForTrail(trailItem.id);
|
|
166
|
+
return {
|
|
167
|
+
content: {
|
|
168
|
+
mimeType: 'application/json',
|
|
169
|
+
text: asJson({
|
|
170
|
+
examples,
|
|
171
|
+
trailId: trailItem.id,
|
|
172
|
+
}),
|
|
173
|
+
uri,
|
|
174
|
+
},
|
|
175
|
+
listing: {
|
|
176
|
+
description: `Structured examples for trail "${trailItem.id}".`,
|
|
177
|
+
mimeType: 'application/json',
|
|
178
|
+
name: `Trail examples: ${trailItem.id}`,
|
|
179
|
+
uri,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const buildExampleResources = (
|
|
185
|
+
graph: Topo,
|
|
186
|
+
tools: readonly McpToolDefinition[]
|
|
187
|
+
): readonly {
|
|
188
|
+
readonly content: McpResourceContent;
|
|
189
|
+
readonly listing: McpResourceDefinition;
|
|
190
|
+
}[] => {
|
|
191
|
+
const visibleTrailIds = exposedTrailIds(tools);
|
|
192
|
+
return graph
|
|
193
|
+
.list()
|
|
194
|
+
.filter((trailItem) => visibleTrailIds.has(trailItem.id))
|
|
195
|
+
.map((trailItem) =>
|
|
196
|
+
buildExampleResource(trailItem as Trail<unknown, unknown, unknown>)
|
|
197
|
+
)
|
|
198
|
+
.filter((resource) => resource !== undefined);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const resourceId = (
|
|
202
|
+
resource: Trail<unknown, unknown, unknown>['resources'][number]
|
|
203
|
+
) => resource.id;
|
|
204
|
+
|
|
205
|
+
const buildTrailGraphResource = (
|
|
206
|
+
trailItem: Trail<unknown, unknown, unknown>,
|
|
207
|
+
tools: readonly McpToolDefinition[]
|
|
208
|
+
): {
|
|
209
|
+
readonly content: McpResourceContent;
|
|
210
|
+
readonly listing: McpResourceDefinition;
|
|
211
|
+
} => {
|
|
212
|
+
const uri = trailUriForTrail(trailItem.id);
|
|
213
|
+
const surfaceTools = tools
|
|
214
|
+
.filter(
|
|
215
|
+
(tool) =>
|
|
216
|
+
tool.trailId === trailItem.id ||
|
|
217
|
+
tool.memberTrailIds?.includes(trailItem.id) === true
|
|
218
|
+
)
|
|
219
|
+
.map(renderSurfaceMapTool);
|
|
220
|
+
const payload: McpTrailResource = {
|
|
221
|
+
composes: trailItem.composes,
|
|
222
|
+
...(trailItem.description === undefined
|
|
223
|
+
? {}
|
|
224
|
+
: { description: trailItem.description }),
|
|
225
|
+
intent: trailItem.intent,
|
|
226
|
+
resources: trailItem.resources.map(resourceId),
|
|
227
|
+
signals: {
|
|
228
|
+
fires: trailItem.fires,
|
|
229
|
+
on: trailItem.on,
|
|
230
|
+
},
|
|
231
|
+
surface: 'mcp',
|
|
232
|
+
tools: surfaceTools,
|
|
233
|
+
trailId: trailItem.id,
|
|
234
|
+
visibility: trailItem.visibility,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
content: {
|
|
239
|
+
mimeType: 'application/json',
|
|
240
|
+
text: asJson(payload),
|
|
241
|
+
uri,
|
|
242
|
+
},
|
|
243
|
+
listing: {
|
|
244
|
+
description: `MCP-visible graph facts for trail "${trailItem.id}".`,
|
|
245
|
+
mimeType: 'application/json',
|
|
246
|
+
name: `Trail graph fact: ${trailItem.id}`,
|
|
247
|
+
uri,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const buildTrailGraphResources = (
|
|
253
|
+
graph: Topo,
|
|
254
|
+
tools: readonly McpToolDefinition[]
|
|
255
|
+
): readonly {
|
|
256
|
+
readonly content: McpResourceContent;
|
|
257
|
+
readonly listing: McpResourceDefinition;
|
|
258
|
+
}[] => {
|
|
259
|
+
const visibleTrailIds = exposedTrailIds(tools);
|
|
260
|
+
return graph
|
|
261
|
+
.list()
|
|
262
|
+
.filter((trailItem) => visibleTrailIds.has(trailItem.id))
|
|
263
|
+
.map((trailItem) =>
|
|
264
|
+
buildTrailGraphResource(
|
|
265
|
+
trailItem as Trail<unknown, unknown, unknown>,
|
|
266
|
+
tools
|
|
267
|
+
)
|
|
268
|
+
);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Build the cold-context MCP resources for a Trails graph and tool set.
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* ```ts
|
|
276
|
+
* import { buildMcpResources, deriveMcpTools } from '@ontrails/mcp';
|
|
277
|
+
*
|
|
278
|
+
* const tools = deriveMcpTools(app).value;
|
|
279
|
+
* const resources = buildMcpResources(app, tools);
|
|
280
|
+
* ```
|
|
281
|
+
*/
|
|
282
|
+
export const buildMcpResources = (
|
|
283
|
+
graph: Topo,
|
|
284
|
+
tools: readonly McpToolDefinition[],
|
|
285
|
+
config: McpResourcesConfig = {}
|
|
286
|
+
): BuiltMcpResources => {
|
|
287
|
+
const listings: McpResourceDefinition[] = [];
|
|
288
|
+
const contents = new Map<string, McpResourceContent>();
|
|
289
|
+
|
|
290
|
+
if (config.surfaceMap !== false) {
|
|
291
|
+
const surfaceMapListing = {
|
|
292
|
+
description: 'Resolved MCP surface rendering for this Trails app.',
|
|
293
|
+
mimeType: 'application/json',
|
|
294
|
+
name: 'Trails MCP surface map',
|
|
295
|
+
uri: MCP_SURFACE_MAP_RESOURCE_URI,
|
|
296
|
+
};
|
|
297
|
+
listings.push(surfaceMapListing);
|
|
298
|
+
contents.set(MCP_SURFACE_MAP_RESOURCE_URI, {
|
|
299
|
+
mimeType: 'application/json',
|
|
300
|
+
text: asJson(buildSurfaceMap(tools)),
|
|
301
|
+
uri: MCP_SURFACE_MAP_RESOURCE_URI,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (config.examples !== false) {
|
|
306
|
+
for (const resource of buildExampleResources(graph, tools)) {
|
|
307
|
+
listings.push(resource.listing);
|
|
308
|
+
contents.set(resource.content.uri, resource.content);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (config.graph === true) {
|
|
313
|
+
for (const resource of buildTrailGraphResources(graph, tools)) {
|
|
314
|
+
listings.push(resource.listing);
|
|
315
|
+
contents.set(resource.content.uri, resource.content);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
list: listings,
|
|
321
|
+
read: (uri) => contents.get(uri),
|
|
322
|
+
};
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Return whether an MCP tool was rendered from a surface trailhead.
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* ```ts
|
|
330
|
+
* import { isMcpTrailheadTool } from '@ontrails/mcp';
|
|
331
|
+
*
|
|
332
|
+
* const trailheadTools = tools.filter(isMcpTrailheadTool);
|
|
333
|
+
* ```
|
|
334
|
+
*/
|
|
335
|
+
export const isMcpTrailheadTool = (tool: McpToolDefinition): boolean =>
|
|
336
|
+
tool._meta?.[MCP_TOOL_TRAILHEAD_META_KEY] !== undefined;
|
package/src/stdio.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
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 surface().
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
9
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Connect an MCP server to stdio transport.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { connectStdio, createServer } from '@ontrails/mcp';
|
|
17
|
+
*
|
|
18
|
+
* const server = createServer(graph);
|
|
19
|
+
* await connectStdio(server);
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export const connectStdio = async (server: Server): Promise<void> => {
|
|
23
|
+
const transport = new StdioServerTransport();
|
|
24
|
+
await server.connect(transport);
|
|
25
|
+
};
|