docs: add MCP, plugin/LSP tool, and RAG notes

- notes/mcp-overview.md: MCP 协议概览,跟 tool use 的分层差异
- notes/mcp-client-internals.md: MCP client 内部实现(发现/连接/catalog)
- notes/plugin-tools.md: plugin tool 与 native/MCP 的对比
- notes/lsp-tools.md: LSP 协议科普 + OpenClaw LSP tool 包装
- notes/rag-overview.md: RAG 与向量化(向量只用来检索,prompt 用原文)
- notes/blog/mcp-101.md: 面向开发者的 MCP 科普,LSP 类比引入
- README 索引更新
This commit is contained in:
clz
2026-06-16 00:59:32 +08:00
parent bc510c269b
commit 3c0cd86352
14 changed files with 2009 additions and 6 deletions
+224
View File
@@ -0,0 +1,224 @@
# LSP Tool
基于 [OpenClaw](https://github.com/openclaw/openclaw) 2026.6.2 版本源码。
本文先讲 **LSP(Language Server Protocol)是什么**——它是 IDE 世界里
一个很重要的协议,但跟 AI 领域关系不大,所以单独介绍——再讲 OpenClaw
怎么把它包装成 agent 的 tool。
## LSP 是什么
### 它解决什么问题
写编辑器的人原来面临一个 M × N 问题:
```
编辑器 (M 个) 语言支持 (N 种)
┌────────────┐ ┌──────────┐
│ VSCode │ ↔↔↔↔ │ Python │
│ Vim │ ↔↔↔↔ │ Go │
│ Emacs │ ↔↔↔↔ │ TypeScript│
│ Sublime │ ↔↔↔↔ │ Rust │
│ JetBrains │ ↔↔↔↔ │ Java │
└────────────┘ └──────────┘
```
每个编辑器都得给每种语言写一遍 "hover 显示类型"、"跳转到定义"、
"查找所有引用"、"实时报错"...这些功能。M 个编辑器 × N 种语言 =
M × N 份重复实现。
LSP 是微软 2016 年提出的解法:**把"语言能力"抽出来变成一个独立的
服务器进程**,编辑器和服务器之间用一套标准协议通信。
```
编辑器 Language Server
┌────────┐ LSP ┌──────────────┐
│ VSCode │ ←─────JSON-RPC→│ tsserver │ ← TypeScript
└────────┘ └──────────────┘
┌────────┐ ┌──────────────┐
│ Vim │ ←──────────────│ gopls │ ← Go
└────────┘ └──────────────┘
┌────────┐ ┌──────────────┐
│ Emacs │ ←──────────────│ rust-analyzer│ ← Rust
└────────┘ └──────────────┘
```
现在变成 M + N:每个编辑器实现一次 LSP client,每种语言实现一次
LSP server。VSCode 装 Rust 插件 = 它在 VSCode 端走 LSP client,
背后跑一个 `rust-analyzer` 进程。
### 协议形态
LSP 用 JSON-RPC 2.0,跑在 stdio 之上(也支持其他 transport,但 stdio
是默认)。消息分帧用 HTTP 风格的 Content-Length header:
```
Content-Length: 213\r\n
\r\n
{"jsonrpc":"2.0","id":1,"method":"textDocument/hover","params":{...}}
```
典型方法名:
- `initialize` — 握手,client 告诉 server 自己支持什么,
server 回 capabilities(支不支持 hover、definition 等)
- `textDocument/didOpen` / `didChange` / `didClose` — 通知文件状态
- `textDocument/hover` — 给我光标位置的类型/文档
- `textDocument/definition` — 跳转到定义
- `textDocument/references` — 找所有引用
- `textDocument/completion` — 自动补全
- `textDocument/publishDiagnostics` — server 主动推:这里有错
每个方法的参数和返回值都在 LSP 规范里固定下来,所以一个 LSP server
可以同时被 VSCode、Vim、Emacs 用。
### 跟 MCP 的对照
| 维度 | LSP | MCP |
| ------------- | -------------------------------- | ------------------------------------ |
| 目标受众 | IDE / 编辑器 | LLM 应用 |
| 服务器干啥 | 提供代码分析(hover/跳转/补全) | 提供任意工具(读文件/查搜索/...) |
| 方法集合 | 固定一套 `textDocument/*` 等 | server 自己声明 `tools/list` |
| 协议层 | JSON-RPC 2.0 + Content-Length | JSON-RPC 2.0 |
| 提出者 | 微软 | Anthropic |
| 年份 | 2016 | 2024 |
两个协议**长得很像**(stdio + JSON-RPC),解决的是不同领域的同一种
M × N 问题——把可复用的能力(代码分析 / agent 工具)抽到独立进程。
某种意义上 MCP 是 LSP 思路在 AI 时代的延续。
## OpenClaw 怎么用 LSP
核心实现 `src/agents/agent-bundle-lsp-runtime.ts`,第一行注释:
> Minimal LSP JSON-RPC framing over stdio (Content-Length header + JSON body).
不是用现成的 LSP client 库,而是自己在 OpenClaw 里实现了 LSP 协议的
最小子集(Content-Length 分帧 + JSON-RPC),spawn 语言服务器子进程,
`initialize` 握手拿能力。
### 暴露的 tool
根据语言服务器 `initialize` 返回的 capabilities 动态生成
(`agent-bundle-lsp-runtime.ts:317` `buildLspTools`):
| Tool 名 | LSP 方法 | 用途 | capability 门槛 |
| -------------------------- | ------------------------- | ----------------- | --------------------- |
| `lsp_hover_<server>` | `textDocument/hover` | 看符号的类型/文档 | `hoverProvider` |
| `lsp_definition_<server>` | `textDocument/definition` | 跳到定义 | `definitionProvider` |
| `lsp_references_<server>` | `textDocument/references` | 找所有引用 | `referencesProvider` |
如果语言服务器没声明对应 capability(比如 `hoverProvider: false`),
对应 tool 就不会被注册。
工具参数都长一样(`uri: string, line: number, character: number`),
因为 LSP 协议本身就是这套 `textDocument + position` 的形态。
**注意没有 completion**:agent 不需要交互式补全,它直接看完整文件就行。
OpenClaw 只挑了"看完整代码不能直接得到的语义信息"做工具——hover 拿
推断类型,definition/references 需要跨文件 AST 跳跃。
### 跟 MCP / plugin 平行
| 维度 | LSP tool | MCP tool | plugin tool |
| -------------- | ----------------------- | --------------------------- | --------------------------- |
| 协议 | LSP(`textDocument/*`) | MCP(`tools/call`) | OpenClaw Plugin SDK |
| 进程 | stdio 子进程 | stdio 子进程 / HTTP | 同进程 |
| Tool 名 | hardcode 三个 + server 后缀 | server 自报 `tools/list` | plugin 代码静态声明 |
| 配置来源 | bundle plugin manifest | bundle plugin + 用户 config | plugin manifest |
| 用户能加吗 | 现在不能 | 能(`openclaw mcp add`) | 装新 plugin 即可 |
| Catalog 动态性 | **静态**(三个固定 tool) | **动态**(server 决定) | 静态 |
| 模型视角 | 跟其他 tool 没区别 | 跟其他 tool 没区别 | 跟其他 tool 没区别 |
跟 MCP 最大不同:**LSP 的 tool 集是 OpenClaw 写死的**(hover/definition
/references 三个),只是是否启用看 server capability。MCP 是 server
想暴露啥就 `tools/list` 出啥,完全运行时决定。
这是 LSP 协议本身决定的——LSP 的方法集就那么固定一套,不像 MCP
让 server 自由声明工具。OpenClaw 只是把 LSP 方法**翻译**成 agent
理解的 tool。
### 在 attempt 时序里
跟 MCP 完全平行(回看 [mcp-client-internals.md](./mcp-client-internals.md)
的 "Attempt 引爆点"):
```ts
// src/agents/embedded-agent-runner/run/attempt.ts:1502-1549
const bundleMcpRuntime = bundleMcpSessionRuntime
? await materializeBundleMcpToolsForRun({...})
: undefined;
bundleLspRuntime = bundleLspEnabled
? await createBundleLspToolRuntime({...}) // ← LSP 在这里 spawn + initialize
: undefined;
const allowedBundleMcpTools = applyEmbeddedAttemptToolsAllow(bundleMcpRuntime?.tools ?? [], ...);
const allowedBundleLspTools = applyEmbeddedAttemptToolsAllow(bundleLspRuntime?.tools ?? [], ...);
const allowedBundledTools = [...allowedBundleMcpTools, ...allowedBundleLspTools];
```
每次 attempt 开始时,两条 runtime 各自独立 spawn(MCP / LSP),
各自维护 session,各自 dispose;最后合到同一个扁平 tool list 给模型。
## 典型用例
模型要回答 "把 `runEmbeddedAttempt` 的所有调用方都列出来"。可能的工具组合:
1. `Grep "runEmbeddedAttempt"` ——快,但会被注释、字符串、相似名字误伤
2. `lsp_references_typescript` ——准,基于 AST 分析,但要先定位符号的
精确 `(uri, line, character)`
3. 常见组合:先 `Grep` 找出大致位置 → `Read` 确认 → `lsp_references_typescript`
拿精确引用列表
LSP tool 的价值在于 **精确度**——code intelligence 这件事 grep 替代不了:
- `Grep` 找的是文本匹配,会把字符串字面量、注释里的同名词都算进来
- `lsp_references` 走 AST + 类型系统,能区分 `foo.runEmbeddedAttempt`
调的是哪个 `foo`,跨 import 跳得对
类似地 `lsp_hover` 拿 inferred type 是 grep / read 替代不了的——
TypeScript 的 `const x = foo()` 你想知道 `x` 是啥类型,只有 tsserver
能告诉你。
## 配置怎么写
跟 MCP server 一样,LSP server 从 plugin bundle manifest 读
(`src/plugins/bundle-lsp.ts``src/agents/embedded-agent-lsp.ts`),
plugin 在自己的 manifest 里声明 `lspServers`,字段形态跟 MCP 的
stdio server 几乎一样(`command``args``env``cwd`)。
OpenClaw 实际上**复用了 MCP 的 stdio launch config 结构**——
`agent-bundle-lsp-runtime.ts` 里直接 import 了 `resolveStdioMcpServerLaunchConfig`
`describeStdioMcpServerLaunchConfig`,因为两者都是 "spawn 一个本地
stdio 子进程跑 JSON-RPC",launch 部分可以共享。
## 现状的限制
`src/agents/embedded-agent-lsp.ts:22` 有一条注释:
> User-configured LSP servers could override bundle defaults here in the future.
意思是**当前只接受 plugin bundle 声明的 LSP server**,用户 config 还
没接通。MCP 已经走得更远(用户 config 可以加自己的 server),LSP 还
停在 plugin 预置的阶段。
退避逻辑也比 MCP 简化——没有 "连续 3 次失败冷却 60s" 这种保护,
失败就直接报错给 agent。
## 心智模型一句话
LSP 是把 "编辑器智能" 抽出来的协议,跟 MCP 是把 "agent 工具" 抽出来的
协议同源同构(stdio + JSON-RPC + 服务器自报能力)。OpenClaw 借了 LSP
这套现成的代码分析基础设施,把 hover/definition/references 三个最有
agent 价值的能力包装成 tool,放进 agent 的扁平 tool 列表里。模型层面
跟其他 tool 没任何区别。
## 相关
- [mcp-overview.md](./mcp-overview.md) — MCP 协议本身
- [mcp-client-internals.md](./mcp-client-internals.md) — MCP client
的内部实现,LSP 在 attempt 时序里跟它平行
- [plugin-tools.md](./plugin-tools.md) — plugin tool,LSP server 的
配置来源就是 plugin bundle