Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .git-ai/lancedb.tar.gz
Git LFS file not shown
285 changes: 285 additions & 0 deletions src/mcp/handlers/astGraphHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
import type { ToolHandler } from '../types';
import { successResponse, errorResponse } from '../types';
import type {
AstGraphQueryArgs,
AstGraphFindArgs,
AstGraphChildrenArgs,
AstGraphRefsArgs,
AstGraphCallersArgs,
AstGraphCalleesArgs,
AstGraphChainArgs
} from '../schemas';
import { resolveGitRoot } from '../../core/git';
import {
runAstGraphQuery,
buildFindSymbolsQuery,
buildChildrenQuery,
buildFindReferencesQuery,
buildCallersByNameQuery,
buildCalleesByNameQuery,
buildCallChainDownstreamByNameQuery,
buildCallChainUpstreamByNameQuery
} from '../../core/astGraphQuery';
import { checkIndex, resolveLangs } from '../../core/indexCheck';
import { sha256Hex } from '../../core/crypto';
import { toPosixPath } from '../../core/paths';
import path from 'path';

export const handleAstGraphQuery: ToolHandler<AstGraphQueryArgs> = async (args) => {
const repoRoot = await resolveGitRoot(path.resolve(args.path));
const query = args.query;
const params = args.params ?? {};
const result = await runAstGraphQuery(repoRoot, query, params);

return successResponse({
repoRoot,
result
});
};

export const handleAstGraphFind: ToolHandler<AstGraphFindArgs> = async (args) => {
const repoRoot = await resolveGitRoot(path.resolve(args.path));
const prefix = args.prefix;
const limit = args.limit ?? 50;
const langSel = args.lang ?? 'auto';

const status = await checkIndex(repoRoot);
if (!status.ok) {
return errorResponse(
new Error('Index incompatible or missing'),
'index_incompatible'
);
}

const langs = resolveLangs(status.found.meta ?? null, langSel as any);
const allRows: any[] = [];

for (const lang of langs) {
const result = await runAstGraphQuery(
repoRoot,
buildFindSymbolsQuery(lang),
{ prefix, lang }
);
const rows = Array.isArray((result as any)?.rows)
? (result as any).rows
: [];
for (const r of rows) {
allRows.push(r);
}
}

return successResponse({
repoRoot,
lang: langSel,
result: {
headers: ['ref_id', 'file', 'lang', 'name', 'kind', 'signature', 'start_line', 'end_line'],
rows: allRows.slice(0, limit)
}
});
};

export const handleAstGraphChildren: ToolHandler<AstGraphChildrenArgs> = async (args) => {
const repoRoot = await resolveGitRoot(path.resolve(args.path));
const id = args.id;
const asFile = args.as_file ?? false;
const parent_id = asFile
? sha256Hex(`file:${toPosixPath(id)}`)
: id;
const result = await runAstGraphQuery(repoRoot, buildChildrenQuery(), {
parent_id
});

return successResponse({
repoRoot,
parent_id,
result
});
};

export const handleAstGraphRefs: ToolHandler<AstGraphRefsArgs> = async (args) => {
const repoRoot = await resolveGitRoot(path.resolve(args.path));
const target = args.name;
const limit = args.limit ?? 200;
const langSel = args.lang ?? 'auto';

const status = await checkIndex(repoRoot);
if (!status.ok) {
return errorResponse(
new Error('Index incompatible or missing'),
'index_incompatible'
);
}

const langs = resolveLangs(status.found.meta ?? null, langSel as any);
const allRows: any[] = [];

for (const lang of langs) {
const result = await runAstGraphQuery(
repoRoot,
buildFindReferencesQuery(lang),
{ name: target, lang }
);
const rows = Array.isArray((result as any)?.rows)
? (result as any).rows
: [];
for (const r of rows) {
allRows.push(r);
}
}

return successResponse({
repoRoot,
name: target,
lang: langSel,
result: {
headers: ['file', 'line', 'col', 'ref_kind', 'from_id', 'from_kind', 'from_name', 'from_lang'],
rows: allRows.slice(0, limit)
}
});
};

export const handleAstGraphCallers: ToolHandler<AstGraphCallersArgs> = async (args) => {
const repoRoot = await resolveGitRoot(path.resolve(args.path));
const target = args.name;
const limit = args.limit ?? 200;
const langSel = args.lang ?? 'auto';

const status = await checkIndex(repoRoot);
if (!status.ok) {
return errorResponse(
new Error('Index incompatible or missing'),
'index_incompatible'
);
}

const langs = resolveLangs(status.found.meta ?? null, langSel as any);
const allRows: any[] = [];

for (const lang of langs) {
const result = await runAstGraphQuery(
repoRoot,
buildCallersByNameQuery(lang),
{ name: target, lang }
);
const rows = Array.isArray((result as any)?.rows)
? (result as any).rows
: [];
for (const r of rows) {
allRows.push(r);
}
}

return successResponse({
repoRoot,
name: target,
lang: langSel,
result: {
headers: ['caller_id', 'caller_kind', 'caller_name', 'file', 'line', 'col', 'caller_lang'],
rows: allRows.slice(0, limit)
}
});
};

