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

How to Query Postgres from GPT-5 via Arcade (MCP)

Large language models need structured data access to provide accurate, data-driven insights. This guide demonstrates how to connect GPT-5 to PostgreSQL databases through Arcade's Model Context Protocol implementation, enabling secure database queries without exposing credentials directly to language models. Prerequisites Before implementing database connectivity, ensure you have: * Python 3.8 or higher installed * PostgreSQL database with connection credentials * Arcade API key (free t

Rays decoration image
THOUGHT LEADERSHIP

How to Connect GPT-5 to Slack with Arcade (MCP)

Building AI agents that interact with Slack requires secure OAuth authentication, proper token management, and reliable tool execution. This guide shows you how to connect GPT-5 to Slack using Arcade's Model Context Protocol (MCP) implementation, enabling your agents to send messages, read conversations, and manage channels with production-grade security. Prerequisites Before starting, ensure you have: * Arcade.dev account with API key * Python 3.10+ or Node.js 18+ installed * OpenAI A

Rays decoration image
THOUGHT LEADERSHIP

How to Build a GPT-5 Gmail Agent with Arcade (MCP)

Building AI agents that can access and act on Gmail data represents a significant challenge in production environments. This guide demonstrates how to build a fully functional Gmail agent using OpenAI's latest models through Arcade's Model Context Protocol implementation, enabling secure OAuth-based authentication and real-world email operations. Prerequisites Before starting, ensure you have: * Active Arcade.dev account with API key * Python 3.10 or higher installed * OpenAI API key w

Blog CTA Icon

Get early access to Arcade, and start building now.