SurfSense/surfsense_backend/tests/e2e/fakes/notion_mcp_module.py
DESKTOP-RTLN3BA\$punk ab747e7a49 feat(agents): consolidate connectors under mcp_discovery; route web search through google_search
MCP consolidation:
- Route all MCP-capable connectors (Slack, Jira, Linear, ClickUp, Airtable,
  Notion, Confluence, interim Gmail/Calendar, custom MCP) through a single
  `mcp_discovery` subagent. Drive/OneDrive/Dropbox stay native to enrich the KB.
- Deprecate Discord/Teams/Luma: no viable official MCP server.

Google-only web search:
- Remove the main-agent `web_search` tool and the SearXNG platform service;
  all public web search now flows through the `google_search` subagent via task().
- Deprecate the Tavily/SearXNG/Linkup/Baidu search connectors (HTTP 410 on
  create, "Deprecated" badge); guide heavy users to the custom MCP connector.
- Remove web search from anonymous chat (pure Q&A).
- Tear SearXNG out of docker compose + install scripts; drop tavily-python
  and linkup-sdk deps and their config/env vars.

Fix:
- metrics._package_version() now swallows any metadata lookup failure. A
  malformed editable-install distribution with no `Version` field raised
  KeyError deep in importlib.metadata, and since it runs on every
  record_subagent_invoke_duration call it was crashing every task()
  delegation. Verified end-to-end against live GPT-5.4.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-04 21:06:04 -07:00

127 lines
4.5 KiB
Python

"""Strict Notion MCP OAuth/tool fakes for Playwright E2E.
Notion migrated from indexed OAuth to the hosted Notion MCP server
(``https://mcp.notion.com/mcp``, DCR/RFC 7591). This fake mirrors
``jira_module`` for the generic MCP OAuth + streamable-HTTP tool boundaries;
the older ``notion_module`` (a ``notion_client`` SDK stand-in) stays only to
satisfy production's import-time ``import notion_client``.
"""
from __future__ import annotations
from types import SimpleNamespace
from typing import Any
from tests.e2e.fakes import mcp_oauth_runtime, mcp_runtime
_AUTHORIZATION_URL = "https://mcp.notion.com/authorize"
_REGISTRATION_URL = "https://mcp.notion.com/register"
_TOKEN_URL = "https://mcp.notion.com/token"
_MCP_URL = "https://mcp.notion.com/mcp"
_CLIENT_ID = "fake-notion-mcp-client-id"
_CLIENT_SECRET = "fake-notion-mcp-client-secret"
_ACCESS_TOKEN = "fake-notion-mcp-access-token"
_REFRESH_TOKEN = "fake-notion-mcp-refresh-token"
_OAUTH_CODE = "fake-notion-oauth-code"
# Keep in sync with FAKE_NOTION_PAGES / CANARY_TOKENS.notionCanary in
# surfsense_web/tests/helpers/canary.ts.
_CANARY_TOKEN = "SURFSENSE_E2E_CANARY_TOKEN_NOTION_001"
_CANARY_PAGE_ID = "fake-notion-page-canary-001"
_CANARY_TITLE = "E2E Canary Notion Page"
_WORKSPACE_NAME = "SurfSense E2E Notion Workspace"
async def _list_tools() -> SimpleNamespace:
return SimpleNamespace(
tools=[
SimpleNamespace(
name="search",
description="Search the connected Notion workspace for pages and databases.",
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Text to search Notion pages for.",
}
},
"required": ["query"],
},
),
SimpleNamespace(
name="fetch",
description="Fetch the full contents of a Notion page by id.",
inputSchema={
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The Notion page id to fetch.",
}
},
"required": ["id"],
},
),
]
)
async def _call_tool(
tool_name: str, arguments: dict[str, Any] | None = None
) -> SimpleNamespace:
arguments = arguments or {}
if tool_name == "search":
query = str(arguments.get("query", ""))
if _CANARY_TITLE.lower() not in query.lower():
raise ValueError(f"Unexpected Notion search query: {query!r}")
text = (
f"{_CANARY_TITLE}\n"
f"id: {_CANARY_PAGE_ID}\n"
f"workspace: {_WORKSPACE_NAME}\n"
f"snippet: {_CANARY_TOKEN}"
)
return SimpleNamespace(content=[SimpleNamespace(text=text)])
if tool_name == "fetch":
page_id = str(arguments.get("id", ""))
if page_id != _CANARY_PAGE_ID:
raise ValueError(f"Unexpected Notion fetch id: {page_id!r}")
text = f"{_CANARY_TITLE}\n\n{_CANARY_TOKEN}"
return SimpleNamespace(content=[SimpleNamespace(text=text)])
raise NotImplementedError(f"Unexpected Notion MCP tool call: {tool_name!r}")
def install(active_patches: list[Any]) -> None:
"""Register Notion MCP OAuth/tool handlers with the shared dispatchers."""
del active_patches
mcp_oauth_runtime.register_service(
mcp_url=_MCP_URL,
discovery_metadata={
"issuer": "https://mcp.notion.com",
"authorization_endpoint": _AUTHORIZATION_URL,
"token_endpoint": _TOKEN_URL,
"registration_endpoint": _REGISTRATION_URL,
"code_challenge_methods_supported": ["S256"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"response_types_supported": ["code"],
},
client_id=_CLIENT_ID,
client_secret=_CLIENT_SECRET,
token_endpoint=_TOKEN_URL,
registration_endpoint=_REGISTRATION_URL,
oauth_code=_OAUTH_CODE,
access_token=_ACCESS_TOKEN,
refresh_token=_REFRESH_TOKEN,
scope="read write",
redirect_uri_substring="/api/v1/auth/mcp/notion/connector/callback",
)
mcp_runtime.register(
url=_MCP_URL,
expected_bearer=_ACCESS_TOKEN,
list_tools=_list_tools,
call_tool=_call_tool,
)