Compare commits

..
Author SHA1 Message Date
rsp2k ddf23d10e4 Support FastMCP 4 and fix silent composition failure
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
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
rsp2k 736b122533 fix: Add missing re-exports to mixins/base.py
The mixin modules import shared types from base.py, but they
weren't being re-exported. Add __all__ with explicit re-exports for:
- Feedback, FeedbackStatus, FeedbackType (from models)
- feedback_to_dict (from models)
- mcp_tool (from fastmcp)

Bump version to 2026.01.12.1
2026-01-12 13:18:34 -07:00
23 changed files with 4453 additions and 167 deletions
+67
View File
@@ -0,0 +1,67 @@
name: Tests
# fastmcp-feedback declares fastmcp>=2.12.2,<5, so every version family in
# that range gets the full suite. A user on PyPI gets the newest FastMCP the
# range allows, not what uv.lock pins, which is how 2026.01.12.1 shipped with
# composition broken on 2.14 and 4.x without anyone noticing.
on:
push:
pull_request:
workflow_dispatch:
schedule:
# Weekly, so a new FastMCP release is caught even when nobody pushes
- cron: '17 9 * * 1'
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
fastmcp: ['==2.12.2', '==2.14.3', '>=4,<5']
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with:
python-version: '3.13'
- name: Install against fastmcp${{ matrix.fastmcp }}
run: |
uv venv
uv pip install -e ".[dev]" "fastmcp${{ matrix.fastmcp }}"
uv pip show fastmcp | grep -i '^version'
- name: Lint
run: uv run --no-sync ruff check src/ examples/ tests/_compat.py
- name: Test
run: uv run --no-sync pytest -q --no-cov
# Newest FastMCP with no ceiling. Allowed to fail: it exists to say
# "a new major version is out and here is what breaks" before users do.
canary:
runs-on: ubuntu-latest
timeout-minutes: 15
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with:
python-version: '3.13'
- name: Install newest FastMCP, ignoring our ceiling
run: |
uv venv
uv pip install fastmcp sqlalchemy pydantic \
pytest pytest-asyncio pytest-cov pytest-mock pytest-html
uv pip install --no-deps -e .
uv pip show fastmcp | grep -i '^version'
- name: Test
run: uv run --no-sync pytest -q --no-cov
+46
View File
@@ -5,6 +5,52 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Calendar Versioning](https://calver.org/) (YYYY.MM.DD).
## [2026.09.27] - 2026-09-27
FastMCP 4 support, and a composition bug that affected every earlier release.
### Fixed
- `compose_feedback_services()` composed nothing on FastMCP 2.14. It called
`import_server()`, which became async in 2.14, without awaiting it, then
logged success anyway. On FastMCP 4, where `import_server()` no longer
exists, it raised `AttributeError`. It now uses `mount()`, which is
synchronous and available on every supported version.
- The test suite could not start (a pytest-html hook with pytest-html not
installed), so it had not run since the package was split out. It runs
again: 407 tests.
### Changed
- Dependency is now `fastmcp>=2.12.2,<5`. Earlier releases had no ceiling,
so a fresh install picked up FastMCP 4 untested.
- `examples/` moved into this repository and ships in the sdist.
`create_demo_server()` accepts an optional `database_url`. Examples list
and call tools through `fastmcp.Client`, which works on 2.x and 4.x.
### Added
- CI (Gitea Actions) runs the suite against FastMCP 2.12.2, 2.14.3 and 4.x
on every push and weekly, plus a non-blocking job against the newest
FastMCP with no ceiling.
- A composition test using real servers instead of mocks.
### Documentation
- README rewritten against the code and every Python example executed.
Previous docs described things that do not exist: four tools (there are
five, including `delete_feedback`), `get_feedback_stats` (it is
`get_feedback_statistics`), `InsightsMixin`, an `enable_insights`
parameter, feedback types `bug_report`/`feature_request`/`general` (they
are `bug`/`feature`/`improvement`/`question`), and a
`new -> reviewing -> planned` workflow (it is
`open -> in_progress -> resolved -> closed`).
- The default database is in-memory SQLite (`sqlite:///:memory:`), not
`sqlite:///feedback.db` as previously documented. Pass `database_url` to
keep feedback across restarts.
## [2026.01.12.1] - 2026-01-12
### Fixed
- `ImportError` on install: `mixins/base.py` did not re-export the names
the other mixin modules import from it.
## [2026.01.12] - 2026-01-12
### Added
+81 -52
View File
@@ -67,59 +67,77 @@ from fastmcp import FastMCP
from fastmcp_feedback import add_feedback_tools
app = FastMCP("My Server")
add_feedback_tools(app) # Adds 4 feedback tools instantly!
add_feedback_tools(app, database_url="sqlite:///feedback.db")
# That's it! Your server now has:
# - submit_feedback: Collect bug reports, feature requests, general feedback
# - list_feedback: Retrieve and filter feedback items
# - get_feedback_stats: Aggregate statistics
# - update_feedback_status: Workflow management (new → reviewing → resolved)
# Your server now has five tools:
# - submit_feedback: bug reports, feature requests, improvements, questions
# - list_feedback: page through feedback, filtered by type or status
# - get_feedback_statistics: counts by type and status, plus the last 7 days
# - update_feedback_status: move an item through open -> in_progress -> resolved -> closed
# - delete_feedback: remove an item
```
### With Custom Database
Pass a `database_url`. Without one the tools use an in-memory SQLite
database, which is handy for tests but loses everything when the process exits.
### PostgreSQL
```python
# SQLite with custom path
add_feedback_tools(app, database_url="sqlite:///./data/feedback.db")
# PostgreSQL for production
add_feedback_tools(app, database_url="postgresql://user:pass@localhost/mydb")
add_feedback_tools(app, database_url="postgresql://user:pass@db.example.com/feedback")
```
### With Analytics Enabled
Install the driver with `uv add "fastmcp-feedback[postgresql]"`.
### Prefixed Tool Names
```python
add_feedback_tools(app, enable_insights=True)
# Enables privacy-compliant usage tracking
add_feedback_tools(app, database_url="sqlite:///feedback.db", prefix="support")
# support_submit_feedback, support_list_feedback, ...
```
The prefix and tool name are joined with `separator` (default `_`), so leave
the trailing underscore off the prefix.
### Usage Analytics
```python
from fastmcp_feedback import FeedbackInsights
add_feedback_tools(
app,
database_url="sqlite:///feedback.db",
insights=FeedbackInsights(enabled=True),
)
```
Analytics are off unless you pass `FeedbackInsights(enabled=True)` or set
`FEEDBACK_INSIGHTS_ENABLED=true`. When enabled they record tool usage and
submission metadata (type, lengths, timing), never feedback text or contact details.
## Advanced Usage
### Mixin Architecture
For fine-grained control over which tools are exposed:
Pick only the tool groups you want to expose:
```python
from fastmcp_feedback import (
SubmissionMixin,
RetrievalMixin,
ManagementMixin,
RetrievalMixin,
SubmissionMixin,
get_database_session,
)
db_session = get_database_session("sqlite:///feedback.db")
db = get_database_session("sqlite:///feedback.db")
# Only expose submission (public-facing)
submission = SubmissionMixin(db_session)
submission.register_tools(app)
# Public server: submission only
SubmissionMixin(db).register_tools(app)
# Add retrieval with custom prefix
retrieval = RetrievalMixin(db_session)
retrieval.register_tools(app, prefix="analytics_")
# Reporting tools under a prefix: analytics_list_feedback, ...
RetrievalMixin(db).register_tools(app, prefix="analytics")
# Admin-only management tools
management = ManagementMixin(db_session)
management.register_tools(admin_app, prefix="admin_")
# Admin server gets the workflow tools
ManagementMixin(db).register_tools(admin_app, prefix="admin")
```
### Available Mixins
@@ -127,18 +145,16 @@ management.register_tools(admin_app, prefix="admin_")
| Mixin | Tools | Use Case |
|-------|-------|----------|
| `SubmissionMixin` | `submit_feedback` | Public feedback collection |
| `RetrievalMixin` | `list_feedback`, `get_feedback_stats` | Dashboards, reporting |
| `ManagementMixin` | `update_feedback_status` | Admin workflow |
| `InsightsMixin` | `record_insight`, `get_insights_summary` | Analytics |
| `RetrievalMixin` | `list_feedback`, `get_feedback_statistics` | Dashboards, reporting |
| `ManagementMixin` | `update_feedback_status`, `delete_feedback` | Admin workflow |
### Multi-Tenant Pattern
```python
# Each tenant gets isolated database
def get_tenant_tools(tenant_id: str):
session = get_database_session(f"sqlite:///data/{tenant_id}.db")
submission = SubmissionMixin(session)
submission.register_tools(app, prefix=f"tenant_{tenant_id}_")
# Each tenant gets its own database
def add_tenant_tools(tenant_id: str):
db = get_database_session(f"sqlite:///data/{tenant_id}.db")
SubmissionMixin(db).register_tools(app, prefix=f"tenant_{tenant_id}")
```
### Server Composition
@@ -146,42 +162,55 @@ def get_tenant_tools(tenant_id: str):
```python
from fastmcp_feedback import create_feedback_server
# Create dedicated feedback server
feedback_server = create_feedback_server("Feedback API")
feedback_server = create_feedback_server(
"Feedback API", database_url="sqlite:///feedback.db"
)
# Compose into main application
main_app.import_server(feedback_server, prefix="feedback_")
# Tools appear on main_app as feedback_submit_feedback, feedback_list_feedback, ...
main_app.mount(feedback_server, "feedback")
```
`mount()` works on every supported FastMCP version. `import_server()` was
removed in FastMCP 4.
## Compatibility
Tested against FastMCP 2.12, 2.14 and 4.0 on every commit. The dependency is
declared as `fastmcp>=2.12.2,<5`, and the ceiling moves up once a new major
version passes the suite.
## API Reference
### `add_feedback_tools(app, database_url=None, enable_insights=False)`
### `add_feedback_tools(mcp, database_url=None, insights=None, prefix="", separator="_")`
Add all feedback tools to a FastMCP server.
Add all five feedback tools to a FastMCP server.
**Parameters:**
- `app`: FastMCP server instance
- `database_url`: Database connection string (default: `sqlite:///feedback.db`)
- `enable_insights`: Enable privacy-compliant analytics (default: `False`)
- `mcp`: FastMCP server instance
- `database_url`: SQLAlchemy URL. Defaults to in-memory SQLite (`sqlite:///:memory:`)
- `insights`: a `FeedbackInsights` instance. Defaults to one with analytics disabled
- `prefix`: prepended to every tool name
- `separator`: placed between `prefix` and the tool name
### Feedback Types
- `bug_report` - Bug reports with severity levels
- `feature_request` - Feature suggestions with priority
- `general` - General feedback and comments
- `bug` - something is broken
- `feature` - a new capability
- `improvement` - make an existing capability better
- `question` - unclear behavior or usage
### Status Workflow
`new` → `reviewing` → `planned` → `in_progress` → `resolved` / `wont_fix`
`open` → `in_progress` → `resolved` → `closed`
## Documentation
Full documentation: https://fastmcp-feedback.l.supported.systems
- [Quick Start Guide](https://fastmcp-feedback.l.supported.systems/guides/quickstart/)
- [Architecture Overview](https://fastmcp-feedback.l.supported.systems/reference/architecture/)
- [Quick Start Tutorial](https://fastmcp-feedback.l.supported.systems/tutorials/quickstart/)
- [Architecture](https://fastmcp-feedback.l.supported.systems/explanation/architecture/)
- [API Reference](https://fastmcp-feedback.l.supported.systems/reference/api/)
- [Examples](https://fastmcp-feedback.l.supported.systems/examples/)
- [Integration Patterns](https://fastmcp-feedback.l.supported.systems/how-to/integration-patterns/)
## Examples
+318
View File
@@ -0,0 +1,318 @@
# FastMCP Feedback Examples
This directory contains comprehensive examples demonstrating all features of the FastMCP Feedback system.
## 🚀 Demo Server
### `demo_server.py`
**Complete demonstration server showcasing all features**
**Features Demonstrated:**
- ✅ Complete feedback collection workflow
- ✅ Privacy-compliant analytics (GDPR-ready)
- ✅ Multi-database support (SQLite, PostgreSQL, MySQL)
- ✅ Thread-safe operations with 97% test coverage
- ✅ Server composition patterns
- ✅ Performance monitoring and health checks
- ✅ Sample data for testing
**Available Tools:**
- `submit_feedback` - Submit bug reports, feature requests, improvements, questions
- `list_feedback` - Query and filter feedback with pagination
- `get_feedback_statistics` - Analytics and aggregate metrics
- `update_feedback_status` - Change status (open → in_progress → resolved)
- `delete_feedback` - Remove feedback items
- `get_system_info` - Version and dependency information
- `setup_sample_data` - Populate with realistic demo data
**Usage:**
```bash
# Run the demo server
python examples/demo_server.py
# Or with uv
uv run examples/demo_server.py
```
**Claude Desktop MCP Configuration:**
```json
{
"mcpServers": {
"fastmcp-feedback-demo": {
"command": "uv",
"args": ["run", "examples/demo_server.py"],
"env": {
"LOG_LEVEL": "INFO"
}
}
}
}
```
## 🏗️ Architecture Examples
### `simple_integration.py`
**Minimal integration example - 3 lines of code**
```python
from fastmcp import FastMCP
from fastmcp_feedback import add_feedback_tools
app = FastMCP("My App")
add_feedback_tools(app, database_url="sqlite:///feedback.db")
```
### `advanced_configuration.py`
**Production-ready configuration with all features**
```python
from fastmcp_feedback import (
add_feedback_tools,
FeedbackInsights,
FeedbackDatabase
)
# Configure privacy-compliant analytics
insights = FeedbackInsights(
enabled=True,
retention_days=90,
privacy_compliant=True
)
# Setup database with connection pooling
database = FeedbackDatabase(
"postgresql://user:pass@localhost/feedback",
pool_size=20,
max_overflow=10
)
# Add tools with custom prefix
add_feedback_tools(
app,
database=database,
insights=insights,
prefix="support_"
)
```
### `server_composition.py`
**Multi-server composition patterns**
```python
from fastmcp_feedback.feedback import (
create_submission_server,
create_management_server,
create_analytics_server
)
# Create specialized servers
public_server = create_submission_server("Public Portal")
admin_server = create_management_server("Admin Panel")
analytics = create_analytics_server("Dashboard")
# Compose into main application
main_app = FastMCP("Main Application")
await main_app.import_server(public_server, prefix="public")
await main_app.import_server(admin_server, prefix="admin")
await main_app.import_server(analytics, prefix="stats")
```
## 🎯 Usage Patterns
### Basic Feedback Workflow
1. **Submit Feedback:**
```python
# Submit a bug report
result = await submit_feedback({
"request": {
"type": "bug",
"title": "Login page not responsive on mobile",
"description": "The login form overflows on screens < 768px width",
"submitter": "user_123",
"contact_info": "user@example.com"
}
})
```
2. **List and Filter:**
```python
# Get all open bug reports
bugs = await list_feedback({
"type_filter": "bug",
"status_filter": "open",
"limit": 20
})
```
3. **Update Status:**
```python
# Mark as in progress
await update_feedback_status({
"feedback_id": "123",
"new_status": "in_progress",
"note": "Engineering team investigating"
})
```
4. **Get Analytics:**
```python
# View statistics
stats = await get_feedback_statistics()
# Returns: {"total_count": 45, "by_type": {"BUG": 12, "FEATURE": 20}, ...}
```
## 🔒 Privacy & Security Examples
### Privacy-Compliant Configuration
```python
insights = FeedbackInsights(
enabled=True, # Enable analytics
retention_days=90, # Auto-cleanup after 90 days
privacy_compliant=True, # Enforce GDPR compliance
anonymize_identifiers=True, # Hash user identifiers
exclude_contact_info=True # Don't store in metrics
)
```
### Security Features
```python
# Input validation with Pydantic
from fastmcp_feedback.models import SubmitFeedbackRequest
# SQL injection prevention via SQLAlchemy ORM
# Rate limiting support (extend mixins)
# Authentication ready (add middleware)
```
## 📊 Performance Examples
### Connection Pooling
```python
database = FeedbackDatabase(
"postgresql://localhost/feedback",
pool_size=20, # Base connections
max_overflow=10, # Burst capacity
pool_timeout=30, # Connection timeout
pool_recycle=3600 # Refresh every hour
)
```
### Bulk Operations
```python
# Bulk status updates
result = await bulk_update_status(
feedback_ids=["1", "2", "3", "4", "5"],
new_status="resolved"
)
```
### Performance Monitoring
```python
# Built-in health checks
health = await database.health_check()
if not health:
# Trigger alerts
pass
# Performance metrics
stats = await get_database_stats()
# Returns connection pool status, query times, etc.
```
## 🧪 Testing Examples
### Unit Testing with FastMCP Feedback
```python
import pytest
from fastmcp_feedback import FeedbackDatabase
@pytest.mark.asyncio
async def test_feedback_submission():
# Use in-memory database for testing
db = FeedbackDatabase("sqlite:///:memory:")
await db.initialize()
# Test feedback submission
# ... test code here
await db.close()
```
### Integration Testing
```python
# Test complete workflows
async def test_complete_feedback_lifecycle():
# Submit → List → Update → Verify → Analytics
pass
```
## 🚀 Production Deployment Examples
### Docker Configuration
```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY pyproject.toml .
RUN pip install -e .
COPY src/ src/
COPY examples/ examples/
# Health check
HEALTHCHECK --interval=30s --timeout=10s \
CMD python -c "import asyncio; from examples.health_check import check; asyncio.run(check())"
CMD ["python", "examples/demo_server.py"]
```
### Environment Configuration
```bash
# Database
DATABASE_URL=postgresql://user:pass@db:5432/feedback
# Analytics
INSIGHTS_ENABLED=true
INSIGHTS_RETENTION_DAYS=90
# Performance
CONNECTION_POOL_SIZE=20
MAX_OVERFLOW=10
# Security
RATE_LIMIT_REQUESTS=100
RATE_LIMIT_WINDOW=3600
```
## 📚 Documentation
Each example includes:
- **Comprehensive comments** explaining every feature
- **Error handling** patterns
- **Performance considerations**
- **Security best practices**
- **Testing approaches**
- **Production deployment guidance**
## 🎯 Getting Started
1. **Start with the demo server** - `python examples/demo_server.py`
2. **Connect to Claude Desktop** using the MCP configuration above
3. **Try the tools** - submit feedback, query results, update status
4. **Explore the code** - see how each feature is implemented
5. **Customize for your needs** - adapt patterns to your use case
## 🤝 Contributing Examples
We welcome additional examples! Please:
- Include comprehensive comments
- Add error handling
- Provide usage instructions
- Update this README
- Add tests if applicable
---
**These examples demonstrate production-ready FastMCP Feedback integration with 97% test coverage and enterprise-grade reliability.**
+247
View File
@@ -0,0 +1,247 @@
# FastMCP Feedback Demo Server
**File:** `demo_server.py`
**Purpose:** Comprehensive demonstration of all FastMCP Feedback features
**Lines:** 396 lines of production-ready code
**Test Coverage:** Validated through 396+ comprehensive tests
## 🎯 Overview
The demo server is a complete, production-ready FastMCP server that showcases every feature of the FastMCP Feedback system. It's designed to demonstrate real-world usage patterns and serve as a reference implementation for developers.
## ✨ Features Demonstrated
### Core Feedback System
- **Complete CRUD Operations** - Submit, retrieve, update, delete feedback
- **Type Support** - Bug reports, feature requests, improvements, questions
- **Status Management** - Open → In Progress → Resolved → Closed workflow
- **Rich Metadata** - Timestamps, submitter info, contact details
### Advanced Capabilities
- **Privacy-Compliant Analytics** - GDPR-ready insights collection
- **Multi-Database Support** - SQLite for development, PostgreSQL for production
- **Thread-Safe Operations** - Concurrent request handling
- **Performance Monitoring** - Health checks and dependency validation
- **Server Composition** - Multiple specialized server types
### Data Integrity
- **Input Validation** - Pydantic models with comprehensive validation
- **Unicode Support** - Emojis, special characters, international text
- **Timezone Handling** - UTC-aware timestamp management
- **Whitespace Normalization** - Consistent data cleaning
## 🛠️ Available Tools (7 Total)
| Tool | Description | Use Case |
|------|-------------|----------|
| `submit_feedback` | Submit new feedback items | Users reporting bugs/requesting features |
| `list_feedback` | Query and filter feedback | Viewing feedback with pagination/filtering |
| `get_feedback_statistics` | Analytics and metrics | Dashboard data and reporting |
| `update_feedback_status` | Change feedback status | Admin workflow management |
| `delete_feedback` | Remove feedback items | Content moderation |
| `get_system_info` | Version/dependency info | System diagnostics |
| `setup_sample_data` | Populate demo data | Testing and demonstration |
## 📊 Sample Data
The demo server includes realistic sample data:
```python
sample_feedback = [
{
"type": "bug",
"title": "Memory leak in analytics processing",
"description": "Analytics worker consumes increasing memory over time...",
"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...",
"submitter": "product_manager",
"contact_info": "pm@example.com"
}
# ... 3 more realistic examples
]
```
## 🚀 Usage
### Direct Execution
```bash
# Run with Python
python examples/demo_server.py
# Run with uv (recommended)
uv run examples/demo_server.py
```
### Claude Desktop Integration
```json
{
"mcpServers": {
"fastmcp-feedback-demo": {
"command": "uv",
"args": ["run", "examples/demo_server.py"],
"env": {
"DATABASE_URL": "sqlite:///feedback_demo.db",
"LOG_LEVEL": "INFO"
}
}
}
}
```
### Docker Deployment
```dockerfile
FROM python:3.11-slim
COPY . /app
WORKDIR /app
RUN pip install uv && uv sync
CMD ["uv", "run", "examples/demo_server.py"]
```
## 🔧 Configuration Options
### Environment Variables
- `DATABASE_URL` - Database connection string (default: temporary SQLite)
- `LOG_LEVEL` - Logging verbosity (DEBUG, INFO, WARNING, ERROR)
- `INSIGHTS_ENABLED` - Enable/disable analytics collection
- `RETENTION_DAYS` - Analytics data retention period
### Database Support
- **SQLite** - `sqlite:///path/to/database.db` (development)
- **PostgreSQL** - `postgresql://user:pass@host:port/db` (production)
- **In-Memory** - `sqlite:///:memory:` (testing)
## 📈 Performance Characteristics
### Throughput
- **Concurrent Requests** - Handles 100+ concurrent submissions
- **Database Performance** - Optimized queries with indexing
- **Memory Usage** - Efficient async/await patterns
### Scalability
- **Horizontal Scaling** - Stateless design supports load balancing
- **Database Scaling** - Connection pooling and query optimization
- **Analytics Scaling** - Configurable retention and aggregation
## 🧪 Testing Integration
The demo server is thoroughly tested through:
### Unit Tests
- Tool registration and configuration
- Sample data generation
- Error handling scenarios
### Integration Tests
- End-to-end workflow validation
- Database operations
- Server composition patterns
### Performance Tests
- High-volume operations
- Concurrent request handling
- Memory usage monitoring
## 🎓 Learning Examples
### Basic Feedback Submission
```python
# Submit a bug report
result = await submit_feedback({
"type": "bug",
"title": "Application crashes on startup",
"description": "Detailed error description...",
"submitter": "developer@company.com"
})
```
### Advanced Filtering
```python
# Get all open bug reports from last week
feedback = await list_feedback({
"type_filter": "bug",
"status_filter": "open",
"limit": 50
})
```
### Analytics Query
```python
# Get comprehensive statistics
stats = await get_feedback_statistics()
# Returns: total_count, by_type, by_status, recent_count
```
## 🔍 Architecture Insights
### Server Composition Pattern
```python
# The demo server demonstrates server composition:
main_server = FastMCP("Demo Server")
feedback_server = create_feedback_server("Feedback Service")
main_server.import_server(feedback_server, prefix="feedback_")
```
### Database Abstraction
```python
# Flexible database backend:
database = FeedbackDatabase(database_url)
await database.initialize() # Auto-creates tables
```
### Analytics Integration
```python
# Privacy-compliant analytics:
insights = FeedbackInsights(enabled=True, retention_days=90)
insights.record_metric("feedback_submitted", metadata)
```
## 📚 Related Documentation
- **[Simple Integration](simple_integration.md)** - Minimal 3-line integration
- **[Production Server](mcp_server.md)** - Environment-configured deployment
- **[Testing Guide](../tests/README.md)** - Comprehensive test documentation
- **[Architecture Overview](../docs/architecture.md)** - System design details
## 🤝 Contributing
When modifying the demo server:
1. **Maintain Compatibility** - Ensure all existing tools continue working
2. **Update Documentation** - Keep this file synchronized with code changes
3. **Add Tests** - Include test coverage for new features
4. **Version Compatibility** - Support multiple FastMCP versions where possible
## 🐛 Troubleshooting
### Common Issues
**Database Errors**
```bash
# Check database permissions
ls -la feedback_demo.db
# Reset database
rm feedback_demo.db && python examples/demo_server.py
```
**Import Errors**
```bash
# Ensure development installation
uv sync
# Check Python path
python -c "import fastmcp_feedback; print('OK')"
```
**Performance Issues**
```bash
# Enable debug logging
LOG_LEVEL=DEBUG python examples/demo_server.py
# Monitor memory usage
ps aux | grep demo_server
```
This demo server represents the gold standard for FastMCP Feedback integration, demonstrating every feature with production-ready code patterns and comprehensive error handling.
+402
View File
@@ -0,0 +1,402 @@
#!/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())
+396
View File
@@ -0,0 +1,396 @@
# FastMCP Feedback Production Server
**File:** `mcp_server.py`
**Purpose:** Production-ready MCP server with environment configuration
**Lines:** 89 lines of deployment-focused code
**Target:** Production and staging environments
## 🎯 Overview
This is a production-ready MCP server launcher designed for real-world deployments. It demonstrates environment-based configuration, proper logging, and integration patterns suitable for staging and production environments.
## ✨ Production Features
### Environment Configuration
- **Database URL** - Configurable via `DATABASE_URL` environment variable
- **Insights Control** - Enable/disable analytics via `INSIGHTS_ENABLED`
- **Logging Levels** - Configurable via `LOG_LEVEL` (DEBUG, INFO, WARNING, ERROR)
- **Graceful Defaults** - Sensible fallbacks for missing configuration
### Security Considerations
- **No Hardcoded Secrets** - All sensitive data via environment variables
- **Input Validation** - Comprehensive Pydantic validation
- **Error Handling** - Graceful failure without exposing internals
- **Audit Logging** - Configurable logging for compliance
### Deployment Ready
- **Docker Compatible** - Environment variable based configuration
- **Kubernetes Ready** - ConfigMap and Secret integration
- **Load Balancer Friendly** - Stateless design
- **Health Checks** - Built-in system info endpoint
## 🚀 Usage
### Local Development
```bash
# Run with default settings
python examples/mcp_server.py
# Run with uv (recommended)
uv run examples/mcp_server.py
# Run with custom database
DATABASE_URL="postgresql://localhost/feedback" uv run examples/mcp_server.py
```
### Environment Variables
```bash
# Required/Optional Configuration
export DATABASE_URL="postgresql://user:pass@host:5432/feedback_prod"
export INSIGHTS_ENABLED="true"
export LOG_LEVEL="INFO"
export RETENTION_DAYS="90"
# Run the server
uv run examples/mcp_server.py
```
## 🔧 Configuration Options
### Database Configuration
```bash
# SQLite (development/testing)
DATABASE_URL="sqlite:///feedback.db"
# PostgreSQL (production)
DATABASE_URL="postgresql://user:password@host:5432/database"
# PostgreSQL with SSL (production)
DATABASE_URL="postgresql://user:pass@host:5432/db?sslmode=require"
# Environment-specific databases
DATABASE_URL_DEV="sqlite:///feedback_dev.db"
DATABASE_URL_STAGING="postgresql://staging_user:pass@staging.db/feedback"
DATABASE_URL_PROD="postgresql://prod_user:pass@prod.db/feedback"
```
### Analytics Configuration
```bash
# Enable analytics with retention
INSIGHTS_ENABLED="true"
RETENTION_DAYS="90"
# Disable analytics (GDPR compliance)
INSIGHTS_ENABLED="false"
# Different retention by environment
RETENTION_DAYS_DEV="7"
RETENTION_DAYS_STAGING="30"
RETENTION_DAYS_PROD="365"
```
### Logging Configuration
```bash
# Production logging
LOG_LEVEL="INFO"
LOG_FORMAT="json" # For log aggregation
# Development logging
LOG_LEVEL="DEBUG"
LOG_FORMAT="standard"
# Disable logging (not recommended)
LOG_LEVEL="CRITICAL"
```
## 🐳 Docker Deployment
### Dockerfile
```dockerfile
FROM python:3.11-slim
# Set working directory
WORKDIR /app
# Install uv for faster dependency management
RUN pip install uv
# Copy dependency files
COPY pyproject.toml uv.lock ./
# Install dependencies
RUN uv sync --frozen
# Copy application code
COPY . .
# Set environment variables
ENV DATABASE_URL="postgresql://postgres:password@db:5432/feedback"
ENV INSIGHTS_ENABLED="true"
ENV LOG_LEVEL="INFO"
# Expose port (if needed)
EXPOSE 8000
# Run the MCP server
CMD ["uv", "run", "examples/mcp_server.py"]
```
### Docker Compose
```yaml
version: '3.8'
services:
feedback-server:
build: .
environment:
- DATABASE_URL=postgresql://postgres:password@db:5432/feedback
- INSIGHTS_ENABLED=true
- LOG_LEVEL=INFO
- RETENTION_DAYS=90
depends_on:
- db
restart: unless-stopped
db:
image: postgres:15
environment:
- POSTGRES_DB=feedback
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
volumes:
postgres_data:
```
## ☸️ Kubernetes Deployment
### ConfigMap
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: feedback-config
data:
LOG_LEVEL: "INFO"
INSIGHTS_ENABLED: "true"
RETENTION_DAYS: "90"
```
### Secret
```yaml
apiVersion: v1
kind: Secret
metadata:
name: feedback-secrets
type: Opaque
stringData:
DATABASE_URL: "postgresql://user:password@postgres-service:5432/feedback"
```
### Deployment
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: feedback-server
spec:
replicas: 3
selector:
matchLabels:
app: feedback-server
template:
metadata:
labels:
app: feedback-server
spec:
containers:
- name: feedback-server
image: fastmcp-feedback:latest
ports:
- containerPort: 8000
envFrom:
- configMapRef:
name: feedback-config
- secretRef:
name: feedback-secrets
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
```
## 🎛️ Claude Desktop Configuration
### Development
```json
{
"mcpServers": {
"fastmcp-feedback-dev": {
"command": "uv",
"args": ["run", "examples/mcp_server.py"],
"env": {
"DATABASE_URL": "sqlite:///feedback_dev.db",
"INSIGHTS_ENABLED": "true",
"LOG_LEVEL": "DEBUG"
}
}
}
}
```
### Production
```json
{
"mcpServers": {
"fastmcp-feedback-prod": {
"command": "uv",
"args": ["run", "examples/mcp_server.py"],
"env": {
"DATABASE_URL": "postgresql://user:pass@prod.db/feedback",
"INSIGHTS_ENABLED": "true",
"LOG_LEVEL": "INFO",
"RETENTION_DAYS": "365"
}
}
}
}
```
## 📊 Monitoring & Observability
### Health Checks
```python
# Built-in health check endpoint
await get_system_info()
# Returns: version, dependencies, database_status, insights_status
```
### Logging Format
```json
{
"timestamp": "2024-01-15T10:30:00Z",
"level": "INFO",
"logger": "fastmcp_feedback.tools",
"message": "Feedback submitted successfully",
"feedback_id": "123",
"submitter_hash": "a1b2c3",
"type": "bug"
}
```
### Metrics Collection
- **Request Counts** - Total submissions, retrievals, updates
- **Error Rates** - Failed operations and reasons
- **Performance** - Response times and database query duration
- **Usage Patterns** - Popular feedback types, status distributions
## 🔍 Troubleshooting
### Common Issues
**Database Connection Errors**
```bash
# Check database connectivity
DATABASE_URL="postgresql://..." python -c "
import asyncio
from fastmcp_feedback import FeedbackDatabase
async def test():
db = FeedbackDatabase('$DATABASE_URL')
await db.initialize()
print('✅ Database connection successful')
asyncio.run(test())
"
```
**Environment Variable Issues**
```bash
# Verify environment variables
env | grep -E "(DATABASE_URL|INSIGHTS_ENABLED|LOG_LEVEL)"
# Test with explicit variables
DATABASE_URL="sqlite:///test.db" \
INSIGHTS_ENABLED="false" \
LOG_LEVEL="DEBUG" \
python examples/mcp_server.py
```
**Permission Issues**
```bash
# Check file permissions
ls -la feedback.db
# Check directory permissions
ls -ld $(dirname $(readlink -f feedback.db))
# Fix permissions
chmod 644 feedback.db
chmod 755 $(dirname $(readlink -f feedback.db))
```
### Performance Tuning
**Database Optimization**
```bash
# PostgreSQL connection pooling
DATABASE_URL="postgresql://user:pass@host/db?pool_size=20&max_overflow=30"
# SQLite optimization
DATABASE_URL="sqlite:///feedback.db?journal_mode=WAL&synchronous=NORMAL"
```
**Memory Optimization**
```bash
# Limit analytics retention
RETENTION_DAYS="30"
# Disable insights in memory-constrained environments
INSIGHTS_ENABLED="false"
```
## 🚀 Scaling Considerations
### Horizontal Scaling
- **Stateless Design** - No server-side session storage
- **Database Sharing** - Multiple instances can share database
- **Load Balancer Compatible** - Standard HTTP/JSON interfaces
### Database Scaling
- **Read Replicas** - Use read-only database URLs for list operations
- **Connection Pooling** - Configure appropriate pool sizes
- **Query Optimization** - Built-in indexing and pagination
### Caching Strategy
```python
# Redis caching for statistics (optional)
REDIS_URL="redis://localhost:6379/0"
CACHE_TTL="300" # 5 minutes
# Memcached alternative
MEMCACHED_SERVERS="localhost:11211"
```
## 📚 Related Documentation
- **[Demo Server](demo_server.md)** - Complete feature showcase
- **[Simple Integration](simple_integration.md)** - Minimal integration
- **[Architecture Guide](../docs/architecture.md)** - System design
- **[Security Guide](../docs/security.md)** - Security best practices
## 🤝 Contributing
When modifying the production server:
1. **Maintain Backward Compatibility** - Don't break existing environment variables
2. **Add Configuration Validation** - Validate new environment variables
3. **Update Documentation** - Keep this file synchronized with changes
4. **Test in Multiple Environments** - Docker, Kubernetes, bare metal
5. **Consider Security Implications** - Audit any configuration changes
This production server demonstrates enterprise-ready deployment patterns while maintaining the simplicity and power of the FastMCP Feedback system.
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""
FastMCP Feedback - MCP Server Launcher
This script provides a clean MCP server launcher that can be easily
configured in Claude Desktop or other MCP clients.
Usage:
# Direct execution
python examples/mcp_server.py
# With uv
uv run examples/mcp_server.py
# Claude Desktop configuration:
{
"mcpServers": {
"fastmcp-feedback": {
"command": "uv",
"args": ["run", "examples/mcp_server.py"],
"env": {
"DATABASE_URL": "sqlite:///feedback.db",
"INSIGHTS_ENABLED": "true",
"LOG_LEVEL": "INFO"
}
}
}
}
"""
import asyncio
import logging
import os
import sys
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 FeedbackInsights, add_feedback_tools
# Configure logging based on environment
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
logging.basicConfig(
level=getattr(logging, log_level),
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
def create_production_server() -> FastMCP:
"""Create a production-ready FastMCP Feedback server."""
# Server configuration
server_name = os.getenv("SERVER_NAME", "FastMCP Feedback Server")
database_url = os.getenv("DATABASE_URL", "sqlite:///feedback.db")
# Create server
server = FastMCP(server_name)
server.description = "Production FastMCP Feedback collection system"
# Configure insights
insights_enabled = os.getenv("INSIGHTS_ENABLED", "true").lower() == "true"
retention_days = int(os.getenv("INSIGHTS_RETENTION_DAYS", "90"))
insights = FeedbackInsights(enabled=insights_enabled, retention_days=retention_days)
# Add feedback tools
tool_prefix = os.getenv("TOOL_PREFIX", "")
add_feedback_tools(
server, database_url=database_url, insights=insights, prefix=tool_prefix
)
logger.info(f"Server '{server_name}' configured")
logger.info(f"Database: {database_url}")
logger.info(f"Insights: {'enabled' if insights_enabled else 'disabled'}")
logger.info(f"Tool prefix: '{tool_prefix}' (empty = no prefix)")
return server
async def main():
"""Main entry point for the MCP server."""
try:
logger.info("🚀 Starting FastMCP Feedback Server")
# Create and start server
server = create_production_server()
# Show available tools
async with Client(server) as client:
tools = await client.list_tools()
logger.info(f"Server ready with {len(tools)} tools:")
for tool in tools:
logger.info(f" • {tool.name}")
# Start the server
await server.run()
except KeyboardInterrupt:
logger.info("Server stopped by user")
except Exception as e:
logger.error(f"Server failed: {e}")
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())
+274
View File
@@ -0,0 +1,274 @@
# Simple FastMCP Feedback Integration
**File:** `simple_integration.py`
**Purpose:** Minimal 3-line integration example
**Lines:** 48 lines of clean, documented code
**Complexity:** Beginner-friendly
## 🎯 Overview
This example demonstrates the absolute minimal integration of FastMCP Feedback into any FastMCP server. With just 3 lines of code, you get a complete, production-ready feedback system.
## ✨ Key Features
### Minimal Code
```python
# This is all you need - 3 lines of code!
app = FastMCP("Simple Feedback App")
add_feedback_tools(app, database_url="sqlite:///simple_feedback.db")
```
### Automatic Tool Registration
The integration automatically adds 5 production-ready tools:
- ✅ `submit_feedback` - Submit bug reports & feature requests
- ✅ `list_feedback` - Query and filter feedback
- ✅ `get_feedback_statistics` - Analytics and metrics
- ✅ `delete_feedback` - Remove feedback items
- ✅ `update_feedback_status` - Change status (admin)
### Zero Configuration
- **Database** - Automatically creates SQLite database
- **Schema** - Auto-generates tables on first run
- **Validation** - Built-in Pydantic input validation
- **Error Handling** - Graceful failure scenarios
## 🚀 Usage
### Direct Execution
```bash
# Run the example
python examples/simple_integration.py
# With uv (recommended)
uv run examples/simple_integration.py
```
### Expected Output
```
🚀 Simple FastMCP Feedback Integration
==================================================
✅ Feedback system added!
📊 Available tools:
• submit_feedback - Submit feedback to the system
• get_feedback_statistics - Get feedback statistics and analytics
• list_feedback - List feedback items with optional filtering
• delete_feedback - Delete feedback item
• update_feedback_status - Update feedback status
🎯 Total tools: 5
🎬 Starting server...
Connect this as an MCP server in Claude Desktop
```
## 📋 Integration Steps
### Step 1: Install Dependencies
```bash
# Using uv (recommended)
uv add fastmcp-feedback
# Using pip
pip install fastmcp-feedback
```
### Step 2: Add to Your Server
```python
from fastmcp import FastMCP
from fastmcp_feedback import add_feedback_tools
# Your existing FastMCP server
app = FastMCP("My Awesome App")
# Add your existing tools here...
# app.add_tool(my_custom_tool)
# Add complete feedback system in one line
add_feedback_tools(app, database_url="sqlite:///feedback.db")
```
### Step 3: Configure Claude Desktop
```json
{
"mcpServers": {
"my-app-with-feedback": {
"command": "python",
"args": ["my_server.py"]
}
}
}
```
## 🔧 Customization Options
### Database Configuration
```python
# SQLite (development)
add_feedback_tools(app, database_url="sqlite:///feedback.db")
# PostgreSQL (production)
add_feedback_tools(app, database_url="postgresql://user:pass@host/db")
# In-memory (testing)
add_feedback_tools(app, database_url="sqlite:///:memory:")
```
### Analytics Configuration
```python
from fastmcp_feedback import add_feedback_tools, FeedbackInsights
# Enable analytics with custom retention
insights = FeedbackInsights(enabled=True, retention_days=30)
add_feedback_tools(app, insights=insights)
```
### Tool Prefixing
```python
# Add with prefix to avoid naming conflicts
add_feedback_tools(app, prefix="support_")
# Creates: support_submit_feedback, support_list_feedback, etc.
```
## 📊 What You Get
### Complete CRUD Operations
```python
# Users can submit feedback
await submit_feedback({
"type": "bug",
"title": "App crashes on login",
"description": "Detailed error description",
"submitter": "user@example.com"
})
# Admins can manage feedback
await update_feedback_status({
"feedback_id": "123",
"new_status": "resolved"
})
# Analytics for insights
stats = await get_feedback_statistics()
# Returns: total_count, by_type, by_status, recent_count
```
### Input Validation
- **Type Validation** - Must be: bug, feature, improvement, question
- **Required Fields** - title, description, submitter cannot be empty
- **Length Limits** - title ≤ 255 chars, description ≤ 10,000 chars
- **Format Validation** - email validation for contact_info (optional)
### Database Schema
Automatically creates:
```sql
CREATE TABLE feedback (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type VARCHAR(50) NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
submitter VARCHAR(255) NOT NULL,
contact_info VARCHAR(255),
status VARCHAR(50) NOT NULL DEFAULT 'open',
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
);
```
## 🎓 Learning Examples
### Basic Usage Pattern
```python
#!/usr/bin/env python3
from fastmcp import FastMCP
from fastmcp_feedback import add_feedback_tools
app = FastMCP("My App")
# Your custom tools
@app.tool()
def my_custom_feature():
return "Custom functionality"
# Add feedback system
add_feedback_tools(app)
# Run server
app.run()
```
### Error Handling
```python
try:
add_feedback_tools(app, database_url="invalid://url")
except Exception as e:
print(f"Failed to add feedback tools: {e}")
# Graceful fallback or alternative configuration
```
## 🔍 Under the Hood
### What `add_feedback_tools()` Does
1. **Creates Database Instance** - Initializes FeedbackDatabase with provided URL
2. **Sets Up Insights** - Configures optional analytics collection
3. **Registers Tools** - Adds 5 tools to your FastMCP server
4. **Validates Configuration** - Ensures database connectivity
5. **Logs Success** - Confirms successful integration
### Tool Implementation Pattern
```python
# Each tool is implemented as an async function
@app.tool(description="Submit feedback to the system")
async def submit_feedback(request: dict) -> dict:
# Validate input with Pydantic
validated_request = SubmitFeedbackRequest(**request)
# Store in database
feedback = await database.create_feedback(validated_request)
# Record analytics (if enabled)
insights.record_metric("feedback_submitted", metadata)
return {"success": True, "feedback_id": feedback.id}
```
## 🚀 Next Steps
### For More Features
If you need advanced capabilities, see:
- **[Demo Server](demo_server.md)** - Complete feature showcase
- **[Production Server](mcp_server.md)** - Environment configuration
- **[Architecture Guide](../docs/architecture.md)** - System design
### For Customization
```python
# Use individual mixins for fine-grained control
from fastmcp_feedback.mixins import SubmissionMixin, RetrievalMixin
submission = SubmissionMixin(database, insights)
submission.register_tools(app, prefix="feedback_")
```
### For Testing
```python
# Test your integration
import pytest
from fastmcp_feedback import add_feedback_tools
@pytest.mark.asyncio
async def test_feedback_integration():
app = FastMCP("Test App")
add_feedback_tools(app, database_url="sqlite:///:memory:")
tools = await app.get_tools()
assert "submit_feedback" in tools
```
## 🤝 Contributing
When modifying this example:
1. **Keep It Simple** - This is the "Hello World" of FastMCP Feedback
2. **Document Changes** - Update this file with any modifications
3. **Test Integration** - Ensure the 3-line pattern still works
4. **Preserve Clarity** - Maintain beginner-friendly explanations
This simple integration example proves that adding a complete, production-ready feedback system to any FastMCP server requires just 3 lines of code while providing enterprise-grade functionality.
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""
Simple FastMCP Feedback Integration Example
This demonstrates the absolute minimal integration - just 3 lines of code
to add a complete feedback system to any FastMCP server.
Usage:
python examples/simple_integration.py
"""
import asyncio
import sys
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 add_feedback_tools
async def main():
"""Minimal FastMCP Feedback integration example."""
print("🚀 Simple FastMCP Feedback Integration")
print("=" * 50)
# This is all you need - 3 lines of code!
app = FastMCP("Simple Feedback App")
add_feedback_tools(app, database_url="sqlite:///simple_feedback.db")
print("✅ Feedback system added!")
print("📊 Available tools:")
# Show what tools were added
async with Client(app) as client:
tools = await client.list_tools()
for tool in tools:
print(f" • {tool.name} - {tool.description}")
print(f"\n🎯 Total tools: {len(tools)}")
print("\n🎬 Starting server...")
print(" Connect this as an MCP server in Claude Desktop")
# Start the server
await app.run()
if __name__ == "__main__":
asyncio.run(main())
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""
Test script to verify demo server functionality without running the full MCP server.
"""
import asyncio
import sys
from pathlib import Path
# Add the src directory to the path for development
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
sys.path.insert(0, str(Path(__file__).parent))
from demo_server import create_demo_server
from fastmcp import Client
async def test_demo_functionality():
"""Test the demo server tools without running the MCP server."""
print("🧪 Testing FastMCP Feedback Demo Functionality")
print("=" * 55)
try:
# Create the demo server
server = create_demo_server()
async with Client(server) as client:
# Get available tools
tools = {tool.name: tool for tool in await client.list_tools()}
print(f"✅ Server created with {len(tools)} tools")
# List available tools
print("\n📋 Available Tools:")
for name, tool in tools.items():
print(f" • {name}: {tool.description}")
# Test system info tool
if "get_system_info" in tools:
print("\n🔍 Testing system info...")
result = await client.call_tool("get_system_info", {})
info = result.structured_content
print(f" Version: {info['versions']['fastmcp_feedback']}")
print(f" Python: {info['versions']['python']}")
print(f" Features: {len(info['demo_features'])}")
# Test sample data setup
if "setup_sample_data" in tools:
print("\n📊 Testing sample data setup...")
result = await client.call_tool("setup_sample_data", {})
data_result = result.structured_content
if data_result["success"]:
print(f" ✅ Added {data_result['items_added']} sample items")
else:
print(f" ❌ Failed: {data_result['error']}")
# Test feedback listing
if "list_feedback" in tools:
print("\n📝 Testing feedback listing...")
result = await client.call_tool("list_feedback", {})
feedback_data = result.structured_content
print(f" Found {len(feedback_data['feedback'])} feedback items")
# Show first item details
if feedback_data["feedback"]:
first = feedback_data["feedback"][0]
print(f" First item: {first['type']} - {first['title']}")
# Test statistics
if "get_feedback_statistics" in tools:
print("\n📈 Testing statistics...")
result = await client.call_tool("get_feedback_statistics", {})
stats = result.structured_content
print(f" Total items: {stats['total_count']}")
print(f" By type: {stats['by_type']}")
print(f" By status: {stats['by_status']}")
print("\n🎉 All tests passed! Demo server is working correctly.")
return True
except Exception as e:
print(f"\n❌ Test failed: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
success = asyncio.run(test_demo_functionality())
sys.exit(0 if success else 1)
+12 -3
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "fastmcp-feedback"
version = "2026.01.12" # YYYYMMDD versioning - update for each release
version = "2026.09.27" # YYYYMMDD.patch versioning - update for each release
description = "Production-ready feedback collection system for FastMCP servers"
readme = "README.md"
license = "MIT"
@@ -43,7 +43,7 @@ classifiers = [
]
requires-python = ">=3.11"
dependencies = [
"fastmcp>=2.12.2",
"fastmcp>=2.12.2,<5", # CI tests 2.12, 2.14 and 4.0; raise the ceiling once 5.x passes
"sqlalchemy>=2.0.43",
"pydantic>=2.11.7"
]
@@ -56,6 +56,7 @@ dev = [
"pytest-asyncio>=0.23.0",
"pytest-cov>=5.0.0",
"pytest-mock>=3.12.0",
"pytest-html>=4.2.0",
"ruff>=0.1.0",
"mypy>=1.7.0",
]
@@ -64,6 +65,7 @@ test = [
"pytest-asyncio>=0.23.0",
"pytest-cov>=5.0.0",
"pytest-mock>=3.12.0",
"pytest-html>=4.2.0",
]
all = ["fastmcp-feedback[postgresql,mysql,dev]"]
@@ -81,6 +83,7 @@ packages = ["src/fastmcp_feedback"]
include = [
"/src",
"/tests",
"/examples",
"README.md",
"LICENSE",
"CHANGELOG.md",
@@ -142,7 +145,13 @@ src = ["src", "tests"]
[tool.ruff.lint]
select = ["E", "W", "F", "UP", "B", "SIM", "I", "N", "C4"]
ignore = ["E501", "B008"]
ignore = [
"E501",
"B008",
# StrEnum changes str()/format() output of FeedbackType/FeedbackStatus,
# which reach the database and API responses. Worth doing, not silently.
"UP042",
]
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["ARG", "PLR2004"]
+3 -3
View File
@@ -44,7 +44,7 @@ Quick Start:
import logging
# Version information - CalVer YYYY.MM.DD
__version__ = "2026.01.12"
__version__ = "2026.09.27"
__author__ = "Ryan Malloy"
__email__ = "ryan@supported.systems"
@@ -259,8 +259,8 @@ from fastmcp_feedback import create_feedback_server
main_app = FastMCP("Main Application")
feedback_service = create_feedback_server("Feedback Service")
# Import feedback service with prefix
main_app.import_server(feedback_service, prefix="feedback")
# Mount feedback service; its tools become feedback_<name>
main_app.mount(feedback_service, "feedback")
""",
"multi_tenant": """
from fastmcp import FastMCP
+4 -2
View File
@@ -260,8 +260,10 @@ def compose_feedback_services(
f"{service_name} Service", database_url, shared_insights
)
# Import service with prefix
main_server.import_server(service_server, prefix=service_name)
# mount() is sync on every supported FastMCP; the second positional
# arg is `prefix` on 2.x and `namespace` on 4.x. import_server() is
# gone in 4.x and was async (so silently never ran here) on 2.14.
main_server.mount(service_server, service_name)
logger.info(f"Composed {len(services)} feedback services into main server")
+16 -1
View File
@@ -3,16 +3,31 @@
import logging
from datetime import UTC, datetime
from fastmcp.contrib.mcp_mixin import MCPMixin
from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool
from ..database import FeedbackDatabase
from ..insights import (
FeedbackInsights,
record_tool_usage,
)
from ..models import Feedback, FeedbackStatus, FeedbackType, feedback_to_dict
logger = logging.getLogger(__name__)
# Explicit re-exports for other mixin modules
__all__ = [
"BaseFeedbackMixin",
"Feedback",
"FeedbackDatabase",
"FeedbackInsights",
"FeedbackStatus",
"FeedbackType",
"feedback_to_dict",
"logger",
"mcp_tool",
"record_tool_usage",
]
class BaseFeedbackMixin(MCPMixin):
"""Base class for all feedback mixins with shared functionality."""
+10
View File
@@ -0,0 +1,10 @@
"""Helpers that paper over FastMCP API differences between 2.x and 4.x."""
from fastmcp import FastMCP
async def tool_map(server: FastMCP) -> dict:
"""Return {name: Tool}. 2.x has get_tools() -> dict, 4.x has list_tools() -> list."""
if hasattr(server, "get_tools"):
return await server.get_tools()
return {tool.name: tool for tool in await server.list_tools()}
+19 -18
View File
@@ -11,11 +11,14 @@ import sys
from pathlib import Path
from unittest.mock import patch
from sqlalchemy import text
# Add the examples directory to the path for imports
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "examples"))
from fastmcp import FastMCP
from fastmcp_feedback import FeedbackDatabase, FeedbackInsights
from tests._compat import tool_map
@pytest.mark.asyncio
@@ -35,7 +38,7 @@ class TestDemoServerIntegration:
assert "demonstration" in server.description.lower()
# Get all tools
tools = await server.get_tools()
tools = await tool_map(server)
# Verify all expected tools are present
expected_tools = {
@@ -72,12 +75,12 @@ class TestDemoServerIntegration:
from fastmcp_feedback.models import Feedback
async with database.session() as session:
# Count feedback items
result = session.execute("SELECT COUNT(*) FROM feedback")
result = session.execute(text("SELECT COUNT(*) FROM feedback"))
count = result.scalar()
assert count >= 5, f"Expected at least 5 sample items, got {count}"
# Verify types are present
result = session.execute("SELECT DISTINCT type FROM feedback")
result = session.execute(text("SELECT DISTINCT type FROM feedback"))
types = [row[0] for row in result.fetchall()]
expected_types = {"BUG", "FEATURE", "IMPROVEMENT", "QUESTION"}
assert expected_types.issubset(set(types)), f"Missing feedback types: {expected_types - set(types)}"
@@ -89,21 +92,19 @@ class TestDemoServerIntegration:
"""Test actual execution of demo server tools."""
from demo_server import create_demo_server, create_temp_database, setup_demo_data
# Create server with temporary database
server = create_demo_server()
# Override database URL for testing
# Seed a temporary database, then point the server at it
db_url = create_temp_database()
database = FeedbackDatabase(db_url)
await setup_demo_data(database)
server = create_demo_server(database_url=db_url)
tools = await server.get_tools()
tools = await tool_map(server)
# Test system info tool
system_info_tool = tools["get_system_info"]
info_result = await system_info_tool.run({})
assert info_result.structured_content["success"] is True
assert "version" in info_result.structured_content
assert info_result.structured_content["status"] == "running"
assert "versions" in info_result.structured_content
assert "dependencies" in info_result.structured_content
# Test statistics tool (should work with sample data)
@@ -126,7 +127,7 @@ class TestDemoServerIntegration:
add_feedback_tools(app, database_url="sqlite:///:memory:")
# Verify tools were added
tools = await app.get_tools()
tools = await tool_map(app)
expected_tools = {
"submit_feedback",
"list_feedback",
@@ -176,7 +177,7 @@ class TestDemoServerIntegration:
from demo_server import create_demo_server
server = create_demo_server()
tools = await server.get_tools()
tools = await tool_map(server)
# Test submit_feedback with invalid data
submit_tool = tools["submit_feedback"]
@@ -199,7 +200,7 @@ class TestDemoServerIntegration:
from demo_server import create_demo_server, create_temp_database
server = create_demo_server()
tools = await server.get_tools()
tools = await tool_map(server)
# Submit feedback
submit_tool = tools["submit_feedback"]
@@ -225,7 +226,7 @@ class TestDemoServerIntegration:
assert our_feedback is not None, "Submitted feedback not found in list"
assert our_feedback["title"] == test_feedback["title"]
assert our_feedback["type"] == test_feedback["type"]
assert our_feedback["status"] == "OPEN"
assert our_feedback["status"] == "open"
# Update feedback status
update_tool = tools["update_feedback_status"]
@@ -239,7 +240,7 @@ class TestDemoServerIntegration:
list_result = await list_tool.run({})
feedback_list = list_result.structured_content["feedback"]
updated_feedback = next((f for f in feedback_list if f["id"] == feedback_id), None)
assert updated_feedback["status"] == "IN_PROGRESS"
assert updated_feedback["status"] == "in_progress"
# Get statistics (should include our feedback)
stats_tool = tools["get_feedback_statistics"]
@@ -272,7 +273,7 @@ class TestDemoServerDocumentation:
from demo_server import create_demo_server
server = create_demo_server()
tools = await server.get_tools()
tools = await tool_map(server)
# According to demo_server.md, should have exactly 7 tools
assert len(tools) == 7, f"Expected 7 tools as documented, got {len(tools)}"
@@ -296,7 +297,7 @@ class TestDemoServerDocumentation:
app = FastMCP("Test App")
add_feedback_tools(app, database_url="sqlite:///:memory:")
tools = await app.get_tools()
tools = await tool_map(app)
assert len(tools) == 5, f"Expected 5 tools as documented, got {len(tools)}"
# Verify documented tool names
@@ -318,7 +319,7 @@ class TestDemoServerPerformance:
from demo_server import create_demo_server
server = create_demo_server()
tools = await server.get_tools()
tools = await tool_map(server)
submit_tool = tools["submit_feedback"]
# Submit multiple feedback items concurrently
+14 -13
View File
@@ -10,6 +10,7 @@ from fastmcp_feedback.feedback import create_feedback_server
from fastmcp_feedback.models import SubmitFeedbackRequest, FeedbackStatus
from fastmcp_feedback.database import FeedbackDatabase
from fastmcp_feedback.insights import FeedbackInsights
from tests._compat import tool_map
@pytest.mark.integration
@@ -25,7 +26,7 @@ class TestCompleteWorkflow:
add_feedback_tools(server, database_url=memory_db_url, insights=insights)
# Get tools
tools = await server.get_tools()
tools = await tool_map(server)
submit_tool = tools["submit_feedback"]
list_tool = tools["list_feedback"]
update_tool = tools["update_feedback_status"]
@@ -106,7 +107,7 @@ class TestCompleteWorkflow:
server = FastMCP("Multi-Type Test Server")
add_feedback_tools(server, database_url=memory_db_url)
tools = await server.get_tools()
tools = await tool_map(server)
submit_tool = tools["submit_feedback"]
list_tool = tools["list_feedback"]
@@ -179,7 +180,7 @@ class TestCompleteWorkflow:
server = FastMCP("Error Test Server")
add_feedback_tools(server, database_url=memory_db_url)
tools = await server.get_tools()
tools = await tool_map(server)
submit_tool = tools["submit_feedback"]
update_tool = tools["update_feedback_status"]
delete_tool = tools["delete_feedback"]
@@ -233,7 +234,7 @@ class TestCompleteWorkflow:
insights = FeedbackInsights(enabled=True)
add_feedback_tools(server, database_url=memory_db_url, insights=insights)
tools = await server.get_tools()
tools = await tool_map(server)
submit_tool = tools["submit_feedback"]
list_tool = tools["list_feedback"]
@@ -285,7 +286,7 @@ class TestCompleteWorkflow:
server = FastMCP("Bulk Test Server")
add_feedback_tools(server, database_url=memory_db_url)
tools = await server.get_tools()
tools = await tool_map(server)
submit_tool = tools["submit_feedback"]
list_tool = tools["list_feedback"]
update_tool = tools["update_feedback_status"]
@@ -355,7 +356,7 @@ class TestServerCompositionWorkflow:
)
# Get tools from dedicated server
tools = await feedback_server.get_tools()
tools = await tool_map(feedback_server)
submit_tool = tools["submit_feedback"]
list_tool = tools["list_feedback"]
@@ -384,11 +385,11 @@ class TestServerCompositionWorkflow:
admin_feedback = create_feedback_server("Admin Feedback", database_url="sqlite:///:memory:")
# Import servers with different prefixes
await main_server.import_server(user_feedback, prefix="user")
await main_server.import_server(admin_feedback, prefix="admin")
main_server.mount(user_feedback, "user")
main_server.mount(admin_feedback, "admin")
# Get tools
tools = await main_server.get_tools()
tools = await tool_map(main_server)
user_submit = tools["user_submit_feedback"]
admin_submit = tools["admin_submit_feedback"]
@@ -439,7 +440,7 @@ class TestServerCompositionWorkflow:
add_feedback_tools(server, database_url=memory_db_url, prefix="public")
add_feedback_tools(server, database_url=memory_db_url, prefix="internal")
tools = await server.get_tools()
tools = await tool_map(server)
public_submit = tools["public_submit_feedback"]
internal_submit = tools["internal_submit_feedback"]
@@ -490,7 +491,7 @@ class TestPerformanceWorkflow:
insights = FeedbackInsights(enabled=True)
add_feedback_tools(server, database_url=memory_db_url, insights=insights)
tools = await server.get_tools()
tools = await tool_map(server)
submit_tool = tools["submit_feedback"]
list_tool = tools["list_feedback"]
stats_tool = tools["get_feedback_statistics"]
@@ -537,7 +538,7 @@ class TestPerformanceWorkflow:
server = FastMCP("Filter Performance Server")
add_feedback_tools(server, database_url=memory_db_url)
tools = await server.get_tools()
tools = await tool_map(server)
submit_tool = tools["submit_feedback"]
list_tool = tools["list_feedback"]
update_tool = tools["update_feedback_status"]
@@ -611,7 +612,7 @@ class TestPerformanceWorkflow:
server = FastMCP("Concurrent Mixed Server")
add_feedback_tools(server, database_url=memory_db_url)
tools = await server.get_tools()
tools = await tool_map(server)
submit_tool = tools["submit_feedback"]
list_tool = tools["list_feedback"]
update_tool = tools["update_feedback_status"]
+28 -3
View File
@@ -345,7 +345,7 @@ class TestServerComposition:
async def test_compose_feedback_services_success(self):
"""Test successful service composition."""
main_server = Mock()
main_server.import_server = Mock()
main_server.mount = Mock()
services = {
"service1": {
@@ -376,13 +376,38 @@ class TestServerComposition:
mock_submission.assert_called_once()
mock_management.assert_called_once()
mock_feedback.assert_called_once()
assert main_server.import_server.call_count == 3
assert main_server.mount.call_count == 3
@pytest.mark.asyncio
async def test_compose_feedback_services_real_servers(self, tmp_path):
"""Composed tools must actually appear under their prefix.
The mocked tests above can't catch a missing await: a sync Mock
records the call either way. This one uses real FastMCP servers.
"""
from fastmcp import FastMCP
from tests._compat import tool_map
main_server = FastMCP("Main")
compose_feedback_services(
main_server,
{
"public": {"database_url": f"sqlite:///{tmp_path}/p.db", "type": "submission"},
"admin": {"database_url": f"sqlite:///{tmp_path}/a.db", "type": "full"},
},
)
names = set(await tool_map(main_server))
assert "public_submit_feedback" in names
assert "admin_list_feedback" in names
assert "admin_update_feedback_status" in names
@pytest.mark.asyncio
async def test_compose_feedback_services_with_shared_insights(self):
"""Test service composition with shared insights."""
main_server = Mock()
main_server.import_server = Mock()
main_server.mount = Mock()
shared_insights = Mock()
services = {
+9 -8
View File
@@ -7,6 +7,7 @@ from fastmcp import FastMCP
from fastmcp_feedback.mixins import SubmissionMixin, RetrievalMixin, ManagementMixin
from fastmcp_feedback.models import SubmitFeedbackRequest, FeedbackStatus
from fastmcp_feedback.database import FeedbackDatabase
from tests._compat import tool_map
@pytest.mark.unit
@@ -95,7 +96,7 @@ class TestSubmissionMixin:
mixin.register_tools(mock_fastmcp_server)
# Verify tool was registered
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
assert "submit_feedback" in tool_names
@@ -107,7 +108,7 @@ class TestSubmissionMixin:
mixin.register_tools(mock_fastmcp_server, prefix="support", separator="_")
# Verify tool was registered with prefix
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
assert "support_submit_feedback" in tool_names
@@ -267,7 +268,7 @@ class TestRetrievalMixin:
mixin.register_tools(mock_fastmcp_server)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
expected_tools = ["list_feedback", "get_feedback_statistics"]
@@ -437,7 +438,7 @@ class TestManagementMixin:
mixin.register_tools(mock_fastmcp_server)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
expected_tools = ["update_feedback_status", "delete_feedback"]
@@ -532,7 +533,7 @@ class TestMixinIntegration:
management.register_tools(mock_fastmcp_server)
# Verify all tools are registered
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
expected_tools = [
@@ -555,7 +556,7 @@ class TestMixinIntegration:
submission.register_tools(mock_fastmcp_server, prefix="admin")
retrieval.register_tools(mock_fastmcp_server, prefix="public")
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
assert "admin_submit_feedback" in tool_names
@@ -663,7 +664,7 @@ class TestMixinConfiguration:
# Register with dash separator
submission.register_tools(mock_fastmcp_server, prefix="api", separator="-")
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
assert "api-submit_feedback" in tool_names
@@ -676,7 +677,7 @@ class TestMixinConfiguration:
# Test the standard registration instead
submission.register_tools(mock_fastmcp_server)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
# SubmissionMixin should only register its tools
+37 -36
View File
@@ -7,6 +7,7 @@ from fastmcp import FastMCP
from fastmcp_feedback.tools import add_feedback_tools
from fastmcp_feedback.feedback import create_feedback_server
from fastmcp_feedback.models import SubmitFeedbackRequest
from tests._compat import tool_map
@pytest.mark.unit
@@ -18,7 +19,7 @@ class TestAddFeedbackTools:
"""Test adding feedback tools with basic configuration."""
add_feedback_tools(mock_fastmcp_server, database_url=memory_db_url)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
expected_tools = [
@@ -40,7 +41,7 @@ class TestAddFeedbackTools:
prefix="support"
)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
expected_tools = [
@@ -62,7 +63,7 @@ class TestAddFeedbackTools:
insights=test_insights
)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
# Should have all standard tools - check tool names in the keys
assert len([name for name in tools.keys() if "feedback" in name]) >= 5
@@ -76,7 +77,7 @@ class TestAddFeedbackTools:
# Should use default SQLite database
add_feedback_tools(mock_fastmcp_server)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
assert "submit_feedback" in tool_names
@@ -91,7 +92,7 @@ class TestAddFeedbackTools:
separator="-"
)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
assert "api-submit_feedback" in tool_names
@@ -105,7 +106,7 @@ class TestAddFeedbackTools:
# Second call with different prefix
add_feedback_tools(mock_fastmcp_server, database_url=memory_db_url, prefix="user")
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
# Should have tools with both prefixes
@@ -128,7 +129,7 @@ class TestCreateFeedbackServer:
assert isinstance(server, FastMCP)
assert server.name == "Test Feedback Server"
tools = await server.get_tools()
tools = await tool_map(server)
tool_names = list(tools.keys())
expected_tools = [
@@ -150,7 +151,7 @@ class TestCreateFeedbackServer:
)
assert server.name == "Custom DB Server"
tools = await server.get_tools()
tools = await tool_map(server)
assert len(tools) >= 5
async def test_create_feedback_server_with_insights(self, test_insights):
@@ -162,7 +163,7 @@ class TestCreateFeedbackServer:
assert server.name == "Analytics Server"
# Verify insights are integrated
tools = await server.get_tools()
tools = await tool_map(server)
assert len(tools) >= 5
async def test_create_feedback_server_with_description(self):
@@ -184,8 +185,8 @@ class TestCreateFeedbackServer:
assert server1.name != server2.name
# Each should have independent tool sets
tools1 = await server1.get_tools()
tools2 = await server2.get_tools()
tools1 = await tool_map(server1)
tools2 = await tool_map(server2)
assert len(tools1) >= 5
assert len(tools2) >= 5
@@ -201,7 +202,7 @@ class TestToolIntegration:
add_feedback_tools(mock_fastmcp_server, database_url=memory_db_url)
# Get the submit_feedback tool
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
submit_tool = tools["submit_feedback"]
assert submit_tool is not None
@@ -218,7 +219,7 @@ class TestToolIntegration:
add_feedback_tools(mock_fastmcp_server, database_url=memory_db_url)
# Get the list_feedback tool
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
list_tool = tools["list_feedback"]
assert list_tool is not None
@@ -234,7 +235,7 @@ class TestToolIntegration:
add_feedback_tools(mock_fastmcp_server, database_url=memory_db_url)
# Get the statistics tool
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
stats_tool = tools["get_feedback_statistics"]
assert stats_tool is not None
@@ -250,7 +251,7 @@ class TestToolIntegration:
add_feedback_tools(mock_fastmcp_server, database_url=memory_db_url)
# Get the update status tool
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
update_tool = tools["update_feedback_status"]
assert update_tool is not None
@@ -269,7 +270,7 @@ class TestToolIntegration:
add_feedback_tools(mock_fastmcp_server, database_url=memory_db_url)
# Get the delete tool
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
delete_tool = tools["delete_feedback"]
assert delete_tool is not None
@@ -290,7 +291,7 @@ class TestToolValidation:
"""Test submit_feedback tool parameter validation."""
add_feedback_tools(mock_fastmcp_server, database_url=memory_db_url)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
submit_tool = tools["submit_feedback"]
# Test with invalid parameters
@@ -301,7 +302,7 @@ class TestToolValidation:
"""Test list_feedback tool parameter validation."""
add_feedback_tools(mock_fastmcp_server, database_url=memory_db_url)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
list_tool = tools["list_feedback"]
# Test with valid optional parameters
@@ -318,7 +319,7 @@ class TestToolValidation:
"""Test update_feedback_status parameter validation."""
add_feedback_tools(mock_fastmcp_server, database_url=memory_db_url)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
update_tool = tools["update_feedback_status"]
# Test with missing required parameters
@@ -329,7 +330,7 @@ class TestToolValidation:
"""Test delete_feedback parameter validation."""
add_feedback_tools(mock_fastmcp_server, database_url=memory_db_url)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
delete_tool = tools["delete_feedback"]
# Test with missing required parameters
@@ -348,9 +349,9 @@ class TestServerComposition:
feedback_server = create_feedback_server("Feedback Service")
# Import feedback server with prefix
await main_server.import_server(feedback_server, prefix="feedback")
main_server.mount(feedback_server, "feedback")
tools = await main_server.get_tools()
tools = await tool_map(main_server)
tool_names = list(tools.keys())
# Should have feedback tools with prefix
@@ -364,10 +365,10 @@ class TestServerComposition:
user_feedback = create_feedback_server("User Feedback")
admin_feedback = create_feedback_server("Admin Feedback")
await main_server.import_server(user_feedback, prefix="user")
await main_server.import_server(admin_feedback, prefix="admin")
main_server.mount(user_feedback, "user")
main_server.mount(admin_feedback, "admin")
tools = await main_server.get_tools()
tools = await tool_map(main_server)
tool_names = list(tools.keys())
# Should have tools from both servers
@@ -386,9 +387,9 @@ class TestServerComposition:
# Implementation would need to support this feature
# FastMCP import_server doesn't support selective tool import
# but we can test the full import with prefix
await main_server.import_server(feedback_server, prefix="public")
main_server.mount(feedback_server, "public")
tools = await main_server.get_tools()
tools = await tool_map(main_server)
tool_names = list(tools.keys())
# All tools should be imported with prefix
@@ -416,7 +417,7 @@ class TestToolPerformance:
assert performance_timer.duration_ms < 1000 # 1 second
# Verify all tools were registered
tools = await server.get_tools()
tools = await tool_map(server)
assert len(tools) >= 5
async def test_multiple_server_creation_performance(self, performance_timer):
@@ -436,14 +437,14 @@ class TestToolPerformance:
# Each server should be functional
for server in servers:
tools = await server.get_tools()
tools = await tool_map(server)
assert len(tools) >= 5
async def test_tool_execution_performance(self, mock_fastmcp_server, memory_db_url, performance_timer):
"""Test performance of tool execution."""
add_feedback_tools(mock_fastmcp_server, database_url=memory_db_url)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
list_tool = tools["list_feedback"]
performance_timer.start()
@@ -467,7 +468,7 @@ class TestToolConfiguration:
"""Test that tools have proper descriptions."""
add_feedback_tools(mock_fastmcp_server, database_url=memory_db_url)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
for tool_name, tool_obj in tools.items():
# Each tool should have a description
@@ -480,7 +481,7 @@ class TestToolConfiguration:
"""Test tool metadata and categorization."""
add_feedback_tools(mock_fastmcp_server, database_url=memory_db_url)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
feedback_tools = [name for name in tools.keys() if "feedback" in name]
# Should have categorized the tools appropriately
@@ -490,7 +491,7 @@ class TestToolConfiguration:
"""Test tool parameter schemas."""
add_feedback_tools(mock_fastmcp_server, database_url=memory_db_url)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
submit_tool = tools["submit_feedback"]
# Tool should have parameter schema defined
@@ -510,7 +511,7 @@ class TestToolErrorHandling:
# Use invalid database URL
add_feedback_tools(server, database_url="sqlite:///nonexistent/path/db.sqlite")
tools = await server.get_tools()
tools = await tool_map(server)
list_tool = tools["list_feedback"]
# Tool execution should handle database errors gracefully
@@ -523,7 +524,7 @@ class TestToolErrorHandling:
"""Test tool handling of invalid parameters."""
add_feedback_tools(mock_fastmcp_server, database_url=memory_db_url)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
submit_tool = tools["submit_feedback"]
# Test with malformed request
@@ -534,7 +535,7 @@ class TestToolErrorHandling:
"""Test concurrent execution of tools."""
add_feedback_tools(mock_fastmcp_server, database_url=memory_db_url)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
list_tool = tools["list_feedback"]
import asyncio
+29 -28
View File
@@ -23,6 +23,7 @@ from fastmcp_feedback.tools import (
)
from fastmcp_feedback.database import FeedbackDatabase
from fastmcp_feedback.insights import FeedbackInsights
from tests._compat import tool_map
@pytest.mark.unit
@@ -34,7 +35,7 @@ class TestSubmissionTools:
"""Test adding submission tools with basic configuration."""
add_submission_tools(mock_fastmcp_server, database_url=memory_db_url)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
# Should have submission-related tools
@@ -51,7 +52,7 @@ class TestSubmissionTools:
prefix="user"
)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
assert "user_submit_feedback" in tool_names
@@ -65,14 +66,14 @@ class TestSubmissionTools:
insights=test_insights
)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
assert len(tools) > 0
async def test_add_submission_tools_without_database_url(self, mock_fastmcp_server):
"""Test adding submission tools without database URL (uses default)."""
add_submission_tools(mock_fastmcp_server)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
assert "submit_feedback" in tools
@patch('fastmcp_feedback.tools.FeedbackDatabase')
@@ -105,7 +106,7 @@ class TestSubmissionTools:
prefix="api_" # With trailing underscore
)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
# Should strip trailing underscore
@@ -121,7 +122,7 @@ class TestRetrievalTools:
"""Test adding retrieval tools with basic configuration."""
add_retrieval_tools(mock_fastmcp_server, database_url=memory_db_url)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
# Should have retrieval-related tools
@@ -139,7 +140,7 @@ class TestRetrievalTools:
prefix="public"
)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
assert "public_list_feedback" in tool_names
@@ -154,14 +155,14 @@ class TestRetrievalTools:
insights=test_insights
)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
assert len(tools) > 0
async def test_add_retrieval_tools_without_database_url(self, mock_fastmcp_server):
"""Test adding retrieval tools without database URL (uses default)."""
add_retrieval_tools(mock_fastmcp_server)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
assert "list_feedback" in tools
assert "get_feedback_statistics" in tools
@@ -197,7 +198,7 @@ class TestManagementTools:
"""Test adding management tools with basic configuration."""
add_management_tools(mock_fastmcp_server, database_url=memory_db_url)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
# Should have management-related tools
@@ -215,7 +216,7 @@ class TestManagementTools:
prefix="admin"
)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
assert "admin_update_feedback_status" in tool_names
@@ -230,14 +231,14 @@ class TestManagementTools:
insights=test_insights
)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
assert len(tools) > 0
async def test_add_management_tools_without_database_url(self, mock_fastmcp_server):
"""Test adding management tools without database URL (uses default)."""
add_management_tools(mock_fastmcp_server)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
assert "update_feedback_status" in tools
assert "delete_feedback" in tools
@@ -347,7 +348,7 @@ class TestAdvancedIntegrationPatterns:
create_multi_tenant_tools(mock_fastmcp_server, tenants)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
# Should have tools for each tenant with prefixes
@@ -365,7 +366,7 @@ class TestAdvancedIntegrationPatterns:
create_multi_tenant_tools(mock_fastmcp_server, tenants, insights=test_insights)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
assert len(tools) >= 10 # At least 5 tools per tenant
async def test_create_multi_tenant_tools_empty_tenants(self, mock_fastmcp_server):
@@ -374,7 +375,7 @@ class TestAdvancedIntegrationPatterns:
create_multi_tenant_tools(mock_fastmcp_server, tenants)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
# Should complete without error but not add any tools
assert len(tools) == 0
@@ -394,7 +395,7 @@ class TestAdvancedIntegrationPatterns:
"""Test creating role-based tools."""
create_role_based_tools(mock_fastmcp_server, database_url=memory_db_url)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
# Should have public tools (submission and retrieval)
@@ -416,14 +417,14 @@ class TestAdvancedIntegrationPatterns:
insights=test_insights
)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
assert len(tools) >= 7 # Public + admin tools
async def test_create_role_based_tools_without_database(self, mock_fastmcp_server):
"""Test creating role-based tools without database URL."""
create_role_based_tools(mock_fastmcp_server)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
assert "public_submit_feedback" in tool_names
@@ -465,7 +466,7 @@ class TestMigrationHelpers:
mock_fastmcp_server.remove_tool.assert_any_call("old_list_tool")
# Should have new tools added
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
assert "submit_feedback" in tool_names
assert "list_feedback" in tool_names
@@ -501,7 +502,7 @@ class TestMigrationHelpers:
insights=test_insights
)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
assert len(tools) >= 5
async def test_migrate_tools_to_mixins_empty_list(self, mock_fastmcp_server, memory_db_url):
@@ -515,7 +516,7 @@ class TestMigrationHelpers:
)
# Should still add new tools
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
assert len(tools) >= 5
@patch('fastmcp_feedback.tools.add_feedback_tools')
@@ -757,7 +758,7 @@ class TestErrorHandlingAndEdgeCases:
# Verify all servers have tools
for server in servers:
tools = await server.get_tools()
tools = await tool_map(server)
assert "submit_feedback" in tools
assert "public_list_feedback" in tools
assert "admin_update_feedback_status" in tools
@@ -774,7 +775,7 @@ class TestErrorHandlingAndEdgeCases:
# Multiple underscores
add_management_tools(mock_fastmcp_server, database_url=memory_db_url, prefix="admin___")
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
# Should handle edge cases gracefully
@@ -812,7 +813,7 @@ class TestIntegrationWithExistingTests:
# Add all tools
add_feedback_tools(mock_fastmcp_server, database_url=memory_db_url, prefix="complete")
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
tool_names = list(tools.keys())
# Should have both individual and complete sets
@@ -830,7 +831,7 @@ class TestIntegrationWithExistingTests:
insights=test_insights
)
tools = await mock_fastmcp_server.get_tools()
tools = await tool_map(mock_fastmcp_server)
submit_tool = tools["submit_feedback"]
# Should work with existing sample data
@@ -848,9 +849,9 @@ class TestIntegrationWithExistingTests:
add_retrieval_tools(feedback_server, prefix="readonly")
# Import with prefix
await main_server.import_server(feedback_server, prefix="external")
main_server.mount(feedback_server, "external")
tools = await main_server.get_tools()
tools = await tool_map(main_server)
tool_names = list(tools.keys())
# Should have properly composed tool names
Generated
+2189
View File
File diff suppressed because it is too large Load Diff