tool-agent-demo
by vamsi513
README.md
# mcp-tool-agent
A minimal end-to-end demonstration of the Model Context Protocol (MCP): one
MCP server exposing three real tools, and a command-line agent that lets an
LLM decide which tool to call and invokes it over the protocol.
Tool calls always travel through MCP (`tools/list` + `tools/call` over a
stdio JSON-RPC connection). The agent never imports the tool functions
directly.
See [ARCHITECTURE.md](ARCHITECTURE.md) for the design choices and the
security model.
## The server
`server.py` is a `FastMCP` server named `tool-agent-demo` that speaks over
stdio. It exposes three tools:
| Tool | Description | Inputs |
|---|---|---|
| `search_github_repos` | Lists a GitHub user's public repos, filtered by a substring match on name/description. Calls the live GitHub REST API, following pagination up to 10 pages. | `username` (str), `query` (str, optional), `limit` (int 1–20, default 5) |
| `fetch_url_text` | Fetches a web page and returns its visible text (scripts/styles/nav stripped) plus the title. Refuses non-public hosts (enforced at connection time, pinned to the validated IP), oversized responses, and non-text content. | `url` (http/https), `max_chars` (int 500–20000, default 4000) |
| `query_books` | Queries a local SQLite database of 20 books. Filters are AND-combined; results ordered by rating descending. | `author` (substring), `genre` (substring), `min_year` (int or null), `min_rating` (float or null), `limit` (int 1–20, default 10) |
Each tool's input schema is generated from its typed signature and returned
in `tools/list`.
It also exposes two **resources**: `books://schema` (the catalog's columns)
and `books://catalog` (the full catalog as JSON).
## The agent
`agent.py` takes a natural-language question and:
1. Spawns `server.py` and connects over stdio with the MCP client.
2. Runs the `initialize` handshake, then `tools/list`.
3. Runs `resources/list` and reads the small ones (the schema) into the system prompt.
4. Converts the MCP tool definitions into the LLM's tool-calling format.
5. Asks the LLM (OpenAI, `gpt-4o-mini` by default) to answer, with the tools available.
6. For each tool call the model returns, executes it via `tools/call` and feeds the result back. Bad tool arguments and protocol errors are turned into text the model can react to.
7. On the final turn the tools are withheld so the model must answer from what it has.
8. Prints the model's answer.
Transient LLM errors are retried with backoff. Every protocol step and token
usage is logged to stderr with an `[agent]` prefix.
```
python agent.py [--model MODEL] [--max-turns N] "your question"
```
## Setup
```bash
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # then set OPENAI_API_KEY
python seed_books.py # creates books.db
```
`.env` keys:
- `OPENAI_API_KEY` – required by the agent.
- `OPENAI_MODEL` – optional, defaults to `gpt-4o-mini`.
- `OPENAI_TEMPERATURE` – optional, defaults to `0`; set to `none` to omit the field.
- `AGENT_MAX_TURNS` – optional, defaults to `5`.
- `GITHUB_TOKEN` – optional, raises the GitHub API rate limit.
Installing the package (`pip install -e .`) also puts a `mcp-tool-agent`
command on your path, equivalent to `python agent.py`.
## Running
```bash
python agent.py "Which fantasy books in the database are rated above 4.3?"
python agent.py "Find repos owned by 'tiangolo' related to 'fastapi', top 3 by stars."
python agent.py "Fetch https://peps.python.org/pep-0020/ and list three aphorisms."
```
To exercise the server on its own without an LLM:
```bash
python - <<'EOF'
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
params = StdioServerParameters(command="./venv/bin/python", args=["server.py"])
async with stdio_client(params) as (r, w):
async with ClientSession(r, w) as s:
await s.initialize()
print([t.name for t in (await s.list_tools()).tools])
res = await s.call_tool("query_books", {"genre": "fantasy", "min_rating": 4.4})
print(res.content[0].text)
asyncio.run(main())
EOF
```
Recorded runs against all three tools, plus error paths (unknown user,
unreachable/404/non-http URLs, missing database, schema validation), are in
[TEST_RUN.md](TEST_RUN.md).
## Tests
```bash
pip install -r requirements-dev.txt
python seed_books.py
ruff check .
mypy server.py agent.py seed_books.py
pytest -q --cov
```
The suite covers the tool logic with mocked HTTP, the SSRF pre-check and the
connection-time backend guard (including IP pinning), `resources/list` and
`resources/read`, a real stdio round-trip that spawns the server, and
`agent.run()` driven by a scripted LLM against that real server. CI runs
ruff, mypy, and the suite on Python 3.11–3.13.
## Notes
- Pinned to `mcp==1.29.x` (the 1.x `FastMCP` API). The 2.x release renames
the server class and changes several APIs.
- No secrets are committed; `.env` and `books.db` are gitignored.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues