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

77 lines
3.1 KiB
Python

"""MCP is opt-in; an incomplete OAuth resource must never start listening."""
from pathlib import Path
from ipaddress import IPv4Address, IPv4Network
from urllib.parse import urlsplit
from pydantic import Field, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class McpSettings(BaseSettings):
enabled: bool = False
issuer_uri: str = ""
dev_http_issuer_uri: str = ""
environment: str = Field(default="production", validation_alias="ENVIRONMENT")
resource_uri: str = ""
audience: str = "tjwater-mcp"
host_client_id: str = "tjwater-agent-mcp"
allowed_hosts: list[str] = Field(
default_factory=lambda: ["127.0.0.1:*", "localhost:*"]
)
allowed_origins: list[str] = Field(default_factory=list)
context_ttl_seconds: int = Field(default=600, ge=30, le=600)
query_timeout_seconds: int = Field(default=30, ge=1, le=60)
max_calculation_timeout_seconds: int = Field(default=3600, ge=120, le=86400)
model_config = SettingsConfigDict(
env_prefix="TJWATER_MCP_",
extra="ignore",
populate_by_name=True,
env_file=Path(__file__).resolve().parents[2] / ".env",
)
@model_validator(mode="after")
def validate_resource(self):
if self.dev_http_issuer_uri:
dev_url = urlsplit(self.dev_http_issuer_uri)
try:
address = IPv4Address(dev_url.hostname or "")
private_ipv4 = any(address in IPv4Network(network) for network in (
"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"
))
except ValueError:
private_ipv4 = False
if (
self.environment != "development"
or dev_url.scheme != "http"
or not private_ipv4
or dev_url.username or dev_url.password or dev_url.query or dev_url.fragment
or self.dev_http_issuer_uri != self.issuer_uri
):
raise ValueError("development HTTP issuer requires development environment and an exact private IPv4 issuer")
if not self.enabled:
return self
for index, value in enumerate((self.issuer_uri, self.resource_uri)):
url = urlsplit(value)
if (
url.scheme not in {"http", "https"}
or not url.hostname
or url.username
or url.password
or url.query
or url.fragment
):
raise ValueError("MCP requires explicit issuer and resource URIs")
if url.scheme == "http" and url.hostname not in {
"localhost",
"127.0.0.1",
"::1",
}:
if index != 0 or value != self.dev_http_issuer_uri:
raise ValueError("non-loopback MCP and OAuth require HTTPS")
if urlsplit(self.resource_uri).path != "/mcp":
raise ValueError("MCP resource URI must identify the /mcp endpoint")
if not self.audience.strip() or not self.host_client_id.strip():
raise ValueError("MCP audience and host client ID are required")
return self