DevTools MCP Server
by notayannn
README.md
# π οΈ DevTools MCP Server
A lightweight **Model Context Protocol (MCP)** server that gives any MCP-compatible LLM client (Claude Desktop, Claude Code, Cursor, etc.) a developer toolbox: web scraping, log inspection, live web search, and read-only SQL querying against SQLite or Postgres/Supabase β all through one server.
[](https://modelcontextprotocol.io)
[](https://www.python.org)
[](#-testing)
[](./LICENSE)
> π **Live Playground:** [Glama MCP Link](https://glama.ai/mcp/servers/notayannn/devtools-mcp) β try the tools directly in the browser once listed (see [Deployment](#-deployment)).
---
## π Overview
**DevTools MCP** exposes four tools over MCP so an LLM assistant can:
- Pull clean, readable text from any webpage
- Tail your local log files to debug errors
- Search the live web for current documentation before writing code
- Run read-only `SELECT` queries against a local SQLite file *or* a live Postgres/Supabase database
Every tool is a plain, testable Python function β nothing here depends on paid APIs except your own optional Supabase project.
---
## β¨ Features
| Tool | Description |
|---|---|
| π `fetch_markdown(url)` | Fetches a webpage, strips `script`/`style`/`nav`/`footer`, and returns clean text (capped at 8,000 characters). |
| π `read_log(file_path, lines)` | Reads the last N lines of a local file β surfaces recent stack traces or error output. |
| π `search_web(query, max_results)` | Searches the live web via DuckDuckGo (`ddgs`, no API key required) for up-to-date docs or solutions. |
| ποΈ `query_database(db_path_or_url, sql_query, limit)` | Runs a **read-only** `SELECT` against a local SQLite file or a Postgres/Supabase connection string, capped at `limit` rows. |
---
## ποΈ Architecture
```
ββββββββββββββββββββββββ
β MCP Client β (Claude Desktop / Claude Code / Cursor / etc.)
ββββββββββββ¬βββββββββββββ
β MCP protocol (stdio)
ββββββββββββΌβββββββββββββ
β DevTools MCP Server β FastMCP("DevTools") β server.py
β β
β ββββββββββββββββββββ β
β β fetch_markdown β ββββΆ requests + BeautifulSoup βββΆ any URL
β ββββββββββββββββββββ€ β
β β read_log β ββββΆ local filesystem
β ββββββββββββββββββββ€ β
β β search_web β ββββΆ DDGS (DuckDuckGo, key-free)
β ββββββββββββββββββββ€ β
β β query_database β ββββΆ _is_safe_select() (SQL safety gate)
β β β β β
β β β β ββββΆ _query_sqlite() βββΆ local .db file
β β β β ββββΆ _query_postgres() βββΆ Postgres / Supabase
β ββββββββββββββββββββ β
ββββββββββββββββββββββββββ
```
### How `query_database` decides where to send a query
```
query_database(db_path_or_url, sql_query, limit)
β
βΌ
_is_safe_select(sql_query)?
β
ββββββ΄βββββ
NO YES
β β
reject does db_path_or_url start with
query "postgres://" or "postgresql://" ?
β
βββββββ΄ββββββ
YES NO
β β
_query_postgres() _query_sqlite()
```
`_is_safe_select` is a hard gate that only allows single, plain `SELECT` statements β no `INSERT`/`UPDATE`/`DELETE`/`DROP`/`ALTER`/etc., and no stacked queries chained with `;`. This matters because the SQL text is generated by an LLM, not typed by hand β the gate is there so a hallucinated or manipulated query can't mutate or destroy your data.
**Stack:**
- [`fastmcp`](https://github.com/jlowin/fastmcp) β MCP server framework; turns Python functions into MCP tools via `@mcp.tool`
- [`requests`](https://pypi.org/project/requests/) + [`beautifulsoup4`](https://pypi.org/project/beautifulsoup4/) β web scraping
- [`ddgs`](https://pypi.org/project/ddgs/) β key-free live web search (formerly `duckduckgo-search`)
- [`sqlite3`](https://docs.python.org/3/library/sqlite3.html) β built into Python, used for local database queries
- [`psycopg2`](https://www.psycopg.org/) β Postgres/Supabase client, imported lazily only when a Postgres URL is used
- [`python-dotenv`](https://pypi.org/project/python-dotenv/) β loads local `.env` variables
- [`pytest`](https://pytest.org) + [`pytest-mock`](https://pypi.org/project/pytest-mock/) β test suite
---
## π Project Structure
```
.
βββ venv/ # Local virtual environment (not committed)
βββ .env # Local secrets β real keys/paths, never committed
βββ .gitignore
βββ README.md
βββ requirements.txt # Runtime + dev/test dependencies
βββ server.py # Main MCP server β all 4 tools live here
βββ test_server.py # Pytest suite covering all 4 tools
βββ Dockerfile # Optional β only needed for Glama's hosted deployment
βββ glama.json # Optional β repo attribution for Glama's listing
βββ smithery.yaml # Optional β only relevant if also listing on Smithery
```
---
## π Getting Started
### 1. Clone the repo
```bash
git clone https://github.com/YOUR_USERNAME/YOUR_REPO.git
cd YOUR_REPO
```
### 2. Create a virtual environment & install dependencies
```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install -r requirements.txt
```
### 3. Configure environment variables (optional)
`server.py` calls `load_dotenv()` on startup, so any variables in a local `.env` file are picked up automatically. None of the current tools *require* env vars β `query_database` takes its connection info as a direct parameter β but you may still want a `.env` for local convenience:
```env
# Only needed if you want a default connection string handy locally.
# Real credentials should live here and nowhere else.
SUPABASE_DB_URL=postgresql://postgres:your-password@db.xxxxxxxx.supabase.co:5432/postgres
```
> β οΈ **Never commit your `.env` file.** It's already excluded via `.gitignore`.
>
> Note: this is different from a Supabase project's `SUPABASE_URL` / `SUPABASE_KEY` (used by the REST/JS client). `query_database` talks to Postgres directly via `psycopg2`, so it needs the **Postgres connection string** from your Supabase dashboard under **Settings β Database β Connection string**, not the API URL/key pair.
### 4. Run the server locally
```bash
python server.py
```
This starts the MCP server over stdio, ready to be connected to any MCP client.
---
## π Connecting to Claude Desktop / Claude Code
Add the server to your MCP client config (e.g. `claude_desktop_config.json`):
```json
{
"mcpServers": {
"devtools": {
"command": "python",
"args": ["/absolute/path/to/server.py"]
}
}
}
```
Restart your client β the four tools (`fetch_markdown`, `read_log`, `search_web`, `query_database`) will appear as functions the assistant can call.
---
## π§° Tool Reference
### `fetch_markdown(url: str) -> str`
Fetches a webpage, strips `<script>`, `<style>`, `<nav>`, and `<footer>` tags, and returns cleaned plain text (capped at 8,000 characters).
```python
fetch_markdown("https://docs.python.org/3/library/asyncio.html")
```
### `read_log(file_path: str, lines: int = 50) -> str`
Reads the last `lines` lines of a local text/log file.
```python
read_log("/var/log/app/error.log", lines=100)
```
### `search_web(query: str, max_results: int = 3) -> str`
Searches DuckDuckGo for the given query and returns title, link, and snippet for each result.
```python
search_web("fastapi background tasks example")
```
### `query_database(db_path_or_url: str, sql_query: str, limit: int = 50) -> str`
Runs a **read-only** `SELECT` against:
- a local SQLite file (pass a file path), or
- a Postgres/Supabase database (pass a connection string starting with `postgres://` or `postgresql://`)
Results are returned as a list of `{column: value}` dictionaries, capped at `limit` rows.
```python
query_database("app.db", "SELECT * FROM users WHERE status = 'active'", limit=5)
query_database("postgresql://user:pass@host:5432/db", "SELECT id, email FROM users", limit=10)
```
**Safety guarantees:**
- Only queries starting with `SELECT` are allowed
- Queries containing `INSERT`, `UPDATE`, `DELETE`, `DROP`, `ALTER`, `TRUNCATE`, `GRANT`, `REVOKE`, `CREATE`, or `ATTACH` anywhere are rejected
- Stacked queries (`SELECT ...; DROP TABLE ...`) are rejected
- Known limitation: the check is a substring match, not a full SQL parser β a harmless query like `SELECT * FROM updates_log` will also be rejected, since it contains the substring `update`. This is a deliberate false-positive-over-false-negative tradeoff.
---
## π§ͺ Testing
The project ships with a 27-test `pytest` suite covering all four tools, run fully offline via mocked network calls and throwaway `tmp_path` fixtures β nothing touches a real file, database, or website.
```bash
pip install -r requirements.txt
pytest test_server.py -v
```
What's covered:
- `_is_safe_select` β 10+ cases across valid selects, every forbidden keyword, stacked queries, and known false-positive behavior
- `query_database` (SQLite) β basic select, `limit`, `WHERE` filtering, blocked unsafe queries, missing file, missing table, empty result set, and Postgres URL routing (mocked)
- `read_log` β tail behavior, missing file, default line count
- `fetch_markdown` β HTML stripping and error handling (network mocked)
- `search_web` β result formatting, empty results, error handling (network mocked)
> `_query_postgres` itself is not exercised against a live database in this suite β only the routing logic that decides *whether* to call it. Testing it live requires a real Postgres/Supabase connection string, which should never be hardcoded into test files or committed to the repo.
---
## π Deployment
### Option A β Glama (free directory listing + browser inspector)
Submit this repo's GitHub URL at [glama.ai/mcp](https://glama.ai/mcp) β Glama indexes your tools directly from the source, no build or manifest required. Visitors get an in-browser inspector to try `fetch_markdown`, `read_log`, `search_web`, and `query_database` without installing anything locally.
Optional: add `glama.json` (already included) to claim/attribute the listing to your GitHub account.
### Option B β Glama hosted deployment (Glama runs it for you, 24/7)
Connect the Glama GitHub App to this repo and it builds the included `Dockerfile` into a running instance behind Glama's gateway (managed TLS, auth, logging). Check [glama.ai/mcp/hosting](https://glama.ai/mcp/hosting) for current plan details before committing to this path.
### Option C β Smithery
β οΈ As of early 2026, Smithery no longer accepts new **free** hosted deployments via GitHub β that now requires a paid plan. The free path on Smithery is registering this server as an **external server** (i.e. you host it yourself β e.g. via Glama's hosted option above β and just point Smithery's listing at that URL). `smithery.yaml` is still included in this repo in case you go that route; see [smithery.ai](https://smithery.ai) for current details, since their hosting model is actively changing.
---
## π Environment Variables
| Variable | Required | Used by |
|---|---|---|
| `SUPABASE_DB_URL` (or any Postgres URL) | β Optional | Not read automatically β `query_database` takes the connection string as a direct argument. Useful only as a personal reference/convenience in `.env`. |
`query_database` is intentionally stateless with respect to credentials β nothing is read from environment variables inside the tool itself, so no database credentials are ever stored server-side by default.
---
## πΊοΈ Roadmap
- [ ] Add a real integration test against a disposable Postgres/Supabase instance (CI-only, credentials never committed)
- [ ] Replace the substring-based SQL keyword check with a proper SQL parser (e.g. `sqlparse`) to eliminate false positives
- [ ] Add caching for `search_web` and `fetch_markdown`
- [ ] Add an authentication layer for hosted Smithery deployments
---
## π€ Contributing
Contributions, issues, and feature requests are welcome β feel free to open a PR or issue.
---
## π License
This project is licensed under the [MIT License](./LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues