@grafema/mcp 0.3.24 → 0.3.27

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.
Files changed (38) hide show
  1. package/README.md +103 -90
  2. package/dist/definitions/query-tools.d.ts.map +1 -1
  3. package/dist/definitions/query-tools.js +71 -0
  4. package/dist/definitions/query-tools.js.map +1 -1
  5. package/dist/handlers/analysis-handlers.d.ts +1 -0
  6. package/dist/handlers/analysis-handlers.d.ts.map +1 -1
  7. package/dist/handlers/analysis-handlers.js +5 -1
  8. package/dist/handlers/analysis-handlers.js.map +1 -1
  9. package/dist/handlers/behavior-handlers.d.ts +29 -0
  10. package/dist/handlers/behavior-handlers.d.ts.map +1 -0
  11. package/dist/handlers/behavior-handlers.js +127 -0
  12. package/dist/handlers/behavior-handlers.js.map +1 -0
  13. package/dist/handlers/dataflow-handlers.d.ts +4 -3
  14. package/dist/handlers/dataflow-handlers.d.ts.map +1 -1
  15. package/dist/handlers/dataflow-handlers.js +93 -3
  16. package/dist/handlers/dataflow-handlers.js.map +1 -1
  17. package/dist/handlers/graphql-handlers.d.ts +1 -1
  18. package/dist/handlers/graphql-handlers.js +1 -1
  19. package/dist/handlers/index.d.ts +4 -3
  20. package/dist/handlers/index.d.ts.map +1 -1
  21. package/dist/handlers/index.js +4 -3
  22. package/dist/handlers/index.js.map +1 -1
  23. package/dist/handlers/query-handlers.js +0 -1
  24. package/dist/handlers/query-handlers.js.map +1 -1
  25. package/dist/server.js +13 -9
  26. package/dist/server.js.map +1 -1
  27. package/dist/types.d.ts +11 -0
  28. package/dist/types.d.ts.map +1 -1
  29. package/package.json +4 -4
  30. package/src/definitions/query-tools.ts +71 -0
  31. package/src/handlers/analysis-handlers.ts +6 -1
  32. package/src/handlers/behavior-handlers.ts +174 -0
  33. package/src/handlers/dataflow-handlers.ts +110 -2
  34. package/src/handlers/graphql-handlers.ts +1 -1
  35. package/src/handlers/index.ts +4 -3
  36. package/src/handlers/query-handlers.ts +1 -1
  37. package/src/server.ts +21 -10
  38. package/src/types.ts +13 -0
@@ -252,6 +252,44 @@ Returns: Indented call tree showing each hop with file:line location.`,
252
252
  required: ['source'],
253
253
  },
254
254
  },
255
+ {
256
+ name: 'trace_effects',
257
+ description: `Trace transitive side effects of a function through its call graph.
258
+
259
+ For any function, traverses CALLS edges (DFS) and collects effects from leaf nodes
260
+ using the effects-db (Node.js builtins, npm packages).
261
+
262
+ Use this when you need to:
263
+ - "What side effects does this function have?" → direct + transitive effects
264
+ - "Does this handler do IO?" → trace shows IO:FILE:READ from fs.readFileSync at depth 3
265
+ - "Where does the fetch() call come from?" → leaf_sources shows the origin at depth N
266
+ - "What crosses module boundaries?" → boundary_crossings shows file-to-file effect flow
267
+
268
+ Effect types: PURE, MUTATION, IO (with subtypes like IO:FILE:READ, IO:HTTP:REQUEST),
269
+ THROW, ASYNC, NONDETERMINISTIC, UNKNOWN.
270
+
271
+ UNKNOWN means: unresolved call, external package not in effects-db, or depth limit reached.
272
+
273
+ Returns: direct effects, transitive effects, boundary crossings, leaf sources.`,
274
+ inputSchema: {
275
+ type: 'object',
276
+ properties: {
277
+ node: {
278
+ type: 'string',
279
+ description: 'Function/method name or semantic ID',
280
+ },
281
+ file: {
282
+ type: 'string',
283
+ description: 'File path to disambiguate (optional)',
284
+ },
285
+ max_depth: {
286
+ type: 'number',
287
+ description: 'Maximum call graph traversal depth (default: 10)',
288
+ },
289
+ },
290
+ required: ['node'],
291
+ },
292
+ },
255
293
  {
256
294
  name: 'get_shape',
257
295
  description: `Get the shape (methods + properties) of a CLASS, INTERFACE, or typed variable.
@@ -351,4 +389,37 @@ Returns: List of nodes violating the rule, with file and line info.`,
351
389
  required: ['rule'],
352
390
  },
353
391
  },
392
+ {
393
+ name: 'find_shared_behaviors',
394
+ description: `List clusters of FEATUREs whose entry-points share an identical BEHAVIOR (same forward-slice hash).
395
+
396
+ Surfaces cross-modality duplication — e.g. "this CLI command is a thin wrapper around the
397
+ same library function as that HTTP endpoint" or "this MCP tool and that VS Code command
398
+ delegate to identical logic".
399
+
400
+ Each cluster contains:
401
+ - hash: sha256 of the shared transitive call set (BEHAVIOR.metadata.hash)
402
+ - effects: transitive effects (IO, MUTATION, …) attributed to the shared behavior
403
+ - coreNodeCount: size of the shared forward slice
404
+ - features: array of { id, type, name, file } — the FEATUREs that share this behavior
405
+
406
+ Cluster types are FEATURE node types: cli:command, mcp:tool, vscode:command (and any future
407
+ domain types created by enrichers).
408
+
409
+ Returns clusters ordered by size (largest first), then hash (deterministic tie-break).
410
+ Empty result means every FEATURE has a unique implementation.`,
411
+ inputSchema: {
412
+ type: 'object',
413
+ properties: {
414
+ minClusterSize: {
415
+ type: 'number',
416
+ description: 'Minimum FEATUREs per cluster (default: 2). Values below 2 are clamped to 2.',
417
+ },
418
+ limit: {
419
+ type: 'number',
420
+ description: 'Maximum clusters to return (default: 100).',
421
+ },
422
+ },
423
+ },
424
+ },
354
425
  ];
@@ -2,7 +2,7 @@
2
2
  * MCP Analysis Handlers
3
3
  */
4
4
 
5
- import { ensureAnalyzed } from '../analysis.js';
5
+ import { ensureAnalyzed, discoverServices } from '../analysis.js';
6
6
  import { getAnalysisStatus, isAnalysisRunning } from '../state.js';
7
7
  import {
8
8
  textResult,
@@ -17,6 +17,11 @@ import type { ServerStats } from '@grafema/types';
17
17
 
18
18
  // === ANALYSIS HANDLERS ===
19
19
 
20
+ export async function handleDiscoverServices(): Promise<ToolResult> {
21
+ const services = await discoverServices();
22
+ return textResult(`Found ${services.length} service(s):\n${JSON.stringify(services, null, 2)}`);
23
+ }
24
+
20
25
  export async function handleAnalyzeProject(args: AnalyzeProjectArgs): Promise<ToolResult> {
21
26
  const { service, force } = args;
22
27
 
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Behavior handlers — surface cross-modality FEATURE duplication.
3
+ *
4
+ * `find_shared_behaviors` lists clusters of FEATUREs (cli:command, mcp:tool,
5
+ * vscode:command, …) whose entry-point forward-slice hashes match. Mirrors the
6
+ * `grafema features --duplicates` CLI subcommand — same algorithm, same
7
+ * cluster shape — so the two channels return symmetric data.
8
+ *
9
+ * REG-1119.
10
+ */
11
+
12
+ import { ensureAnalyzed } from '../analysis.js';
13
+ import { textResult, errorResult } from '../utils.js';
14
+ import type { ToolResult, FindSharedBehaviorsArgs } from '../types.js';
15
+ import type { EdgeType, EdgeRecord } from '@grafema/types';
16
+
17
+ /**
18
+ * Minimal backend interface used by the logic function. Allows testing with
19
+ * a simple mock without importing the full GraphBackend. Matches the subset
20
+ * of RFDBServerBackend that we need.
21
+ */
22
+ interface BehaviorBackendLike {
23
+ getNode(id: string): Promise<Record<string, unknown> | null>;
24
+ queryNodes(query: { type?: string; nodeType?: string }): AsyncIterable<Record<string, unknown>>;
25
+ getIncomingEdges(nodeId: string, edgeTypes?: EdgeType[] | null): Promise<EdgeRecord[]>;
26
+ }
27
+
28
+ interface SharedFeatureRef {
29
+ id: string;
30
+ type: string;
31
+ name: string;
32
+ file: string;
33
+ }
34
+
35
+ interface SharedBehaviorCluster {
36
+ hash: string;
37
+ effects: string[];
38
+ coreNodeCount: number;
39
+ features: SharedFeatureRef[];
40
+ }
41
+
42
+ const DEFAULT_MIN_CLUSTER_SIZE = 2;
43
+ const DEFAULT_LIMIT = 100;
44
+ const MAX_LIMIT = 10_000;
45
+
46
+ // === Logic function (testable, accepts backend directly) ===
47
+
48
+ export async function findSharedBehaviorsLogic(
49
+ db: BehaviorBackendLike,
50
+ args: FindSharedBehaviorsArgs,
51
+ ): Promise<ToolResult> {
52
+ const { minClusterSize: rawMin, limit: rawLimit } = args ?? {};
53
+
54
+ if (rawMin !== undefined && (!Number.isFinite(rawMin) || rawMin < 0)) {
55
+ return errorResult('minClusterSize must be a non-negative number');
56
+ }
57
+ if (rawLimit !== undefined && (!Number.isFinite(rawLimit) || rawLimit <= 0)) {
58
+ return errorResult('limit must be a positive number');
59
+ }
60
+ if (rawLimit !== undefined && rawLimit > MAX_LIMIT) {
61
+ return errorResult(`limit must be <= ${MAX_LIMIT}`);
62
+ }
63
+
64
+ const minClusterSize = Math.max(2, rawMin ?? DEFAULT_MIN_CLUSTER_SIZE);
65
+ const limit = rawLimit ?? DEFAULT_LIMIT;
66
+
67
+ // 1. Collect all BEHAVIOR nodes.
68
+ interface BehaviorRow {
69
+ id: string;
70
+ hash: string;
71
+ effects: string[];
72
+ coreNodeCount: number;
73
+ }
74
+ const behaviors: BehaviorRow[] = [];
75
+ for await (const node of db.queryNodes({ type: 'BEHAVIOR' })) {
76
+ const id = String(node.id ?? '');
77
+ if (!id) continue;
78
+ const hash = readString(node.hash);
79
+ if (!hash) continue;
80
+ behaviors.push({
81
+ id,
82
+ hash,
83
+ effects: readStringArray(node.effects),
84
+ coreNodeCount: readNumber(node.coreNodeCount) ?? 0,
85
+ });
86
+ }
87
+
88
+ // 2. Bucket by hash.
89
+ const byHash = new Map<string, BehaviorRow[]>();
90
+ for (const b of behaviors) {
91
+ let bucket = byHash.get(b.hash);
92
+ if (!bucket) {
93
+ bucket = [];
94
+ byHash.set(b.hash, bucket);
95
+ }
96
+ bucket.push(b);
97
+ }
98
+
99
+ // 3. Resolve features (incoming IMPLEMENTED_BY) for clusters.
100
+ const clusters: SharedBehaviorCluster[] = [];
101
+ for (const [hash, bucket] of byHash) {
102
+ if (bucket.length < minClusterSize) continue;
103
+
104
+ const features: SharedFeatureRef[] = [];
105
+ const seen = new Set<string>();
106
+ for (const beh of bucket) {
107
+ const edges = await db.getIncomingEdges(beh.id, ['IMPLEMENTED_BY'] as EdgeType[]);
108
+ for (const edge of edges) {
109
+ const featureId = String(edge.src);
110
+ if (!featureId || seen.has(featureId)) continue;
111
+ seen.add(featureId);
112
+ const node = await db.getNode(featureId);
113
+ if (!node) continue;
114
+ features.push({
115
+ id: featureId,
116
+ type: readString(node.type) ?? 'UNKNOWN',
117
+ name: readString(node.name) ?? '',
118
+ file: readString(node.file) ?? '',
119
+ });
120
+ }
121
+ }
122
+
123
+ if (features.length < minClusterSize) continue;
124
+
125
+ features.sort((a, b) => (a.type === b.type ? a.name.localeCompare(b.name) : a.type.localeCompare(b.type)));
126
+ clusters.push({
127
+ hash,
128
+ effects: bucket[0].effects,
129
+ coreNodeCount: bucket[0].coreNodeCount,
130
+ features,
131
+ });
132
+ }
133
+
134
+ clusters.sort((a, b) => (b.features.length - a.features.length) || a.hash.localeCompare(b.hash));
135
+ const truncated = clusters.length > limit;
136
+ const limited = clusters.slice(0, limit);
137
+
138
+ return textResult(JSON.stringify({
139
+ count: limited.length,
140
+ truncated,
141
+ minClusterSize,
142
+ clusters: limited,
143
+ }, null, 2));
144
+ }
145
+
146
+ // === Public handler (calls ensureAnalyzed, used by MCP routing) ===
147
+
148
+ export async function handleFindSharedBehaviors(args: FindSharedBehaviorsArgs): Promise<ToolResult> {
149
+ const db = await ensureAnalyzed();
150
+ return findSharedBehaviorsLogic(db as unknown as BehaviorBackendLike, args);
151
+ }
152
+
153
+ // === Internal helpers ===
154
+
155
+ function readString(v: unknown): string | undefined {
156
+ return typeof v === 'string' ? v : undefined;
157
+ }
158
+
159
+ function readNumber(v: unknown): number | undefined {
160
+ return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
161
+ }
162
+
163
+ function readStringArray(v: unknown): string[] {
164
+ if (Array.isArray(v)) return v.filter((x): x is string => typeof x === 'string');
165
+ if (typeof v === 'string') {
166
+ try {
167
+ const parsed = JSON.parse(v);
168
+ if (Array.isArray(parsed)) return parsed.filter((x): x is string => typeof x === 'string');
169
+ } catch {
170
+ // not JSON
171
+ }
172
+ }
173
+ return [];
174
+ }
@@ -4,6 +4,8 @@
4
4
  * Delegates BFS tracing to @grafema/util's shared traceDataflow module.
5
5
  */
6
6
 
7
+ import { join } from 'path';
8
+ import { existsSync } from 'fs';
7
9
  import { ensureAnalyzed } from '../analysis.js';
8
10
  import { getProjectPath } from '../state.js';
9
11
  import {
@@ -17,6 +19,7 @@ import type {
17
19
  TraceAliasArgs,
18
20
  TraceDataFlowArgs,
19
21
  TraceCallChainArgs,
22
+ TraceEffectsArgs,
20
23
  CheckInvariantArgs,
21
24
  GraphNode,
22
25
  } from '../types.js';
@@ -24,6 +27,8 @@ import {
24
27
  traceDataflow,
25
28
  renderTraceNarrative,
26
29
  traceCallChain,
30
+ traceEffects,
31
+ EffectsLookup,
27
32
  type DataflowBackend,
28
33
  type TraceDetail,
29
34
  } from '@grafema/util';
@@ -103,7 +108,7 @@ export async function handleTraceAlias(args: TraceAliasArgs): Promise<ToolResult
103
108
 
104
109
  // === TRACE DATAFLOW ===
105
110
 
106
- export async function handleTraceDataFlow(args: TraceDataFlowArgs): Promise<ToolResult> {
111
+ export async function handleTraceDataflow(args: TraceDataFlowArgs): Promise<ToolResult> {
107
112
  const db = await ensureAnalyzed();
108
113
  const { source, file, direction = 'forward', max_depth = 10, limit = 50, detail } = args;
109
114
 
@@ -159,7 +164,7 @@ export async function handleTraceDataFlow(args: TraceDataFlowArgs): Promise<Tool
159
164
 
160
165
  // === TRACE CALL CHAIN ===
161
166
 
162
- export async function handleTraceCallChain(args: TraceCallChainArgs): Promise<ToolResult> {
167
+ export async function handleTraceCalls(args: TraceCallChainArgs): Promise<ToolResult> {
163
168
  const db = await ensureAnalyzed();
164
169
  const { source, file, direction = 'forward', max_depth = 10 } = args;
165
170
 
@@ -373,3 +378,106 @@ export async function handleCheckInvariant(args: CheckInvariantArgs): Promise<To
373
378
  return errorResult(message);
374
379
  }
375
380
  }
381
+
382
+ // === TRACE EFFECTS ===
383
+
384
+ let _effectsLookup: EffectsLookup | null = null;
385
+
386
+ function getEffectsLookup(): EffectsLookup {
387
+ if (!_effectsLookup) {
388
+ // Find effects-db directory relative to the project
389
+ // The effects-db is at the project root: <repo>/effects-db/
390
+ // Also check node_modules for installed packages
391
+ const candidates = [
392
+ join(getProjectPath(), 'effects-db'),
393
+ join(getProjectPath(), 'node_modules', '@grafema', 'effects-db'),
394
+ ];
395
+
396
+ for (const candidate of candidates) {
397
+ if (existsSync(candidate)) {
398
+ _effectsLookup = EffectsLookup.load(candidate);
399
+ break;
400
+ }
401
+ }
402
+
403
+ if (!_effectsLookup) {
404
+ _effectsLookup = EffectsLookup.empty();
405
+ }
406
+ }
407
+ return _effectsLookup;
408
+ }
409
+
410
+ export async function handleTraceEffects(args: TraceEffectsArgs): Promise<ToolResult> {
411
+ const db = await ensureAnalyzed();
412
+ const { node, file, max_depth = 10 } = args;
413
+
414
+ // Find source node (same pattern as handleTraceCalls)
415
+ let sourceNode: GraphNode | null = await db.getNode(node);
416
+ if (!sourceNode) {
417
+ let fallbackNode: GraphNode | null = null;
418
+ for (const type of ['FUNCTION', 'METHOD', 'CONSTRUCTOR']) {
419
+ for await (const n of db.queryNodes({ type, name: node })) {
420
+ if (file && !n.file?.includes(file)) {
421
+ if (!fallbackNode) fallbackNode = n;
422
+ continue;
423
+ }
424
+ sourceNode = n;
425
+ break;
426
+ }
427
+ if (sourceNode) break;
428
+ }
429
+ if (!sourceNode && fallbackNode) {
430
+ sourceNode = fallbackNode;
431
+ }
432
+ }
433
+ if (!sourceNode) {
434
+ const displaySource = isGrafemaUri(node) ? toCompactSemanticId(node) : node;
435
+ return errorResult(`Source "${displaySource}" not found. Provide a FUNCTION or METHOD name.`);
436
+ }
437
+
438
+ const effectsLookup = getEffectsLookup();
439
+ const dfDb = db as unknown as DataflowBackend;
440
+
441
+ const result = await traceEffects(dfDb, sourceNode.id, effectsLookup, { maxDepth: max_depth });
442
+
443
+ if (!result) {
444
+ return errorResult(`Could not trace effects for "${sourceNode.name || node}"`);
445
+ }
446
+
447
+ // Format as readable text
448
+ const lines: string[] = [];
449
+ const name = result.node.name;
450
+ const fileShort = result.node.file ? result.node.file.split('/').pop() : '?';
451
+
452
+ lines.push(`## Effects of ${name} (${fileShort}:${result.node.line ?? '?'})`);
453
+ lines.push('');
454
+
455
+ lines.push(`**Direct effects:** ${result.direct.join(', ') || 'PURE'}`);
456
+ lines.push(`**Transitive effects:** ${result.transitive.join(', ')}`);
457
+ lines.push('');
458
+
459
+ if (result.leaf_sources.length > 0) {
460
+ lines.push(`### Leaf sources (${result.leaf_sources.length})`);
461
+ for (const leaf of result.leaf_sources) {
462
+ const leafFile = leaf.file ? leaf.file.split('/').pop() : '';
463
+ const loc = leafFile ? ` (${leafFile})` : '';
464
+ lines.push(` depth ${leaf.depth}: ${leaf.node}${loc} → ${leaf.effects.join(', ')}`);
465
+ }
466
+ lines.push('');
467
+ }
468
+
469
+ if (result.boundary_crossings.length > 0) {
470
+ lines.push(`### Boundary crossings (${result.boundary_crossings.length})`);
471
+ for (const bc of result.boundary_crossings) {
472
+ const fromShort = bc.from_file.split('/').pop();
473
+ const toShort = bc.to_file.split('/').pop();
474
+ lines.push(` ${bc.caller} (${fromShort}) → ${bc.callee} (${toShort}): ${bc.effects.join(', ')}`);
475
+ }
476
+ lines.push('');
477
+ }
478
+
479
+ lines.push(`---`);
480
+ lines.push(`Nodes visited: ${result.nodes_visited}, max depth: ${result.max_depth}${result.max_depth_reached ? ' (TRUNCATED — some paths marked UNKNOWN)' : ''}`);
481
+
482
+ return textResult(lines.join('\n'));
483
+ }
@@ -29,7 +29,7 @@ async function getYoga(backend: RFDBServerBackend) {
29
29
  return yogaInstance;
30
30
  }
