Skip to main content
Glama
README.md
# DevTools MCP

A small developer-utility MCP server, built to learn the Model Context
Protocol end-to-end: server implementation, local testing, using an existing
MCP, public deployment, and Smithery publishing.

## 1. Overview

DevTools MCP exposes four small developer tools over the Model Context
Protocol: explaining an error message, validating/formatting JSON,
generating a regex from a description, and summarizing text with an LLM
(Groq). A minimal TypeScript/Vite dashboard lets you exercise the tools from
a browser, connecting as a real MCP client.

## 2. Why MCP

MCP standardizes how an LLM host (Claude Desktop, an IDE, an agent) discovers
and calls tools, instead of every project inventing its own bespoke
tool-calling API. Building a real MCP server - not a REST API with an MCP
label slapped on - was the primary learning goal of this project.

## 3. Architecture

```
MCP Client
    |
MCP Protocol
    |
DevTools MCP Server
    |-- explain_error    (local/deterministic)
    |-- format_json      (local/deterministic)
    |-- generate_regex   (local/deterministic)
    `-- summarize_text
            |
        Groq API
            |
        GPT-OSS 120B
```

The server (`server/server.py`) is an `mcp.server.MCPServer` (MCP Python SDK
v2). It runs over **stdio** for local testing (MCP Inspector, `Client(mcp)`)
and over **Streamable HTTP** (`/mcp`) for remote/browser access. The
TypeScript frontend (`frontend/`) is a genuine MCP client: it uses
`@modelcontextprotocol/sdk`'s `Client` + `StreamableHTTPClientTransport` to
talk to the server directly over Streamable HTTP (CORS-enabled on the
server), not a hand-rolled REST bridge.

## 4. Tools

| Tool | Inputs | What it does |
|---|---|---|
| `explain_error` | `error_message`, `language_or_framework?` | Matches the error against a library of common error patterns (Python/JS/general) and returns a likely cause + practical fix. Local/deterministic. |
| `format_json` | `json_text` | Validates JSON and returns a pretty-printed version, or a precise parse error (line/column). Local/deterministic. |
| `generate_regex` | `description` | Matches the description against a small library of common regex patterns (email, URL, IPv4, date, UUID, etc.) and returns the pattern plus an explanation. Local/deterministic. |
| `summarize_text` | `text`, `max_length?` | Calls Groq (`openai/gpt-oss-120b`) to produce a concise summary. Handles missing credentials, timeouts, and API errors gracefully. |

## 5. Project structure

```
devtools-mcp/
├── server/
│   ├── server.py               # MCPServer + tool registration + ASGI app
│   ├── tools.py                # explain_error / format_json / generate_regex logic
│   ├── ai.py                   # Groq-backed summarize_text logic
│   └── tests/
│       └── test_server.py      # pytest suite using the SDK's in-memory Client
├── frontend/
│   ├── index.html
│   ├── src/
│   │   ├── main.ts             # real MCP client (StreamableHTTPClientTransport)
│   │   └── style.css
│   ├── package.json
│   ├── tsconfig.json
│   └── vite.config.ts
├── .env.example
├── .gitignore
├── requirements.txt
├── render.yaml                 # optional Render Blueprint
├── README.md
└── EXISTING_MCP_EXPERIENCE.md
```

## 6. Prerequisites

- Python 3.10+
- Node.js 18+ and npm (for the frontend, and to run the MCP Inspector via `npx`)
- A [Groq](https://console.groq.com/) API key (only required for `summarize_text`)
- (Optional, for deployment) A [Render](https://render.com/) account and a
  [Smithery](https://smithery.ai/) account

## 7. Installation

```bash
git clone <this-repo>
cd devtools-mcp
python3 -m venv .venv
. .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install -r requirements.txt
```

## 8. Environment variables

Copy `.env.example` to `.env` and fill in what you need:

```
GROQ_API_KEY=            # required for summarize_text
GROQ_MODEL=openai/gpt-oss-120b
MCP_ALLOWED_HOSTS=        # only needed when deployed behind a real hostname
MCP_ALLOWED_ORIGINS=      # comma-separated browser origins allowed via CORS
```

`.env` is git-ignored. Never commit real secrets.

## 9. Local setup

Stdio (default, for local MCP clients):

```bash
python -m server.server
```

Streamable HTTP (for the frontend, or any HTTP-based MCP client), local only:

```bash
uvicorn server.server:app --host 127.0.0.1 --port 8000
```

`MCP_ALLOWED_HOSTS` can stay unset locally - the SDK's built-in
localhost-only DNS-rebinding protection covers `127.0.0.1`/`localhost`
automatically. Health check: `curl http://127.0.0.1:8000/health`.

