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
+132
View File
@@ -0,0 +1,132 @@
"""Read existing project-owned peripheral facts without forwarding MCP tokens."""
from datetime import datetime, timezone
from uuid import uuid4
from fastapi import HTTPException
from app.infra.db.postgresql.read_queries import MAX_QUERY_BYTES, read_rows, scada_devices, series_rows
from app.domain.schemas.operational_data import TaskDataSnapshot, TaskDataQuery
from app.services.content_digest import digest
from samples.peripheral.store import PostgresStore
from app.infra.integrations.operational_data import _select_demo_observations
async def read_peripheral_query(scope, args):
conn = scope.connection
project = str(scope.project.project_id)
truncated = False
origin = "system_data"
if args.source in {"peripheral_work_orders", "peripheral_test_data"}:
table, column = (
("work_orders", "work_order_id")
if args.source == "peripheral_work_orders"
else ("test_data_records", "record_id")
)
rows = await read_rows(
conn,
"SELECT document FROM peripheral_sample."
+ table
+ " WHERE project_id=%s AND deleted_at IS NULL ORDER BY "
+ column
+ " LIMIT %s OFFSET %s",
(project, args.limit + 1, args.offset),
)
items = [
PostgresStore._active_managed_document(row["document"])
for row in rows[: args.limit]
]
truncated = len(rows) > args.limit
if any(item.get("project_id") != project for item in items):
raise HTTPException(502, "Peripheral project mismatch")
data = {
"items": items,
"truncated": truncated,
"limit": args.limit,
"offset": args.offset,
}
origin = (
"synthetic"
if items and any(item.get("origin") == "synthetic" for item in items)
else "system_data"
)
elif args.source == "peripheral_points":
rows = await scada_devices(conn, limit=args.limit + 1, offset=args.offset)
data = {
"origin": "historical_replay",
"items": [
{
"point_id": row["device_id"],
"label": row["device_id"] + " · " + row["device_type"],
"unit": row["measurement_unit"],
"status": "unknown",
"origin": "historical_replay",
}
for row in rows[: args.limit]
],
"truncated": len(rows) > args.limit,
}
truncated = data["truncated"]
elif args.point_id.startswith("DEMO-"):
rows = await read_rows(
conn,
"SELECT CASE WHEN octet_length(document::text)<=%s THEN document END AS document,"
"digest,octet_length(document::text)>%s AS too_large "
"FROM peripheral_sample.demo_datasets WHERE project_id=%s AND dataset_id='water-demo-v1'",
(MAX_QUERY_BYTES, MAX_QUERY_BYTES, project),
)
if rows and rows[0]["too_large"]:
raise HTTPException(413, "Synthetic dataset exceeds the integrity-check byte budget")
if not rows or digest(rows[0]["document"]) != rows[0]["digest"]:
raise HTTPException(503, "Synthetic dataset is unavailable or invalid")
query = TaskDataQuery(
source=args.source, point_id=args.point_id, start=args.start, end=args.end
)
data = _select_demo_observations(
{**rows[0]["document"], "project_id": project}, query, project
)
items = data["items"]
truncated = len(items) > args.limit + args.offset
data["items"] = items[args.offset : args.offset + args.limit]
data["truncated"] = truncated
origin = "synthetic"
else:
devices = await scada_devices(conn, device_id=args.point_id, limit=1)
if not devices:
raise HTTPException(404, "Peripheral point was not found")
device = devices[0]
if not device.get("api_query_id"):
raise HTTPException(503, "Peripheral history mapping is unavailable")
async with scope.timescale() as ts:
rows = await series_rows(
ts,
source="scada",
element_type="junction",
ids=[device["api_query_id"]],
start=args.start,
end=args.end,
field="monitored_value",
limit=args.limit + 1,
offset=args.offset,
end_exclusive=True,
)
truncated = len(rows) > args.limit
data = {
"origin": "historical_replay",
"items": [
{
"point_id": args.point_id,
"observed_at": row["time"].isoformat(),
"value": row["value"],
"unit": device["measurement_unit"],
}
for row in rows[: args.limit]
],
"truncated": truncated,
}
return TaskDataSnapshot(
source=args.source,
source_ref="mcp:" + str(uuid4()),
queried_at=datetime.now(timezone.utc),
data=data,
origin=origin,
truncated=truncated,
)