/**
* view_code_items 工具 (批量版)
* 查看多个特定的代码项(类、方法或函数)
*
* 推荐使用场景:
* 1. 已明确文件路径与目标名称,需要快速查看类/方法/函数的完整定义块。
* 2. 想对接口与实现类进行对比,确认接口声明与实现逻辑是否一致。
* 3. 需要在分析调用链之前,先精确抽取关键方法的实现代码片段。
*
* 能做到的功能:
* 1. 按“文件路径 + 代码项名称”批量定位并返回定义块或附近片段。
* 2. 对 Java 接口自动尝试匹配 Impl 实现类,并返回同名方法/类片段。
* 3. 提取方法片段中的字段调用,并尝试推导字段对应类型的类路径。
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import * as fs from "fs";
import * as path from "path";
import { pathExists, isFile, getExtension } from "../utils/fileSystem.js";
import { parseOutline, OutlineItem } from "./viewFileOutline.js";
/**
* 默认忽略的目录名称集合。
*/
const DEFAULT_IGNORE_DIRS = new Set([
".git",
".idea",
".vscode",
"node_modules",
"dist",
"build",
"out",
"target",
".gradle",
".mvn",
"logs",
"log",
"tmp",
"temp",
]);
/**
* Java 类路径搜索缓存(key: 根目录|类型|类名)。
*/
const JAVA_CLASS_SEARCH_CACHE = new Map<string, string[]>();
/**
* Java 根目录缓存(key: 项目根目录)。
*/
const JAVA_ROOT_CACHE = new Map<string, string[]>();
/**
* 代码项提取结果。
*/
type SnippetResult = {
found: boolean;
text: string;
startLine?: number;
endLine?: number;
};
/**
* 提取指定代码项的片段信息。
* @param filePath 文件路径
* @param itemName 代码项名称
*/
function extractSnippet(filePath: string, itemName: string): SnippetResult {
if (!pathExists(filePath) || !isFile(filePath)) {
return { found: false, text: `--- 文件: ${filePath} (错误: 文件不存在或无效) ---` };
}
try {
const content = fs.readFileSync(filePath, "utf-8");
const ext = getExtension(filePath);
const lines = content.split("\n");
if (ext === "java") {
const outline = parseOutline(content, ext);
const outlineRange = findOutlineRange(lines, outline, itemName);
if (outlineRange) {
const snippet = lines.slice(outlineRange.start, outlineRange.end + 1).join("\n");
return {
found: true,
text: `--- 文件: ${filePath} (第 ${outlineRange.start + 1} 行至第 ${outlineRange.end + 1} 行, 定位: '${itemName}') ---\n\n${snippet}`,
startLine: outlineRange.start + 1,
endLine: outlineRange.end + 1,
};
}
}
const itemIndex = lines.findIndex(line => line.includes(itemName));
if (itemIndex === -1) {
return { found: false, text: `--- 文件: ${filePath} (错误: 找不到代码项 '${itemName}') ---` };
}
const blockRange = findBlockRange(lines, itemIndex);
if (!blockRange) {
const start = Math.max(0, itemIndex - 10);
const end = Math.min(lines.length, itemIndex + 60);
const snippet = lines.slice(start, end).join("\n");
return {
found: true,
text: `--- 文件: ${filePath} (第 ${start + 1} 行至第 ${end} 行, 定位: '${itemName}') ---\n\n${snippet}`,
startLine: start + 1,
endLine: end,
};
}
const snippet = lines.slice(blockRange.start, blockRange.end + 1).join("\n");
return {
found: true,
text: `--- 文件: ${filePath} (第 ${blockRange.start + 1} 行至第 ${blockRange.end + 1} 行, 定位: '${itemName}') ---\n\n${snippet}`,
startLine: blockRange.start + 1,
endLine: blockRange.end + 1,
};
} catch (error) {
return {
found: false,
text: `--- 文件: ${filePath} (错误: 读取失败 - ${error instanceof Error ? error.message : String(error)}) ---`
};
}
}
/**
* 基于大纲定位代码块范围。
* @param lines 文件行列表
* @param outline 大纲列表
* @param itemName 代码项名称
*/
function findOutlineRange(lines: string[], outline: OutlineItem[], itemName: string): { start: number; end: number } | null {
const target = pickOutlineItem(outline, itemName);
if (!target) {
return null;
}
const startIndex = Math.max(0, findSignatureStart(lines, Math.max(0, target.startLine - 1)));
const endIndex = Math.min(lines.length - 1, (target.endLine ?? target.startLine) - 1);
if (endIndex < startIndex) {
return null;
}
return { start: startIndex, end: endIndex };
}
/**
* 从大纲中选择最匹配的条目。
* @param outline 大纲列表
* @param itemName 代码项名称
*/
function pickOutlineItem(outline: OutlineItem[], itemName: string): OutlineItem | null {
const candidates = outline.filter(item => item.name === itemName);
if (candidates.length === 0) {
return null;
}
const typeWeight: Record<OutlineItem["type"], number> = {
method: 1,
function: 1,
class: 2,
interface: 2,
field: 3,
other: 4,
};
candidates.sort((a, b) => {
const weightDiff = typeWeight[a.type] - typeWeight[b.type];
if (weightDiff !== 0) {
return weightDiff;
}
return a.startLine - b.startLine;
});
return candidates[0];
}
/**
* 根据接口文件路径推导实现类文件路径。
* @param filePath 接口文件路径
*/
function buildImplFilePath(filePath: string): string | null {
if (!filePath.endsWith(".java")) {
return null;
}
const fileName = path.basename(filePath);
if (!fileName.endsWith("Service.java")) {
return null;
}
const implFileName = fileName.replace("Service.java", "ServiceImpl.java");
const serviceDirToken = `${path.sep}service${path.sep}`;
const implDirToken = `${path.sep}service${path.sep}impl${path.sep}`;
if (filePath.includes(implDirToken)) {
return path.join(path.dirname(filePath), implFileName);
}
if (filePath.includes(serviceDirToken)) {
return filePath.replace(serviceDirToken, implDirToken).replace(fileName, implFileName);
}
return path.join(path.dirname(filePath), implFileName);
}
/**
* 基于花括号匹配推导完整代码块范围。
* @param lines 文件行列表
* @param hitIndex 命中行索引
*/
function findBlockRange(lines: string[], hitIndex: number): { start: number; end: number } | null {
const signatureIndex = findSignatureStart(lines, hitIndex);
const signatureEndIndex = findSignatureEndIndex(lines, signatureIndex);
if (signatureEndIndex !== -1) {
return { start: signatureIndex, end: signatureEndIndex };
}
const openIndex = findOpenBraceIndex(lines, signatureIndex);
if (openIndex === -1) {
return null;
}
const endIndex = findMatchingBraceIndex(lines, openIndex);
if (endIndex === -1) {
return null;
}
return { start: signatureIndex, end: endIndex };
}
/**
* 向上定位方法/类签名的起始行(包含注解与注释)。
* @param lines 文件行列表
* @param hitIndex 命中行索引
*/
function findSignatureStart(lines: string[], hitIndex: number): number {
let index = hitIndex;
while (index > 0) {
const line = lines[index - 1];
const trimmed = line.trim();
if (trimmed === "") {
index -= 1;
continue;
}
if (trimmed.startsWith("@") || trimmed.startsWith("*") || trimmed.startsWith("/**") || trimmed.startsWith("*/")) {
index -= 1;
continue;
}
if (trimmed.endsWith(";") || trimmed.endsWith("}") || trimmed.startsWith("}")) {
break;
}
break;
}
return index;
}
/**
* 从指定行开始向下查找第一个左花括号位置。
* @param lines 文件行列表
* @param startIndex 起始行索引
*/
function findOpenBraceIndex(lines: string[], startIndex: number): number {
for (let i = startIndex; i < lines.length; i += 1) {
if (lines[i].includes("{")) {
return i;
}
}
return -1;
}
/**
* 向下查找方法签名结束位置(接口/抽象方法的分号)。
* @param lines 文件行列表
* @param startIndex 起始行索引
*/
function findSignatureEndIndex(lines: string[], startIndex: number): number {
for (let i = startIndex; i < lines.length; i += 1) {
const line = lines[i];
if (line.includes("{")) {
return -1;
}
if (line.includes(";")) {
return i;
}
}
return -1;
}
/**
* 从左花括号行开始进行花括号计数匹配。
* @param lines 文件行列表
* @param openIndex 左花括号行索引
*/
function findMatchingBraceIndex(lines: string[], openIndex: number): number {
let depth = 0;
for (let i = openIndex; i < lines.length; i += 1) {
const line = lines[i];
for (const char of line) {
if (char === "{") {
depth += 1;
} else if (char === "}") {
depth -= 1;
if (depth === 0) {
return i;
}
}
}
}
return -1;
}
/**
* 根据文件路径推导实现类名称。
* @param filePath 文件路径
*/
function buildImplClassName(filePath: string): string | null {
if (!filePath.endsWith(".java")) {
return null;
}
const baseName = path.basename(filePath, ".java");
if (baseName.endsWith("Impl")) {
return baseName;
}
return `${baseName}Impl`;
}
/**
* 从类文件中提取字段与类型映射。
* @param lines 文件行列表
*/
function extractFieldTypeMap(lines: string[]): Map<string, string> {
const map = new Map<string, string>();
const fieldRegex = /^\s*(private|protected|public)\s+(final\s+)?([A-Za-z_][\w<>]*)\s+([A-Za-z_][\w]*)\s*;/;
for (const line of lines) {
const match = line.match(fieldRegex);
if (!match) {
continue;
}
const rawType = match[3];
const fieldName = match[4];
const cleanType = rawType.replace(/<.*>/g, "");
map.set(fieldName, cleanType);
}
return map;
}
/**
* 从方法代码中提取调用到的字段名称。
* @param snippet 代码片段
*/
function extractCalledFields(snippet: string): Set<string> {
const result = new Set<string>();
const callRegex = /\b([A-Za-z_][\w]*)\s*\./g;
let match: RegExpExecArray | null;
while ((match = callRegex.exec(snippet)) !== null) {
result.add(match[1]);
}
return result;
}
/**
* 生成方法调用类路径说明。
* @param filePath 文件路径
* @param snippetText 代码片段文本
*/
function buildCallPathSection(filePath: string, snippetText: string): string | null {
if (!filePath.endsWith(".java")) {
return null;
}
if (!pathExists(filePath) || !isFile(filePath)) {
return null;
}
const searchRoot = resolveSearchRoot(filePath);
const content = fs.readFileSync(filePath, "utf-8");
const lines = content.split("\n");
const fieldTypeMap = extractFieldTypeMap(lines);
const calledFields = extractCalledFields(snippetText);
const callEntries: string[] = [];
calledFields.forEach(fieldName => {
const typeName = fieldTypeMap.get(fieldName);
if (!typeName) {
return;
}
const classFiles = findClassFiles(searchRoot, typeName);
if (classFiles.length === 0) {
callEntries.push(`${fieldName} -> ${typeName} -> 未找到路径`);
return;
}
classFiles.forEach(classFile => {
callEntries.push(`${fieldName} -> ${typeName} -> ${classFile}`);
});
});
if (callEntries.length === 0) {
return null;
}
return ["--- 方法调用类路径 ---", ...callEntries].join("\n");
}
/**
* 解析检索应使用的项目根目录。
* @param filePath 文件路径
*/
function resolveSearchRoot(filePath: string): string {
return findProjectRoot(filePath) ?? process.cwd();
}
/**
* 查找项目根目录(支持多模块 Maven/Gradle)。
* @param startPath 起始路径
*/
function findProjectRoot(startPath: string): string | null {
let current = path.dirname(startPath);
let lastSrcRoot: string | null = null;
let lastModuleRoot: string | null = null;
let lastBuildRoot: string | null = null;
while (current !== path.parse(current).root) {
const srcPath = path.join(current, "src");
const pomPath = path.join(current, "pom.xml");
const buildGradlePath = path.join(current, "build.gradle");
const settingsGradlePath = path.join(current, "settings.gradle");
if (fs.existsSync(srcPath)) {
lastSrcRoot = current;
}
if (fs.existsSync(pomPath) && hasModuleJavaRoots(current)) {
lastModuleRoot = current;
}
if (fs.existsSync(buildGradlePath) || fs.existsSync(settingsGradlePath)) {
lastBuildRoot = current;
}
current = path.dirname(current);
}
if (lastModuleRoot) {
return lastModuleRoot;
}
return lastBuildRoot ?? lastSrcRoot;
}
/**
* 判断目录下是否存在多个 Java 模块目录。
* @param root 根目录
*/
function hasModuleJavaRoots(root: string): boolean {
try {
const entries = fs.readdirSync(root, { withFileTypes: true });
let count = 0;
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
const javaRoot = path.join(root, entry.name, "src/main/java");
if (fs.existsSync(javaRoot)) {
count += 1;
if (count >= 2) {
return true;
}
}
}
} catch (e) {
return false;
}
return false;
}
/**
* 获取项目内所有 Java 根目录(src/main/java)。
* @param rootDir 项目根目录
*/
function getJavaRoots(rootDir: string): string[] {
const cached = JAVA_ROOT_CACHE.get(rootDir);
if (cached) {
return cached;
}
const roots: string[] = [];
const directJavaRoot = path.join(rootDir, "src/main/java");
if (fs.existsSync(directJavaRoot)) {
roots.push(directJavaRoot);
}
const stack: string[] = [rootDir];
while (stack.length > 0) {
const current = stack.pop();
if (!current) {
continue;
}
let entries: fs.Dirent[] = [];
try {
entries = fs.readdirSync(current, { withFileTypes: true });
} catch (error) {
continue;
}
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
if (DEFAULT_IGNORE_DIRS.has(entry.name)) {
continue;
}
const fullPath = path.join(current, entry.name);
const javaRoot = path.join(fullPath, "src/main/java");
if (fs.existsSync(javaRoot)) {
roots.push(javaRoot);
continue;
}
stack.push(fullPath);
}
}
const uniqueRoots = Array.from(new Set(roots));
JAVA_ROOT_CACHE.set(rootDir, uniqueRoots);
return uniqueRoots;
}
/**
* 生成 Java 类搜索缓存 Key。
* @param rootDir 根目录
* @param kind 类型
* @param className 类名
*/
function buildJavaCacheKey(rootDir: string, kind: "class" | "impl", className: string): string {
return `${rootDir}|${kind}|${className}`;
}
/**
* 在 Java 根目录中检索指定类名文件。
* @param javaRoots Java 根目录列表
* @param className 类名
* @param classRegex 类定义匹配正则
*/
function scanJavaRootsForClass(javaRoots: string[], className: string, classRegex: RegExp): string[] {
const results: string[] = [];
const targetFileName = `${className}.java`;
const stack: string[] = [...javaRoots];
while (stack.length > 0) {
const current = stack.pop();
if (!current) {
continue;
}
let entries: fs.Dirent[] = [];
try {
entries = fs.readdirSync(current, { withFileTypes: true });
} catch (error) {
continue;
}
for (const entry of entries) {
if (entry.isDirectory()) {
if (DEFAULT_IGNORE_DIRS.has(entry.name)) {
continue;
}
stack.push(path.join(current, entry.name));
continue;
}
if (!entry.isFile() || entry.name !== targetFileName) {
continue;
}
const filePath = path.join(current, entry.name);
try {
const content = fs.readFileSync(filePath, "utf-8");
if (classRegex.test(content)) {
results.push(filePath);
}
} catch (error) {
continue;
}
}
}
return results;
}
/**
* 全局搜索指定类名对应的 Java 文件。
* @param rootDir 搜索根目录
* @param className 类名
*/
function findClassFiles(rootDir: string, className: string): string[] {
const cacheKey = buildJavaCacheKey(rootDir, "class", className);
const cached = JAVA_CLASS_SEARCH_CACHE.get(cacheKey);
if (cached) {
return cached;
}
const javaRoots = getJavaRoots(rootDir);
const classRegex = new RegExp(`\\b(class|interface|enum|record)\\s+${className}\\b`);
const results = scanJavaRootsForClass(javaRoots, className, classRegex);
JAVA_CLASS_SEARCH_CACHE.set(cacheKey, results);
return results;
}
/**
* 全局搜索指定实现类名对应的 Java 文件。
* @param rootDir 搜索根目录
* @param implClassName 实现类名称
*/
function findImplFiles(rootDir: string, implClassName: string): string[] {
const cacheKey = buildJavaCacheKey(rootDir, "impl", implClassName);
const cached = JAVA_CLASS_SEARCH_CACHE.get(cacheKey);
if (cached) {
return cached;
}
const javaRoots = getJavaRoots(rootDir);
const classRegex = new RegExp(`\\bclass\\s+${implClassName}\\b`);
const results = scanJavaRootsForClass(javaRoots, implClassName, classRegex);
JAVA_CLASS_SEARCH_CACHE.set(cacheKey, results);
return results;
}
/**
* 注册 view_code_items 工具到 MCP 服务器
* @param server MCP 服务器实例
*/
export function registerViewCodeItem(server: McpServer): void {
server.tool(
"view_code_items",
"[Precise Code Retrieval] Best for cases where the file path and item name are known. Batch returns full definition blocks for classes/methods/functions. If the input is a Java interface and implementations exist, it will attempt to traverse to Impl classes and return same-named method snippets (multiple implementations are supported and labeled). It also parses fields referenced in the method snippet to infer their type-to-class paths. Ideal for interface/implementation comparison, fast location of key methods, and precise extraction before call-chain analysis; if the path or name is unknown, use file search tools first.",
{
// Batch request list
Items: z.array(z.object({
File: z.string().describe("Absolute path of the file"),
ItemName: z.string().describe("Exact name of the code item to locate (e.g., class name 'UserService' or method name 'findUser')")
})).describe("List of code items to query."),
} as any,
async (input: any) => {
const { Items } = input;
if (!Array.isArray(Items) || Items.length === 0) {
return {
content: [{ type: "text", text: JSON.stringify({ error: "请提供至少一个待查询的代码项" }) }],
} as any;
}
// 并发处理所有请求
const results = await Promise.all(Items.map(async (item) => {
const { File, ItemName } = item;
const searchRoot = resolveSearchRoot(File);
const resultSnippets: string[] = [];
const mainSnippet = extractSnippet(File, ItemName);
resultSnippets.push(mainSnippet.text);
const mainCallSection = buildCallPathSection(File, mainSnippet.text);
if (mainCallSection) {
resultSnippets.push(mainCallSection);
}
const implCandidates = new Set<string>();
const implFilePath = buildImplFilePath(File);
if (implFilePath && implFilePath !== File) {
implCandidates.add(implFilePath);
}
const implClassName = buildImplClassName(File);
if (implClassName) {
const implFiles = findImplFiles(searchRoot, implClassName);
implFiles.forEach(filePath => implCandidates.add(filePath));
}
if (implCandidates.size > 0) {
const implList = Array.from(implCandidates).filter(filePath => filePath !== File);
let implIndex = 1;
for (const filePath of implList) {
const implSnippet = extractSnippet(filePath, ItemName);
if (!implSnippet.found) {
continue;
}
const implHeader = `--- Impl匹配: ${implClassName ?? "未知实现类"} (${implIndex}/${implList.length}) ---`;
resultSnippets.push(`${implHeader}\n${implSnippet.text}`);
const implCallSection = buildCallPathSection(filePath, implSnippet.text);
if (implCallSection) {
resultSnippets.push(implCallSection);
}
implIndex += 1;
}
}
return resultSnippets.join("\n\n");
}));
return {
content: [
{
type: "text",
text: results.join("\n\n" + "=".repeat(30) + "\n\n"),
},
],
} as any;
}
);
}