## 10. MCP Inspector testing

```bash
# Against stdio:
uv run mcp dev server/server.py     # requires uv; or: npx @modelcontextprotocol/inspector
# Against a running Streamable HTTP server:
npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8000/mcp --method tools/list
```

This was run against the local Streamable HTTP server during development and
verified all four tools are discoverable with correct input/output schemas
(see "Testing" below for the exact results).

## 11. Frontend setup

```bash
cd frontend
npm install
npm run dev          # http://localhost:5173
```

In the running dashboard, set the server URL field to your MCP server's
`/mcp` endpoint (default `http://localhost:8000/mcp`), click **Connect**,
pick a tool, fill in the form, and click **Run**. For local use, start the
backend with `MCP_ALLOWED_ORIGINS=http://localhost:5173` so CORS allows it.

Production build: `npm run build` (outputs to `frontend/dist/`).

## 12. Groq setup

1. Create an API key at [console.groq.com](https://console.groq.com/).
2. Set `GROQ_API_KEY` (and optionally `GROQ_MODEL`, default
   `openai/gpt-oss-120b`) in your `.env` or your deployment platform's
   environment variables.
3. No other LLM provider is used anywhere in this project.

## 13. Existing MCP experience

See [`EXISTING_MCP_EXPERIENCE.md`](./EXISTING_MCP_EXPERIENCE.md) for the
required demonstration of using an existing MCP server (Context7) - what it
is, how it was connected, the actual query run, and what was learned.

## 14. Deployment to Render

Render's native Python runtime is used (no Docker required).

**Dashboard setup:**

1. Push this repo to GitHub.
2. In Render: New → Web Service → connect the repo.
3. Runtime: Python 3. Build command: `pip install -r requirements.txt`.
   Start command: `uvicorn server.server:app --host 0.0.0.0 --port $PORT`.
4. Set environment variables: `GROQ_API_KEY`, `GROQ_MODEL`,
   `MCP_ALLOWED_HOSTS=<your-service>.onrender.com,<your-service>.onrender.com:*`,
   and `MCP_ALLOWED_ORIGINS=<your-frontend-origin>` (if you also deploy the
   frontend).
5. Deploy. The MCP endpoint will be `https://<your-service>.onrender.com/mcp`.

A `render.yaml` Blueprint is included as a convenience for the same setup.

**Manual verification step required:** actually deploying requires a Render
account and was not performed as part of this response - see the
completion report for what remains a manual step.

## 15. Smithery publishing

The current Smithery CLI supports publishing an already-hosted remote MCP
server URL directly (no Docker/container packaging needed for this path):

```bash
npm install -g smithery
smithery auth login
smithery mcp publish "https://<your-service>.onrender.com/mcp" -n "<your-org>/devtools-mcp"
```

After publishing, verify the four tools are exposed:

```bash
smithery mcp add "https://<your-service>.onrender.com/mcp" --id devtools-mcp
smithery tool list devtools-mcp
```

**Manual step required:** this needs a Smithery account and a live, publicly
reachable Render deployment first; it was not performed as part of this
response.

## 16. Public MCP usage

Once deployed, any Streamable HTTP MCP client can connect to:

```
https://<your-service>.onrender.com/mcp
```

Example with the SDK's `Client`:

```python
from mcp import Client
from mcp.client.streamable_http import streamable_http_client

async with streamable_http_client("https://<your-service>.onrender.com/mcp") as (r, w, _):
    async with Client(r, w) as client:
        await client.initialize()
        print(await client.list_tools())
```

## 17. Testing

Actually run in this environment:

```bash
pytest server/tests/ -v
```

Result: **11 passed** - tool discovery; valid, malformed, and empty
`format_json` input; matched and unmatched `explain_error` patterns
(including empty input); `generate_regex` for a known pattern (with a live
regex match check) and an unmatched description; `summarize_text` with a
missing `GROQ_API_KEY` and with empty input.

Also actually run (manual, outside pytest):

- `uvicorn server.server:app` started successfully; `/health` returned
  `{"status":"ok",...}`.
- A raw `initialize` JSON-RPC POST to `/mcp` returned `200`.
- The real MCP Inspector CLI (`npx @modelcontextprotocol/inspector --cli`)
  connected over Streamable HTTP, listed all four tools with correct
  schemas, and successfully called `generate_regex`, `explain_error`, and
  `format_json` (both valid and invalid JSON), and `summarize_text`
  (correctly reported the missing-API-key error, since no real Groq key was
  available in this environment).
- Transport security verified: a request with a spoofed `Host` header
  correctly received `421 Misdirected Request`.
- CORS preflight verified: an `OPTIONS /mcp` request with
  `Origin: http://localhost:5173` returned `200` with the correct
  `access-control-*` headers once `MCP_ALLOWED_ORIGINS` was set.
- Frontend: `npx tsc --noEmit` passed with no errors; `npm run build`
  succeeded and produced `frontend/dist/`.

**Not verified** (requires external accounts/credentials not available in
this environment): a real `summarize_text` call with a live Groq API key,
the Render deployment itself, and Smithery publishing/listing.

## 18. Limitations

- `summarize_text` was only tested end-to-end for its error paths; it has
  not been called with real Groq credentials.
- Render deployment and Smithery publishing require manual steps with your
  own accounts (see sections 14-15) and have not been performed here.
- `explain_error` and `generate_regex` use small, hand-written pattern
  libraries, not an LLM - they're deliberately simple/deterministic per the
  project's scope, so they won't recognize every possible error or pattern
  description.
- The frontend has no authentication and is meant for local/demo use, per
  the project's explicit "no accounts/auth" scope.

## 19. Learning outcomes

- **What MCP is:** a standardized protocol separating "providing context/
  actions to an LLM" from "the LLM interaction itself," so a server built
  once (like this one) works with any compliant client.
- **Host / client / server:** the host is the LLM application (Claude
  Desktop, or the app behind the browser dashboard); the client is the
  MCP-speaking component inside it (the SDK's `Client`, or our frontend's
  `StreamableHTTPClientTransport`-based client); the server is what we
  built - it never talks to a model directly.
- **Tools vs. resources vs. prompts:** tools are model-controlled (the LLM
  decides to call `format_json`); resources are application-controlled data
  loads; prompts are user-invoked templates. This project only needed tools.
- **Tool discovery and invocation:** a client calls `tools/list` to learn
  what's available (name, description, JSON-schema inputs/outputs, all
  derived automatically from Python type hints and docstrings), then
  `tools/call` to invoke one by name with arguments.
- **Why MCP vs. a plain REST API:** a REST API needs a bespoke integration
  per client; an MCP server describes its own capabilities and schemas, so
  any MCP-aware host can use it without custom glue code - demonstrated
  directly by connecting the same server to both the MCP Inspector and our
  own hand-built frontend client with zero server-side changes.
- **Where the LLM fits:** only inside `summarize_text`, which calls out to
  Groq. The rest of the server is plain deterministic code - a useful
  reminder that "MCP server" and "AI application" are not the same thing.
- **Deployment realities:** Streamable HTTP servers default to
  localhost-only Host/Origin allowlisting for safety, and that has to be
  explicitly opened up (`TransportSecuritySettings`) once deployed behind a
  real hostname - confirmed hands-on by triggering and then fixing a `421`.