A web-based GUI for exploring and interacting with Model Context Protocol (MCP) servers. Connect to any MCP server, browse its capabilities, and execute tools, read resources, and test prompts — all from your browser.
- Connect to any MCP server — supports both Streamable HTTP and SSE transports (auto-detects)
- Both protocol eras — speaks the stateless
2026-07-28spec and the olderinitialize-handshake revisions, negotiated automatically or pinned by hand - Authentication — OAuth 2.1 (PKCE, discovery, CIMD/DCR), Bearer token, custom header, or none
- Browse Tools — list all tools, view schemas, fill in parameters, and execute them
- Browse Resources — list and read resources, including URI templates
(
repo://{owner}/{name}/blob/{path}), which get a form and are expanded per RFC 6570 - Browse Prompts — list prompts, fill in arguments, and retrieve rendered output
- Parameter Store — save and reuse frequently used parameter values across sessions
- URL History — remembers previously connected server URLs
- Clean UI — minimal, responsive interface with tabbed navigation
- Python 3.13+
- uv (recommended) or pip
# Clone the repo
git clone https://github.com/ventz/mcp-explorer.git
cd mcp-explorer
# Install dependencies
uv sync
# Run the server
uv run python app.pyOpen http://localhost:8000 in your browser.
pip install fastapi 'uvicorn[standard]' jinja2 'mcp>=2.0.0'
python app.py- Enter an MCP server URL (e.g.,
http://localhost:3000/mcp) - Optionally configure authentication (OAuth 2.1, Bearer token, or custom header)
- Optionally pick a protocol mode — Auto suits almost everything
- Click Connect
- Use the tabs to explore Tools, Resources, and Prompts
- Select an item to view its details, fill in parameters, and execute
The badge next to the status dot shows the negotiated protocol version and the transport actually in use.
Select OAuth 2.1 as the auth type and click Connect. Nothing else is required — no token to paste, no client to pre-register. The explorer follows the MCP authorization spec end to end:
- It calls the server, which answers
401with a pointer to its metadata:WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource" - It fetches that Protected Resource Metadata (RFC 9728) to learn which authorization server to trust.
- It discovers that server's metadata, registers a client (or uses one you
supplied), and builds an authorization URL with PKCE
S256and an RFC 8707resourceindicator naming the MCP server the token is for. - It opens the consent page in a browser pop-up.
- You approve; the authorization server redirects back to
http://localhost:8000/oauth/callback; the explorer exchanges the code for a token and finishes connecting.
Important
Allow pop-ups for localhost:8000. Step 4 is a real browser navigation to
your identity provider — it cannot happen inside the page. If your browser
blocks the pop-up, the connection cannot complete on its own.
You are not stuck if that happens: an Open authorization page ↗ link appears in the OAuth row for as long as the grant is pending. Click it to run the same flow in a normal tab. Better, allow pop-ups once for this origin:
- Chrome / Edge — click the blocked-pop-up icon in the address bar → Always allow pop-ups and redirects from http://localhost:8000
- Firefox — Options on the yellow notification bar → Allow pop-ups for localhost
- Safari — Safari → Settings → Websites → Pop-up Windows → set
localhostto Allow
Closing the consent window before approving cancels the attempt and the explorer says so, rather than waiting for the grant to time out.
Every field on the OAuth row is optional. Reach for them only when the defaults do not fit your provider:
| Field | When you need it | Example |
|---|---|---|
| Scope | The authorization server publishes no scopes_supported, but still rejects a request that omits scope. Overrides discovery. Treated as a floor — if the server later demands more, the extra scopes are added rather than replacing yours. |
notes:read notes:write |
| Client ID | You registered a client with your provider by hand and want to skip registration. | mcp-explorer-local |
| Client secret | Your pre-registered client is confidential rather than public. Stored only if Remember is ticked. | s3cr3t-value-from-your-idp |
| Client metadata URL | You host a Client ID Metadata Document. Must be an HTTPS URL with a path, and the document's own client_id must equal that URL exactly. Preferred by the spec over the now-deprecated Dynamic Client Registration. |
https://apps.example.com/mcp-explorer/client.json |
All values above are illustrative. Substitute the ones your own identity provider issues.
How the client is identified, in the order the explorer tries:
- Client ID you supplied — used as-is, no registration request.
- Client Metadata URL (CIMD) — used when the authorization server
advertises
client_id_metadata_document_supported. - Dynamic Client Registration — the deprecated fallback, used only when the server supports nothing better.
Tokens are held in memory only. They are reused while the app is running, so reconnecting to the same server does not prompt you again, and they are gone the moment the process exits. Sign out discards them immediately and forces a fresh grant on the next connect.
If the server later demands a permission the current token lacks
(403 insufficient_scope), the explorer starts a step-up grant and re-opens the
pop-up mid-session — the same pop-up allowance applies.
Behind a proxy or on a non-default origin, set MCP_EXPLORER_REDIRECT_URI
to the callback URL the browser can actually reach, and register that exact URL
with your provider:
MCP_EXPLORER_REDIRECT_URI="https://mcp-explorer.internal.example/oauth/callback" \
uv run python app.pyServers can expose resources as parameterised URI templates rather than fixed
URIs. Those appear in the Resources tab marked TEMPLATE, listed after the
concrete resources.
Selecting one builds a form from the template's variables. Filling it in and clicking Read Resource expands the template (RFC 6570) and reads the resulting URI, which is shown above the form so you can see exactly what was requested:
repo://{owner}/{name}/blob/{path} → repo://acme/widgets/blob/src%2Fmain.py
Values are percent-encoded, so a value containing / or ? becomes part of the
variable rather than a new path segment or query string. The simple ({var}),
reserved ({+var}), fragment ({#var}), path ({/var}), label ({.var}), and
query ({?var} / {&var}) operators are all supported; templates are also fed
by the Parameter Store like any other form.
A server that implements no templates is not an error — many answer
resources/templates/listwith "method not found", which is treated as an empty list rather than a failure.
mcp-explorer/
├── app.py # FastAPI backend — MCP client + API routes
├── oauth.py # OAuth 2.1 plumbing (token storage, browser redirect bridge)
├── pyproject.toml # Python project metadata & dependencies
├── uv.lock # Pinned dependency versions (committed for reproducible installs)
├── templates/
│ └── index.html # Main HTML template
└── static/
├── app.js # Frontend application logic
└── style.css # Styles
| Method | Path | Description |
|---|---|---|
| GET | / |
Web UI |
| GET | /health |
Health check |
| POST | /api/connect |
Connect to an MCP server |
| POST | /api/disconnect |
Disconnect |
| GET | /api/status |
Connection status |
| GET | /api/tools |
List available tools |
| POST | /api/tools/call |
Execute a tool |
| GET | /api/resources |
List resources and URI templates |
| POST | /api/resources/read |
Read a resource (or an expanded template) |
| GET | /api/prompts |
List available prompts |
| GET | /api/ping |
Liveness probe (polled by the UI) |
| POST | /api/prompts/get |
Get a rendered prompt |
| GET | /oauth/callback |
OAuth 2.1 authorization redirect target |
| POST | /api/oauth/forget |
Discard cached OAuth tokens |
All optional; the defaults suit local use.
| Variable | Default | Purpose |
|---|---|---|
MCP_EXPLORER_HOST |
127.0.0.1 |
Bind address. Only change if you understand the exposure — see below. |
MCP_EXPLORER_BLOCK_PRIVATE |
unset | Set to 1 to also refuse loopback and RFC1918 targets. |
MCP_EXPLORER_REDIRECT_URI |
derived from the request | OAuth callback URL, for running behind a proxy. |
MCP_EXPLORER_ALLOWED_HOSTS |
localhost,127.0.0.1,::1 |
Extra Host values to accept, comma-separated. |
This is a local developer tool. It connects to whatever URL you give it and has no authentication of its own, so keep the following in mind:
- Binds to
127.0.0.1by default. Only processes on your machine can reach it. Override withMCP_EXPLORER_HOSTonly if you understand that exposing the proxy lets anyone who can reach it connect outward through your machine. - SSRF guard. Requests to link-local (including the cloud metadata address
169.254.169.254), multicast, and reserved addresses are always refused. Loopback and private (RFC1918) addresses are allowed by default so you can reach local MCP servers; setMCP_EXPLORER_BLOCK_PRIVATE=1to refuse those too (useful if you ever expose the server). The guard covers every outbound request, including the authorization-server, token, and registration endpoints an OAuth-protected server names in its own metadata. - Host header is pinned. Only
localhost,127.0.0.1, and::1are accepted, which is what makes DNS rebinding (resolving an attacker-controlled domain to127.0.0.1to gain same-origin access to this app) fail. Add more withMCP_EXPLORER_ALLOWED_HOSTS=host1,host2. - Cross-site writes are rejected. State-changing
/api/*requests must come from this app's own origin, so a page you happen to have open cannot drive the proxy on your behalf. Command-line clients are unaffected. - Security headers — a strict
Content-Security-Policy(no inline script),X-Frame-Options: DENY,nosniff,no-referrer, andCross-Origin-Opener-Policy: same-origin, which severswindow.openerfor the authorization pop-up. - Run as a single worker. Connection state is held in-process, so do not run under multiple uvicorn workers — each worker would have its own state.
- Auth credentials are only saved to
localStoragewhen you tick Remember; otherwise the token is used for the connection but not stored. - OAuth tokens never touch disk. They live in memory for the life of the process, so a restart means re-authorizing.
Built on the official mcp Python SDK (v2), which speaks both protocol eras:
| Mode | What happens on connect |
|---|---|
| Auto (default) | Probes server/discover; falls back to the initialize handshake for pre-2026 servers. Picks the newest version both sides support, either era. |
| 2026-07-28 | Pins the stateless spec with no negotiation round trip. One server/discover still runs to confirm the endpoint answers and to fill in identity. |
| Legacy | Forces the initialize handshake (2025-11-25 and earlier). |
2026-07-28 is stateless — there is no session id, no initialize, and ping
no longer exists, so liveness is probed with server/discover instead. Legacy
connections still terminate the session (DELETE /mcp) on disconnect. The
negotiated version and transport are reported by /api/status and shown in the
UI.
MIT © Ventz Petkov