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:
@@ -0,0 +1,76 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user