chore(mcp): Remove dev artifacts and simplify credential lookup

- Remove MCP_BLOCK_IMPLEMENTATION.md development doc
- Remove console.log debug statements from OAuth callback
- Simplify credential lookup to single call (get_creds_by_provider
  already handles Python 3.13 StrEnum bug via _provider_matches)
- Remove unused Credentials import from routes.py
This commit is contained in:
Zamil Majdy
2026-02-10 15:18:55 +04:00
parent c1c269c4a9
commit 8bea7cf875
4 changed files with 13 additions and 104 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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 (

View File

@@ -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");