How to Use MCP with Python Agent through Arcade

How to Use MCP with Python Agent through Arcade

Arcade.dev Team's avatar
Arcade.dev Team
OCTOBER 27, 2025
6 MIN READ
THOUGHT LEADERSHIP
Rays decoration image
Ghost Icon

Model Context Protocol (MCP) standardizes tool calling for AI agents. Arcade extends MCP with OAuth-backed authentication, enabling Python agents to access authenticated services like Gmail, Slack, and GitHub on behalf of users.

Prerequisites

  • Python 3.10 or higher
  • Arcade account and API key
  • Basic async/await knowledge in Python
  • Development environment (VS Code, PyCharm)

Installation

Install the Arcade Python client:

pip install arcadepy

For OpenAI Agents integration:

pip install agents-arcade

For building custom MCP servers:

pip install arcade-mcp-server

Set your API key:

export ARCADE_API_KEY="your_api_key"

Method 1: Using Pre-Built MCP Servers

Arcade hosts MCP servers for Gmail, Slack, GitHub, and other services with OAuth already configured.

Basic Python Agent Implementation

from arcadepy import Arcade

client = Arcade(api_key="your_api_key")
user_id = "user@example.com"

# List available Gmail tools
gmail_toolkit = await client.tools.list(
    toolkit="gmail",
    limit=30
)

# Execute a tool
response = await client.tools.execute(
    tool_name="Gmail.ListEmails",
    input={"max_results": 10},
    user_id=user_id
)

print(response.output)

OAuth Authorization Flow

Handle OAuth for tools requiring authentication:

from arcadepy import Arcade

async def execute_with_auth(user_id: str):
    client = Arcade()

    # Check authorization status
    auth_response = await client.tools.authorize(
        tool_name="Gmail.SendEmail",
        user_id=user_id
    )

    if auth_response.status != "completed":
        print(f"Authorize at: {auth_response.url}")
        await client.auth.wait_for_completion(auth_response)

    # Execute after authorization
    result = await client.tools.execute(
        tool_name="Gmail.SendEmail",
        input={
            "to": "recipient@example.com",
            "subject": "Test",
            "body": "Message from Python agent"
        },
        user_id=user_id
    )

    return result.output

OpenAI Agents Integration

Arcade provides native OpenAI Agents support:

from agents import Agent, Runner
from arcadepy import AsyncArcade
from agents_arcade import get_arcade_tools
from agents_arcade.errors import AuthorizationError

async def main():
    client = AsyncArcade()

    # Get tools from MCP server
    tools = await get_arcade_tools(client, toolkits=["gmail"])

    # Create agent
    agent = Agent(
        name="Gmail Agent",
        instructions="Manage Gmail operations",
        model="gpt-4o-mini",
        tools=tools,
    )

    try:
        result = await Runner.run(
            starting_agent=agent,
            input="List my recent emails",
            context={"user_id": "user@example.com"},
        )
        print(result.final_output)
    except AuthorizationError as e:
        print(f"Authorization URL: {e.url}")

Method 2: Building Custom MCP Servers

Create custom MCP servers for domain-specific tools using arcade-mcp-server.

Creating a New MCP Server

Install the CLI and generate a project:

# Install CLI
uv tool install arcade-mcp

# Create new server
arcade new custom_server
cd custom_server

Project structure:

  • server.py - Main server file
  • pyproject.toml - Dependencies
  • .env.example - Environment variables

Defining Custom Tools

Create tools with the @app.tool decorator:

#!/usr/bin/env python3
from typing import Annotated
from arcade_mcp_server import Context, MCPApp

app = MCPApp(
    name="custom_server",
    version="1.0.0",
    instructions="Custom MCP server"
)

@app.tool
async def fetch_data(
    context: Context,
    data_id: Annotated[str, "Data identifier"],
    format: Annotated[str, "Output format"] = "json"
) -> Annotated[str, "Fetched data"]:
    """Fetch data by ID."""

    await context.log.info(f"Fetching {data_id}")

    # Your logic here
    result = f"Data for {data_id} in {format}"

    return result

if __name__ == "__main__":
    import sys
    transport = sys.argv[1] if len(sys.argv) > 1 else "stdio"
    app.run(transport=transport)

Adding OAuth Authentication

Tools requiring OAuth use auth decorators:

from arcade_mcp_server import Context, MCPApp
from arcade_mcp_server.auth import Google
from typing import Annotated
import httpx

app = MCPApp(name="google_server", version="1.0.0")

@app.tool(
    requires_auth=Google(
        scopes=["https://www.googleapis.com/auth/drive.readonly"]
    )
)
async def read_drive_file(
    context: Context,
    file_id: Annotated[str, "File ID"],
) -> Annotated[str, "File contents"]:
    """Read Google Drive file."""

    if not context.authorization or not context.authorization.token:
        raise ValueError("Authorization required")

    token = context.authorization.token

    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"https://www.googleapis.com/drive/v3/files/{file_id}",
            headers={"Authorization": f"Bearer {token}"}
        )
        response.raise_for_status()
        return response.text

