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.
61 lines
2.1 KiB
Python
61 lines
2.1 KiB
Python
"""Host-only MCP bindings: declared in REST contracts even when MCP is disabled."""
|
|
|
|
from fastapi import APIRouter, Header, HTTPException, Request, Response
|
|
from psycopg.errors import UndefinedTable, InvalidSchemaName
|
|
from app.mcp.models import BindInput, BindResponse
|
|
from app.infra.db.postgresql.mcp_queries import McpQueryRepository
|
|
|
|
router = APIRouter(tags=["MCP Query Context"])
|
|
|
|
|
|
def runtime_for(request: Request):
|
|
runtime = getattr(request.app.state, "mcp_runtime", None)
|
|
if runtime is None:
|
|
raise HTTPException(503, "Product MCP is disabled")
|
|
return runtime
|
|
|
|
|
|
@router.post("/mcp-query-contexts", response_model=BindResponse)
|
|
async def create_context(
|
|
command: BindInput,
|
|
request: Request,
|
|
x_project_id: str = Header(alias="X-Project-Id"),
|
|
) -> BindResponse:
|
|
runtime = runtime_for(request)
|
|
principal = await runtime.identity(request)
|
|
from app.mcp.contexts import bind_context
|
|
|
|
try:
|
|
async with runtime.scope_factory(principal, x_project_id) as scope:
|
|
binding = await bind_context(scope, principal, command, runtime.settings)
|
|
except (UndefinedTable, InvalidSchemaName):
|
|
raise HTTPException(
|
|
503, "MCP project storage has not been provisioned"
|
|
) from None
|
|
return BindResponse(
|
|
context_handle=binding.context_handle,
|
|
expires_at=binding.expires_at,
|
|
task_id=binding.task_id,
|
|
run_id=binding.run_id,
|
|
)
|
|
|
|
|
|
@router.delete("/mcp-query-contexts/{context_handle}", status_code=204)
|
|
async def delete_context(
|
|
context_handle: str,
|
|
request: Request,
|
|
x_project_id: str = Header(alias="X-Project-Id"),
|
|
) -> Response:
|
|
runtime = runtime_for(request)
|
|
principal = await runtime.identity(request)
|
|
try:
|
|
async with runtime.scope_factory(principal, x_project_id) as scope:
|
|
await McpQueryRepository(scope.connection, scope.project.project_id).revoke(
|
|
context_handle, principal.subject, principal.client_id
|
|
)
|
|
except (UndefinedTable, InvalidSchemaName):
|
|
raise HTTPException(
|
|
503, "MCP project storage has not been provisioned"
|
|
) from None
|
|
return Response(status_code=204)
|