Skip to main content
Glama

πŸ› οΈ 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 Python Tests License

πŸ”— Live Playground: Glama MCP Link β€” try the tools directly in the browser once listed (see 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.


Related MCP server: FireScrape MCP Server

✨ 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 β€” MCP server framework; turns Python functions into MCP tools via @mcp.tool

  • requests + beautifulsoup4 β€” web scraping

  • ddgs β€” key-free live web search (formerly duckduckgo-search)

  • sqlite3 β€” built into Python, used for local database queries

  • psycopg2 β€” Postgres/Supabase client, imported lazily only when a Postgres URL is used

  • python-dotenv β€” loads local .env variables

  • pytest + 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

git clone https://github.com/YOUR_USERNAME/YOUR_REPO.git
cd YOUR_REPO

2. Create a virtual environment & install dependencies

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:

# 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

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):

{
  "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).

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.

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.

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.

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.

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 β€” 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 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 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.

Related MCP Connectors

Related MCP Servers