Using Secrets

Access secrets through context:

@app.tool
async def call_api(
    context: Context,
    endpoint: Annotated[str, "API endpoint"],
) -> Annotated[str, "API response"]:
    """Call external API with stored credentials."""

    try:
        api_key = context.get_secret("API_KEY")
    except ValueError:
        await context.log.error("API_KEY not configured")
        return "Error: Missing API key"

    async with httpx.AsyncClient() as client:
        response = await client.get(
            endpoint,
            headers={"Authorization": f"Bearer {api_key}"}
        )
        return response.text

Running the MCP Server

For stdio transport (Claude Desktop):

python server.py stdio

For HTTP transport (Cursor, VS Code):

python server.py http --port 8000

Using Arcade CLI:

arcade serve --port 8002 --reload --mcp

Options:

  • -reload - Auto-reload on file changes
  • -mcp - Run as stdio MCP server
  • -no-auth - Disable authentication (development only)
  • -debug - Enable debug logging

Multi-User Authentication

Production agents serve multiple users with isolated authentication contexts.

Session Management

from arcadepy import Arcade
from datetime import datetime
from typing import Dict, Any

class AgentManager:
    def __init__(self):
        self.client = Arcade()
        self.sessions: Dict[str, Any] = {}

    async def auth_user(self, user_id: str) -> Dict[str, Any]:
        """Handle user OAuth flow."""

        auth_response = await self.client.tools.authorize(
            tool_name="Gmail.SendEmail",
            user_id=user_id
        )

        if auth_response.status != "completed":
            return {
                "auth_required": True,
                "url": auth_response.url
            }

        await self.client.auth.wait_for_completion(auth_response)

        self.sessions[user_id] = {
            "authenticated": True,
            "timestamp": datetime.now()
        }

        return {"authenticated": True}

    async def execute_tool(
        self,
        user_id: str,
        tool_name: str,
        params: Dict
    ):
        """Execute tool with user context."""

        if user_id not in self.sessions:
            return await self.auth_user(user_id)

        response = await self.client.tools.execute(
            tool_name=tool_name,
            input=params,
            user_id=user_id
        )

        return response.output

Toolset Caching

Optimize performance with per-user caching:

from collections import OrderedDict
import time

class ToolCache:
    def __init__(self, max_size: int = 1000, ttl: int = 3600):
        self.cache = OrderedDict()
        self.max_size = max_size
        self.ttl = ttl

    def set(self, user_id: str, tools):
        if user_id in self.cache:
            self.cache.move_to_end(user_id)

        self.cache[user_id] = {
            "tools": tools,
            "timestamp": time.time()
        }

        if len(self.cache) > self.max_size:
            self.cache.popitem(last=False)

    def get(self, user_id: str):
        entry = self.cache.get(user_id)
        if not entry:
            return None

        if time.time() - entry["timestamp"] > self.ttl:
            del self.cache[user_id]
            return None

        self.cache.move_to_end(user_id)
        return entry["tools"]

Production Deployment

Using Arcade Deploy

Arcade Deploy hosts MCP servers with OAuth support.

Create worker.toml:

[[worker]]
[worker.config]
id = "production-worker"
secret = "secure_worker_secret"

[worker.local_source]
packages = ["./toolkit"]

Deploy:

arcade deploy

Verify:

arcade workers list

Docker Deployment

Build containerized MCP server:

FROM python:3.10-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY server.py .
COPY .env .

CMD ["python", "server.py", "http", "--port", "8000"]

Run:

docker build -t mcp-server .
docker run -p 8000:8000 --env-file .env mcp-server

Kubernetes Deployment

Scale with Kubernetes:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mcp-server
spec:
  replicas: 3
  selector:
    matchLabels:
      app: mcp-server
  template:
    metadata:
      labels:
        app: mcp-server
    spec:
      containers:
        - name: server
          image: registry/mcp-server:latest
          env:
            - name: ARCADE_API_KEY
              valueFrom:
                secretKeyRef:
                  name: arcade-secrets
                  key: api-key
          ports:
            - containerPort: 8000
          resources:
            requests:
              memory: "512Mi"
              cpu: "500m"
            limits:
              memory: "1Gi"
              cpu: "1000m"

Error Handling

Authorization Errors

Handle authorization failures:

from arcadepy import Arcade
import asyncio

