Files
jiang be6317eb48 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.
2026-10-19 18:46:58 +08:00

102 lines
3.2 KiB
Python

"""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())