mcp-tools-server
mcp-tools-server
MCP server on the official Python SDK (FastMCP): four tools, stdio and streamable HTTP transports from one codebase, read-only SQL, a filesystem sandbox, offline test suite.
What this demonstrates
MCP servers with the official SDK: tools, resources, schemas, lifecycle, both transports.
Tool design for LLMs: names, argument schemas and docstrings are the interface the model reasons over, so descriptions carry the DB schema, examples and constraints on purpose.
Least-privilege security controls: SQL locked to read-only via a SQLite authorizer rather than regex filtering (plus multi-statement injection rejection), and a filesystem sandbox built on
resolve()+is_relative_to, with size caps and binary detection.Structured JSON logging to stderr: every tool call, rejection and startup is one machine-parseable line, shapes and sizes only — never SQL text, rows or file contents.
Layering: tool logic is pure typed functions in
app/tools/with zero MCP imports;app/server.pyregisters thin wrappers.
Architecture
flowchart LR
CD["Claude Desktop / Inspector"] -- stdio --> S["FastMCP<br/>portfolio-tools"]
ANY["any MCP client"] -- "streamable HTTP :8082/mcp" --> S
S --> SW["search_web"] --> IDX[("search_index.json")]
S --> QD["query_database"] --> DB[("demo.db<br/>SQLite, read-only")]
S --> FS["read_file / list_dir"] --> DOCS[("data/docs<br/>sandbox")]Tools
Tool | Arguments | Returns |
|
| ranked |
|
|
|
|
| UTF-8 file content inside the sandbox, 100 KB cap |
|
| sorted entries with type and size |
All four tools declare TypedDict results, so responses ship as machine-readable structuredContent (with an output schema) alongside the usual JSON text.
The files in data/docs/ are also published as MCP resources — concrete docs://<file> entries plus a docs://{name} template; resource reads go through read_file's sandbox and size cap.
Demo DB (seeded on first query, ~25 rows of job-market data): companies(id, name, industry, city), vacancies(id, company_id, title, grade, salary_rub, stack), applications(id, vacancy_id, applied_at, status).
Quickstart
make install
make run-stdio # stdio (default), for clients that spawn the server
make run-http # streamable HTTP on :8082/mcp
# or: docker compose up --buildDebugging: npx @modelcontextprotocol/inspector python -m app.server.
Claude Desktop
Add to claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/, Windows: %APPDATA%\Claude\), restart, and the four tools appear in a new chat:
{
"mcpServers": {
"portfolio-tools": {
"command": "python",
"args": ["-m", "app.server", "--transport", "stdio"],
"env": {
"PYTHONPATH": "/absolute/path/to/mcp-tools-server",
"DATA_DIR": "/absolute/path/to/mcp-tools-server/data"
}
}
}
}Claude Desktop sets no working directory for servers, so both paths must be absolute; if deps live in a venv, use its interpreter as command.
Configuration
Read from the process environment, no dotenv (see .env.example): DATA_DIR — sandbox root holding search_index.json, demo.db and docs/ (default ./data); MCP_HOST / MCP_PORT — HTTP transport bind (default 0.0.0.0:8082); MCP_API_KEYS — comma-separated bearer keys required by the HTTP transport (default demo-key; stdio is not gated — its client is whoever spawned the process).
Notes
search_webis an offline stub: keyword ranking over 14 curated entries, so demos and tests are deterministic. A real search API drops in behind the same contract.query_databaselayers, least-privilege: read-only URI (mode=ro), asqlite3authorizer that allowlists SELECT/READ/FUNCTION and denies writes, DDL, PRAGMA, ATTACH and transactions,complete_statementrejection ofSELECT 1; DROP ...payloads, and row/cell/result-size caps.read_file/list_dirare sandboxed toDATA_DIR: absolute paths are accepted only when they resolve inside the root; any escape (.., outside path, or symlink target) is denied.Structured JSON logging (
app/core/logging.py) writes one JSON object per line to stderr (stdout carries the stdio JSON-RPC frames), logging only call shapes/sizes/paths — never data.All failures raise
ToolErrorwith a single-line message — clients see actionable errors, never tracebacks.
Testing
106 tests, offline: units hit the pure functions directly (including the HTTP bearer gate through httpx.ASGITransport); integration runs a real MCP client against the server in memory via the SDK's create_connected_server_and_client_session — tools, structured output, resources and error mapping included.
make install-dev && make test; make lint for ruff, make typecheck for strict mypy. CI runs lint + strict mypy + tests, a security job (pip-audit + bandit), and CodeQL; Dependabot keeps deps current. The seeded data/ (demo.db, docs/, search_index.json) is resolved from the repo root by default; set DATA_DIR to relocate the sandbox.
MIT. Portfolio demo — siblings: llm-gateway · rag-pgvector · agent-orchestrator, which calls this server's search_web over HTTP. Run the whole platform (all four services + web console + Postgres/Redis behind one origin) from the umbrella compose in llm-platform-console — docker compose up --build there brings everything up offline on mock models.
Releases
Version history is in CHANGELOG.md.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/INTERpol21/mcp-tools-server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server