Skip to content

Add read-only nautilus_sources MCP tool for source discovery parity with REST /v1/sources #124

Description

@se-jo-ma

Context

MCP is a first-class transport but is currently request-only. create_server registers only nautilus_request (mcp_server.py:232) plus the optional nautilus_declare_handoff (mcp_server.py:259, gated on config.mcp.expose_declare_handoff). MCP agents therefore have no discovery surface: to learn which sources/classifications exist they must blindly issue an intent and read back sources_queried/sources_denied.

REST already solves this with GET /v1/sources (fastapi_app.py:300-319), which returns a metadata-only projection (id, type, description, classification, data_types) over broker.sources and explicitly never exposes the DSN or credentials (AC-12.3). Note that SourceConfig does carry secrets — connection (post-interpolation DSN, config/models.py:109) and auth (config/models.py:121) — so the projection's field whitelist is security-relevant, not cosmetic. The projection is currently an inline dict comprehension in the REST handler, so a second copy in MCP would risk drift (e.g. a future field added to one transport but not the other, or a secret accidentally leaked from one).

Task

Add a read-only nautilus_sources MCP tool mirroring GET /v1/sources, and factor the metadata projection into one shared helper used by both transports.

  • Extract the projection logic (the id/type/description/classification/data_types dict built over broker.sources) from get_sources in fastapi_app.py:308-318 into a shared helper, and have both the REST handler and the new MCP tool call it.
  • Register nautilus_sources in create_server alongside nautilus_request; it returns the same metadata-only projection and never exposes connection (DSN) or auth/credentials.
  • Match the existing transport auth posture rather than inventing per-tool auth: stdio MCP is intentionally un-gated (mcp_server.py:38-40); HTTP is gated at the ASGI-wrapper layer via wrap_http_with_api_key/http_app (mcp_server.py:126, 289). A read-only tool inherits whatever transport-level gate is active — no extra per-tool check is needed (and would be inconsistent with nautilus_request).
  • Handle the broker is None / empty-sources case the same way the REST handler does ({"sources": []}).

Where

  • nautilus/transport/mcp_server.py — register nautilus_sources in create_server
  • nautilus/transport/fastapi_app.pyget_sources (lines 300-319); extract the shared projection helper here or in a shared module
  • nautilus/core/broker.py:808-811Broker.sources property (list[SourceConfig])
  • nautilus/config/models.py:78-127SourceConfig (note connection/auth must stay out of the projection)
  • Tests: tests/unit/transport/test_mcp_unit.py, tests/unit/transport/test_mcp_server_smoke.py, tests/integration/test_mcp_e2e.py

Acceptance criteria

  • An MCP client can call nautilus_sources and enumerate sources with id/type/description/classification/data_types.
  • The response never contains connection (DSN), auth, or any other credential field.
  • The projection emitted by nautilus_sources is byte-for-byte identical to GET /v1/sources, produced by a single shared helper (no duplicated dict literal).
  • nautilus_sources is registered unconditionally alongside nautilus_request (not behind the expose_declare_handoff gate).
  • Empty/uninitialized broker returns an empty sources list rather than erroring, matching REST behavior.
  • A unit test asserts no secret fields appear in the MCP tool output; uv run pytest -m unit passes.

Size: S

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium priority: v2 epic or non-blocking gaparea/coreBroker core, CLIPS routing, session stateenhancementNew feature or requestsize/S<half-day: small focused change

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions