Files
fastmcp-feedback/examples/demo_server.py
T
rsp2k ddf23d10e4
Tests / test (==2.12.2) (push) Canceled after 0s
Tests / test (==2.14.3) (push) Canceled after 0s
Tests / test (>=4,<5) (push) Canceled after 0s
Tests / canary (push) Canceled after 0s
Support FastMCP 4 and fix silent composition failure
compose_feedback_services() called import_server() without awaiting it.
That method became async in FastMCP 2.14, so on the version this package
was built against it composed nothing while logging success. FastMCP 4
removed it outright. Use mount(), which is sync on 2.12 through 4.x.

The test suite could not start (pytest-html hook, plugin not installed),
so nothing caught this. Add pytest-html, bring examples/ over from the docs
repo since the integration tests import them, and fix the tests that had
drifted from the code. 407 tests pass on FastMCP 2.12.2, 2.14.3 and 4.0.10.

- Declare fastmcp>=2.12.2,<5; CI matrix covers each family plus a
  non-blocking canary on the newest FastMCP
- Add a composition test with real servers; the mocked ones could not
  see a missing await
- Rewrite the README against the actual API and execute its examples
- Ignore ruff UP042: StrEnum would change stored enum formatting
- Version 2026.09.27
2026-09-27 06:09:30 -06:00

403 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
FastMCP Feedback Demo Server
A comprehensive demonstration server that showcases all features of the
FastMCP Feedback system including:
- Complete feedback collection workflow
- Privacy-compliant analytics
- Multi-database support
- Performance monitoring
- Server composition patterns
Usage:
# Run the demo server
python examples/demo_server.py
# Or use uv
uv run examples/demo_server.py
# Connect from Claude Desktop with this MCP server configuration:
# {
# "feedback-demo": {
# "command": "uv",
# "args": ["run", "examples/demo_server.py"]
# }
# }
"""
import asyncio
import logging
import sys
import tempfile
from datetime import UTC, datetime
from pathlib import Path
# Add the src directory to the path for development
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from fastmcp import Client, FastMCP
from fastmcp_feedback import (
FeedbackDatabase,
FeedbackInsights,
add_feedback_tools,
check_dependencies,
get_version_info,
)
from fastmcp_feedback.feedback import (
create_analytics_server,
create_management_server,
create_submission_server,
)
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[logging.StreamHandler(), logging.FileHandler("feedback_demo.log")],
)
logger = logging.getLogger(__name__)
def create_temp_database() -> str:
"""Create a temporary SQLite database for the demo."""
temp_dir = tempfile.mkdtemp(prefix="fastmcp_feedback_demo_")
db_path = Path(temp_dir) / "demo_feedback.db"
return f"sqlite:///{db_path}"
async def setup_demo_data(database: FeedbackDatabase) -> None:
"""Populate the database with sample feedback for demonstration."""
logger.info("Setting up demo data...")
sample_feedback = [
{
"type": "bug",
"title": "Memory leak in analytics processing",
"description": "Analytics worker consumes increasing memory over time, eventually causing OOM errors.",
"submitter": "alice@example.com",
"contact_info": "alice@engineering.example.com",
},
{
"type": "feature",
"title": "Add export functionality for feedback data",
"description": "Users need ability to export feedback data as CSV or JSON for external analysis.",
"submitter": "product_manager",
"contact_info": "pm@example.com",
},
{
"type": "improvement",
"title": "Optimize database query performance",
"description": "List feedback queries are slow with large datasets. Need pagination and indexing improvements.",
"submitter": "performance_team",
},
{
"type": "question",
"title": "Best practices for rate limiting?",
"description": "What are recommended rate limits for feedback submission to prevent abuse?",
"submitter": "security_team",
"contact_info": "security@example.com",
},
{
"type": "bug",
"title": "Unicode handling in feedback titles",
"description": "Emojis and special characters cause encoding errors in feedback titles. 🐛",
"submitter": "qa_engineer",
},
]
try:
await database.initialize()
# Import the model here to avoid circular imports
from fastmcp_feedback.models import Feedback, FeedbackStatus, FeedbackType
async with database.session() as session:
for item in sample_feedback:
feedback = Feedback(
type=FeedbackType(item["type"]),
status=FeedbackStatus.OPEN,
title=item["title"],
description=item["description"],
submitter=item["submitter"],
contact_info=item.get("contact_info"),
created_at=datetime.now(UTC),
updated_at=datetime.now(UTC),
)
session.add(feedback)
session.commit()
logger.info(f"Added {len(sample_feedback)} sample feedback items")
except Exception as e:
logger.error(f"Failed to setup demo data: {e}")
raise
def create_demo_server(database_url: str | None = None) -> FastMCP:
"""Create the main demo server with all feedback tools.
Uses a fresh temporary SQLite database unless database_url is given.
"""
logger.info("🚀 Creating FastMCP Feedback Demo Server")
# Create the main server
server = FastMCP("FastMCP Feedback Demo")
server.description = (
"Comprehensive demonstration of FastMCP Feedback system featuring "
"complete feedback lifecycle, analytics, and multi-database support."
)
# Create temporary database for demo
database_url = database_url or create_temp_database()
logger.info(f"Using demo database: {database_url}")
# Configure insights for demo (privacy-compliant by design)
insights = FeedbackInsights(
enabled=True,
retention_days=30, # Short retention for demo
)
# Add all feedback tools to the main server
add_feedback_tools(
server,
database_url=database_url,
insights=insights,
prefix="", # No prefix for main tools
)
# Add version info tool
@server.tool(description="Get FastMCP Feedback version and dependency information")
def get_system_info() -> dict:
"""Get comprehensive system information."""
version_info = get_version_info()
dependency_status = check_dependencies()
return {
"system": "FastMCP Feedback Demo Server",
"status": "running",
"started_at": datetime.now(UTC).isoformat(),
"versions": version_info,
"dependencies": dependency_status,
"demo_features": [
"Complete feedback lifecycle",
"Privacy-compliant analytics",
"Multi-database support",
"Performance monitoring",
"Sample data included",
],
}
# Add demo data setup tool
@server.tool(description="Populate database with sample feedback data")
async def setup_sample_data() -> dict:
"""Setup sample feedback data for demonstration."""
try:
database = FeedbackDatabase(database_url)
await setup_demo_data(database)
await database.close()
return {
"success": True,
"message": "Sample data created successfully",
"items_added": 5,
"data_includes": [
"Bug reports with technical details",
"Feature requests from product team",
"Performance improvement suggestions",
"Security questions",
"Unicode handling issues",
],
}
except Exception as e:
logger.error(f"Failed to setup sample data: {e}")
return {"success": False, "error": str(e)}
return server
async def create_composed_demo_server() -> FastMCP:
"""Create a demo server using server composition patterns."""
logger.info("🏗️ Creating Composed Demo Server")
# Create main application server
main_server = FastMCP("Composed Feedback System")
main_server.description = "Demonstration of FastMCP server composition patterns"
# Create temporary databases for different services
public_db = create_temp_database()
admin_db = create_temp_database()
analytics_db = create_temp_database()
# Create specialized servers
public_server = create_submission_server(
"Public Feedback Portal",
database_url=public_db,
insights=FeedbackInsights(enabled=True, retention_days=90),
)
admin_server = create_management_server(
"Admin Management Panel",
database_url=admin_db,
insights=FeedbackInsights(enabled=True, retention_days=365),
)
analytics_server = create_analytics_server(
"Analytics Dashboard",
database_url=analytics_db,
insights=FeedbackInsights(enabled=True, retention_days=30),
)
# Compose servers with prefixes
main_server.mount(public_server, "public")
main_server.mount(admin_server, "admin")
main_server.mount(analytics_server, "analytics")
# Add composition info tool
@main_server.tool(description="Get information about server composition")
def get_composition_info() -> dict:
"""Get information about the composed server architecture."""
return {
"architecture": "Multi-server composition",
"servers": {
"public": {
"purpose": "Feedback submission for end users",
"tools": ["submit_feedback"],
"database": "isolated",
},
"admin": {
"purpose": "Management and moderation",
"tools": [
"update_feedback_status",
"delete_feedback",
"bulk_operations",
],
"database": "isolated",
},
"analytics": {
"purpose": "Reporting and insights",
"tools": ["generate_reports", "export_data", "usage_statistics"],
"database": "isolated",
},
},
"benefits": [
"Separation of concerns",
"Independent scaling",
"Role-based access control",
"Database isolation",
"Service modularity",
],
}
return main_server
async def run_demo_tests(server: FastMCP) -> dict:
"""Run basic functionality tests on the demo server."""
logger.info("🧪 Running demo functionality tests...")
try:
# Go through a client so this works the same on every FastMCP version
async with Client(server) as client:
tool_names = [tool.name for tool in await client.list_tools()]
system_info = None
if "get_system_info" in tool_names:
result = await client.call_tool("get_system_info", {})
system_info = result.structured_content
setup_result = None
if "setup_sample_data" in tool_names:
result = await client.call_tool("setup_sample_data", {})
setup_result = result.structured_content
return {
"test_status": "passed",
"tools_available": len(tool_names),
"tool_names": tool_names,
"system_info": system_info,
"sample_data_setup": setup_result,
"test_timestamp": datetime.now(UTC).isoformat(),
}
except Exception as e:
logger.error(f"Demo tests failed: {e}")
return {
"test_status": "failed",
"error": str(e),
"test_timestamp": datetime.now(UTC).isoformat(),
}
def print_demo_banner():
"""Print an informative banner for the demo server."""
banner = """
╔══════════════════════════════════════════════════════════════════╗
║ 🚀 FastMCP Feedback Demo Server ║
║ ║
║ Features Demonstrated: ║
║ • Complete feedback collection system ║
║ • Privacy-compliant analytics (GDPR-ready) ║
║ • Multi-database support (SQLite, PostgreSQL) ║
║ • Thread-safe operations with 97% test coverage ║
║ • Server composition patterns ║
║ • Performance monitoring and health checks ║
║ ║
║ Available Tools: ║
║ 📝 submit_feedback - Submit bug reports & feature requests║
║ 📋 list_feedback - Query and filter feedback ║
║ 📊 get_feedback_statistics - Analytics and metrics ║
║ 🔧 update_feedback_status - Change status (admin) ║
║ 🗑️ delete_feedback - Remove feedback items ║
║ ℹ️ get_system_info - Version and dependency info ║
║ 🎯 setup_sample_data - Populate with demo data ║
║ ║
║ Usage: Connect this as an MCP server in Claude Desktop ║
║ Configuration: See comments in this file ║
╚══════════════════════════════════════════════════════════════════╝
"""
print(banner)
async def main():
"""Main entry point for the demo server."""
print_demo_banner()
try:
# Check dependencies first
deps = check_dependencies()
if not deps["all_available"]:
logger.error("Missing required dependencies!")
for dep, info in deps["details"].items():
if not info["available"]:
logger.error(f" Missing: {dep} {info['required']}")
sys.exit(1)
logger.info("All dependencies satisfied ✅")
# Create the main demo server
server = create_demo_server()
# Run functionality tests
test_results = await run_demo_tests(server)
logger.info(f"Demo tests: {test_results['test_status']}")
if test_results["test_status"] == "passed":
logger.info(f"Server ready with {test_results['tools_available']} tools")
# Start the FastMCP server
logger.info("🎬 Starting FastMCP Feedback Demo Server...")
await server.run()
except KeyboardInterrupt:
logger.info("Demo server stopped by user")
except Exception as e:
logger.error(f"Demo server failed: {e}")
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())