02 - Building Apps
Build and run an MCP server programmatically using the FastAPI-like MCPApp
interface.
Running the Example
- Run HTTP:
python examples/02_building_apps.py
- Run stdio:
python examples/02_building_apps.py stdio
Source Code
#!/usr/bin/env python
"""
02_building_apps.py - Build an MCP server using MCPApp
This example shows how to build and run an MCP server programmatically
using `MCPApp` instead of relying on the arcade_mcp_server CLI.
To run (HTTP transport by default):
python 02_building_apps.py
To run with stdio transport (for Claude Desktop):
python 02_building_apps.py stdio
"""
import sys
from typing import Annotated
from arcade_mcp_server import Context, MCPApp
# Create the MCP application
app = MCPApp(
name="my_mcp_server", version="0.1.0", instructions="Example MCP server built with MCPApp"
)
@app.tool
def greet(
name: Annotated[str, "Name of the person to greet"],
) -> Annotated[str, "Greeting message"]:
"""Return a friendly greeting.
Parameters:
name: Person's name
Returns:
Greeting message.
"""
return f"Hello, {name}!"
@app.tool
async def whoami(context: Context) -> Annotated[dict, "Basic server and user information"]:
"""Return basic information from the tool context.
Returns:
Dictionary with `user_id` and whether MCP features are available.
"""
user_id = context.user_id or "anonymous"
if context:
await context.log.info(f"whoami called by: {user_id}")
secret_keys = [secret.key for secret in context.secrets] if context.secrets else []
return {
"user_id": user_id,
"secret_keys": secret_keys,
}
if __name__ == "__main__":
# Check if stdio transport was requested
if len(sys.argv) > 1 and sys.argv[1] == "stdio":
app.run(transport="stdio")
else:
# Default to HTTP transport
app.run(host="127.0.0.1", port=8000)
MCPApp Features
1. Creating an App
from arcade_mcp_server import MCPApp
app = MCPApp(
name="my_server",
version="1.0.0",
title="My MCP Server",
instructions="This server provides utility tools",
log_level="INFO"
)
2. Adding Tools
Method 1: Direct Tool Definition
Use the @app.tool
decorator to define tools directly:
@app.tool
def my_tool(param: Annotated[str, "Description"]) -> str:
"""Tool description."""
return f"Result: {param}"
Method 2: Importing Tools from Files
Import tools from other files and add them explicitly:
from my_tools import calculate, process_data
# Add imported tools to the app
app.add_tool(calculate)
app.add_tool(process_data)
Method 3: Importing from Packages
Import tools from Arcade packages:
This approach gives you explicit control over which tools are loaded and allows for modular organization.
For a comprehensive example of tool organization, see 06_tool_organization.md.
3. Running the Server
# Default HTTP transport
app.run()
# Specify options
app.run(
host="0.0.0.0",
port=8080,
reload=True, # Auto-reload on code changes
transport="http"
)
# For stdio transport (Claude Desktop)
app.run(transport="stdio")
4. Using Context
Tools can access runtime context:
@app.tool
async def context_aware(context: Context, value: str) -> dict:
"""Tool that uses context features."""
# Access user info
user_id = context.user_id
# Use MCP features if available
if context:
await context.log.info(f"Processing for user: {user_id}")
# Access secrets
secret_keys = list(context.secrets.keys())
return {
"user": user_id,
"value": value,
"available_secrets": secret_keys
}
Key Concepts
- FastAPI-like Interface: Familiar decorator-based API design
- Programmatic Control: Build servers without CLI dependency
- Transport Flexibility: Support for both HTTP and stdio transports
- Context Integration: Access to user info, logging, and secrets
- Development Features: Hot reload, debug logging, and more