Build and deploy custom MCP tools using the Arcade MCP framework. Use when the user wants to create, build, scaffold, or deploy an MCP tool or server to Arcade Cloud, or when working with arcade_mcp_server, MCPApp, @tool decorators, arcade deploy, or Arcade tool development.
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
Build and deploy custom MCP tools using the Arcade MCP framework. Use when the user wants to create, build, scaffold, or deploy an MCP tool or server to Arcade Cloud, or when working with arcade_mcp_server, MCPApp, @tool decorators, arcade deploy, or Arcade tool development.
Build and Deploy Custom Arcade MCP Tools
What is Arcade?
Arcade is the MCP runtime for AI agents. It provides secure agent authorization, tool hosting, and centralized governance so you can ship production-grade tools without building auth infrastructure yourself.
What Arcade handles for you:
OAuth flows: Just-in-time authorization -- Arcade manages the entire OAuth lifecycle (consent, token issuance, refresh, storage) with zero code from you
Secrets management: API keys and credentials are injected at runtime via Context, never exposed to LLMs or clients
Multi-user support: When deployed, each user gets their own auth session automatically
Tool hosting: Deploy with arcade deploy and Arcade runs your MCP server in the cloud with health checks, scaling, and monitoring
Built-in auth providers: Google, Slack, GitHub, Reddit, and more work out of the box -- no need to register OAuth apps or manage client credentials
Development model: Build tools locally with arcade_mcp_server -> test with stdio/HTTP -> deploy to Arcade Cloud with arcade deploy. For deeper platform context, fetch https://docs.arcade.dev/llms.txt
Before You Begin
Gather these decisions from the user before writing any code:
Integration target: What API, service, or system will this tool connect to?
: OAuth (user-delegated), API Key / Secrets, Both, or None?
Scopes (if OAuth): What permissions does the tool need?
Secrets (if API key): What secret names are needed (e.g., SERVICE_API_KEY)?
Language: Python (recommended, primary support) or TypeScript?
Use the AskQuestion tool if available to ask about integration target, auth type (options: "OAuth", "API Key / Secrets", "Both", "No auth"), and OAuth provider if applicable.
Step 1: Scaffold the Project
uv tool install arcade-mcp
arcade login
arcade new my_server
cd my_server
from arcade_mcp_server.exceptions import RetryableToolError, ToolExecutionError
Tool Function Signature
Every tool MUST follow this exact pattern:
@tool(
requires_auth=ProviderClass(scopes=["scope1", "scope2"]),
# OR requires_secrets=["SECRET_NAME"],# OR both)asyncdefmy_tool_name(
context: Context,
required_param: Annotated[str, "Clear description for the LLM"],
optional_param: Annotated[int, "Description with constraints"] = 10,
enum_param: Annotated[MyEnum, "Constrained choices"] = MyEnum.DEFAULT,
) -> Annotated[OutputType, "Description of the return value"]:
"""Concise, LLM-optimized description of what this tool does."""
...
Mandatory Rules
Always async def for all tool functions
Context is always the first parameter -- never omit it for tools that need auth/secrets
Annotated[Type, "description"] on every parameter AND return type
Docstrings are for the LLM -- write them for machine comprehension, not humans
Return structured dicts or TypedDicts -- flat, relevant fields only
Never accept secrets as parameters -- use context.get_secret() instead
app.run() must be inside if __name__ == "__main__": -- required for deployment
Step 4: Authentication Patterns
How Arcade OAuth Works (Just-in-Time Authorization)
When you declare requires_auth on a tool, Arcade handles the entire OAuth flow automatically:
Agent calls the tool -- Arcade checks if the user has authorized the required scopes
If not authorized -- Arcade initiates the OAuth flow: the user sees a URL, logs in, and grants consent in their browser. The tool is then re-invoked automatically.
If authorized -- the OAuth token is securely injected into Context. The LLM and MCP client never see it.
Token persistence -- Arcade remembers the authorization until the user revokes it. No re-auth on subsequent calls.
Token refresh -- Arcade handles token expiration and refresh transparently.
As a tool developer, you write zero auth code. Just declare requires_auth and call context.get_auth_token_or_empty(). Arcade does the rest.
Built-in providers (Google, Slack, GitHub, Reddit) work out of the box -- Arcade provides default OAuth apps so you don't need to register your own. For other services, use OAuth2(id="provider-id", scopes=[...]) with credentials configured in the Arcade Dashboard.
OAuth Code Pattern
@tool(
requires_auth=Google(
scopes=["https://www.googleapis.com/auth/gmail.readonly"]
)
)asyncdefmy_oauth_tool(
context: Context,
query: Annotated[str, "Search query"],
) -> Annotated[dict, "Search results"]:
"""Search for items using the service API."""
token = context.get_auth_token_or_empty()
asyncwith httpx.AsyncClient() as client:
response = await client.get(
"https://api.service.com/search",
headers={"Authorization": f"Bearer {token}"},
params={"q": query},
)
response.raise_for_status()
return response.json()
Available OAuth providers and import paths:
Provider
Import
Usage
Google
from arcade_mcp_server.auth import Google
Google(scopes=["..."])
Slack
from arcade_mcp_server.auth import Slack
Slack(scopes=["..."])
GitHub
from arcade_mcp_server.auth import GitHub
GitHub(scopes=["..."])
Reddit
from arcade_mcp_server.auth import Reddit
Reddit(scopes=["..."])
Custom
from arcade_mcp_server.auth import OAuth2
OAuth2(id="provider-id", scopes=["..."])
Secrets / API Key Pattern
@tool(requires_secrets=["SERVICE_API_KEY", "ACCOUNT_ID"])asyncdefmy_secret_tool(
context: Context,
item_id: Annotated[str, "The item ID to retrieve"],
) -> Annotated[dict, "Item details"]:
"""Retrieve an item by ID from the service."""
api_key = context.get_secret("SERVICE_API_KEY")
account_id = context.get_secret("ACCOUNT_ID")
asyncwith httpx.AsyncClient() as client:
response = await client.get(
f"https://api.service.com/v1/accounts/{account_id}/items/{item_id}",
headers={"X-Api-Key": api_key},
)
response.raise_for_status()
return response.json()
Hybrid Pattern (OAuth + Secrets)
@tool(
requires_auth=GitHub(scopes=["repo"]),
requires_secrets=["GITHUB_SERVER_URL"],
)asyncdefmy_hybrid_tool(context: Context, ...) -> Annotated[dict, "..."]:
"""Tool needing both user auth and server config."""
token = context.get_auth_token_or_empty()
server_url = context.get_secret("GITHUB_SERVER_URL")
...
Step 5: Apply Quality Patterns
Apply these patterns for production quality. For the full patterns reference, read patterns-reference.md.
Constrained Inputs -- use Enums instead of free-form strings
from enum import Enum
classSortOrder(str, Enum):
ASCENDING = "ascending"
DESCENDING = "descending"classContentType(str, Enum):
PLAIN = "plain"
HTML = "html"
Smart Defaults with Bounds Clamping
MIN_RESULTS = 1
MAX_RESULTS = 50
DEFAULT_RESULTS = 10@tool(...)asyncdeflist_items(
context: Context,
max_results: Annotated[
int, f"Number of items to return (Min {MIN_RESULTS}, Max {MAX_RESULTS})"
] = DEFAULT_RESULTS,
) -> Annotated[ListItemsOutput, "..."]:
"""List items from the service."""
max_results = min(max(max_results, MIN_RESULTS), MAX_RESULTS)
...
Use cursor-based pagination (not page numbers). Accept page_token: Annotated[str | None, "..."] = None and return next_page_token in the output.
Step 6: Assemble the Server
import sys
from typing import cast
from arcade_mcp_server import MCPApp
from arcade_mcp_server.mcp_app import TransportType
import my_server
app = MCPApp(
name="MyServer",
version="1.0.0",
instructions="Use this server to interact with ServiceX to manage items and workflows.",
)
app.add_tools_from_module(my_server)
defmain() -> None:
transport = sys.argv[1] iflen(sys.argv) > 1else"stdio"
app.run(transport=cast(TransportType, transport), host="127.0.0.1", port=8000)
if __name__ == "__main__":
main()
Alternatives: app.add_tool(fn) for individual tools, or @app.tool for inline definitions in simple servers.
Add SDK-specific deps as needed (e.g., google-api-python-client, slack_sdk). See templates.md for full pyproject.toml examples with dev dependencies.
For local secrets, create a .env file alongside server.py (see templates.md for examples).
Step 8: Test Locally
# stdio transport -- supports auth + secrets locally
uv run src/my_server/server.py stdio
# HTTP transport -- view docs at http://127.0.0.1:8000/docs
uv run src/my_server/server.py http
# Configure your MCP client
arcade configure cursor # Cursor IDE
arcade configure claude # Claude Desktop
arcade configure vscode # VS Code
stdio supports full auth and secrets locally. HTTP transport does NOT support tool-level auth/secrets locally -- use arcade deploy for that.
Step 9: Deploy to Arcade Cloud
Set secrets for production
arcade secret set SERVICE_API_KEY="production-key"
arcade secret set ACCOUNT_ID="production-account"