关于 Theodolite
Theodolite 是面向无编辑器工作环境的无头 MCP 服务,为 worker、检查门和沙箱 Agent 提供语言服务器访问。它提供固定且有界的 LSP 读取工具集,并支持显式门控的可选编辑。
主要功能
- 01固定的语言服务器会话
- 02LSP 诊断与代码导航
- 03符号、引用和悬停查询
- 04预览重命名并列出代码操作
- 05可选的白名单编辑操作
- 06有界排序结果与类型化错误
适用场景
- 01在无编辑器 worker 中检查诊断
- 02在沙箱 Agent 中浏览源代码
- 03更改文件前预览重命名
- 04允许获准客户端应用编辑
MCP 相关
通过 stdio 向 MCP 客户端提供封闭的一组 LSP 读取工具。只有启用变更门控且连接客户端在白名单中时,才会注册编辑工具。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
我们还没有获取到这个服务的安装包或远程地址,请按项目 README 中的说明安装。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
theodolite
一个无头、确定性的 LSP 桥接 MCP 服务(IDEA-0092,
ai.qompass/theodolite):固定语言服务会话,并将其作为范围受限的读取工具提供
给无需编辑器的场景——primo workers、
gates、sandbox agents。
Theodolite 是 periscope(IDEA-0013)的互补方案:periscope 通过用户正在运行的 Neovim 提供响应;theodolite 拥有自己的固定服务会话,绝不会触碰编辑器。
设计权威文档:~/workspace/lsp-mcp-study-20261025/SPEC.md。
构建记录:本目录中的 BUILD-REPORT.md。
使用方章程(与 periscope / SCIP / phlow checks 的路由、
客户端设置、修改策略):docs/CHARTER.md。
接口(读取类;v1 + P3 读取类补全)
工具:session_status、diagnostics(将推送累积结果与
服务器支持时的拉取结果合并,并记录每条诊断的通道来源)、hover、definition、declaration、
type_definition、implementation、references、
document_symbols、workspace_symbols、code_actions(仅列出,
绝不返回或执行负载)、
rename_preview、session_restart。没有通用的
any-LSP-method 工具,没有 edit_file,没有格式化工具,也没有
workspace/executeCommand 直通接口——接口从设计上始终保持为封闭列表。
服务端(配置数据,而非代码):bacon-ls(诊断所有者)、 rust-analyzer(语义分析)、lua-language-server(第二个验证服务端)。
Mutation 类(P5;受门控,默认关闭)
还有两个工具仅在启用 mutation 门控时注册:code_action_apply 和 rename_apply。rename_preview 属于读取类——它会计算并呈现重命名的编辑集,不会写入任何内容;门控不适用于它。
- 运行策略。 配置包含
mutation = "off"(默认值)或mutation = "gated",并在后一种情况下包含非空的mutation_allowlist客户端名称列表。任何其他组合——未知值、启用门控但未提供允许列表,或关闭门控时仍提供允许列表——都会以类型化错误ConfigInvalid拒绝启动;对于含糊不清的运行策略,程序绝不会悄然继续运行。 - 身份与注册。 门控使用 MCP 握手中的
clientInfo.name作为身份——这是主机信任边界内的路由标签(SPEC §8),不是身份验证层。只有在运行策略为 gated 且已连接的客户端在允许列表中时,这两个 apply 工具才会出现在tools/list中;其他所有情况下它们都不会出现,调用时会返回UnsupportedMethod。gated 配置若未提供握手身份,则会安全地拒绝访问。 - 决策。 每次 apply 都由纯函数(
gate::decide)根据运行策略、身份、允许列表、句柄状态和版本状态作出决策。每个决策——无论允许还是拒绝及其原因——都会记录在会话记录中(session_status→gate_decisions,仅包含元数据,且有大小上限)。 - 句柄。
code_actions会为每个携带编辑的操作生成不透明的一次性句柄,但仅当 apply 类已为该客户端注册时才会生成。编辑内容以及列出时对每个目标计算的 SHA-256 快照都保留在服务端;列表中仍不会携带有效载荷。code_action_apply仅接受句柄——调用方提供编辑内容会导致类型化拒绝。应用时,伪造、已使用、跨会话或版本已变更的句柄会触发类型化错误GateDenied,并记录日志。允许的 apply 会在写入前消耗句柄,因此已使用的句柄无法重试。 rename_apply接受预览的参数,而非句柄:它会重新计算,并在计算和写入之间重新验证每个受跟踪文档的哈希值(若有变更,则拒绝整个重命名,且不会写入任何内容);与预览返回applied: false不同,它会返回applied: true。- 应用器是此桥接唯一的写入路径。它会在写入任何内容前验证整个编辑集(检查每个目标是否位于工作区边界内、是否存在、是否为 UTF-8、范围是否有效以及是否重叠),然后逐个文件原子写入——创建同级临时文件并重命名,保留原文件权限——并重新同步已触及的文件。文件创建/重命名/删除操作在任何情况下都会被类型化拒绝:此桥接仅编辑文件内容,绝不会接受服务端推送的
workspace/applyEdit。
运行
cargo build --release
./target/release/theodolite --config bridge.toml
通过 stdio 使用 MCP,采用 NDJSON JSON-RPC。配置无效时,此桥接会拒绝启动。配置格式请参见 bridge.toml.example;在目标机器上固定真实的二进制文件路径和准确的 --version 输出。
特性
- 子进程通过绝对路径在经过清理的环境中由
setpriv --pdeathsig SIGKILL启动,每个会话对应一个监督进程(见下一节);对桥接发送 SIGKILL 会连带终止其所有会话进程树(测试套件中的孤儿门控测试)。 - 会话以
(workspace, server)为键;最多 4 个;已终止的会话绝不会悄然重新启动——session_restart是唯一的重启途径。 - 所有结果均按规范顺序排列且有字节数上限;超限时会返回类型化错误
ResultTooLarge和实际条目数,绝不会静默截断。错误均为类型化错误,且具有稳定代码(SPEC §4.6)。 - 就绪状态由状态决定(initialize 握手以及每个服务各自的谓词;rust-analyzer 要求观察到的加载活动已结束,且探测成功)。固定休眠时间绝不是判断就绪状态的机制。
关闭与进程树所有权(SPEC §3.6)
每个会话都在一个监管进程下运行:桥接程序以 --supervise 参数重新执行自身,将其作为会话的直接子进程和进程组组长;监管进程在该组内启动服务(现在,setpriv --pdeathsig SIGKILL 包装器会将服务绑定到监管进程)。服务及其泄漏的所有守护进程都与监管进程共享进程组,而监管进程会在每条路径上回收整个进程树——按组枚举 /proc,按明确的 pid 终止进程,从不使用负 pgid:
- **传输 EOF/会话关闭/驱逐:**桥接程序发送 LSP
shutdown+exit,并等待shutdown_grace_ms;如果进程树仍然存活,则通过关闭监管进程的 stdin 触发升级处理,其 EOF 会启动监管进程自身的宽限期,之后终止整个进程树。仅对监管进程调用Child::kill是最后手段。 - **服务死亡(任何原因):**监管进程会终止进程树中的其余进程,并以与服务相同的状态退出,因此桥接程序的回收器记录到的死亡情况与直接启动服务时完全一致。
- **桥接程序收到 SIGKILL/SIGTERM:**SIGKILL 下无法运行任何桥接程序代码;SIGTERM 下则未安装处理程序——这是刻意如此,因为这两种情况下都无法执行 LSP 优雅关闭步骤,我们会明确说明这一点,而不会粉饰事实。保证交由监管进程负责:每个监管进程每隔 50 ms 检查桥接程序在
/proc中是否存在,并核对记录的启动时间——pid 被回收重用不算仍然存活;桥接程序一旦消失,监管进程就会立即终止其进程树。
验证关卡:tests/mcp_stdio.rs 中的 tree_gate_* 会运行一个会泄漏真实守护进程孙进程的伪服务,并断言在会话重启、传输 EOF、SIGKILL 和 SIGTERM 后的 5 s 内,服务、守护进程和监管进程均已退出;仅检查服务的 P4 前孤儿进程关卡仍会运行。
测得的限制,按限制如实陈述:桥接程序收到 SIGKILL 后,进程树回收的耗时受监管进程的 50 ms 轮询间隔及其 /proc 扫描所限,并非瞬时完成;如果监管进程本身严重卡死,以至于连 EOF 路径也无法运行,桥接程序的最后手段只会终止监管进程本身——理论上,守护进程可能会在这一条路径下继续存活。目前没有发现其他残留情况:在 P4 primo 冒烟测试中(一个真实的 bacon-ls 会话,其无头后端守护进程驻留在会话进程树中),无论 EOF、SIGKILL 还是 SIGTERM,该守护进程都会在相同的 5 s 时限内退出。
测试
cargo test # unit + stdio suites + parity harness
cargo clippy --all-targets -- -D warnings
cargo fmt --check
奇偶一致性关卡(tests/parity.rs)会逐字节比较桥接程序诊断信息与独立运行的 bacon-ls;如果找不到 bacon-ls 二进制文件,会明确跳过;在 primo 上则会进行真实测试。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。