How I configured Pi as a coding agent: LSP, MCP, skills and extensions
My complete reproducible Pi setup: local models, LSP, RAG, MCP, codemode, extensions, session commands, skills and security.
I am regularly asked how I have configured my coding agent. The short answer fits in one sentence: I use Pi, and then I only add the pieces I actually need. The useful answer, however, requires a whole article.
Pi is not the most spectacular agent to show in a video. It does not arrive with a control room, twelve digital characters, and a button that is supposed to replace a team. It first looks like an interface in a terminal. That is precisely what I liked about it: instead of imposing a factory on me, it gives me a small, understandable, and extensible core.
Why minimalism matters more than a long list of features
I have experienced this scenario with many pieces of software. At first, a program performs a task very well. Then each new version adds a reader, a converter, a synchronizer, an assistant, a store, and three panels that nobody remembers. The software eventually starts up like a small operating system when I only wanted to burn a disc, edit a file, or run a command.
Pi adopts the opposite logic. It reminds me of what I like about a minimal Linux distribution: the base does not pretend to guess my ideal environment. I gradually build the one that suits me. This sobriety is not an aesthetic affectation. The fewer rules, tools, and unnecessary servers the agent loads, the more I understand what it does, the less I waste on tokens, and the easier it is to find the cause of strange behavior.
However, we must be honest: “minimal” does not mean “magically simple.” An extensible tool also leaves me the choice to install and maintain its extensions. Pi suits me because I like to know the mechanics. If you are looking for a fully preconfigured product, this freedom may feel like extra work.
Installation and workspace
The official installation fits in a single command on macOS or Linux. As always with a remote script, I recommend checking its origin before running it:
curl -fsSL https://pi.dev/install.sh | sh
You can also go through npm — with a recent version of Node.js — and a separate procedure exists for native Windows. Once installed, I immediately check the version, navigate to the project directory, and then launch Pi from there. The starting folder matters: it is used to discover instructions and to group sessions.
pi --version
cd /path/to/the/project
pi
The configuration shown here corresponds to Pi 1.0.4, the version used during my trial. The project evolves quickly: the pi update command updates an installation performed with the script, whereas an npm or Nix installation is updated with its own package manager. It is a small detail, but it prevents believing that a feature is missing when you are simply running an old version.
Pi runs in the terminal, but the terminal is not a minor detail. I use Kitty and its tabs to separate my projects, then Zellij inside each tab. I often keep the code in one pane, Pi in a second, the development process in a third, and then tests, logs, or a remote environment in a fourth. I can thus immediately see if the modification compiles, if the server restarts, and if errors change while the agent works.
This is not mandatory: tmux, a standard terminal, or the editor’s terminal work perfectly fine. The idea is not to reproduce my screen pixel for pixel, but to avoid the single terminal where Pi, the server, and tests crush each other. An agent becomes much more pleasant when its work remains observable.
The user configuration is located in ~/.pi/agent/. Pi can also read a project-specific configuration. This separation is healthy: my personal preferences and credentials should not end up in the repository, while architecture rules and test commands should be shareable with the team.
| Item | Role | My rule |
|---|---|---|
auth.json | Provider credentials and access tokens | Private, strict permissions, never in Git |
models.json | Compatible endpoints and custom or local models | Only if built-in providers are not enough |
settings.json | General interface and behavior settings | Small changes at a time |
keybindings.json | Terminal interface shortcuts | Useful only after identifying my frequent gestures |
mcp.json | Available MCP servers | A server must address an identified need |
SYSTEM.md | Complete replacement of the system prompt | I hardly ever touch it |
APPEND_SYSTEM.md | Supplement to the system prompt | For truly cross-cutting general behavior |
AGENTS.md | Project context, rules, and commands | My usual starting point |
extensions/, skills/, prompts/, themes/ | Capabilities, procedures, prompt templates, and appearance | I add them after encountering the need several times |
At the project level, executable settings live rather in .pi/: .pi/settings.json, .pi/mcp.json, local extensions, skills, prompts, and themes. Pi only loads this configuration after the project’s trust decision. Context files, on the other hand, are discovered in the current directory and its parents. After a manual modification, /reload reloads resources without forcing a full restart.
The exact tree structure may evolve with Pi. I therefore always check the documentation of the installed version instead of blindly copying a configuration found six months ago.
The model first, accessories second
An agent without a model is just a pretty loop of tools. The first real step is therefore to connect the provider with /login, then to choose the model with /model. The reasoning level is set with /thinking when the model supports it.
For a built-in provider, I prefer this path rather than immediately building a large models.json. This file becomes useful for a compatible endpoint, a model served locally by Ollama, LM Studio, or vLLM, or an internal infrastructure. Local does not necessarily mean fast, by the way: a small model may be enough to search or classify, but a refactoring across a monorepo requires much more reasoning and context.
Here is a deliberately minimal example for Ollama. The key is fake — Ollama ignores it — and the address points to the local machine. For an authenticated endpoint, I reference an environment variable instead of writing the secret in plain text.
{
"$schema": "https://pi.dev/schemas/models.schema.json",
"providers": {
"ollama": {
"baseUrl": "http://localhost:11434/v1",
"api": "openai-completions",
"apiKey": "ollama",
"models": [
{ "id": "qwen2.5-coder:7b" }
]
}
}
}
The model name is not enough. You must also respect its capabilities, context window, and reasoning levels. Some offer only low, medium, and high; others add xhigh or max. Pi normally limits /thinking to the levels declared by the model. In the selector, Ctrl+S saves the model or default level, while Ctrl+P allows quickly browsing the allowed models. A session also memorizes model changes: resuming it restores its choice without modifying the default for new sessions.
I never put an API key in AGENTS.md nor in an example intended for the repository. Credentials remain in the mechanism designed for them. This seems obvious, but agents read precisely the files you give them: a key written “temporarily” in an instruction ends up very quickly in a session, a log, or a commit.
Why I leave the system prompt alone
The temptation is strong to immediately rewrite SYSTEM.md to get a “perfect” agent. That is also a good way to erase instructions that Pi uses for its tools and interface. I only replace the system if I know exactly what I want to remove and what consequences I accept.
In most projects, AGENTS.md is enough. I describe the architecture, commands, conventions, what is forbidden, and the definition of done there. A general rule can go in the user file; a repository rule stays in the repository. I thus gain a coherent agent without transforming its engine.
# Project
- Read existing code before proposing an architecture.
- Run relevant tests after each modification.
- Never modify already deployed migrations.
- Present the diff and risks before any irreversible action.
- Use commands defined in package.json.
LSP: the first extension I install
If I had to keep only one improvement for coding work, it would be the Language Server Protocol. Without LSP, the agent inspects the project with text search, reads files, and deduces relationships. It is useful, but it remains partially blind to the structure understood by the compiler and the editor.
With a language server, it can ask where a symbol is defined, which references point to it, what type is expected, what errors exist, and which files will be touched by a rename. For TypeScript, you need the TypeScript server; for Go, gopls; for each language, the appropriate component. The Pi extension does not replace this server: it gives it access to its answers.
This is a two-layer setup that I had insufficiently explained. You must first install the real language server on the machine, then the extension that exposes its functions to Pi. Installing only the extension does not magically make gopls, typescript-language-server, Pyright, or rust-analyzer appear.
pi install npm:pi-lsp-extension
# then, in Pi
/reload
/lsp doctor
The extension name and options may evolve, and several LSP implementations exist in the catalog. I therefore check its source and choose the one that matches my language. The useful check is simple: the server must appear active, then a request for definition, references, or diagnostics must actually trigger the LSP tool. Simply having installed a package does not prove that the agent uses it.
The difference is not theoretical. On a reference search, a grep had found thirty occurrences. The LSP identified thirty-five spread across three files, because it understood the symbol and not just its character string. Five forgotten references are more than enough to turn a “simple” rename into a Friday evening bug.
LSP does not make the model infallible. It simply provides a more accurate map. I still ask it to read implementations, run tests, and show me the diff. But I prefer an agent that queries the compiler’s map to an agent that guesses the roads by looking at the signs.
RAG: useful for knowledge, not as decoration
Text search answers “where does this function appear?” well. It answers less well “which documents explain our retention policy?” when the texts use different words. This is where a RAG system can become interesting.
The principle is simple: documents are split, converted into vectors, and then stored in a suitable database, for example Qdrant. The question undergoes the same transformation. The search brings back semantically close passages, even when they do not contain the exact words of the query. Pi can then use them as context.
I connected this type of service via MCP, with a few clear actions: add documents, search, delete, and check status. It works well for voluminous documentation, technical decisions, a corpus of procedures, or multiple repositories. For a small project of twenty files, it is often an unnecessary complication. LSP and a standard search are simpler, fresher, and easier to control there.
What my local RAG looks like
My installation is not a mysterious cloud. Qdrant runs in Docker as a vector database. A small RAG service, launched next to the model server, converts documents into embeddings and exposes a few operations: add, search, delete, and health check. An MCP server then translates these operations into tools that Pi can call. I can see the processes, restart the service, and query Qdrant independently of the agent.
Pi
└─ local-rag MCP
└─ RAG service
├─ embeddings
└─ Qdrant in Docker
In my case, this RAG also serves to filter a large stream of publications. A literal search for “pink van” will not necessarily bring up a text talking about a “red truck,” whereas semantic proximity can judge the two subjects as close. I use this principle to compare content to my interests, then classify it. It is a good example of what RAG actually brings: not a magical memory, but retrieval by meaning.
My set of MCP servers, and why I do not activate them all
MCP provides a common way to expose tools and resources to the agent. It is practical, so the ecosystem quickly tempts you to install fifteen of them. I resist this temptation. Each server adds descriptions, permissions, dependencies, sometimes secrets, and a new way to fail. My mcp.json file is not a collection: it is a work toolbox.
Current versions of Pi know how to handle MCP directly. A server can be global in ~/.pi/agent/mcp.json or local to the project in .pi/mcp.json. Here is the cycle I use to avoid confusing “written in JSON” with “functional”:
pi mcp add -l docs --url https://example.com/mcp \
--bearer-token-env-var DOCS_TOKEN
pi mcp list
# in an already open session
/reload
/mcp
pi mcp list connects to activated servers and displays tools or errors. In Pi, /mcp is used to inspect connections, restart a server, connect via OAuth, and change its exposure. I keep ordinary tools in codemode, small frequently used tools in direct, large catalogs in deferred, and anything dangerous or useless in hidden. This distinction limits the amount of descriptions sent to the model.
Context7 for current documentation
The model may know a library well, but its memory corresponds to a past state. A method may have changed name, an option may have disappeared, or an old example may no longer compile. Context7 allows the agent to retrieve the current documentation of a library at the moment it needs it. This avoids a particularly annoying category of hallucinations: perfectly plausible code calling a non-existent API.
SearXNG to search without driving a browser
I also use a SearXNG instance exposed by MCP. The agent can launch a search, read structured results, and cross multiple engines without graphically opening a browser for each question. To check a recent error, find documentation, or compare several leads, it is much lighter.
SearXNG is a metasearch engine: depending on its configuration, it aggregates Web, encyclopedic, and code repository results. The call goes through HTTP and the result comes back in a structured form, for example in JSON. Context7 and SearXNG are therefore not competitors. The first answers “what is the current API of this library?”; the second “what can be found today on this error or topic?”.
This does not dispense with checking sources. A search result is not a truth and a Web page can contain hostile instructions intended for agents. I treat retrieved content as untrusted data, never as a new system rule.
Chrome DevTools and Playwright do not do the same job
For Web development, the Chrome DevTools server is one of the most practical additions. The agent opens the page, clicks, fills a field, inspects the DOM, reads the console, examines network requests, responses, cookies, or local storage, and takes a screenshot. It no longer just asserts that the button should work: it can see what is happening.
| Chrome DevTools | Playwright |
|---|---|
| Interactive exploration and debugging | Reproducible and automated scenarios |
| Console, network, DOM, and storage during development | End-to-end tests and regression |
| Understand why the page fails now | Verify that the flow will continue to work |
| Flexible conversation with the browser | Test suite intended to be replayed |
I use the first to investigate while I build, the second to turn expected behavior into a durable test. Both can click in a page, but that is not a reason to confuse them.
PostgreSQL, yes; Postman, not necessarily
Limited PostgreSQL access allows the agent to read the schema, examine a slow query, run an EXPLAIN, spot a missing index, or prepare a migration. I preferably give it a read-only connection on a development environment. An agent optimizing a query has no reason to be able to delete the production database.
Conversely, I have not found it essential to add a Postman server to all my projects. Pi already knows how to call an API with terminal tools, read an OpenAPI schema, or run existing tests. If the team keeps all its truth in Postman collections, integration can be justified. Otherwise, it often duplicates a capability already present.
Codemode: stop making a round trip for each screwdriver
A naive succession of tools looks like this: the model asks for a search, waits for the result, thinks, asks for a file, waits again, then calls an MCP server. Each step adds a round trip and swells the context.
Codemode allows Pi to write a small JavaScript program executed in a controlled environment to orchestrate multiple calls. It can, for example, query two resources in parallel, filter the responses, and return only the useful result to the model. It is not just faster: it avoids filling the history with every intermediate detail.
const [docs, search] = await Promise.all([
tools.mcp__context7__lookup({ library: "next" }),
tools.mcp__search__query({ q: "Next.js current cache behavior" })
]);
return { docs, search };
The script runs in a QuickJS sandbox, with no direct access to Node.js, the disk, or the network: it only exits through authorized tools. Only its result returns to the model. This is important when two calls each produce thousands of lines: JavaScript can sort, filter, and summarize mechanically before consuming tokens in the conversation.
With an MCP server exposed in codemode mode, Pi normally activates this mechanism automatically. To keep it available even without MCP, I can add it in settings.json:
{
"defaultTools": ["+codemode"]
}
I still keep a simple rule: the orchestration program must remain short and readable. If the agent produces a half-finished software product hidden in a tool call, it becomes harder to examine than the original task.
Extensions that have truly changed my usage
The extension ecosystem is where Pi goes from a minimal core to my tool. Here are those that make sense in my organization, not a mandatory list.
Accessing another folder without opening the whole disk
Pi reasons first from its working directory. The additional folders extension serves when a project depends on a neighboring repository, a documentation folder, or a shared schema. I declare this path explicitly instead of starting Pi too high in the tree. The agent sees what it needs, not my entire personal directory.
fzf: fuzzy search and multi-grep
The fzf extension generally adds several tools: file search, fuzzy grep, and multi-pattern search. The agent can continue to use find or grep; fzf becomes an additional option, not a forced replacement. In the interface, the @ character also opens file search and avoids retyping a long path.
Neovim: start from the line where I work
With the Neovim integration, I place the cursor in a function, use my shortcut — in my configuration, a combination around the leader key — and open a small dialogue box. A question like “explain this passage” arrives at Pi with the file and current position. This context is much more precise than “look somewhere in the controller.”
Subagents: only if separation brings something
I still often work with a single model. Subagents become useful when you want to reserve an economical model for simple searches and a more robust model for difficult decisions, or call a specialized capability: image, video, audio transcription, or speech synthesis. Their interest comes from the separation of roles, not the number of little characters displayed on the screen.
Apply patch and Toolwatch: putting back a visible barrier
Minimal Pi does not interrupt every modification to ask permission. Apply patch suits when I want to examine the diff before applying each change. Toolwatch plays a similar role for tools and commands: I can authorize common operations, ask for validation for some, and block dangerous actions. These two extensions add friction, but that is precisely the friction I want before a deletion or a command touching the environment.
An extension runs in the Pi process with my user rights. It is not a small plugin magically locked behind glass. I read the code, I check the origin, and I limit access. Randomly installing an extension that promises to “make the agent ten times more powerful” amounts to running a stranger’s software on my workstation.
Sessions are trees, not long magnetic tapes
Pi keeps its sessions in JSONL under ~/.pi/agent/sessions. The interest is not just to resume a conversation. A session forms a tree: I can go back to an earlier point, create a branch, and try another solution without throwing away all previous work.
/tree serves me to navigate or diverge in the existing session. /fork creates a separate session when I want to turn a lead into independent work. I can also name, clone, export, or resume a session. This distinction seems secondary until the day a refactoring goes in the wrong direction: I prefer to return cleanly to the junction rather than ask the model to mentally forget twenty messages.
The most telling example is a trial with Redis. I ask Pi to integrate it, it modifies several files, then I realize that this direction was not the right one. With /tree, I go back to the message before this decision and create another branch in the same session file; the old branch remains viewable. With /fork, I transform an earlier entry into a new independent session. With /clone, I simply copy the active branch as it is.
| Command | What it does | When I use it |
|---|---|---|
/session | Displays file, ID, messages, tokens, and cost | Checking where I am before a long operation |
/resume | Opens a saved session | Resuming a project after leaving Pi |
/new | Starts a blank session | Avoiding dragging useless context |
/tree | Navigates and diverges in the current session | Comparing two related solutions |
/fork | Creates a session from an old message | Turning an alternative into separate work |
/clone | Copies the active branch | Preserving an exact state before experimentation |
/compact | Summarizes old context | Freeing space while preserving decisions |
/name | Gives a readable name to the session | Finding it among JSONL files |
/export | Exports in HTML or JSONL | Archiving or rereading — after checking secrets |
When context approaches its limit, compaction summarizes the old part and frees up space. The summary serves as current context, while original events remain in the session. It is not a perfect memory: a nuance may disappear in the synthesis. For important decisions, I write them in the repository — ADR, ticket, documentation, or AGENTS.md — instead of relying on an infinite conversation.
The limit depends on the model: 128,000, 256,000 tokens, sometimes more. Pi normally compacts automatically, but /compact allows indicating what the summary must absolutely preserve. I also keep in mind that the original JSONL contains prompts, tool calls, command outputs, and sometimes file content. An exported session is therefore not an innocuous document to publish without rereading.
A root AGENTS.md, then local rules
In a monorepo, a single two-hundred-line file quickly becomes as annoying as missing documentation. Pi can combine instructions located at several levels. I therefore keep rules common to the root, then add specifics near the concerned code.
monorepo/
├── AGENTS.md # architecture, Git, common commands
├── apps/
│ ├── web/AGENTS.md # Next.js, accessibility, UI tests
│ └── admin/AGENTS.md # React, rights, and internal components
└── services/
└── api/AGENTS.md # Go, migrations, and integration tests
Local files complement parent rules. Pi also recognizes CLAUDE.md and AGENTS.override.md variants; the latter replaces the context file of the same folder, but does not erase instructions inherited from parent folders. A team can thus enforce test creation everywhere, and in the API only, specify how to generate a migration.
I keep these documents concrete: Next.js or React versions, strict TypeScript, prohibition of any, preference for server components, exact commands, directories, constraints, and short examples. Thirty-line sermons on “code quality” help much less than a correct test command.
Skills: turning a habit into a procedure
A skill is essentially a reusable instruction that the agent loads when a task matches. I create one after repeating the same process several times: preparing a migration, publishing a version, analyzing a CI failure, checking a page before production, or writing a changelog note.
The good skill does not just say “deploy the application.” It describes preconditions, the order of checks, commands, stop criteria, and elements to return at the end. It must allow a colleague to understand the process, even without an agent.
I distrust huge collections downloaded in bulk. They fill the environment with assumptions that do not match my infrastructure and they constitute untrusted code or instructions. Three short skills, written for my project and maintained with it, are worth better than three hundred mysterious recipes.
Packages: sharing a configuration that has become stable
When a set has proven itself, a Pi package can group extensions, skills, prompts, and themes. It is practical to find the same environment on several machines or to provide a common base for a team.
I do not start with the package. I start with the problem, I add the tool, I use it, I remove what does not serve, and only then do I package it. Otherwise, I turn still vague choices into an internal standard and everyone inherits my trials.
The point that demonstrations often skip: security
Pi can read, modify, and execute with the permissions of the account that launched it. By default, you should not imagine an impenetrable sandbox boundary nor automatic confirmation before each command. It is a powerful development tool, so the radius of action is real.
I treat it like a very fast technical colleague who could misunderstand a phrase:
- work under Git and available backups;
- secrets reduced to the strict necessary, never a complete keyring;
- dedicated account, container, or VM when the project is sensitive;
- read-only database connection for analysis;
- diff reread before merge and tests executed;
- destructive commands blocked or subject to validation;
- Web content, issues, and external files considered untrusted.
The last point concerns prompt injection. A page or a ticket can contain a phrase intended to convince the agent to ignore its rules or exfiltrate a secret. For me, a consulted document is a piece to analyze, not someone who suddenly obtains the right to command the terminal.
The configuration I recommend to start
After all these tools, the best initial installation seems almost disappointing. That is a good sign.
- Install Pi and connect a single model.
- Create a short
AGENTS.mdwith the project commands and limits. - Install the language LSP server and verify that it responds.
- Work for a few days with built-in tools.
- Add an MCP server only when a lack clearly returns.
- Turn repeated procedures into skills.
- Package the whole set after stabilization.
This progression avoids a frequent problem: when an agent possesses from the first day two search engines, three browsers, four memories, and ten databases, I no longer know which component produced an answer nor why context costs so many tokens.
When I do not use Pi
If the task is deterministic, known, and repeated without ambiguity, I write a script or a test. Always renaming the same file, checking a port, backing up a directory, or calling an API with fixed parameters does not need a model that reasons at each execution.
Pi becomes interesting when I must navigate in unknown code, link multiple sources, choose between options, explain a failure, or evolve a change after test results. In other words, I use it for controlled uncertainty, not as a costly replacement for every shell command.
What I have finally built
My Pi no longer has much in common with an empty installation. It understands code thanks to LSP, consults current documentation, queries a search, observes the browser, explores certain databases, delegates subtasks, and knows how to resume a conversation branch. Yet, the whole remains readable because each piece answers a precise reason.
This is where minimalism becomes really interesting. It does not consist of staying with the fewest features possible. It consists of being able to explain why each feature is there, what data it sees, and what happens when it breaks.
I am therefore not looking for the best agent in the absolute. I am looking for a tool that I can shape, inspect, and stop. Pi suits me because it starts small and leaves me the decision of how far it grows. In a world where every software wants to become a total platform, this modesty is almost a premium feature.
Technical references
- Pi: installation and getting started
- Pi: configuration files and levels
- Pi: providers, models, and reasoning level
- Pi: MCP servers and tool exposure
- Pi: how codemode works
- Pi: sessions, branches, export, and context
- Pi: extensions
- Pi Catalog: LSP extension used in this guide
- Pi: skills
- Pi: sessions and context compaction
- Pi: security model
Discussion
Comments
No comment yet. Feel free to open the discussion with a question or your own experience.