@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/surface.ts
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface helpers for exposing a topo over MCP.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
6
|
+
import {
|
|
7
|
+
CallToolRequestSchema,
|
|
8
|
+
ErrorCode,
|
|
9
|
+
ListResourcesRequestSchema,
|
|
10
|
+
ListToolsRequestSchema,
|
|
11
|
+
McpError,
|
|
12
|
+
ReadResourceRequestSchema,
|
|
13
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
14
|
+
import type {
|
|
15
|
+
BaseSurfaceOptions,
|
|
16
|
+
Layer,
|
|
17
|
+
OverlayEnvelopeLike,
|
|
18
|
+
ResourceOverrideMap,
|
|
19
|
+
Topo,
|
|
20
|
+
TrailContextInit,
|
|
21
|
+
} from '@ontrails/core';
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
McpSurfaceTrailheadMap,
|
|
25
|
+
McpToolDefinition,
|
|
26
|
+
ResolveMcpPermit,
|
|
27
|
+
} from './build.js';
|
|
28
|
+
import { deriveMcpTools } from './build.js';
|
|
29
|
+
import { buildMcpResources } from './resources.js';
|
|
30
|
+
import type { BuiltMcpResources, McpResourcesConfig } from './resources.js';
|
|
31
|
+
import { connectStdio } from './stdio.js';
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Options
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
export interface CreateServerOptions extends BaseSurfaceOptions {
|
|
38
|
+
readonly createContext?:
|
|
39
|
+
| (() => TrailContextInit | Promise<TrailContextInit>)
|
|
40
|
+
| undefined;
|
|
41
|
+
readonly description?: string | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* App-authored overlay envelopes (the same collection compile embeds in
|
|
44
|
+
* `trails.lock`). The `surfaces` overlay's `mcp` bindings are the authored,
|
|
45
|
+
* lockable default: list bindings become grouped trailhead tools and scalar
|
|
46
|
+
* bindings become tool synonyms.
|
|
47
|
+
*/
|
|
48
|
+
readonly overlays?: readonly OverlayEnvelopeLike[] | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Call-site trailhead map. Override-in-context by design: when both this
|
|
51
|
+
* map and overlay `mcp` list bindings are present, the call-site map wins
|
|
52
|
+
* at runtime.
|
|
53
|
+
*/
|
|
54
|
+
readonly trailheads?: McpSurfaceTrailheadMap | undefined;
|
|
55
|
+
readonly layers?: readonly Layer[] | undefined;
|
|
56
|
+
readonly mcpResources?: McpResourcesConfig | false | undefined;
|
|
57
|
+
readonly name?: string | undefined;
|
|
58
|
+
readonly resources?: ResourceOverrideMap | undefined;
|
|
59
|
+
readonly resolvePermit?: ResolveMcpPermit | undefined;
|
|
60
|
+
readonly version?: string | undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface SurfaceMcpResult {
|
|
64
|
+
readonly close: () => Promise<void>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Internal: create MCP server with tool handlers
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create an MCP Server instance and register all tools.
|
|
73
|
+
*
|
|
74
|
+
* When provided, `info.description` is forwarded to the MCP SDK as the
|
|
75
|
+
* server's `instructions` field — the SDK's documented channel for
|
|
76
|
+
* "optional instructions describing how to use the server and its features."
|
|
77
|
+
*/
|
|
78
|
+
const createMcpServer = (
|
|
79
|
+
tools: McpToolDefinition[],
|
|
80
|
+
info: {
|
|
81
|
+
readonly name: string;
|
|
82
|
+
readonly version: string;
|
|
83
|
+
readonly description?: string | undefined;
|
|
84
|
+
},
|
|
85
|
+
mcpResources?: BuiltMcpResources | undefined
|
|
86
|
+
): Server => {
|
|
87
|
+
const server = new Server(
|
|
88
|
+
{ name: info.name, version: info.version },
|
|
89
|
+
{
|
|
90
|
+
capabilities: {
|
|
91
|
+
...(mcpResources === undefined ? {} : { resources: {} }),
|
|
92
|
+
tools: {},
|
|
93
|
+
},
|
|
94
|
+
...(info.description === undefined
|
|
95
|
+
? {}
|
|
96
|
+
: { instructions: info.description }),
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Build a lookup map for tool dispatch
|
|
101
|
+
const toolMap = new Map<string, McpToolDefinition>();
|
|
102
|
+
for (const tool of tools) {
|
|
103
|
+
toolMap.set(tool.name, tool);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Register tools/list handler
|
|
107
|
+
// oxlint-disable-next-line require-await -- MCP SDK requires async handler
|
|
108
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
109
|
+
tools: tools.map((t) => ({
|
|
110
|
+
_meta: t._meta,
|
|
111
|
+
annotations: t.annotations,
|
|
112
|
+
description: t.description,
|
|
113
|
+
inputSchema: t.inputSchema,
|
|
114
|
+
name: t.name,
|
|
115
|
+
outputSchema: t.outputSchema,
|
|
116
|
+
})),
|
|
117
|
+
}));
|
|
118
|
+
|
|
119
|
+
// Register tools/call handler
|
|
120
|
+
server.setRequestHandler(
|
|
121
|
+
CallToolRequestSchema,
|
|
122
|
+
async (request, requestExtra) => {
|
|
123
|
+
const tool = toolMap.get(request.params.name);
|
|
124
|
+
if (tool === undefined) {
|
|
125
|
+
return {
|
|
126
|
+
content: [
|
|
127
|
+
{
|
|
128
|
+
text: `Unknown tool: ${request.params.name}`,
|
|
129
|
+
type: 'text' as const,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
isError: true,
|
|
133
|
+
} as Record<string, unknown>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const args = (request.params.arguments ?? {}) as Record<string, unknown>;
|
|
137
|
+
const progressToken = request.params._meta?.progressToken;
|
|
138
|
+
const { authInfo } = requestExtra as {
|
|
139
|
+
readonly authInfo?:
|
|
140
|
+
| {
|
|
141
|
+
readonly accessToken?: string | undefined;
|
|
142
|
+
readonly sessionId?: string | undefined;
|
|
143
|
+
readonly token?: string | undefined;
|
|
144
|
+
}
|
|
145
|
+
| undefined;
|
|
146
|
+
};
|
|
147
|
+
const authorizationToken = authInfo?.accessToken ?? authInfo?.token;
|
|
148
|
+
|
|
149
|
+
const sendProgress =
|
|
150
|
+
progressToken === undefined
|
|
151
|
+
? undefined
|
|
152
|
+
: async (current: number, total: number) => {
|
|
153
|
+
await server.notification({
|
|
154
|
+
method: 'notifications/progress',
|
|
155
|
+
params: {
|
|
156
|
+
progress: current,
|
|
157
|
+
progressToken,
|
|
158
|
+
total,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const extra = {
|
|
164
|
+
abortSignal: requestExtra.signal,
|
|
165
|
+
...(authorizationToken === undefined
|
|
166
|
+
? {}
|
|
167
|
+
: { authorization: `Bearer ${authorizationToken}` }),
|
|
168
|
+
progressToken,
|
|
169
|
+
sendProgress,
|
|
170
|
+
...(authInfo?.sessionId === undefined
|
|
171
|
+
? {}
|
|
172
|
+
: { sessionId: authInfo.sessionId }),
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const result = await tool.handler(args, extra);
|
|
176
|
+
// Spread to satisfy MCP SDK's index-signature requirement
|
|
177
|
+
return { ...result } as Record<string, unknown>;
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
if (mcpResources !== undefined) {
|
|
182
|
+
// oxlint-disable-next-line require-await -- MCP SDK requires async handler
|
|
183
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
184
|
+
resources: mcpResources.list.map((resource) => ({
|
|
185
|
+
description: resource.description,
|
|
186
|
+
mimeType: resource.mimeType,
|
|
187
|
+
name: resource.name,
|
|
188
|
+
uri: resource.uri,
|
|
189
|
+
})),
|
|
190
|
+
}));
|
|
191
|
+
|
|
192
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
193
|
+
const content = mcpResources.read(request.params.uri);
|
|
194
|
+
if (content === undefined) {
|
|
195
|
+
throw new McpError(
|
|
196
|
+
ErrorCode.InvalidParams,
|
|
197
|
+
`Resource ${request.params.uri} not found`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
contents: [content],
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return server;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// createServer
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Build MCP tools from a topo and create an MCP server.
|
|
215
|
+
*
|
|
216
|
+
* @remarks This is a host materialization boundary. Derivation failures are
|
|
217
|
+
* thrown for server bootstrap code after `deriveMcpTools` has already
|
|
218
|
+
* represented the framework error as a Result.
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* ```ts
|
|
222
|
+
* import { connectStdio, createServer } from '@ontrails/mcp';
|
|
223
|
+
*
|
|
224
|
+
* const server = createServer(graph, { name: 'demo' });
|
|
225
|
+
* await connectStdio(server);
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
228
|
+
export const createServer = (
|
|
229
|
+
graph: Topo,
|
|
230
|
+
options: CreateServerOptions = {}
|
|
231
|
+
): Server => {
|
|
232
|
+
const toolsResult = deriveMcpTools(graph, {
|
|
233
|
+
configValues: options.configValues,
|
|
234
|
+
createContext: options.createContext,
|
|
235
|
+
exclude: options.exclude,
|
|
236
|
+
include: options.include,
|
|
237
|
+
intent: options.intent,
|
|
238
|
+
layers: options.layers,
|
|
239
|
+
overlays: options.overlays,
|
|
240
|
+
resolvePermit: options.resolvePermit,
|
|
241
|
+
resources: options.resources,
|
|
242
|
+
trailheads: options.trailheads,
|
|
243
|
+
validate: options.validate,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (toolsResult.isErr()) {
|
|
247
|
+
throw toolsResult.error;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const mcpResources =
|
|
251
|
+
options.mcpResources === false
|
|
252
|
+
? undefined
|
|
253
|
+
: buildMcpResources(graph, toolsResult.value, options.mcpResources);
|
|
254
|
+
|
|
255
|
+
return createMcpServer(
|
|
256
|
+
toolsResult.value,
|
|
257
|
+
{
|
|
258
|
+
description: options.description ?? graph.description,
|
|
259
|
+
name: options.name ?? graph.name,
|
|
260
|
+
version: options.version ?? graph.version ?? '0.1.0',
|
|
261
|
+
},
|
|
262
|
+
mcpResources
|
|
263
|
+
);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// surface
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Build MCP tools from a topo, create a server, and connect via stdio.
|
|
272
|
+
*
|
|
273
|
+
* @remarks Opens the MCP server on stdio. For custom transports, use
|
|
274
|
+
* `createServer(graph)` with `connectStdio` or your own adapter.
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* ```ts
|
|
278
|
+
* import { surface } from '@ontrails/mcp';
|
|
279
|
+
*
|
|
280
|
+
* await surface(graph, { name: 'demo' });
|
|
281
|
+
* ```
|
|
282
|
+
*/
|
|
283
|
+
export const surface = async (
|
|
284
|
+
graph: Topo,
|
|
285
|
+
options: CreateServerOptions = {}
|
|
286
|
+
): Promise<SurfaceMcpResult> => {
|
|
287
|
+
const server = createServer(graph, options);
|
|
288
|
+
await connectStdio(server);
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
close: async () => {
|
|
292
|
+
await server.close();
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
};
|
package/src/tool-name.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive MCP-safe tool names from app name + trail ID.
|
|
3
|
+
*
|
|
4
|
+
* The derivation itself lives in `@ontrails/core` (`deriveMcpToolName`) so
|
|
5
|
+
* governance readers such as Warden's `surface-overlay-coherence` rule check
|
|
6
|
+
* collisions against the exact rendering the MCP surface renders.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { deriveMcpToolName } from '@ontrails/core';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Convert app name + trail ID to an MCP-safe tool name.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* deriveToolName("myapp", "entity.show") // "myapp_entity_show"
|
|
16
|
+
* deriveToolName("dispatch", "patch.search") // "dispatch_patch_search"
|
|
17
|
+
*/
|
|
18
|
+
export const deriveToolName = deriveMcpToolName;
|