diff --git a/autogpt_platform/backend/MCP_BLOCK_IMPLEMENTATION.md b/autogpt_platform/backend/MCP_BLOCK_IMPLEMENTATION.md deleted file mode 100644 index 34128a02d9..0000000000 --- a/autogpt_platform/backend/MCP_BLOCK_IMPLEMENTATION.md +++ /dev/null @@ -1,76 +0,0 @@ -# MCP Block Implementation Plan - -## Overview - -Create a single **MCPBlock** that dynamically integrates with any MCP (Model Context Protocol) -server. Users provide a server URL, the block discovers available tools, presents them as a -dropdown, and dynamically adjusts input/output schema based on the selected tool — exactly like -`AgentExecutorBlock` handles dynamic schemas. - -## Architecture - -``` -User provides MCP server URL + credentials - ↓ -MCPBlock fetches tools via MCP protocol (tools/list) - ↓ -User selects tool from dropdown (stored in constantInput) - ↓ -Input schema dynamically updates based on selected tool's inputSchema - ↓ -On execution: MCPBlock calls the tool via MCP protocol (tools/call) - ↓ -Result yielded as block output -``` - -## Design Decisions - -1. **Single block, not many blocks** — One `MCPBlock` handles all MCP servers/tools -2. **Dynamic schema via AgentExecutorBlock pattern** — Override `get_input_schema()`, - `get_input_defaults()`, `get_missing_input()` on the Input class -3. **Auth via API key or OAuth2 credentials** — Use existing `APIKeyCredentials` or - `OAuth2Credentials` with `ProviderName.MCP` provider. API keys are sent as Bearer tokens; - OAuth2 uses the access token. -4. **HTTP-based MCP client** — Use `aiohttp` (already a dependency) to implement MCP Streamable - HTTP transport directly. No need for the `mcp` Python SDK — the protocol is simple JSON-RPC - over HTTP. Handles both JSON and SSE response formats. -5. **No new DB tables** — Everything fits in existing `AgentBlock` + `AgentNode` tables - -## Implementation Files - -### New Files -- `backend/blocks/mcp/` — MCP block package - - `__init__.py` - - `block.py` — MCPToolBlock implementation - - `client.py` — MCP HTTP client (list_tools, call_tool) - - `oauth.py` — MCP OAuth handler for dynamic endpoint discovery - - `test_mcp.py` — Unit tests - - `test_oauth.py` — OAuth handler tests - - `test_integration.py` — Integration tests with local test server - - `test_e2e.py` — E2E tests against real MCP servers - -### Modified Files -- `backend/integrations/providers.py` — Add `MCP = "mcp"` to ProviderName - -## Dev Loop - -```bash -cd autogpt_platform/backend -poetry run pytest backend/blocks/mcp/test_mcp.py -xvs # Unit tests -poetry run pytest backend/blocks/mcp/test_oauth.py -xvs # OAuth tests -poetry run pytest backend/blocks/mcp/test_integration.py -xvs # Integration tests -poetry run pytest backend/blocks/mcp/ -xvs # All MCP tests -``` - -## Status - -- [x] Research & Design -- [x] Add ProviderName.MCP -- [x] Implement MCP client (client.py) -- [x] Implement MCPToolBlock (block.py) -- [x] Add OAuth2 support (oauth.py) -- [x] Write unit tests -- [x] Write integration tests -- [x] Write E2E tests -- [x] Run tests & fix issues -- [x] Create PR diff --git a/autogpt_platform/backend/backend/api/features/mcp/routes.py b/autogpt_platform/backend/backend/api/features/mcp/routes.py index 14bd541b2d..4a190d0cf7 100644 --- a/autogpt_platform/backend/backend/api/features/mcp/routes.py +++ b/autogpt_platform/backend/backend/api/features/mcp/routes.py @@ -17,7 +17,7 @@ from pydantic import BaseModel, Field from backend.api.features.integrations.router import CredentialsMetaResponse from backend.blocks.mcp.client import MCPClient, MCPClientError from backend.blocks.mcp.oauth import MCPOAuthHandler -from backend.data.model import Credentials, OAuth2Credentials +from backend.data.model import OAuth2Credentials from backend.integrations.creds_manager import IntegrationCredentialsManager from backend.integrations.providers import ProviderName from backend.util.request import HTTPClientError, Requests @@ -77,14 +77,11 @@ async def discover_tools( auth_token = request.auth_token # Auto-use stored MCP credential when no explicit token is provided. - # Also check for the wrong provider string from Python 3.13 str(Enum) bug. if not auth_token: try: - mcp_creds: list[Credentials] = [] - for prov in (ProviderName.MCP.value, "ProviderName.MCP"): - mcp_creds.extend( - await creds_manager.store.get_creds_by_provider(user_id, prov) - ) + mcp_creds = await creds_manager.store.get_creds_by_provider( + user_id, ProviderName.MCP.value + ) # Find the freshest credential for this server URL best_cred: OAuth2Credentials | None = None for cred in mcp_creds: @@ -359,14 +356,10 @@ async def mcp_oauth_callback( credentials.title = f"MCP: {hostname}" # Remove old MCP credentials for the same server to prevent stale token buildup. - # Also clean up credentials stored with the wrong provider string - # ("ProviderName.MCP" instead of "mcp") from a Python 3.13 str(Enum) bug. try: - old_creds: list[Credentials] = [] - for prov in (ProviderName.MCP.value, "ProviderName.MCP"): - old_creds.extend( - await creds_manager.store.get_creds_by_provider(user_id, prov) - ) + old_creds = await creds_manager.store.get_creds_by_provider( + user_id, ProviderName.MCP.value + ) for old in old_creds: if ( isinstance(old, OAuth2Credentials) diff --git a/autogpt_platform/backend/backend/blocks/mcp/block.py b/autogpt_platform/backend/backend/blocks/mcp/block.py index 339f564f10..ebb7aacc6b 100644 --- a/autogpt_platform/backend/backend/blocks/mcp/block.py +++ b/autogpt_platform/backend/backend/blocks/mcp/block.py @@ -215,15 +215,14 @@ class MCPToolBlock(Block): This is a fallback for nodes that don't have ``credentials`` explicitly set (e.g. nodes created before the credential field was wired up). """ - from backend.data.model import Credentials from backend.integrations.creds_manager import IntegrationCredentialsManager from backend.integrations.providers import ProviderName try: mgr = IntegrationCredentialsManager() - mcp_creds: list[Credentials] = [] - for prov in (ProviderName.MCP.value, "ProviderName.MCP"): - mcp_creds.extend(await mgr.store.get_creds_by_provider(user_id, prov)) + mcp_creds = await mgr.store.get_creds_by_provider( + user_id, ProviderName.MCP.value + ) best: OAuth2Credentials | None = None for cred in mcp_creds: if ( diff --git a/autogpt_platform/frontend/src/app/(platform)/auth/integrations/mcp_callback/route.ts b/autogpt_platform/frontend/src/app/(platform)/auth/integrations/mcp_callback/route.ts index b005311111..326f42e049 100644 --- a/autogpt_platform/frontend/src/app/(platform)/auth/integrations/mcp_callback/route.ts +++ b/autogpt_platform/frontend/src/app/(platform)/auth/integrations/mcp_callback/route.ts @@ -47,16 +47,13 @@ export async function GET(request: Request) { var msg = ${safeJsonStringify(message)}; var sent = false; - console.log("[MCP Callback] Script running, success:", msg.success, "code:", !!msg.code, "state:", !!msg.state); - // Method 1: BroadcastChannel (reliable across tabs/popups, no opener needed) try { var bc = new BroadcastChannel("mcp_oauth"); bc.postMessage({ type: "mcp_oauth_result", success: msg.success, code: msg.code, state: msg.state, message: msg.message }); bc.close(); sent = true; - console.log("[MCP Callback] BroadcastChannel message sent"); - } catch(e) { console.warn("[MCP Callback] BroadcastChannel failed:", e); } + } catch(e) { /* BroadcastChannel not supported */ } // Method 2: window.opener.postMessage (fallback for same-origin popups) try { @@ -66,18 +63,14 @@ export async function GET(request: Request) { window.location.origin ); sent = true; - console.log("[MCP Callback] postMessage sent to opener"); - } else { - console.log("[MCP Callback] window.opener not available (COOP or popup blocked)"); } - } catch(e) { console.warn("[MCP Callback] postMessage failed:", e); } + } catch(e) { /* opener not available (COOP) */ } // Method 3: localStorage (most reliable cross-tab fallback) try { localStorage.setItem("mcp_oauth_result", JSON.stringify(msg)); sent = true; - console.log("[MCP Callback] localStorage set"); - } catch(e) { console.warn("[MCP Callback] localStorage failed:", e); } + } catch(e) { /* localStorage not available */ } var statusEl = document.getElementById("status"); var spinnerEl = document.getElementById("spinner");