MCP Quickstart
This quickstart gets a minimal MCP-enabled ActingWeb server running with FastAPI, adds one MCP tool and one MCP prompt using the correct decorators, and shows how to test with simple JSON‑RPC calls.
Requirements
Python 3.11+
Install extras for FastAPI and MCP:
# pip
pip install 'actingweb[fastapi,mcp]'
# or with Poetry
poetry add actingweb -E fastapi -E mcp
Minimal App
# app_mcp.py
import os
from datetime import datetime
from fastapi import FastAPI
from actingweb.interface import ActingWebApp, ActorInterface
from actingweb.mcp import mcp_tool, mcp_prompt
api = FastAPI(title="My MCP Server")
# Configure ActingWeb
aw = (
ActingWebApp(
aw_type="urn:actingweb:example.com:mcp",
database="dynamodb",
fqdn=os.getenv("APP_HOST_FQDN", "localhost:5000"),
)
.with_web_ui(True)
# Configure OAuth2 for real auth in production (example only)
# .with_oauth(client_id=os.getenv("OAUTH_CLIENT_ID"), client_secret=os.getenv("OAUTH_CLIENT_SECRET"))
)
# Lifecycle example
@aw.lifecycle_hook("actor_created")
def init_actor(actor: ActorInterface, **kwargs):
actor.properties.email = actor.creator
actor.properties.created_at = datetime.now().isoformat()
# MCP tool: attach to an action hook and expose with @mcp_tool
@aw.action_hook("create_note")
@mcp_tool(description="Create a new note for this actor")
def create_note_tool(actor: ActorInterface, action_name: str, data: dict):
title = data.get("title", "Untitled")
content = data.get("content", "")
key = f"note_{datetime.now().isoformat()}"
actor.properties[key] = {"title": title, "content": content}
return {"status": "ok", "note": key}
# MCP prompt: attach to a method hook and expose with @mcp_prompt
@aw.method_hook("analyze_notes")
@mcp_prompt(description="Summarize notes for this actor")
def analyze_notes_prompt(actor: ActorInterface, method_name: str, params: dict):
notes = [v for k, v in actor.properties.items() if k.startswith("note_")]
return f"Found {len(notes)} notes. Titles: " + ", ".join(n.get("title", "Untitled") for n in notes)
# Integrate all ActingWeb routes on FastAPI app
aw.integrate_fastapi(api)
# Run: uvicorn app_mcp:api --reload --port 5000
Note
Async Hook Support: MCP tools and prompts can be async functions for optimal performance.
FastAPI automatically uses AsyncMCPHandler which executes async hooks natively in the
event loop without thread pool overhead. This enables true concurrent execution and significantly
better performance for I/O-bound operations (database queries, API calls, etc.).
# Async MCP tool - optimal for I/O operations
@aw.action_hook("fetch_external_data")
@mcp_tool(description="Fetch data from external API")
async def fetch_data_tool(actor: ActorInterface, action_name: str, data: dict):
async with aiohttp.ClientSession() as session:
async with session.get(f"https://api.example.com/data/{data['id']}") as resp:
result = await resp.json()
return {"content": [{"type": "text", "text": str(result)}]}
Testing with JSON‑RPC
Call initialize (no auth required):
curl -s http://localhost:5000/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"curl"}}}'
List tools and prompts (requires auth in production; open in dev):
curl -s http://localhost:5000/mcp -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
curl -s http://localhost:5000/mcp -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":3,"method":"prompts/list"}'
Call the tool:
curl -s http://localhost:5000/mcp -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"create_note","arguments":{"title":"Hello","content":"World"}}}'
Tool Safety Annotations
IMPORTANT: For production MCP servers, always add safety annotations to your tools. ChatGPT and other MCP clients use these to evaluate server safety:
@aw.action_hook("search")
@mcp_tool(
description="Search your notes",
annotations={
"readOnlyHint": True, # Only reads, never modifies
"destructiveHint": False, # Doesn't delete data
}
)
def search(actor, action_name, data):
pass
@aw.action_hook("delete_note")
@mcp_tool(
description="Delete a note permanently",
annotations={
"destructiveHint": True, # Destroys data - needs confirmation
"readOnlyHint": False,
}
)
def delete_note(actor, action_name, data):
pass
Key annotations:
destructiveHint: True- Tool can permanently delete/destroy datareadOnlyHint: True- Tool only reads, never modifies dataidempotentHint: True- Same input always gives same resultopenWorldHint: True- Tool accesses external services
See the MCP Applications Guide for complete documentation.
Recommendations
For production, enable OAuth2 with Google/GitHub and ensure /mcp returns 401 with a proper WWW-Authenticate header for unauthenticated clients.
Use the unified access control to filter tools/prompts per trust relationship.