@ontrails/mcp 1.0.0-beta.32 → 1.0.0-beta.39
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 +28 -0
- package/package.json +2 -2
- package/src/build.ts +225 -78
- package/src/index.ts +5 -5
- package/src/resources.ts +8 -8
- package/src/surface.ts +17 -3
- package/src/tool-name.ts +6 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @ontrails/mcp
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.39
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`81373bc`](https://github.com/outfitter-dev/trails/commit/81373bc5e980bb06d56fb06af4f0986f72e318c7): Wave-2 MCP cutover to the app-authored `surfaces` overlay. The overlay's `mcp` bindings are now the authored, lockable default for the MCP surface: a list binding derives one grouped trailhead tool (member selection in `{ trail, input }`, member identity preserved in `{ trail, output }`, deterministic derived description), and a scalar binding derives an additional tool synonym whose MCP-safe name is published verbatim and must expand to exactly one trail. `deriveMcpTools`/`createServer` accept the new `overlays` option; `@ontrails/core` gains `expandMcpSurfaceBindings` and `deriveMcpTrailheadDescription`.
|
|
8
|
+
|
|
9
|
+
The call-site `CreateServerOptions.trailheads` map survives as permanent override-in-context design, not a compatibility bridge: when both channels are present, the call-site map wins at runtime. Warden's new `trailhead-override-divergence` rule (warn) names both sides when a call-site map's binding names or member selectors diverge from the authored overlay default.
|
|
10
|
+
|
|
11
|
+
Topographer now derives `graph.trailheads` from the overlay's `mcp` list bindings in both `deriveTopoGraph` and the store-side graph build, so trailhead facts flow from compiled locks into Wayfinder reads for the first time. The never-wired `DeriveTopoGraphOptions.trailheads` option and the `TopoGraphTrailheadDeclaration`/`TopoGraphTrailheadTrailSelector` types are removed — a beta-window hard cutover of an option no caller could reach; author the equivalent `mcp` list binding in `surfaceOverlay({ mcp })` instead.
|
|
12
|
+
|
|
13
|
+
- [`820b4ad`](https://github.com/outfitter-dev/trails/commit/820b4ad9c40ea383b3c489a05fe7e4b2328e324f): Add `surfaceOverlay` — the shared surface-naming schema (scalar binding = synonym, list binding = grouped entry, singleton list stays a group) with app-authored/adapter-derived overlay provenance enforced at collection and consumption, and the `surface-overlay-coherence` Warden rule. MCP tool-name derivation moves to `@ontrails/core` (`deriveMcpToolName`) so the surface and governance read one projection; `@ontrails/mcp`'s `deriveToolName` now delegates to it. The coherence rule activates on standard warden runs once fresh derivations collect app-module overlays through the shared compile channel (TRL-1209, next in this stack).
|
|
14
|
+
|
|
15
|
+
## 1.0.0-beta.38
|
|
16
|
+
|
|
17
|
+
## 1.0.0-beta.37
|
|
18
|
+
|
|
19
|
+
## 1.0.0-beta.36
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- [`6e63e48`](https://github.com/outfitter-dev/trails/commit/6e63e483617b84cb6868d0c4d58d5b5a8d3b9ed2): Complete the v1 grouped surface-entry vocabulary cutover from facet to trailhead, including Regrade dogfood support for governed string literal renames and composed AST rewrite application.
|
|
24
|
+
|
|
25
|
+
## 1.0.0-beta.35
|
|
26
|
+
|
|
27
|
+
## 1.0.0-beta.34
|
|
28
|
+
|
|
29
|
+
## 1.0.0-beta.33
|
|
30
|
+
|
|
3
31
|
## 1.0.0-beta.32
|
|
4
32
|
|
|
5
33
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ontrails/mcp",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.39",
|
|
4
4
|
"files": [
|
|
5
5
|
"src/**/*.ts",
|
|
6
6
|
"!src/**/__tests__/**",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"clean": "rm -rf dist *.tsbuildinfo"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@ontrails/core": "^1.0.0-beta.
|
|
25
|
+
"@ontrails/core": "^1.0.0-beta.39"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"@modelcontextprotocol/sdk": "^1.28.0",
|
package/src/build.ts
CHANGED
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
AuthError,
|
|
11
|
+
InternalError,
|
|
11
12
|
Result,
|
|
12
13
|
ValidationError,
|
|
13
14
|
collectAttachedTypedLayers,
|
|
15
|
+
deriveMcpTrailheadDescription,
|
|
14
16
|
deriveSurfaceTrailVersionProjections,
|
|
15
17
|
deriveStructuredTrailExamples,
|
|
16
18
|
executeTrail,
|
|
19
|
+
expandMcpSurfaceBindings,
|
|
17
20
|
filterSurfaceTrails,
|
|
18
21
|
isBlobRef,
|
|
19
22
|
isTrailsError,
|
|
@@ -21,6 +24,7 @@ import {
|
|
|
21
24
|
matchesTrailPattern,
|
|
22
25
|
projectLayerFieldName,
|
|
23
26
|
projectPublicSurfaceError,
|
|
27
|
+
resolveSurfaceOverlayBindings,
|
|
24
28
|
toBlobRefDescriptor,
|
|
25
29
|
validateSurfaceTopo,
|
|
26
30
|
withSurfaceLayerNames,
|
|
@@ -32,6 +36,8 @@ import type {
|
|
|
32
36
|
BaseSurfaceOptions,
|
|
33
37
|
BlobRef,
|
|
34
38
|
Layer,
|
|
39
|
+
McpSurfaceBindingExpansion,
|
|
40
|
+
OverlayEnvelopeLike,
|
|
35
41
|
ResourceOverrideMap,
|
|
36
42
|
SurfaceErrorProjection,
|
|
37
43
|
SurfaceTrailVersionProjection,
|
|
@@ -71,20 +77,20 @@ export const MCP_TOOL_EXAMPLES_META_KEY = 'ontrails/examples';
|
|
|
71
77
|
export const MCP_TOOL_ERROR_META_KEY = 'ontrails/error';
|
|
72
78
|
|
|
73
79
|
/**
|
|
74
|
-
* Metadata key used to identify MCP tools derived from surface
|
|
80
|
+
* Metadata key used to identify MCP tools derived from surface trailheads.
|
|
75
81
|
*
|
|
76
|
-
* Surface
|
|
77
|
-
* contracts. The metadata names the
|
|
82
|
+
* Surface trailheads preserve member trail identity rather than merging member
|
|
83
|
+
* contracts. The metadata names the trailhead and its member trail IDs so clients
|
|
78
84
|
* can inspect the grouped entry before choosing a selected trail.
|
|
79
85
|
*
|
|
80
86
|
* @example
|
|
81
87
|
* ```ts
|
|
82
|
-
* import {
|
|
88
|
+
* import { MCP_TOOL_TRAILHEAD_META_KEY } from '@ontrails/mcp';
|
|
83
89
|
*
|
|
84
|
-
* const
|
|
90
|
+
* const trailhead = tool._meta?.[MCP_TOOL_TRAILHEAD_META_KEY];
|
|
85
91
|
* ```
|
|
86
92
|
*/
|
|
87
|
-
export const
|
|
93
|
+
export const MCP_TOOL_TRAILHEAD_META_KEY = 'ontrails/trailhead';
|
|
88
94
|
|
|
89
95
|
/**
|
|
90
96
|
* Metadata key used as a compatibility hint for clients that support
|
|
@@ -107,17 +113,29 @@ export interface DeriveMcpToolsOptions extends BaseSurfaceOptions {
|
|
|
107
113
|
readonly createContext?:
|
|
108
114
|
| (() => TrailContextInit | Promise<TrailContextInit>)
|
|
109
115
|
| undefined;
|
|
110
|
-
|
|
116
|
+
/**
|
|
117
|
+
* App-authored overlay envelopes (the same collection compile embeds in
|
|
118
|
+
* `trails.lock`). The `surfaces` overlay's `mcp` bindings are the authored,
|
|
119
|
+
* lockable default: list bindings become grouped trailhead tools and scalar
|
|
120
|
+
* bindings become tool synonyms.
|
|
121
|
+
*/
|
|
122
|
+
readonly overlays?: readonly OverlayEnvelopeLike[] | undefined;
|
|
123
|
+
/**
|
|
124
|
+
* Call-site trailhead map. Override-in-context by design: when both this
|
|
125
|
+
* map and overlay `mcp` list bindings are present, the call-site map wins
|
|
126
|
+
* at runtime.
|
|
127
|
+
*/
|
|
128
|
+
readonly trailheads?: McpSurfaceTrailheadMap | undefined;
|
|
111
129
|
readonly layers?: readonly Layer[] | undefined;
|
|
112
130
|
readonly resources?: ResourceOverrideMap | undefined;
|
|
113
131
|
readonly resolvePermit?: ResolveMcpPermit | undefined;
|
|
114
132
|
}
|
|
115
133
|
|
|
116
|
-
export type
|
|
134
|
+
export type McpSurfaceTrailheadTrailSelector = string | readonly string[];
|
|
117
135
|
|
|
118
136
|
/** Surface-side grouped entry over existing trails. */
|
|
119
|
-
export interface
|
|
120
|
-
readonly trails:
|
|
137
|
+
export interface McpSurfaceTrailheadDefinition {
|
|
138
|
+
readonly trails: McpSurfaceTrailheadTrailSelector;
|
|
121
139
|
readonly description: string;
|
|
122
140
|
readonly visibility?: 'public' | 'internal' | undefined;
|
|
123
141
|
readonly descriptionStableThrough?: string | undefined;
|
|
@@ -129,8 +147,8 @@ export interface McpSurfaceFacetDefinition {
|
|
|
129
147
|
| undefined;
|
|
130
148
|
}
|
|
131
149
|
|
|
132
|
-
export type
|
|
133
|
-
Record<string,
|
|
150
|
+
export type McpSurfaceTrailheadMap = Readonly<
|
|
151
|
+
Record<string, McpSurfaceTrailheadDefinition>
|
|
134
152
|
>;
|
|
135
153
|
|
|
136
154
|
export interface ResolveMcpPermitInput {
|
|
@@ -149,7 +167,7 @@ export interface McpToolDefinition {
|
|
|
149
167
|
readonly _meta?: Record<string, unknown> | undefined;
|
|
150
168
|
readonly annotations: McpAnnotations | undefined;
|
|
151
169
|
readonly description: string | undefined;
|
|
152
|
-
readonly
|
|
170
|
+
readonly trailheadId?: string | undefined;
|
|
153
171
|
readonly handler: (
|
|
154
172
|
args: Record<string, unknown>,
|
|
155
173
|
extra: McpExtra
|
|
@@ -1025,19 +1043,19 @@ const buildToolDefinition = (
|
|
|
1025
1043
|
};
|
|
1026
1044
|
};
|
|
1027
1045
|
|
|
1028
|
-
const
|
|
1029
|
-
selector:
|
|
1046
|
+
const trailheadSelectors = (
|
|
1047
|
+
selector: McpSurfaceTrailheadTrailSelector
|
|
1030
1048
|
): readonly string[] => (typeof selector === 'string' ? [selector] : selector);
|
|
1031
1049
|
|
|
1032
|
-
const
|
|
1050
|
+
const matchesTrailheadSelector = (
|
|
1033
1051
|
trailId: string,
|
|
1034
|
-
selector:
|
|
1052
|
+
selector: McpSurfaceTrailheadTrailSelector
|
|
1035
1053
|
): boolean =>
|
|
1036
|
-
|
|
1054
|
+
trailheadSelectors(selector).some((pattern) =>
|
|
1037
1055
|
matchesTrailPattern(trailId, pattern)
|
|
1038
1056
|
);
|
|
1039
1057
|
|
|
1040
|
-
interface
|
|
1058
|
+
interface TrailheadMemberTool {
|
|
1041
1059
|
readonly tool: McpToolDefinition;
|
|
1042
1060
|
readonly trail: Trail<unknown, unknown, unknown>;
|
|
1043
1061
|
}
|
|
@@ -1045,8 +1063,8 @@ interface FacetMemberTool {
|
|
|
1045
1063
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
1046
1064
|
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1047
1065
|
|
|
1048
|
-
const
|
|
1049
|
-
members: readonly
|
|
1066
|
+
const buildTrailheadInputSchema = (
|
|
1067
|
+
members: readonly TrailheadMemberTool[]
|
|
1050
1068
|
): Record<string, unknown> => ({
|
|
1051
1069
|
anyOf: members.map(({ tool, trail }) => ({
|
|
1052
1070
|
properties: {
|
|
@@ -1067,8 +1085,8 @@ const buildFacetInputSchema = (
|
|
|
1067
1085
|
type: 'object',
|
|
1068
1086
|
});
|
|
1069
1087
|
|
|
1070
|
-
const
|
|
1071
|
-
members: readonly
|
|
1088
|
+
const buildTrailheadOutputSchema = (
|
|
1089
|
+
members: readonly TrailheadMemberTool[]
|
|
1072
1090
|
): Record<string, unknown> => {
|
|
1073
1091
|
const outputSchemas = members.map(({ tool }) => tool.outputSchema ?? {});
|
|
1074
1092
|
return {
|
|
@@ -1101,7 +1119,7 @@ const parseJsonTextContent = (
|
|
|
1101
1119
|
}
|
|
1102
1120
|
};
|
|
1103
1121
|
|
|
1104
|
-
const
|
|
1122
|
+
const wrapTrailheadResult = (
|
|
1105
1123
|
trailId: string,
|
|
1106
1124
|
result: McpToolResult
|
|
1107
1125
|
): McpToolResult => {
|
|
@@ -1121,9 +1139,9 @@ const wrapFacetResult = (
|
|
|
1121
1139
|
};
|
|
1122
1140
|
};
|
|
1123
1141
|
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1126
|
-
members: readonly
|
|
1142
|
+
const createTrailheadHandler = (
|
|
1143
|
+
trailheadId: string,
|
|
1144
|
+
members: readonly TrailheadMemberTool[]
|
|
1127
1145
|
): McpToolDefinition['handler'] => {
|
|
1128
1146
|
const byTrailId = new Map(
|
|
1129
1147
|
members.map((member) => [member.trail.id, member.tool])
|
|
@@ -1135,7 +1153,7 @@ const createFacetHandler = (
|
|
|
1135
1153
|
if (tool === undefined) {
|
|
1136
1154
|
return mcpError(
|
|
1137
1155
|
new ValidationError(
|
|
1138
|
-
`MCP
|
|
1156
|
+
`MCP trailhead "${trailheadId}" received unknown trail selector "${trailId || '(missing)'}"`
|
|
1139
1157
|
)
|
|
1140
1158
|
);
|
|
1141
1159
|
}
|
|
@@ -1144,17 +1162,17 @@ const createFacetHandler = (
|
|
|
1144
1162
|
if (!isRecord(input)) {
|
|
1145
1163
|
return mcpError(
|
|
1146
1164
|
new ValidationError(
|
|
1147
|
-
`MCP
|
|
1165
|
+
`MCP trailhead "${trailheadId}" expects an object input for trail "${trailId}"`
|
|
1148
1166
|
)
|
|
1149
1167
|
);
|
|
1150
1168
|
}
|
|
1151
1169
|
|
|
1152
|
-
return
|
|
1170
|
+
return wrapTrailheadResult(trailId, await tool.handler(input, extra));
|
|
1153
1171
|
};
|
|
1154
1172
|
};
|
|
1155
1173
|
|
|
1156
|
-
const
|
|
1157
|
-
members: readonly
|
|
1174
|
+
const deriveTrailheadIntent = (
|
|
1175
|
+
members: readonly TrailheadMemberTool[]
|
|
1158
1176
|
): Pick<Trail<unknown, unknown, unknown>, 'intent'>['intent'] => {
|
|
1159
1177
|
if (members.every(({ trail }) => trail.intent === 'read')) {
|
|
1160
1178
|
return 'read';
|
|
@@ -1165,14 +1183,14 @@ const deriveFacetIntent = (
|
|
|
1165
1183
|
return 'write';
|
|
1166
1184
|
};
|
|
1167
1185
|
|
|
1168
|
-
const
|
|
1169
|
-
definition:
|
|
1170
|
-
members: readonly
|
|
1186
|
+
const deriveTrailheadAnnotations = (
|
|
1187
|
+
definition: McpSurfaceTrailheadDefinition,
|
|
1188
|
+
members: readonly TrailheadMemberTool[]
|
|
1171
1189
|
): McpAnnotations | undefined => {
|
|
1172
1190
|
const annotations = deriveAnnotations({
|
|
1173
1191
|
description: definition.description,
|
|
1174
1192
|
idempotent: false,
|
|
1175
|
-
intent:
|
|
1193
|
+
intent: deriveTrailheadIntent(members),
|
|
1176
1194
|
} as Pick<
|
|
1177
1195
|
Trail<unknown, unknown, unknown>,
|
|
1178
1196
|
'description' | 'idempotent' | 'intent'
|
|
@@ -1180,15 +1198,15 @@ const deriveFacetAnnotations = (
|
|
|
1180
1198
|
return Object.keys(annotations).length > 0 ? annotations : undefined;
|
|
1181
1199
|
};
|
|
1182
1200
|
|
|
1183
|
-
const
|
|
1184
|
-
|
|
1185
|
-
definition:
|
|
1201
|
+
const buildTrailheadMeta = (
|
|
1202
|
+
trailheadId: string,
|
|
1203
|
+
definition: McpSurfaceTrailheadDefinition,
|
|
1186
1204
|
memberTrailIds: readonly string[]
|
|
1187
1205
|
): Record<string, unknown> | undefined =>
|
|
1188
1206
|
mergeMeta(
|
|
1189
1207
|
{
|
|
1190
|
-
[
|
|
1191
|
-
id:
|
|
1208
|
+
[MCP_TOOL_TRAILHEAD_META_KEY]: {
|
|
1209
|
+
id: trailheadId,
|
|
1192
1210
|
memberTrailIds,
|
|
1193
1211
|
},
|
|
1194
1212
|
},
|
|
@@ -1197,23 +1215,23 @@ const buildFacetMeta = (
|
|
|
1197
1215
|
: undefined
|
|
1198
1216
|
);
|
|
1199
1217
|
|
|
1200
|
-
const
|
|
1218
|
+
const buildTrailheadToolDefinition = (
|
|
1201
1219
|
graph: Topo,
|
|
1202
|
-
|
|
1203
|
-
definition:
|
|
1204
|
-
members: readonly
|
|
1220
|
+
trailheadId: string,
|
|
1221
|
+
definition: McpSurfaceTrailheadDefinition,
|
|
1222
|
+
members: readonly TrailheadMemberTool[]
|
|
1205
1223
|
): McpToolDefinition => {
|
|
1206
1224
|
const memberTrailIds = members.map(({ trail }) => trail.id);
|
|
1207
1225
|
return {
|
|
1208
|
-
_meta:
|
|
1209
|
-
annotations:
|
|
1226
|
+
_meta: buildTrailheadMeta(trailheadId, definition, memberTrailIds),
|
|
1227
|
+
annotations: deriveTrailheadAnnotations(definition, members),
|
|
1210
1228
|
description: definition.description,
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
inputSchema: buildFacetInputSchema(members),
|
|
1229
|
+
handler: createTrailheadHandler(trailheadId, members),
|
|
1230
|
+
inputSchema: buildTrailheadInputSchema(members),
|
|
1214
1231
|
memberTrailIds,
|
|
1215
|
-
name: deriveToolName(graph.name,
|
|
1216
|
-
outputSchema:
|
|
1232
|
+
name: deriveToolName(graph.name, trailheadId),
|
|
1233
|
+
outputSchema: buildTrailheadOutputSchema(members),
|
|
1234
|
+
trailheadId,
|
|
1217
1235
|
};
|
|
1218
1236
|
};
|
|
1219
1237
|
|
|
@@ -1256,71 +1274,135 @@ const validateToolBuild = (
|
|
|
1256
1274
|
options: DeriveMcpToolsOptions
|
|
1257
1275
|
): Result<void, Error> => validateSurfaceTopo(graph, options);
|
|
1258
1276
|
|
|
1259
|
-
const
|
|
1277
|
+
const collectTrailheadMembers = (
|
|
1260
1278
|
graph: Topo,
|
|
1261
|
-
definition:
|
|
1279
|
+
definition: McpSurfaceTrailheadDefinition,
|
|
1262
1280
|
availableTrails: readonly Trail<unknown, unknown, unknown>[],
|
|
1263
1281
|
layers: readonly Layer[],
|
|
1264
1282
|
options: DeriveMcpToolsOptions
|
|
1265
|
-
): readonly
|
|
1283
|
+
): readonly TrailheadMemberTool[] =>
|
|
1266
1284
|
availableTrails
|
|
1267
1285
|
.filter((trailItem) =>
|
|
1268
|
-
|
|
1286
|
+
matchesTrailheadSelector(trailItem.id, definition.trails)
|
|
1269
1287
|
)
|
|
1270
1288
|
.map((trailItem) => ({
|
|
1271
1289
|
tool: buildToolDefinition(graph, trailItem, layers, options),
|
|
1272
1290
|
trail: trailItem,
|
|
1273
1291
|
}));
|
|
1274
1292
|
|
|
1275
|
-
const
|
|
1293
|
+
const registerTrailhead = (
|
|
1276
1294
|
graph: Topo,
|
|
1277
|
-
|
|
1278
|
-
definition:
|
|
1279
|
-
members: readonly
|
|
1295
|
+
trailheadId: string,
|
|
1296
|
+
definition: McpSurfaceTrailheadDefinition,
|
|
1297
|
+
members: readonly TrailheadMemberTool[],
|
|
1280
1298
|
nameToSourceId: Map<string, string>,
|
|
1281
1299
|
tools: McpToolDefinition[]
|
|
1282
1300
|
): Result<void, Error> => {
|
|
1283
1301
|
if (members.length === 0) {
|
|
1284
1302
|
return Result.err(
|
|
1285
1303
|
new ValidationError(
|
|
1286
|
-
`MCP
|
|
1304
|
+
`MCP trailhead "${trailheadId}" did not match any surface-eligible trails`
|
|
1287
1305
|
)
|
|
1288
1306
|
);
|
|
1289
1307
|
}
|
|
1290
1308
|
|
|
1291
|
-
const toolName = deriveToolName(graph.name,
|
|
1309
|
+
const toolName = deriveToolName(graph.name, trailheadId);
|
|
1292
1310
|
const existingId = nameToSourceId.get(toolName);
|
|
1293
1311
|
if (existingId !== undefined) {
|
|
1294
1312
|
return Result.err(
|
|
1295
1313
|
new ValidationError(
|
|
1296
|
-
`MCP tool-name collision: "${existingId}" and "
|
|
1314
|
+
`MCP tool-name collision: "${existingId}" and "trailhead:${trailheadId}" both derive the tool name "${toolName}"`
|
|
1297
1315
|
)
|
|
1298
1316
|
);
|
|
1299
1317
|
}
|
|
1300
1318
|
|
|
1301
|
-
nameToSourceId.set(toolName, `
|
|
1302
|
-
tools.push(
|
|
1319
|
+
nameToSourceId.set(toolName, `trailhead:${trailheadId}`);
|
|
1320
|
+
tools.push(
|
|
1321
|
+
buildTrailheadToolDefinition(graph, trailheadId, definition, members)
|
|
1322
|
+
);
|
|
1303
1323
|
return Result.ok();
|
|
1304
1324
|
};
|
|
1305
1325
|
|
|
1306
|
-
|
|
1326
|
+
/**
|
|
1327
|
+
* Resolve the `surfaces` overlay's `mcp` bindings against the
|
|
1328
|
+
* surface-eligible trails.
|
|
1329
|
+
*
|
|
1330
|
+
* Framework overlay/binding validation failures are represented as
|
|
1331
|
+
* `Result.err` so `deriveMcpTools` keeps its no-throw contract.
|
|
1332
|
+
*/
|
|
1333
|
+
const resolveOverlayBindingExpansion = (
|
|
1334
|
+
options: DeriveMcpToolsOptions,
|
|
1335
|
+
availableTrails: readonly Trail<unknown, unknown, unknown>[]
|
|
1336
|
+
): Result<McpSurfaceBindingExpansion | undefined, Error> => {
|
|
1337
|
+
try {
|
|
1338
|
+
const bindings = resolveSurfaceOverlayBindings(options.overlays);
|
|
1339
|
+
return Result.ok(
|
|
1340
|
+
expandMcpSurfaceBindings(
|
|
1341
|
+
bindings?.mcp,
|
|
1342
|
+
availableTrails.map((trailItem) => trailItem.id)
|
|
1343
|
+
)
|
|
1344
|
+
);
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
return Result.err(
|
|
1347
|
+
error instanceof Error
|
|
1348
|
+
? error
|
|
1349
|
+
: new InternalError(
|
|
1350
|
+
`MCP surface overlay resolution failed: ${String(error)}`
|
|
1351
|
+
)
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* Construct call-site-equivalent trailhead definitions from the overlay's
|
|
1358
|
+
* `mcp` list bindings.
|
|
1359
|
+
*
|
|
1360
|
+
* Each grouped binding becomes one definition with the expanded member trail
|
|
1361
|
+
* ids as exact selectors and the shared derived default description, so the
|
|
1362
|
+
* existing trailhead machinery builds the tool exactly as a call-site map
|
|
1363
|
+
* would.
|
|
1364
|
+
*/
|
|
1365
|
+
const trailheadDefinitionsFromOverlay = (
|
|
1366
|
+
expansion: McpSurfaceBindingExpansion | undefined
|
|
1367
|
+
): McpSurfaceTrailheadMap | undefined => {
|
|
1368
|
+
if (expansion === undefined) {
|
|
1369
|
+
return undefined;
|
|
1370
|
+
}
|
|
1371
|
+
const groups = Object.entries(expansion.groups);
|
|
1372
|
+
if (groups.length === 0) {
|
|
1373
|
+
return undefined;
|
|
1374
|
+
}
|
|
1375
|
+
return Object.fromEntries(
|
|
1376
|
+
groups.map(([name, memberIds]) => [
|
|
1377
|
+
name,
|
|
1378
|
+
{
|
|
1379
|
+
description: deriveMcpTrailheadDescription(memberIds),
|
|
1380
|
+
trails: memberIds,
|
|
1381
|
+
},
|
|
1382
|
+
])
|
|
1383
|
+
);
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
const registerTrailheads = (
|
|
1307
1387
|
graph: Topo,
|
|
1388
|
+
trailheads: McpSurfaceTrailheadMap | undefined,
|
|
1308
1389
|
options: DeriveMcpToolsOptions,
|
|
1309
1390
|
layers: readonly Layer[],
|
|
1310
1391
|
availableTrails: readonly Trail<unknown, unknown, unknown>[],
|
|
1311
1392
|
nameToSourceId: Map<string, string>,
|
|
1312
1393
|
tools: McpToolDefinition[]
|
|
1313
1394
|
): Result<ReadonlySet<string>, Error> => {
|
|
1314
|
-
const { facets } = options;
|
|
1315
1395
|
const consumedTrailIds = new Set<string>();
|
|
1316
1396
|
const ownerByTrailId = new Map<string, string>();
|
|
1317
1397
|
|
|
1318
|
-
if (
|
|
1398
|
+
if (trailheads === undefined || Object.keys(trailheads).length === 0) {
|
|
1319
1399
|
return Result.ok(consumedTrailIds);
|
|
1320
1400
|
}
|
|
1321
1401
|
|
|
1322
|
-
for (const [
|
|
1323
|
-
|
|
1402
|
+
for (const [trailheadId, definition] of Object.entries(
|
|
1403
|
+
trailheads
|
|
1404
|
+
).toSorted()) {
|
|
1405
|
+
const members = collectTrailheadMembers(
|
|
1324
1406
|
graph,
|
|
1325
1407
|
definition,
|
|
1326
1408
|
availableTrails,
|
|
@@ -1332,17 +1414,17 @@ const registerFacets = (
|
|
|
1332
1414
|
if (previous !== undefined) {
|
|
1333
1415
|
return Result.err(
|
|
1334
1416
|
new ValidationError(
|
|
1335
|
-
`MCP
|
|
1417
|
+
`MCP trailhead overlap: trail "${memberTrail.id}" is selected by trailheads "${previous}" and "${trailheadId}"`
|
|
1336
1418
|
)
|
|
1337
1419
|
);
|
|
1338
1420
|
}
|
|
1339
|
-
ownerByTrailId.set(memberTrail.id,
|
|
1421
|
+
ownerByTrailId.set(memberTrail.id, trailheadId);
|
|
1340
1422
|
consumedTrailIds.add(memberTrail.id);
|
|
1341
1423
|
}
|
|
1342
1424
|
|
|
1343
|
-
const registered =
|
|
1425
|
+
const registered = registerTrailhead(
|
|
1344
1426
|
graph,
|
|
1345
|
-
|
|
1427
|
+
trailheadId,
|
|
1346
1428
|
definition,
|
|
1347
1429
|
members,
|
|
1348
1430
|
nameToSourceId,
|
|
@@ -1356,6 +1438,49 @@ const registerFacets = (
|
|
|
1356
1438
|
return Result.ok(consumedTrailIds);
|
|
1357
1439
|
};
|
|
1358
1440
|
|
|
1441
|
+
/**
|
|
1442
|
+
* Register overlay tool synonyms: additional MCP tools whose names are the
|
|
1443
|
+
* scalar binding names, sharing the target trail's schema, annotations, and
|
|
1444
|
+
* handler.
|
|
1445
|
+
*/
|
|
1446
|
+
const registerSynonymTools = (
|
|
1447
|
+
graph: Topo,
|
|
1448
|
+
options: DeriveMcpToolsOptions,
|
|
1449
|
+
layers: readonly Layer[],
|
|
1450
|
+
availableTrails: readonly Trail<unknown, unknown, unknown>[],
|
|
1451
|
+
synonyms: Readonly<Record<string, string>>,
|
|
1452
|
+
nameToSourceId: Map<string, string>,
|
|
1453
|
+
tools: McpToolDefinition[]
|
|
1454
|
+
): Result<void, Error> => {
|
|
1455
|
+
const trailById = new Map(
|
|
1456
|
+
availableTrails.map((trailItem) => [trailItem.id, trailItem])
|
|
1457
|
+
);
|
|
1458
|
+
for (const [name, trailId] of Object.entries(synonyms)) {
|
|
1459
|
+
const trailItem = trailById.get(trailId);
|
|
1460
|
+
if (trailItem === undefined) {
|
|
1461
|
+
return Result.err(
|
|
1462
|
+
new ValidationError(
|
|
1463
|
+
`MCP overlay binding "${name}" targets trail "${trailId}", which is not surface-eligible`
|
|
1464
|
+
)
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
const existingId = nameToSourceId.get(name);
|
|
1468
|
+
if (existingId !== undefined) {
|
|
1469
|
+
return Result.err(
|
|
1470
|
+
new ValidationError(
|
|
1471
|
+
`MCP tool-name collision: "${existingId}" and "binding:${name}" both use the tool name "${name}"`
|
|
1472
|
+
)
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
nameToSourceId.set(name, `binding:${name}`);
|
|
1476
|
+
tools.push({
|
|
1477
|
+
...buildToolDefinition(graph, trailItem, layers, options),
|
|
1478
|
+
name,
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
return Result.ok();
|
|
1482
|
+
};
|
|
1483
|
+
|
|
1359
1484
|
const registerTools = (
|
|
1360
1485
|
graph: Topo,
|
|
1361
1486
|
options: DeriveMcpToolsOptions,
|
|
@@ -1364,18 +1489,27 @@ const registerTools = (
|
|
|
1364
1489
|
const tools: McpToolDefinition[] = [];
|
|
1365
1490
|
const nameToSourceId = new Map<string, string>();
|
|
1366
1491
|
const availableTrails = eligibleTrails(graph, options);
|
|
1367
|
-
const
|
|
1492
|
+
const expansion = resolveOverlayBindingExpansion(options, availableTrails);
|
|
1493
|
+
if (expansion.isErr()) {
|
|
1494
|
+
return expansion;
|
|
1495
|
+
}
|
|
1496
|
+
// Override-in-context: the call-site trailhead map wins over the authored
|
|
1497
|
+
// overlay default whenever the caller supplies one.
|
|
1498
|
+
const trailheads =
|
|
1499
|
+
options.trailheads ?? trailheadDefinitionsFromOverlay(expansion.value);
|
|
1500
|
+
const registeredTrailheads = registerTrailheads(
|
|
1368
1501
|
graph,
|
|
1502
|
+
trailheads,
|
|
1369
1503
|
options,
|
|
1370
1504
|
layers,
|
|
1371
1505
|
availableTrails,
|
|
1372
1506
|
nameToSourceId,
|
|
1373
1507
|
tools
|
|
1374
1508
|
);
|
|
1375
|
-
if (
|
|
1376
|
-
return
|
|
1509
|
+
if (registeredTrailheads.isErr()) {
|
|
1510
|
+
return registeredTrailheads;
|
|
1377
1511
|
}
|
|
1378
|
-
const consumedTrailIds =
|
|
1512
|
+
const consumedTrailIds = registeredTrailheads.value;
|
|
1379
1513
|
|
|
1380
1514
|
for (const trailItem of availableTrails) {
|
|
1381
1515
|
if (consumedTrailIds.has(trailItem.id)) {
|
|
@@ -1394,6 +1528,19 @@ const registerTools = (
|
|
|
1394
1528
|
}
|
|
1395
1529
|
}
|
|
1396
1530
|
|
|
1531
|
+
const registeredSynonyms = registerSynonymTools(
|
|
1532
|
+
graph,
|
|
1533
|
+
options,
|
|
1534
|
+
layers,
|
|
1535
|
+
availableTrails,
|
|
1536
|
+
expansion.value?.synonyms ?? {},
|
|
1537
|
+
nameToSourceId,
|
|
1538
|
+
tools
|
|
1539
|
+
);
|
|
1540
|
+
if (registeredSynonyms.isErr()) {
|
|
1541
|
+
return registeredSynonyms;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1397
1544
|
return Result.ok(tools);
|
|
1398
1545
|
};
|
|
1399
1546
|
|
package/src/index.ts
CHANGED
|
@@ -3,12 +3,12 @@ export {
|
|
|
3
3
|
MCP_TOOL_ERROR_META_KEY,
|
|
4
4
|
MCP_TOOL_EXAMPLES_META_KEY,
|
|
5
5
|
MCP_TOOL_DEFERRED_META_KEY,
|
|
6
|
-
|
|
6
|
+
MCP_TOOL_TRAILHEAD_META_KEY,
|
|
7
7
|
deriveMcpTools,
|
|
8
8
|
type DeriveMcpToolsOptions,
|
|
9
|
-
type
|
|
10
|
-
type
|
|
11
|
-
type
|
|
9
|
+
type McpSurfaceTrailheadDefinition,
|
|
10
|
+
type McpSurfaceTrailheadMap,
|
|
11
|
+
type McpSurfaceTrailheadTrailSelector,
|
|
12
12
|
type McpToolDefinition,
|
|
13
13
|
type McpToolResult,
|
|
14
14
|
type McpToolErrorMeta,
|
|
@@ -24,7 +24,7 @@ export {
|
|
|
24
24
|
MCP_SURFACE_MAP_RESOURCE_URI,
|
|
25
25
|
MCP_TRAIL_RESOURCE_PREFIX,
|
|
26
26
|
buildMcpResources,
|
|
27
|
-
|
|
27
|
+
isMcpTrailheadTool,
|
|
28
28
|
type BuiltMcpResources,
|
|
29
29
|
type McpResourceContent,
|
|
30
30
|
type McpResourceDefinition,
|
package/src/resources.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { Topo, Trail } from '@ontrails/core';
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
MCP_TOOL_DEFERRED_META_KEY,
|
|
10
|
-
|
|
10
|
+
MCP_TOOL_TRAILHEAD_META_KEY,
|
|
11
11
|
} from './build.js';
|
|
12
12
|
import type { McpToolDefinition } from './build.js';
|
|
13
13
|
|
|
@@ -74,7 +74,7 @@ export interface BuiltMcpResources {
|
|
|
74
74
|
interface McpSurfaceMapTool {
|
|
75
75
|
readonly annotations: McpToolDefinition['annotations'];
|
|
76
76
|
readonly description: string | undefined;
|
|
77
|
-
readonly
|
|
77
|
+
readonly trailheadId?: string | undefined;
|
|
78
78
|
readonly inputSchema: Record<string, unknown>;
|
|
79
79
|
readonly memberTrailIds?: readonly string[] | undefined;
|
|
80
80
|
readonly name: string;
|
|
@@ -112,7 +112,7 @@ const projectSurfaceMapTool = (tool: McpToolDefinition): McpSurfaceMapTool => ({
|
|
|
112
112
|
description: tool.description,
|
|
113
113
|
inputSchema: tool.inputSchema,
|
|
114
114
|
name: tool.name,
|
|
115
|
-
...(tool.
|
|
115
|
+
...(tool.trailheadId === undefined ? {} : { trailheadId: tool.trailheadId }),
|
|
116
116
|
...(tool.memberTrailIds === undefined
|
|
117
117
|
? {}
|
|
118
118
|
: { memberTrailIds: tool.memberTrailIds }),
|
|
@@ -323,14 +323,14 @@ export const buildMcpResources = (
|
|
|
323
323
|
};
|
|
324
324
|
|
|
325
325
|
/**
|
|
326
|
-
* Return whether an MCP tool was projected from a surface
|
|
326
|
+
* Return whether an MCP tool was projected from a surface trailhead.
|
|
327
327
|
*
|
|
328
328
|
* @example
|
|
329
329
|
* ```ts
|
|
330
|
-
* import {
|
|
330
|
+
* import { isMcpTrailheadTool } from '@ontrails/mcp';
|
|
331
331
|
*
|
|
332
|
-
* const
|
|
332
|
+
* const trailheadTools = tools.filter(isMcpTrailheadTool);
|
|
333
333
|
* ```
|
|
334
334
|
*/
|
|
335
|
-
export const
|
|
336
|
-
tool._meta?.[
|
|
335
|
+
export const isMcpTrailheadTool = (tool: McpToolDefinition): boolean =>
|
|
336
|
+
tool._meta?.[MCP_TOOL_TRAILHEAD_META_KEY] !== undefined;
|
package/src/surface.ts
CHANGED
|
@@ -14,13 +14,14 @@ import {
|
|
|
14
14
|
import type {
|
|
15
15
|
BaseSurfaceOptions,
|
|
16
16
|
Layer,
|
|
17
|
+
OverlayEnvelopeLike,
|
|
17
18
|
ResourceOverrideMap,
|
|
18
19
|
Topo,
|
|
19
20
|
TrailContextInit,
|
|
20
21
|
} from '@ontrails/core';
|
|
21
22
|
|
|
22
23
|
import type {
|
|
23
|
-
|
|
24
|
+
McpSurfaceTrailheadMap,
|
|
24
25
|
McpToolDefinition,
|
|
25
26
|
ResolveMcpPermit,
|
|
26
27
|
} from './build.js';
|
|
@@ -38,7 +39,19 @@ export interface CreateServerOptions extends BaseSurfaceOptions {
|
|
|
38
39
|
| (() => TrailContextInit | Promise<TrailContextInit>)
|
|
39
40
|
| undefined;
|
|
40
41
|
readonly description?: string | undefined;
|
|
41
|
-
|
|
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;
|
|
42
55
|
readonly layers?: readonly Layer[] | undefined;
|
|
43
56
|
readonly mcpResources?: McpResourcesConfig | false | undefined;
|
|
44
57
|
readonly name?: string | undefined;
|
|
@@ -220,12 +233,13 @@ export const createServer = (
|
|
|
220
233
|
configValues: options.configValues,
|
|
221
234
|
createContext: options.createContext,
|
|
222
235
|
exclude: options.exclude,
|
|
223
|
-
facets: options.facets,
|
|
224
236
|
include: options.include,
|
|
225
237
|
intent: options.intent,
|
|
226
238
|
layers: options.layers,
|
|
239
|
+
overlays: options.overlays,
|
|
227
240
|
resolvePermit: options.resolvePermit,
|
|
228
241
|
resources: options.resources,
|
|
242
|
+
trailheads: options.trailheads,
|
|
229
243
|
validate: options.validate,
|
|
230
244
|
});
|
|
231
245
|
|
package/src/tool-name.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Derive MCP-safe tool names from app name + trail ID.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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 projection the MCP surface renders.
|
|
6
7
|
*/
|
|
7
8
|
|
|
9
|
+
import { deriveMcpToolName } from '@ontrails/core';
|
|
10
|
+
|
|
8
11
|
/**
|
|
9
12
|
* Convert app name + trail ID to an MCP-safe tool name.
|
|
10
13
|
*
|
|
@@ -12,8 +15,4 @@
|
|
|
12
15
|
* deriveToolName("myapp", "entity.show") // "myapp_entity_show"
|
|
13
16
|
* deriveToolName("dispatch", "patch.search") // "dispatch_patch_search"
|
|
14
17
|
*/
|
|
15
|
-
export const deriveToolName =
|
|
16
|
-
const prefix = appName.toLowerCase().replaceAll(/[.-]/g, '_');
|
|
17
|
-
const suffix = trailId.toLowerCase().replaceAll(/[.-]/g, '_');
|
|
18
|
-
return `${prefix}_${suffix}`;
|
|
19
|
-
};
|
|
18
|
+
export const deriveToolName = deriveMcpToolName;
|