Add project-bound OAuth MCP queries and controlled analysis with scoped evidence, limits, timeouts and a nearby-model spatial query using EPSG:4326 inputs. Default nearby searches to 200 meters and five results. Integrate investigation messaging/presentation, health/readiness and typed history; remove duplicate web search and fix online simulation and compressed baseline writes. Include project migrations 023-025; keep local credentials and demo seeding excluded. Validation: 319 focused pytest checks; real PostGIS temporary-table regression; read-only Jinqian bridge radius comparison; cross-language MCP discovery/calls.
77 lines
3.1 KiB
Python
77 lines
3.1 KiB
Python
"""Resolve authoritative metadata and project routing for each MCP request."""
|
|
|
|
from contextlib import asynccontextmanager
|
|
from dataclasses import dataclass
|
|
from typing import Any, AsyncIterator
|
|
from psycopg import AsyncConnection
|
|
from app.auth.project_dependencies import ProjectContext
|
|
from app.infra.db.project_routing import ActiveProjectRouting
|
|
from fastapi import HTTPException
|
|
from mcp.server.auth.provider import AccessToken
|
|
|
|
from app.auth.metadata_dependencies import get_current_metadata_user
|
|
from app.auth.project_dependencies import (
|
|
resolve_project_context,
|
|
resolve_project_business_routing,
|
|
resolve_project_simulation_routing,
|
|
get_project_pg_connection,
|
|
get_project_timescale_connection,
|
|
)
|
|
from app.infra.db.metadb.database import get_metadata_session
|
|
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
|
from app.infra.db.project_routing import activate_project_routing
|
|
|
|
|
|
class QueryMetadataRepository(MetadataRepository):
|
|
async def get_project_db_routing(self, project_id, role):
|
|
routing = await super().get_project_db_routing(project_id, role)
|
|
# Existing connection generators obtain routing before awaiting a pool.
|
|
# End that implicit SELECT transaction here, before the network wait.
|
|
await self.session.commit()
|
|
return routing
|
|
|
|
|
|
@dataclass
|
|
class QueryScope:
|
|
project: ProjectContext
|
|
user: Any
|
|
connection: AsyncConnection
|
|
routing: ActiveProjectRouting
|
|
metadata: QueryMetadataRepository
|
|
|
|
@asynccontextmanager
|
|
async def simulation_routing(self) -> AsyncIterator[ActiveProjectRouting]:
|
|
"""Resolve both databases before a native calculation can modify its model."""
|
|
routing = await resolve_project_simulation_routing(self.project, self.metadata)
|
|
await self.metadata.session.commit()
|
|
with activate_project_routing(routing):
|
|
yield routing
|
|
|
|
@asynccontextmanager
|
|
async def timescale(self):
|
|
# Metadata repository here is used only to resolve a known project DB.
|
|
async with asynccontextmanager(get_project_timescale_connection)(
|
|
self.project, self.metadata
|
|
) as conn:
|
|
await self.metadata.session.commit()
|
|
yield conn
|
|
|
|
|
|
@asynccontextmanager
|
|
async def project_scope(principal: AccessToken, project_id: str):
|
|
if not principal.claims:
|
|
raise HTTPException(401, "MCP identity is missing")
|
|
async with asynccontextmanager(get_metadata_session)() as session:
|
|
metadata = QueryMetadataRepository(session)
|
|
user = await get_current_metadata_user(principal.claims, metadata)
|
|
project = await resolve_project_context(project_id, user, metadata)
|
|
routing = await resolve_project_business_routing(project, metadata)
|
|
# End the metadata read before any external query/IO waits.
|
|
await session.commit()
|
|
async with asynccontextmanager(get_project_pg_connection)(
|
|
project, metadata
|
|
) as conn:
|
|
await session.commit()
|
|
with activate_project_routing(routing):
|
|
yield QueryScope(project, user, conn, routing, metadata)
|