Slnmap:基于Roslyn的Code Graph MCP Server,让AI读懂.NET大型代码库

Slnmap:基于Roslyn的Code Graph MCP Server,让AI读懂.NET大型代码库 如果你维护过大型 .NET 解决方案一定遇到过这样的场景项目里有几十个 csproj类与类之间的依赖关系盘根错节方法调用链跨越了三四个程序集。本地调试还能靠 IDE 的“转到定义”一点点摸但当你试图让 AI 编程助手帮你改代码、做重构、解释业务逻辑时它却常常答非所问——因为 AI 看到的只是你贴给它的那几个文件它看不到整个解决方案的依赖关系和调用上下文。这个问题的本质是传统 AI 编程工具缺少对代码库全局结构的感知能力。最近在 Hacker News 看到的一个项目很有意思Slnmap一个基于 Roslyn 的 code graph MCP server专门为 .NET 代码库服务。它把 Roslyn 的语法树和符号分析能力与 MCPModel Context Protocol结合起来让 AI 编程助手可以按需查询代码库的依赖图、调用关系、类型结构等语义信息。这篇文章会从实际工程角度拆解 Slnmap它解决的是什么问题、核心原理是什么、怎么配置和启动、如何在 Cursor / Claude Desktop / 自研 Agent 中接入它以及真实项目中的坑和最佳实践。1. 这篇文章真正要解决的问题先说一个经常被忽略的事实给 AI 编程助手“喂文件”和让它“理解代码库”是两回事。如果你用过 Copilot Chat、Cursor 或 Claude Code 处理大型解决方案大概率遇到过以下几种情况上下文窗口不够用。一个大型 .NET 解决方案可能有几百个源文件全部塞进上下文既不可能也没必要但 AI 需要的那几个关键文件恰恰不在上下文里。缺少依赖关系信息。AI 看到一个OrderService类但它不知道OrderService依赖了哪些仓储接口、被哪个控制器调用、实现了哪个抽象基类。这些信息不直接写在当前文件里却决定了重构时会不会破坏其他模块。符号解析不准确。同名类、Partial 类、通过依赖注入注册的接口实现只看源码文本很难判断运行时真正用的是哪个实现。人工挑选上下文太累。让开发者手动把相关文件拖进对话在大型代码库里筛选成本极高。Slnmap 的思路是把代码库的图谱信息通过 MCP 协议暴露给 AI 助手让 AI 按需查询而不是靠猜。Roslyn 在这里起到的作用是“语义分析引擎”。它不是简单读取文本而是能够解析出完整的符号信息类型在哪里定义、方法被谁调用、项目之间谁引用谁。Slnmap 把 Roslyn 分析出的这些信息构造成一张 code graph再通过 MCP server 提供给 AI 客户端查询。换句话说Slnmap 让 AI 从“读文件”升级为“读代码库”。这是本篇要讲的核心判断。2. 三个绕不开的概念Roslyn、代码图与 MCP在动手操作之前需要先弄清楚三个基础概念。如果这三个概念没有对齐后面配置和调试的时候会很难受。2.1 Roslyn不只是编译器Roslyn 是微软开源的 C# 和 Visual Basic 编译器平台但它和传统编译器最大的区别是Roslyn 把自己做成了一组 API。传统编译器是“源码进、程序集出”的黑盒你很难在编译过程中拿到中间信息。Roslyn 则把编译过程拆成多个可访问的阶段Syntax Tree语法树代码的语法结构比如类声明、方法声明、语句块。Symbol符号语法树背后的语义信息比如OrderService这个类名对应哪个类型、GetOrderById这个方法的完整签名是什么。Semantic Model语义模型把语法树和符号关联起来告诉你某个标识符在特定上下文里到底引用了哪个类型或方法。Slnmap 用 Roslyn 不是为了编译出 DLL而是利用 Roslyn 的语义分析能力构建一个可供查询的代码关系网络。2.2 代码图Code Graph把代码变成关系数据“代码图”可以理解为代码库的“关系型抽象”。它关注的是实体之间的关系而不是具体代码内容。在 .NET 代码库中常见的图关系包括项目之间的 ProjectReference。类型之间的继承关系和接口实现。方法之间的调用关系Caller / Callee。类型之间的属性引用、方法参数引用、字段类型引用。举个例子CustomerController依赖ICustomerServiceCustomerService实现了ICustomerService并依赖ICustomerRepositorySqlCustomerRepository实现了ICustomerRepository。这段依赖链如果靠人肉梳理需要打开三四个文件来回跳。而代码图可以把这些关系直接输出为结构化数据{ type: CustomerController, dependsOn: [ICustomerService], implementedBy: [], references: [CustomerService, CustomerRepository] }2.3 MCPAI 世界的“USB-C 接口”MCPModel Context Protocol是 Anthropic 提出的开放协议目的是统一 AI 应用与外部数据、工具之间的连接方式。可以把它理解成 AI 领域的 USB-C 标准不同的工具通过统一协议接入不同的 AI 客户端不需要为每一个 AI 产品单独定制集成。MCP 协议中有几个核心角色MCP Server提供服务的一方把某个能力封装成协议接口。Slnmap 就是一个 MCP Server。MCP Client消费服务的一方比如 Claude Desktop、Cursor或者你自己写的 Agent。ToolServer 暴露给 Client 调用的具体能力比如“查询类型依赖”“查找方法调用链”。Slnmap 属于 MCP Server它把 Roslyn 分析结果包装成若干 ToolAI 客户端通过标准协议调用这些 Tool就能拿到结构化的代码图谱信息。2.4 三者的关系用一句话概括Roslyn 负责分析代码图负责存储MCP 负责传输。Slnmap 启动时先用 Roslyn 加载目标解决方案或项目分析出符号和关系构建代码图然后启动一个 MCP ServerAI 编程助手通过 MCP 协议向 Server 发起查询请求Server 返回图谱数据。整个过程不需要把整个代码库塞进上下文。3. Slnmap 工作原理与架构拆解从架构上看Slnmap 可以分为三个层次分析层、图构建层、服务层。3.1 分析层Roslyn 项目加载与符号解析Slnmap 首先需要定位目标代码库这一步通常通过传入.sln或.csproj文件路径来完成。Roslyn 提供了MSBuildWorkspace类可以从解决方案文件加载项目并获取每个项目的Compilation对象。Compilation是所有语义分析的入口// 示意代码使用 MSBuildWorkspace 加载解决方案 using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.MSBuild; var workspace MSBuildWorkspace.Create(); var solution await workspace.OpenSolutionAsync(path/to/YourSolution.sln); foreach (var project in solution.Projects) { var compilation await project.GetCompilationAsync(); // 拿到 compilation 后可以遍历语法树、获取符号、分析依赖 }这里的关键点是Slnmap 分析的是Roslyn 的语义模型Semantic Model而不是简单的文本匹配。这意味着它能准确区分两个同名但来自不同命名空间的类型。通过别名引用的类型。泛型实例化后的实际类型参数。扩展方法实际调用的静态类。这些信息仅靠 GPT 的文本理解是拿不到的而 Roslyn 可以精确给出。3.2 图构建层提取并存储关系拿到Compilation之后Slnmap 会遍历所有语法树提取两类信息类型级信息类型名称、命名空间、文件路径。基类、实现的接口。类型中声明的成员属性、方法、字段。每个成员的类型引用。依赖级信息类 A 是否在方法体内调用了类 B 的方法。类 A 是否在属性类型中引用了类 B。项目 X 是否引用了项目 Y。方法 M 被哪些方法调用。这些关系会组织成一个图数据结构。在实际存储上可以选择内存数据结构适合单次会话分析也可以序列化成 JSON、图数据库或内存数据库以便反复查询。3.3 服务层MCP 工具暴露Slnmap 启动 MCP Server 后会暴露一组查询工具。根据项目定位常见工具包括get_project_structure获取解决方案的项目列表和依赖关系。get_type_info按名称查询类型的定义位置、基类、接口和成员。get_dependencies查询某个类型的直接依赖和反向依赖。get_callers查询某个方法被谁调用。get_callees查询某个方法调用了哪些方法。AI 客户端拿到这些工具后就能在对话中按需调用。例如用户问“OrderService 的 ExecuteAsync 被哪些地方调用了”AI 不会自己瞎猜而是调用get_callers工具拿到 Slnmap 返回的结构化方法调用列表。3.4 为什么这个设计比“贴文件”更好这里做一个对比更容易理解对比维度传统方式把文件贴给 AISlnmap 方式MCP 查询上下文占用随文件数量线性增长只占用查询结果关系准确性依赖 AI 推断容易出错Roslyn 语义分析精确大型代码库支持很差窗口很快打满按需查询天然支持信息完整度只看到贴进去的文件能看到全库关系交互体验用户手动挑选文件AI 自主调用工具这个设计的核心价值是上下文窗口不再是代码库大小的瓶颈。4. 环境准备与前置条件Slnmap 是面向 .NET 生态的工具所以环境准备以 .NET SDK 为核心。4.1 运行环境清单依赖项说明.NET SDK建议使用当前 LTS 版本实际项目请以官方 README 要求为准操作系统Windows、macOS、Linux 均可.NET 本身跨平台目标代码库需要分析解决方案中包含 .NET 项目.NET Framework 或 .NET / .NET Core 均可MCP 客户端Claude Desktop、Cursor、自研 MCP Client 等4.2 安装与启动如果目标是快速体验一般方式是从源码运行或安装发布包。这里给出通用的启动方式# 克隆代码库如果 Slnmap 是开放源码项目 git clone https://github.com/your-repo/slnmap.git cd slnmap # 还原依赖并构建 dotnet restore dotnet build # 启动 MCP Server 并指定要分析的目标解决方案 dotnet run --project src/Slnmap -- --solution /path/to/YourSolution.sln --transport stdio另一种常见方式是通过npx或其他包管理工具直接运行具体取决于项目发布形式。无论哪种方式核心启动参数都会包括--solution或--project指定要分析的代码库入口。--transportMCP 传输方式常见有stdio标准输入输出和sseHTTP 流式。4.3 验证 MCP Server 是否启动成功对于stdio传输模式启动后进程会等待标准输入上的 JSON-RPC 消息。你可以在终端里手动输入一条 MCPinitialize请求来验证{jsonrpc:2.0,id:1,method:initialize,params:{protocolVersion:2024-11-05,capabilities:{},clientInfo:{name:test,version:1.0.0}}}如果服务正常你会收到包含 server 信息和 capabilities 的响应。如果没有任何输出说明启动阶段就失败了优先检查路径参数和依赖还原是否成功。5. 核心流程拆解让 Slnmap 跑起来这一节我们拆解一个完整的最小流程从准备测试代码库到启动 Slnmap再到通过 MCP 调用工具获取代码图。5.1 准备一个示例解决方案为了演示我们创建一个最简但包含依赖关系的解决方案MyShop/ ├── MyShop.sln ├── src/ │ ├── MyShop.Domain/ │ │ └── Customer.cs │ ├── MyShop.Application/ │ │ └── CustomerService.cs │ └── MyShop.Api/ │ ├── Controllers/ │ │ └── CustomerController.cs │ └── MyShop.Api.csproj各文件内容如下// 文件路径src/MyShop.Domain/Customer.cs namespace MyShop.Domain; public class Customer { public int Id { get; set; } public string Name { get; set; } string.Empty; }// 文件路径src/MyShop.Application/CustomerService.cs using MyShop.Domain; namespace MyShop.Application; public interface ICustomerRepository { TaskCustomer? GetByIdAsync(int id); } public class CustomerService { private readonly ICustomerRepository _repository; public CustomerService(ICustomerRepository repository) { _repository repository; } public async TaskCustomer? GetCustomerAsync(int id) { return await _repository.GetByIdAsync(id); } }// 文件路径src/MyShop.Api/Controllers/CustomerController.cs using Microsoft.AspNetCore.Mvc; using MyShop.Application; namespace MyShop.Api.Controllers; [ApiController] [Route(api/customers)] public class CustomerController : ControllerBase { private readonly CustomerService _service; public CustomerController(CustomerService service) { _service service; } [HttpGet({id})] public async TaskActionResultCustomer? Get(int id) { var customer await _service.GetCustomerAsync(id); if (customer is null) return NotFound(); return Ok(customer); } }三个项目之间的依赖关系很清晰MyShop.Api引用MyShop.Application。MyShop.Application引用MyShop.Domain。CustomerController依赖CustomerService。CustomerService依赖ICustomerRepository。这个例子虽然小但已经包含项目引用、接口依赖、类依赖、方法调用四种图关系。5.2 启动 Slnmap 并指向示例解决方案dotnet run --project src/Slnmap -- \ --solution /path/to/MyShop/MyShop.sln \ --transport stdio启动日志如果显示类似“MCP server started”的信息说明服务已经就绪。在stdio模式下你会看到进程挂起等待输入这是正常行为。5.3 调用工具查询项目依赖MCP 协议中调用工具使用tools/call方法。假设 Slnmap 暴露了get_project_dependencies工具请求格式如下{jsonrpc:2.0,id:2,method:tools/call,params:{name:get_project_dependencies,arguments:{projectName:MyShop.Api}}}预期响应中会包含 MyShop.Api 直接和间接依赖的项目信息类似{ jsonrpc: 2.0, id: 2, result: { content: [ { type: text, text: [{\Project\:\MyShop.Api\,\DependsOn\:[\MyShop.Application\,\MyShop.Domain\]}] } ] } }这个查询结果说明MyShop.Api不仅直接引用了MyShop.Application还通过传递引用依赖了MyShop.Domain。5.4 查询类型反向依赖假设你想知道CustomerService被谁用到可以调用对应的反向依赖查询工具{jsonrpc:2.0,id:3,method:tools/call,params:{name:get_reverse_dependencies,arguments:{typeName:CustomerService}}}从代码库结构看CustomerController构造时注入了CustomerService所以查询结果会指向CustomerController。这个能力在重构时非常有价值改一个构造函数签名前先查一下谁在用它。6. 在 AI 编程助手中接入 SlnmapSlnmap 的价值要在 AI 客户端中才能真正体现。这里分别说明在两种场景下的接入方式。6.1 在 Claude Desktop 中注册 MCP ServerClaude Desktop 支持通过配置文件注册 MCP Server。如果你把 Slnmap 编译成可执行文件可以将其注册为一个command类型的 MCP Server。配置位置通常为claude_desktop_config.json示例{ mcpServers: { slnmap: { command: dotnet, args: [ /path/to/Slnmap.dll, --solution, /path/to/MyShop/MyShop.sln, --transport, stdio ] } } }重启 Claude Desktop 后会话中就能看到 Slnmap 提供的工具列表。之后你可以直接在对话里提问Claude 会在需要时自动调用工具。6.2 在 Cursor 中配置Cursor 的 MCP 配置入口在 Settings → MCP选择 Add New MCP Server填写NameslnmapTypecommandCommand启动命令例如dotnet /path/to/Slnmap.dll --solution /path/to/MyShop/MyShop.sln --transport stdio保存后Cursor 会自动发现 Slnmap 暴露的工具。在写代码时AI 就能调用工具查询代码库关系而不是只依赖当前打开的文件。6.3 在自研 Agent 中调用如果你在写自己的 AI Agent可以用 C# 或其他语言构造 MCP 标准 JSON-RPC 请求通过标准输入输出与 Slnmap 通信。核心逻辑如下// 示意代码发送 MCP initialize 请求 var process new Process { StartInfo new ProcessStartInfo { FileName dotnet, Arguments Slnmap.dll --solution /path/to/MyShop/MyShop.sln --transport stdio, RedirectStandardInput true, RedirectStandardOutput true, UseShellExecute false } }; process.Start(); var request {\jsonrpc\:\2.0\,\id\:1,\method\:\initialize\,\params\:{}}; await process.StandardInput.WriteLineAsync(request); var response await process.StandardOutput.ReadLineAsync(); Console.WriteLine(response);自研接入时需要注意 MCP 协议要求先完成initialize握手再发送notifications/initialized通知最后才能调用工具。7. 运行结果与效果验证7.1 验证项目依赖查询启动 Slnmap 并完成一次initialize握手之后调用get_project_dependencies。如果返回结果中包含了 MyShop.Api 对 MyShop.Application 和 MyShop.Domain 的依赖说明 Roslyn 正确解析了解决方案中的ProjectReference。7.2 验证类型信息查询调用类型查询工具传入CustomerService{jsonrpc:2.0,id:4,method:tools/call,params:{name:get_type_info,arguments:{typeName:CustomerService}}}预期结果包括类型所在命名空间MyShop.Application定义文件路径src/MyShop.Application/CustomerService.cs构造函数参数类型ICustomerRepository公开方法GetCustomerAsync这些信息说明 Slnmap 不只是做了文本索引而是真的解析了语义模型。如果 Slnmap 只是简单做正则匹配它是无法告诉你构造函数参数类型的。7.3 验证方法调用链如果你在示例代码里让CustomerService.GetCustomerAsync调用_repository.GetByIdAsync那么通过方法调用查询工具可以得到CustomerService.GetCustomerAsync调用了ICustomerRepository.GetByIdAsync。这个信息对 AI 助手理解代码执行路径非常关键。没有代码图AI 可能看到_repository.GetByIdAsync却不知道调用的目标是接口方法。7.4 如何判断 Slnmap 是否正常工作判断标准有三条能返回结构化结果返回的是 JSON 而不是报错信息。结果与代码库实际一致对照源码项目依赖、类型信息、调用关系都正确。查询响应及时MCP 调用没有明显超时。如果其中任何一条不满足优先查看启动日志和标准错误输出。8. 常见问题与排查思路问题现象可能原因排查方式解决方案启动后没有任何输出解决方案路径错误或依赖还原失败检查--solution路径在项目目录手动执行dotnet restore确保路径正确先还原依赖再启动MCP initialize 请求无响应传输模式不匹配确认客户端用 stdio 连接时服务端也用 stdio统一传输模式检查启动参数查询类型时返回空结果类型名称写错或代码库不是 Roslyn 可加载的 .NET 项目确认类型全名含命名空间检查目标项目能否在命令行下编译使用全名查询先确认项目可以dotnet build分析大型解决方案时耗时过长代码库规模大Roslyn 全量分析需要时间查看启动日志中的分析进度检查是否重复分析同一项目明确分析范围必要时先分析子集项目客户端显示工具不存在MCP Server 版本与客户端不兼容或工具名称变化查看 MCP Server 的tools/list响应按实际工具名称调用不要写死.NET Framework 项目加载失败目标项目需要 MSBuild 相关组件检查是否安装了对应版本的 .NET Framework Developer Pack在具备构建该项目的环境中运行分析一个很关键的经验是Slnmap 能不能正确分析首先取决于目标代码库能不能被 Roslyn 正确加载。如果命令行下连dotnet build都过不了Slnmap 大概率也会失败。所以在排查 MCP 层问题之前先确认代码库本身可用。9. 最佳实践与工程建议9.1 按需分析不要全量加载对大型企业级解决方案不要一次加载所有项目再查询。Slnmap 这类工具必然受 Roslyn 编译时间和内存的约束。如果你的解决方案包含几十甚至上百个项目优先考虑启动时指定要分析的子集例如只分析应用层和 API 层。按需增量加载先拿到项目列表再针对特定项目做深度分析。把分析结果缓存下来重复使用。9.2 使用全名查询避免歧义在 MCP 调用时类型名尽量带命名空间。因为不同命名空间下可能有同名类型只传CustomerService可能命中多个结果。推荐格式{name:get_type_info,arguments:{typeName:MyShop.Application.CustomerService}}9.3 与代码搜索工具配合使用Slnmap 提供的是语义图谱信息它不一定包含“某个字符串出现在哪些文件”这种文本搜索能力。与 grep、ripgrep 等文本搜索工具配合才能覆盖代码理解和代码定位的完整链路。9.4 安全边界与权限控制MCP Server 本质上是本机代码执行服务。在使用 Slnmap 时要注意不要在未授权的情况下分析敏感项目的完整依赖图尤其是包含密钥、连接字符串、内部实现细节的代码库。如果 Slnmap 部署为远程服务必须做访问控制避免任意 MCP 客户端都能查询代码库结构。在用 AI 助手时注意不要把不应暴露的代码内容发送给云端大模型。9.5 制定 MCP 工具命名规范如果你在团队内推广 Slnmap 或自研类似的工具给 MCP 工具命名时尽量遵循统一模式get_前缀的查询工具get_type_info、get_dependencies。find_前缀的探索工具find_callers、find_references。list_前缀的枚举工具list_projects、list_members。规范命名不仅让 AI 更容易理解工具用途也能减少把参数传错的概率。10. 总结与后续学习方向Slnmap 代表了一类新的 .NET 开发工具方向用编译器级的语义分析能力为 AI 编程助手补上代码库全局视角。它的意义不在于“又一个 MCP Server”而在于它把 Roslyn 多年积累的代码分析能力通过标准协议开放给了 AI 工具链。对于维护大型 .NET 解决方案的开发者这种能力意味着 AI 不再是一个“只看局部文件的实习生”而是一个“能随时查询全局关系的高级助手”。如果你正在做 .NET 开发并且频繁使用 AI 编程助手建议按这篇文章的流程把 Slnmap 跑起来。先在小解决方案上验证工具能力再逐步引入到真实项目中。理解 Roslyn 语义模型是 .NET 开发者被低估的一项技能Slnmap 恰好是理解 Roslyn 能力边界的一个极好入口。后续可以继续探索的方向包括基于代码图做架构守护、自动生成依赖文档、将代码图导出到图数据库做更深度的分析或者把 Slnmap 集成到 CI 流水线中对每次变更做影响范围分析。对这个领域有兴趣的开发者值得持续关注。