feat: add MCP endpoint with get_schedule tool #455
No reviewers
Labels
No labels
domain:backend
domain:devops
domain:frontend
status:approved
status:in-progress
status:needs-fix
status:qa
type:bug
type:devops
type:feature
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
ldraney/basketball-api!455
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "454-mcp-hello-world"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Adds an MCP server mounted at
/mcpon the FastAPI app using the streamable HTTP transport protocol. Exposes aget_scheduletool that returns practices and events from the database, enabling AI assistant integration (e.g., claude.ai Connectors).Changes
pyproject.toml— addedmcp>=1.0dependencysrc/basketball_api/mcp_server.py— new file: MCP server definition withget_scheduletool andcreate_mcp_app()factory for session manager lifecyclesrc/basketball_api/main.py— importscreate_mcp_app, initializes MCP session manager in lifespan, mounts ASGI app at/mcptests/test_mcp.py— 8 tests: server init, MCP protocol (initialize, tools/list, tools/call with empty and seeded data), existing endpoints unaffectedTest Plan
pytest tests/test_mcp.py -v— all 8 tests passpytest tests/test_mcp.py tests/test_health.py tests/test_public.py -v— 33 tests pass, existing endpoints unaffectedhttps://basketball-api.tail5b443a.ts.net/mcp/and ask "What's the schedule?"Review Checklist
mcppackage added topyproject.tomldependencies/mcpon the FastAPI appget_scheduletool returns upcoming schedule entries from the databasepytest tests/ -k test_mcp)ruff checkandruff formatcleanRelated Notes
story-westside-basketball-ai-assistant(stage 1 of 4)Related
Closes #454
QA Review -- PR #455
Scope Check
454-mcp-hello-worldfollows conventionPositive
create_mcp_app()factory pattern correctly handles theStreamableHTTPSessionManager.run()single-use constraint by creating fresh instances per lifespan cycleSessionLocal()with try/finally/close -- correct since MCP tools run outside FastAPI dependency injectionstateless_http=Trueandjson_response=Trueare appropriate for hello-world scopewith TestClient(app) as clientto trigger lifespan -- required for MCP session manager initializationNits (non-blocking)
test_mcp_tool_registeredis a duplicate oftest_mcp_server_imports_without_error-- both just assertmcp is not None. Could verify the tool is actually in the registry._mcp_requestposts to/mcpwhich 307-redirects to/mcp/. Posting to/mcp/directly would skip the redirect hop.mcp._mcp_serveris private attribute access increate_mcp_app(). Fragile if SDK internals change, but no public API exists for this. Acceptable for hello-world.Acceptance Criteria
mcppackage added topyproject.toml/mcpget_scheduletool returns schedule entries from DBVERDICT: APPROVED
All acceptance criteria met. Nits are non-blocking and can be addressed in follow-up tickets.
PR #455 Review
DOMAIN REVIEW
Stack: Python / FastAPI / SQLAlchemy / MCP SDK
Architecture -- double
create_mcp_app()callThe module-level call
_, mcp_asgi_app = create_mcp_app()creates a session manager that is immediately discarded (never started viasm.run()). Then the lifespan creates a second session manager and monkey-patches it onto the first ASGI app viamcp_asgi_app.session_manager = sm. This works but is fragile -- it relies onStreamableHTTPASGIAppallowing post-construction session manager replacement. The comment explains the rationale well, but this is a pattern that could break on MCP SDK upgrades.Session management --
SessionLocal()bypassmcp_server.py:get_schedule()callsSessionLocal()directly instead of usingget_db(). Every other endpoint in basketball-api uses FastAPI'sDepends(get_db)pattern. The MCP tool operates outside FastAPI's dependency injection, soDependscannot be used directly, but this creates:get_dbdo not affect the MCP tool's DB accessIn practice, tests pass because both
SessionLocal()and the test engine connect to the samebasketball_testdatabase. The data seeded by thedbfixture is committed (db.commit()) before the MCP tool runs, soSessionLocal()sees it. This is correct but coincidental -- if tests ever switch to rollback-based isolation, the MCP tool will silently return stale data.Recommendation: Accept for now since the data is read-only and publicly available. File a follow-up ticket to refactor the MCP tool to accept a session factory or use a shared helper, aligning with the rest of the codebase.
Auth -- no authentication on
/mcpThe
/mcpendpoint has no authentication. This is acceptable because:get_schedule) is identical to/public/schedulewhich is also unauthenticatedstateless_http=Trueandstateless=Truesettings are correct for a public toolIf future MCP tools expose protected data, auth must be added at that point. This should be documented as a constraint.
MCP protocol correctness
stateless_http=TrueonFastMCPandstateless=TrueonStreamableHTTPSessionManagerare consistentjson_response=Trueis appropriate for claude.ai Connector integrationBLOCKERS
None. The new functionality has 8 tests covering: server init, MCP protocol handshake, tool listing, empty schedule, seeded schedule, and regression checks on existing endpoints.
NITS
SessionLocal()direct usage (file:src/basketball_api/mcp_server.py, line ~63): As discussed above, this diverges from theget_db()pattern used everywhere else. Not a blocker for read-only public data, but should be a follow-up ticket.Hardcoded
tenant_id == 1(file:src/basketball_api/mcp_server.py, lines ~71, ~78): This matches the existing pattern inroutes/public.py:192-210(also hardcoded to tenant 1), so it is consistent. Just noting the inherited tech debt._DAY_NAMESdict vs list: A tuple("Monday", "Tuesday", ...)indexed by int would be simpler than a dict keyed 0-6, but this is purely cosmetic.test_mcp_tool_registeredis a no-op: The test body only assertsmcp is not None, which the previous test already covers. It does not actually verifyget_scheduleis registered (e.g., by inspectingmcp._mcp_serverinternals). Low value.Missing
joinedloadon public endpoint: The MCP tool addsjoinedload(PracticeSchedule.team)which the REST/public/scheduleendpoint does not use. This is actually better (avoids N+1), but creates a minor inconsistency between the two code paths returning the same data.SOP COMPLIANCE
454-mcp-hello-worldmatches issue #454story-westside-basketball-ai-assistantreferenced (no plan slug -- parent story is sufficient)PROCESS OBSERVATIONS
SessionLocal()pattern should be tracked as tech debt before more tools are added. A second tool using direct sessions doubles the divergence.VERDICT: APPROVED