feat(server): integrate MCP queries and investigation messaging

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.
This commit is contained in:
2026-10-19 18:46:58 +08:00
parent 2b6fc5aa52
commit be6317eb48
117 changed files with 12703 additions and 638 deletions
+76
View File
@@ -0,0 +1,76 @@
"""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)