export const handleAstGraphCallees: ToolHandler<AstGraphCalleesArgs> = async (args) => {
const repoRoot = await resolveGitRoot(path.resolve(args.path));
const target = args.name;
const limit = args.limit ?? 200;
const langSel = args.lang ?? 'auto';

const status = await checkIndex(repoRoot);
if (!status.ok) {
return errorResponse(
new Error('Index incompatible or missing'),
'index_incompatible'
);
}

const langs = resolveLangs(status.found.meta ?? null, langSel as any);
const allRows: any[] = [];

for (const lang of langs) {
const result = await runAstGraphQuery(
repoRoot,
buildCalleesByNameQuery(lang),
{ name: target, lang }
);
const rows = Array.isArray((result as any)?.rows)
? (result as any).rows
: [];
for (const r of rows) {
allRows.push(r);
}
}

return successResponse({
repoRoot,
name: target,
lang: langSel,
result: {
headers: ['caller_id', 'caller_lang', 'callee_id', 'callee_file', 'callee_name', 'callee_kind', 'file', 'line', 'col'],
rows: allRows.slice(0, limit)
}
});
};

export const handleAstGraphChain: ToolHandler<AstGraphChainArgs> = async (args) => {
const repoRoot = await resolveGitRoot(path.resolve(args.path));
const target = args.name;
const direction = args.direction ?? 'downstream';
const maxDepth = args.max_depth ?? 3;
const limit = args.limit ?? 500;
const minNameLen = Math.max(1, args.min_name_len ?? 1);
const langSel = args.lang ?? 'auto';

const status = await checkIndex(repoRoot);
if (!status.ok) {
return errorResponse(
new Error('Index incompatible or missing'),
'index_incompatible'
);
}

const langs = resolveLangs(status.found.meta ?? null, langSel as any);
const query =
direction === 'upstream'
? buildCallChainUpstreamByNameQuery()
: buildCallChainDownstreamByNameQuery();
const rawRows: any[] = [];

for (const lang of langs) {
const result = await runAstGraphQuery(repoRoot, query, {
name: target,
max_depth: maxDepth,
lang
});
const rows = Array.isArray((result as any)?.rows)
? (result as any).rows
: [];
for (const r of rows) {
rawRows.push(r);
}
}

const filtered =
minNameLen > 1
? rawRows.filter(
(r: any[]) =>
String(r?.[3] ?? '').length >= minNameLen &&
String(r?.[4] ?? '').length >= minNameLen
)
: rawRows;
const rows = filtered.slice(0, limit);

return successResponse({
repoRoot,
name: target,
lang: langSel,
direction,
max_depth: maxDepth,
min_name_len: minNameLen,
result: {
headers: ['caller_id', 'callee_id', 'depth', 'caller_name', 'callee_name', 'lang'],
rows
}
});
};
75 changes: 75 additions & 0 deletions src/mcp/handlers/dsrHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { ToolHandler } from '../types';
import { successResponse, errorResponse } from '../types';
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import errorResponse.

Suggested change
import { successResponse, errorResponse } from '../types';
import { successResponse } from '../types';

Copilot uses AI. Check for mistakes.
import type {
DsrContextArgs,
DsrGenerateArgs,
DsrRebuildIndexArgs,
DsrSymbolEvolutionArgs
} from '../schemas';
import { resolveGitRoot } from '../../core/git';
import { detectRepoGitContext } from '../../core/dsr/gitContext';
import { generateDsrForCommit } from '../../core/dsr/generate';
import { materializeDsrIndex } from '../../core/dsr/indexMaterialize';
import { symbolEvolution } from '../../core/dsr/query';
import { getDsrDirectoryState } from '../../core/dsr/state';
import path from 'path';

export const handleDsrContext: ToolHandler<DsrContextArgs> = async (args) => {
const repoRoot = await resolveGitRoot(path.resolve(args.path));
const ctx = await detectRepoGitContext(repoRoot);
const state = await getDsrDirectoryState(ctx.repo_root);

return successResponse({
commit_hash: ctx.head_commit,
repo_root: ctx.repo_root,
branch: ctx.branch,
detached: ctx.detached,
dsr_directory_state: state
});
};

export const handleDsrGenerate: ToolHandler<DsrGenerateArgs> = async (args) => {
const repoRoot = await resolveGitRoot(path.resolve(args.path));
const commit = args.commit ?? 'HEAD';
const res = await generateDsrForCommit(repoRoot, commit);

return successResponse({
commit_hash: res.dsr.commit_hash,
file_path: res.file_path,
existed: res.existed,
counts: {
affected_symbols: res.dsr.affected_symbols.length,
ast_operations: res.dsr.ast_operations.length
},
semantic_change_type: res.dsr.semantic_change_type,
risk_level: res.dsr.risk_level
});
};

export const handleDsrRebuildIndex: ToolHandler<DsrRebuildIndexArgs> = async (args) => {
const repoRoot = await resolveGitRoot(path.resolve(args.path));
const res = await materializeDsrIndex(repoRoot);

return successResponse({
repoRoot,
...res
});
};
Comment on lines +49 to +57
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleDsrRebuildIndex wraps materializeDsrIndex with successResponse(...), so the MCP isError flag is never set even when res.enabled is false. Previously the tool surfaced failure via isError. Adjust the response to mark failures as errors (or switch to errorResponse) when enabled is false.

Copilot uses AI. Check for mistakes.

export const handleDsrSymbolEvolution: ToolHandler<DsrSymbolEvolutionArgs> = async (args) => {
const repoRoot = await resolveGitRoot(path.resolve(args.path));
const symbol = args.symbol;
const opts = {
start: args.start,
all: args.all ?? false,
limit: args.limit ?? 200,
contains: args.contains ?? false
};
const res = await symbolEvolution(repoRoot, symbol, opts);

return successResponse({
repoRoot,
symbol,
...res
});
};
Comment on lines +59 to +75
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleDsrSymbolEvolution always returns successResponse({ ...res }), so if symbolEvolution returns ok: false the MCP isError flag still won't be set, and server logs will record the call as successful. Consider marking the CallToolResult as error (or using errorResponse) when res.ok is false to match prior behavior.

Copilot uses AI. Check for mistakes.
Loading
Loading