A minimal toolkit for building your own agent harness in Python — tools, skills, sub-agents, and observability on top of any OpenAI-compatible backend (OpenAI, Anthropic, OpenRouter, or a local server).
Requires Python >= 3.11.
pip install mini-agent-kit
# with the bundled web UI server:
pip install "mini-agent-kit[server]"The distribution is named mini-agent-kit on PyPI; the import name is minimal_agent (from minimal_agent import Agent).
To hack on the framework itself, clone the repo and use uv:
cd minimal_agent
uv syncCopy .env.example to .env and set your API key:
cp .env.example .env
# Edit .env — set LLM_BACKEND and LLM_BACKEND_API_KEY| Backend | LLM_BACKEND |
Notes |
|---|---|---|
| OpenAI | openai |
Default. Uses gpt-4o-mini by default |
| Anthropic | anthropic |
Via OpenAI-compatible endpoint |
| OpenRouter | openrouter |
Any model on OpenRouter |
| Local server | localhost |
vLLM, llama.cpp, LM Studio, Ollama — set LLM_BACKEND_BASE_URL |
minimal_agent is an installable library. You create a new project that depends on it, wire up the tools you want, and run it however you like.
import asyncio
from pathlib import Path
from minimal_agent import LLM, Agent, Message, Role, Settings
from minimal_agent.tools.builtin import ReadFile, RunShell
settings = Settings()
workspace = Path.cwd()
llm = LLM(
model=settings.LLM_MODEL,
backend=settings.LLM_BACKEND,
)
agent = Agent(
llm=llm,
tools=[
ReadFile(workspace_root=workspace, read_timestamps={}),
RunShell(workspace_root=workspace),
],
workspace_root=workspace,
)
async def main():
session = await agent.create_session()
session.context.add(Message(role=Role.USER, content="List the files in this directory"))
async for message in agent.run(session.context):
if message.role == Role.ASSISTANT and message.content:
print(message.content)
asyncio.run(main())That's a working agent. It reads the user message, calls the LLM, uses tools if needed, and prints the response. Sessions are persisted to disk automatically and can be resumed with agent.load_session(session_id).
For the full guide to building your own agent — custom tools, prompts, context sources, reasoning, skills, sub-agents, and the observability model — see minimal_agent/README.md.
- Tools — subclass
BaseToolwith a Pydantic input schema and aninvoke()method. Built-in:read_file,write_file,edit_file,glob,grep,run_shell,spawn_agents,web_search,web_extract. See Tools. - MCP — plug any MCP server's tools in alongside the built-ins, over stdio or streamable HTTP. See Connect MCP servers.
- Context sources — inject dynamic environment info (git status, directory trees, your own) into what the model sees, with control over when it's gathered and how fresh it stays. See Context sources.
- Reasoning — turn on a model's "thinking" via
ReasoningConfig; the trace rides onmessage.reasoning. See Reasoning. - Skills — reusable prompt templates loaded on demand from
.minimal_agent/skills/, following the Agent Skills Specification. See Skills. - Sub-agents — the built-in
spawn_agentstool fans work out to concurrent sub-agents, each fully recorded under the parent session. See Spawning sub-agents. - Observability — every session records its transcript, timeline, and a byte-exact audit of every LLM call to disk, with no wiring required — sub-agents included. See Observability.
Any agent you build can be served over HTTP with a bundled chat web UI — one process, one port, no Node required.
pip install "mini-agent-kit[server]"# my_app.py
from minimal_agent import App
app = App(agents=agent) # or {"swe": swe_agent, "research": research_agent}
if __name__ == "__main__":
app.serve() # → http://localhost:8000python my_app.py serves the chat UI at /, the JSON API under /api, and interactive docs at /docs. Responses stream over SSE, sessions persist to disk and resume across restarts, and with multiple agents registered the UI's new-session dialog lets you pick one. App subclasses FastAPI, so routes, middleware, lifespan, and uvicorn my_app:app --reload all work as usual.
Ready-to-run examples live in example/ — a swe_agent/ (full read/write/shell toolset plus sub-agents) and a research_agent/ (web search + read-only files, reasoning on). Each is one main.py you can copy as a starting point; see example/README.md. The UI's source is in web/; from a source checkout, build it once into the package with make ui (needs Node), or hack on it live with npm run dev against a running App.
Any MCP server's tools can sit alongside the built-ins. Install the extra:
pip install "mini-agent-kit[mcp]"Wrap your agent in an MCPToolProvider — it connects to each server, discovers its tools, and hands you ready-to-register tool instances:
import asyncio
from pathlib import Path
from minimal_agent import LLM, Agent, Message, Role, Settings
from minimal_agent.tools.builtin import ReadFile, RunShell
from minimal_agent.tools.mcp import MCPServerHTTP, MCPServerStdio, MCPToolProvider
settings = Settings()
workspace = Path.cwd()
llm = LLM(model=settings.LLM_MODEL, backend=settings.LLM_BACKEND)
async def main():
async with MCPToolProvider(
servers=[
# A local server, spawned as a subprocess:
MCPServerStdio(
name="fs",
command="npx",
args=["-y", "@modelcontextprotocol/server-filesystem", str(workspace)],
),
# A remote server over streamable HTTP:
MCPServerHTTP(
name="linear",
url="https://mcp.linear.app/mcp",
headers={"Authorization": "Bearer <token>"},
),
],
) as mcp_tools:
agent = Agent(
llm=llm,
tools=[
ReadFile(workspace_root=workspace, read_timestamps={}),
RunShell(workspace_root=workspace),
*mcp_tools, # e.g. mcp__fs__read_file, mcp__linear__create_issue
],
workspace_root=workspace,
)
session = await agent.create_session()
session.context.add(
Message(role=Role.USER, content="File a Linear issue for the TODOs in main.py")
)
async for message in agent.run(session.context):
if message.role == Role.ASSISTANT and message.content:
print(message.content)
asyncio.run(main())Tool names are prefixed mcp__<server>__<tool>, so nothing collides with your local tools. Non-read-only MCP tools ask for permission through the same permission_callback as built-in tools; servers you trust can be marked MCPServerStdio(..., require_permission=False).
Instead of programmatic configs, drop the standard mcpServers JSON — the same format MCP marketplaces publish and Claude Desktop / Cursor read, so install snippets paste in unchanged — into .minimal_agent/mcp.json:
{
"mcpServers": {
"notionApi": {
"command": "npx",
"args": ["-y", "@notionhq/notion-mcp-server"],
"env": {
"OPENAPI_MCP_HEADERS": "{\"Authorization\": \"Bearer ${NOTION_TOKEN}\", \"Notion-Version\": \"2022-06-28\"}"
}
},
"linear": {
"url": "https://mcp.linear.app/mcp",
"headers": { "Authorization": "Bearer ${LINEAR_TOKEN}" }
}
}
}async with MCPToolProvider.from_json() as mcp_tools: # reads .minimal_agent/mcp.json
agent = Agent(llm=llm, tools=[*local_tools, *mcp_tools], workspace_root=workspace)Entries with command run as stdio subprocesses; entries with url connect over streamable HTTP. ${VAR} references are expanded from the environment at load time (and fail fast if unset), so tokens stay in your .env — never commit secrets into the JSON itself. MCPToolProvider.from_json(path) accepts a custom location, and load_mcp_servers(path) returns the parsed configs if you want to filter or extend them before constructing the provider.
The provider must stay open for the agent's whole life. To serve an MCP-equipped agent over HTTP, use the async app.a_serve() inside the provider block — the blocking app.serve() would try to own its own event loop:
async def main():
async with MCPToolProvider(servers=[...]) as mcp_tools:
app = App(agents=Agent(llm=llm, tools=[*local_tools, *mcp_tools]))
await app.a_serve() # async serve() — same host/port argscd minimal_agent
make format # ruff format
make lint # ruff check
make test # pytest