31
31
 
32
- export async function handleGraphQLQuery(args: GraphQLQueryArgs): Promise<ToolResult> {
32
+ export async function handleQueryGraphql(args: GraphQLQueryArgs): Promise<ToolResult> {
33
33
  const { query, variables, operationName } = args;
34
34
 
35
35
  if (!query || query.trim() === '') {
@@ -3,9 +3,9 @@
3
3
  */
4
4
 
5
5
  export { handleQueryGraph, handleFindCalls, handleFindNodes } from './query-handlers.js';
6
- export { handleTraceAlias, handleTraceDataFlow, handleTraceCallChain, handleCheckInvariant, handleExplain } from './dataflow-handlers.js';
6
+ export { handleTraceAlias, handleTraceDataflow, handleTraceCalls, handleCheckInvariant, handleExplain, handleTraceEffects } from './dataflow-handlers.js';
7
7
  export type { ExplainArgs } from './dataflow-handlers.js';
8
- export { handleAnalyzeProject, handleGetAnalysisStatus, handleGetStats, handleGetSchema } from './analysis-handlers.js';
8
+ export { handleDiscoverServices, handleAnalyzeProject, handleGetAnalysisStatus, handleGetStats, handleGetSchema } from './analysis-handlers.js';
9
9
  export { handleCreateGuarantee, handleListGuarantees, handleCheckGuarantees, handleDeleteGuarantee } from './guarantee-handlers.js';
10
10
  export { handleGetFunctionDetails, handleGetContext, handleGetFileOverview, handleGetShape } from './context-handlers.js';
11
11
  export { handleReadProjectStructure, handleWriteConfig } from './project-handlers.js';
@@ -18,5 +18,6 @@ export { handleAddKnowledge, handleQueryKnowledge, handleQueryDecisions, handleS
18
18
  // Disabled: requires git-ingest (US-17). See US-17 in AI-AGENT-STORIES.md
19
19
  // export { handleGitChurn, handleGitCoChange, handleGitOwnership, handleGitArchaeology } from './knowledge-handlers.js';
20
20
  export { handleDescribe } from './notation-handlers.js';
21
- export { handleGraphQLQuery } from './graphql-handlers.js';
21
+ export { handleQueryGraphql } from './graphql-handlers.js';
22
22
  export { handleQueryRegistry } from './registry-handlers.js';
23
+ export { handleFindSharedBehaviors } from './behavior-handlers.js';
@@ -436,7 +436,7 @@ export async function handleFindNodes(args: FindNodesArgs): Promise<ToolResult>
436
436
  // === Rich context enrichment ===
437
437
  // For each found node, add structural context (methods, calls, imports)
438
438
  // Only enrich when result set is small enough (≤10 nodes) to avoid latency
439
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
439
+
440
440
  const enriched = nodes.length <= 10
441
441
  ? await enrichNodes(db as any, nodes)
442
442
  : nodes;
package/src/server.ts CHANGED
@@ -35,17 +35,17 @@ import { PROMPTS, getPrompt } from './prompts.js';
35
35
 
36
36
  import { TOOLS } from './definitions/index.js';
37
37
  import { initializeFromArgs, setupLogging, getProjectPath } from './state.js';
38
- import { textResult, errorResult, log } from './utils.js';
38
+ import { errorResult, log } from './utils.js';
39
39
  import { getSocketPathOverride } from './state.js';
40
- import { discoverServices } from './analysis.js';
41
40
  import {
42
41
  handleQueryGraph,
43
42
  handleFindCalls,
44
43
  handleFindNodes,
45
44
  handleTraceAlias,
46
- handleTraceDataFlow,
47
- handleTraceCallChain,
45
+ handleTraceDataflow,
46
+ handleTraceCalls,
48
47
  handleCheckInvariant,
48
+ handleDiscoverServices,
49
49
  handleAnalyzeProject,
50
50
  handleGetAnalysisStatus,
51
51
  handleGetStats,
@@ -78,9 +78,11 @@ import {
78
78
  // handleGitOwnership,
79
79
  // handleGitArchaeology,
80
80
  handleDescribe,
81
- handleGraphQLQuery,
81
+ handleQueryGraphql,
82
82
  handleQueryRegistry,
83
83
  handleExplain,
84
+ handleTraceEffects,
85
+ handleFindSharedBehaviors,
84
86
  } from './handlers/index.js';
85
87
  import type { ExplainArgs } from './handlers/index.js';
86
88
  import type {
@@ -119,9 +121,11 @@ import type {
119
121
  // GitCoChangeArgs,
120
122
  // GitOwnershipArgs,
121
123
  // GitArchaeologyArgs,
124
+ TraceEffectsArgs,
122
125
  DescribeArgs,
123
126
  GraphQLQueryArgs,
124
127
  QueryRegistryArgs,
128
+ FindSharedBehaviorsArgs,
125
129
  } from './types.js';
126
130
 
127
131
  /**
@@ -247,24 +251,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
247
251
  break;
248
252
 
249
253
  case 'trace_dataflow':
250
- result = await handleTraceDataFlow(asArgs<TraceDataFlowArgs>(args));
254
+ result = await handleTraceDataflow(asArgs<TraceDataFlowArgs>(args));
251
255
  break;
252
256
 
253
257
  case 'trace_calls':
254
- result = await handleTraceCallChain(asArgs<TraceCallChainArgs>(args));
258
+ result = await handleTraceCalls(asArgs<TraceCallChainArgs>(args));
255
259
  break;
256
260
 
257
261
  case 'explain':
258
262
  result = await handleExplain(asArgs<ExplainArgs>(args));
259
263
  break;
260
264
 
265
+ case 'trace_effects':
266
+ result = await handleTraceEffects(asArgs<TraceEffectsArgs>(args));
267
+ break;
268
+
261
269
  case 'check_invariant':
262
270
  result = await handleCheckInvariant(asArgs<CheckInvariantArgs>(args));
263
271
  break;
264
272
 
265
273
  case 'discover_services':
266
- const services = await discoverServices();
267
- result = textResult(`Found ${services.length} service(s):\n${JSON.stringify(services, null, 2)}`);
274
+ result = await handleDiscoverServices();
268
275
  break;
269
276
 
270
277
  case 'analyze_project':
@@ -393,13 +400,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
393
400
  break;
394
401
 
395
402
  case 'query_graphql':
396
- result = await handleGraphQLQuery(asArgs<GraphQLQueryArgs>(args));
403
+ result = await handleQueryGraphql(asArgs<GraphQLQueryArgs>(args));
397
404
  break;
398
405
 
399
406
  case 'query_registry':
400
407
  result = await handleQueryRegistry(asArgs<QueryRegistryArgs>(args));
401
408
  break;
402
409
 
410
+ case 'find_shared_behaviors':
411
+ result = await handleFindSharedBehaviors(asArgs<FindSharedBehaviorsArgs>(args));
412
+ break;
413
+
403
414
  default:
404
415
  result = errorResult(`Unknown tool: ${name}`);
405
416
  }
package/src/types.ts CHANGED
@@ -81,6 +81,12 @@ export interface TraceCallChainArgs {
81
81
  max_depth?: number;
82
82
  }
83
83
 
84
+ export interface TraceEffectsArgs {
85
+ node: string;
86
+ file?: string;
87
+ max_depth?: number;
88
+ }
89
+
84
90
  export interface GetShapeArgs {
85
91
  target: string;
86
92
  file?: string;
@@ -108,6 +114,13 @@ export interface FindNodesArgs {
108
114
  offset?: number;
109
115
  }
110
116
 
117
+ export interface FindSharedBehaviorsArgs {
118
+ /** Minimum cluster size to include. Default 2. */
119
+ minClusterSize?: number;
120
+ /** Maximum number of clusters to return. Default 100. */
121
+ limit?: number;
122
+ }
123
+
111
124
  export interface AnalyzeProjectArgs {
112
125
  service?: string;
113
126
  force?: boolean;