feat(ai): durcissement phase F (#79) - rate limit, redaction, OpenAPI/MCP, E2E
This commit is contained in:
@@ -32,6 +32,7 @@ TAGS_METADATA: list[dict[str, str]] = [
|
||||
{"name": "Export", "description": "Export notes or whole vaults to HTML, Markdown bundle or ePub."},
|
||||
{"name": "AI", "description": "AI-powered editor actions, provider status and model discovery."},
|
||||
{"name": "BooksLM", "description": "Directory-scoped AI chat (NotebookLM-style) over a vault folder."},
|
||||
{"name": "MCP", "description": "Model Context Protocol server (Streamable HTTP) exposing the shared AI tool layer to external clients (Claude Desktop, Cursor…)."},
|
||||
{"name": "Sharing", "description": "Create and manage public read-only share links for documents."},
|
||||
{"name": "Webhooks", "description": "HTTP callbacks signed with HMAC-SHA256 for file events."},
|
||||
{"name": "Conflicts", "description": "Detect and resolve Syncthing sync-conflict files."},
|
||||
@@ -79,6 +80,7 @@ _TAG_RULES: list[tuple[re.Pattern[str], str]] = [
|
||||
(re.compile(r"^/api/push"), "Push"),
|
||||
(re.compile(r"^/api/ai/bookslm"), "BooksLM"),
|
||||
(re.compile(r"^/api/ai"), "AI"),
|
||||
(re.compile(r"^/mcp"), "MCP"),
|
||||
(re.compile(r"^/api/config/ai-"), "AI"),
|
||||
(re.compile(r"^/api/share"), "Sharing"),
|
||||
(re.compile(r"^/api/shares"), "Sharing"),
|
||||
@@ -141,6 +143,7 @@ _TAG_ALIASES: dict[str, str] = {
|
||||
"frontend": "Frontend",
|
||||
"ai": "AI",
|
||||
"bookslm": "BooksLM",
|
||||
"mcp": "MCP",
|
||||
"pdf": "PDF",
|
||||
"bookmarks": "Bookmarks",
|
||||
}
|
||||
@@ -224,6 +227,67 @@ def _is_binary_operation(operation: dict[str, Any]) -> bool:
|
||||
return any(media.startswith(_BINARY_MEDIA) for media in content)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MCP endpoint (not a FastAPI route: custom ASGI mount) — documented manually
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_MCP_DESCRIPTION = (
|
||||
"**Model Context Protocol** server over Streamable HTTP (JSON-RPC 2.0). "
|
||||
"Exposes the shared AI tool layer to external MCP clients (Claude Desktop, "
|
||||
"Cursor…). Authentication uses `Authorization: Bearer <JWT>` (the same "
|
||||
"token as the REST API).\n\n"
|
||||
"Primitives: read/search **tools** directly; write/destructive tools as a "
|
||||
"two-step `propose_<tool>` / `apply_<tool>` pair (signed, single-use "
|
||||
"confirmation token); **resources** `vault://<name>` and "
|
||||
"`vault://<name>/<path>` (read-only, secrets redacted); **prompts** "
|
||||
"`summarize-directory`, `generate-note`, `find-related`.\n\n"
|
||||
"See `docs/MCP_GUIDE.md` for client setup."
|
||||
)
|
||||
|
||||
|
||||
def _inject_mcp_path(schema: dict[str, Any]) -> None:
|
||||
"""Add the MCP Streamable HTTP endpoint to the schema (idempotent)."""
|
||||
paths = schema.setdefault("paths", {})
|
||||
if "/mcp" in paths:
|
||||
return
|
||||
paths["/mcp"] = {
|
||||
"post": {
|
||||
"tags": ["MCP"],
|
||||
"summary": "MCP Streamable HTTP endpoint (JSON-RPC 2.0)",
|
||||
"operationId": "mcp_streamable_http",
|
||||
"description": _MCP_DESCRIPTION,
|
||||
"requestBody": {
|
||||
"required": True,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/list",
|
||||
"params": {},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "JSON-RPC response (or 202 for notifications)",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {"tools": []},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"security": [{"bearerAuth": []}],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def enrich_openapi_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Enrich a FastAPI-generated OpenAPI schema in place and return it.
|
||||
|
||||
@@ -242,6 +306,8 @@ def enrich_openapi_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
||||
}
|
||||
schema["tags"] = TAGS_METADATA
|
||||
|
||||
_inject_mcp_path(schema)
|
||||
|
||||
components = schema.setdefault("components", {})
|
||||
security_schemes = components.setdefault("securitySchemes", {})
|
||||
security_schemes.setdefault("bearerAuth", {
|
||||
|
||||
Reference in New Issue
Block a user