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
+91
View File
@@ -0,0 +1,91 @@
"""Persistent MCP context and receipt storage in the selected project's BizDB."""
from datetime import datetime, timezone
from psycopg import AsyncConnection
from psycopg.types.json import Jsonb
from app.mcp.models import BoundContext
class McpQueryRepository:
def __init__(self, conn: AsyncConnection, project_id):
self.conn, self.project_id = conn, project_id
async def create(self, context: BoundContext):
async with self.conn.transaction(), self.conn.cursor() as cur:
# Bounded maintenance; expired/revoked handles remain invalid even if cleanup fails.
await cur.execute(
"""DELETE FROM workbench.mcp_query_contexts WHERE context_handle IN
(SELECT context_handle FROM workbench.mcp_query_contexts
WHERE project_id=%s AND (expires_at < now() OR revoked_at IS NOT NULL)
ORDER BY expires_at LIMIT 100 FOR UPDATE SKIP LOCKED)""",
(self.project_id,),
)
await cur.execute(
"""INSERT INTO workbench.mcp_query_contexts
(context_handle,project_id,subject,client_id,document,expires_at)
VALUES (%s,%s,%s,%s,%s,%s)""",
(
context.context_handle,
self.project_id,
context.subject,
context.client_id,
Jsonb(context.model_dump(mode="json")),
context.expires_at,
),
)
async def get(self, handle: str) -> BoundContext | None:
async with self.conn.transaction(), self.conn.cursor() as cur:
await cur.execute(
"""SELECT document,revoked_at FROM workbench.mcp_query_contexts
WHERE context_handle=%s AND project_id=%s""",
(handle, self.project_id),
)
row = await cur.fetchone()
if not row:
return None
document = dict(row["document"])
document["revoked_at"] = row["revoked_at"]
return BoundContext.model_validate(document)
async def revoke(self, handle: str, subject: str, client_id: str):
async with self.conn.transaction(), self.conn.cursor() as cur:
await cur.execute(
"""UPDATE workbench.mcp_query_contexts SET revoked_at=COALESCE(revoked_at,%s)
WHERE context_handle=%s AND project_id=%s AND subject=%s AND client_id=%s""",
(
datetime.now(timezone.utc),
handle,
self.project_id,
subject,
client_id,
),
)
async def receipt(self, scope_key: str, call_id: str) -> dict | None:
async with self.conn.transaction(), self.conn.cursor() as cur:
await cur.execute(
"""SELECT parameters,result FROM workbench.mcp_query_receipts
WHERE project_id=%s AND scope_key=%s AND call_id=%s""",
(self.project_id, scope_key, call_id),
)
return await cur.fetchone()
async def save_receipt(
self, scope_key: str, call_id: str, parameters: dict, result: dict
):
# Caller owns the short evidence + receipt transaction.
async with self.conn.cursor() as cur:
await cur.execute(
"""INSERT INTO workbench.mcp_query_receipts
(project_id,scope_key,call_id,parameters,result) VALUES (%s,%s,%s,%s,%s)
ON CONFLICT (project_id,scope_key,call_id) DO NOTHING""",
(self.project_id, scope_key, call_id, Jsonb(parameters), Jsonb(result)),
)
async def lock_receipt(self, scope_key: str, call_id: str):
async with self.conn.cursor() as cur:
await cur.execute(
"SELECT pg_advisory_xact_lock(hashtextextended(%s,0))",
(f"mcp:{self.project_id}:{scope_key}:{call_id}",),
)