Skip to main content
Glama
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.

[![MCP](https://img.shields.io/badge/MCP-compatible-blueviolet)](https://modelcontextprotocol.io)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org)
[![Tests](https://img.shields.io/badge/tests-27%20passing-brightgreen)](#-testing)
[![License](https://img.shields.io/badge/license-MIT-green)](./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).