Background Monitor:让 Claude 主动响应日志和状态变化的实现机制
第五十四章:LSP 插件开发:让 Claude Code 理解你的领域语言
54.1 为什么需要 LSP Plugin
当你在 Claude Code 中请求分析一个 .sol 文件(Solidity 智能合约),或者一个 .tf 文件(Terraform 配置),或者你公司内部自研的 DSL,会发生什么?
默认情况下,Claude 依赖自己训练数据中的知识来理解这些语言。对于 Solidity 这样相对主流的语言,Claude 的理解尚可接受。但对于:
- 发布时间晚于 Claude 训练截止日期的新版本语言特性
- 企业内部 DSL(领域特定语言)
- 专有配置格式
- 小众框架的特殊语法
Claude 的理解就会出现明显的盲区。LSP Plugin 正是为填补这些盲区而存在的。
54.2 Language Server Protocol 基础
LSP 的设计哲学
LSP(Language Server Protocol)由 Microsoft 在 2016 年提出,最初是为了解决编辑器与语言服务之间的 N×M 集成问题。在 LSP 之前,每个编辑器(VS Code、Vim、Emacs)都需要为每种语言(Python、Java、Go)分别实现智能功能,形成 N 个编辑器 × M 种语言 = N×M 个集成工作。
LSP 将这个问题变成了 N+M:每个语言只需实现一个 LSP Server,每个编辑器只需实现一个 LSP Client。
Claude Code 的 LSP Plugin 系统继承了这一设计,让语言服务可以向 Claude 提供精确的语言理解能力。
LSP 通信格式
LSP 基于 JSON-RPC 2.0 协议,消息通过标准输入/输出或 HTTP 传输。每条消息包含:
{
"jsonrpc": "2.0",
"id": 1,
"method": "textDocument/hover",
"params": {
"textDocument": {
"uri": "file:///project/contracts/Token.sol"
},
"position": {
"line": 42,
"character": 15
}
}
}
LSP 核心能力
Claude Code 目前集成了以下 LSP 能力:
| LSP 方法 | 作用 | Claude Code 的使用方式 |
|---|---|---|
textDocument/diagnostics |
提供错误和警告 | 注入到 Claude 的上下文,帮助识别问题 |
textDocument/hover |
光标处的符号文档 | Claude 分析代码时获取符号详情 |
textDocument/definition |
跳转到定义 | Claude 追踪代码引用链 |
textDocument/references |
查找所有引用 | Claude 分析变量/函数的使用范围 |
textDocument/documentSymbol |
文件中的所有符号 | Claude 快速了解文件结构 |
workspace/symbol |
全工作区符号搜索 | Claude 在大型项目中定位符号 |
textDocument/completion |
代码补全建议 | Claude 生成代码时参考领域规则 |
textDocument/codeAction |
快速修复建议 | Claude 提供修复方案时参考 LSP 建议 |
54.3 实现一个 LSP Server
项目准备
我们将实现一个 Solidity LSP Server 作为示例。Solidity 是以太坊智能合约的编程语言,有一些独特的安全规范需要实时检查。
mkdir solidity-lsp-plugin
cd solidity-lsp-plugin
claude-plugin init --type lsp
npm install vscode-languageserver vscode-languageserver-textdocument
npm install -D typescript @types/node
LSP Server 骨架
// lsp/server.ts
import {
createConnection,
TextDocuments,
ProposedFeatures,
InitializeParams,
TextDocumentSyncKind,
InitializeResult,
Diagnostic,
DiagnosticSeverity,
CompletionItem,
CompletionItemKind,
TextDocumentPositionParams,
HoverParams,
Hover,
MarkupKind,
} from "vscode-languageserver/node";
import { TextDocument } from "vscode-languageserver-textdocument";
// 创建 LSP 连接(通过 STDIO)
const connection = createConnection(ProposedFeatures.all);
const documents = new TextDocuments(TextDocument);
// 初始化握手
connection.onInitialize((params: InitializeParams): InitializeResult => {
return {
capabilities: {
textDocumentSync: TextDocumentSyncKind.Incremental,
completionProvider: {
triggerCharacters: [".", "("],
resolveProvider: true,
},
hoverProvider: true,
diagnosticProvider: {
interFileDependencies: false,
workspaceDiagnostics: false,
},
},
serverInfo: {
name: "Solidity Language Server",
version: "1.0.0",
},
};
});
// 启动文档管理和连接
documents.listen(connection);
connection.listen();
实现诊断(Diagnostics)
诊断是 LSP 最核心的功能。下面实现 Solidity 的常见安全漏洞检测:
// lsp/diagnostics/solidity.ts
interface DiagnosticRule {
id: string;
pattern: RegExp;
message: string;
severity: DiagnosticSeverity;
suggestion?: string;
}
const SOLIDITY_RULES: DiagnosticRule[] = [
{
id: "SWC-107",
pattern: /\.call\{value:/g,
message: "Potential reentrancy vulnerability: external call with value transfer",
severity: DiagnosticSeverity.Warning,
suggestion: "Use the checks-effects-interactions pattern. Update state before making external calls.",
},
{
id: "SWC-101",
pattern: /\+\+|\-\-|\+|\-|\*|\//g,
message: "Unchecked arithmetic: Consider using SafeMath or Solidity 0.8+",
severity: DiagnosticSeverity.Information,
suggestion: "Upgrade to Solidity ^0.8.0 for built-in overflow protection, or use OpenZeppelin's SafeMath.",
},
{
id: "SWC-115",
pattern: /tx\.origin/g,
message: "Use of tx.origin for authorization is dangerous",
severity: DiagnosticSeverity.Error,
suggestion: "Use msg.sender instead of tx.origin for authorization checks.",
},
{
id: "STYLE-001",
pattern: /function\s+\w+\s*\([^)]*\)\s*public(?!\s+view|\s+pure|\s+returns)/g,
message: "Public function that modifies state should consider adding a reentrancy guard",
severity: DiagnosticSeverity.Hint,
},
];
export function analyzeSolidity(
document: TextDocument
): Diagnostic[] {
const text = document.getText();
const diagnostics: Diagnostic[] = [];
const lines = text.split("\n");
for (const rule of SOLIDITY_RULES) {
// 跳过注释行(简化处理)
lines.forEach((line, lineIndex) => {
if (line.trimStart().startsWith("//") ||
line.trimStart().startsWith("*")) {
return;
}
let match;
rule.pattern.lastIndex = 0; // 重置正则状态
while ((match = rule.pattern.exec(line)) !== null) {
const diagnostic: Diagnostic = {
range: {
start: { line: lineIndex, character: match.index },
end: { line: lineIndex, character: match.index + match[0].length },
},
severity: rule.severity,
code: rule.id,
source: "solidity-lsp",
message: rule.message,
data: rule.suggestion, // 传递修复建议
};
diagnostics.push(diagnostic);
}
});
}
return diagnostics;
}
注册诊断处理器
// 在 server.ts 中注册
import { analyzeSolidity } from "./diagnostics/solidity.js";
// 文档变更时重新分析
documents.onDidChangeContent((change) => {
const document = change.document;
if (!document.uri.endsWith(".sol")) return;
const diagnostics = analyzeSolidity(document);
connection.sendDiagnostics({ uri: document.uri, diagnostics });
});
实现悬停文档(Hover)
当 Claude 分析 Solidity 代码时,悬停功能可以提供关键字和函数的详细解释:
// lsp/hover/solidity-docs.ts
const SOLIDITY_KEYWORDS: Record<string, string> = {
"msg.sender": `**msg.sender** (address)
The address of the account that called this function.
For external function calls, this is the contract address of the caller.
**Security Note**: Never use \`tx.origin\` as a substitute for \`msg.sender\`.
\`tx.origin\` refers to the original external account that started the transaction,
which can be exploited in phishing attacks.
**Example**:
\`\`\`solidity
require(msg.sender == owner, "Not authorized");
\`\`\``,
"msg.value": `**msg.value** (uint256)
The amount of Wei (1 Ether = 10^18 Wei) sent with the current call.
Only available in \`payable\` functions.
**Best Practice**: Always check \`msg.value\` before processing payments.
Consider using a pull-over-push pattern for withdrawals.`,
"selfdestruct": `**selfdestruct(address payable recipient)**
Destroys the current contract and sends its remaining Ether balance to \`recipient\`.
⚠️ **DEPRECATION WARNING**: \`selfdestruct\` is deprecated since EIP-6049.
Avoid using it in new contracts. It may be removed or its behavior changed
in future hard forks.`,
"delegatecall": `**delegatecall(bytes memory data)** → (bool success, bytes memory returndata)
Executes the code of the target contract in the context of the calling contract.
Storage, \`msg.sender\`, and \`msg.value\` all remain those of the calling contract.
⚠️ **Security Risk**: Improper use of \`delegatecall\` is a critical vulnerability.
Ensure the target contract's storage layout is compatible with the calling contract.
Never \`delegatecall\` to user-provided addresses.`,
};
connection.onHover((params: HoverParams): Hover | null => {
const document = documents.get(params.textDocument.uri);
if (!document) return null;
// 获取光标处的词
const wordRange = getWordRangeAtPosition(document, params.position);
if (!wordRange) return null;
const word = document.getText(wordRange);
// 查找文档
const docs = SOLIDITY_KEYWORDS[word];
if (!docs) return null;
return {
contents: {
kind: MarkupKind.Markdown,
value: docs,
},
range: wordRange,
};
});
实现代码补全(Completion)
// Solidity 关键字和常用函数的补全
connection.onCompletion(
(params: TextDocumentPositionParams): CompletionItem[] => {
const document = documents.get(params.textDocument.uri);
if (!document?.uri.endsWith(".sol")) return [];
const completions: CompletionItem[] = [
{
label: "require",
kind: CompletionItemKind.Keyword,
detail: "require(condition, message)",
documentation: {
kind: MarkupKind.Markdown,
value: "Validates a condition. Reverts the transaction with `message` if condition is false.",
},
insertText: "require(${1:condition}, \"${2:error message}\");",
insertTextFormat: 2, // Snippet
},
{
label: "ReentrancyGuard",
kind: CompletionItemKind.Class,
detail: "OpenZeppelin ReentrancyGuard",
documentation: {
kind: MarkupKind.Markdown,
value: "Modifier to prevent reentrant calls.\n\n```solidity\ncontract MyContract is ReentrancyGuard {\n function withdraw() external nonReentrant {\n // safe from reentrancy\n }\n}\n```",
},
insertText: "ReentrancyGuard",
},
{
label: "SPDX-License-Identifier",
kind: CompletionItemKind.Snippet,
detail: "SPDX license comment",
insertText: "// SPDX-License-Identifier: ${1|MIT,Apache-2.0,GPL-3.0,UNLICENSED|}",
insertTextFormat: 2,
},
];
return completions;
}
);
54.4 将 LSP Server 集成进 Plugin
plugin.json 配置
{
"name": "solidity-lsp-plugin",
"version": "1.0.0",
"description": "Solidity language support with security analysis for Claude Code",
"lsp": {
"languages": ["solidity"],
"server": {
"command": "node",
"args": ["./dist/lsp/server.js"],
"transport": "stdio"
},
"documentSelector": [
{ "language": "solidity", "pattern": "**/*.sol" }
],
"initializationOptions": {
"solcVersion": "0.8.24",
"enableSecurityAnalysis": true
}
},
"engines": {
"claude-code": ">=1.0.0"
}
}
Claude Code 如何使用 LSP 数据
当 Claude 分析 .sol 文件时,Claude Code 会自动:
- 启动 LSP Server(如果尚未运行)
- 通知 LSP Server 打开文件(
textDocument/didOpen) - 请求文件诊断(
textDocument/diagnostic) - 将诊断结果追加到 Claude 的上下文:
[LSP Diagnostics for Token.sol]
Line 47: WARNING (SWC-107) - Potential reentrancy vulnerability: external call with value transfer
Suggestion: Use the checks-effects-interactions pattern. Update state before making external calls.
Line 52: ERROR (SWC-115) - Use of tx.origin for authorization is dangerous
Suggestion: Use msg.sender instead of tx.origin for authorization checks.
Claude 收到这些上下文后,在分析代码时就拥有了 LSP Server 提供的专业领域知识。
54.5 为企业内部 DSL 构建 LSP
DSL 解析器设计
假设你的公司有一种内部工作流定义语言 .flow:
# example.flow
workflow OrderProcessing {
trigger: order.created
step ValidateOrder {
action: validate_order
on_failure: NotifyTeam
}
step ChargePayment {
action: charge_stripe
retry: 3
on_failure: RefundAndNotify
}
step FulfillOrder {
action: create_fulfillment
depends_on: ChargePayment
}
}
为这个 DSL 构建 LSP Server:
// lsp/flow-parser.ts
interface WorkflowAST {
name: string;
trigger: string;
steps: StepNode[];
}
interface StepNode {
name: string;
action: string;
retry?: number;
onFailure?: string;
dependsOn?: string[];
range: Range;
}
export function parseFlowDocument(text: string): {
ast: WorkflowAST | null;
errors: ParseError[];
} {
const errors: ParseError[] = [];
const lines = text.split("\n");
// 简化的递归下降解析器
// 生产环境中建议使用 ANTLR、tree-sitter 或 Ohm.js
const workflowMatch = text.match(/workflow\s+(\w+)\s*\{/);
if (!workflowMatch) {
return { ast: null, errors: [{ message: "Missing workflow declaration", line: 0 }] };
}
const ast: WorkflowAST = {
name: workflowMatch[1],
trigger: "",
steps: [],
};
// 检测步骤的循环依赖
const stepNames = new Set<string>();
const dependencies: Map<string, string[]> = new Map();
const stepRegex = /step\s+(\w+)\s*\{([^}]+)\}/g;
let match;
while ((match = stepRegex.exec(text)) !== null) {
const stepName = match[1];
const stepBody = match[2];
stepNames.add(stepName);
const dependsOnMatch = stepBody.match(/depends_on:\s*(\w+)/);
if (dependsOnMatch) {
dependencies.set(stepName, [dependsOnMatch[1]]);
}
}
// 检查依赖是否指向不存在的步骤
for (const [step, deps] of dependencies) {
for (const dep of deps) {
if (!stepNames.has(dep)) {
const lineNum = findLineNumber(text, `depends_on: ${dep}`);
errors.push({
message: `Step "${step}" depends on undefined step "${dep}"`,
line: lineNum,
severity: DiagnosticSeverity.Error,
});
}
}
}
return { ast, errors };
}
54.6 LSP 性能优化
增量更新
避免每次文档变更都重新分析整个文件。利用 LSP 的增量同步机制:
// 使用防抖减少分析频率
const analysisDebounce = new Map<string, NodeJS.Timeout>();
documents.onDidChangeContent((change) => {
const uri = change.document.uri;
// 清除之前的定时器
const existing = analysisDebounce.get(uri);
if (existing) clearTimeout(existing);
// 300ms 后执行分析(等待用户停止输入)
const timer = setTimeout(() => {
const document = documents.get(uri);
if (document) {
const diagnostics = analyzeDocument(document);
connection.sendDiagnostics({ uri, diagnostics });
}
analysisDebounce.delete(uri);
}, 300);
analysisDebounce.set(uri, timer);
});
缓存解析结果
// 缓存 AST 避免重复解析
const astCache = new Map<string, { version: number; ast: WorkflowAST }>();
function getCachedAST(document: TextDocument) {
const cached = astCache.get(document.uri);
if (cached && cached.version === document.version) {
return cached.ast;
}
const { ast } = parseFlowDocument(document.getText());
if (ast) {
astCache.set(document.uri, { version: document.version, ast });
}
return ast;
}
54.7 调试 LSP Server
# 以调试模式启动 LSP Server(输出通信日志)
LSP_DEBUG=1 node dist/lsp/server.js 2>lsp-debug.log
# 使用 LSP Inspector 手动测试
npx @vscode/lsp-inspector
# 查看 Claude Code 收到的 LSP 数据
CLAUDE_LSP_DEBUG=1 claude
常见问题排查
LSP Server 无法识别文件类型:检查 documentSelector 中的 pattern 是否正确匹配文件路径。
诊断没有显示在 Claude 的上下文中:确认 Claude Code 版本是否支持你使用的 LSP 能力(查看 engines.claude-code 声明)。
补全建议不出现:确认 capabilities.completionProvider 中声明了正确的触发字符。
小结
LSP Plugin 通过 Language Server Protocol 向 Claude 提供精确的领域语言理解能力:诊断暴露代码中的问题,悬停文档提供符号解释,代码补全帮助生成符合领域规范的代码。实现一个 LSP Server 的核心步骤是:建立 JSON-RPC 通信连接、注册 onInitialize、实现具体的 LSP 能力处理器,最后在 plugin.json 中配置语言绑定。企业内部 DSL 的 LSP 支持,是提升 Claude 在特定代码库中准确率的最有效手段之一。下一章讨论监控与可观测性插件。