MCP Tool Server
MCP Tool Server — AI Agent Tool Integration Platform
A small, production-shaped example of how an AI agent can discover and call reusable backend tools through the Model Context Protocol (MCP), with the same business logic also exposed as a conventional REST API — proving that MCP is an additional interface, not a parallel implementation.
Problem Statement
AI agents need a standard way to call backend capabilities (databases, external APIs, calculators, utilities) without every application inventing its own bespoke tool-calling glue. This project demonstrates that pattern end-to-end: a real agent, backed by a real LLM, discovering and invoking real tools against a real database and a real external API — while proving the tools aren't just an AI-only side channel by exposing the identical logic over REST.
What is MCP?
The Model Context Protocol is an open protocol for connecting AI applications to external tools and data sources through a standard client/server interface. An MCP server exposes typed, discoverable tools (and optionally resources/prompts); an MCP client connects to it, lists what's available, and invokes tools with structured arguments, getting structured results back.
The key difference from "just calling a REST API": tool discovery is dynamic and machine-readable. An agent doesn't need hardcoded knowledge of your API's shape — it asks the server what it can do, gets typed schemas back, and an LLM can reason about which tool fits a request. This project's agent (see app/agent/) never hardcodes tool names; it calls list_tools() at runtime.
Why This Project Exists
To demonstrate, concretely and testably:
A clean separation between transport/adapter layers (MCP tools, REST endpoints) and business logic (services)
Zero duplication of logic between the MCP and REST interfaces
A real (not simulated) LLM-driven agent loop doing tool discovery and selection
Production-style error handling, logging, validation, and testing at every layer
Architecture
flowchart TD
User --> Agent
Agent --> LLM[LLM Provider - Groq]
Agent --> MCPClient[MCP Client]
MCPClient --> MCPServer[FastMCP Server]
MCPServer --> CalcTool[Calculator Tools]
MCPServer --> TaskTool[Task Tools]
MCPServer --> GitHubTool[GitHub Tool]
MCPServer --> UtilTool[Utility Tool]
TaskTool --> TaskService[Task Service]
GitHubTool --> GitHubService[GitHub Service]
TaskService --> TaskRepo[Task Repository]
TaskRepo --> SQLite[(SQLite)]
GitHubService --> GitHubAPI[GitHub Public API]
RestAPI[FastAPI REST API] --> TaskService
RestAPI --> GitHubServiceMCP tools and FastAPI endpoints both call directly into the same service layer (app/services/). Neither interface contains business logic of its own.
Key Features
Calculator tools:
add,subtract,multiply,divide— typed, noeval(), clean division-by-zero handlingTask management: full CRUD (
create_task,list_tasks,get_task,complete_task,delete_task) backed by SQLite, with a repository/service splitGitHub tool:
get_repository_info(owner, repo)against the public GitHub API, with input validation, timeouts, and clean 404/error handlingUtility tool:
text_statistics— word/character/sentence counts and average word lengthREST API: FastAPI endpoints reusing the identical service layer, with OpenAPI docs at
/docsAI Agent: connects via MCP, dynamically discovers tools, uses Groq for tool selection, executes via MCP, and never touches the database or GitHub directly
Tests: 23+ unit and integration tests covering services, repositories, REST endpoints, and MCP tool invocation
Technology Stack
Layer | Technology |
MCP server | FastMCP 2.14 |
REST API | FastAPI |
Validation | Pydantic v2 |
Persistence | SQLite (stdlib |
Testing | pytest, pytest-asyncio |
LLM (agent tool selection) | Groq ( |
Config | pydantic-settings, |
FastMCP is a third-party framework, not part of this project. Credit: PrefectHQ/fastmcp.
Project Structure
MCP-Tool-Server/ ├── app/ │ ├── main.py # FastAPI entrypoint │ ├── config.py # env-based settings │ ├── logging_config.py │ ├── agent/ # LLM provider + agent loop + CLI │ ├── mcp/ │ │ ├── server.py # FastMCP server, tool registration │ │ └── tools/ # thin MCP adapters │ ├── api/ │ │ └── routes.py # thin REST adapters │ ├── services/ # business logic (shared by MCP + REST) │ ├── repositories/ # SQL, isolated here only │ ├── database/ # connection + schema │ └── schemas/ # Pydantic models ├── tests/ │ ├── unit/ │ └── integration/ ├── docs/ │ └── architecture.md ├── requirements.txt ├── requirements-dev.txt └── .env.example
Installation
git clone <your-repo-url>
cd MCP-Tool-Server
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install -r requirements.txt -r requirements-dev.txtEnvironment Configuration
Copy .env.example to .env and fill in your Groq API key (get one free at https://console.groq.com):
DATABASE_URL=sqlite:///./data/app.db GITHUB_API_URL=https://api.github.com LOG_LEVEL=INFO LLM_PROVIDER=groq LLM_API_KEY=your_key_here LLM_MODEL=openai/gpt-oss-120b
.env is gitignored and must never be committed.
Running the MCP Server
python -m app.mcp.serverStarts in stdio transport mode, waiting for an MCP client.
Running the FastAPI REST API
uvicorn app.main:app --reloadDocs at http://127.0.0.1:8000/docs.
Running the AI Agent
python -m app.agent.cliExample prompts: Calculate 125 / 5. Create a high priority task called Finish Resume. What are the stars and forks of PrefectHQ/fastmcp?
Example Tool Calls (via MCP Client)
async with Client(mcp) as client:
result = await client.call_tool("add", {"a": 25, "b": 18})
result = await client.call_tool("create_task", {"title": "Finish Resume", "priority": "high"})
result = await client.call_tool("get_repository_info", {"owner": "PrefectHQ", "repo": "fastmcp"})API Examples (REST)
$body = @{ title = "Prepare for interview"; priority = "high" } | ConvertTo-Json
Invoke-RestMethod -Uri http://127.0.0.1:8000/tasks -Method Post -Body $body -ContentType "application/json"
Invoke-RestMethod -Uri http://127.0.0.1:8000/tasks -Method Get
Invoke-RestMethod -Uri "http://127.0.0.1:8000/github/PrefectHQ/fastmcp" -Method GetTesting
pytest -v23 tests covering calculator logic, task service CRUD + edge cases, GitHub service against faked HTTP responses (no real network calls in tests), REST endpoint integration, and MCP tool invocation via FastMCP's in-process Client.
Design Decisions
No ORM for SQLite: a single-table app doesn't justify SQLAlchemy; hand-written parameterized SQL, isolated entirely inside
repositories/, is simpler to reason about and defend.Service layer is the single source of business logic: both MCP tools and REST routes are thin adapters that call the same functions — verified directly by tests that exercise both paths against the same behavior.
lru_cached settings: environment variables are read once per process; tests explicitly clear this cache when they need to override config (seetests/conftest.py).Logging to stderr, not stdout: the MCP stdio transport uses stdout for protocol messages; logging there would corrupt the stream.
Agent gives a direct, transparent answer rather than a second LLM "explain the result" call: keeps the demo deterministic; a natural-language rephrasing pass is a documented future improvement, not a hidden non-determinism.
Error Handling
Every service raises specific, named exceptions (TaskNotFoundError, RepositoryNotFoundError, GitHubServiceError, ValueError for bad input). FastAPI translates these into structured JSON responses with appropriate status codes (404, 400, 502, 500) via app/main.py's exception handlers — no stack traces are ever returned to a client. On the MCP side, FastMCP converts the same exceptions into ToolErrors that a calling agent must explicitly catch, exactly as demonstrated in app/agent/agent.py.
Security Considerations
No secrets in source code; all config via environment variables,
.envgitignoredNo
eval()anywhere — calculator tools are plain arithmeticGitHub
owner/repoinputs validated against a strict character pattern before being used in a URLAll SQL is parameterized; no string-interpolated queries
External HTTP calls have explicit timeouts
Errors never leak stack traces or internal details to callers
LLM-generated tool arguments are validated by the same Pydantic schemas every other caller goes through — the agent does not get a trusted bypass
Future Improvements
Migrate SQLite → PostgreSQL for concurrent/multi-process use
Containerize with Docker
Add a second LLM provider (OpenAI/Gemini) behind the existing
LLMProviderabstractionAdd a rephrasing pass so the agent's final answer reads naturally rather than stating the raw tool result
Add retry/backoff around the GitHub HTTP client
Correlation IDs across log lines for request tracing
FastMCP Attribution
Built on FastMCP by PrefectHQ. This project is an independent application using FastMCP as a dependency; it is not a fork of, or affiliated with, FastMCP itself.
License
MIT