@grafema/mcp 0.2.5-beta → 0.2.7
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 +49 -25
- package/dist/analysis-worker.js +8 -4
- package/dist/analysis-worker.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +15 -3
- package/dist/config.js.map +1 -1
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +69 -0
- package/dist/definitions.js.map +1 -1
- package/dist/handlers/analysis-handlers.d.ts +9 -0
- package/dist/handlers/analysis-handlers.d.ts.map +1 -0
- package/dist/handlers/analysis-handlers.js +73 -0
- package/dist/handlers/analysis-handlers.js.map +1 -0
- package/dist/handlers/context-handlers.d.ts +21 -0
- package/dist/handlers/context-handlers.d.ts.map +1 -0
- package/dist/handlers/context-handlers.js +330 -0
- package/dist/handlers/context-handlers.js.map +1 -0
- package/dist/handlers/coverage-handlers.d.ts +6 -0
- package/dist/handlers/coverage-handlers.d.ts.map +1 -0
- package/dist/handlers/coverage-handlers.js +42 -0
- package/dist/handlers/coverage-handlers.js.map +1 -0
- package/dist/handlers/dataflow-handlers.d.ts +8 -0
- package/dist/handlers/dataflow-handlers.d.ts.map +1 -0
- package/dist/handlers/dataflow-handlers.js +140 -0
- package/dist/handlers/dataflow-handlers.js.map +1 -0
- package/dist/handlers/documentation-handlers.d.ts +6 -0
- package/dist/handlers/documentation-handlers.d.ts.map +1 -0
- package/dist/handlers/documentation-handlers.js +79 -0
- package/dist/handlers/documentation-handlers.js.map +1 -0
- package/dist/handlers/guarantee-handlers.d.ts +21 -0
- package/dist/handlers/guarantee-handlers.d.ts.map +1 -0
- package/dist/handlers/guarantee-handlers.js +251 -0
- package/dist/handlers/guarantee-handlers.js.map +1 -0
- package/dist/handlers/guard-handlers.d.ts +14 -0
- package/dist/handlers/guard-handlers.d.ts.map +1 -0
- package/dist/handlers/guard-handlers.js +77 -0
- package/dist/handlers/guard-handlers.js.map +1 -0
- package/dist/handlers/index.d.ts +14 -0
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/handlers/index.js +14 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/handlers/issue-handlers.d.ts +6 -0
- package/dist/handlers/issue-handlers.d.ts.map +1 -0
- package/dist/handlers/issue-handlers.js +66 -0
- package/dist/handlers/issue-handlers.js.map +1 -0
- package/dist/handlers/project-handlers.d.ts +7 -0
- package/dist/handlers/project-handlers.d.ts.map +1 -0
- package/dist/handlers/project-handlers.js +153 -0
- package/dist/handlers/project-handlers.js.map +1 -0
- package/dist/handlers/query-handlers.d.ts +8 -0
- package/dist/handlers/query-handlers.d.ts.map +1 -0
- package/dist/handlers/query-handlers.js +171 -0
- package/dist/handlers/query-handlers.js.map +1 -0
- package/dist/handlers.d.ts +3 -1
- package/dist/handlers.d.ts.map +1 -1
- package/dist/handlers.js +199 -4
- package/dist/handlers.js.map +1 -1
- package/dist/server.js +7 -1
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/analysis-worker.ts +10 -2
- package/src/config.ts +24 -0
- package/src/definitions.ts +70 -0
- package/src/handlers/analysis-handlers.ts +105 -0
- package/src/handlers/context-handlers.ts +410 -0
- package/src/handlers/coverage-handlers.ts +56 -0
- package/src/handlers/dataflow-handlers.ts +193 -0
- package/src/handlers/documentation-handlers.ts +89 -0
- package/src/handlers/guarantee-handlers.ts +278 -0
- package/src/handlers/guard-handlers.ts +100 -0
- package/src/handlers/index.ts +14 -0
- package/src/handlers/issue-handlers.ts +81 -0
- package/src/handlers/project-handlers.ts +200 -0
- package/src/handlers/query-handlers.ts +232 -0
- package/src/server.ts +13 -1
- package/src/types.ts +15 -0
- package/src/handlers.ts +0 -1373
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Issue Handlers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
textResult,
|
|
7
|
+
} from '../utils.js';
|
|
8
|
+
import type {
|
|
9
|
+
ToolResult,
|
|
10
|
+
ReportIssueArgs,
|
|
11
|
+
} from '../types.js';
|
|
12
|
+
|
|
13
|
+
// === BUG REPORTING ===
|
|
14
|
+
|
|
15
|
+
export async function handleReportIssue(args: ReportIssueArgs): Promise<ToolResult> {
|
|
16
|
+
const { title, description, context, labels = ['bug'] } = args;
|
|
17
|
+
// Use user's token if provided, otherwise fall back to project's issue-only token
|
|
18
|
+
const GRAFEMA_ISSUE_TOKEN = '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';
|
|
19
|
+
const githubToken = process.env.GITHUB_TOKEN || GRAFEMA_ISSUE_TOKEN;
|
|
20
|
+
const repo = 'Disentinel/grafema';
|
|
21
|
+
|
|
22
|
+
// Build issue body
|
|
23
|
+
const body = `## Description
|
|
24
|
+
${description}
|
|
25
|
+
|
|
26
|
+
${context ? `## Context\n\`\`\`\n${context}\n\`\`\`\n` : ''}
|
|
27
|
+
## Environment
|
|
28
|
+
- Grafema version: 0.1.0-alpha.1
|
|
29
|
+
- Reported via: MCP tool
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
*This issue was automatically created via Grafema MCP server.*`;
|
|
33
|
+
|
|
34
|
+
// Try GitHub API if token is available
|
|
35
|
+
if (githubToken) {
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch(`https://api.github.com/repos/${repo}/issues`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: {
|
|
40
|
+
'Authorization': `token ${githubToken}`,
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
title,
|
|
46
|
+
body,
|
|
47
|
+
labels: labels.filter(l => ['bug', 'enhancement', 'documentation', 'question'].includes(l)),
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (response.ok) {
|
|
52
|
+
const issue = await response.json() as { html_url: string; number: number };
|
|
53
|
+
return textResult(
|
|
54
|
+
`✅ Issue created successfully!\n\n` +
|
|
55
|
+
`**Issue #${issue.number}**: ${issue.html_url}\n\n` +
|
|
56
|
+
`Thank you for reporting this issue.`
|
|
57
|
+
);
|
|
58
|
+
} else {
|
|
59
|
+
const error = await response.text();
|
|
60
|
+
throw new Error(`GitHub API error: ${response.status} - ${error}`);
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// Fall through to manual template if API fails
|
|
64
|
+
console.error('[report_issue] GitHub API failed:', error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Fallback: return template for manual submission
|
|
69
|
+
const issueUrl = `https://github.com/${repo}/issues/new`;
|
|
70
|
+
const encodedTitle = encodeURIComponent(title);
|
|
71
|
+
const encodedBody = encodeURIComponent(body);
|
|
72
|
+
const encodedLabels = encodeURIComponent(labels.join(','));
|
|
73
|
+
const directUrl = `${issueUrl}?title=${encodedTitle}&body=${encodedBody}&labels=${encodedLabels}`;
|
|
74
|
+
|
|
75
|
+
return textResult(
|
|
76
|
+
`⚠️ Failed to create issue automatically. Please create it manually:\n\n` +
|
|
77
|
+
`**Quick link** (may truncate long descriptions):\n${directUrl}\n\n` +
|
|
78
|
+
`**Or copy this template to** ${issueUrl}:\n\n` +
|
|
79
|
+
`---\n**Title:** ${title}\n\n${body}\n---`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Project Handlers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getProjectPath } from '../state.js';
|
|
6
|
+
import { validateServices, validatePatterns, validateWorkspace, GRAFEMA_VERSION, getSchemaVersion } from '@grafema/core';
|
|
7
|
+
import { existsSync, readdirSync, statSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
|
+
import type { Dirent } from 'fs';
|
|
9
|
+
import { join, basename } from 'path';
|
|
10
|
+
import { stringify as stringifyYAML } from 'yaml';
|
|
11
|
+
import {
|
|
12
|
+
textResult,
|
|
13
|
+
errorResult,
|
|
14
|
+
} from '../utils.js';
|
|
15
|
+
import type {
|
|
16
|
+
ToolResult,
|
|
17
|
+
ReadProjectStructureArgs,
|
|
18
|
+
WriteConfigArgs,
|
|
19
|
+
} from '../types.js';
|
|
20
|
+
|
|
21
|
+
// === PROJECT STRUCTURE (REG-173) ===
|
|
22
|
+
|
|
23
|
+
export async function handleReadProjectStructure(
|
|
24
|
+
args: ReadProjectStructureArgs
|
|
25
|
+
): Promise<ToolResult> {
|
|
26
|
+
const projectPath = getProjectPath();
|
|
27
|
+
const subPath = args.path || '.';
|
|
28
|
+
const maxDepth = Math.min(Math.max(1, args.depth || 3), 5);
|
|
29
|
+
const includeFiles = args.include_files !== false;
|
|
30
|
+
|
|
31
|
+
const targetPath = join(projectPath, subPath);
|
|
32
|
+
|
|
33
|
+
if (!existsSync(targetPath)) {
|
|
34
|
+
return errorResult(`Path does not exist: ${subPath}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!statSync(targetPath).isDirectory()) {
|
|
38
|
+
return errorResult(`Path is not a directory: ${subPath}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const EXCLUDED = new Set([
|
|
42
|
+
'node_modules', '.git', 'dist', 'build', '.grafema',
|
|
43
|
+
'coverage', '.next', '.nuxt', '.cache', '.output',
|
|
44
|
+
'__pycache__', '.tox', 'target',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const lines: string[] = [];
|
|
48
|
+
|
|
49
|
+
function walk(dir: string, prefix: string, depth: number): void {
|
|
50
|
+
if (depth > maxDepth) return;
|
|
51
|
+
|
|
52
|
+
let entries: Dirent[];
|
|
53
|
+
try {
|
|
54
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
55
|
+
} catch {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const dirs: string[] = [];
|
|
60
|
+
const files: string[] = [];
|
|
61
|
+
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
if (EXCLUDED.has(entry.name)) continue;
|
|
64
|
+
|
|
65
|
+
if (entry.isDirectory()) {
|
|
66
|
+
dirs.push(entry.name);
|
|
67
|
+
} else if (includeFiles) {
|
|
68
|
+
files.push(entry.name);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
dirs.sort();
|
|
73
|
+
files.sort();
|
|
74
|
+
|
|
75
|
+
const allEntries = [
|
|
76
|
+
...dirs.map(d => ({ name: d, isDir: true })),
|
|
77
|
+
...files.map(f => ({ name: f, isDir: false })),
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < allEntries.length; i++) {
|
|
81
|
+
const entry = allEntries[i];
|
|
82
|
+
const isLast = i === allEntries.length - 1;
|
|
83
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
84
|
+
const childPrefix = isLast ? ' ' : '│ ';
|
|
85
|
+
|
|
86
|
+
if (entry.isDir) {
|
|
87
|
+
lines.push(`${prefix}${connector}${entry.name}/`);
|
|
88
|
+
walk(join(dir, entry.name), prefix + childPrefix, depth + 1);
|
|
89
|
+
} else {
|
|
90
|
+
lines.push(`${prefix}${connector}${entry.name}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
lines.push(subPath === '.' ? basename(projectPath) + '/' : subPath + '/');
|
|
96
|
+
walk(targetPath, '', 1);
|
|
97
|
+
|
|
98
|
+
if (lines.length === 1) {
|
|
99
|
+
return textResult(`Directory is empty or contains only excluded entries: ${subPath}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return textResult(lines.join('\n'));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// === WRITE CONFIG (REG-173) ===
|
|
106
|
+
|
|
107
|
+
export async function handleWriteConfig(
|
|
108
|
+
args: WriteConfigArgs
|
|
109
|
+
): Promise<ToolResult> {
|
|
110
|
+
const projectPath = getProjectPath();
|
|
111
|
+
const grafemaDir = join(projectPath, '.grafema');
|
|
112
|
+
const configPath = join(grafemaDir, 'config.yaml');
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
if (args.services) {
|
|
116
|
+
validateServices(args.services, projectPath);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (args.include !== undefined || args.exclude !== undefined) {
|
|
120
|
+
const warnings: string[] = [];
|
|
121
|
+
validatePatterns(args.include, args.exclude, {
|
|
122
|
+
warn: (msg: string) => warnings.push(msg),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (args.workspace) {
|
|
127
|
+
validateWorkspace(args.workspace, projectPath);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const config: Record<string, unknown> = {
|
|
131
|
+
version: getSchemaVersion(GRAFEMA_VERSION),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
if (args.services && args.services.length > 0) {
|
|
135
|
+
config.services = args.services;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (args.plugins) {
|
|
139
|
+
config.plugins = args.plugins;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (args.include) {
|
|
143
|
+
config.include = args.include;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (args.exclude) {
|
|
147
|
+
config.exclude = args.exclude;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (args.workspace) {
|
|
151
|
+
config.workspace = args.workspace;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const yaml = stringifyYAML(config, { lineWidth: 0 });
|
|
155
|
+
const content =
|
|
156
|
+
'# Grafema Configuration\n' +
|
|
157
|
+
'# Generated by Grafema onboarding\n' +
|
|
158
|
+
'# Documentation: https://github.com/grafema/grafema#configuration\n\n' +
|
|
159
|
+
yaml;
|
|
160
|
+
|
|
161
|
+
if (!existsSync(grafemaDir)) {
|
|
162
|
+
mkdirSync(grafemaDir, { recursive: true });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
writeFileSync(configPath, content);
|
|
166
|
+
|
|
167
|
+
const summary: string[] = ['Configuration written to .grafema/config.yaml'];
|
|
168
|
+
|
|
169
|
+
if (args.services && args.services.length > 0) {
|
|
170
|
+
summary.push(`Services: ${args.services.map(s => s.name).join(', ')}`);
|
|
171
|
+
} else {
|
|
172
|
+
summary.push('Services: using auto-discovery (none explicitly configured)');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (args.plugins) {
|
|
176
|
+
summary.push('Plugins: custom configuration');
|
|
177
|
+
} else {
|
|
178
|
+
summary.push('Plugins: using defaults');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (args.include) {
|
|
182
|
+
summary.push(`Include patterns: ${args.include.join(', ')}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (args.exclude) {
|
|
186
|
+
summary.push(`Exclude patterns: ${args.exclude.join(', ')}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (args.workspace?.roots) {
|
|
190
|
+
summary.push(`Workspace roots: ${args.workspace.roots.join(', ')}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
summary.push('\nNext step: run analyze_project to build the graph.');
|
|
194
|
+
|
|
195
|
+
return textResult(summary.join('\n'));
|
|
196
|
+
} catch (error) {
|
|
197
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
198
|
+
return errorResult(`Failed to write config: ${message}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Query Handlers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ensureAnalyzed } from '../analysis.js';
|
|
6
|
+
import {
|
|
7
|
+
normalizeLimit,
|
|
8
|
+
formatPaginationInfo,
|
|
9
|
+
guardResponseSize,
|
|
10
|
+
serializeBigInt,
|
|
11
|
+
findSimilarTypes,
|
|
12
|
+
textResult,
|
|
13
|
+
errorResult,
|
|
14
|
+
} from '../utils.js';
|
|
15
|
+
import type {
|
|
16
|
+
ToolResult,
|
|
17
|
+
QueryGraphArgs,
|
|
18
|
+
FindCallsArgs,
|
|
19
|
+
FindNodesArgs,
|
|
20
|
+
GraphNode,
|
|
21
|
+
DatalogBinding,
|
|
22
|
+
CallResult,
|
|
23
|
+
} from '../types.js';
|
|
24
|
+
|
|
25
|
+
// === QUERY HANDLERS ===
|
|
26
|
+
|
|
27
|
+
export async function handleQueryGraph(args: QueryGraphArgs): Promise<ToolResult> {
|
|
28
|
+
const db = await ensureAnalyzed();
|
|
29
|
+
const { query, limit: requestedLimit, offset: requestedOffset, format: _format, explain: _explain } = args;
|
|
30
|
+
|
|
31
|
+
const limit = normalizeLimit(requestedLimit);
|
|
32
|
+
const offset = Math.max(0, requestedOffset || 0);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
// Check if backend supports Datalog queries
|
|
36
|
+
if (!('checkGuarantee' in db)) {
|
|
37
|
+
return errorResult('Backend does not support Datalog queries');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const checkFn = (db as unknown as { checkGuarantee: (q: string) => Promise<Array<{ bindings: Array<{ name: string; value: string }> }>> }).checkGuarantee;
|
|
41
|
+
const results = await checkFn(query);
|
|
42
|
+
const total = results.length;
|
|
43
|
+
|
|
44
|
+
if (total === 0) {
|
|
45
|
+
const nodeCounts = await db.countNodesByType();
|
|
46
|
+
const totalNodes = Object.values(nodeCounts).reduce((a, b) => a + b, 0);
|
|
47
|
+
|
|
48
|
+
const typeMatch = query.match(/node\([^,]+,\s*"([^"]+)"\)/);
|
|
49
|
+
const queriedType = typeMatch ? typeMatch[1] : null;
|
|
50
|
+
|
|
51
|
+
let hint = '';
|
|
52
|
+
if (queriedType && !nodeCounts[queriedType]) {
|
|
53
|
+
const availableTypes = Object.keys(nodeCounts);
|
|
54
|
+
const similar = findSimilarTypes(queriedType, availableTypes);
|
|
55
|
+
if (similar.length > 0) {
|
|
56
|
+
hint = `\n💡 Did you mean: ${similar.join(', ')}?`;
|
|
57
|
+
} else {
|
|
58
|
+
hint = `\n💡 Available types: ${availableTypes.slice(0, 10).join(', ')}${availableTypes.length > 10 ? '...' : ''}`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return textResult(
|
|
63
|
+
`Query returned no results.${hint}\n📊 Graph: ${totalNodes.toLocaleString()} nodes`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const paginatedResults = results.slice(offset, offset + limit);
|
|
68
|
+
const hasMore = offset + limit < total;
|
|
69
|
+
|
|
70
|
+
const enrichedResults: unknown[] = [];
|
|
71
|
+
for (const result of paginatedResults) {
|
|
72
|
+
const nodeId = result.bindings?.find((b: DatalogBinding) => b.name === 'X')?.value;
|
|
73
|
+
if (nodeId) {
|
|
74
|
+
const node = await db.getNode(nodeId);
|
|
75
|
+
if (node) {
|
|
76
|
+
enrichedResults.push({
|
|
77
|
+
...node,
|
|
78
|
+
id: nodeId,
|
|
79
|
+
file: node.file,
|
|
80
|
+
line: node.line,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const paginationInfo = formatPaginationInfo({
|
|
87
|
+
limit,
|
|
88
|
+
offset,
|
|
89
|
+
returned: enrichedResults.length,
|
|
90
|
+
total,
|
|
91
|
+
hasMore,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const responseText = `Found ${total} result(s):${paginationInfo}\n\n${JSON.stringify(
|
|
95
|
+
serializeBigInt(enrichedResults),
|
|
96
|
+
null,
|
|
97
|
+
2
|
|
98
|
+
)}`;
|
|
99
|
+
|
|
100
|
+
return textResult(guardResponseSize(responseText));
|
|
101
|
+
} catch (error) {
|
|
102
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
103
|
+
return errorResult(message);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function handleFindCalls(args: FindCallsArgs): Promise<ToolResult> {
|
|
108
|
+
const db = await ensureAnalyzed();
|
|
109
|
+
const { target: name, limit: requestedLimit, offset: requestedOffset, className } = args;
|
|
110
|
+
|
|
111
|
+
const limit = normalizeLimit(requestedLimit);
|
|
112
|
+
const offset = Math.max(0, requestedOffset || 0);
|
|
113
|
+
|
|
114
|
+
const calls: CallResult[] = [];
|
|
115
|
+
let skipped = 0;
|
|
116
|
+
let totalMatched = 0;
|
|
117
|
+
|
|
118
|
+
for await (const node of db.queryNodes({ type: 'CALL' })) {
|
|
119
|
+
if (node.name !== name && node['method'] !== name) continue;
|
|
120
|
+
if (className && node['object'] !== className) continue;
|
|
121
|
+
|
|
122
|
+
totalMatched++;
|
|
123
|
+
|
|
124
|
+
if (skipped < offset) {
|
|
125
|
+
skipped++;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (calls.length >= limit) continue;
|
|
130
|
+
|
|
131
|
+
const callsEdges = await db.getOutgoingEdges(node.id, ['CALLS']);
|
|
132
|
+
const isResolved = callsEdges.length > 0;
|
|
133
|
+
|
|
134
|
+
let target = null;
|
|
135
|
+
if (isResolved) {
|
|
136
|
+
const targetNode = await db.getNode(callsEdges[0].dst);
|
|
137
|
+
target = targetNode
|
|
138
|
+
? {
|
|
139
|
+
type: targetNode.type,
|
|
140
|
+
name: targetNode.name ?? '',
|
|
141
|
+
file: targetNode.file,
|
|
142
|
+
line: targetNode.line,
|
|
143
|
+
}
|
|
144
|
+
: null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
calls.push({
|
|
148
|
+
id: node.id,
|
|
149
|
+
name: node.name,
|
|
150
|
+
object: node['object'] as string | undefined,
|
|
151
|
+
file: node.file,
|
|
152
|
+
line: node.line,
|
|
153
|
+
resolved: isResolved,
|
|
154
|
+
target,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (totalMatched === 0) {
|
|
159
|
+
return textResult(`No calls found for "${className ? className + '.' : ''}${name}"`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const resolved = calls.filter(c => c.resolved).length;
|
|
163
|
+
const unresolved = calls.length - resolved;
|
|
164
|
+
const hasMore = offset + calls.length < totalMatched;
|
|
165
|
+
|
|
166
|
+
const paginationInfo = formatPaginationInfo({
|
|
167
|
+
limit,
|
|
168
|
+
offset,
|
|
169
|
+
returned: calls.length,
|
|
170
|
+
total: totalMatched,
|
|
171
|
+
hasMore,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const responseText =
|
|
175
|
+
`Found ${totalMatched} call(s) to "${className ? className + '.' : ''}${name}":${paginationInfo}\n` +
|
|
176
|
+
`- Resolved: ${resolved}\n` +
|
|
177
|
+
`- Unresolved: ${unresolved}\n\n` +
|
|
178
|
+
JSON.stringify(serializeBigInt(calls), null, 2);
|
|
179
|
+
|
|
180
|
+
return textResult(guardResponseSize(responseText));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function handleFindNodes(args: FindNodesArgs): Promise<ToolResult> {
|
|
184
|
+
const db = await ensureAnalyzed();
|
|
185
|
+
const { type, name, file, limit: requestedLimit, offset: requestedOffset } = args;
|
|
186
|
+
|
|
187
|
+
const limit = normalizeLimit(requestedLimit);
|
|
188
|
+
const offset = Math.max(0, requestedOffset || 0);
|
|
189
|
+
|
|
190
|
+
const filter: Record<string, unknown> = {};
|
|
191
|
+
if (type) filter.type = type;
|
|
192
|
+
if (name) filter.name = name;
|
|
193
|
+
if (file) filter.file = file;
|
|
194
|
+
|
|
195
|
+
const nodes: GraphNode[] = [];
|
|
196
|
+
let skipped = 0;
|
|
197
|
+
let totalMatched = 0;
|
|
198
|
+
|
|
199
|
+
for await (const node of db.queryNodes(filter)) {
|
|
200
|
+
totalMatched++;
|
|
201
|
+
|
|
202
|
+
if (skipped < offset) {
|
|
203
|
+
skipped++;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (nodes.length < limit) {
|
|
208
|
+
nodes.push(node);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (totalMatched === 0) {
|
|
213
|
+
return textResult('No nodes found matching criteria');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const hasMore = offset + nodes.length < totalMatched;
|
|
217
|
+
const paginationInfo = formatPaginationInfo({
|
|
218
|
+
limit,
|
|
219
|
+
offset,
|
|
220
|
+
returned: nodes.length,
|
|
221
|
+
total: totalMatched,
|
|
222
|
+
hasMore,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return textResult(
|
|
226
|
+
`Found ${totalMatched} node(s):${paginationInfo}\n\n${JSON.stringify(
|
|
227
|
+
serializeBigInt(nodes),
|
|
228
|
+
null,
|
|
229
|
+
2
|
|
230
|
+
)}`
|
|
231
|
+
);
|
|
232
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -39,14 +39,17 @@ import {
|
|
|
39
39
|
handleFindGuards,
|
|
40
40
|
handleReportIssue,
|
|
41
41
|
handleGetFunctionDetails,
|
|
42
|
+
handleGetContext,
|
|
42
43
|
handleReadProjectStructure,
|
|
43
44
|
handleWriteConfig,
|
|
44
|
-
|
|
45
|
+
handleGetFileOverview,
|
|
46
|
+
} from './handlers/index.js';
|
|
45
47
|
import type {
|
|
46
48
|
ToolResult,
|
|
47
49
|
ReportIssueArgs,
|
|
48
50
|
GetDocumentationArgs,
|
|
49
51
|
GetFunctionDetailsArgs,
|
|
52
|
+
GetContextArgs,
|
|
50
53
|
QueryGraphArgs,
|
|
51
54
|
FindCallsArgs,
|
|
52
55
|
FindNodesArgs,
|
|
@@ -62,6 +65,7 @@ import type {
|
|
|
62
65
|
FindGuardsArgs,
|
|
63
66
|
ReadProjectStructureArgs,
|
|
64
67
|
WriteConfigArgs,
|
|
68
|
+
GetFileOverviewArgs,
|
|
65
69
|
} from './types.js';
|
|
66
70
|
|
|
67
71
|
/**
|
|
@@ -203,6 +207,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
|
203
207
|
result = await handleGetFunctionDetails(asArgs<GetFunctionDetailsArgs>(args));
|
|
204
208
|
break;
|
|
205
209
|
|
|
210
|
+
case 'get_context':
|
|
211
|
+
result = await handleGetContext(asArgs<GetContextArgs>(args));
|
|
212
|
+
break;
|
|
213
|
+
|
|
214
|
+
case 'get_file_overview':
|
|
215
|
+
result = await handleGetFileOverview(asArgs<GetFileOverviewArgs>(args));
|
|
216
|
+
break;
|
|
217
|
+
|
|
206
218
|
case 'read_project_structure':
|
|
207
219
|
result = await handleReadProjectStructure(asArgs<ReadProjectStructureArgs>(args));
|
|
208
220
|
break;
|
package/src/types.ts
CHANGED
|
@@ -300,6 +300,21 @@ export interface GuardInfo {
|
|
|
300
300
|
line: number;
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
+
// === NODE CONTEXT (REG-406) ===
|
|
304
|
+
|
|
305
|
+
export interface GetContextArgs {
|
|
306
|
+
semanticId: string;
|
|
307
|
+
contextLines?: number;
|
|
308
|
+
edgeType?: string;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// === FILE OVERVIEW (REG-412) ===
|
|
312
|
+
|
|
313
|
+
export interface GetFileOverviewArgs {
|
|
314
|
+
file: string;
|
|
315
|
+
include_edges?: boolean;
|
|
316
|
+
}
|
|
317
|
+
|
|
303
318
|
// === PROJECT STRUCTURE (REG-173) ===
|
|
304
319
|
|
|
305
320
|
export interface ReadProjectStructureArgs {
|