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.
102 lines
3.2 KiB
Python
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())
|