async def handle_execution(user_id: str, tool_name: str, params: dict):
    """Execute tool with error handling."""

    client = Arcade()

    try:
        result = await client.tools.execute(
            tool_name=tool_name,
            input=params,
            user_id=user_id
        )
        return {"success": True, "data": result.output}

    except Exception as e:
        error_type = getattr(e, 'type', type(e).__name__)

        if error_type == "authorization_required":
            return {
                "success": False,
                "auth_required": True,
                "url": getattr(e, 'url', '')
            }

        elif error_type == "token_expired":
            refresh = await client.auth.refresh(
                user_id=user_id,
                provider='google'
            )

            if refresh.success:
                return await handle_execution(user_id, tool_name, params)
            else:
                return {"success": False, "reauth_required": True}

        elif error_type == "rate_limit_exceeded":
            retry_count = getattr(e, 'retry_count', 1)
            await asyncio.sleep(2 ** retry_count)
            return {"success": False, "retry": True}

        else:
            return {"success": False, "error": str(e)}

Monitoring

Track metrics for production systems:

import logging
from datetime import datetime
from typing import Dict

class Monitor:
    def __init__(self):
        self.logger = logging.getLogger("mcp.monitor")
        self.metrics = {
            "auth_attempts": 0,
            "auth_success": 0,
            "tool_calls": 0,
            "errors": 0
        }

    async def track_auth(self, user_id: str, success: bool):
        """Track authentication metrics."""
        self.metrics["auth_attempts"] += 1
        if success:
            self.metrics["auth_success"] += 1

        await self.log_event({
            "timestamp": datetime.now().isoformat(),
            "event": "auth",
            "user": self.hash_id(user_id),
            "success": success
        })

    async def track_tool(self, tool: str, success: bool):
        """Track tool execution."""
        self.metrics["tool_calls"] += 1
        if not success:
            self.metrics["errors"] += 1

    def health_status(self) -> Dict:
        """Generate health report."""
        auth_rate = (
            self.metrics["auth_success"] / self.metrics["auth_attempts"]
            if self.metrics["auth_attempts"] > 0 else 0
        )

        error_rate = (
            self.metrics["errors"] / self.metrics["tool_calls"]
            if self.metrics["tool_calls"] > 0 else 0
        )

        return {
            "status": "healthy" if error_rate < 0.05 else "degraded",
            "auth_rate": auth_rate,
            "error_rate": error_rate,
            "total_calls": self.metrics["tool_calls"]
        }

    def hash_id(self, user_id: str) -> str:
        """Hash user ID for privacy."""
        import hashlib
        return hashlib.sha256(user_id.encode()).hexdigest()[:16]

Common Issues

Authentication Failures

If users cannot authenticate:

  • Verify API key is set correctly
  • Check OAuth credentials in Arcade dashboard
  • Confirm redirect URLs match configuration
  • Verify scopes in tool definitions

Tools Not Loading

If tools don't appear:

  • Confirm toolkit name matches documented server name
  • Check MCP server registration
  • Verify user completed authorization
  • Review server startup logs

Rate Limiting

Handle rate limits:

  • Implement exponential backoff
  • Cache tool results when appropriate
  • Batch operations
  • Contact support for quota increases

Resources

MCP provides the protocol foundation for tool calling. Arcade extends it with OAuth authentication, enabling Python agents to interact with services securely. Use pre-built servers for rapid development or build custom MCP servers for specific requirements. The platform handles authentication, letting you focus on agent functionality.

SHARE THIS POST

RECENT ARTICLES

Rays decoration image
THOUGHT LEADERSHIP

Agent Skills vs Tools: What Actually Matters

The agent ecosystem has a terminology problem that masks a real architectural choice. "Tools" and "skills" get used interchangeably in marketing decks and conference talks, but they represent fundamentally different approaches to extending agent capabilities. Understanding this distinction is the difference between building agents that work in demos versus agents that work in production. But here's the uncomfortable truth that gets lost in the semantic debates: from the agent's perspective, it'

Rays decoration image
THOUGHT LEADERSHIP

Using LangChain and Arcade.dev to Build AI Agents For Consumer Packaged Goods: Top 3 Use Cases

Key Takeaways * CPG companies hit a multi-user authorization wall, not a capability gap: Most agent projects stall in production because leaders can’t safely govern what permissions and scopes an agent has after it’s logged in across fragmented, domain specific systems (ERPs, retailer portals, communications). Arcade.dev’s MCP runtime replaces months of custom permissioning, token/secret handling, and auditability work. * Weather-based demand forecasting delivers fastest ROI: Unilever achiev

Rays decoration image
THOUGHT LEADERSHIP

Using LangChain and Arcade.dev to Build AI Agents For Energy & Utilities: Top 3 Use Cases

Key Takeaways * Multi-user authorization blocks AI agent production in energy utilities: While AI agents show transformative potential across industries, energy utilities struggle to move past proof-of-concept because agents need secure, scoped access to SCADA systems, customer databases, and field operations platforms — Arcade.dev's MCP runtime solves this gap * LangChain + LangGraph are widely used for agent orchestration: Together they provide a proven way to model multi-step utility work

Blog CTA Icon

Get early access to Arcade, and start building now.