Welcome to the LSPTool project tutorial! In this series, we will build a powerful bridge between Artificial Intelligence and the tools developers use every day to write code.
Imagine you are an AI trying to understand a massive codebase. You see a function call: processData(). You want to know what that function does.
Without help, you might try to search the text files for the string "processData". But what if there are 50 functions named "processData" in different files? How do you know which one is the real one being called here?
Modern code editors (like VS Code) solve this using something called the Language Server Protocol (LSP). They "understand" the code structure.
The Problem: LSPs speak a complex technical language (JSON-RPC) that is hard to manage directly. The Solution: We need an Orchestrator.
Think of LSP Tool Orchestration as a Conductor for an orchestra.
The Orchestrator handles the messy details so the AI doesn't have to.
At a high level, the AI sends a simplified command to our tool.
Example Input (The Request):
{
"operation": "goToDefinition",
"filePath": "/projects/my-app/src/utils.ts",
"line": 15,
"character": 8
}
Example Output (The Result):
Found definition in file: /projects/my-app/src/defs.ts
Line: 42, Character: 1
Content: export function calculateTotal(a, b) { ... }
The Orchestrator takes that simple input, does all the heavy lifting, and returns the clear answer.
Let's look at how we build this Orchestrator in LSPTool.ts. We use a helper called buildTool to define how our conductor behaves.
First, we give the tool a name and description so the AI knows when to use it.
// LSPTool.ts
export const LSPTool = buildTool({
name: 'LSP', // The name the AI sees
searchHint: 'code intelligence (definitions, references...)',
// This tells the AI what this tool does
async description() {
return DESCRIPTION
},
// ... configuration continues
})
Explanation: This is the ID card of our tool. It tells the system, "I am the LSP tool, and I help with code intelligence."
We need to define strictly what "notes" the conductor accepts. We can't just ask for "some code stuff"; we need specific operations.
// LSPTool.ts
// We use 'zod' to define the shape of valid inputs
const inputSchema = lazySchema(() =>
z.strictObject({
operation: z.enum([
'goToDefinition',
'findReferences',
// ... other operations
]),
filePath: z.string(),
line: z.number().int().positive(),
character: z.number().int().positive(),
}),
)
Explanation: This validates that the AI only asks for operations we support (like goToDefinition) and always provides a location (file, line, character).
This is the main event loop. When the AI uses the tool, this function runs.
// LSPTool.ts
async call(input: Input, _context) {
const absolutePath = expandPath(input.filePath)
// 1. Check if the "Orchestra" (Server) is ready
const manager = getLspServerManager()
// 2. Translate the request into specific instructions
const { method, params } = getMethodAndParams(input, absolutePath)
// 3. Ask the manager to execute the request
let result = await manager.sendRequest(absolutePath, method, params)
// 4. Format and return the answer
return { data: formatResult(input.operation, result) }
}
Explanation: This is the core logic. It prepares the path, converts the operation to something the server understands, sends it, and packages the result.
What actually happens when that call function runs? Let's visualize the sequence.
One of the most important jobs of the Orchestrator is translation. The AI says goToDefinition, but the LSP Server expects textDocument/definition.
We handle this translation in a specific helper function:
// LSPTool.ts
function getMethodAndParams(input: Input, path: string) {
// Convert line numbers: User uses 1-based, LSP uses 0-based
const position = {
line: input.line - 1,
character: input.character - 1,
}
switch (input.operation) {
case 'goToDefinition':
return {
method: 'textDocument/definition', // The LSP technical name
params: { textDocument: { uri: path }, position },
}
// ... other cases for hover, references, etc.
}
}
Explanation: This switch statement acts as a router. It ensures that findReferences gets routed to textDocument/references and creates the exact parameter object the LSP standard requires.
Before we even route the request, we must ensure safety. We don't want the AI looking at files it shouldn't access.
// LSPTool.ts
async validateInput(input: Input): Promise<ValidationResult> {
// Check if file exists on disk
const fs = getFsImplementation()
const absolutePath = expandPath(input.filePath)
try {
const stats = await fs.stat(absolutePath)
if (!stats.isFile()) return { result: false, message: "Not a file" }
} catch (error) {
return { result: false, message: "File does not exist" }
}
return { result: true }
}
Explanation: This step happens before the call function. If the file doesn't exist or isn't a regular file, we stop immediately and tell the AI what went wrong.
In this chapter, we've established the foundation of LSP Tool Orchestration. We learned:
buildTool with input schemas and a call method.This orchestration is the "brain" of the operation. However, for the brain to work, it needs to understand exactly what defines a valid request in more detail.
Next Chapter: Operation Schemas & Validation
Generated by Code IQ