当 AI 重构工具开始进入研发流程后,很多团队会遇到一个共同的尴尬:模型很聪明,但工具拿不到准确的代码语义。改名能改到注释和字符串,却不知道哪些地方是真的引用;重构建议能给出漂亮的方案,却没法在仓库级别验证影响范围。再加上多团队接入、多项目隔离、不同权限体系,问题会从“算法够不够好”变成“架构能不能支撑”。
MCP(Model Context Protocol,模型上下文协议)正好把“大模型的能力”和“外部工具/数据源”之间的通道标准化了。而本文要聊的 Henka,就是围绕“多租户 MCP Server”和“结构化、语义感知代码重构”这两件事展开的技术方案。文章会先讲清楚 MCP 和语义感知重构的核心概念,再给出一个可落地的 Henka 风格实现思路,包括代码结构、配置、多租户隔离、常见排错和工程建议。
1. 背景与核心概念
1.1 为什么需要 MCP
在 MCP 出现之前,AI 应用连接外部工具的方式非常零散。每个模型生态都自带一套工具调用协议,没有统一标准。比如一个代码助手要读取文件、执行命令、查数据库,通常需要为它单独开发插件或通过 Prompt 硬编码能力边界。
MCP 解决的是“模型应用(Host)”与“数据/工具服务端(Server)”之间的连接问题。它采用类似客户端-服务器结构,通过 JSON-RPC 2.0 规范定义请求和响应。大模型应用不需要关心服务端具体是什么技术栈,只要通过 MCP Client 暴露的工具、资源和提示词去调用即可。
这里可以简单理解成:
- MCP Host:运行 AI 模型的应用,比如 Claude Desktop、IDE 插件、自研 Agent。
- MCP Client:Host 内部用来连接 Server 的客户端组件。
- MCP Server:提供工具、数据上下文或提示词的服务端。
对代码重构场景来说,MCP Server 能统一暴露“读取语法树”“查找符号引用”“分析依赖关系”这类能力,而不是让模型直接去拼接字符串。
1.2 什么是语义感知重构
传统 IDE 里的代码重构大多是语法层面的机械化操作。比如“重命名符号”,IDE 会基于编译器的符号解析能力,把某个方法名在所有地方的引用全部改掉。这种操作不是简单字符串替换,而是需要理解作用域、继承关系、导入路径和重载规则。
语义感知重构(semantics-aware code refactoring)的意思,就是在重构过程中真正利用代码的语义信息,而不只是文本信息。
举一个典型的例子:
public class OrderService { public void pay(Order order) { order.setStatus("PAID"); } } public class Order { private String status; public void setStatus(String status) { this.status = status; } }如果你想把setStatus方法重命名为markStatus,简单字符串搜索会改到 JSON 序列化字段、数据库映射注解或者其它类里的同名方法。但语义感知重构会通过 AST(抽象语法树)和符号表,定位到OrderService.pay方法中真正调用的那个Order#setStatus,再结合当前文件的 import 关系,决定哪些调用点可以改,哪些不能改。
1.3 Henka 的定位:多租户 MCP Server
Henka 在本文语境下,不是某一家厂商的封闭产品,而是代表一类面向代码重构的 MCP Server 设计理念:
- 以 MCP 协议为对外接口。
- 以多租户模式支持多个团队/项目/用户。
- 以语义分析为核心重构引擎。
- 以结构化重构任务为目标。
所谓多租户,是指同一个 MCP Server 实例可以同时服务多个隔离的用户或团队。每个租户拥有独立配置、独立代码索引、独立权限范围。这样能显著降低运维成本,不用每个业务线单独部署一套服务。
但多租户也带来新的挑战:请求级别的隔离、缓存隔离、资源配额、审计日志、安全边界。Henka 的设计重点就是把这些能力与 MCP 协议对齐。
2. 环境准备与项目结构
2.1 运行环境
本文示例以 Python 为主,因为围绕代码分析有许多成熟的语法树库,比如 Tree-sitter、LibCST,以及用于 Java 分析的 JavaParser(可以包装成服务)。需要准备的环境如下:
- 操作系统:Linux / macOS / Windows(WSL 更推荐)
- Python 版本:3.10 或以上
- Node.js 版本(可选,用于前端或需要调用 npm 生态的分析工具时)
- 包管理工具:pip / poetry
- 开发工具:VS Code、PyCharm 等均可
如果你在 Windows 上直接用,注意路径分隔符和子进程调用;生产环境建议跑在 Linux 容器中。
版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。
2.2 技术选型
一个 Henka 风格的 MCP Server 通常会包含下面几个部分:
| 模块 | 作用 | 可选技术 |
|---|---|---|
| MCP 协议层 | 暴露 tools/resources/prompts | FastMCP、官方 MCP SDK、自研 JSON-RPC 层 |
| 租户管理 | 识别请求归属,校验权限 | JWT / API Key / 请求头 |
| 语义分析引擎 | 解析代码、生成 AST、符号解析 | Tree-sitter、LibCST、JavaParser、Semgrep |
| 重构执行器 | 执行文本编辑、格式化 | 语言格式化工具 + 自定义 Diff |
| 存储与缓存 | 索引代码、缓存 AST | SQLite、Redis、对象存储 |
| 治理能力 | 日志、配额、审计 | 结构化日志、Prometheus、OpenTelemetry |
2.3 项目目录结构
下面是一个可参考的项目结构。
henka-demo/ ├── pyproject.toml ├── README.md ├── src/ │ └── henka/ │ ├── __init__.py │ ├── server.py # MCP 入口 │ ├── tenant.py # 租户上下文 │ ├── analyzer.py # 语义分析引擎 │ ├── refactor.py # 结构化重构工具 │ ├── storage.py # 索引与缓存 │ └── config.py # 配置加载 ├── data/ │ └── tenants/ │ └── demo/ │ └── index.db ├── tests/ │ ├── test_analyzer.py │ └── test_refactor.py └── examples/ └── java-project/ └── src/main/java/com/example/这个结构把协议层、业务层、存储层拆开,方便后续扩展更多重构工具。
3. MCP 通信机制与 Henka 的设计要点
3.1 MCP 通信模式对比
MCP 支持多种传输模式,常见的是 stdio、HTTP+SSE、Streamable HTTP。三者的区别可以这样理解:
| 特性 | stdio | HTTP + SSE | Streamable HTTP |
|---|---|---|---|
| 进程模型 | 由 MCP Client 拉起子进程 | Server 独立运行 | Server 独立运行 |
| 网络支持 | 仅本机进程 | 可跨网络 | 可跨网络 |
| 长连接 | 依赖进程生命周期 | SSE 单向推送 | 双向流式 |
| 适用场景 | 本地配置,如 Claude Desktop、IDE 插件 | 远程服务、老式浏览器兼容 | 高并发远程服务 |
| 排错难度 | 相对简单 | 需要关注 SSE 重连 | 需要关注客户端兼容性 |
Henka 这类服务通常以远程形式提供给多个团队使用,所以更适合 Streamable HTTP 或 HTTP+SSE。如果只是本地单机体验,可以用 stdio 模式。
有一个容易踩的坑是:许多开发者直接在本地用 stdio 模式,但把服务部署到远程后才发现模型应用无法跨主机访问本地进程。因此,定义 MCP Server 时要把传输层抽象出来,而不是写死在代码里。
3.2 MCP 中的 Tools / Resources / Prompts
Henka 实现代码重构时,主要暴露的是 Tools。举几个例子:
analyze_code:分析代码结构,返回 AST 和符号表。find_references:查找某个符号的所有引用位置。apply_rename:执行语义安全的重命名。preview_refactoring:生成重构预览 Diff。apply_refactoring:确认后应用重构。
每一个 Tool 都应该包含明确的输入参数描述,这样大模型才能更好地理解和调用。如果参数描述模糊,AI 可能会生成不合理的调用。
3.3 多租户隔离边界
多租户是 Henka 的核心难点。隔离要贯穿以下层面:
- 请求隔离:通过租户标识区分每次请求。
- 数据隔离:每个租户拥有独立的代码索引、缓存目录、数据库。
- 权限隔离:限制某个租户只能访问自己授权范围内的代码仓库。
- 资源隔离:控制单个租户的请求频率、并发数和 Token 配额。
- 审计隔离:日志中记录租户 ID,保证问题可追踪。
最简单的实现方式,是在 HTTP Header 中携带租户 ID,比如X-Tenant-Id: acme。MCP Server 在中间件中解析该 Header,把租户信息注入到请求上下文。
3.4 语义分析流程
语义感知重构不是一次魔法调用,而是一套流程:
- 读取待分析的源码文件。
- 解析成 AST。
- 构建符号表和引用关系。
- 根据重构类型(重命名、提取方法、改变签名等)收集影响范围。
- 生成编辑操作并应用。
4. 实战:实现一个 Henka 风格 MCP 重构服务
下面我们动手实现一个简化版。目标是跑通“多租户 + 语义分析 + 结构化重构”的最小闭环。代码只是为了演示思路,实际使用需要根据项目的编程语言和 MCP SDK 版本调整。
4.1 初始化项目并引入依赖
先用 pip 创建虚拟环境并安装基础依赖。
mkdir henka-demo cd henka-demo python3 -m venv .venv source .venv/bin/activate pip install "mcp" "tree-sitter" "tree-sitter-java" "fastapi" "uvicorn" "pydantic"这里说明一下:
mcp是官方 MCP Python SDK。tree-sitter和tree-sitter-java用于解析 Java 代码。fastapi+uvicorn用于暴露 HTTP 服务。pydantic用于参数校验。
不同版本适配情况可能不同,如果你安装的 SDK 接口有变化,以官方文档为准。
4.2 定义租户上下文
先封装一个租户上下文类,它负责读取 Header 并传递租户信息。
# 文件路径:src/henka/tenant.py from fastapi import Request, HTTPException class TenantContext: def __init__(self, tenant_id: str): self.tenant_id = tenant_id async def get_tenant_context(request: Request) -> TenantContext: tenant_id = request.headers.get("X-Tenant-Id") if not tenant_id: raise HTTPException(status_code=401, detail="Missing X-Tenant-Id header") # 这里可以做 API Key 校验、租户状态检查 return TenantContext(tenant_id=tenant_id)为什么要放在请求上下文而不是全局变量?因为多租户服务同时处理多个请求,如果租户信息存在全局变量里,会出现请求串号问题。这个是一个很容易被忽视的安全隐患。
4.3 语义分析逻辑
使用 Tree-sitter 解析 Java 代码并提取类和方法信息。
# 文件路径:src/henka/analyzer.py from tree_sitter import Language, Parser import tree_sitter_java JAVA_LANGUAGE = Language(tree_sitter_java.language()) parser = Parser(JAVA_LANGUAGE) def analyze_java_source(source: bytes): tree = parser.parse(source) root_node = tree.root_node symbols = [] def walk(node): if node.type == "method_declaration": name_node = node.child_by_field_name("name") if name_node: symbols.append({ "type": "method", "name": name_node.text.decode("utf-8"), "start": name_node.start_point, "end": name_node.end_point, }) for child in node.children: walk(child) walk(root_node) return symbols这段代码的作用是遍历语法树,收集所有方法声明的名称和位置。真正的语义感知还需要处理作用域、继承关系,但这里已经足够说明“基于 AST 而不是字符串搜索”的思路。
4.4 实现重命名工具
重命名工具需要结合符号表,不能直接全局替换。简化版会先找到方法名节点,然后使用TextEdit替换对应范围。
# 文件路径:src/henka/refactor.py from analyzer import analyze_java_source def rename_method(source: str, old_name: str, new_name: str): # 这里做简化处理:仅演示逐个方法名替换的思路 tree = parse(source.encode("utf-8")) edits = [] root_node = tree.root_node def walk(node): if node.type == "method_declaration": name_node = node.child_by_field_name("name") if name_node and name_node.text.decode("utf-8") == old_name: edits.append((name_node.start_byte, name_node.end_byte, new_name)) for child in node.children: walk(child) walk(root_node) # 按 byte offset 从后往前替换,避免影响后续坐标 new_source = bytearray(source.encode("utf-8")) for start, end, text in sorted(edits, reverse=True): new_source[start:end] = text.encode("utf-8") return new_source.decode("utf-8"), edits实际工程中,还需要分析调用点、确认符号解析唯一性,这里只做结构化替换演示。注意从后往前替换,是因为字节偏移会在修改后变化。
4.5 快速验证语义感知效果
我们可以写一个简单的测试用例,看重命名只改方法声明而不影响同名局部变量。
# 文件路径:tests/test_refactor.py from refactor import rename_method source = ''' class Order { String status = "PAID"; void setStatus(String status) { this.status = status; } } ''' new_source, edits = rename_method(source, "setStatus", "markStatus") print(new_source)预期结果是void setStatus变成void markStatus,方法体内部的参数status不受影响。如果使用字符串替换,很容易把参数名一起改掉,而基于 AST 的替换能做到精准定位。
4.6 将能力暴露为 MCP Tool
MCP SDK 提供了注册 Tool 的装饰器方式。下面是一个示意代码,你需要根据当前 MCP SDK 的版本调整实际写法。
# 文件路径:src/henka/server.py from mcp.server import Server from mcp.server.stdio import stdio_server app = Server("henka") @app.tool() async def rename_symbol(source: str, old_name: str, new_name: str) -> str: """在给定源码中执行结构化重命名。""" new_source, edits = rename_method(source, old_name, new_name) return new_source async def main(): async with stdio_server() as (read_stream, write_stream): await app.run(read_stream, write_stream) if __name__ == "__main__": import asyncio asyncio.run(main())这段代码把基础的重命名能力封装成了 MCP Tool。真正的 Henka 服务还会把文件读取、代码索引、租户校验都接到这里。
4.7 启动与调用验证
运行下面的命令启动本地服务:
source .venv/bin/activate export HENKA_TENANT_ID=acme python -m henka.server如果使用 MCP Client 连接,你可以用 Python 写一个简单的 Client 脚本:
# 文件路径:examples/mcp_client_demo.py import asyncio from mcp.client.stdio import stdio_client async def main(): # 这里需要根据你的 MCP SDK 版本调整 async with stdio_client(cmd=["python", "-m", "henka.server"]) as (read, write): async with Client(read, write) as client: result = await client.call_tool("rename_symbol", { "source": "class A { void foo() {} }", "old_name": "foo", "new_name": "bar", }) print(result) asyncio.run(main())运行后你会看到重命名之后的新源码输出。
如果是在 HTTP 模式,MCP 请求本身就是 JSON-RPC 格式,可以通过curl做冒烟验证:
curl -X POST http://localhost:8000/mcp \ -H "Content-Type: application/json" \ -H "X-Tenant-Id: acme" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "rename_symbol", "arguments": { "source": "class A { void foo() {} }", "old_name": "foo", "new_name": "bar" } } }'这里的/mcp路径和 JSON-RPC 格式需要与你的 MCP HTTP 实现保持一致。不同 SDK 的路径可能不同,不要照搬。
5. 常见问题与排查思路
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 上下文过大,多次自动总结仍超出限制 | MCP Server 一次返回了太多代码或分析结果 | 在 Tool 返回中只返回摘要和必要位置;支持增量读取;对超长文件按行/块切分 |
| stdio 模式启动后没有输出 | 模型应用无法找到 Python 环境或启动命令 | 使用绝对路径;检查 stdio 模式是否适合远程部署;改用 Streamable HTTP |
| HTTP + SSE 连接经常断开 | 代理服务器没有正确支持 SSE 长连接 | 调整 Nginx/网关的 proxy_buffering 和超时时间;开启心跳 |
| 多租户请求串号 | 租户信息存在全局变量,未随请求传递 | 使用依赖注入或请求作用域存储租户上下文,禁止用全局变量保存租户 ID |
| 重命名结果错误,把参数名也改了 | 没有用 AST,而是用了字符串替换 | 接入 Tree-sitter 或对应语言的解析器,基于语法树节点做替换 |
| 无法解析某些新语法 | Tree-sitter grammar 版本过旧 | 升级 grammar,或为不同语言注册不同的 parser |
| 调用 MCP Tool 时返回 401 | 租户 Header 缺失或 API Key 错误 | 检查请求头;在后端记录租户 ID 便于审计 |
| 部署后模型生成无效调用 | Tool 参数描述不清晰 | 完善参数 schema,在描述中注明格式和取值范围 |
尤其值得关注的是“上下文过大”问题。这在真实代码场景中非常常见。一个大型 Java 文件的 AST 可能有几万个节点,如果全部塞进返回结果,对话上下文很快会被撑爆。正确做法是让 MCP Tool 返回“结构化摘要”,例如类列表、方法签名、关键引用位置,而不是整个 AST 原文。
6. 最佳实践与工程建议
6.1 租户隔离要贯穿全链路
多租户不是加一个 Header 那么简单。代码索引、Redis 缓存、后台任务、日志系统中的租户 ID 都要保持一致。建议在每个操作入口统一校验租户状态,并在日志里输出tenant_id和request_id。
6.2 从只读分析工具开始
不要一开始就把“写入重构”直接暴露给所有租户。先提供preview_refactoring这类只读工具,让模型先生成 Diff,再由用户在 IDE 或 Code Review 平台中确认。这样能减少误操作风险,也更容易建立信任。
6.3 谨慎处理权限和意外变更
代码重构涉及文件写入时,必须明确授权边界。生产环境中,建议先备份仓库或要求用户确认 Diff。任何批量修改都要有审计日志,方便回滚。
6.4 缓存和性能优化
语义分析比较耗时,尤其是大仓库。常用的优化手段有:
- 按文件或模块缓存 AST。
- 监听文件变化,增量更新索引。
- 对不常用的冷仓库延迟加载。
- 将重型分析任务放入队列,避免阻塞 MCP 请求线程。
6.5 日志与可观测性
建议使用结构化日志,至少记录以下字段:
{ "timestamp": "2025-01-01T10:00:00Z", "tenant_id": "acme", "request_id": "req-123", "tool": "rename_symbol", "repo": "order-service", "changed_files": 3, "status": "success" }这样既能满足审计需求,也能帮助排查多租户请求串号、性能瓶颈等问题。
6.6 语义分析准确率要持续回归
语义感知重构特别怕“大多数时候正确,偶尔破坏代码”。因此要建立重构结果的回归测试集,覆盖继承、重载、泛型、Lambda 等场景。每次修改语法分析器或重构逻辑,都要跑一遍测试集,保证不引入回退。
7. 总结与下一步
Henka 这类多租户 MCP Server 的价值,在于把 MCP 协议、语义分析引擎和代码重构流程组合成一个可治理、可扩展的服务。它让 AI 代码助手不再只是“根据上下文生成建议”,而是能真正调用语义感知工具,完成结构化、可验证的代码重构操作。
如果你接下来想自己动手实践,建议按这个顺序推进:
- 本地起一个最简单的 MCP Server,只暴露一个分析类名称的 Tool。
- 用 MCP Client 连上,确认通信链路正常。
- 接入 Tree-sitter,实现 AST 解析。
- 增加租户 Header,模拟两个租户请求隔离。
- 再逐步加入重命名、预览 Diff、执行写入等能力。
重点关注三件事:一是通信链路,二是语义分析准确性,三是多租户隔离边界。把这三件事做好,Henka 这类方案就能真正在团队里落地,而不是停留在 Demo 阶段。