DevPilot MCP
by walianitin
README.md
# DevPilot
DevPilot is an MCP-based local development agent. An LLM reasons over the user's request and available tools, calls MCP tools (git / docker / filesystem), observes the results, and loops until it can produce a final answer.
---
## Quick start
1. Copy `.env.example` to `.env` and set `OPENAI_API_KEY` (OpenRouter / Groq / OpenAI-compatible).
2. From the repo root:
```bash
PYTHONPATH=client poetry run python client/client.py
```
3. Type a prompt. Type `quit` to exit.
Hiring-manager demo (broken backend container):
```bash
PYTHONPATH=client poetry run python demo/run_demo.py
```
See `demo/README.md` for the talking script and a no-Docker backup.
Optional env vars:
| Variable | Default | Purpose |
|----------|---------|---------|
| `OPENAI_API_KEY` | (required) | API key |
| `OPENAI_BASE_URL` | `https://openrouter.ai/api/v1` | Compatible API base URL |
| `OPENAI_MODEL` | `openai/gpt-4o-mini` | Model name |
---
## Architecture
```text
User
│
▼
Conversation Manager
│
▼
Tool Registry (MCP)
│
list_tools() + inputSchema
│
▼
LLM (Planner)
│
┌───────────────┴────────────────┐
│ │
▼ ▼
Tool Call(s) Final Answer
│ │
▼ ▼
Executor Exit
│
▼
Tool Result(s)
│
▼
Conversation Manager
│
└───────────────► Back to LLM
```
---
## File map
| Stage | File | Role |
|-------|------|------|
| Entry | `client/client.py` | Starts MCP server over stdio, REPL, prints answers |
| Loop | `client/agent/main.py` | Orchestrates plan → execute → update history (max 8 turns) |
| History | `client/agent/conversation.py` | Stores user / assistant / tool messages |
| Schemas | `client/agent/registry.py` | Formats MCP tool schemas for the LLM |
| Planner | `client/agent/llm.py` | LLM returns `tool_call` / `final_answer` / `clarification` / `error` |
| Execute | `client/agent/executor.py` | Calls `session.call_tool(...)` over MCP |
| Format | `client/agent/context.py` | Pretty-prints tool results into conversation |
| Models | `client/agent/models.py` | `AgentStatus`, `ToolCall`, `ToolResult`, etc. |
| MCP app | `server/app.py` | `FastMCP("DevPilot")` |
| Tools | `server/server.py` | `@mcp.tool` registrations |
| Impl | `server/git/*`, `server/doocker/*`, `server/filesystem/*` | Real commands |
| Helpers | `server/utils/{validator,Commands,response}.py` | Validate paths, run subprocess, `ok`/`fail` dicts |
---
## Request flow (by file)
### 1. Boot — `client/client.py`
- Loads `.env`
- Spawns the MCP server: `poetry run python -m server.server` over **stdio**
- Creates an MCP `ClientSession`, calls `initialize()`, then `list_tools()`
- Starts a REPL and passes each prompt into `agent_main`
### 2. Agent loop — `client/agent/main.py`
Builds:
- `ConversationManager` — history
- `ToolRegistry` — tool schemas
- `LLMPlanner` — next decision
- `Executor` — MCP tool calls
Then:
1. `conversation.add_user(prompt)`
2. Loop up to `MAX_TURNS = 8`:
- `planner.plan(conversation, registry)`
- If `tool_call` → record calls → `executor.execute` → record results → continue
- If `final_answer` / `clarification` / `error` → return
### 3. Conversation — `client/agent/conversation.py`
Stores messages as `user`, `assistant`, or `tool`.
`to_prompt_text()` is what the LLM sees every turn.
### 4. Tool registry — `client/agent/registry.py`
Turns MCP `list_tools()` into JSON schema text (name, description, properties, required).
### 5. LLM planner — `client/agent/llm.py`
Receives:
- System prompt (DevPilot rules)
- Available tool schemas
- Conversation history
Returns a single JSON object parsed into `AgentResponse`.
### 6. Executor — `client/agent/executor.py`
For each `ToolCall`:
- `await session.call_tool(tool_name, tool_args)`
- Collects text content into `ToolResult`
The executor never decides — it only runs tools.
### 7. MCP server — `server/server.py` + implementations
`@mcp.tool` handlers delegate to modules under:
- `server/git/` — status, branch, log, diff, commit, push, pull, stash, …
- `server/doocker/` — ps, logs, inspect, exec, start, stop, restart, …
- `server/filesystem/` — read/write/list/search/find/delete/symbols
Helpers:
- `validator` — ensure path is a git repo / file / directory
- `Commands.run_cmd` / `run_shell_cmd` — subprocess wrappers
- `response.ok` / `fail` — standard `{success, result|message}` dicts
### 8. Context — `client/agent/context.py`
Formats each `ToolResult` into readable text, then appends it to the conversation for the next LLM turn.
---
## Example: git status + branch
**User**
```text
What's the status of my repo at /Users/nitin/Desktop/local-git-mcp? Which branch am I on?
```
### Turn 1 — LLM (`client/agent/llm.py`)
```json
{
"status": "tool_call",
"tool_calls": [
{
"tool_name": "git_status",
"tool_args": {
"repo_path": "/Users/nitin/Desktop/local-git-mcp"
}
},
{
"tool_name": "git_current_branch",
"tool_args": {
"repo_path": "/Users/nitin/Desktop/local-git-mcp"
}
}
]
}
```
### Executor → MCP → server
| Tool | Path | Command |
|------|------|---------|
| `git_status` | `server/git/status.py` | `git -C <path> status` |
| `git_current_branch` | `server/git/current_branch.py` | `git -C <path> …` (current branch) |
**Tool result shape**
```python
{"success": True, "result": "<command stdout>"}
```
Results are formatted by `context.py` and stored in the conversation.
### Turn 2 — LLM
```json
{
"status": "final_answer",
"answer": "You're on branch main. The working tree has unstaged changes in client/agent/llm.py."
}
```
`client/client.py` prints:
```text
DevPilot> You're on branch main. The working tree has unstaged changes in client/agent/llm.py.
```
---
## Example: docker debugging
**User**
```text
Why is my backend container failing?
```
```text
LLM → docker_ps()
backend -> exited, db -> running
LLM → docker_logs(container="backend")
panic: database connection refused
LLM → final_answer
The backend exits because it cannot connect to the database.
```
---
## LLM response types
### Tool call
```json
{
"status": "tool_call",
"tool_calls": [
{
"tool_name": "docker_logs",
"tool_args": { "container": "backend" }
}
]
}
```
### Final answer
```json
{
"status": "final_answer",
"answer": "The backend container exits because it cannot connect to the database."
}
```
### Clarification
```json
{
"status": "clarification",
"answer": "Which repository path should I inspect?"
}
```
### Error
```json
{
"status": "error",
"error": "Unable to execute the requested tool."
}
```
---
## Agent loop (simplified)
```python
conversation.add_user(prompt)
for _ in range(MAX_TURNS):
response = planner.plan(conversation, registry)
if response.status == TOOL_CALL:
results = await executor.execute(response.tool_calls)
conversation.add_tool_calls_and_results(...)
continue
return response # final_answer | clarification | error
```
---
## Design principles
- **LLM reasons; MCP tools execute** — the executor never makes decisions.
- **Discoverable tools** — schemas come from MCP `list_tools()`, not hard-coded client logic.
- **Conversation is stateful** — multi-step flows (e.g. `docker_ps` → `docker_logs`) work naturally.
- **Structured LLM I/O** — forced JSON statuses; the agent must not invent tool output.
- **Deterministic tools** — validators + subprocess; standard `ok`/`fail` responses.
- **Bounded loop** — stops after `MAX_TURNS` (8) to avoid infinite tool calling.
---
## Available tools (high level)
**Git:** status, current branch, branch list, log, diff, show, checkout, create branch, commit, push, pull, stash
**Docker:** ps, images, logs, inspect, exec, start, stop, restart
**Filesystem:** read/write file, list directory, search text, find files, file info, delete file, list/read symbols
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues