Skip to main content
Glama
ABeltramo

mcp-explorer

by ABeltramo
README.md
# mcp-explorer

A **controllable, observable MCP server**. It speaks the Model Context Protocol to
real MCP clients (Claude, MCP Inspector, your own client), but its behavior is
driven entirely at runtime by *you* over a REST API + SSE stream:

- **Dynamically set the tools** exposed to a given MCP client — per session, with a
  global default. Changing a session's tools pushes `tools/list_changed` so the
  client re-fetches.
- **Observe tool calls in real time and set the response** before it returns —
  either from a **preset** (canned response, optional simulated latency/errors) or
  a **live** human-in-the-loop reply while the call blocks.

Useful as a mock / MITM harness for testing how MCP clients behave against
arbitrary tool sets and arbitrary responses, without writing a bespoke server.

## Architecture

```
              ┌──────────────────────── FastAPI app ────────────────────────┐
 MCP client ──┤  /mcp         Streamable HTTP (stateful)  → low-level Server  │
 (Inspector)  │  /api/*       REST control plane           → SessionRegistry  │
              │  /api/events  SSE event stream             → EventBus         │
              └──────────────────────────────────────────────────────────────┘
 controller ──(REST: set tools, respond)──┘        controller ──(SSE: watch)──┘
```

- Sessions are distinguished by the `Mcp-Session-Id` the server assigns on
  `initialize` (stateful Streamable HTTP). All state is in-memory in one process.
- Built on the MCP Python SDK **v2** low-level `Server` (arbitrary JSON-schema
  tools, per-session tool lists) — see `src/mcp_explorer/mcp_server.py`.

## Run

```bash
uv run mcp-explorer            # serves on http://127.0.0.1:8000
```

Config via env vars: `MCP_EXPLORER_HOST`, `MCP_EXPLORER_PORT`,
`MCP_EXPLORER_TIMEOUT_S` (live-response timeout, default 300s),
`MCP_EXPLORER_MODE` (`manual` | `auto`), `MCP_EXPLORER_API_TOKEN` (optional
bearer token guarding `/api/*`).

- MCP endpoint: `http://127.0.0.1:8000/mcp`
- REST + SSE: `http://127.0.0.1:8000/api/...` (OpenAPI docs at `/docs`)
- Web dashboard: `http://127.0.0.1:8000/live`

## Web dashboard

Open `http://127.0.0.1:8000/live` for a dependency-free (vanilla HTML/CSS/JS)
dashboard that uses the REST + SSE APIs: watch the live event stream, see
connected sessions (each gets a friendly auto-assigned `adjective-animal` name,
which you can override), edit the tool set (global or per-session), toggle a
session's mode, set presets, and answer blocked tool calls inline.

The UI is **fully stream-driven — it does not poll**. It fetches state once on
load, then updates reactively from the SSE stream: pending-call counts come
straight from tool-call events, and structural changes (a session connecting, or
tools/mode/preset changes — the latter emit a `session_updated` event) trigger a
single authoritative resync. It also resyncs whenever the `EventSource`
reconnects. Because browsers can't attach headers to an `EventSource`, the live
stream assumes `MCP_EXPLORER_API_TOKEN` is unset (the default).

## Quick start

```bash
# 1. start the server
uv run mcp-explorer

# 2. define a default tool set
uv run python examples/set_tools.py

# 3. connect a client, e.g. the MCP Inspector
npx @modelcontextprotocol/inspector    # point it at http://127.0.0.1:8000/mcp

# 4. watch calls and answer them live
uv run python examples/respond_live.py
```

## Interception modes

Each session has a `mode` (default from `MCP_EXPLORER_MODE`):

- **manual** — a tool call blocks until either a matching preset exists or you
  POST a live response; if neither arrives within the timeout it returns an error.
- **auto** — a call with a preset returns it immediately; a call *without* a
  preset returns a fast "no preset configured" error instead of blocking.

Presets always take precedence over blocking, in either mode.

## REST API

| Method & path | Purpose |
|---|---|
| `GET /api/healthz` | Liveness. |
| `GET /api/sessions` | List active sessions. |
| `GET /api/sessions/{sid}` | Session detail. |
| `PUT /api/sessions/{sid}/name` | Rename a session (human-readable label). |
| `PUT /api/tools` | Set the global default tool set. |
| `PUT /api/sessions/{sid}/tools` | Set tools for one session (→ `list_changed`). |
| `DELETE /api/sessions/{sid}/tools` | Revert a session to the default tool set. |
| `PUT /api/sessions/{sid}/mode` | Set `manual` / `auto`. |
| `PUT /api/sessions/{sid}/tools/{name}/preset` | Set/replace a preset response. |
| `DELETE /api/sessions/{sid}/tools/{name}/preset` | Remove a preset. |
| `GET /api/sessions/{sid}/pending` | Calls currently awaiting a response. |
| `POST /api/calls/{call_id}/respond` | Resolve a blocked call with a live payload. |
| `GET /api/events` | SSE stream (optional `?session_id=` filter). |

Tool definitions (`ToolDef`) carry a raw JSON Schema in `input_schema`, so you can
expose any tool shape. Responses (`PresetResponse` / live `RespondPayload`) accept
`text` (single text block) or `content` (list of MCP content blocks),
`structured_content`, and `is_error`; presets also accept `delay_s`.

## Events

SSE emits: `session_created`, `session_closed`, `tools_updated`,
`tool_call_started`, `tool_call_awaiting_response`, `tool_call_completed`,
`tool_call_timeout` — each with `type`, `session_id`, `timestamp`, and `data`.

## Tests

```bash
uv run pytest
```

`tests/test_end_to_end.py` boots a real server and drives it with a real MCP
client to exercise dynamic tools, live interception, presets, and timeouts.

## Notes / limitations

- Single-process, in-memory state. Horizontal scaling would need shared state +
  sticky sessions (out of scope).
- `tools/list_changed` requires the client to hold its stream open; modern
  protocol clients (e.g. the Inspector) do.