feat: add fail-closed Hub access profile foundation
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e747-8f27-7242-8df8-8bc44f88c929
This commit is contained in:
parent
df39fd5f43
commit
3e386147fd
35 changed files with 2009 additions and 195 deletions
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import json
|
||||
from typing import Any
|
||||
from collections.abc import Callable
|
||||
|
||||
import httpx
|
||||
from fastmcp import FastMCP
|
||||
|
|
@ -57,8 +58,14 @@ class HubCoreMCPServer:
|
|||
api_base: str,
|
||||
instructions: str | None = None,
|
||||
register_tools: bool = True,
|
||||
token_provider: Callable[[], str] | None = None,
|
||||
require_credentials: bool = False,
|
||||
trailing_slash: bool = True,
|
||||
) -> None:
|
||||
self.api_base = api_base.rstrip("/")
|
||||
self.token_provider = token_provider
|
||||
self.require_credentials = require_credentials
|
||||
self.trailing_slash = trailing_slash
|
||||
self.mcp = FastMCP(
|
||||
name=name,
|
||||
instructions=instructions or "Generic FOS hub MCP server.",
|
||||
|
|
@ -503,40 +510,51 @@ class HubCoreMCPServer:
|
|||
try:
|
||||
with self._client() as client:
|
||||
response = client.get(
|
||||
normalize_trailing_slash(path),
|
||||
normalize_trailing_slash(path, trailing=self.trailing_slash),
|
||||
params=self._clean(params or {}),
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except httpx.HTTPStatusError as exc:
|
||||
return {"error": f"API {exc.response.status_code}: {exc.response.text[:300]}"}
|
||||
except Exception as exc:
|
||||
return {"error": f"Request failed: {exc}"}
|
||||
return {"error": f"API {exc.response.status_code}"}
|
||||
except Exception:
|
||||
return {"error": "Request failed"}
|
||||
|
||||
def _post(self, path: str, body: dict[str, Any]) -> Any:
|
||||
try:
|
||||
with self._client() as client:
|
||||
response = client.post(normalize_trailing_slash(path), json=self._clean(body))
|
||||
response = client.post(normalize_trailing_slash(path, trailing=self.trailing_slash), json=self._clean(body))
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except httpx.HTTPStatusError as exc:
|
||||
return {"error": f"API {exc.response.status_code}: {exc.response.text[:300]}"}
|
||||
except Exception as exc:
|
||||
return {"error": f"Request failed: {exc}"}
|
||||
return {"error": f"API {exc.response.status_code}"}
|
||||
except Exception:
|
||||
return {"error": "Request failed"}
|
||||
|
||||
def _patch(self, path: str, body: dict[str, Any]) -> Any:
|
||||
try:
|
||||
with self._client() as client:
|
||||
response = client.patch(normalize_trailing_slash(path), json=self._clean(body))
|
||||
response = client.patch(normalize_trailing_slash(path, trailing=self.trailing_slash), json=self._clean(body))
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except httpx.HTTPStatusError as exc:
|
||||
return {"error": f"API {exc.response.status_code}: {exc.response.text[:300]}"}
|
||||
except Exception as exc:
|
||||
return {"error": f"Request failed: {exc}"}
|
||||
return {"error": f"API {exc.response.status_code}"}
|
||||
except Exception:
|
||||
return {"error": "Request failed"}
|
||||
|
||||
def _client(self) -> httpx.Client:
|
||||
return httpx.Client(base_url=self.api_base, timeout=30.0, follow_redirects=True)
|
||||
# The host resolves a Hub-audience credential from the current invocation.
|
||||
# Never retain it on the MCP server or fall back to a shared root token.
|
||||
headers = {}
|
||||
if self.token_provider is not None:
|
||||
token = self.token_provider()
|
||||
if not token or any(c.isspace() for c in token):
|
||||
raise ValueError("current invocation has no Hub credential")
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
elif self.require_credentials:
|
||||
raise ValueError("MCP host must provide a current Hub credential")
|
||||
return httpx.Client(base_url=self.api_base, timeout=30.0,
|
||||
headers=headers, follow_redirects=not bool(headers))
|
||||
|
||||
@staticmethod
|
||||
def _clean(data: dict[str, Any]) -> dict[str, Any]:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue