feat(mcp): add MCP server — skeleton, free tools, and resources (PR-B Stage 1) - #155
Draft
wpfleger96 wants to merge 4 commits into
Draft
feat(mcp): add MCP server — skeleton, free tools, and resources (PR-B Stage 1)#155wpfleger96 wants to merge 4 commits into
wpfleger96 wants to merge 4 commits into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 inmcp/**. 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:FastMCPserver,tool_error_boundarydecorator,RESPONSE_SIZE_LIMITguard,docs://tools+docs://schemas/{type}+docs://capabilitiesresources,clinical_profilepresets (neutral,uars,osa,csa)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 mcpCLI entry: stdio transport only (--transportvalidated and passed through; HTTP is a post-PR-C relaxation of the validator, not a stub)_scope_provider) isolatessession_scope()so PR-C swaps in an actor-scoped factory at one assignment sitestart_time_wall_clock) +timezone_status+offset_secondsfor in-session positionspage >= 1,page_size >= 1,compliance_threshold_hours >= 0,min_duration >= 0) viavalidation.pyhelpers; 13 rejection testsjust checkgreen (mypy strict, ruff);just test1231 passed;just web-checkgreenRelated: will/import-time-analysis (PR-A — substrate, merges first)