How sharpit works¶
sharpit is a developer console that runs inside your FastAPI app while you develop it. You add one line:
Then open http://127.0.0.1:8000/sharpit and you see what your app is really running with: its configuration, its
routes, its loggers, its dependency graph and the known vulnerabilities in its packages. It also provides live runtime
diagnostics: HTTP exchanges, exceptions, database queries, logs, thread and task state, health, and advisors. An AI
agent can ask the same questions over MCP, the protocol coding agents use to call tools. sharpit is a Python port of
BootUI, the console for Spring Boot. It speaks BootUI's JSON contract and has
a UI of its own.
The parts¶
your FastAPI app
└─ sharpit.install(app) src/sharpit/fastapi/adapter.py
├─ settings (env > kwargs > defaults) src/sharpit/settings.py
├─ activation: on or off? src/sharpit/engine/activation.py + fastapi/signals.py
├─ capture hooks (active only) fastapi/capture.py + engine/capture/
└─ mounts /sharpit, a small Starlette sub-app
├─ guard middleware (every request) src/sharpit/engine/guard.py
├─ /sharpit/ the UI (Svelte 5 bundle) src/sharpit/ui/static/
├─ /sharpit/api/... JSON panels engine/collectors/*
├─ /sharpit/api/cli/tools/<tool> CLI tools over HTTP engine/tools.py
└─ /sharpit/api/mcp MCP server (off by default) engine/mcp.py
There are two layers:
- The engine (
src/sharpit/engine/) knows nothing about FastAPI or Starlette. It holds the rules: activation, the guard, masking, collectors, live streams, and the MCP server. Each collector takes plain data and returns BootUI's JSON shapes. - The FastAPI adapter (
src/sharpit/fastapi/) is the only code that touches the web framework. It reads your app's routes, dependencies and lifespan (introspector.py), mounts the sub-app, attaches runtime capture hooks, and turns HTTP requests into engine calls. A Django or Flask adapter would replace only this layer.
When it turns on¶
install() decides once, at startup, in this order. The first rule that matches wins:
- An invalid
SHARPIT_ENABLEDvalue → off. - An active profile is
prodorproduction→ off. OnlySHARPIT_ENABLED=ONoverrides this, with a warning. SHARPIT_ENABLED=OFF→ off.SHARPIT_ENABLED=ON→ on.- An active profile is
dev,localordevelopment→ on. The profile comes fromSHARPIT_PROFILES,APP_ENVorENVIRONMENT. - The process was started by a reloading dev server,
uvicorn --reloadorfastapi dev→ on. - Otherwise → off.
When it is off, every path under /sharpit answers 404 and none of sharpit's code runs for those requests.
When it is off, capture hooks are not registered and runtime monitoring is completely skipped.
The capture model and per-panel hooks¶
When active, install() wires runtime capture hooks. Each hook is installed only if its associated panels are enabled:
-
ASGI capture middleware (
CaptureMiddleware). Placed as the outermost middleware inside Starlette'sServerErrorMiddleware. It records request method, masked path, status code, duration, and error flag. If a client disconnects before the app finishes, it records status 499. It creates aContextVar[RequestInfo | None]token (REQUEST) so SQL statements and exceptions correlate with the HTTP request. This middleware is skipped whenhttp-exchanges,exceptions,sql-trace, andactivityare all disabled. -
Exception handler wrapping. Wraps exception handlers in
app.exception_handlers(unhandled exceptions and status code 500). It replacesapp.exception_handlerswith a dict subclass that wraps handlers added afterinstall(). Exceptions are recorded, fingerprinted, and deduped with log records. This step is skipped whenexceptionsis disabled. -
Logging handler (
SharpitLogHandler). Attaches to non-propagating loggers (propagate=False, such as uvicorn loggers) and attaches to the root logger at lifespan startup. It collects log lines into a bounded buffer and capturesexc_inforecords for exceptions. This step is skipped when bothlog-tailandexceptionsare disabled. -
SQLAlchemy listeners. Attaches class-level listeners to SQLAlchemy
Engineif SQLAlchemy is imported. Anengine_connectlistener discovers active engines for connection pools and health checks. Statement listeners (before_cursor_execute,after_cursor_execute,handle_error) record SQL text, durations, and bind parameter lengths. Statements executed during internal sharpit checks set anINTERNALcontext variable and are ignored. This step is skipped whensql-traceis disabled.
If a panel is disabled (SHARPIT_PANELS_<ID>_ENABLED=false), its hooks are skipped.
A disabled panel adds zero overhead to your app.
Live streams (SSE) and worker processes¶
sharpit provides live Server-Sent Events (SSE) streams for activity, exceptions, SQL trace, and log tail.
- Stream lifetime.
Each SSE stream ends after
SHARPIT_STREAM_MAX_SECONDS(default 3 seconds) with aretry: 1000comment. The browser reconnects automatically. uvicorn reload and lifespan shutdown wait for open HTTP connections to finish. A short stream lifetime prevents live streams from blocking reload or shutdown. - Stream limit.
A global cap allows at most 20 concurrent streams across all panels.
If the cap is reached, new stream requests receive HTTP 503
{"error":"Too many concurrent sharpit streams"}. When a browser tab closes or disconnects, the stream counter decrements immediately. - Rotation in the UI. The UI client treats a 3-second server close as a normal rotation. It reconnects without displaying error banners or flickering.
- Multiple workers.
In-memory buffers live inside each worker process.
Under
uvicorn --workers N, each request, stream, and POST is handled by whichever worker accepts the connection. Generated authentication tokens are created per process. If you allow non-loopback access with multiple workers, configure a sharedSHARPIT_AUTHENTICATION_TOKEN.
The guard¶
Every request to /sharpit passes these checks in order. Any failure stops it with a 403 (or a 401 for a missing
token):
- Who is asking. Only loopback (
127.0.0.1,::1) by default. A request carrying a forwarding header (X-Forwarded-For,Forwarded,X-Real-IP) never counts as loopback. - Host header. It must be
localhost, a loopback address or a name inSHARPIT_ALLOWED_HOSTS. This blocks DNS rebinding, where a web page tricks your browser into calling your local server. - Cross-site. An
OriginorSec-Fetch-Sitefrom another site is rejected, for every method. On API paths the full origin (scheme, host and port) must match. - Token. A caller you have allowed from outside loopback (a container gateway, a proxy range or
SHARPIT_ALLOW_NON_LOCALHOST) must send the sharpit token:Authorization: Bearer …, or the session cookie the UI gets. The token is generated per process and printed at startup, unless you set one. - Panel access. Each panel can be disabled or made read-only (
SHARPIT_PANELS_<PANEL>_ENABLED,…_READ_ONLY). Writes such as setting a logger level, clearing traces, or dismissing rules are refused on a read-only panel.
The panels¶
| Panel | What it shows | How it gets it |
|---|---|---|
| Overview | App name, Python and package versions, activation reason, warnings | install() facts, the Python runtime and the host app |
| Configuration | Every setting with its value and where it came from (.env:12, env var, pydantic default, os.getenv in app.py:40) |
Reads env and .env files, walks pydantic-settings classes and scans your source for os.getenv calls (AST only, never executes it) |
| Mappings | Every route: method, path, handler module:function, including mounts and included routers |
Walks the router tree |
| Loggers | Every logger with its configured and effective level; you can change a level at runtime | The logging module; changes are in memory only |
| Dependency graph | Providers: your Depends(...) functions, lifespans and middleware, labelled app or framework |
Walks FastAPI's dependency tree |
| Vulnerabilities | Known advisories for every installed package, with severity, EPSS exploit probability and the fixed version | Queries OSV.dev and FIRST EPSS on demand; you can dismiss findings (kept in a local file) |
| HTTP Exchanges | Ring buffer of recent HTTP requests with duration, headers, status code, and client info | ASGI capture middleware |
| Exceptions | Unhandled errors grouped by exception class and innermost application frame, with status management | Middleware, wrapped exception handlers, and exc_info log records |
| SQL Trace | Executed SQL statements, execution durations, slow query alerts, and live SSE feed | SQLAlchemy cursor event listeners |
| Live Activity | Correlated timeline of requests, SQL queries, and exceptions, with N+1 query detection | ContextVar request correlation and event streams |
| Log Tail | Recent log records and live SSE feed with resumption token | SharpitLogHandler ring buffer (capped at 1 MiB) |
| Alembic | Read-only database migration status, current revision, and unapplied scripts | Alembic environment inspection (refreshed on POST alembic/refresh) |
| Database Pools | SQLAlchemy engine connection pool sizes, active checkouts, and overflow status | SQLAlchemy pool inspection |
| Health | Status of registered database engines and custom health checks | Database dialect ping and registered health callables (POST health/check) |
| Threads | Thread dump and running asyncio tasks with request correlation | threading.enumerate() and asyncio.all_tasks() |
| REST API Advisor | Static inspection of FastAPI route signatures for API design violations | Route analysis with severity scoring and rule dismissals |
| HTTP Probe | In-process HTTP request execution against local routes without external network calls | Direct ASGI call using in-process client |
| CLI | The same tools callable with curl (POST /sharpit/api/cli/tools/<tool>) |
engine/tools.py, shared with MCP |
Secrets and masking¶
Values that look secret are masked (******) across the UI, JSON API, live streams, MCP, and CLI.
- Text masking (
Masker.mask_text). Masks userinfo in URLs (https://user:pass@host). Masks value portions of sensitivekey=value,key: value, and"key": "value"patterns. Masks secret-shaped tokens identified by heuristic scanners. Replaces SQLAlchemy query parameter blocks[parameters: ...]with[parameters: ******]when parameter capture is disabled or exposure isMETADATA_ONLY. - Path masking (
Masker.mask_path). Replaces sensitive segments in URL paths with******. - Header filtering.
HTTP Probe allow-lists response headers (
content-type,content-length,location). HTTP Exchanges captures request and response headers with sensitive header names and values masked. - Always masked.
sharpit's own token is always masked, even when
SHARPIT_EXPOSE_VALUES=FULL.SHARPIT_EXPOSE_VALUEScan hide values entirely (METADATA_ONLY) or expose them (FULL, which logs a warning). Masking runs at capture time so raw secrets are not stored in memory buffers.
MCP¶
MCP is off until you set SHARPIT_MCP_ENABLED=true. It then serves JSON-RPC at /sharpit/api/mcp, behind the same guard.
Its 18 tools are read-only: get_overview, get_config, get_mappings, get_loggers, get_beans,
get_http_exchanges, get_log_tail, get_exceptions, get_exception_detail, get_sql_traces,
get_live_activity, get_alembic_migrations, get_health, get_database_connection_pools, get_threads,
get_vulnerabilities_report, get_rest_api_report, and get_rest_api_rule_violations.
Action tools (such as starting a vulnerability or REST API scan) can only be called from the CLI endpoint or the UI.
The tools never execute arbitrary code or evaluate expressions.
How it was tested¶
- pytest suite. Safety matrix over every guard rule and endpoint, BootUI JSON contract conformance tests, masking corpus tests, and no-side-effects tests on GET requests.
- UI: vitest unit tests and Playwright browser tests, with axe accessibility checks.
- CI on GitHub: Python 3.12 to 3.14 on Linux, Python 3.14 on Windows, the oldest supported dependency versions, a check that the committed UI bundle matches its source, and a smoke test of the built wheel.
- Parity: Follows BootUI's feature set and contract. Deliberate differences are documented in spec §17.
- Dogfood: Verified against sample and real-world applications.