@zigrivers/surface-mcp 0.1.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/LICENSE +22 -0
- package/README.md +15 -0
- package/dist/index.d.ts +136 -0
- package/dist/index.js +1030 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1030 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { pathToFileURL } from "url";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_COMPOSITION_STAGE_IDS,
|
|
5
|
+
DEFAULT_SURFACE_CONFIG,
|
|
6
|
+
createBoundedAlternatives,
|
|
7
|
+
createTrackedFinding,
|
|
8
|
+
createSurfaceComposition,
|
|
9
|
+
createSurfaceError,
|
|
10
|
+
diffTrackedFindings,
|
|
11
|
+
err,
|
|
12
|
+
instantiateLensExecutionPlan,
|
|
13
|
+
isOk,
|
|
14
|
+
ok,
|
|
15
|
+
scoreFinding,
|
|
16
|
+
selectLensExecutionPlan,
|
|
17
|
+
synthesizeBacklog,
|
|
18
|
+
toMcpError,
|
|
19
|
+
transitionTrackedFinding
|
|
20
|
+
} from "@zigrivers/surface-core";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
var SURFACE_MCP_SERVER_NAME = "surface";
|
|
23
|
+
var SURFACE_MCP_SERVER_VERSION = "1.0.0";
|
|
24
|
+
var SURFACE_MCP_TOOL_SCHEMA_VERSION = "1.0.0";
|
|
25
|
+
var TOOL_ORDER = [
|
|
26
|
+
"surface_capture",
|
|
27
|
+
"surface_audit",
|
|
28
|
+
"surface_explain",
|
|
29
|
+
"surface_backlog",
|
|
30
|
+
"surface_gate",
|
|
31
|
+
"surface_validate",
|
|
32
|
+
"surface_baseline",
|
|
33
|
+
"surface_verdict",
|
|
34
|
+
"surface_diff",
|
|
35
|
+
"surface_alternatives",
|
|
36
|
+
"surface_trace",
|
|
37
|
+
"surface_run",
|
|
38
|
+
"surface_next",
|
|
39
|
+
"surface_status"
|
|
40
|
+
];
|
|
41
|
+
var TargetSchema = z.object({
|
|
42
|
+
kind: z.enum(["url", "localhost", "route", "screenshot", "component", "dom"]),
|
|
43
|
+
ref: z.string().min(1),
|
|
44
|
+
theme: z.enum(["light", "dark"]).optional(),
|
|
45
|
+
viewport: z.object({
|
|
46
|
+
height: z.number().int().positive(),
|
|
47
|
+
label: z.enum(["mobile", "tablet", "desktop"]),
|
|
48
|
+
width: z.number().int().positive()
|
|
49
|
+
}).strict().optional()
|
|
50
|
+
}).strict();
|
|
51
|
+
var AuthStateRefSchema = z.string().min(1);
|
|
52
|
+
var RunRefSchema = z.object({ runId: z.string().min(1) }).strict();
|
|
53
|
+
var GatePolicyInputSchema = z.record(z.string(), z.unknown());
|
|
54
|
+
var TOOL_INPUT_SCHEMAS = {
|
|
55
|
+
surface_capture: z.object({
|
|
56
|
+
authState: AuthStateRefSchema.optional(),
|
|
57
|
+
target: TargetSchema
|
|
58
|
+
}).strict(),
|
|
59
|
+
surface_audit: z.object({
|
|
60
|
+
authState: AuthStateRefSchema.optional(),
|
|
61
|
+
depth: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4), z.literal(5)]).optional(),
|
|
62
|
+
persona: z.string().min(1).optional(),
|
|
63
|
+
preset: z.string().min(1).optional(),
|
|
64
|
+
target: TargetSchema,
|
|
65
|
+
task: z.string().min(1).optional()
|
|
66
|
+
}).strict(),
|
|
67
|
+
surface_explain: z.object({ findingId: z.string().min(1) }).strict(),
|
|
68
|
+
surface_backlog: z.object({
|
|
69
|
+
exportTarget: z.string().min(1).optional(),
|
|
70
|
+
runId: z.string().min(1).optional()
|
|
71
|
+
}).strict(),
|
|
72
|
+
surface_gate: z.object({
|
|
73
|
+
policy: GatePolicyInputSchema.optional(),
|
|
74
|
+
runId: z.string().min(1).optional()
|
|
75
|
+
}).strict(),
|
|
76
|
+
surface_validate: z.object({ runId: z.string().min(1) }).strict(),
|
|
77
|
+
surface_baseline: z.object({ reason: z.string().min(1).optional() }).strict(),
|
|
78
|
+
surface_verdict: z.object({
|
|
79
|
+
decision: z.enum(["accept", "reject", "correct", "defer"]),
|
|
80
|
+
findingId: z.string().min(1),
|
|
81
|
+
rationale: z.string().min(1)
|
|
82
|
+
}).strict(),
|
|
83
|
+
surface_diff: z.object({ after: RunRefSchema, before: RunRefSchema }).strict(),
|
|
84
|
+
surface_alternatives: z.object({
|
|
85
|
+
authState: AuthStateRefSchema.optional(),
|
|
86
|
+
target: TargetSchema
|
|
87
|
+
}).strict(),
|
|
88
|
+
surface_trace: z.object({ findingId: z.string().min(1) }).strict(),
|
|
89
|
+
surface_run: z.object({
|
|
90
|
+
step: z.string().min(1),
|
|
91
|
+
target: TargetSchema.optional()
|
|
92
|
+
}).strict(),
|
|
93
|
+
surface_next: z.object({}).strict(),
|
|
94
|
+
surface_status: z.object({}).strict()
|
|
95
|
+
};
|
|
96
|
+
var TOOL_METADATA = {
|
|
97
|
+
surface_capture: {
|
|
98
|
+
title: "Capture Target",
|
|
99
|
+
description: "Capture a target into Surface artifacts."
|
|
100
|
+
},
|
|
101
|
+
surface_audit: {
|
|
102
|
+
title: "Audit Target",
|
|
103
|
+
description: "Run a Surface audit over a target."
|
|
104
|
+
},
|
|
105
|
+
surface_explain: {
|
|
106
|
+
title: "Explain Finding",
|
|
107
|
+
description: "Explain one Surface finding with evidence."
|
|
108
|
+
},
|
|
109
|
+
surface_backlog: {
|
|
110
|
+
title: "Read Backlog",
|
|
111
|
+
description: "Return or export the implementation backlog."
|
|
112
|
+
},
|
|
113
|
+
surface_gate: {
|
|
114
|
+
title: "Evaluate Gate",
|
|
115
|
+
description: "Evaluate the configured Surface quality gate."
|
|
116
|
+
},
|
|
117
|
+
surface_validate: {
|
|
118
|
+
title: "Validate Run",
|
|
119
|
+
description: "Run validation checks for an audit run."
|
|
120
|
+
},
|
|
121
|
+
surface_baseline: {
|
|
122
|
+
title: "Create Baseline",
|
|
123
|
+
description: "Baseline current findings for future gate comparisons."
|
|
124
|
+
},
|
|
125
|
+
surface_verdict: {
|
|
126
|
+
title: "Record Verdict",
|
|
127
|
+
description: "Record a human verdict for a finding."
|
|
128
|
+
},
|
|
129
|
+
surface_diff: {
|
|
130
|
+
title: "Diff Runs",
|
|
131
|
+
description: "Compare findings across two audit runs."
|
|
132
|
+
},
|
|
133
|
+
surface_alternatives: {
|
|
134
|
+
title: "Suggest Alternatives",
|
|
135
|
+
description: "Suggest bounded alternatives for a target."
|
|
136
|
+
},
|
|
137
|
+
surface_trace: {
|
|
138
|
+
title: "Trace Finding",
|
|
139
|
+
description: "Trace a finding through closed-loop state."
|
|
140
|
+
},
|
|
141
|
+
surface_run: {
|
|
142
|
+
title: "Run Pipeline Step",
|
|
143
|
+
description: "Run a Surface pipeline step."
|
|
144
|
+
},
|
|
145
|
+
surface_next: {
|
|
146
|
+
title: "List Next Steps",
|
|
147
|
+
description: "List eligible Surface pipeline steps."
|
|
148
|
+
},
|
|
149
|
+
surface_status: {
|
|
150
|
+
title: "Read Status",
|
|
151
|
+
description: "Read Surface project status."
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
var INTERNAL_TOOLS = TOOL_ORDER.map((name) => {
|
|
155
|
+
const metadata = TOOL_METADATA[name];
|
|
156
|
+
const inputZodSchema = TOOL_INPUT_SCHEMAS[name];
|
|
157
|
+
return {
|
|
158
|
+
name,
|
|
159
|
+
...metadata,
|
|
160
|
+
inputZodSchema,
|
|
161
|
+
inputSchema: z.toJSONSchema(inputZodSchema),
|
|
162
|
+
schemaVersion: SURFACE_MCP_TOOL_SCHEMA_VERSION
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
function createSurfaceMcpToolRegistry() {
|
|
166
|
+
const toolsByName = new Map(
|
|
167
|
+
INTERNAL_TOOLS.map((tool) => [tool.name, tool])
|
|
168
|
+
);
|
|
169
|
+
return {
|
|
170
|
+
serverInfo: {
|
|
171
|
+
name: SURFACE_MCP_SERVER_NAME,
|
|
172
|
+
version: SURFACE_MCP_SERVER_VERSION
|
|
173
|
+
},
|
|
174
|
+
getTool: (name) => {
|
|
175
|
+
const tool = toolsByName.get(name);
|
|
176
|
+
return tool === void 0 ? void 0 : publicToolDefinition(tool);
|
|
177
|
+
},
|
|
178
|
+
listTools: () => INTERNAL_TOOLS.map(publicToolDefinition)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function createSurfaceMcpServer(options = {}) {
|
|
182
|
+
const registry = createSurfaceMcpToolRegistry();
|
|
183
|
+
const composition = options.composition ?? createSurfaceComposition(options);
|
|
184
|
+
const session = createSurfaceMcpSessionState();
|
|
185
|
+
return {
|
|
186
|
+
composition,
|
|
187
|
+
callTool: async (name, input) => {
|
|
188
|
+
const hydrated = await hydrateSurfaceMcpSession(composition, session);
|
|
189
|
+
if (!hydrated.ok) {
|
|
190
|
+
return hydrated;
|
|
191
|
+
}
|
|
192
|
+
return await callSurfaceMcpTool({
|
|
193
|
+
composition,
|
|
194
|
+
input,
|
|
195
|
+
name,
|
|
196
|
+
session
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
registry,
|
|
200
|
+
listTools: () => registry.listTools()
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function createMcpToolSchemaSnapshot(registry = createSurfaceMcpToolRegistry()) {
|
|
204
|
+
return registry.listTools().map((tool) => ({
|
|
205
|
+
inputSchema: tool.inputSchema,
|
|
206
|
+
name: tool.name,
|
|
207
|
+
schemaVersion: tool.schemaVersion
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
function assertMcpToolSchemaCompatibility(input) {
|
|
211
|
+
if (input.current.name !== input.next.name) {
|
|
212
|
+
return mcpSchemaIncompatible(
|
|
213
|
+
"MCP tool names cannot change without a major version bump.",
|
|
214
|
+
input
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
if (!hasMajorVersionIncrement(input.current.schemaVersion, input.next.schemaVersion)) {
|
|
218
|
+
const currentRequired = requiredFields(input.current.inputSchema);
|
|
219
|
+
const nextRequired = requiredFields(input.next.inputSchema);
|
|
220
|
+
const addedRequired = [...nextRequired].filter((field) => !currentRequired.has(field));
|
|
221
|
+
if (addedRequired.length > 0) {
|
|
222
|
+
return mcpSchemaIncompatible(
|
|
223
|
+
"MCP tool schema added required fields without a major version bump.",
|
|
224
|
+
input,
|
|
225
|
+
{ addedRequired }
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return ok(true);
|
|
230
|
+
}
|
|
231
|
+
function createSurfaceMcpSessionState() {
|
|
232
|
+
return {
|
|
233
|
+
baselines: /* @__PURE__ */ new Map(),
|
|
234
|
+
baselineOrder: [],
|
|
235
|
+
runs: /* @__PURE__ */ new Map(),
|
|
236
|
+
runOrder: [],
|
|
237
|
+
trackedByIdentity: /* @__PURE__ */ new Map(),
|
|
238
|
+
verdicts: /* @__PURE__ */ new Map(),
|
|
239
|
+
nextBaselineSequence: 1,
|
|
240
|
+
nextRunSequence: 1
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
async function hydrateSurfaceMcpSession(composition, session) {
|
|
244
|
+
const state = await composition.stateStore.readState();
|
|
245
|
+
if (!state.ok) {
|
|
246
|
+
return state;
|
|
247
|
+
}
|
|
248
|
+
resetSurfaceMcpSessionFromState(session, state.value);
|
|
249
|
+
return ok(void 0);
|
|
250
|
+
}
|
|
251
|
+
function resetSurfaceMcpSessionFromState(session, state) {
|
|
252
|
+
session.baselines.clear();
|
|
253
|
+
session.baselineOrder.length = 0;
|
|
254
|
+
session.runs.clear();
|
|
255
|
+
session.runOrder.length = 0;
|
|
256
|
+
session.trackedByIdentity.clear();
|
|
257
|
+
session.verdicts.clear();
|
|
258
|
+
for (const trackedFinding of state.trackedFindings ?? []) {
|
|
259
|
+
session.trackedByIdentity.set(trackedFinding.identityKey, trackedFinding);
|
|
260
|
+
}
|
|
261
|
+
for (const record of state.runRecords ?? []) {
|
|
262
|
+
const mcpRecord = surfaceMcpRunRecordFromProjectRecord(record);
|
|
263
|
+
if (mcpRecord === void 0) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
session.runs.set(mcpRecord.runId, mcpRecord);
|
|
267
|
+
session.runOrder.push(mcpRecord.runId);
|
|
268
|
+
for (const trackedFinding of mcpRecord.trackedFindings) {
|
|
269
|
+
session.trackedByIdentity.set(trackedFinding.identityKey, trackedFinding);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
for (const baseline of state.baselines ?? []) {
|
|
273
|
+
const mcpBaseline = surfaceMcpBaselineFromProjectBaseline(baseline);
|
|
274
|
+
session.baselines.set(mcpBaseline.baselineId, mcpBaseline);
|
|
275
|
+
session.baselineOrder.push(mcpBaseline.baselineId);
|
|
276
|
+
}
|
|
277
|
+
for (const verdict of state.verdicts ?? []) {
|
|
278
|
+
if (isSurfaceMcpVerdict(verdict)) {
|
|
279
|
+
session.verdicts.set(verdict.findingId, verdict);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
session.nextRunSequence = nextSequenceFromIds(session.runOrder, "run_mcp_");
|
|
283
|
+
session.nextBaselineSequence = nextSequenceFromIds(session.baselineOrder, "baseline_mcp_");
|
|
284
|
+
}
|
|
285
|
+
function surfaceMcpRunRecordFromProjectRecord(record) {
|
|
286
|
+
if (record.backlog === void 0 || record.capture === void 0 || record.findings === void 0 || record.status === void 0 || record.skippedLenses === void 0) {
|
|
287
|
+
return void 0;
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
backlog: record.backlog,
|
|
291
|
+
capture: record.capture,
|
|
292
|
+
findings: record.findings,
|
|
293
|
+
runId: record.runId,
|
|
294
|
+
skippedLenses: record.skippedLenses,
|
|
295
|
+
status: record.status,
|
|
296
|
+
trackedFindings: record.trackedFindings
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function surfaceMcpBaselineFromProjectBaseline(baseline) {
|
|
300
|
+
return {
|
|
301
|
+
baselineId: baseline.baselineId,
|
|
302
|
+
identityKeys: new Set(baseline.identityKeys),
|
|
303
|
+
...baseline.reason === void 0 ? {} : { reason: baseline.reason }
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function isSurfaceMcpVerdict(verdict) {
|
|
307
|
+
return verdict.decision === "accept" || verdict.decision === "reject" || verdict.decision === "correct" || verdict.decision === "defer";
|
|
308
|
+
}
|
|
309
|
+
async function persistSurfaceMcpSession(composition, session) {
|
|
310
|
+
const updateState = stateSnapshotForMcpSession(session);
|
|
311
|
+
if (composition.stateStore.updateState !== void 0) {
|
|
312
|
+
const updated = await composition.stateStore.updateState(updateState);
|
|
313
|
+
return updated.ok ? ok(void 0) : updated;
|
|
314
|
+
}
|
|
315
|
+
const current = await composition.stateStore.readState();
|
|
316
|
+
if (!current.ok) {
|
|
317
|
+
return current;
|
|
318
|
+
}
|
|
319
|
+
const written = await composition.stateStore.writeState(updateState(current.value));
|
|
320
|
+
return written.ok ? ok(void 0) : written;
|
|
321
|
+
}
|
|
322
|
+
function stateSnapshotForMcpSession(session) {
|
|
323
|
+
return (state) => {
|
|
324
|
+
const runRecords = session.runOrder.map((runId) => session.runs.get(runId)).filter((record) => record !== void 0).map(projectRunRecordFromSurfaceMcpRecord);
|
|
325
|
+
const latestRun = runRecords.at(-1);
|
|
326
|
+
const baselines = session.baselineOrder.map((baselineId) => session.baselines.get(baselineId)).filter((baseline) => baseline !== void 0).map(projectBaselineFromSurfaceMcpBaseline);
|
|
327
|
+
const trackedFindings = [...session.trackedByIdentity.values()];
|
|
328
|
+
const verdicts = [...session.verdicts.values()];
|
|
329
|
+
const currentStage = latestRun?.status ?? state.currentStage;
|
|
330
|
+
return {
|
|
331
|
+
...state,
|
|
332
|
+
...latestRun?.backlog === void 0 ? {} : { backlog: latestRun.backlog },
|
|
333
|
+
...currentStage === void 0 ? {} : { currentStage },
|
|
334
|
+
...latestRun?.findings === void 0 ? {} : { findings: latestRun.findings },
|
|
335
|
+
baselines,
|
|
336
|
+
runRecords,
|
|
337
|
+
trackedFindings,
|
|
338
|
+
verdicts
|
|
339
|
+
};
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function projectRunRecordFromSurfaceMcpRecord(record) {
|
|
343
|
+
return {
|
|
344
|
+
backlog: record.backlog,
|
|
345
|
+
capture: record.capture,
|
|
346
|
+
findings: record.findings,
|
|
347
|
+
runId: record.runId,
|
|
348
|
+
skippedLenses: record.skippedLenses,
|
|
349
|
+
status: record.status,
|
|
350
|
+
trackedFindings: record.trackedFindings
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function projectBaselineFromSurfaceMcpBaseline(baseline) {
|
|
354
|
+
return {
|
|
355
|
+
baselineId: baseline.baselineId,
|
|
356
|
+
identityKeys: [...baseline.identityKeys],
|
|
357
|
+
...baseline.reason === void 0 ? {} : { reason: baseline.reason },
|
|
358
|
+
waivers: []
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function nextSequenceFromIds(ids, prefix) {
|
|
362
|
+
let maxSequence = 0;
|
|
363
|
+
for (const id of ids) {
|
|
364
|
+
if (!id.startsWith(prefix)) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const sequence = Number.parseInt(id.slice(prefix.length), 10);
|
|
368
|
+
if (Number.isInteger(sequence)) {
|
|
369
|
+
maxSequence = Math.max(maxSequence, sequence);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return maxSequence + 1;
|
|
373
|
+
}
|
|
374
|
+
async function callSurfaceMcpTool(input) {
|
|
375
|
+
switch (input.name) {
|
|
376
|
+
case "surface_capture":
|
|
377
|
+
return await callSurfaceCapture(input.composition, input.input);
|
|
378
|
+
case "surface_audit":
|
|
379
|
+
return await callSurfaceAudit(input.composition, input.session, input.input);
|
|
380
|
+
case "surface_explain":
|
|
381
|
+
return callSurfaceExplain(input.session, input.input);
|
|
382
|
+
case "surface_backlog":
|
|
383
|
+
return await callSurfaceBacklog(input.composition, input.session, input.input);
|
|
384
|
+
case "surface_gate":
|
|
385
|
+
return await callSurfaceGate(input.composition, input.session, input.input);
|
|
386
|
+
case "surface_validate":
|
|
387
|
+
return callSurfaceValidate(input.session, input.input);
|
|
388
|
+
case "surface_baseline":
|
|
389
|
+
return await callSurfaceBaseline(input.composition, input.session, input.input);
|
|
390
|
+
case "surface_verdict":
|
|
391
|
+
return await callSurfaceVerdict(input.composition, input.session, input.input);
|
|
392
|
+
case "surface_diff":
|
|
393
|
+
return callSurfaceDiff(input.session, input.input);
|
|
394
|
+
case "surface_alternatives":
|
|
395
|
+
return await callSurfaceAlternatives(input.composition, input.input);
|
|
396
|
+
case "surface_trace":
|
|
397
|
+
return callSurfaceTrace(input.session, input.input);
|
|
398
|
+
case "surface_run":
|
|
399
|
+
return await callSurfaceRun(input.composition, input.input);
|
|
400
|
+
case "surface_next":
|
|
401
|
+
return await callSurfaceNext(input.composition);
|
|
402
|
+
case "surface_status":
|
|
403
|
+
return callSurfaceStatus(input.session);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async function callSurfaceCapture(composition, rawInput) {
|
|
407
|
+
const parsed = parseToolInput("surface_capture", rawInput);
|
|
408
|
+
if (!parsed.ok) {
|
|
409
|
+
return parsed;
|
|
410
|
+
}
|
|
411
|
+
return await composition.captureService.capture(
|
|
412
|
+
targetForCore(parsed.value.target),
|
|
413
|
+
captureOptionsFor(DEFAULT_SURFACE_CONFIG, parsed.value.authState)
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
async function callSurfaceAudit(composition, session, rawInput) {
|
|
417
|
+
const parsed = parseToolInput("surface_audit", rawInput);
|
|
418
|
+
if (!parsed.ok) {
|
|
419
|
+
return parsed;
|
|
420
|
+
}
|
|
421
|
+
const config = configForAuditInput(parsed.value);
|
|
422
|
+
const capture = await composition.captureService.capture(
|
|
423
|
+
targetForCore(parsed.value.target),
|
|
424
|
+
captureOptionsFor(config, parsed.value.authState)
|
|
425
|
+
);
|
|
426
|
+
if (!capture.ok) {
|
|
427
|
+
return capture;
|
|
428
|
+
}
|
|
429
|
+
const evidence = await groundingEvidenceFor(composition, capture.value);
|
|
430
|
+
if (!evidence.ok) {
|
|
431
|
+
return evidence;
|
|
432
|
+
}
|
|
433
|
+
const plan = selectLensExecutionPlan({
|
|
434
|
+
capture: capture.value,
|
|
435
|
+
config,
|
|
436
|
+
modelAvailability: {
|
|
437
|
+
available: false,
|
|
438
|
+
message: "No MCP model provider is configured.",
|
|
439
|
+
reason: "no-model-configured"
|
|
440
|
+
},
|
|
441
|
+
registry: composition.lensRegistry
|
|
442
|
+
});
|
|
443
|
+
const findings = await findingsForPlan(composition, config, capture.value, evidence.value, plan);
|
|
444
|
+
if (!findings.ok) {
|
|
445
|
+
return findings;
|
|
446
|
+
}
|
|
447
|
+
const runId = nextRunId(session);
|
|
448
|
+
const backlog = synthesizeBacklog(runId, findings.value);
|
|
449
|
+
if (!backlog.ok) {
|
|
450
|
+
return backlog;
|
|
451
|
+
}
|
|
452
|
+
const trackedFindings = trackedFindingsForRun(session, runId, findings.value);
|
|
453
|
+
const record = {
|
|
454
|
+
backlog: backlog.value,
|
|
455
|
+
capture: capture.value,
|
|
456
|
+
findings: findings.value,
|
|
457
|
+
runId,
|
|
458
|
+
skippedLenses: plan.skipped,
|
|
459
|
+
status: "completed",
|
|
460
|
+
trackedFindings
|
|
461
|
+
};
|
|
462
|
+
session.runs.set(runId, record);
|
|
463
|
+
session.runOrder.push(runId);
|
|
464
|
+
const persisted = await persistSurfaceMcpSession(composition, session);
|
|
465
|
+
if (!persisted.ok) {
|
|
466
|
+
return persisted;
|
|
467
|
+
}
|
|
468
|
+
return ok({
|
|
469
|
+
backlog: record.backlog,
|
|
470
|
+
capture: record.capture,
|
|
471
|
+
findings: record.findings,
|
|
472
|
+
runId: record.runId,
|
|
473
|
+
skippedLenses: record.skippedLenses
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
function callSurfaceExplain(session, rawInput) {
|
|
477
|
+
const parsed = parseToolInput("surface_explain", rawInput);
|
|
478
|
+
if (!parsed.ok) {
|
|
479
|
+
return parsed;
|
|
480
|
+
}
|
|
481
|
+
const finding = findStoredFinding(session, parsed.value.findingId);
|
|
482
|
+
if (finding === void 0) {
|
|
483
|
+
return err(
|
|
484
|
+
createSurfaceError("finding_not_found", "No stored MCP finding matched the requested id.", {
|
|
485
|
+
details: { findingId: parsed.value.findingId }
|
|
486
|
+
})
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
if (finding.evidence.length === 0) {
|
|
490
|
+
return err(
|
|
491
|
+
createSurfaceError("evidence_missing", "Stored MCP finding has no evidence to explain.", {
|
|
492
|
+
details: { findingId: finding.id }
|
|
493
|
+
})
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
return ok({
|
|
497
|
+
evidence: finding.evidence,
|
|
498
|
+
finding,
|
|
499
|
+
rationale: finding.rationale
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
async function callSurfaceBacklog(composition, session, rawInput) {
|
|
503
|
+
const parsed = parseToolInput("surface_backlog", rawInput);
|
|
504
|
+
if (!parsed.ok) {
|
|
505
|
+
return parsed;
|
|
506
|
+
}
|
|
507
|
+
const record = runRecordFor(session, parsed.value.runId);
|
|
508
|
+
if (record === void 0) {
|
|
509
|
+
return err(
|
|
510
|
+
createSurfaceError("run_not_found", "No stored MCP run matched the requested backlog.", {
|
|
511
|
+
details: { runId: parsed.value.runId ?? null }
|
|
512
|
+
})
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
if (parsed.value.exportTarget === void 0) {
|
|
516
|
+
return ok(record.backlog);
|
|
517
|
+
}
|
|
518
|
+
const exporter = composition.issueExporters.find(
|
|
519
|
+
(candidate) => candidate.target === parsed.value.exportTarget
|
|
520
|
+
);
|
|
521
|
+
if (exporter === void 0) {
|
|
522
|
+
return err(
|
|
523
|
+
createSurfaceError(
|
|
524
|
+
"unknown_export_target",
|
|
525
|
+
"No issue exporter matched the MCP backlog target.",
|
|
526
|
+
{
|
|
527
|
+
details: { exportTarget: parsed.value.exportTarget }
|
|
528
|
+
}
|
|
529
|
+
)
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
const artifact = await composition.stateStore.writeArtifact({
|
|
533
|
+
bytes: new TextEncoder().encode(JSON.stringify(record.backlog, null, 2)),
|
|
534
|
+
kind: "report",
|
|
535
|
+
relativePath: `reports/${record.runId}/backlog.json`
|
|
536
|
+
});
|
|
537
|
+
if (!artifact.ok) {
|
|
538
|
+
return artifact;
|
|
539
|
+
}
|
|
540
|
+
const exported = await exporter.export({
|
|
541
|
+
backlogId: record.backlog.id,
|
|
542
|
+
path: artifact.value.path
|
|
543
|
+
});
|
|
544
|
+
if (!exported.ok) {
|
|
545
|
+
return exported;
|
|
546
|
+
}
|
|
547
|
+
if (exported.value.status === "partial") {
|
|
548
|
+
return err(
|
|
549
|
+
createSurfaceError("export_partial", "MCP backlog export completed partially.", {
|
|
550
|
+
details: { exportId: exported.value.id, target: exported.value.target }
|
|
551
|
+
})
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
return ok(exported.value);
|
|
555
|
+
}
|
|
556
|
+
async function callSurfaceGate(composition, session, rawInput) {
|
|
557
|
+
const parsed = parseToolInput("surface_gate", rawInput);
|
|
558
|
+
if (!parsed.ok) {
|
|
559
|
+
return parsed;
|
|
560
|
+
}
|
|
561
|
+
const record = runRecordFor(session, parsed.value.runId);
|
|
562
|
+
if (record === void 0) {
|
|
563
|
+
return err(
|
|
564
|
+
createSurfaceError("run_not_found", "No stored MCP run matched the requested gate.", {
|
|
565
|
+
details: { runId: parsed.value.runId ?? null }
|
|
566
|
+
})
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
const baseline = latestBaseline(session);
|
|
570
|
+
const baselineIdentityKeys = baseline?.identityKeys ?? /* @__PURE__ */ new Set();
|
|
571
|
+
const findings = record.findings.filter((finding) => {
|
|
572
|
+
const tracked = trackedFindingForRunFinding(record, finding.id);
|
|
573
|
+
return tracked === void 0 || !baselineIdentityKeys.has(tracked.identityKey);
|
|
574
|
+
});
|
|
575
|
+
const policy = parsed.value.policy === void 0 ? DEFAULT_SURFACE_CONFIG.reporting.gatePolicy : parsed.value.policy;
|
|
576
|
+
return await composition.gateEvaluator.evaluate(findings, policy);
|
|
577
|
+
}
|
|
578
|
+
function callSurfaceValidate(session, rawInput) {
|
|
579
|
+
const parsed = parseToolInput("surface_validate", rawInput);
|
|
580
|
+
if (!parsed.ok) {
|
|
581
|
+
return parsed;
|
|
582
|
+
}
|
|
583
|
+
const record = runRecordFor(session, parsed.value.runId);
|
|
584
|
+
if (record === void 0) {
|
|
585
|
+
return err(
|
|
586
|
+
createSurfaceError("run_not_found", "No stored MCP run matched the requested validation.", {
|
|
587
|
+
details: { runId: parsed.value.runId }
|
|
588
|
+
})
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
return ok({
|
|
592
|
+
checks: record.trackedFindings.map((trackedFinding) => ({
|
|
593
|
+
id: trackedFinding.identityKey,
|
|
594
|
+
passed: trackedFinding.status !== "identity-broken",
|
|
595
|
+
validation: trackedFinding.validation,
|
|
596
|
+
...trackedFinding.currentFindingId === void 0 ? {} : { findingId: trackedFinding.currentFindingId }
|
|
597
|
+
}))
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
async function callSurfaceBaseline(composition, session, rawInput) {
|
|
601
|
+
const parsed = parseToolInput("surface_baseline", rawInput);
|
|
602
|
+
if (!parsed.ok) {
|
|
603
|
+
return parsed;
|
|
604
|
+
}
|
|
605
|
+
const record = runRecordFor(session, void 0);
|
|
606
|
+
if (record === void 0 || record.trackedFindings.length === 0) {
|
|
607
|
+
return err(
|
|
608
|
+
createSurfaceError("no_findings_to_baseline", "No MCP findings are available to baseline.")
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
const baselineId = nextBaselineId(session);
|
|
612
|
+
const baseline = {
|
|
613
|
+
baselineId,
|
|
614
|
+
identityKeys: new Set(
|
|
615
|
+
record.trackedFindings.map((trackedFinding) => trackedFinding.identityKey)
|
|
616
|
+
),
|
|
617
|
+
...parsed.value.reason === void 0 ? {} : { reason: parsed.value.reason }
|
|
618
|
+
};
|
|
619
|
+
session.baselines.set(baselineId, baseline);
|
|
620
|
+
session.baselineOrder.push(baselineId);
|
|
621
|
+
const persisted = await persistSurfaceMcpSession(composition, session);
|
|
622
|
+
if (!persisted.ok) {
|
|
623
|
+
return persisted;
|
|
624
|
+
}
|
|
625
|
+
return ok({
|
|
626
|
+
baselineId,
|
|
627
|
+
count: baseline.identityKeys.size,
|
|
628
|
+
...baseline.reason === void 0 ? {} : { reason: baseline.reason }
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
async function callSurfaceVerdict(composition, session, rawInput) {
|
|
632
|
+
const parsed = parseToolInput("surface_verdict", rawInput);
|
|
633
|
+
if (!parsed.ok) {
|
|
634
|
+
return parsed;
|
|
635
|
+
}
|
|
636
|
+
const finding = findStoredFinding(session, parsed.value.findingId);
|
|
637
|
+
if (finding === void 0) {
|
|
638
|
+
return err(
|
|
639
|
+
createSurfaceError("finding_not_found", "No stored MCP finding matched the verdict.", {
|
|
640
|
+
details: { findingId: parsed.value.findingId }
|
|
641
|
+
})
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
const verdict = {
|
|
645
|
+
decision: parsed.value.decision,
|
|
646
|
+
findingId: parsed.value.findingId,
|
|
647
|
+
rationale: parsed.value.rationale
|
|
648
|
+
};
|
|
649
|
+
session.verdicts.set(parsed.value.findingId, verdict);
|
|
650
|
+
const persisted = await persistSurfaceMcpSession(composition, session);
|
|
651
|
+
if (!persisted.ok) {
|
|
652
|
+
return persisted;
|
|
653
|
+
}
|
|
654
|
+
return ok(verdict);
|
|
655
|
+
}
|
|
656
|
+
function callSurfaceDiff(session, rawInput) {
|
|
657
|
+
const parsed = parseToolInput("surface_diff", rawInput);
|
|
658
|
+
if (!parsed.ok) {
|
|
659
|
+
return parsed;
|
|
660
|
+
}
|
|
661
|
+
const before = runRecordFor(session, parsed.value.before.runId);
|
|
662
|
+
const after = runRecordFor(session, parsed.value.after.runId);
|
|
663
|
+
if (before === void 0 || after === void 0) {
|
|
664
|
+
return err(
|
|
665
|
+
createSurfaceError("run_not_found", "Both MCP diff runs must exist.", {
|
|
666
|
+
details: { after: parsed.value.after.runId, before: parsed.value.before.runId }
|
|
667
|
+
})
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
return ok(diffTrackedFindings(before.trackedFindings, after.trackedFindings));
|
|
671
|
+
}
|
|
672
|
+
async function callSurfaceAlternatives(composition, rawInput) {
|
|
673
|
+
const parsed = parseToolInput("surface_alternatives", rawInput);
|
|
674
|
+
if (!parsed.ok) {
|
|
675
|
+
return parsed;
|
|
676
|
+
}
|
|
677
|
+
const target = targetForCore(parsed.value.target);
|
|
678
|
+
const capture = await composition.captureService.capture(
|
|
679
|
+
target,
|
|
680
|
+
captureOptionsFor(DEFAULT_SURFACE_CONFIG, parsed.value.authState)
|
|
681
|
+
);
|
|
682
|
+
if (!capture.ok) {
|
|
683
|
+
return capture;
|
|
684
|
+
}
|
|
685
|
+
return ok({
|
|
686
|
+
alternatives: createBoundedAlternatives(target)
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
function callSurfaceTrace(session, rawInput) {
|
|
690
|
+
const parsed = parseToolInput("surface_trace", rawInput);
|
|
691
|
+
if (!parsed.ok) {
|
|
692
|
+
return parsed;
|
|
693
|
+
}
|
|
694
|
+
const trackedFinding = findStoredTrackedFinding(session, parsed.value.findingId);
|
|
695
|
+
if (trackedFinding === void 0) {
|
|
696
|
+
return err(
|
|
697
|
+
createSurfaceError("finding_not_found", "No tracked MCP finding matched the requested id.", {
|
|
698
|
+
details: { findingId: parsed.value.findingId }
|
|
699
|
+
})
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
return ok({ trackedFinding });
|
|
703
|
+
}
|
|
704
|
+
async function callSurfaceRun(composition, rawInput) {
|
|
705
|
+
const parsed = parseToolInput("surface_run", rawInput);
|
|
706
|
+
if (!parsed.ok) {
|
|
707
|
+
return parsed;
|
|
708
|
+
}
|
|
709
|
+
if (parsed.value.step !== "all" && !isExecutableStageId(parsed.value.step)) {
|
|
710
|
+
return err(
|
|
711
|
+
createSurfaceError("unknown_step", `Unknown MCP pipeline step "${parsed.value.step}".`, {
|
|
712
|
+
details: { step: parsed.value.step }
|
|
713
|
+
})
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
const runId = nextMcpPipelineRunId();
|
|
717
|
+
const run = await composition.pipelineOrchestrator.run({
|
|
718
|
+
config: DEFAULT_SURFACE_CONFIG,
|
|
719
|
+
runId
|
|
720
|
+
});
|
|
721
|
+
if (!run.ok) {
|
|
722
|
+
return err(
|
|
723
|
+
createSurfaceError("step_failed", `MCP pipeline run ${runId} failed.`, {
|
|
724
|
+
cause: run.error,
|
|
725
|
+
details: { runId, stage: parsed.value.step }
|
|
726
|
+
})
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
return ok({
|
|
730
|
+
runId: run.value.runId,
|
|
731
|
+
stage: parsed.value.step,
|
|
732
|
+
status: "completed"
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
async function callSurfaceNext(composition) {
|
|
736
|
+
const state = await composition.stateStore.readState();
|
|
737
|
+
if (!state.ok) {
|
|
738
|
+
return state;
|
|
739
|
+
}
|
|
740
|
+
const lastCompletedStage = state.value.pipeline?.lastCompletedStage;
|
|
741
|
+
const eligible = lastCompletedStage === void 0 ? ["run discovery", "run all"] : eligibleStagesAfter(lastCompletedStage).map((stage) => `run ${stage}`);
|
|
742
|
+
return ok({ eligible });
|
|
743
|
+
}
|
|
744
|
+
function callSurfaceStatus(session) {
|
|
745
|
+
const runHistory = session.runOrder.map((runId) => session.runs.get(runId)).filter((record) => record !== void 0).map((record) => ({
|
|
746
|
+
findings: record.findings.length,
|
|
747
|
+
runId: record.runId,
|
|
748
|
+
status: record.status,
|
|
749
|
+
target: record.capture.target
|
|
750
|
+
}));
|
|
751
|
+
const completedRuns = runHistory.filter((entry) => entry.status === "completed").length;
|
|
752
|
+
const failedRuns = runHistory.filter((entry) => entry.status === "failed").length;
|
|
753
|
+
return ok({
|
|
754
|
+
currentStage: runHistory.at(-1)?.status === "completed" ? "completed" : "pending",
|
|
755
|
+
progress: {
|
|
756
|
+
completedRuns,
|
|
757
|
+
failedRuns,
|
|
758
|
+
findings: runHistory.reduce((total, entry) => total + entry.findings, 0)
|
|
759
|
+
},
|
|
760
|
+
runHistory
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
async function groundingEvidenceFor(composition, capture) {
|
|
764
|
+
const evidence = [];
|
|
765
|
+
for (const tool of composition.groundingTools) {
|
|
766
|
+
const result = await tool.run(capture);
|
|
767
|
+
if (!result.ok) {
|
|
768
|
+
return result;
|
|
769
|
+
}
|
|
770
|
+
for (const toolResult of result.value) {
|
|
771
|
+
evidence.push(...toolResult.evidence);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return ok(evidence);
|
|
775
|
+
}
|
|
776
|
+
async function findingsForPlan(composition, config, capture, evidence, plan) {
|
|
777
|
+
const findings = [];
|
|
778
|
+
const lenses = instantiateLensExecutionPlan(plan, composition.lensFactoryOptions);
|
|
779
|
+
for (const { lens } of lenses) {
|
|
780
|
+
const drafts = await lens.evaluate({
|
|
781
|
+
capture,
|
|
782
|
+
config,
|
|
783
|
+
evidence: [...evidence],
|
|
784
|
+
knowledge: composition.knowledgeSource
|
|
785
|
+
});
|
|
786
|
+
if (!drafts.ok) {
|
|
787
|
+
return drafts;
|
|
788
|
+
}
|
|
789
|
+
for (const draft of drafts.value) {
|
|
790
|
+
const scored = scoreFinding(draft, config.findings);
|
|
791
|
+
if (!scored.ok) {
|
|
792
|
+
return scored;
|
|
793
|
+
}
|
|
794
|
+
findings.push(scored.value);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return ok(findings);
|
|
798
|
+
}
|
|
799
|
+
function configForAuditInput(input) {
|
|
800
|
+
return {
|
|
801
|
+
...DEFAULT_SURFACE_CONFIG,
|
|
802
|
+
capture: { ...DEFAULT_SURFACE_CONFIG.capture },
|
|
803
|
+
evaluation: {
|
|
804
|
+
...DEFAULT_SURFACE_CONFIG.evaluation,
|
|
805
|
+
...input.depth === void 0 ? {} : { depth: input.depth },
|
|
806
|
+
...input.preset === void 0 ? {} : { preset: input.preset }
|
|
807
|
+
},
|
|
808
|
+
findings: { ...DEFAULT_SURFACE_CONFIG.findings },
|
|
809
|
+
reporting: { ...DEFAULT_SURFACE_CONFIG.reporting }
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
function captureOptionsFor(config, authStateRef) {
|
|
813
|
+
return {
|
|
814
|
+
config: config.capture,
|
|
815
|
+
...authStateRef === void 0 ? {} : { authStateRef }
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
function targetForCore(input) {
|
|
819
|
+
return {
|
|
820
|
+
kind: input.kind,
|
|
821
|
+
ref: input.ref,
|
|
822
|
+
...input.theme === void 0 ? {} : { theme: input.theme },
|
|
823
|
+
...input.viewport === void 0 ? {} : {
|
|
824
|
+
viewport: {
|
|
825
|
+
height: input.viewport.height,
|
|
826
|
+
label: input.viewport.label,
|
|
827
|
+
width: input.viewport.width
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
function parseToolInput(name, input) {
|
|
833
|
+
const parsed = TOOL_INPUT_SCHEMAS[name].safeParse(input);
|
|
834
|
+
if (!parsed.success) {
|
|
835
|
+
return err(
|
|
836
|
+
createSurfaceError("config_invalid", "MCP tool input did not match the registered schema.", {
|
|
837
|
+
cause: parsed.error,
|
|
838
|
+
details: { tool: name }
|
|
839
|
+
})
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
return ok(parsed.data);
|
|
843
|
+
}
|
|
844
|
+
function nextRunId(session) {
|
|
845
|
+
const runId = `run_mcp_${session.nextRunSequence.toString().padStart(4, "0")}`;
|
|
846
|
+
session.nextRunSequence += 1;
|
|
847
|
+
return runId;
|
|
848
|
+
}
|
|
849
|
+
function nextBaselineId(session) {
|
|
850
|
+
const baselineId = `baseline_mcp_${session.nextBaselineSequence.toString().padStart(4, "0")}`;
|
|
851
|
+
session.nextBaselineSequence += 1;
|
|
852
|
+
return baselineId;
|
|
853
|
+
}
|
|
854
|
+
function nextMcpPipelineRunId() {
|
|
855
|
+
return `run_mcp_pipeline_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
856
|
+
}
|
|
857
|
+
function isExecutableStageId(step) {
|
|
858
|
+
return DEFAULT_COMPOSITION_STAGE_IDS.includes(step);
|
|
859
|
+
}
|
|
860
|
+
function eligibleStagesAfter(lastCompletedStage) {
|
|
861
|
+
const index = DEFAULT_COMPOSITION_STAGE_IDS.findIndex((stage) => stage === lastCompletedStage);
|
|
862
|
+
if (index === -1) {
|
|
863
|
+
return DEFAULT_COMPOSITION_STAGE_IDS;
|
|
864
|
+
}
|
|
865
|
+
return DEFAULT_COMPOSITION_STAGE_IDS.slice(index + 1);
|
|
866
|
+
}
|
|
867
|
+
function runRecordFor(session, runId) {
|
|
868
|
+
if (runId !== void 0) {
|
|
869
|
+
return session.runs.get(runId);
|
|
870
|
+
}
|
|
871
|
+
const latestRunId = session.runOrder.at(-1);
|
|
872
|
+
return latestRunId === void 0 ? void 0 : session.runs.get(latestRunId);
|
|
873
|
+
}
|
|
874
|
+
function findStoredFinding(session, findingId) {
|
|
875
|
+
for (const runId of session.runOrder) {
|
|
876
|
+
const record = session.runs.get(runId);
|
|
877
|
+
const finding = record?.findings.find((candidate) => candidate.id === findingId);
|
|
878
|
+
if (finding !== void 0) {
|
|
879
|
+
return finding;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return void 0;
|
|
883
|
+
}
|
|
884
|
+
function trackedFindingsForRun(session, runId, findings) {
|
|
885
|
+
return findings.map((finding) => {
|
|
886
|
+
const initial = createTrackedFinding({
|
|
887
|
+
finding,
|
|
888
|
+
runId,
|
|
889
|
+
validation: validationForFinding(finding)
|
|
890
|
+
});
|
|
891
|
+
const previous = session.trackedByIdentity.get(initial.identityKey);
|
|
892
|
+
const trackedFinding = previous === void 0 ? initial : transitionTrackedFinding(previous, { finding, kind: "detected", runId });
|
|
893
|
+
session.trackedByIdentity.set(trackedFinding.identityKey, trackedFinding);
|
|
894
|
+
return trackedFinding;
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
function validationForFinding(finding) {
|
|
898
|
+
if (finding.method === "measured") {
|
|
899
|
+
const toolEvidence = finding.evidence.find((entry) => entry.kind === "tool-result");
|
|
900
|
+
return {
|
|
901
|
+
expectation: toolEvidence === void 0 ? `${finding.lens}/${finding.issueType} remains resolved.` : `${toolEvidence.tool} ${toolEvidence.rule} passes for ${locationLabelFor(finding)}.`,
|
|
902
|
+
kind: "measured-rule"
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
return {
|
|
906
|
+
expectation: `${finding.lens}/${finding.issueType} should be re-evaluated at ${locationLabelFor(
|
|
907
|
+
finding
|
|
908
|
+
)}.`,
|
|
909
|
+
kind: "re-evaluate-lens"
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
function locationLabelFor(finding) {
|
|
913
|
+
return finding.location.elementRef ?? finding.location.selector ?? finding.location.component ?? finding.location.file ?? finding.id;
|
|
914
|
+
}
|
|
915
|
+
function latestBaseline(session) {
|
|
916
|
+
const baselineId = session.baselineOrder.at(-1);
|
|
917
|
+
return baselineId === void 0 ? void 0 : session.baselines.get(baselineId);
|
|
918
|
+
}
|
|
919
|
+
function trackedFindingForRunFinding(record, findingId) {
|
|
920
|
+
return record.trackedFindings.find(
|
|
921
|
+
(trackedFinding) => trackedFinding.currentFindingId === findingId
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
function findStoredTrackedFinding(session, findingId) {
|
|
925
|
+
for (const trackedFinding of session.trackedByIdentity.values()) {
|
|
926
|
+
if (trackedFinding.currentFindingId === findingId) {
|
|
927
|
+
return trackedFinding;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return void 0;
|
|
931
|
+
}
|
|
932
|
+
async function createSurfaceSdkMcpServer(options = {}) {
|
|
933
|
+
const surfaceServer = createSurfaceMcpServer(options);
|
|
934
|
+
const [{ McpServer }] = await Promise.all([import("@modelcontextprotocol/sdk/server/mcp.js")]);
|
|
935
|
+
const server = new McpServer({
|
|
936
|
+
name: SURFACE_MCP_SERVER_NAME,
|
|
937
|
+
version: SURFACE_MCP_SERVER_VERSION
|
|
938
|
+
});
|
|
939
|
+
for (const tool of INTERNAL_TOOLS) {
|
|
940
|
+
server.registerTool(
|
|
941
|
+
tool.name,
|
|
942
|
+
{
|
|
943
|
+
description: tool.description,
|
|
944
|
+
inputSchema: tool.inputZodSchema,
|
|
945
|
+
title: tool.title
|
|
946
|
+
},
|
|
947
|
+
async (input) => mcpToolCallResult(await surfaceServer.callTool(tool.name, input))
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
return server;
|
|
951
|
+
}
|
|
952
|
+
async function runSurfaceMcpStdioServer(options = {}) {
|
|
953
|
+
const [{ StdioServerTransport }, server] = await Promise.all([
|
|
954
|
+
import("@modelcontextprotocol/sdk/server/stdio.js"),
|
|
955
|
+
createSurfaceSdkMcpServer(options)
|
|
956
|
+
]);
|
|
957
|
+
await server.connect(new StdioServerTransport());
|
|
958
|
+
}
|
|
959
|
+
function mcpToolCallResult(result) {
|
|
960
|
+
if (isOk(result)) {
|
|
961
|
+
return {
|
|
962
|
+
content: [{ text: JSON.stringify(result.value, null, 2), type: "text" }],
|
|
963
|
+
structuredContent: result.value
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
return {
|
|
967
|
+
content: [{ text: result.error.message, type: "text" }],
|
|
968
|
+
isError: true,
|
|
969
|
+
structuredContent: toMcpError(result.error)
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
function publicToolDefinition(tool) {
|
|
973
|
+
return {
|
|
974
|
+
description: tool.description,
|
|
975
|
+
inputSchema: tool.inputSchema,
|
|
976
|
+
name: tool.name,
|
|
977
|
+
schemaVersion: tool.schemaVersion,
|
|
978
|
+
title: tool.title
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
function requiredFields(schema) {
|
|
982
|
+
const required = schema.required;
|
|
983
|
+
return Array.isArray(required) ? new Set(required.filter((field) => typeof field === "string")) : /* @__PURE__ */ new Set();
|
|
984
|
+
}
|
|
985
|
+
function hasMajorVersionIncrement(currentVersion, nextVersion) {
|
|
986
|
+
const currentMajor = majorVersion(currentVersion);
|
|
987
|
+
const nextMajor = majorVersion(nextVersion);
|
|
988
|
+
return currentMajor !== void 0 && nextMajor !== void 0 && nextMajor > currentMajor;
|
|
989
|
+
}
|
|
990
|
+
function majorVersion(version) {
|
|
991
|
+
const match = /^(\d+)\./u.exec(version);
|
|
992
|
+
const value = match?.[1];
|
|
993
|
+
if (value === void 0) {
|
|
994
|
+
return void 0;
|
|
995
|
+
}
|
|
996
|
+
const major = Number(value);
|
|
997
|
+
return Number.isInteger(major) ? major : void 0;
|
|
998
|
+
}
|
|
999
|
+
function mcpSchemaIncompatible(message, input, details = {}) {
|
|
1000
|
+
return err(
|
|
1001
|
+
createSurfaceError("mcp_schema_incompatible", message, {
|
|
1002
|
+
details: {
|
|
1003
|
+
current: {
|
|
1004
|
+
name: input.current.name,
|
|
1005
|
+
schemaVersion: input.current.schemaVersion
|
|
1006
|
+
},
|
|
1007
|
+
next: {
|
|
1008
|
+
name: input.next.name,
|
|
1009
|
+
schemaVersion: input.next.schemaVersion
|
|
1010
|
+
},
|
|
1011
|
+
...details
|
|
1012
|
+
}
|
|
1013
|
+
})
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
if (process.argv[1] !== void 0 && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
1017
|
+
await runSurfaceMcpStdioServer();
|
|
1018
|
+
}
|
|
1019
|
+
export {
|
|
1020
|
+
SURFACE_MCP_SERVER_NAME,
|
|
1021
|
+
SURFACE_MCP_SERVER_VERSION,
|
|
1022
|
+
SURFACE_MCP_TOOL_SCHEMA_VERSION,
|
|
1023
|
+
assertMcpToolSchemaCompatibility,
|
|
1024
|
+
createMcpToolSchemaSnapshot,
|
|
1025
|
+
createSurfaceMcpServer,
|
|
1026
|
+
createSurfaceMcpToolRegistry,
|
|
1027
|
+
createSurfaceSdkMcpServer,
|
|
1028
|
+
runSurfaceMcpStdioServer
|
|
1029
|
+
};
|
|
1030
|
+
//# sourceMappingURL=index.js.map
|