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
+101
View File
@@ -0,0 +1,101 @@
"""Owned loopback HTTP fixture for cross-language SDK smoke tests. No database/auth service."""
import asyncio
import base64
import json
import socket
import sys
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from pathlib import Path
from types import SimpleNamespace
from uuid import UUID
import uvicorn
from fastapi import FastAPI
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "unit"))
from test_mcp_query import Repository, Connection, Verifier, PERMISSIONS
from app.mcp import application, contexts, query_service
from app.mcp.application import McpQueryApplication
from app.mcp.config import McpSettings
from app.mcp.dispatch import McpDispatchMiddleware
from app.mcp.models import QueryResult
application.McpQueryRepository = contexts.McpQueryRepository = Repository
contexts.permissions_for_context = query_service.permissions_for_context = (
lambda project: PERMISSIONS
)
@asynccontextmanager
async def query_scope(identity, project):
yield SimpleNamespace(
project=SimpleNamespace(project_id=UUID(project)), connection=Connection()
)
async def execute(scope, binding, tool, args):
return QueryResult(
source=tool.name,
source_ref="fixture:query",
queried_at=datetime.now(timezone.utc),
semantics=tool.semantics,
data=[{"id": "P-1"}],
)
async def main():
sock = socket.socket()
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
settings = McpSettings(
enabled=True,
issuer_uri="http://localhost/realms/test",
resource_uri=f"http://127.0.0.1:{port}/mcp",
_env_file=None,
)
class JwtFixtureVerifier(Verifier):
async def verify_token(self, token):
try:
payload = json.loads(
base64.urlsafe_b64decode(token.split(".")[1] + "==")
)
value = self.identities.get(payload.get("fixture"))
if value is not None:
value.subject = payload["sub"]
value.claims = {"sub": value.subject}
return value
except (ValueError, IndexError, KeyError):
return None
verifier = JwtFixtureVerifier()
for identity in verifier.identities.values():
identity.resource = settings.resource_uri
runtime = McpQueryApplication(
settings, verifier=verifier, scope_factory=query_scope, executor=execute
)
@asynccontextmanager
async def lifespan(app):
async with runtime.lifespan():
yield
app = FastAPI(lifespan=lifespan)
app.state.mcp_runtime = runtime
app.include_router(runtime.router, prefix="/api/v1")
app.add_middleware(McpDispatchMiddleware, runtime=runtime)
server = uvicorn.Server(uvicorn.Config(app, log_level="error", access_log=False))
serving = asyncio.create_task(server.serve(sockets=[sock]))
while not server.started:
if serving.done():
await serving
raise RuntimeError("MCP fixture exited before listening")
await asyncio.sleep(0.01)
print(f"http://127.0.0.1:{port}/mcp", flush=True)
await serving
if __name__ == "__main__":
asyncio.run(main())