ctx-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.
@@ -0,0 +1,522 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
2
+
3
+ import {
4
+ traceAddEdgeInputSchema,
5
+ traceAddEdgeOutputSchema,
6
+ traceAddNodeInputSchema,
7
+ traceAddNodeOutputSchema,
8
+ traceAttachArtifactInputSchema,
9
+ traceAttachArtifactOutputSchema,
10
+ traceExplainDecisionInputSchema,
11
+ traceExplainDecisionOutputSchema,
12
+ traceRiskCheckInputSchema,
13
+ traceRiskCheckOutputSchema,
14
+ traceSimilarityInputSchema,
15
+ traceSimilarityOutputSchema,
16
+ traceFindPathsInputSchema,
17
+ traceFindPathsOutputSchema,
18
+ traceFinishInputSchema,
19
+ traceFinishOutputSchema,
20
+ traceGetSubgraphInputSchema,
21
+ traceGetSubgraphOutputSchema,
22
+ traceQueryInputSchema,
23
+ traceQueryOutputSchema,
24
+ traceStartInputSchema,
25
+ traceStartOutputSchema,
26
+ } from "../schemas.js";
27
+ import { newId } from "../util/ids.js";
28
+ import { sha256 } from "../util/hashing.js";
29
+ import { redactSecrets } from "../util/redact.js";
30
+ import {
31
+ finishTrace,
32
+ getArtifactsForNodes,
33
+ getTrace,
34
+ getTraceEdges,
35
+ getTraceNodes,
36
+ insertArtifact,
37
+ insertEdge,
38
+ insertNode,
39
+ insertTrace,
40
+ searchNodes,
41
+ searchNodesForTraces,
42
+ type ArtifactRow,
43
+ type NodeRow,
44
+ type TraceRow,
45
+ } from "../storage/queries.js";
46
+ import type { StorageEngine } from "../storage/engine.js";
47
+ import { buildGraph, extractSubgraph, findPaths } from "../graph/graph.js";
48
+ import { explainDecision } from "../explain/explain.js";
49
+ import {
50
+ assessDecisionRisk,
51
+ findSimilarDecisions,
52
+ invalidateTraceContext,
53
+ } from "../similarity/similarity.js";
54
+
55
+ const ok = <T>(payload: T) => payload;
56
+
57
+ const error = (code: string, message: string, details?: unknown) => ({
58
+ ok: false,
59
+ error: { code, message, details },
60
+ });
61
+
62
+ function toNodeOutput(node: NodeRow) {
63
+ return {
64
+ ...node,
65
+ data: JSON.parse(node.data_json),
66
+ metadata: JSON.parse(node.metadata_json),
67
+ };
68
+ }
69
+
70
+ function toTraceOutput(trace: TraceRow) {
71
+ return {
72
+ ...trace,
73
+ tags: JSON.parse(trace.tags_json),
74
+ metadata: JSON.parse(trace.metadata_json),
75
+ outcome: trace.outcome_json ? JSON.parse(trace.outcome_json) : null,
76
+ };
77
+ }
78
+
79
+ function toArtifactOutput(artifact: ArtifactRow) {
80
+ return {
81
+ ...artifact,
82
+ metadata: JSON.parse(artifact.metadata_json),
83
+ };
84
+ }
85
+
86
+ function ensureTrace(engine: StorageEngine, traceId: string): TraceRow {
87
+ const trace = getTrace(engine, traceId);
88
+ if (!trace) {
89
+ throw new Error(`Trace ${traceId} not found`);
90
+ }
91
+ return trace;
92
+ }
93
+
94
+ export function registerTools(server: McpServer, engine: StorageEngine): void {
95
+ server.tool(
96
+ "trace.start",
97
+ traceStartInputSchema,
98
+ async (input) => {
99
+ const payload = traceStartInputSchema.parse(input);
100
+ const traceId = payload.trace_id ?? newId();
101
+
102
+ const row: TraceRow = {
103
+ trace_id: traceId,
104
+ workflow_id: payload.workflow_id ?? null,
105
+ actor: payload.actor ?? null,
106
+ intent: payload.intent,
107
+ tags_json: JSON.stringify(payload.tags ?? []),
108
+ metadata_json: JSON.stringify(payload.metadata ?? {}),
109
+ started_at: payload.started_at ?? Date.now(),
110
+ finished_at: null,
111
+ status: null,
112
+ outcome_json: null,
113
+ };
114
+
115
+ try {
116
+ engine.transaction(() => {
117
+ insertTrace(engine, row);
118
+ });
119
+ } catch (err) {
120
+ return error("trace_start_failed", "Failed to start trace", (err as Error).message);
121
+ }
122
+
123
+ return ok(traceStartOutputSchema.parse({ ok: true, trace_id: traceId }));
124
+ }
125
+ );
126
+
127
+ server.tool(
128
+ "trace.add_node",
129
+ traceAddNodeInputSchema,
130
+ async (input) => {
131
+ const payload = traceAddNodeInputSchema.parse(input);
132
+ const nodeId = payload.node_id ?? newId();
133
+
134
+ try {
135
+ engine.transaction(() => {
136
+ ensureTrace(engine, payload.trace_id);
137
+ insertNode(engine, {
138
+ node_id: nodeId,
139
+ trace_id: payload.trace_id,
140
+ type: payload.type,
141
+ summary: payload.summary,
142
+ data_json: JSON.stringify(payload.data ?? {}),
143
+ confidence: payload.confidence ?? null,
144
+ metadata_json: JSON.stringify(payload.metadata ?? {}),
145
+ created_at: payload.created_at ?? Date.now(),
146
+ });
147
+ });
148
+ invalidateTraceContext(engine, payload.trace_id);
149
+ } catch (err) {
150
+ return error("trace_add_node_failed", "Failed to add node", (err as Error).message);
151
+ }
152
+
153
+ return ok(traceAddNodeOutputSchema.parse({ ok: true, node_id: nodeId }));
154
+ }
155
+ );
156
+
157
+ server.tool(
158
+ "trace.add_edge",
159
+ traceAddEdgeInputSchema,
160
+ async (input) => {
161
+ const payload = traceAddEdgeInputSchema.parse(input);
162
+ const edgeId = payload.edge_id ?? newId();
163
+
164
+ try {
165
+ engine.transaction(() => {
166
+ ensureTrace(engine, payload.trace_id);
167
+ insertEdge(engine, {
168
+ edge_id: edgeId,
169
+ trace_id: payload.trace_id,
170
+ from_node_id: payload.from_node_id,
171
+ to_node_id: payload.to_node_id,
172
+ relation_type: payload.relation_type,
173
+ data_json: JSON.stringify(payload.data ?? {}),
174
+ created_at: payload.created_at ?? Date.now(),
175
+ });
176
+ });
177
+ invalidateTraceContext(engine, payload.trace_id);
178
+ } catch (err) {
179
+ return error("trace_add_edge_failed", "Failed to add edge", (err as Error).message);
180
+ }
181
+
182
+ return ok(traceAddEdgeOutputSchema.parse({ ok: true, edge_id: edgeId }));
183
+ }
184
+ );
185
+
186
+ server.tool(
187
+ "trace.attach_artifact",
188
+ traceAttachArtifactInputSchema,
189
+ async (input) => {
190
+ const payload = traceAttachArtifactInputSchema.parse(input);
191
+ const artifactId = payload.artifact_id ?? newId();
192
+ const redactResult = redactSecrets(payload.content);
193
+ const redactionLevel = redactResult.wasRedacted
194
+ ? "scrubbed"
195
+ : payload.redaction_level ?? "internal";
196
+
197
+ try {
198
+ engine.transaction(() => {
199
+ ensureTrace(engine, payload.trace_id);
200
+ insertArtifact(engine, {
201
+ artifact_id: artifactId,
202
+ trace_id: payload.trace_id,
203
+ node_id: payload.node_id ?? null,
204
+ artifact_type: payload.artifact_type,
205
+ content: redactResult.redacted,
206
+ redaction_level: redactionLevel,
207
+ metadata_json: JSON.stringify(payload.metadata ?? {}),
208
+ sha256: sha256(redactResult.redacted),
209
+ created_at: payload.created_at ?? Date.now(),
210
+ });
211
+ });
212
+ } catch (err) {
213
+ return error(
214
+ "trace_attach_artifact_failed",
215
+ "Failed to attach artifact",
216
+ (err as Error).message
217
+ );
218
+ }
219
+
220
+ return ok(
221
+ traceAttachArtifactOutputSchema.parse({
222
+ ok: true,
223
+ artifact_id: artifactId,
224
+ sha256: sha256(redactResult.redacted),
225
+ redaction_level: redactionLevel,
226
+ })
227
+ );
228
+ }
229
+ );
230
+
231
+ server.tool(
232
+ "trace.finish",
233
+ traceFinishInputSchema,
234
+ async (input) => {
235
+ const payload = traceFinishInputSchema.parse(input);
236
+ const finishedAt = payload.finished_at ?? Date.now();
237
+
238
+ try {
239
+ engine.transaction(() => {
240
+ ensureTrace(engine, payload.trace_id);
241
+ finishTrace(
242
+ engine,
243
+ payload.trace_id,
244
+ finishedAt,
245
+ payload.status ?? null,
246
+ payload.outcome ? JSON.stringify(payload.outcome) : null
247
+ );
248
+ });
249
+ } catch (err) {
250
+ return error("trace_finish_failed", "Failed to finish trace", (err as Error).message);
251
+ }
252
+
253
+ return ok(traceFinishOutputSchema.parse({ ok: true, trace_id: payload.trace_id }));
254
+ }
255
+ );
256
+
257
+ server.tool(
258
+ "trace.query",
259
+ traceQueryInputSchema,
260
+ async (input) => {
261
+ const payload = traceQueryInputSchema.parse(input);
262
+ const limit = payload.limit ?? 50;
263
+ const offset = payload.offset ?? 0;
264
+ const hasTags = Boolean(payload.tags && payload.tags.length > 0);
265
+
266
+ let nodes: NodeRow[] = [];
267
+ try {
268
+ if (hasTags) {
269
+ if (payload.trace_id) {
270
+ const trace = getTrace(engine, payload.trace_id);
271
+ const traceTags = trace ? toTraceOutput(trace).tags : [];
272
+ if (!payload.tags?.every((tag) => traceTags.includes(tag))) {
273
+ nodes = [];
274
+ } else {
275
+ nodes = searchNodes(engine, {
276
+ traceId: payload.trace_id,
277
+ type: payload.type,
278
+ text: payload.text,
279
+ limit,
280
+ offset,
281
+ });
282
+ }
283
+ } else {
284
+ const traceRows = engine.queryAll<TraceRow>("SELECT * FROM traces;");
285
+ const matchingTraceIds = traceRows
286
+ .map((row) => toTraceOutput(row))
287
+ .filter((row) => payload.tags?.every((tag) => row.tags.includes(tag)))
288
+ .map((row) => row.trace_id);
289
+
290
+ nodes = searchNodesForTraces(engine, {
291
+ traceIds: matchingTraceIds,
292
+ type: payload.type,
293
+ text: payload.text,
294
+ limit,
295
+ offset,
296
+ });
297
+ }
298
+ } else {
299
+ nodes = searchNodes(engine, {
300
+ traceId: payload.trace_id,
301
+ type: payload.type,
302
+ text: payload.text,
303
+ limit,
304
+ offset,
305
+ });
306
+ }
307
+ } catch (err) {
308
+ return error("trace_query_failed", "Failed to query trace", (err as Error).message);
309
+ }
310
+
311
+ return ok(traceQueryOutputSchema.parse({ ok: true, nodes: nodes.map(toNodeOutput) }));
312
+ }
313
+ );
314
+
315
+ server.tool(
316
+ "trace.get_subgraph",
317
+ traceGetSubgraphInputSchema,
318
+ async (input) => {
319
+ const payload = traceGetSubgraphInputSchema.parse(input);
320
+
321
+ try {
322
+ ensureTrace(engine, payload.trace_id);
323
+ const nodes = getTraceNodes(engine, payload.trace_id);
324
+ const edges = getTraceEdges(engine, payload.trace_id);
325
+ const graph = buildGraph(nodes, edges);
326
+ const subgraph = extractSubgraph(graph, {
327
+ center: payload.center,
328
+ depth: payload.depth ?? 2,
329
+ direction: payload.direction ?? "both",
330
+ maxNodes: payload.max_nodes ?? 250,
331
+ });
332
+
333
+ return ok(
334
+ traceGetSubgraphOutputSchema.parse({
335
+ ok: true,
336
+ nodes: subgraph.nodes.map(toNodeOutput),
337
+ edges: subgraph.edges,
338
+ })
339
+ );
340
+ } catch (err) {
341
+ return error("trace_get_subgraph_failed", "Failed to get subgraph", (err as Error).message);
342
+ }
343
+ }
344
+ );
345
+
346
+ server.tool(
347
+ "trace.find_paths",
348
+ traceFindPathsInputSchema,
349
+ async (input) => {
350
+ const payload = traceFindPathsInputSchema.parse(input);
351
+
352
+ try {
353
+ ensureTrace(engine, payload.trace_id);
354
+ const nodes = getTraceNodes(engine, payload.trace_id);
355
+ const edges = getTraceEdges(engine, payload.trace_id);
356
+ const graph = buildGraph(nodes, edges);
357
+ const paths = findPaths(graph, {
358
+ from: payload.from,
359
+ to: payload.to,
360
+ maxDepth: payload.max_depth ?? 4,
361
+ maxPaths: payload.max_paths ?? 10,
362
+ allowRelations: payload.allow_relations
363
+ ? new Set(payload.allow_relations)
364
+ : undefined,
365
+ });
366
+
367
+ return ok(traceFindPathsOutputSchema.parse({ ok: true, paths }));
368
+ } catch (err) {
369
+ return error("trace_find_paths_failed", "Failed to find paths", (err as Error).message);
370
+ }
371
+ }
372
+ );
373
+
374
+ server.tool(
375
+ "trace.explain_decision",
376
+ traceExplainDecisionInputSchema,
377
+ async (input) => {
378
+ const payload = traceExplainDecisionInputSchema.parse(input);
379
+
380
+ try {
381
+ ensureTrace(engine, payload.trace_id);
382
+ const nodes = getTraceNodes(engine, payload.trace_id);
383
+ const edges = getTraceEdges(engine, payload.trace_id);
384
+ const artifacts = getArtifactsForNodes(
385
+ engine,
386
+ nodes.map((node) => node.node_id)
387
+ );
388
+ const explanation = explainDecision(
389
+ payload.decision_node_id,
390
+ nodes,
391
+ edges,
392
+ artifacts,
393
+ payload.depth ?? 4
394
+ );
395
+
396
+ return ok(
397
+ traceExplainDecisionOutputSchema.parse({
398
+ ok: true,
399
+ explanation: {
400
+ ...explanation,
401
+ decision_node: toNodeOutput(explanation.decision_node),
402
+ assumptions: explanation.assumptions.map(toNodeOutput),
403
+ evidence: explanation.evidence.map(toNodeOutput),
404
+ alternatives: explanation.alternatives.map(toNodeOutput),
405
+ checks: explanation.checks.map(toNodeOutput),
406
+ outcome: explanation.outcome ? toNodeOutput(explanation.outcome) : null,
407
+ artifacts: explanation.artifacts.map(toArtifactOutput),
408
+ },
409
+ })
410
+ );
411
+ } catch (err) {
412
+ return error(
413
+ "trace_explain_decision_failed",
414
+ "Failed to explain decision",
415
+ (err as Error).message
416
+ );
417
+ }
418
+ }
419
+ );
420
+
421
+ server.tool(
422
+ "trace.similarity",
423
+ traceSimilarityInputSchema,
424
+ async (input) => {
425
+ const payload = traceSimilarityInputSchema.parse(input);
426
+
427
+ try {
428
+ const matches = findSimilarDecisions(engine, payload.decision_node_id, {
429
+ scope: payload.scope ?? "all",
430
+ limit: payload.limit ?? 10,
431
+ depth: payload.depth ?? 4,
432
+ maxTraces: payload.max_traces ?? 200,
433
+ });
434
+
435
+ return ok(
436
+ traceSimilarityOutputSchema.parse({
437
+ ok: true,
438
+ matches: matches.map((match) => ({
439
+ node: toNodeOutput(match.node),
440
+ trace: toTraceOutput(match.trace),
441
+ score: match.score,
442
+ reasons: match.reasons,
443
+ })),
444
+ })
445
+ );
446
+ } catch (err) {
447
+ return error(
448
+ "trace_similarity_failed",
449
+ "Failed to compute similarity",
450
+ (err as Error).message
451
+ );
452
+ }
453
+ }
454
+ );
455
+
456
+ server.tool(
457
+ "trace.risk_check",
458
+ traceRiskCheckInputSchema,
459
+ async (input) => {
460
+ const payload = traceRiskCheckInputSchema.parse(input);
461
+
462
+ try {
463
+ const assessment = assessDecisionRisk(engine, payload.decision_node_id, {
464
+ scope: payload.scope ?? "all",
465
+ limit: payload.limit ?? 10,
466
+ depth: payload.depth ?? 4,
467
+ threshold: payload.threshold ?? 0.6,
468
+ maxTraces: payload.max_traces ?? 200,
469
+ });
470
+
471
+ return ok(
472
+ traceRiskCheckOutputSchema.parse({
473
+ ok: true,
474
+ assessment: {
475
+ decision: toNodeOutput(assessment.decision),
476
+ novelty: assessment.novelty,
477
+ similar_failures: assessment.similar_failures.map((match) => ({
478
+ node: toNodeOutput(match.node),
479
+ trace: toTraceOutput(match.trace),
480
+ score: match.score,
481
+ reasons: match.reasons,
482
+ })),
483
+ signature_failed_match: assessment.signature_failed_match,
484
+ warnings: assessment.warnings,
485
+ },
486
+ })
487
+ );
488
+ } catch (err) {
489
+ return error(
490
+ "trace_risk_check_failed",
491
+ "Failed to assess decision risk",
492
+ (err as Error).message
493
+ );
494
+ }
495
+ }
496
+ );
497
+ }
498
+
499
+ export const ToolSchemas = {
500
+ traceStartInputSchema,
501
+ traceStartOutputSchema,
502
+ traceAddNodeInputSchema,
503
+ traceAddNodeOutputSchema,
504
+ traceAddEdgeInputSchema,
505
+ traceAddEdgeOutputSchema,
506
+ traceAttachArtifactInputSchema,
507
+ traceAttachArtifactOutputSchema,
508
+ traceFinishInputSchema,
509
+ traceFinishOutputSchema,
510
+ traceQueryInputSchema,
511
+ traceQueryOutputSchema,
512
+ traceGetSubgraphInputSchema,
513
+ traceGetSubgraphOutputSchema,
514
+ traceFindPathsInputSchema,
515
+ traceFindPathsOutputSchema,
516
+ traceExplainDecisionInputSchema,
517
+ traceExplainDecisionOutputSchema,
518
+ traceSimilarityInputSchema,
519
+ traceSimilarityOutputSchema,
520
+ traceRiskCheckInputSchema,
521
+ traceRiskCheckOutputSchema,
522
+ };