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
@@ -0,0 +1,71 @@
"""Bounded nearest-model queries on the already authorized project connection."""
from math import asin, cos, degrees, radians, sin
from typing import Any
from psycopg import AsyncConnection, sql
from app.infra.db.postgresql.read_queries import ELEMENT_VIEWS, read_rows
_MODEL_RELATIONS = {
kind: sql.Identifier("gis", view) for kind, view in ELEMENT_VIEWS.items()
}
def _search_boxes(
longitude: float, latitude: float, radius_m: float,
) -> list[tuple[float, float, float, float]]:
# A deliberately smaller-than-WGS84 curvature radius gives a conservative
# geographic envelope. It is only a GiST candidate filter; geography below
# determines the actual radius and distances. Split at the antimeridian.
angle = (radius_m + 10) / 6_300_000
low, high = latitude - degrees(angle), latitude + degrees(angle)
if low < -85 or high > 85:
# Avoid Mercator's polar singularity; retain exact geography filtering.
return []
width = degrees(asin(min(1.0, sin(angle) / cos(radians(latitude)))))
left, right = longitude - width, longitude + width
if left < -180:
return [(-180, low, right, high), (left + 360, low, 180, high)]
if right > 180:
return [(left, low, 180, high), (-180, low, right - 360, high)]
return [(left, low, right, high)]
async def nearby_elements(
conn: AsyncConnection,
element_type: str,
*,
center: tuple[float, float],
radius_m: float,
limit: int,
offset: int,
) -> list[dict[str, Any]]:
"""Rank complete model geometries by spheroidal distance, never endpoints."""
relation = _MODEL_RELATIONS[element_type]
filters = []
parameters: list[Any] = [*center]
for box in _search_boxes(*center, radius_m):
filters.append(sql.SQL(
"v.geom && ST_Transform(ST_MakeEnvelope(%s,%s,%s,%s,4326),3857)"
))
parameters.extend(box)
candidate_filter = sql.SQL(" OR ").join(filters) if filters else sql.SQL("TRUE")
statement = sql.SQL("""
WITH target AS (
SELECT ST_SetSRID(ST_MakePoint(%s,%s),4326)::geography AS point
), candidates AS MATERIALIZED (
SELECT v.id, to_jsonb(v)-'geom' AS properties,
ST_Transform(v.geom,4326)::geography AS shape
FROM {} v
WHERE v.geom IS NOT NULL AND NOT ST_IsEmpty(v.geom) AND ({})
)
SELECT c.id AS element_id, c.properties,
ST_Distance(c.shape,t.point,true) AS distance_m
FROM candidates c CROSS JOIN target t
WHERE ST_DWithin(c.shape,t.point,%s,true)
ORDER BY distance_m,c.id
LIMIT %s OFFSET %s
""").format(relation, candidate_filter)
parameters.extend((radius_m, limit, offset))
return await read_rows(conn, statement, tuple(parameters))