@supermodeltools/mcp-server 0.7.0 → 0.7.2
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/README.md +71 -2
- package/dist/server.js +74 -11
- package/dist/tools/feature-request.js +84 -0
- package/dist/tools/find-call-sites.js +141 -0
- package/dist/tools/find-definition.js +161 -0
- package/dist/tools/report-bug.js +133 -0
- package/dist/tools/task-query-tools.js +81 -0
- package/dist/tools/trace-call-chain.js +179 -0
- package/dist/tools/trace-data-flow.js +233 -0
- package/dist/utils/github.js +253 -0
- package/package.json +2 -2
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tool for agents to submit bug reports as GitHub issues.
|
|
4
|
+
* Creates issues on the supermodeltools/mcp repository.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.handler = exports.tool = exports.metadata = void 0;
|
|
8
|
+
exports.formatBugReportBody = formatBugReportBody;
|
|
9
|
+
const types_1 = require("../types");
|
|
10
|
+
const github_1 = require("../utils/github");
|
|
11
|
+
exports.metadata = {
|
|
12
|
+
resource: 'issues',
|
|
13
|
+
operation: 'write',
|
|
14
|
+
tags: ['bug-report', 'github'],
|
|
15
|
+
httpMethod: 'post',
|
|
16
|
+
httpPath: '/repos/supermodeltools/mcp/issues',
|
|
17
|
+
operationId: 'createBugReport',
|
|
18
|
+
};
|
|
19
|
+
exports.tool = {
|
|
20
|
+
name: 'report_bug',
|
|
21
|
+
description: `Submit a bug report to the Supermodel MCP server GitHub repository (${github_1.GITHUB_REPO}).
|
|
22
|
+
|
|
23
|
+
Creates a GitHub issue with structured fields (steps to reproduce, expected vs actual behavior) formatted into a clear bug report. The Supermodel team actively monitors and responds to all submitted issues.
|
|
24
|
+
|
|
25
|
+
Use this tool whenever you encounter a bug, error, or unexpected behavior with any Supermodel MCP tool or with the underlying Supermodel API. Examples include:
|
|
26
|
+
- A tool returned an error that seems incorrect or unexpected
|
|
27
|
+
- A tool produced results that don't match its description or documentation
|
|
28
|
+
- You encountered a crash, hang, timeout, or other failure that appears to be a server-side issue
|
|
29
|
+
- The API returned malformed, incomplete, or nonsensical data
|
|
30
|
+
- An edge case or specific input caused a tool to break
|
|
31
|
+
- You received an error with "reportable: true" in the structured error response
|
|
32
|
+
|
|
33
|
+
Providing steps_to_reproduce, expected_behavior, and actual_behavior helps the team fix bugs faster, but only title and description are required.
|
|
34
|
+
|
|
35
|
+
This tool requires a GITHUB_TOKEN environment variable. To set it up:
|
|
36
|
+
1. Create a GitHub personal access token at https://github.com/settings/tokens with the "public_repo" scope
|
|
37
|
+
2. Set it in your environment: export GITHUB_TOKEN=ghp_your_token_here
|
|
38
|
+
3. Restart the MCP server`,
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {
|
|
42
|
+
title: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
description: 'Short, descriptive title for the bug (e.g. "get_call_graph fails on monorepo with symlinks").',
|
|
45
|
+
},
|
|
46
|
+
description: {
|
|
47
|
+
type: 'string',
|
|
48
|
+
description: 'What happened? Describe the bug clearly.',
|
|
49
|
+
},
|
|
50
|
+
steps_to_reproduce: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
description: 'Step-by-step instructions to reproduce the bug.',
|
|
53
|
+
},
|
|
54
|
+
expected_behavior: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
description: 'What you expected to happen.',
|
|
57
|
+
},
|
|
58
|
+
actual_behavior: {
|
|
59
|
+
type: 'string',
|
|
60
|
+
description: 'What actually happened instead.',
|
|
61
|
+
},
|
|
62
|
+
labels: {
|
|
63
|
+
type: 'array',
|
|
64
|
+
items: { type: 'string' },
|
|
65
|
+
description: 'Optional labels to categorize the issue (e.g. ["bug", "crash"]).',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
required: ['title', 'description'],
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Format structured bug report fields into a markdown issue body.
|
|
73
|
+
*/
|
|
74
|
+
function formatBugReportBody(fields) {
|
|
75
|
+
const sections = [];
|
|
76
|
+
sections.push('## Description\n\n' + fields.description);
|
|
77
|
+
if (fields.steps_to_reproduce) {
|
|
78
|
+
sections.push('## Steps to Reproduce\n\n' + fields.steps_to_reproduce);
|
|
79
|
+
}
|
|
80
|
+
if (fields.expected_behavior) {
|
|
81
|
+
sections.push('## Expected Behavior\n\n' + fields.expected_behavior);
|
|
82
|
+
}
|
|
83
|
+
if (fields.actual_behavior) {
|
|
84
|
+
sections.push('## Actual Behavior\n\n' + fields.actual_behavior);
|
|
85
|
+
}
|
|
86
|
+
return sections.join('\n\n');
|
|
87
|
+
}
|
|
88
|
+
const handler = async (_client, args) => {
|
|
89
|
+
const tokenError = (0, github_1.validateGitHubToken)();
|
|
90
|
+
if (tokenError)
|
|
91
|
+
return (0, types_1.asErrorResult)(tokenError);
|
|
92
|
+
if (!args) {
|
|
93
|
+
return (0, types_1.asErrorResult)({
|
|
94
|
+
type: 'validation_error',
|
|
95
|
+
message: 'Missing required parameters: title and description.',
|
|
96
|
+
code: 'MISSING_PARAMETERS',
|
|
97
|
+
recoverable: false,
|
|
98
|
+
suggestion: 'Provide both "title" and "description" parameters.',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const { title, description, steps_to_reproduce, expected_behavior, actual_behavior, labels, } = args;
|
|
102
|
+
const titleError = (0, github_1.validateRequiredString)(title, 'title', 'INVALID_TITLE', 'Provide a short, descriptive title for the bug.');
|
|
103
|
+
if (titleError)
|
|
104
|
+
return (0, types_1.asErrorResult)(titleError);
|
|
105
|
+
const descError = (0, github_1.validateRequiredString)(description, 'description', 'INVALID_DESCRIPTION', 'Describe the bug clearly.');
|
|
106
|
+
if (descError)
|
|
107
|
+
return (0, types_1.asErrorResult)(descError);
|
|
108
|
+
const stepsError = (0, github_1.validateOptionalString)(steps_to_reproduce, 'steps_to_reproduce', 'INVALID_STEPS', 'Provide steps to reproduce as a string.');
|
|
109
|
+
if (stepsError)
|
|
110
|
+
return (0, types_1.asErrorResult)(stepsError);
|
|
111
|
+
const expectedError = (0, github_1.validateOptionalString)(expected_behavior, 'expected_behavior', 'INVALID_EXPECTED_BEHAVIOR', 'Describe expected behavior as a string.');
|
|
112
|
+
if (expectedError)
|
|
113
|
+
return (0, types_1.asErrorResult)(expectedError);
|
|
114
|
+
const actualError = (0, github_1.validateOptionalString)(actual_behavior, 'actual_behavior', 'INVALID_ACTUAL_BEHAVIOR', 'Describe actual behavior as a string.');
|
|
115
|
+
if (actualError)
|
|
116
|
+
return (0, types_1.asErrorResult)(actualError);
|
|
117
|
+
const labelsError = (0, github_1.validateLabels)(labels);
|
|
118
|
+
if (labelsError)
|
|
119
|
+
return (0, types_1.asErrorResult)(labelsError);
|
|
120
|
+
const body = formatBugReportBody({
|
|
121
|
+
description: description,
|
|
122
|
+
steps_to_reproduce: steps_to_reproduce,
|
|
123
|
+
expected_behavior: expected_behavior,
|
|
124
|
+
actual_behavior: actual_behavior,
|
|
125
|
+
});
|
|
126
|
+
return (0, github_1.createGitHubIssue)('report_bug', {
|
|
127
|
+
title: title,
|
|
128
|
+
body,
|
|
129
|
+
labels: labels,
|
|
130
|
+
}, 'Bug report created successfully.');
|
|
131
|
+
};
|
|
132
|
+
exports.handler = handler;
|
|
133
|
+
exports.default = { metadata: exports.metadata, tool: exports.tool, handler: exports.handler };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Task-specific query tools for focused codebase queries.
|
|
4
|
+
* These tools provide fast, targeted answers to specific code navigation questions.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.taskQueryTools = exports.traceDataFlowToolDef = exports.findDefinitionToolDef = exports.traceCallChainToolDef = exports.findCallSitesToolDef = void 0;
|
|
8
|
+
const types_1 = require("../types");
|
|
9
|
+
const find_call_sites_1 = require("./find-call-sites");
|
|
10
|
+
const trace_call_chain_1 = require("./trace-call-chain");
|
|
11
|
+
const find_definition_1 = require("./find-definition");
|
|
12
|
+
const trace_data_flow_1 = require("./trace-data-flow");
|
|
13
|
+
/**
|
|
14
|
+
* Create a handler wrapper that calls the tool function and formats the result
|
|
15
|
+
*/
|
|
16
|
+
function createTaskQueryHandler(toolFunction, toolName) {
|
|
17
|
+
return async (client, args, defaultWorkdir) => {
|
|
18
|
+
if (!args) {
|
|
19
|
+
args = {};
|
|
20
|
+
}
|
|
21
|
+
// Inject default workdir if directory not provided
|
|
22
|
+
if (!args.path && defaultWorkdir) {
|
|
23
|
+
args.path = defaultWorkdir;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
// Call the tool function
|
|
27
|
+
const result = await toolFunction(args);
|
|
28
|
+
// Return formatted JSON response
|
|
29
|
+
return (0, types_1.asTextContentResult)(JSON.stringify(result, null, 2));
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
const message = typeof error?.message === 'string' ? error.message : String(error);
|
|
33
|
+
// Handle common error cases
|
|
34
|
+
if (message.includes('Graph not cached')) {
|
|
35
|
+
return (0, types_1.asErrorResult)({
|
|
36
|
+
type: 'validation_error',
|
|
37
|
+
message: 'Graph not cached. Run explore_codebase or get_call_graph first to analyze the repository.',
|
|
38
|
+
code: 'GRAPH_NOT_CACHED',
|
|
39
|
+
recoverable: true,
|
|
40
|
+
suggestion: 'Call explore_codebase or one of the get_*_graph tools first to analyze and cache the repository graph.',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return (0, types_1.asErrorResult)({
|
|
44
|
+
type: 'internal_error',
|
|
45
|
+
message: `${toolName} failed: ${message}`,
|
|
46
|
+
code: 'TOOL_EXECUTION_FAILED',
|
|
47
|
+
recoverable: false,
|
|
48
|
+
reportable: true,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Create tool metadata for each task-specific query tool
|
|
55
|
+
*/
|
|
56
|
+
function createTaskQueryTool(config) {
|
|
57
|
+
const metadata = {
|
|
58
|
+
resource: 'queries',
|
|
59
|
+
operation: 'read',
|
|
60
|
+
tags: ['task-specific', 'query'],
|
|
61
|
+
};
|
|
62
|
+
const tool = {
|
|
63
|
+
name: config.name,
|
|
64
|
+
description: config.description,
|
|
65
|
+
inputSchema: config.inputSchema,
|
|
66
|
+
};
|
|
67
|
+
const handler = createTaskQueryHandler(config.handler, config.name);
|
|
68
|
+
return { metadata, tool, handler };
|
|
69
|
+
}
|
|
70
|
+
// Create individual tool definitions
|
|
71
|
+
exports.findCallSitesToolDef = createTaskQueryTool(find_call_sites_1.findCallSitesTool);
|
|
72
|
+
exports.traceCallChainToolDef = createTaskQueryTool(trace_call_chain_1.traceCallChainTool);
|
|
73
|
+
exports.findDefinitionToolDef = createTaskQueryTool(find_definition_1.findDefinitionTool);
|
|
74
|
+
exports.traceDataFlowToolDef = createTaskQueryTool(trace_data_flow_1.traceDataFlowTool);
|
|
75
|
+
// Export all task query tools as an array for easy registration
|
|
76
|
+
exports.taskQueryTools = [
|
|
77
|
+
exports.findCallSitesToolDef,
|
|
78
|
+
exports.traceCallChainToolDef,
|
|
79
|
+
exports.findDefinitionToolDef,
|
|
80
|
+
exports.traceDataFlowToolDef,
|
|
81
|
+
];
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Task-specific tool: Trace call chain between two functions
|
|
4
|
+
* Find shortest path showing how control flows from one function to another
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.traceCallChainTool = void 0;
|
|
8
|
+
exports.traceCallChain = traceCallChain;
|
|
9
|
+
const zod_1 = require("zod");
|
|
10
|
+
const zod_to_json_schema_1 = require("zod-to-json-schema");
|
|
11
|
+
const cache_1 = require("../cache");
|
|
12
|
+
const TraceCallChainArgsSchema = zod_1.z.object({
|
|
13
|
+
path: zod_1.z.string().describe('Repository path'),
|
|
14
|
+
from_function: zod_1.z.string().describe('Starting function name'),
|
|
15
|
+
to_function: zod_1.z.string().describe('Target function name'),
|
|
16
|
+
max_depth: zod_1.z.number().optional().describe('Maximum call chain depth to search'),
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* Trace call chain from one function to another using BFS
|
|
20
|
+
*/
|
|
21
|
+
async function traceCallChain(args) {
|
|
22
|
+
const { path, from_function, to_function, max_depth = 10 } = args;
|
|
23
|
+
// Get cached graph
|
|
24
|
+
const cacheKey = getCacheKey(path);
|
|
25
|
+
const graph = cache_1.graphCache.get(cacheKey);
|
|
26
|
+
if (!graph) {
|
|
27
|
+
throw new Error('Graph not cached. Run explore_codebase first to analyze the repository.');
|
|
28
|
+
}
|
|
29
|
+
// Find source function
|
|
30
|
+
const fromNodes = findFunctionsByName(graph, from_function);
|
|
31
|
+
if (fromNodes.length === 0) {
|
|
32
|
+
return {
|
|
33
|
+
from_function,
|
|
34
|
+
to_function,
|
|
35
|
+
path_exists: false,
|
|
36
|
+
call_chain: [],
|
|
37
|
+
summary: `Source function "${from_function}" not found in codebase.`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// Find target function
|
|
41
|
+
const toNodes = findFunctionsByName(graph, to_function);
|
|
42
|
+
if (toNodes.length === 0) {
|
|
43
|
+
return {
|
|
44
|
+
from_function,
|
|
45
|
+
to_function,
|
|
46
|
+
path_exists: false,
|
|
47
|
+
call_chain: [],
|
|
48
|
+
summary: `Target function "${to_function}" not found in codebase.`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// Use first match if multiple functions with same name
|
|
52
|
+
const fromNode = fromNodes[0];
|
|
53
|
+
const toNode = toNodes[0];
|
|
54
|
+
// BFS to find shortest path
|
|
55
|
+
const path_result = findShortestCallPath(graph, fromNode.id, toNode.id, max_depth);
|
|
56
|
+
if (!path_result) {
|
|
57
|
+
return {
|
|
58
|
+
from_function,
|
|
59
|
+
to_function,
|
|
60
|
+
path_exists: false,
|
|
61
|
+
call_chain: [],
|
|
62
|
+
summary: `No call chain found from "${from_function}" to "${to_function}" within depth ${max_depth}.`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// Build call chain steps
|
|
66
|
+
const callChain = [];
|
|
67
|
+
for (let i = 0; i < path_result.length; i++) {
|
|
68
|
+
const nodeId = path_result[i];
|
|
69
|
+
const node = graph.nodeById.get(nodeId);
|
|
70
|
+
if (!node)
|
|
71
|
+
continue;
|
|
72
|
+
const step = {
|
|
73
|
+
function_name: node.properties?.name || 'unknown',
|
|
74
|
+
file: node.properties?.filePath || 'unknown',
|
|
75
|
+
line: node.properties?.startLine || 0,
|
|
76
|
+
};
|
|
77
|
+
// Add call site info if there's a next function
|
|
78
|
+
if (i < path_result.length - 1) {
|
|
79
|
+
const nextNodeId = path_result[i + 1];
|
|
80
|
+
const edge = findCallEdge(graph, nodeId, nextNodeId);
|
|
81
|
+
if (edge) {
|
|
82
|
+
step.call_to_next = {
|
|
83
|
+
line: edge.properties?.lineNumber || 0,
|
|
84
|
+
column: edge.properties?.columnNumber,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
callChain.push(step);
|
|
89
|
+
}
|
|
90
|
+
// Generate summary
|
|
91
|
+
const summary = generateSummary(from_function, to_function, callChain);
|
|
92
|
+
return {
|
|
93
|
+
from_function,
|
|
94
|
+
to_function,
|
|
95
|
+
path_exists: true,
|
|
96
|
+
call_chain: callChain,
|
|
97
|
+
summary,
|
|
98
|
+
chain_length: callChain.length,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* BFS to find shortest path between two functions
|
|
103
|
+
*/
|
|
104
|
+
function findShortestCallPath(graph, fromId, toId, maxDepth) {
|
|
105
|
+
if (fromId === toId) {
|
|
106
|
+
return [fromId];
|
|
107
|
+
}
|
|
108
|
+
const queue = [{ id: fromId, path: [fromId] }];
|
|
109
|
+
const visited = new Set([fromId]);
|
|
110
|
+
while (queue.length > 0) {
|
|
111
|
+
const { id, path } = queue.shift();
|
|
112
|
+
// Check depth limit
|
|
113
|
+
if (path.length > maxDepth) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// Get callees
|
|
117
|
+
const adj = graph.callAdj.get(id);
|
|
118
|
+
if (!adj)
|
|
119
|
+
continue;
|
|
120
|
+
for (const calleeId of adj.out) {
|
|
121
|
+
if (calleeId === toId) {
|
|
122
|
+
// Found target
|
|
123
|
+
return [...path, calleeId];
|
|
124
|
+
}
|
|
125
|
+
if (!visited.has(calleeId)) {
|
|
126
|
+
visited.add(calleeId);
|
|
127
|
+
queue.push({ id: calleeId, path: [...path, calleeId] });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Helper: Find function nodes by name
|
|
135
|
+
*/
|
|
136
|
+
function findFunctionsByName(graph, name) {
|
|
137
|
+
const lowerName = name.toLowerCase();
|
|
138
|
+
const nodeIds = graph.nameIndex.get(lowerName) || [];
|
|
139
|
+
return nodeIds
|
|
140
|
+
.map((id) => graph.nodeById.get(id))
|
|
141
|
+
.filter((node) => node && node.labels?.[0] === 'Function');
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Helper: Find specific call edge between two functions
|
|
145
|
+
*/
|
|
146
|
+
function findCallEdge(graph, fromId, toId) {
|
|
147
|
+
const relationships = graph.raw?.graph?.relationships || [];
|
|
148
|
+
return relationships.find((rel) => rel.type === 'calls' && rel.startNode === fromId && rel.endNode === toId);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Helper: Generate natural language summary
|
|
152
|
+
*/
|
|
153
|
+
function generateSummary(fromName, toName, chain) {
|
|
154
|
+
if (chain.length === 0) {
|
|
155
|
+
return `No call chain found from "${fromName}" to "${toName}".`;
|
|
156
|
+
}
|
|
157
|
+
if (chain.length === 2) {
|
|
158
|
+
return `"${fromName}" directly calls "${toName}".`;
|
|
159
|
+
}
|
|
160
|
+
const pathNames = chain.map(step => step.function_name);
|
|
161
|
+
const arrow = ' → ';
|
|
162
|
+
const pathStr = pathNames.join(arrow);
|
|
163
|
+
return `Call chain (${chain.length} steps): ${pathStr}`;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Helper: Generate cache key
|
|
167
|
+
*/
|
|
168
|
+
function getCacheKey(path) {
|
|
169
|
+
return `cache_${path}`;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Tool metadata for MCP registration
|
|
173
|
+
*/
|
|
174
|
+
exports.traceCallChainTool = {
|
|
175
|
+
name: 'trace_call_chain',
|
|
176
|
+
description: 'Trace the call chain from one function to another, showing the shortest path of function calls',
|
|
177
|
+
inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(TraceCallChainArgsSchema),
|
|
178
|
+
handler: traceCallChain,
|
|
179
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Task-specific tool: Trace data flow for a variable/parameter
|
|
4
|
+
* Follow how data flows through function parameters and variables
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.traceDataFlowTool = void 0;
|
|
8
|
+
exports.traceDataFlow = traceDataFlow;
|
|
9
|
+
const zod_1 = require("zod");
|
|
10
|
+
const zod_to_json_schema_1 = require("zod-to-json-schema");
|
|
11
|
+
const cache_1 = require("../cache");
|
|
12
|
+
const TraceDataFlowArgsSchema = zod_1.z.object({
|
|
13
|
+
path: zod_1.z.string().describe('Repository path'),
|
|
14
|
+
variable: zod_1.z.string().describe('Variable or parameter name to trace'),
|
|
15
|
+
function_name: zod_1.z.string().optional().describe('Function context (optional, helps narrow scope)'),
|
|
16
|
+
max_depth: zod_1.z.number().optional().describe('Maximum depth to trace'),
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* Trace data flow for a variable/parameter
|
|
20
|
+
*/
|
|
21
|
+
async function traceDataFlow(args) {
|
|
22
|
+
const { path, variable, function_name, max_depth = 5 } = args;
|
|
23
|
+
// Get cached graph
|
|
24
|
+
const cacheKey = getCacheKey(path);
|
|
25
|
+
const graph = cache_1.graphCache.get(cacheKey);
|
|
26
|
+
if (!graph) {
|
|
27
|
+
throw new Error('Graph not cached. Run explore_codebase first to analyze the repository.');
|
|
28
|
+
}
|
|
29
|
+
// Find variable/parameter nodes
|
|
30
|
+
const variableNodes = findVariablesByName(graph, variable, function_name);
|
|
31
|
+
if (variableNodes.length === 0) {
|
|
32
|
+
return {
|
|
33
|
+
variable,
|
|
34
|
+
function_context: function_name,
|
|
35
|
+
found: false,
|
|
36
|
+
flow_steps: [],
|
|
37
|
+
summary: function_name
|
|
38
|
+
? `Variable "${variable}" not found in function "${function_name}".`
|
|
39
|
+
: `Variable "${variable}" not found in codebase.`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// Build flow steps from the variable node
|
|
43
|
+
const flowSteps = [];
|
|
44
|
+
const visited = new Set();
|
|
45
|
+
// Start with the first matching variable
|
|
46
|
+
const startNode = variableNodes[0];
|
|
47
|
+
traceFromNode(graph, startNode, flowSteps, visited, 0, max_depth);
|
|
48
|
+
// If no flow found, at least report the definition
|
|
49
|
+
if (flowSteps.length === 0) {
|
|
50
|
+
const props = startNode.properties || {};
|
|
51
|
+
flowSteps.push({
|
|
52
|
+
step_type: 'definition',
|
|
53
|
+
location: {
|
|
54
|
+
function: props.scope || 'unknown',
|
|
55
|
+
file: props.filePath || 'unknown',
|
|
56
|
+
line: props.startLine || 0,
|
|
57
|
+
},
|
|
58
|
+
description: `Variable "${variable}" is defined here`,
|
|
59
|
+
variable_name: variable,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// Generate summary
|
|
63
|
+
const summary = generateSummary(variable, function_name, flowSteps);
|
|
64
|
+
return {
|
|
65
|
+
variable,
|
|
66
|
+
function_context: function_name,
|
|
67
|
+
found: true,
|
|
68
|
+
flow_steps: flowSteps,
|
|
69
|
+
summary,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Recursively trace data flow from a node
|
|
74
|
+
*/
|
|
75
|
+
function traceFromNode(graph, node, steps, visited, depth, maxDepth) {
|
|
76
|
+
if (depth >= maxDepth || visited.has(node.id)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
visited.add(node.id);
|
|
80
|
+
const props = node.properties || {};
|
|
81
|
+
// Add definition step
|
|
82
|
+
if (depth === 0) {
|
|
83
|
+
steps.push({
|
|
84
|
+
step_type: 'definition',
|
|
85
|
+
location: {
|
|
86
|
+
function: props.scope || 'unknown',
|
|
87
|
+
file: props.filePath || 'unknown',
|
|
88
|
+
line: props.startLine || 0,
|
|
89
|
+
},
|
|
90
|
+
description: `Variable "${props.name}" is defined`,
|
|
91
|
+
variable_name: props.name,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
// Find relationships from this node
|
|
95
|
+
const relationships = graph.raw?.graph?.relationships || [];
|
|
96
|
+
for (const rel of relationships) {
|
|
97
|
+
if (rel.startNode !== node.id)
|
|
98
|
+
continue;
|
|
99
|
+
const targetNode = graph.nodeById.get(rel.endNode);
|
|
100
|
+
if (!targetNode)
|
|
101
|
+
continue;
|
|
102
|
+
const targetProps = targetNode.properties || {};
|
|
103
|
+
// Handle different relationship types
|
|
104
|
+
if (rel.type === 'USES' || rel.type === 'reads') {
|
|
105
|
+
steps.push({
|
|
106
|
+
step_type: 'usage',
|
|
107
|
+
location: {
|
|
108
|
+
function: targetProps.name || 'unknown',
|
|
109
|
+
file: targetProps.filePath || props.filePath || 'unknown',
|
|
110
|
+
line: targetProps.startLine || 0,
|
|
111
|
+
},
|
|
112
|
+
description: `Used in ${targetProps.name || 'expression'}`,
|
|
113
|
+
variable_name: props.name,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else if (rel.type === 'ASSIGNS' || rel.type === 'writes') {
|
|
117
|
+
steps.push({
|
|
118
|
+
step_type: 'assignment',
|
|
119
|
+
location: {
|
|
120
|
+
function: props.scope || 'unknown',
|
|
121
|
+
file: props.filePath || 'unknown',
|
|
122
|
+
line: rel.properties?.lineNumber || 0,
|
|
123
|
+
},
|
|
124
|
+
description: `Assigned value`,
|
|
125
|
+
variable_name: props.name,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
else if (rel.type === 'PASSED_TO' || rel.type === 'argument_to') {
|
|
129
|
+
steps.push({
|
|
130
|
+
step_type: 'passed_to',
|
|
131
|
+
location: {
|
|
132
|
+
function: targetProps.name || 'unknown',
|
|
133
|
+
file: targetProps.filePath || 'unknown',
|
|
134
|
+
line: targetProps.startLine || 0,
|
|
135
|
+
},
|
|
136
|
+
description: `Passed as argument to ${targetProps.name}`,
|
|
137
|
+
variable_name: props.name,
|
|
138
|
+
});
|
|
139
|
+
// Continue tracing in the called function
|
|
140
|
+
traceFromNode(graph, targetNode, steps, visited, depth + 1, maxDepth);
|
|
141
|
+
}
|
|
142
|
+
else if (rel.type === 'RETURNS') {
|
|
143
|
+
steps.push({
|
|
144
|
+
step_type: 'returned_from',
|
|
145
|
+
location: {
|
|
146
|
+
function: props.scope || 'unknown',
|
|
147
|
+
file: props.filePath || 'unknown',
|
|
148
|
+
line: rel.properties?.lineNumber || 0,
|
|
149
|
+
},
|
|
150
|
+
description: `Returned from function`,
|
|
151
|
+
variable_name: props.name,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
else if (rel.type === 'transforms_to' || rel.type === 'TRANSFORMS') {
|
|
155
|
+
steps.push({
|
|
156
|
+
step_type: 'transformation',
|
|
157
|
+
location: {
|
|
158
|
+
function: props.scope || 'unknown',
|
|
159
|
+
file: props.filePath || 'unknown',
|
|
160
|
+
line: rel.properties?.lineNumber || 0,
|
|
161
|
+
},
|
|
162
|
+
description: `Transformed to ${targetProps.name}`,
|
|
163
|
+
variable_name: props.name,
|
|
164
|
+
transformed_to: targetProps.name,
|
|
165
|
+
});
|
|
166
|
+
// Continue tracing the transformed variable
|
|
167
|
+
traceFromNode(graph, targetNode, steps, visited, depth + 1, maxDepth);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Find variable/parameter nodes by name
|
|
173
|
+
*/
|
|
174
|
+
function findVariablesByName(graph, name, functionContext) {
|
|
175
|
+
const lowerName = name.toLowerCase();
|
|
176
|
+
const nodeIds = graph.nameIndex.get(lowerName) || [];
|
|
177
|
+
let nodes = nodeIds
|
|
178
|
+
.map((id) => graph.nodeById.get(id))
|
|
179
|
+
.filter((node) => {
|
|
180
|
+
if (!node)
|
|
181
|
+
return false;
|
|
182
|
+
const label = node.labels?.[0] || '';
|
|
183
|
+
return ['Variable', 'Parameter', 'Field', 'Constant'].includes(label);
|
|
184
|
+
});
|
|
185
|
+
// Filter by function context if provided
|
|
186
|
+
if (functionContext) {
|
|
187
|
+
const lowerContext = functionContext.toLowerCase();
|
|
188
|
+
nodes = nodes.filter((node) => {
|
|
189
|
+
const scope = (node.properties?.scope || '').toLowerCase();
|
|
190
|
+
return scope.includes(lowerContext);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return nodes;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Generate natural language summary
|
|
197
|
+
*/
|
|
198
|
+
function generateSummary(variable, functionContext, steps) {
|
|
199
|
+
if (steps.length === 0) {
|
|
200
|
+
return `No data flow found for "${variable}".`;
|
|
201
|
+
}
|
|
202
|
+
if (steps.length === 1) {
|
|
203
|
+
return `Variable "${variable}" is defined but not used in tracked data flows.`;
|
|
204
|
+
}
|
|
205
|
+
// Count step types
|
|
206
|
+
const usages = steps.filter(s => s.step_type === 'usage').length;
|
|
207
|
+
const passedTo = steps.filter(s => s.step_type === 'passed_to').length;
|
|
208
|
+
const transforms = steps.filter(s => s.step_type === 'transformation').length;
|
|
209
|
+
const parts = [];
|
|
210
|
+
if (usages > 0)
|
|
211
|
+
parts.push(`${usages} usage(s)`);
|
|
212
|
+
if (passedTo > 0)
|
|
213
|
+
parts.push(`passed to ${passedTo} function(s)`);
|
|
214
|
+
if (transforms > 0)
|
|
215
|
+
parts.push(`${transforms} transformation(s)`);
|
|
216
|
+
const context = functionContext ? ` in "${functionContext}"` : '';
|
|
217
|
+
return `Data flow for "${variable}"${context}: ${parts.join(', ')} (${steps.length} total steps)`;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Helper: Generate cache key
|
|
221
|
+
*/
|
|
222
|
+
function getCacheKey(path) {
|
|
223
|
+
return `cache_${path}`;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Tool metadata for MCP registration
|
|
227
|
+
*/
|
|
228
|
+
exports.traceDataFlowTool = {
|
|
229
|
+
name: 'trace_data_flow',
|
|
230
|
+
description: 'Trace how data flows through a variable or parameter, showing usage, transformations, and passing between functions',
|
|
231
|
+
inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(TraceDataFlowArgsSchema),
|
|
232
|
+
handler: traceDataFlow,
|
|
233
|
+
};
|