Skip to content

feat(mcp): add MCP server — skeleton, free tools, and resources (PR-B Stage 1) - #155

Draft
wpfleger96 wants to merge 4 commits into
mainfrom
will/mcp-skeleton
Draft

feat(mcp): add MCP server — skeleton, free tools, and resources (PR-B Stage 1)#155
wpfleger96 wants to merge 4 commits into
mainfrom
will/mcp-skeleton

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Adds the MCP presentation layer for SNORE — a thin adapter over the async service layer, peer of the CLI and FastAPI. This is PR-B (Stage 1) of the two-PR MCP implementation; it merges after PR-A (substrate: will/import-time-analysis).

The implementation follows the v3.8 plan in docs/mcp-server-plan.md: tools are pure adapters (validate → open scope → call service → size-guard → map DTO to MCP schema), with no domain computation in mcp/**. Full Stage 2 (breath/epoch tools) and Stage 3 (vision + CA) land after PR-A merges and the seam-adoption rework completes.

  • src/snore/mcp/ package: FastMCP server, tool_error_boundary decorator, RESPONSE_SIZE_LIMIT guard, docs://tools + docs://schemas/{type} + docs://capabilities resources, clinical_profile presets (neutral, uars, osa, csa)
  • Phase 1 tools: get_data_overview, get_settings_timeline, get_nightly_summary (compliance fields; PR-A seam TODOs for RERA/FL aggregation, capability blocks, and pagination-invariant compliance), get_events (PR-A seam TODO for contextual waveform data)
  • snore mcp CLI entry: stdio transport only (--transport validated and passed through; HTTP is a post-PR-C relaxation of the validator, not a stub)
  • DB-access seam (_scope_provider) isolates session_scope() so PR-C swaps in an actor-scoped factory at one assignment site
  • Three-tier timestamp contract (A6): device wall-clock (start_time_wall_clock) + timezone_status + offset_seconds for in-session positions
  • Numeric arg bounds at the MCP boundary (page >= 1, page_size >= 1, compliance_threshold_hours >= 0, min_duration >= 0) via validation.py helpers; 13 rejection tests
  • just check green (mypy strict, ruff); just test 1231 passed; just web-check green

Related: will/import-time-analysis (PR-A — substrate, merges first)

npub17xpz0p704l6vlapga6nahzevr9h0kd9ggfzw640d9yevhmcgst2ql280uq and others added 4 commits August 2, 2026 16:09
Add src/snore/mcp/ as the third presentation layer over the async service
layer, alongside the CLI and FastAPI. Implements the v3.3 plan Stage 1:
skeleton, free tools, resources, and clinical profiles. Stage 2 (tuning
tools) and Stage 3 (vision/CA) will follow after PR-A (import-time analysis
+ Breath model) merges.

Package layout:
- server.py: FastMCP instance, lifespan (DatabaseTarget), tool_error_boundary,
  RESPONSE_SIZE_LIMIT, docs://tools + docs://schemas/{type} + docs://capabilities
- tools/overview.py: get_data_overview — cold-start orientation
- tools/settings.py: get_settings_timeline — RxTracker adapter
- tools/summary.py: get_nightly_summary — StatsService/DayService adapter
  with pagination, compliance fields, and analysis_not_run fallback (A2)
- tools/events.py: get_events — EventService adapter with inline context
- profiles.py: neutral/uars/osa/csa clinical presets (instructions only, G1)
- schemas.py: Pydantic response models + SCHEMA_MODEL_MAP for docs://schemas
- validation.py: ISO 8601 date parsing helpers
- errors.py: MCP-specific exception types

CLI: snore mcp [--db PATH] [--profile NAME] [--transport stdio]
pyproject.toml: add fastmcp>=3 to main dependencies

Tests: 61 new tests (44 unit + 17 integration), all passing.
Covers: validation, profiles, schemas, error boundary, size guard,
get_data_overview, get_settings_timeline, get_nightly_summary (with
compliance and null-reason), get_events (type/duration filters, context).

Compliant with v3.1 doctrine: G1 (profile shapes instructions only),
G2 (null + reason), G3 (no module-global state, session_scope per call),
G4 (no vendor branching in MCP). just check + just test green.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Four Thufir pass-1 items addressed:

A6 (timestamp contract): EventRow now emits offset-free ISO 8601
wall-clock (start_time_wall_clock) + timezone_status=unknown + tier-3
offset_seconds instead of the old start_time_iso with fabricated offset.
EventsResponse gains session_start_wall_clock anchor. schemas.py docstring
updated to document all three tiers.

M2 (DB-access honesty): tools now call _scope_provider(), never
session_scope() directly. _scope_provider is a module-level seam
(type: Callable[[], AbstractAsyncContextManager[AsyncSession]]) installed
by lifespan at startup. PR-C can swap in an actor-scoped factory at that
one assignment site without touching any tool. Lifespan teardown calls
cleanup_database() in finally and resets _scope_provider.

M4 (capabilities): docs://capabilities now calls register_all_parsers()
idempotently before querying the registry; channels/event-types derive
from DB rows (G2); parser list is supplementary context only. Cold-process
test added.

F5 (domain-computation boundary): compliance calc and event context
(pressure/leak at event, MV prior 120s) are marked with structured
TODO(PR-A seam) comments. Compliance block is explicitly flagged as
temporary pending BreathService.get_nightly_range_summary(). RERA/FL
analysis fields already read latest AnalysisResult by created_at via
row_number() window (no change needed; confirmed correct).

New tests: A6 timestamp contract (offset-free assertion), A6 non-UTC
determinism (TZ env var swap), M4 cold-process capabilities, M4
register_all idempotency. 1218 total passing; just check green.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Supersedes v3.3 plan with the final reviewed specification. v3.8 is the
implementation-released plan (Will's option-1 ruling, 2026-08-02T22:22Z)
incorporating four Thufir review passes and all accepted corrections:

- Universal terminal-payload durability (cancellation after import commit
  carries import_committed + import_result)
- primary_mode threaded end-to-end through AnalysisFacade, coordinator,
  API request models, and CLI
- Epoch contribution by data (analyzed_session_count > 0, nullable epoch
  algorithm_identity + NO_DATA_IN_RANGE)
- RawWaveformChannel.sample_count field
- Nested engine_versions_json {identity, run} shape (§14 note 5)

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Six items from Thufir's pass-1 MINOR sweep and minimalism score:

- Numeric arg bounds: extract validate_page_args/validate_compliance_threshold/
  validate_min_duration into validation.py (testable without FastMCP client);
  wire from server.py; 13 new unit tests in test_mcp_server.py cover
  zero/negative rejection and page_size cap at 90.
- Non-UTC test: add time.tzset() call (POSIX) after os.environ[TZ] change so
  the process timezone actually shifts; restore tzset() in finally block.
  Comment updated to reflect what the test now actually proves.
- docs/tools.md: replace stale start_time_iso with start_time_wall_clock,
  timezone_status, offset_seconds (matches EventRow schema).
- Transport honesty: pass validated --transport to server.run(transport=...)
  instead of hardcoding 'stdio'; type: ignore[arg-type] since the validator
  above already gates to literal values.
- Minimalism: delete unused _active_profile module-level variable and its
  set/clear in lifespan; delete the executed-then-discarded analysis_count_result
  query (overview.py:113-125) and its _ = ... suppressor.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
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