Skip to content

feat(mcp_servers): admin UI for managing MCP servers#1256

Open
srtab wants to merge 42 commits into
mainfrom
feat/mcp-ui-config
Open

feat(mcp_servers): admin UI for managing MCP servers#1256
srtab wants to merge 42 commits into
mainfrom
feat/mcp-ui-config

Conversation

@srtab

@srtab srtab commented May 25, 2026

Copy link
Copy Markdown
Owner

Summary

Introduces a new mcp_servers Django app that lets admins manage MCP servers through the dashboard (create / edit / delete / toggle enable / test connection / discover tools), and wires the agent's MCP registry to read from the DB instead of .mcp.json.

  • New MCPServer model with encrypted-header storage, source (builtin/custom), transport, env-var-ref headers, and a tool-filter (allow/block) — all enforced at the DB level via TextChoices and a CheckConstraint
  • mcp_servers.services.build_runtime_servers() is the single adapter that converts DB rows into the UserMcpServer DTO the registry consumes; MCPRegistry is now a pure function over DTOs
  • MCPToolkit.get_tools iterates per server so a single bad endpoint cannot blank tools from healthy peers
  • Migration 0002 imports legacy JSON config (MCP_SERVERS_CONFIG_FILE) into the DB; built-ins are upserted via post_migrate; a one-shot startup warning fires when the deprecated env var is still set
  • Admin-only enforcement on every endpoint (AdminRequiredMixin), preserve-blank header semantics matching sandbox_envs, and a 60s tool-discovery cache keyed on modified
  • New list-view "Health" badge surfaces enabled servers that would silently be skipped at runtime (undecryptable headers, missing env-var refs)
  • Edit form for built-ins renders non-editable fields read-only and only honors the enabled flip server-side

Test plan

  • make test — full unit suite (2530 passing)
  • make lint-fix — clean
  • make lint-typing — no new errors vs main
  • Apply migrations against a fresh DB and verify built-in upsert + legacy JSON import
  • Apply migrations against an existing DB with MCP_SERVERS_CONFIG_FILE set and verify the one-shot deprecation warning
  • Create a custom server through the UI, test connection, discover tools, save a tool filter
  • Edit the server; confirm headers preserve-blank works (blank literal = "keep existing")
  • Disable a built-in and confirm the agent stops loading its tools
  • Trigger decryption failure (corrupt _headers_encrypted) and confirm:
    • List view shows "Broken: headers cannot be decrypted"
    • Detail / edit / tools pages do not 500
    • Edit POST is refused without overwriting the ciphertext
  • Set a ${VAR} header to an unset env var and confirm the list view shows "Broken: missing env var(s): VAR"

srtab added 28 commits June 30, 2026 23:37
Move `import os` to module top; wrap per-row header read in
`try/except DecryptionError` so a corrupted row is skipped with an
error log while other servers still load. Add three tests covering
env_ref happy path, missing env var drop, and decryption-error skip.
Wire MCPToolkit.get_tools() to call build_runtime_servers() via
sync_to_async and pass the result into mcp_registry.get_connections_and_filters(),
so DB-configured MCP servers are included in every tool fetch.
On app ready(), upsert_builtin_rows() creates a MCPServer row (source=BUILTIN,
enabled=True) for each registered built-in class if one doesn't exist yet,
preserving admin's enabled choice on subsequent boots. The base MCPServer class
now delegates is_enabled() to a DB-backed _db_enabled() classmethod; SentryMCPServer
and Context7MCPServer AND their URL check with super().is_enabled() to keep both
gates in effect.
Call MCPServerDetailView._tools_or_empty on the edit GET path so the
tool_filter_items field swaps from a textarea to checkboxes when tools
are discoverable; updates form.html to render fields individually so
the widget change is visible.
Querying the DB during AppConfig.ready() raised RuntimeWarning and broke
in-memory test DB setup (every transactional async test failed with
'no such table: accounts_user'). post_migrate is the Django-recommended
hook for 'ensure rows exist after migrations'.
Build ToolFilter DTO from DB row in build_runtime_servers so the registry
filter is no longer silently None. Pass discovered_tools into MCPServerForm
on POST so tool_filter_items uses MultipleChoiceField, preventing checkbox
selections from collapsing to the last value.
Address PR review findings around silent failures and data loss:

- Refuse to POST edits when stored headers are undecryptable; the
  formset would otherwise overwrite recoverable ciphertext with [].
- Iterate per server in MCPToolkit.get_tools so one bad endpoint
  cannot blank tools from healthy peers; also fix a latent
  SynchronousOnlyOperation by wrapping get_connections_and_filters
  in sync_to_async.
- Only classify legacy headers as env_ref when the entire value is
  ${VAR}; preserve mixed strings like "Bearer ${TOKEN}" as literal.
- Render tool-filter as a textarea when discovery returns nothing,
  so a transient handshake failure cannot wipe the persisted filter.
- Only swallow ProgrammingError in _db_enabled; let OperationalError
  propagate so a DB outage cannot silently re-enable built-ins.
- test_connection logs via logger.exception and prefixes the
  exception class name when str(err) is empty.
- New server_health() and "Broken: reason" badge in the list view
  flag enabled servers that would silently be skipped at runtime.
- CheckConstraint forbids empty tool_filter_items when the filter
  mode is set.
- Built-in edit form renders non-editable fields read-only; Save
  is disabled when headers cannot be decrypted.

Tests cover the new error paths, the cache-bust on modify, the
per-server toolkit isolation, the form fallback to textarea, and
the new CheckConstraint.
@srtab srtab force-pushed the feat/mcp-ui-config branch from a3ec551 to 503dc6b Compare June 30, 2026 22:46
Address PR review feedback on the MCP servers admin UI:

- Tool discovery: degrade to an empty list on DecryptionError (key
  rotation) in every entry point instead of 500-ing the tools endpoint;
  skip discovery for built-ins (their builtin:// URL can never handshake);
  negative-cache empty results with a short TTL so a broken server is not
  re-probed on every render nor pinned empty for the full TTL.
- Migration 0002: normalize a non-"none" tool-filter mode with an empty
  item list to "none" so legacy import can't create a row that the 0003
  check constraint would reject and abort the migration.
- Admin UI: rebuild the list/detail/form/delete templates on the design
  system (card rows, btn-* and status-badge component classes); add the
  MCP servers entry to the admin sidebar and nav section map.
- Models/registry: add MCPServer.is_builtin(); share one header-resolver
  between the runtime and test-connection paths (consistent warnings,
  fail-loud on an unknown header mode); skip and warn on an unknown
  transport in the registry; reject reserved names that shadow URL routes.
- Fix inaccurate docstrings and add tests covering the discovery
  degradation, built-in skip, negative caching, migration normalization,
  the end-to-end tool filter, and _build_client transport mapping.
srtab added 13 commits July 1, 2026 21:22
Replace the static header table with an Alpine-driven formset editor that
lets admins add and remove HTTP header rows in the create/edit forms.

- Extract the row markup into a reusable _header_row.html partial and wire
  an empty-form template plus an 'Add header' control.
- Extend the shared django-formset.js to keep server-rendered rows (marked
  via -id or data-initial) in the POST so their index slots survive removal.
- Override MCPServerHeaderForm.has_changed so a trailing blank extra row is
  skipped instead of failing clean_name, while blank initial rows are still
  rejected.
- Blank the stored literal value on edit GET so secrets are never echoed,
  and preserve the stored value when a blank literal is re-submitted.
Final whole-plan review fixes for the MCP built-in->remote migration:
correct the false CHANGELOG upgrade note for v2.0.0-or-earlier upgrades,
drop the stale built-in empty-state copy in the tools table template,
fix the inverted is_builtin() docstring, mark MCP_SERVERS_CONFIG_FILE
deprecated in the env-vars reference, update two e2e test docstrings
to the real toolkit chain, and note the migration 0004 coupling on the
deprecated Sentry/Context7 URL settings.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant