Initial commit
This commit is contained in:
411
skills/mxcp-expert/references/agent-centric-design.md
Normal file
411
skills/mxcp-expert/references/agent-centric-design.md
Normal file
@@ -0,0 +1,411 @@
|
||||
# Agent-Centric Design for MXCP Tools
|
||||
|
||||
**Designing MXCP tools that LLMs can effectively use with zero prior context.**
|
||||
|
||||
## Overview
|
||||
|
||||
When building MXCP servers, remember: **LLMs are your primary users**. Your tools must enable LLMs to accomplish real-world tasks effectively. This guide provides principles for designing tools that work well for AI agents.
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. Build for Workflows, Not Just Data Access
|
||||
|
||||
**Don't simply expose database tables or API endpoints - design tools around complete workflows.**
|
||||
|
||||
#### ❌ Poor Design: Raw Data Access
|
||||
```yaml
|
||||
# tools/get_user.yml
|
||||
tool:
|
||||
name: get_user
|
||||
description: "Get user by ID"
|
||||
parameters:
|
||||
- name: user_id
|
||||
type: integer
|
||||
source:
|
||||
code: SELECT * FROM users WHERE id = $user_id
|
||||
|
||||
# tools/get_orders.yml
|
||||
tool:
|
||||
name: get_orders
|
||||
description: "Get orders by user"
|
||||
parameters:
|
||||
- name: user_id
|
||||
type: integer
|
||||
source:
|
||||
code: SELECT * FROM orders WHERE user_id = $user_id
|
||||
```
|
||||
|
||||
**Problem**: LLM needs multiple tool calls to answer "What did user 123 buy?"
|
||||
|
||||
#### ✅ Good Design: Workflow-Oriented
|
||||
```yaml
|
||||
# tools/get_user_purchase_summary.yml
|
||||
tool:
|
||||
name: get_user_purchase_summary
|
||||
description: "Get complete purchase history for a user including orders, products, and total spending. Use this to understand a user's buying behavior and preferences."
|
||||
parameters:
|
||||
- name: user_id
|
||||
type: integer
|
||||
description: "User identifier"
|
||||
- name: date_range
|
||||
type: string
|
||||
description: "Optional date range: 'last_30_days', 'last_year', or 'all_time'"
|
||||
default: "all_time"
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
user_info: { type: object, description: "Basic user information" }
|
||||
order_count: { type: integer, description: "Total number of orders" }
|
||||
total_spent: { type: number, description: "Total amount spent in USD" }
|
||||
top_products: { type: array, description: "Most frequently purchased products" }
|
||||
source:
|
||||
code: |
|
||||
WITH user_orders AS (
|
||||
SELECT o.*, p.name as product_name, p.category
|
||||
FROM orders o
|
||||
JOIN order_items oi ON o.id = oi.order_id
|
||||
JOIN products p ON oi.product_id = p.id
|
||||
WHERE o.user_id = $user_id
|
||||
AND ($date_range = 'all_time'
|
||||
OR ($date_range = 'last_30_days' AND o.created_at > CURRENT_DATE - INTERVAL 30 DAY)
|
||||
OR ($date_range = 'last_year' AND o.created_at > CURRENT_DATE - INTERVAL 1 YEAR))
|
||||
)
|
||||
SELECT
|
||||
json_object(
|
||||
'user_info', (SELECT json_object('id', id, 'name', name) FROM users WHERE id = $user_id),
|
||||
'order_count', COUNT(DISTINCT id),
|
||||
'total_spent', SUM(total_amount),
|
||||
'top_products', (
|
||||
SELECT json_group_array(json_object('product', product_name, 'count', count))
|
||||
FROM (SELECT product_name, COUNT(*) as count FROM user_orders GROUP BY product_name ORDER BY count DESC LIMIT 5)
|
||||
)
|
||||
) as result
|
||||
FROM user_orders
|
||||
```
|
||||
|
||||
**Benefit**: Single tool call answers complete questions about user behavior.
|
||||
|
||||
### 2. Optimize for Limited Context
|
||||
|
||||
**LLMs have constrained context windows - make every token count.**
|
||||
|
||||
#### Design for Concise Responses
|
||||
|
||||
```yaml
|
||||
tool:
|
||||
name: search_products
|
||||
parameters:
|
||||
- name: query
|
||||
type: string
|
||||
description: "Search query"
|
||||
- name: detail_level
|
||||
type: string
|
||||
description: "Response detail level"
|
||||
enum: ["minimal", "standard", "full"]
|
||||
default: "standard"
|
||||
examples:
|
||||
- "minimal: Only ID, name, price"
|
||||
- "standard: Basic info + category + stock"
|
||||
- "full: All fields including descriptions"
|
||||
source:
|
||||
code: |
|
||||
SELECT
|
||||
CASE $detail_level
|
||||
WHEN 'minimal' THEN json_object('id', id, 'name', name, 'price', price)
|
||||
WHEN 'standard' THEN json_object('id', id, 'name', name, 'price', price, 'category', category, 'stock', stock)
|
||||
ELSE json_object('id', id, 'name', name, 'price', price, 'category', category, 'stock', stock, 'description', description, 'specs', specs)
|
||||
END as product
|
||||
FROM products
|
||||
WHERE name LIKE '%' || $query || '%'
|
||||
```
|
||||
|
||||
**Principle**: Default to high-signal information, provide options for more detail.
|
||||
|
||||
#### Use Human-Readable Identifiers
|
||||
|
||||
```yaml
|
||||
# ✅ GOOD: Return names alongside IDs
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
customer_id: { type: string, description: "Customer ID (e.g., 'CUST_12345')" }
|
||||
customer_name: { type: string, description: "Customer display name" }
|
||||
assigned_to_id: { type: string, description: "Assigned user ID" }
|
||||
assigned_to_name: { type: string, description: "Assigned user name" }
|
||||
|
||||
# ❌ BAD: Only return opaque IDs
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
customer_id: { type: integer }
|
||||
assigned_to: { type: integer }
|
||||
```
|
||||
|
||||
**Benefit**: LLM can understand relationships without additional lookups.
|
||||
|
||||
### 3. Design Actionable Error Messages
|
||||
|
||||
**Error messages should guide LLMs toward correct usage patterns.**
|
||||
|
||||
#### ✅ Good Error Messages (Python Tools)
|
||||
|
||||
```python
|
||||
def search_large_dataset(query: str, limit: int = 100) -> dict:
|
||||
"""Search with intelligent error guidance"""
|
||||
|
||||
# Validate inputs
|
||||
if not query or len(query) < 3:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Query must be at least 3 characters. Provide a more specific search term to get better results.",
|
||||
"error_code": "QUERY_TOO_SHORT",
|
||||
"suggestion": "Try adding more keywords or using specific product names"
|
||||
}
|
||||
|
||||
if limit > 1000:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Limit of {limit} exceeds maximum allowed (1000). Use filters to narrow your search: add 'category' or 'price_range' parameters.",
|
||||
"error_code": "LIMIT_EXCEEDED",
|
||||
"max_limit": 1000,
|
||||
"suggestion": "Try using category='electronics' or price_range='0-100' to reduce results"
|
||||
}
|
||||
|
||||
# Execute search
|
||||
results = db.execute(
|
||||
"SELECT * FROM products WHERE name LIKE $query LIMIT $limit",
|
||||
{"query": f"%{query}%", "limit": limit}
|
||||
)
|
||||
|
||||
if not results:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"No products found matching '{query}'. Try broader terms or check spelling.",
|
||||
"error_code": "NO_RESULTS",
|
||||
"suggestion": "Use 'list_categories' tool to see available product categories"
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"count": len(results),
|
||||
"results": results
|
||||
}
|
||||
```
|
||||
|
||||
**Principle**: Every error should suggest a specific next action.
|
||||
|
||||
### 4. Follow Natural Task Subdivisions
|
||||
|
||||
**Tool names should reflect how humans think about tasks, not just database structure.**
|
||||
|
||||
#### ✅ Good: Task-Oriented Naming
|
||||
```
|
||||
get_customer_purchase_history # What users want to know
|
||||
analyze_sales_by_region # Natural analysis task
|
||||
check_inventory_status # Action-oriented
|
||||
schedule_report_generation # Complete workflow
|
||||
```
|
||||
|
||||
#### ❌ Poor: Database-Oriented Naming
|
||||
```
|
||||
select_from_orders # Database operation
|
||||
join_users_and_purchases # Technical operation
|
||||
aggregate_by_column # Generic operation
|
||||
```
|
||||
|
||||
**Use consistent prefixes for discoverability**:
|
||||
```yaml
|
||||
# Customer operations
|
||||
- get_customer_details
|
||||
- get_customer_orders
|
||||
- get_customer_analytics
|
||||
|
||||
# Product operations
|
||||
- search_products
|
||||
- get_product_details
|
||||
- check_product_availability
|
||||
|
||||
# Analytics operations
|
||||
- analyze_sales_trends
|
||||
- analyze_customer_behavior
|
||||
- analyze_inventory_turnover
|
||||
```
|
||||
|
||||
### 5. Provide Comprehensive Documentation
|
||||
|
||||
**Every field must have a description that helps LLMs understand usage.**
|
||||
|
||||
See **references/llm-friendly-documentation.md** for complete documentation guidelines.
|
||||
|
||||
**Quick checklist**:
|
||||
- [ ] Tool description explains WHAT, returns WHAT, WHEN to use
|
||||
- [ ] Every parameter has description with examples
|
||||
- [ ] Return type properties all have descriptions
|
||||
- [ ] Cross-references to related tools
|
||||
- [ ] Examples show realistic usage
|
||||
|
||||
## MXCP-Specific Best Practices
|
||||
|
||||
### Use SQL for Workflow Consolidation
|
||||
|
||||
**SQL is powerful for combining multiple data sources in one query:**
|
||||
|
||||
```yaml
|
||||
tool:
|
||||
name: get_order_fulfillment_status
|
||||
description: "Get complete order fulfillment information including shipping, payments, and inventory status. Use this to answer questions about order status and estimated delivery."
|
||||
source:
|
||||
code: |
|
||||
SELECT
|
||||
o.id as order_id,
|
||||
o.status as order_status,
|
||||
u.name as customer_name,
|
||||
s.carrier,
|
||||
s.tracking_number,
|
||||
s.estimated_delivery,
|
||||
p.status as payment_status,
|
||||
json_group_array(
|
||||
json_object(
|
||||
'product', prod.name,
|
||||
'quantity', oi.quantity,
|
||||
'in_stock', prod.stock >= oi.quantity
|
||||
)
|
||||
) as items
|
||||
FROM orders o
|
||||
JOIN users u ON o.user_id = u.id
|
||||
LEFT JOIN shipments s ON o.id = s.order_id
|
||||
LEFT JOIN payments p ON o.id = p.order_id
|
||||
JOIN order_items oi ON o.id = oi.order_id
|
||||
JOIN products prod ON oi.product_id = prod.id
|
||||
WHERE o.id = $order_id
|
||||
GROUP BY o.id
|
||||
```
|
||||
|
||||
**Single tool call provides complete fulfillment picture.**
|
||||
|
||||
### Use Python for Complex Workflows
|
||||
|
||||
```python
|
||||
async def analyze_customer_churn_risk(customer_id: str) -> dict:
|
||||
"""
|
||||
Comprehensive churn risk analysis combining multiple data sources.
|
||||
|
||||
Returns risk score, contributing factors, and recommended actions.
|
||||
Use this to identify customers who may leave and take preventive action.
|
||||
"""
|
||||
# Get customer history
|
||||
orders = db.execute(
|
||||
"SELECT * FROM orders WHERE customer_id = $cid ORDER BY created_at DESC",
|
||||
{"cid": customer_id}
|
||||
)
|
||||
|
||||
support_tickets = db.execute(
|
||||
"SELECT * FROM support_tickets WHERE customer_id = $cid",
|
||||
{"cid": customer_id}
|
||||
)
|
||||
|
||||
# Calculate risk factors
|
||||
days_since_last_order = (datetime.now() - orders[0]["created_at"]).days if orders else 999
|
||||
unresolved_tickets = len([t for t in support_tickets if t["status"] != "resolved"])
|
||||
total_spent = sum(o["total_amount"] for o in orders)
|
||||
|
||||
# Determine risk level
|
||||
risk_score = 0
|
||||
factors = []
|
||||
|
||||
if days_since_last_order > 90:
|
||||
risk_score += 30
|
||||
factors.append("No purchases in 90+ days")
|
||||
|
||||
if unresolved_tickets > 0:
|
||||
risk_score += 20 * unresolved_tickets
|
||||
factors.append(f"{unresolved_tickets} unresolved support tickets")
|
||||
|
||||
if total_spent < 100:
|
||||
risk_score += 10
|
||||
factors.append("Low lifetime value")
|
||||
|
||||
# Generate recommendations
|
||||
recommendations = []
|
||||
if days_since_last_order > 90:
|
||||
recommendations.append("Send re-engagement email with discount")
|
||||
if unresolved_tickets > 0:
|
||||
recommendations.append("Prioritize resolution of open support tickets")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"customer_id": customer_id,
|
||||
"risk_score": min(risk_score, 100),
|
||||
"risk_level": "high" if risk_score > 60 else "medium" if risk_score > 30 else "low",
|
||||
"contributing_factors": factors,
|
||||
"recommendations": recommendations,
|
||||
"days_since_last_order": days_since_last_order,
|
||||
"unresolved_tickets": unresolved_tickets
|
||||
}
|
||||
```
|
||||
|
||||
### Leverage MXCP Policies for Context-Aware Tools
|
||||
|
||||
```yaml
|
||||
tool:
|
||||
name: get_employee_compensation
|
||||
description: "Get employee compensation details. Returns salary and benefits information based on user permissions."
|
||||
parameters:
|
||||
- name: employee_id
|
||||
type: string
|
||||
description: "Employee identifier"
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
employee_id: { type: string }
|
||||
name: { type: string }
|
||||
salary: { type: number, description: "Annual salary (admin only)" }
|
||||
benefits: { type: array, description: "Benefits package" }
|
||||
policies:
|
||||
output:
|
||||
- condition: "user.role != 'hr_manager' && user.role != 'admin'"
|
||||
action: filter_fields
|
||||
fields: ["salary"]
|
||||
reason: "Salary information restricted to HR managers and admins"
|
||||
source:
|
||||
code: |
|
||||
SELECT
|
||||
employee_id,
|
||||
name,
|
||||
salary,
|
||||
benefits
|
||||
FROM employees
|
||||
WHERE employee_id = $employee_id
|
||||
```
|
||||
|
||||
**LLM can call same tool, MXCP automatically filters based on user context.**
|
||||
|
||||
## Testing Agent-Centric Design
|
||||
|
||||
### Create Realistic Evaluation Scenarios
|
||||
|
||||
See **references/mxcp-evaluation-guide.md** for complete evaluation guidelines.
|
||||
|
||||
**Quick validation**:
|
||||
1. Can an LLM answer complex multi-step questions using your tools?
|
||||
2. Do tool descriptions clearly indicate when to use each tool?
|
||||
3. Do error messages guide the LLM toward correct usage?
|
||||
4. Can common tasks be completed with minimal tool calls?
|
||||
|
||||
## Summary
|
||||
|
||||
**Agent-centric design principles for MXCP**:
|
||||
|
||||
1. ✅ **Build for workflows** - Consolidate related operations
|
||||
2. ✅ **Optimize for context** - Provide detail level options, use readable identifiers
|
||||
3. ✅ **Actionable errors** - Guide LLMs with specific suggestions
|
||||
4. ✅ **Natural naming** - Task-oriented, not database-oriented
|
||||
5. ✅ **Comprehensive docs** - Every parameter and field documented
|
||||
|
||||
**MXCP advantages**:
|
||||
- SQL enables powerful workflow consolidation
|
||||
- Python handles complex multi-step logic
|
||||
- Policies provide automatic context-aware filtering
|
||||
- Type system ensures clear contracts
|
||||
|
||||
**Remember**: Design for the LLM as your user, not the human. Humans configure tools, LLMs use them.
|
||||
990
skills/mxcp-expert/references/build-and-validate-workflow.md
Normal file
990
skills/mxcp-expert/references/build-and-validate-workflow.md
Normal file
@@ -0,0 +1,990 @@
|
||||
# Build and Validate Workflow
|
||||
|
||||
**Mandatory workflow to ensure MXCP servers always work correctly.**
|
||||
|
||||
## Definition of Done
|
||||
|
||||
An MXCP server is **DONE** only when ALL of these criteria are met:
|
||||
|
||||
- [ ] **Virtual environment created**: `uv venv` completed (if Python tools exist)
|
||||
- [ ] **Dependencies installed**: `uv pip install mxcp black pyright pytest pytest-asyncio pytest-httpx pytest-cov` (if Python tools exist)
|
||||
- [ ] **Structure valid**: `mxcp validate` passes with no errors
|
||||
- [ ] **MXCP tests pass**: `mxcp test` passes for all tools
|
||||
- [ ] **Python code formatted**: `black python/` passes (if Python tools exist)
|
||||
- [ ] **Type checking passes**: `pyright python/` passes with 0 errors (if Python tools exist)
|
||||
- [ ] **Python unit tests pass**: `pytest tests/ -v` passes (if Python tools exist)
|
||||
- [ ] **Data quality**: `dbt test` passes (if using dbt)
|
||||
- [ ] **Result correctness verified**: Tests check actual values, not just structure
|
||||
- [ ] **Mocking implemented**: External API calls are mocked in unit tests
|
||||
- [ ] **Concurrency safe**: Python tools avoid race conditions
|
||||
- [ ] **Documentation quality verified**: LLMs can understand tools with zero context
|
||||
- [ ] **Error handling implemented**: Python tools return structured errors
|
||||
- [ ] **Manual verification**: At least one manual test per tool succeeds
|
||||
- [ ] **Security reviewed**: Checklist completed (see below)
|
||||
- [ ] **Config provided**: Project has config.yml with usage instructions
|
||||
- [ ] **Dependencies listed**: requirements.txt includes all dev dependencies
|
||||
|
||||
**NEVER declare a project complete without ALL checkboxes checked.**
|
||||
|
||||
## Mandatory Build Order
|
||||
|
||||
Follow this exact order to ensure correctness:
|
||||
|
||||
### Phase 1: Foundation (Must complete before Phase 2)
|
||||
|
||||
1. **Initialize project**
|
||||
```bash
|
||||
mkdir project-name && cd project-name
|
||||
mxcp init --bootstrap
|
||||
```
|
||||
|
||||
2. **Set up Python virtual environment** (CRITICAL - do this BEFORE any MXCP commands)
|
||||
```bash
|
||||
# Create virtual environment with uv
|
||||
uv venv
|
||||
|
||||
# Activate virtual environment
|
||||
source .venv/bin/activate # On Unix/macOS
|
||||
# OR
|
||||
.venv\Scripts\activate # On Windows
|
||||
|
||||
# Verify activation (prompt should show (.venv))
|
||||
which python
|
||||
# Output: /path/to/project-name/.venv/bin/python
|
||||
|
||||
# Install MXCP and development tools
|
||||
uv pip install mxcp black pyright pytest pytest-asyncio pytest-httpx pytest-cov
|
||||
|
||||
# Create requirements.txt for reproducibility
|
||||
cat > requirements.txt <<'EOF'
|
||||
mxcp>=0.1.0
|
||||
black>=24.0.0
|
||||
pyright>=1.1.0
|
||||
pytest>=7.0.0
|
||||
pytest-asyncio>=0.21.0
|
||||
pytest-httpx>=0.21.0
|
||||
pytest-cov>=4.0.0
|
||||
EOF
|
||||
```
|
||||
|
||||
**IMPORTANT**: Virtual environment must be active for ALL subsequent commands. If you close your terminal, re-activate with `source .venv/bin/activate`.
|
||||
|
||||
3. **Create project structure**
|
||||
```bash
|
||||
mkdir -p seeds models tools resources prompts python tests
|
||||
touch tests/__init__.py
|
||||
```
|
||||
|
||||
4. **Set up dbt (if needed)**
|
||||
```bash
|
||||
# Create dbt_project.yml if needed
|
||||
# Create profiles.yml connection
|
||||
```
|
||||
|
||||
5. **Validation checkpoint**: Verify structure
|
||||
```bash
|
||||
# Ensure virtual environment is active
|
||||
echo $VIRTUAL_ENV # Should show: /path/to/project-name/.venv
|
||||
|
||||
ls -la # Confirm directories exist
|
||||
mxcp validate # Should pass (no tools yet, but structure valid)
|
||||
```
|
||||
|
||||
**CRITICAL: Directory Structure Enforcement**
|
||||
|
||||
MXCP **enforces** organized directory structure. Files in wrong directories are **ignored** by discovery commands:
|
||||
|
||||
- ✅ Tools MUST be in `tools/*.yml`
|
||||
- ✅ Resources MUST be in `resources/*.yml`
|
||||
- ✅ Prompts MUST be in `prompts/*.yml`
|
||||
- ❌ Tool files in root directory will be **ignored**
|
||||
- ❌ Tool files in wrong directories will be **ignored**
|
||||
|
||||
**Common mistake to avoid**:
|
||||
```bash
|
||||
# ❌ WRONG - tool in root directory (will be ignored)
|
||||
my_tool.yml
|
||||
|
||||
# ✅ CORRECT - tool in tools/ directory
|
||||
tools/my_tool.yml
|
||||
```
|
||||
|
||||
Use `mxcp init --bootstrap` to create proper structure automatically.
|
||||
|
||||
### Phase 2: Data Layer (if applicable)
|
||||
|
||||
1. **Add data source** (CSV, Excel, etc.)
|
||||
```bash
|
||||
# Option A: CSV seed
|
||||
cp data.csv seeds/
|
||||
|
||||
# Option B: Excel conversion
|
||||
python -c "import pandas as pd; pd.read_excel('data.xlsx').to_csv('seeds/data.csv', index=False)"
|
||||
```
|
||||
|
||||
2. **Create schema.yml** (CRITICAL - don't skip!)
|
||||
```yaml
|
||||
# seeds/schema.yml
|
||||
version: 2
|
||||
seeds:
|
||||
- name: data
|
||||
description: "Data description here"
|
||||
columns:
|
||||
- name: id
|
||||
tests: [unique, not_null]
|
||||
# Add ALL columns with tests
|
||||
```
|
||||
|
||||
3. **Load and test data**
|
||||
```bash
|
||||
dbt seed --select data
|
||||
dbt test --select data
|
||||
```
|
||||
|
||||
4. **Validation checkpoint**: Data quality verified
|
||||
```bash
|
||||
# Check data loaded
|
||||
mxcp query "SELECT COUNT(*) FROM data"
|
||||
# Should return row count
|
||||
```
|
||||
|
||||
### Phase 3: Build Tools ONE AT A TIME
|
||||
|
||||
**CRITICAL: Build ONE tool, validate, test, THEN move to next.**
|
||||
|
||||
For EACH tool:
|
||||
|
||||
#### Step 1: Create Test FIRST (with LLM-friendly documentation)
|
||||
|
||||
```yaml
|
||||
# tools/my_tool.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: my_tool
|
||||
description: "Retrieve data from table by filtering on column. Returns array of matching records. Use this to query specific records by their identifier."
|
||||
parameters:
|
||||
- name: param1
|
||||
type: string
|
||||
description: "Filter value for column (e.g., 'value123'). Must match exact column value."
|
||||
required: true
|
||||
examples: ["value123", "test_value"]
|
||||
return:
|
||||
type: array
|
||||
description: "Array of matching records"
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer, description: "Record identifier" }
|
||||
column: { type: string, description: "Filtered column value" }
|
||||
source:
|
||||
code: |
|
||||
SELECT * FROM data WHERE column = $param1
|
||||
tests:
|
||||
- name: "basic_test"
|
||||
arguments:
|
||||
- key: param1
|
||||
value: "test_value"
|
||||
result:
|
||||
# Expected result structure with actual values to verify
|
||||
- id: 1
|
||||
column: "test_value"
|
||||
```
|
||||
|
||||
**Documentation requirements (check before proceeding)**:
|
||||
- [ ] Tool description explains WHAT, returns WHAT, WHEN to use
|
||||
- [ ] Parameters have descriptions with examples
|
||||
- [ ] Return type properties all described
|
||||
- [ ] An LLM with zero context could understand how to use this
|
||||
|
||||
#### Step 2: Validate Structure
|
||||
|
||||
```bash
|
||||
mxcp validate
|
||||
# Must pass before proceeding
|
||||
```
|
||||
|
||||
**Common errors at this stage:**
|
||||
- Indentation wrong (use spaces, not tabs)
|
||||
- Missing required fields (name, description, return)
|
||||
- Type mismatch (array vs object)
|
||||
- Invalid SQL syntax
|
||||
|
||||
**If validation fails:**
|
||||
1. Read error message carefully
|
||||
2. Check YAML indentation (use yamllint)
|
||||
3. Verify all required fields present
|
||||
4. Check type definitions match return data
|
||||
5. Fix and re-validate
|
||||
|
||||
#### Step 3: Test Functionality
|
||||
|
||||
**A. MXCP Integration Tests**
|
||||
|
||||
```bash
|
||||
# Run the test case
|
||||
mxcp test tool my_tool
|
||||
|
||||
# Run manually with different inputs
|
||||
mxcp run tool my_tool --param param1=test_value
|
||||
```
|
||||
|
||||
**If test fails:**
|
||||
1. Check SQL syntax in source
|
||||
2. Verify table/column names exist
|
||||
3. Test SQL directly: `mxcp query "SELECT ..."`
|
||||
4. Check parameter binding ($param1 syntax)
|
||||
5. Verify return type matches actual data
|
||||
6. Fix and re-test
|
||||
|
||||
**B. Python Code Quality (For Python Tools)**
|
||||
|
||||
**MANDATORY workflow after creating or editing ANY Python file:**
|
||||
|
||||
```bash
|
||||
# CRITICAL: Always ensure virtual environment is active first
|
||||
source .venv/bin/activate
|
||||
|
||||
# Step 1: Format code with black
|
||||
black python/
|
||||
# Must see: "All done! ✨ 🍰 ✨" or "N file(s) reformatted"
|
||||
|
||||
# Step 2: Type check with pyright
|
||||
pyright python/
|
||||
# Must see: "0 errors, 0 warnings, 0 informations"
|
||||
|
||||
# Step 3: Run unit tests
|
||||
pytest tests/ -v
|
||||
# Must see: All tests PASSED
|
||||
|
||||
# If ANY step fails, fix before proceeding!
|
||||
```
|
||||
|
||||
**Create Unit Tests:**
|
||||
|
||||
```bash
|
||||
# Create test file
|
||||
cat > tests/test_my_tool.py <<'EOF'
|
||||
"""Tests for my_module."""
|
||||
|
||||
import pytest
|
||||
from python.my_module import my_function
|
||||
from typing import Dict, Any
|
||||
|
||||
def test_my_function_correctness():
|
||||
"""Verify result correctness"""
|
||||
result = my_function("test_input")
|
||||
assert result["expected_key"] == "expected_value" # Verify actual value!
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_function():
|
||||
"""Test async functions"""
|
||||
result = await async_function()
|
||||
assert result is not None
|
||||
EOF
|
||||
|
||||
# Run tests with coverage
|
||||
pytest tests/ -v --cov=python --cov-report=term-missing
|
||||
```
|
||||
|
||||
**Common Python Type Errors and Fixes:**
|
||||
|
||||
```python
|
||||
# ❌ WRONG: Using 'any' type
|
||||
from typing import Dict
|
||||
async def get_data(id: str) -> Dict[str, any]: # 'any' is not valid
|
||||
pass
|
||||
|
||||
# ✅ CORRECT: Use proper types
|
||||
from typing import Dict, Any, Union
|
||||
async def get_data(id: str) -> Dict[str, Union[str, int, float, bool]]:
|
||||
pass
|
||||
|
||||
# ✅ BETTER: Define response type
|
||||
from typing import TypedDict
|
||||
class DataResponse(TypedDict):
|
||||
success: bool
|
||||
data: str
|
||||
count: int
|
||||
|
||||
async def get_data(id: str) -> DataResponse:
|
||||
pass
|
||||
```
|
||||
|
||||
**If unit tests fail:**
|
||||
1. Check function logic
|
||||
2. Verify test assertions are correct
|
||||
3. Check imports
|
||||
4. Fix and re-test
|
||||
|
||||
**C. Mocking External Calls (Required for API tools)**
|
||||
|
||||
```python
|
||||
# tests/test_api_tool.py
|
||||
import pytest
|
||||
from python.api_wrapper import fetch_data
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_data_with_mock(httpx_mock):
|
||||
"""Mock external API call"""
|
||||
# Mock the HTTP response
|
||||
httpx_mock.add_response(
|
||||
url="https://api.example.com/data",
|
||||
json={"key": "value", "count": 5}
|
||||
)
|
||||
|
||||
# Call function
|
||||
result = await fetch_data("param")
|
||||
|
||||
# Verify correctness
|
||||
assert result["key"] == "value"
|
||||
assert result["count"] == 5
|
||||
```
|
||||
|
||||
**D. Error Handling (Required for Python tools)**
|
||||
|
||||
Python tools MUST return structured error objects, never raise exceptions to MXCP.
|
||||
|
||||
```python
|
||||
# python/my_module.py
|
||||
import httpx
|
||||
|
||||
async def fetch_user(user_id: int) -> dict:
|
||||
"""
|
||||
Fetch user with comprehensive error handling.
|
||||
|
||||
Returns:
|
||||
Success: {"success": True, "user": {...}}
|
||||
Error: {"success": False, "error": "...", "error_code": "..."}
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(
|
||||
f"https://api.example.com/users/{user_id}"
|
||||
)
|
||||
|
||||
if response.status_code == 404:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"User with ID {user_id} not found. Use list_users to see available users.",
|
||||
"error_code": "NOT_FOUND",
|
||||
"user_id": user_id
|
||||
}
|
||||
|
||||
if response.status_code >= 500:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "External API is currently unavailable. Please try again later.",
|
||||
"error_code": "API_ERROR",
|
||||
"status_code": response.status_code
|
||||
}
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"user": response.json()
|
||||
}
|
||||
|
||||
except httpx.TimeoutException:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Request timed out after 10 seconds. The API may be slow or unavailable.",
|
||||
"error_code": "TIMEOUT"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Unexpected error: {str(e)}",
|
||||
"error_code": "UNKNOWN_ERROR"
|
||||
}
|
||||
```
|
||||
|
||||
**Test error handling**:
|
||||
```python
|
||||
# tests/test_error_handling.py
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_not_found(httpx_mock):
|
||||
"""Verify 404 returns structured error"""
|
||||
httpx_mock.add_response(
|
||||
url="https://api.example.com/users/999",
|
||||
status_code=404
|
||||
)
|
||||
|
||||
result = await fetch_user(999)
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["error_code"] == "NOT_FOUND"
|
||||
assert "999" in result["error"] # Error mentions the ID
|
||||
assert "list_users" in result["error"] # Actionable suggestion
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timeout_error(httpx_mock):
|
||||
"""Verify timeout returns structured error"""
|
||||
httpx_mock.add_exception(httpx.TimeoutException("Timeout"))
|
||||
|
||||
result = await fetch_user(123)
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["error_code"] == "TIMEOUT"
|
||||
assert "timeout" in result["error"].lower()
|
||||
```
|
||||
|
||||
**Error message principles**:
|
||||
- ✅ Be specific (exactly what went wrong)
|
||||
- ✅ Be actionable (suggest next steps)
|
||||
- ✅ Provide context (relevant values/IDs)
|
||||
- ✅ Use plain language (LLM-friendly)
|
||||
|
||||
See **references/error-handling-guide.md** for comprehensive patterns.
|
||||
|
||||
**E. Concurrency Safety Tests (For stateful Python tools)**
|
||||
|
||||
```python
|
||||
# tests/test_concurrency.py
|
||||
import pytest
|
||||
import asyncio
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_calls():
|
||||
"""Verify no race conditions"""
|
||||
tasks = [my_function(i) for i in range(100)]
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
# Verify all succeeded
|
||||
assert len(results) == 100
|
||||
assert all(r is not None for r in results)
|
||||
```
|
||||
|
||||
#### Step 4: Verification Checkpoint
|
||||
|
||||
Before moving to next tool:
|
||||
|
||||
**For ALL tools:**
|
||||
- [ ] `mxcp validate` passes
|
||||
- [ ] `mxcp test tool my_tool` passes
|
||||
- [ ] Manual test with real data works
|
||||
- [ ] Tool returns expected data structure
|
||||
- [ ] Error cases handled (null params, no results, etc.)
|
||||
- [ ] **Result correctness verified** (not just structure)
|
||||
- [ ] **Documentation quality verified**:
|
||||
- [ ] Tool description explains WHAT, WHAT it returns, WHEN to use
|
||||
- [ ] All parameters have descriptions with examples
|
||||
- [ ] Return fields all have descriptions
|
||||
- [ ] Cross-references to related tools (if applicable)
|
||||
- [ ] **LLM can understand with zero context** (test: read YAML only, would you know how to use it?)
|
||||
|
||||
**For Python tools (additionally):**
|
||||
- [ ] **Virtual environment active**: `echo $VIRTUAL_ENV` shows path
|
||||
- [ ] **Code formatted**: `black python/` shows "All done!"
|
||||
- [ ] **Type checking passes**: `pyright python/` shows "0 errors"
|
||||
- [ ] `pytest tests/test_my_tool.py -v` passes
|
||||
- [ ] External calls are mocked (if applicable)
|
||||
- [ ] Concurrency tests pass (if stateful)
|
||||
- [ ] No global mutable state OR proper locking used
|
||||
- [ ] Test coverage >80% (`pytest --cov=python tests/`)
|
||||
- [ ] **Error handling implemented**:
|
||||
- [ ] All try/except blocks return structured errors
|
||||
- [ ] Error format: `{"success": False, "error": "...", "error_code": "..."}`
|
||||
- [ ] Error messages are specific and actionable
|
||||
- [ ] Never raise exceptions to MXCP (return error objects)
|
||||
|
||||
**Only proceed to next tool when ALL checks pass.**
|
||||
|
||||
### Phase 4: Integration Testing
|
||||
|
||||
After all tools created:
|
||||
|
||||
1. **Run full validation suite**
|
||||
```bash
|
||||
# CRITICAL: Ensure virtual environment is active
|
||||
source .venv/bin/activate
|
||||
|
||||
# Python code quality (if Python tools exist)
|
||||
black python/ # Must show: "All done!"
|
||||
pyright python/ # Must show: "0 errors"
|
||||
pytest tests/ -v --cov=python --cov-report=term # All tests must pass
|
||||
|
||||
# MXCP validation and integration tests
|
||||
mxcp validate # All tools
|
||||
mxcp test # All tests
|
||||
mxcp lint # Documentation quality
|
||||
|
||||
# dbt tests (if applicable)
|
||||
dbt test
|
||||
```
|
||||
|
||||
2. **Test realistic scenarios**
|
||||
```bash
|
||||
# Test each tool with realistic inputs
|
||||
mxcp run tool tool1 --param key=realistic_value
|
||||
mxcp run tool tool2 --param key=realistic_value
|
||||
|
||||
# Test error cases
|
||||
mxcp run tool tool1 --param key=invalid_value
|
||||
mxcp run tool tool1 # Missing required param
|
||||
```
|
||||
|
||||
3. **Performance check** (if applicable)
|
||||
```bash
|
||||
# Test with large inputs
|
||||
mxcp run tool query_data --param limit=1000
|
||||
|
||||
# Check response time is reasonable
|
||||
time mxcp run tool my_tool --param key=value
|
||||
```
|
||||
|
||||
### Phase 5: Security & Configuration
|
||||
|
||||
1. **Security review checklist**
|
||||
- [ ] All SQL uses parameterized queries ($param)
|
||||
- [ ] No hardcoded secrets in code
|
||||
- [ ] Input validation on all parameters
|
||||
- [ ] Sensitive fields filtered with policies (if needed)
|
||||
- [ ] Authentication configured (if needed)
|
||||
|
||||
2. **Create config.yml**
|
||||
```yaml
|
||||
# config.yml
|
||||
mxcp: 1
|
||||
profiles:
|
||||
default:
|
||||
secrets:
|
||||
- name: secret_name
|
||||
type: env
|
||||
parameters:
|
||||
env_var: SECRET_ENV_VAR
|
||||
```
|
||||
|
||||
3. **Create README or usage instructions**
|
||||
```markdown
|
||||
# Project Name
|
||||
|
||||
## Setup
|
||||
1. Install dependencies: pip install -r requirements.txt
|
||||
2. Set environment variables: export SECRET=xxx
|
||||
3. Load data: dbt seed (if applicable)
|
||||
4. Start server: mxcp serve
|
||||
|
||||
## Available Tools
|
||||
- tool1: Description
|
||||
- tool2: Description
|
||||
```
|
||||
|
||||
### Phase 6: Final Validation
|
||||
|
||||
**This is the FINAL checklist before declaring DONE:**
|
||||
|
||||
```bash
|
||||
# 0. Activate virtual environment
|
||||
source .venv/bin/activate
|
||||
echo $VIRTUAL_ENV # Must show path
|
||||
|
||||
# 1. Python code quality (if Python tools exist)
|
||||
black python/ && pyright python/ && pytest tests/ -v
|
||||
# All must pass
|
||||
|
||||
# 2. Clean start test
|
||||
cd .. && cd project-name
|
||||
mxcp validate
|
||||
# Should pass
|
||||
|
||||
# 3. All tests pass
|
||||
mxcp test
|
||||
# Should show all tests passing
|
||||
|
||||
# 4. Manual smoke test
|
||||
mxcp run tool <main_tool> --param key=value
|
||||
# Should return valid data
|
||||
|
||||
# 5. Lint check
|
||||
mxcp lint
|
||||
# Should have no critical issues
|
||||
|
||||
# 6. dbt tests (if applicable)
|
||||
dbt test
|
||||
# All data quality tests pass
|
||||
|
||||
# 7. Serve test
|
||||
mxcp serve --transport http --port 8080 &
|
||||
SERVER_PID=$!
|
||||
sleep 2
|
||||
curl http://localhost:8080/health || true
|
||||
kill $SERVER_PID
|
||||
# Server should start without errors
|
||||
```
|
||||
|
||||
## Common Failure Patterns & Fixes
|
||||
|
||||
### YAML Validation Errors
|
||||
|
||||
**Error**: "Invalid YAML: expected <thing>"
|
||||
```yaml
|
||||
# WRONG: Mixed spaces and tabs
|
||||
tool:
|
||||
name: my_tool
|
||||
description: "..." # Tab here
|
||||
|
||||
# CORRECT: Consistent spaces (2 or 4)
|
||||
tool:
|
||||
name: my_tool
|
||||
description: "..."
|
||||
```
|
||||
|
||||
**Error**: "Missing required field: description"
|
||||
```yaml
|
||||
# WRONG: Missing description
|
||||
tool:
|
||||
name: my_tool
|
||||
parameters: [...]
|
||||
|
||||
# CORRECT: All required fields
|
||||
tool:
|
||||
name: my_tool
|
||||
description: "What this tool does"
|
||||
parameters: [...]
|
||||
```
|
||||
|
||||
**Error**: "Invalid type for field 'return'"
|
||||
```yaml
|
||||
# WRONG: String instead of type object
|
||||
return: "array"
|
||||
|
||||
# CORRECT: Proper type definition
|
||||
return:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
```
|
||||
|
||||
### SQL Errors
|
||||
|
||||
**Error**: "Table 'xyz' not found"
|
||||
```sql
|
||||
-- WRONG: Table doesn't exist
|
||||
SELECT * FROM xyz
|
||||
|
||||
-- FIX: Check table name, run dbt seed
|
||||
SELECT * FROM actual_table_name
|
||||
|
||||
-- VERIFY: List tables
|
||||
-- mxcp query "SHOW TABLES"
|
||||
```
|
||||
|
||||
**Error**: "Column 'abc' not found"
|
||||
```sql
|
||||
-- WRONG: Column name typo or doesn't exist
|
||||
SELECT abc FROM table
|
||||
|
||||
-- FIX: Check exact column name (case-sensitive in some DBs)
|
||||
SELECT actual_column_name FROM table
|
||||
|
||||
-- VERIFY: List columns
|
||||
-- mxcp query "DESCRIBE table"
|
||||
```
|
||||
|
||||
**Error**: "Unbound parameter: $param1"
|
||||
```yaml
|
||||
# WRONG: Parameter not defined in parameters list
|
||||
parameters:
|
||||
- name: other_param
|
||||
source:
|
||||
code: SELECT * FROM table WHERE col = $param1
|
||||
|
||||
# CORRECT: Define all parameters used in SQL
|
||||
parameters:
|
||||
- name: param1
|
||||
type: string
|
||||
source:
|
||||
code: SELECT * FROM table WHERE col = $param1
|
||||
```
|
||||
|
||||
### Type Mismatch Errors
|
||||
|
||||
**Error**: "Expected object, got array"
|
||||
```yaml
|
||||
# WRONG: Return type doesn't match actual data
|
||||
return:
|
||||
type: object
|
||||
source:
|
||||
code: SELECT * FROM table # Returns multiple rows (array)
|
||||
|
||||
# CORRECT: Match return type to SQL result
|
||||
return:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
source:
|
||||
code: SELECT * FROM table
|
||||
```
|
||||
|
||||
**Error**: "Expected string, got number"
|
||||
```yaml
|
||||
# WRONG: Parameter type doesn't match usage
|
||||
parameters:
|
||||
- name: age
|
||||
type: string
|
||||
source:
|
||||
code: SELECT * FROM users WHERE age > $age # Numeric comparison
|
||||
|
||||
# CORRECT: Use appropriate type
|
||||
parameters:
|
||||
- name: age
|
||||
type: integer
|
||||
source:
|
||||
code: SELECT * FROM users WHERE age > $age
|
||||
```
|
||||
|
||||
### Python Import Errors
|
||||
|
||||
**Error**: "ModuleNotFoundError: No module named 'pandas'"
|
||||
```bash
|
||||
# WRONG: Library not installed OR virtual environment not active
|
||||
import pandas as pd
|
||||
|
||||
# FIX:
|
||||
# 1. Ensure virtual environment is active
|
||||
source .venv/bin/activate
|
||||
|
||||
# 2. Add to requirements.txt
|
||||
echo "pandas>=2.0.0" >> requirements.txt
|
||||
|
||||
# 3. Install using uv
|
||||
uv pip install pandas
|
||||
```
|
||||
|
||||
**Error**: "ImportError: cannot import name 'db' from 'mxcp.runtime'"
|
||||
```python
|
||||
# WRONG: Import path incorrect
|
||||
from mxcp import db
|
||||
|
||||
# CORRECT: Import from runtime
|
||||
from mxcp.runtime import db
|
||||
```
|
||||
|
||||
### Python Code Quality Errors
|
||||
|
||||
**Error**: Black formatting fails with "INTERNAL ERROR"
|
||||
```bash
|
||||
# WRONG: Syntax error in Python code
|
||||
# FIX: Check syntax first
|
||||
python -m py_compile python/your_file.py
|
||||
# Fix syntax errors, then run black
|
||||
black python/
|
||||
```
|
||||
|
||||
**Error**: Pyright shows "Type of 'any' is unknown"
|
||||
```python
|
||||
# WRONG: Using lowercase 'any'
|
||||
def get_data() -> Dict[str, any]:
|
||||
pass
|
||||
|
||||
# CORRECT: Use 'Any' from typing
|
||||
from typing import Dict, Any
|
||||
def get_data() -> Dict[str, Any]:
|
||||
pass
|
||||
```
|
||||
|
||||
**Error**: "command not found: mxcp"
|
||||
```bash
|
||||
# WRONG: Virtual environment not active
|
||||
mxcp validate
|
||||
|
||||
# FIX: Activate virtual environment
|
||||
source .venv/bin/activate
|
||||
which mxcp # Should show: /path/to/project/.venv/bin/mxcp
|
||||
mxcp validate
|
||||
```
|
||||
|
||||
### dbt Errors
|
||||
|
||||
**Error**: "Seed file not found"
|
||||
```bash
|
||||
# WRONG: File not in seeds/ directory
|
||||
dbt seed --select data
|
||||
|
||||
# FIX: Check file location
|
||||
ls seeds/
|
||||
# Ensure data.csv exists in seeds/
|
||||
|
||||
# Or check seed name matches filename
|
||||
# seeds/my_data.csv → dbt seed --select my_data
|
||||
```
|
||||
|
||||
**Error**: "Test failed: unique_column_id"
|
||||
```yaml
|
||||
# Data has duplicates
|
||||
# FIX: Clean data or remove test
|
||||
seeds:
|
||||
- name: data
|
||||
columns:
|
||||
- name: id
|
||||
tests: [unique] # Remove if duplicates are valid
|
||||
```
|
||||
|
||||
## Debugging Workflow
|
||||
|
||||
When something doesn't work:
|
||||
|
||||
### Step 1: Identify the Layer
|
||||
|
||||
- **YAML layer**: `mxcp validate` fails → YAML structure issue
|
||||
- **SQL layer**: `mxcp test` fails but validate passes → SQL issue
|
||||
- **Data layer**: SQL syntax OK but wrong results → Data issue
|
||||
- **Type layer**: Runtime error about types → Type mismatch
|
||||
- **Python layer**: Import or runtime error → Python code issue
|
||||
|
||||
### Step 2: Isolate the Problem
|
||||
|
||||
```bash
|
||||
# Test YAML structure
|
||||
mxcp validate --debug
|
||||
|
||||
# Test SQL directly
|
||||
mxcp query "SELECT * FROM table LIMIT 5"
|
||||
|
||||
# Test tool with minimal input
|
||||
mxcp run tool my_tool --param key=simple_value
|
||||
|
||||
# Check logs
|
||||
mxcp serve --debug
|
||||
# Look for error messages
|
||||
```
|
||||
|
||||
### Step 3: Fix Incrementally
|
||||
|
||||
1. **Fix one error at a time**
|
||||
2. **Re-validate after each fix**
|
||||
3. **Don't move forward until green**
|
||||
|
||||
### Step 4: Verify Fix
|
||||
|
||||
```bash
|
||||
# After fixing, run full suite
|
||||
mxcp validate && mxcp test && mxcp lint
|
||||
|
||||
# If all pass, manual test
|
||||
mxcp run tool my_tool --param key=test_value
|
||||
```
|
||||
|
||||
## Self-Checking for Agents
|
||||
|
||||
**Before declaring a project complete, agent must verify:**
|
||||
|
||||
### 0. Is virtual environment set up? (CRITICAL)
|
||||
```bash
|
||||
# Check virtual environment exists
|
||||
ls .venv/bin/activate # Must exist
|
||||
|
||||
# Activate it
|
||||
source .venv/bin/activate
|
||||
|
||||
# Verify activation
|
||||
echo $VIRTUAL_ENV # Must show: /path/to/project/.venv
|
||||
which python # Must show: /path/to/project/.venv/bin/python
|
||||
```
|
||||
|
||||
### 1. Can project be initialized?
|
||||
```bash
|
||||
cd project-directory
|
||||
ls mxcp-site.yml # Must exist
|
||||
```
|
||||
|
||||
### 2. Python code quality passes? (if Python tools exist)
|
||||
```bash
|
||||
# Ensure venv active first
|
||||
source .venv/bin/activate
|
||||
|
||||
# Check formatting
|
||||
black --check python/
|
||||
# Exit code 0 = success
|
||||
|
||||
# Check types
|
||||
pyright python/
|
||||
# Must show: "0 errors, 0 warnings, 0 informations"
|
||||
|
||||
# Check tests
|
||||
pytest tests/ -v
|
||||
# All tests show PASSED
|
||||
```
|
||||
|
||||
### 3. Does MXCP validation pass?
|
||||
```bash
|
||||
# Ensure venv active
|
||||
source .venv/bin/activate
|
||||
|
||||
mxcp validate
|
||||
# Exit code 0 = success
|
||||
```
|
||||
|
||||
### 4. Do MXCP tests pass?
|
||||
```bash
|
||||
# Ensure venv active
|
||||
source .venv/bin/activate
|
||||
|
||||
mxcp test
|
||||
# All tests show PASSED
|
||||
```
|
||||
|
||||
### 5. Can tools be executed?
|
||||
```bash
|
||||
# Ensure venv active
|
||||
source .venv/bin/activate
|
||||
|
||||
mxcp run tool <each_tool> --param key=value
|
||||
# Returns data without errors
|
||||
```
|
||||
|
||||
### 6. Is configuration complete?
|
||||
```bash
|
||||
ls config.yml # Exists
|
||||
cat config.yml | grep "mxcp: 1" # Valid
|
||||
```
|
||||
|
||||
### 7. Are dependencies listed?
|
||||
```bash
|
||||
# Must have requirements.txt with all dependencies
|
||||
ls requirements.txt # Exists
|
||||
cat requirements.txt # Has mxcp, black, pyright, pytest
|
||||
```
|
||||
|
||||
### 8. Can server start?
|
||||
```bash
|
||||
# Ensure venv active
|
||||
source .venv/bin/activate
|
||||
|
||||
timeout 5 mxcp serve --transport http --port 8080 || true
|
||||
# Should start without immediate errors
|
||||
```
|
||||
|
||||
## Retry Strategy
|
||||
|
||||
If validation fails:
|
||||
|
||||
### Attempt 1: Fix Based on Error Message
|
||||
- Read error message carefully
|
||||
- Apply specific fix
|
||||
- Re-validate
|
||||
|
||||
### Attempt 2: Check Examples
|
||||
- Compare with working examples
|
||||
- Verify structure matches pattern
|
||||
- Re-validate
|
||||
|
||||
### Attempt 3: Simplify
|
||||
- Remove optional features
|
||||
- Test minimal version
|
||||
- Add features back incrementally
|
||||
|
||||
### If Still Failing:
|
||||
- Report exact error to user
|
||||
- Provide working minimal example
|
||||
- Ask for clarification on requirements
|
||||
|
||||
## Summary: The Golden Rule
|
||||
|
||||
**Build → Validate → Test → Verify → THEN Next**
|
||||
|
||||
Never skip steps. Never batch multiple tools without validating each one. Always verify before declaring done.
|
||||
|
||||
**If validation fails, the project is NOT done. Fix until all checks pass.**
|
||||
264
skills/mxcp-expert/references/claude-desktop.md
Normal file
264
skills/mxcp-expert/references/claude-desktop.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# Claude Desktop Integration
|
||||
|
||||
Guide to connecting MXCP servers with Claude Desktop.
|
||||
|
||||
## Quick Setup
|
||||
|
||||
### 1. Initialize MXCP Project
|
||||
|
||||
```bash
|
||||
mkdir my-mxcp-tools && cd my-mxcp-tools
|
||||
mxcp init --bootstrap
|
||||
```
|
||||
|
||||
The `--bootstrap` flag automatically creates `server_config.json` with the correct configuration for your environment.
|
||||
|
||||
### 2. Locate Claude Config
|
||||
|
||||
**macOS**:
|
||||
```
|
||||
~/Library/Application Support/Claude/claude_desktop_config.json
|
||||
```
|
||||
|
||||
**Windows**:
|
||||
```
|
||||
%APPDATA%\Claude\claude_desktop_config.json
|
||||
```
|
||||
|
||||
**Linux**:
|
||||
```
|
||||
~/.config/Claude/claude_desktop_config.json
|
||||
```
|
||||
|
||||
### 3. Add MXCP Server
|
||||
|
||||
Edit the Claude config file:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-tools": {
|
||||
"command": "mxcp",
|
||||
"args": ["serve", "--transport", "stdio"],
|
||||
"cwd": "/absolute/path/to/my-mxcp-tools"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Important**: Use absolute paths for `cwd`.
|
||||
|
||||
### 4. Restart Claude Desktop
|
||||
|
||||
Close and reopen Claude Desktop. Your tools should now be available.
|
||||
|
||||
## Verifying Connection
|
||||
|
||||
### Check Tool Availability
|
||||
|
||||
Ask Claude:
|
||||
- "What tools do you have available?"
|
||||
- "List the MXCP tools you can use"
|
||||
|
||||
### Test a Tool
|
||||
|
||||
Ask Claude to use one of your tools:
|
||||
- "Use the hello_world tool with name='Claude'"
|
||||
- "Show me what the calculate_fibonacci tool does"
|
||||
|
||||
## Environment-Specific Configurations
|
||||
|
||||
### Virtual Environment
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-tools": {
|
||||
"command": "/path/to/venv/bin/mxcp",
|
||||
"args": ["serve", "--transport", "stdio"],
|
||||
"cwd": "/path/to/project"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Poetry Project
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-tools": {
|
||||
"command": "poetry",
|
||||
"args": ["run", "mxcp", "serve", "--transport", "stdio"],
|
||||
"cwd": "/path/to/project"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### System-Wide Installation
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-tools": {
|
||||
"command": "mxcp",
|
||||
"args": ["serve", "--transport", "stdio"],
|
||||
"cwd": "/path/to/project"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Multiple MCP Servers
|
||||
|
||||
You can connect multiple MXCP projects:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"company-data": {
|
||||
"command": "mxcp",
|
||||
"args": ["serve", "--transport", "stdio"],
|
||||
"cwd": "/path/to/company-data-project"
|
||||
},
|
||||
"ml-tools": {
|
||||
"command": "mxcp",
|
||||
"args": ["serve", "--transport", "stdio"],
|
||||
"cwd": "/path/to/ml-tools-project"
|
||||
},
|
||||
"external-apis": {
|
||||
"command": "mxcp",
|
||||
"args": ["serve", "--transport", "stdio"],
|
||||
"cwd": "/path/to/external-apis-project"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Using Profiles
|
||||
|
||||
Connect to different environments:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"company-dev": {
|
||||
"command": "mxcp",
|
||||
"args": ["serve", "--transport", "stdio", "--profile", "dev"],
|
||||
"cwd": "/path/to/project"
|
||||
},
|
||||
"company-prod": {
|
||||
"command": "mxcp",
|
||||
"args": ["serve", "--transport", "stdio", "--profile", "prod"],
|
||||
"cwd": "/path/to/project"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Tools Not Appearing
|
||||
|
||||
1. Check Claude config syntax:
|
||||
```bash
|
||||
cat ~/Library/Application\ Support/Claude/claude_desktop_config.json | jq
|
||||
```
|
||||
|
||||
2. Verify MXCP installation:
|
||||
```bash
|
||||
which mxcp
|
||||
mxcp --version
|
||||
```
|
||||
|
||||
3. Test server manually:
|
||||
```bash
|
||||
cd /path/to/project
|
||||
mxcp serve --transport stdio
|
||||
# Should wait for input
|
||||
# Press Ctrl+C to exit
|
||||
```
|
||||
|
||||
4. Check project structure:
|
||||
```bash
|
||||
ls -la /path/to/project
|
||||
# Should see mxcp-site.yml and tools/ directory
|
||||
```
|
||||
|
||||
### Connection Errors
|
||||
|
||||
**Error: Command not found**
|
||||
- Use absolute path to mxcp executable
|
||||
- Check virtual environment activation
|
||||
|
||||
**Error: Permission denied**
|
||||
- Ensure mxcp executable has execute permissions
|
||||
- Check directory permissions
|
||||
|
||||
**Error: No tools available**
|
||||
- Verify `tools/` directory exists
|
||||
- Run `mxcp validate` to check endpoints
|
||||
- Check `mxcp-site.yml` configuration
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable debug logging:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-tools": {
|
||||
"command": "mxcp",
|
||||
"args": ["serve", "--transport", "stdio", "--debug"],
|
||||
"cwd": "/path/to/project"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Check Claude logs:
|
||||
- macOS: `~/Library/Logs/Claude/`
|
||||
- Windows: `%APPDATA%\Claude\logs\`
|
||||
- Linux: `~/.config/Claude/logs/`
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use --bootstrap** - Creates correct config automatically
|
||||
2. **Absolute Paths** - Always use absolute paths in config
|
||||
3. **Test Locally** - Run `mxcp serve` manually before adding to Claude
|
||||
4. **Multiple Projects** - Organize related tools in separate projects
|
||||
5. **Profiles** - Use different profiles for dev/staging/prod
|
||||
6. **Validation** - Run `mxcp validate` before deployment
|
||||
7. **Version Control** - Keep `server_config.json` in .gitignore
|
||||
|
||||
## Example Workflow
|
||||
|
||||
```bash
|
||||
# 1. Create project
|
||||
mkdir my-tools && cd my-tools
|
||||
mxcp init --bootstrap
|
||||
|
||||
# 2. Test locally
|
||||
mxcp serve
|
||||
# Ctrl+C to exit
|
||||
|
||||
# 3. Copy config path from server_config.json
|
||||
cat server_config.json
|
||||
|
||||
# 4. Edit Claude config
|
||||
vim ~/Library/Application\ Support/Claude/claude_desktop_config.json
|
||||
|
||||
# 5. Restart Claude Desktop
|
||||
|
||||
# 6. Test in Claude
|
||||
# Ask: "What tools do you have?"
|
||||
```
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Never commit API keys in Claude config
|
||||
- Use secrets management (Vault, 1Password)
|
||||
- Set appropriate file permissions
|
||||
- Use read-only mode for production: `--readonly`
|
||||
- Enable audit logging for compliance
|
||||
432
skills/mxcp-expert/references/cli-reference.md
Normal file
432
skills/mxcp-expert/references/cli-reference.md
Normal file
@@ -0,0 +1,432 @@
|
||||
# CLI Reference
|
||||
|
||||
Quick reference for MXCP command-line interface.
|
||||
|
||||
## Core Commands
|
||||
|
||||
### mxcp init
|
||||
|
||||
Initialize new MXCP project.
|
||||
|
||||
```bash
|
||||
mxcp init # Current directory
|
||||
mxcp init my-project # New directory
|
||||
mxcp init --bootstrap # With examples
|
||||
mxcp init --project=myapp --profile=dev
|
||||
```
|
||||
|
||||
### mxcp serve
|
||||
|
||||
Start MCP server.
|
||||
|
||||
```bash
|
||||
mxcp serve # Use config defaults
|
||||
mxcp serve --transport stdio # For Claude Desktop
|
||||
mxcp serve --transport http --port 8080
|
||||
mxcp serve --profile production
|
||||
mxcp serve --sql-tools true # Enable SQL query tools
|
||||
mxcp serve --readonly # Read-only database
|
||||
mxcp serve --stateless # For serverless deployment
|
||||
mxcp serve --debug # Debug mode
|
||||
```
|
||||
|
||||
### mxcp list
|
||||
|
||||
List available endpoints.
|
||||
|
||||
```bash
|
||||
mxcp list # All endpoints
|
||||
mxcp list --json-output # JSON format
|
||||
mxcp list --profile prod # Specific profile
|
||||
```
|
||||
|
||||
### mxcp run
|
||||
|
||||
Execute endpoint.
|
||||
|
||||
```bash
|
||||
# Tools
|
||||
mxcp run tool my_tool --param name=value
|
||||
|
||||
# Resources
|
||||
mxcp run resource my_resource --param id=123
|
||||
|
||||
# Prompts
|
||||
mxcp run prompt my_prompt --param text="hello"
|
||||
|
||||
# Complex parameters from JSON file
|
||||
mxcp run tool analyze --param data=@input.json
|
||||
|
||||
# With user context
|
||||
mxcp run tool secure_data --user-context '{"role": "admin"}'
|
||||
|
||||
# Read-only mode
|
||||
mxcp run tool query_data --readonly
|
||||
```
|
||||
|
||||
## Quality Commands
|
||||
|
||||
### mxcp validate
|
||||
|
||||
Check structure and types.
|
||||
|
||||
```bash
|
||||
mxcp validate # All endpoints
|
||||
mxcp validate my_tool # Specific endpoint
|
||||
mxcp validate --json-output # JSON format
|
||||
mxcp validate --readonly # Read-only database
|
||||
```
|
||||
|
||||
### mxcp test
|
||||
|
||||
Run endpoint tests.
|
||||
|
||||
```bash
|
||||
mxcp test # All tests
|
||||
mxcp test tool my_tool # Specific endpoint
|
||||
mxcp test --user-context '{"role": "admin"}'
|
||||
mxcp test --user-context @user.json
|
||||
mxcp test --json-output
|
||||
mxcp test --readonly
|
||||
```
|
||||
|
||||
### mxcp lint
|
||||
|
||||
Check metadata quality.
|
||||
|
||||
```bash
|
||||
mxcp lint # All endpoints
|
||||
mxcp lint --severity warning # Warnings only
|
||||
mxcp lint --severity info # All issues
|
||||
mxcp lint --json-output
|
||||
```
|
||||
|
||||
### mxcp evals
|
||||
|
||||
Test LLM behavior.
|
||||
|
||||
```bash
|
||||
mxcp evals # All eval suites
|
||||
mxcp evals safety_checks # Specific suite
|
||||
mxcp evals --model gpt-4o # Override model
|
||||
mxcp evals --user-context '{"role": "user"}'
|
||||
mxcp evals --json-output
|
||||
```
|
||||
|
||||
## Data Commands
|
||||
|
||||
### mxcp query
|
||||
|
||||
Execute SQL directly.
|
||||
|
||||
```bash
|
||||
mxcp query "SELECT * FROM users"
|
||||
mxcp query "SELECT * FROM sales WHERE region = $region" --param region=US
|
||||
mxcp query --file query.sql
|
||||
mxcp query --file query.sql --param date=@dates.json
|
||||
mxcp query "SELECT COUNT(*) FROM data" --json-output
|
||||
mxcp query "SELECT * FROM users" --readonly
|
||||
```
|
||||
|
||||
### mxcp dbt
|
||||
|
||||
Run dbt commands.
|
||||
|
||||
```bash
|
||||
mxcp dbt run # Run all models
|
||||
mxcp dbt run --select model
|
||||
mxcp dbt test # Run tests
|
||||
mxcp dbt docs generate # Generate docs
|
||||
mxcp dbt-config # Generate dbt config
|
||||
mxcp dbt-config --dry-run # Preview config
|
||||
```
|
||||
|
||||
### mxcp drift-snapshot
|
||||
|
||||
Create baseline snapshot.
|
||||
|
||||
```bash
|
||||
mxcp drift-snapshot # Default profile
|
||||
mxcp drift-snapshot --profile prod
|
||||
mxcp drift-snapshot --force # Overwrite existing
|
||||
mxcp drift-snapshot --dry-run
|
||||
```
|
||||
|
||||
### mxcp drift-check
|
||||
|
||||
Check for changes.
|
||||
|
||||
```bash
|
||||
mxcp drift-check # Use default baseline
|
||||
mxcp drift-check --baseline path/to/snapshot.json
|
||||
mxcp drift-check --profile prod
|
||||
mxcp drift-check --json-output
|
||||
mxcp drift-check --readonly
|
||||
```
|
||||
|
||||
## Monitoring Commands
|
||||
|
||||
### mxcp log
|
||||
|
||||
Query audit logs.
|
||||
|
||||
```bash
|
||||
# Basic queries
|
||||
mxcp log # Recent logs
|
||||
mxcp log --since 1h # Last hour
|
||||
mxcp log --since 2d # Last 2 days
|
||||
|
||||
# Filtering
|
||||
mxcp log --tool my_tool # Specific tool
|
||||
mxcp log --resource my_resource
|
||||
mxcp log --prompt my_prompt
|
||||
mxcp log --type tool # By type
|
||||
mxcp log --status error # Errors only
|
||||
mxcp log --status success # Successes only
|
||||
mxcp log --policy deny # Denied by policy
|
||||
|
||||
# Output
|
||||
mxcp log --limit 50 # Limit results
|
||||
mxcp log --json # JSON format
|
||||
mxcp log --export-csv audit.csv
|
||||
mxcp log --export-duckdb audit.db
|
||||
|
||||
# Combined filters
|
||||
mxcp log --since 1d --tool my_tool --status error
|
||||
```
|
||||
|
||||
### mxcp log-cleanup
|
||||
|
||||
Apply retention policies.
|
||||
|
||||
```bash
|
||||
mxcp log-cleanup # Apply retention
|
||||
mxcp log-cleanup --dry-run # Preview deletions
|
||||
mxcp log-cleanup --profile prod
|
||||
mxcp log-cleanup --json
|
||||
```
|
||||
|
||||
## Common Options
|
||||
|
||||
Available for most commands:
|
||||
|
||||
```bash
|
||||
--profile PROFILE # Use specific profile
|
||||
--json-output # Output as JSON
|
||||
--debug # Debug logging
|
||||
--readonly # Read-only database access
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# Config location (use project-local config)
|
||||
export MXCP_CONFIG=./config.yml
|
||||
# Or for global config (user manually copies)
|
||||
# export MXCP_CONFIG=~/.mxcp/config.yml
|
||||
|
||||
# Default profile
|
||||
export MXCP_PROFILE=production
|
||||
|
||||
# Debug mode
|
||||
export MXCP_DEBUG=1
|
||||
|
||||
# Read-only mode
|
||||
export MXCP_READONLY=1
|
||||
|
||||
# Override database path
|
||||
export MXCP_DUCKDB_PATH=/path/to/custom.duckdb
|
||||
|
||||
# Disable analytics
|
||||
export MXCP_DISABLE_ANALYTICS=1
|
||||
```
|
||||
|
||||
## Time Formats
|
||||
|
||||
For `--since` option:
|
||||
|
||||
```bash
|
||||
10s # 10 seconds
|
||||
5m # 5 minutes
|
||||
2h # 2 hours
|
||||
1d # 1 day
|
||||
3w # 3 weeks
|
||||
```
|
||||
|
||||
## Exit Codes
|
||||
|
||||
- `0` - Success
|
||||
- `1` - Error or validation failure
|
||||
- `2` - Invalid arguments
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Project Structure Enforcement
|
||||
|
||||
**CRITICAL**: MXCP enforces organized directory structure. Files in wrong locations are **ignored**.
|
||||
|
||||
Required structure:
|
||||
- Tools: `tools/*.yml`
|
||||
- Resources: `resources/*.yml`
|
||||
- Prompts: `prompts/*.yml`
|
||||
- Python: `python/*.py`
|
||||
- SQL: `sql/*.sql`
|
||||
|
||||
Use `mxcp init --bootstrap` to create proper structure.
|
||||
|
||||
### Profile Configuration (mxcp-site.yml)
|
||||
|
||||
```yaml
|
||||
mxcp: 1
|
||||
project: my-project
|
||||
|
||||
# Generic SQL tools for database exploration (optional)
|
||||
sql_tools:
|
||||
enabled: true
|
||||
|
||||
profiles:
|
||||
default:
|
||||
database:
|
||||
path: data.duckdb
|
||||
|
||||
production:
|
||||
# Authentication
|
||||
auth:
|
||||
provider: github
|
||||
# OAuth config in project config.yml
|
||||
|
||||
# Audit logging
|
||||
audit:
|
||||
enabled: true
|
||||
path: audit/logs.jsonl
|
||||
retention_days: 90
|
||||
|
||||
# OpenTelemetry observability
|
||||
telemetry:
|
||||
enabled: true
|
||||
endpoint: "http://otel-collector:4318"
|
||||
|
||||
# Policy enforcement
|
||||
policies:
|
||||
strict_mode: true
|
||||
|
||||
# Database
|
||||
database:
|
||||
path: /app/data/production.duckdb
|
||||
```
|
||||
|
||||
### Configuration Details
|
||||
|
||||
**Telemetry (OpenTelemetry)**:
|
||||
```yaml
|
||||
profiles:
|
||||
production:
|
||||
telemetry:
|
||||
enabled: true
|
||||
endpoint: "http://localhost:4318" # OTLP endpoint
|
||||
# Optional: service name
|
||||
service_name: "my-mxcp-server"
|
||||
```
|
||||
|
||||
Provides:
|
||||
- Distributed tracing for requests
|
||||
- Performance metrics
|
||||
- Integration with Jaeger, Grafana, etc.
|
||||
|
||||
**Stateless Mode** (`--stateless` flag):
|
||||
- For Claude.ai and serverless deployments
|
||||
- Disables state that persists across requests
|
||||
- Use for horizontal scaling
|
||||
|
||||
**SQL Tools**:
|
||||
|
||||
Generic SQL tools provide built-in database exploration capabilities for LLMs:
|
||||
- **`list_tables`** - View all available tables
|
||||
- **`get_table_schema`** - Examine table structure and columns
|
||||
- **`execute_sql_query`** - Run custom SQL queries
|
||||
|
||||
Enable via config file (recommended):
|
||||
```yaml
|
||||
# mxcp-site.yml
|
||||
sql_tools:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
Or via command-line flag:
|
||||
```bash
|
||||
mxcp serve --sql-tools true # Enable
|
||||
mxcp serve --sql-tools false # Disable (default)
|
||||
```
|
||||
|
||||
**Use cases:**
|
||||
- Natural language data exploration
|
||||
- Ad-hoc analysis and discovery
|
||||
- Prototyping query patterns
|
||||
- Working with dbt-transformed data
|
||||
|
||||
**Security:** Generic SQL tools allow arbitrary SQL execution. Use read-only database connections and consider policy-based restrictions for production deployments.
|
||||
|
||||
**Example:** See `assets/project-templates/covid_owid/` for complete implementation.
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
# Initialize with proper directory structure
|
||||
mxcp init --bootstrap
|
||||
|
||||
# Validate structure
|
||||
mxcp validate
|
||||
|
||||
# Test functionality
|
||||
mxcp test
|
||||
|
||||
# Check documentation
|
||||
mxcp lint
|
||||
|
||||
# Run locally with debug
|
||||
mxcp serve --debug
|
||||
```
|
||||
|
||||
### Deployment
|
||||
|
||||
```bash
|
||||
# Create snapshot
|
||||
mxcp drift-snapshot --profile prod
|
||||
|
||||
# Run tests
|
||||
mxcp test --profile prod
|
||||
|
||||
# Run evals
|
||||
mxcp evals --profile prod
|
||||
|
||||
# Deploy
|
||||
mxcp serve --profile prod
|
||||
```
|
||||
|
||||
### Monitoring
|
||||
|
||||
```bash
|
||||
# Check drift
|
||||
mxcp drift-check --profile prod
|
||||
|
||||
# View recent errors
|
||||
mxcp log --since 1h --status error
|
||||
|
||||
# Export audit trail
|
||||
mxcp log --since 7d --export-duckdb audit.db
|
||||
|
||||
# Clean old logs
|
||||
mxcp log-cleanup
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Use --debug** for troubleshooting
|
||||
2. **Test locally** before deployment
|
||||
3. **Use profiles** for different environments
|
||||
4. **Export logs** for analysis
|
||||
5. **Run drift checks** in CI/CD
|
||||
6. **Validate before committing**
|
||||
7. **Use --readonly** for query tools
|
||||
769
skills/mxcp-expert/references/comprehensive-testing-guide.md
Normal file
769
skills/mxcp-expert/references/comprehensive-testing-guide.md
Normal file
@@ -0,0 +1,769 @@
|
||||
# Comprehensive Testing Guide
|
||||
|
||||
**Complete testing strategy for MXCP servers: MXCP tests, Python unit tests, mocking, test databases, and concurrency safety.**
|
||||
|
||||
## Two Types of Tests
|
||||
|
||||
### 1. MXCP Tests (Integration Tests)
|
||||
|
||||
**Purpose**: Test the full tool/resource/prompt as it will be called by LLMs.
|
||||
|
||||
**Located**: In tool YAML files under `tests:` section
|
||||
|
||||
**Run with**: `mxcp test`
|
||||
|
||||
**Tests**:
|
||||
- Tool can be invoked with parameters
|
||||
- Return type matches specification
|
||||
- Result structure is correct
|
||||
- Parameter validation works
|
||||
|
||||
**Example**:
|
||||
```yaml
|
||||
# tools/get_customers.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: get_customers
|
||||
tests:
|
||||
- name: "basic_query"
|
||||
arguments:
|
||||
- key: city
|
||||
value: "Chicago"
|
||||
result:
|
||||
- customer_id: 3
|
||||
name: "Bob Johnson"
|
||||
```
|
||||
|
||||
### 2. Python Unit Tests (Isolation Tests)
|
||||
|
||||
**Purpose**: Test Python functions in isolation with mocking, edge cases, concurrency.
|
||||
|
||||
**Located**: In `tests/` directory (pytest format)
|
||||
|
||||
**Run with**: `pytest` or `python -m pytest`
|
||||
|
||||
**Tests**:
|
||||
- Function logic correctness
|
||||
- Edge cases and error handling
|
||||
- Mocked external dependencies
|
||||
- Concurrency safety
|
||||
- Result correctness verification
|
||||
|
||||
**Example**:
|
||||
```python
|
||||
# tests/test_api_wrapper.py
|
||||
import pytest
|
||||
from python.api_wrapper import fetch_users
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_users_correctness():
|
||||
"""Test that fetch_users returns correct structure"""
|
||||
result = await fetch_users(limit=5)
|
||||
|
||||
assert "users" in result
|
||||
assert "count" in result
|
||||
assert result["count"] == 5
|
||||
assert len(result["users"]) == 5
|
||||
assert all("id" in user for user in result["users"])
|
||||
```
|
||||
|
||||
## When to Use Which Tests
|
||||
|
||||
| Scenario | MXCP Tests | Python Unit Tests |
|
||||
|----------|------------|-------------------|
|
||||
| SQL-only tool | ✅ Required | ❌ Not applicable |
|
||||
| Python tool (no external calls) | ✅ Required | ✅ Recommended |
|
||||
| Python tool (with API calls) | ✅ Required | ✅ **Required** (with mocking) |
|
||||
| Python tool (with DB writes) | ✅ Required | ✅ **Required** (test DB) |
|
||||
| Python tool (async/concurrent) | ✅ Required | ✅ **Required** (concurrency tests) |
|
||||
|
||||
## Complete Testing Workflow
|
||||
|
||||
### Phase 1: MXCP Tests (Always First)
|
||||
|
||||
**For every tool, add test cases to YAML:**
|
||||
|
||||
```yaml
|
||||
tool:
|
||||
name: my_tool
|
||||
# ... definition ...
|
||||
tests:
|
||||
- name: "happy_path"
|
||||
arguments:
|
||||
- key: param1
|
||||
value: "test_value"
|
||||
result:
|
||||
expected_field: "expected_value"
|
||||
|
||||
- name: "edge_case_empty"
|
||||
arguments:
|
||||
- key: param1
|
||||
value: "nonexistent"
|
||||
result: []
|
||||
|
||||
- name: "missing_optional_param"
|
||||
arguments: []
|
||||
# Should work with defaults
|
||||
```
|
||||
|
||||
**Run**:
|
||||
```bash
|
||||
mxcp test tool my_tool
|
||||
```
|
||||
|
||||
### Phase 2: Python Unit Tests (For Python Tools)
|
||||
|
||||
**Create test file structure**:
|
||||
```bash
|
||||
mkdir -p tests
|
||||
touch tests/__init__.py
|
||||
touch tests/test_my_module.py
|
||||
```
|
||||
|
||||
**Write unit tests with pytest**:
|
||||
```python
|
||||
# tests/test_my_module.py
|
||||
import pytest
|
||||
from python.my_module import my_function
|
||||
|
||||
def test_my_function_correctness():
|
||||
"""Verify correct results"""
|
||||
result = my_function("input")
|
||||
assert result["key"] == "expected_value"
|
||||
assert len(result["items"]) == 5
|
||||
|
||||
def test_my_function_edge_cases():
|
||||
"""Test edge cases"""
|
||||
assert my_function("") == {"error": "Empty input"}
|
||||
assert my_function(None) == {"error": "Invalid input"}
|
||||
```
|
||||
|
||||
**Run**:
|
||||
```bash
|
||||
pytest tests/
|
||||
# Or with coverage
|
||||
pytest --cov=python tests/
|
||||
```
|
||||
|
||||
## Testing SQL Tools with Test Database
|
||||
|
||||
**CRITICAL**: SQL tools must be tested with real data to verify result correctness.
|
||||
|
||||
### Pattern 1: Use dbt Seeds for Test Data
|
||||
|
||||
```bash
|
||||
# 1. Create test data seed
|
||||
cat > seeds/test_data.csv <<'EOF'
|
||||
id,name,value
|
||||
1,test1,100
|
||||
2,test2,200
|
||||
3,test3,300
|
||||
EOF
|
||||
|
||||
# 2. Create schema
|
||||
cat > seeds/schema.yml <<'EOF'
|
||||
version: 2
|
||||
seeds:
|
||||
- name: test_data
|
||||
columns:
|
||||
- name: id
|
||||
tests: [unique, not_null]
|
||||
EOF
|
||||
|
||||
# 3. Load test data
|
||||
dbt seed --select test_data
|
||||
|
||||
# 4. Create tool with tests
|
||||
cat > tools/query_test_data.yml <<'EOF'
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: query_test_data
|
||||
parameters:
|
||||
- name: min_value
|
||||
type: integer
|
||||
return:
|
||||
type: array
|
||||
source:
|
||||
code: |
|
||||
SELECT * FROM test_data WHERE value >= $min_value
|
||||
tests:
|
||||
- name: "filter_200"
|
||||
arguments:
|
||||
- key: min_value
|
||||
value: 200
|
||||
result:
|
||||
- id: 2
|
||||
value: 200
|
||||
- id: 3
|
||||
value: 300
|
||||
EOF
|
||||
|
||||
# 5. Test
|
||||
mxcp test tool query_test_data
|
||||
```
|
||||
|
||||
### Pattern 2: Create Test Fixtures in SQL
|
||||
|
||||
```sql
|
||||
-- models/test_fixtures.sql
|
||||
{{ config(materialized='table') }}
|
||||
|
||||
-- Create predictable test data
|
||||
SELECT 1 as id, 'Alice' as name, 100 as score
|
||||
UNION ALL
|
||||
SELECT 2 as id, 'Bob' as name, 200 as score
|
||||
UNION ALL
|
||||
SELECT 3 as id, 'Charlie' as name, 150 as score
|
||||
```
|
||||
|
||||
```yaml
|
||||
# tools/top_scores.yml
|
||||
tool:
|
||||
name: top_scores
|
||||
source:
|
||||
code: |
|
||||
SELECT * FROM test_fixtures ORDER BY score DESC LIMIT $limit
|
||||
tests:
|
||||
- name: "top_2"
|
||||
arguments:
|
||||
- key: limit
|
||||
value: 2
|
||||
result:
|
||||
- id: 2
|
||||
name: "Bob"
|
||||
score: 200
|
||||
- id: 3
|
||||
name: "Charlie"
|
||||
score: 150
|
||||
```
|
||||
|
||||
### Pattern 3: Verify Aggregation Correctness
|
||||
|
||||
```yaml
|
||||
# tools/calculate_stats.yml
|
||||
tool:
|
||||
name: calculate_stats
|
||||
source:
|
||||
code: |
|
||||
SELECT
|
||||
COUNT(*) as total_count,
|
||||
SUM(score) as total_score,
|
||||
AVG(score) as avg_score,
|
||||
MAX(score) as max_score
|
||||
FROM test_fixtures
|
||||
tests:
|
||||
- name: "verify_aggregations"
|
||||
arguments: []
|
||||
result:
|
||||
- total_count: 3
|
||||
total_score: 450
|
||||
avg_score: 150.0
|
||||
max_score: 200
|
||||
```
|
||||
|
||||
**If aggregations don't match expected values, the SQL logic is WRONG.**
|
||||
|
||||
## Testing Python Tools with Mocking
|
||||
|
||||
**CRITICAL**: Python tools with external API calls MUST use mocking in tests.
|
||||
|
||||
### Pattern 1: Mock HTTP Calls with pytest-httpx
|
||||
|
||||
```bash
|
||||
# Install
|
||||
pip install pytest-httpx
|
||||
```
|
||||
|
||||
```python
|
||||
# python/api_client.py
|
||||
import httpx
|
||||
|
||||
async def fetch_external_data(api_key: str, user_id: int) -> dict:
|
||||
"""Fetch data from external API"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"https://api.example.com/users/{user_id}",
|
||||
headers={"Authorization": f"Bearer {api_key}"}
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
```
|
||||
|
||||
```python
|
||||
# tests/test_api_client.py
|
||||
import pytest
|
||||
from httpx import Response
|
||||
from python.api_client import fetch_external_data
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_external_data_success(httpx_mock):
|
||||
"""Test successful API call with mocked response"""
|
||||
# Mock the HTTP call
|
||||
httpx_mock.add_response(
|
||||
url="https://api.example.com/users/123",
|
||||
json={"id": 123, "name": "Test User", "email": "test@example.com"}
|
||||
)
|
||||
|
||||
# Call function
|
||||
result = await fetch_external_data("fake_api_key", 123)
|
||||
|
||||
# Verify correctness
|
||||
assert result["id"] == 123
|
||||
assert result["name"] == "Test User"
|
||||
assert result["email"] == "test@example.com"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_external_data_error(httpx_mock):
|
||||
"""Test API error handling"""
|
||||
httpx_mock.add_response(
|
||||
url="https://api.example.com/users/999",
|
||||
status_code=404,
|
||||
json={"error": "User not found"}
|
||||
)
|
||||
|
||||
# Should handle error gracefully
|
||||
with pytest.raises(httpx.HTTPStatusError):
|
||||
await fetch_external_data("fake_api_key", 999)
|
||||
```
|
||||
|
||||
### Pattern 2: Mock Database Calls
|
||||
|
||||
```python
|
||||
# python/db_operations.py
|
||||
from mxcp.runtime import db
|
||||
|
||||
def get_user_orders(user_id: int) -> list[dict]:
|
||||
"""Get orders for a user"""
|
||||
result = db.execute(
|
||||
"SELECT * FROM orders WHERE user_id = $1",
|
||||
{"user_id": user_id}
|
||||
)
|
||||
return result.fetchall()
|
||||
```
|
||||
|
||||
```python
|
||||
# tests/test_db_operations.py
|
||||
import pytest
|
||||
from unittest.mock import Mock, MagicMock
|
||||
from python.db_operations import get_user_orders
|
||||
|
||||
def test_get_user_orders(monkeypatch):
|
||||
"""Test with mocked database"""
|
||||
# Create mock result
|
||||
mock_result = MagicMock()
|
||||
mock_result.fetchall.return_value = [
|
||||
{"order_id": 1, "user_id": 123, "amount": 50.0},
|
||||
{"order_id": 2, "user_id": 123, "amount": 75.0}
|
||||
]
|
||||
|
||||
# Mock db.execute
|
||||
mock_db = Mock()
|
||||
mock_db.execute.return_value = mock_result
|
||||
|
||||
# Inject mock
|
||||
import python.db_operations
|
||||
monkeypatch.setattr(python.db_operations, "db", mock_db)
|
||||
|
||||
# Test
|
||||
orders = get_user_orders(123)
|
||||
|
||||
# Verify
|
||||
assert len(orders) == 2
|
||||
assert orders[0]["order_id"] == 1
|
||||
assert sum(o["amount"] for o in orders) == 125.0
|
||||
```
|
||||
|
||||
### Pattern 3: Mock Third-Party Libraries
|
||||
|
||||
```python
|
||||
# python/stripe_wrapper.py
|
||||
import stripe
|
||||
|
||||
def create_customer(email: str, name: str) -> dict:
|
||||
"""Create Stripe customer"""
|
||||
customer = stripe.Customer.create(email=email, name=name)
|
||||
return {"id": customer.id, "email": customer.email}
|
||||
```
|
||||
|
||||
```python
|
||||
# tests/test_stripe_wrapper.py
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch
|
||||
from python.stripe_wrapper import create_customer
|
||||
|
||||
@patch('stripe.Customer.create')
|
||||
def test_create_customer(mock_create):
|
||||
"""Test Stripe customer creation with mock"""
|
||||
# Mock Stripe response
|
||||
mock_customer = Mock()
|
||||
mock_customer.id = "cus_test123"
|
||||
mock_customer.email = "test@example.com"
|
||||
mock_create.return_value = mock_customer
|
||||
|
||||
# Call function
|
||||
result = create_customer("test@example.com", "Test User")
|
||||
|
||||
# Verify correctness
|
||||
assert result["id"] == "cus_test123"
|
||||
assert result["email"] == "test@example.com"
|
||||
|
||||
# Verify Stripe was called correctly
|
||||
mock_create.assert_called_once_with(
|
||||
email="test@example.com",
|
||||
name="Test User"
|
||||
)
|
||||
```
|
||||
|
||||
## Result Correctness Verification
|
||||
|
||||
**CRITICAL**: Tests must verify results are CORRECT, not just that code doesn't crash.
|
||||
|
||||
### Bad Test (Only checks structure):
|
||||
```python
|
||||
def test_calculate_total_bad():
|
||||
result = calculate_total([10, 20, 30])
|
||||
assert "total" in result # ❌ Doesn't verify correctness
|
||||
```
|
||||
|
||||
### Good Test (Verifies correct value):
|
||||
```python
|
||||
def test_calculate_total_good():
|
||||
result = calculate_total([10, 20, 30])
|
||||
assert result["total"] == 60 # ✅ Verifies correct calculation
|
||||
assert result["count"] == 3 # ✅ Verifies correct count
|
||||
assert result["average"] == 20.0 # ✅ Verifies correct average
|
||||
```
|
||||
|
||||
### Pattern: Test Edge Cases for Correctness
|
||||
|
||||
```python
|
||||
def test_aggregation_correctness():
|
||||
"""Test various aggregations for correctness"""
|
||||
data = [
|
||||
{"id": 1, "value": 100},
|
||||
{"id": 2, "value": 200},
|
||||
{"id": 3, "value": 150}
|
||||
]
|
||||
|
||||
result = aggregate_data(data)
|
||||
|
||||
# Verify each aggregation
|
||||
assert result["sum"] == 450 # 100 + 200 + 150
|
||||
assert result["avg"] == 150.0 # 450 / 3
|
||||
assert result["min"] == 100
|
||||
assert result["max"] == 200
|
||||
assert result["count"] == 3
|
||||
|
||||
# Verify derived values
|
||||
assert result["range"] == 100 # 200 - 100
|
||||
assert result["median"] == 150
|
||||
|
||||
def test_empty_data_correctness():
|
||||
"""Test edge case: empty data"""
|
||||
result = aggregate_data([])
|
||||
|
||||
assert result["sum"] == 0
|
||||
assert result["avg"] == 0.0
|
||||
assert result["count"] == 0
|
||||
# Ensure no crashes, correct behavior for empty data
|
||||
```
|
||||
|
||||
## Concurrency Safety for Python Tools
|
||||
|
||||
**CRITICAL**: MXCP tools run as a server - multiple requests can happen simultaneously.
|
||||
|
||||
### Common Concurrency Issues
|
||||
|
||||
#### ❌ WRONG: Global State with Race Conditions
|
||||
|
||||
```python
|
||||
# python/unsafe_counter.py
|
||||
counter = 0 # ❌ DANGER: Race condition!
|
||||
|
||||
def increment_counter() -> dict:
|
||||
global counter
|
||||
counter += 1 # ❌ Not thread-safe!
|
||||
return {"count": counter}
|
||||
|
||||
# Two simultaneous requests could both read counter=5,
|
||||
# both increment to 6, both write 6 -> one increment lost!
|
||||
```
|
||||
|
||||
#### ✅ CORRECT: Use Thread-Safe Approaches
|
||||
|
||||
**Option 1: Avoid shared state (stateless)**
|
||||
```python
|
||||
# python/safe_stateless.py
|
||||
def process_request(data: dict) -> dict:
|
||||
"""Completely stateless - safe for concurrent calls"""
|
||||
result = compute_something(data)
|
||||
return {"result": result}
|
||||
# No global state, no problem!
|
||||
```
|
||||
|
||||
**Option 2: Use thread-safe structures**
|
||||
```python
|
||||
# python/safe_with_lock.py
|
||||
import threading
|
||||
|
||||
counter_lock = threading.Lock()
|
||||
counter = 0
|
||||
|
||||
def increment_counter() -> dict:
|
||||
global counter
|
||||
with counter_lock: # ✅ Thread-safe
|
||||
counter += 1
|
||||
current = counter
|
||||
return {"count": current}
|
||||
```
|
||||
|
||||
**Option 3: Use atomic operations**
|
||||
```python
|
||||
# python/safe_atomic.py
|
||||
from threading import Lock
|
||||
from collections import defaultdict
|
||||
|
||||
# Thread-safe counter
|
||||
class SafeCounter:
|
||||
def __init__(self):
|
||||
self._value = 0
|
||||
self._lock = Lock()
|
||||
|
||||
def increment(self):
|
||||
with self._lock:
|
||||
self._value += 1
|
||||
return self._value
|
||||
|
||||
counter = SafeCounter()
|
||||
|
||||
def increment_counter() -> dict:
|
||||
return {"count": counter.increment()}
|
||||
```
|
||||
|
||||
### Concurrency-Safe Patterns
|
||||
|
||||
#### Pattern 1: Database as State (DuckDB is thread-safe)
|
||||
|
||||
```python
|
||||
# python/db_counter.py
|
||||
from mxcp.runtime import db
|
||||
|
||||
def increment_counter() -> dict:
|
||||
"""Use database for state - thread-safe"""
|
||||
db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS counter (
|
||||
id INTEGER PRIMARY KEY,
|
||||
value INTEGER
|
||||
)
|
||||
""")
|
||||
|
||||
db.execute("""
|
||||
INSERT INTO counter (id, value) VALUES (1, 1)
|
||||
ON CONFLICT(id) DO UPDATE SET value = value + 1
|
||||
""")
|
||||
|
||||
result = db.execute("SELECT value FROM counter WHERE id = 1")
|
||||
return {"count": result.fetchone()["value"]}
|
||||
```
|
||||
|
||||
#### Pattern 2: Local Variables Only (Immutable)
|
||||
|
||||
```python
|
||||
# python/safe_processing.py
|
||||
async def process_data(input_data: list[dict]) -> dict:
|
||||
"""Local variables only - safe for concurrent calls"""
|
||||
# All state is local to this function call
|
||||
results = []
|
||||
total = 0
|
||||
|
||||
for item in input_data:
|
||||
processed = transform(item) # Pure function
|
||||
results.append(processed)
|
||||
total += processed["value"]
|
||||
|
||||
return {
|
||||
"results": results,
|
||||
"total": total,
|
||||
"count": len(results)
|
||||
}
|
||||
# When function returns, all state is discarded
|
||||
```
|
||||
|
||||
#### Pattern 3: Async/Await (Concurrent, Not Parallel)
|
||||
|
||||
```python
|
||||
# python/safe_async.py
|
||||
import asyncio
|
||||
import httpx
|
||||
|
||||
async def fetch_multiple_users(user_ids: list[int]) -> list[dict]:
|
||||
"""Concurrent API calls - safe with async"""
|
||||
|
||||
async def fetch_one(user_id: int) -> dict:
|
||||
# Each call has its own context - no shared state
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"https://api.example.com/users/{user_id}")
|
||||
return response.json()
|
||||
|
||||
# Run concurrently, but each fetch_one is independent
|
||||
results = await asyncio.gather(*[fetch_one(uid) for uid in user_ids])
|
||||
return results
|
||||
```
|
||||
|
||||
### Testing Concurrency Safety
|
||||
|
||||
```python
|
||||
# tests/test_concurrency.py
|
||||
import pytest
|
||||
import asyncio
|
||||
from python.my_module import concurrent_function
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_calls_no_race_condition():
|
||||
"""Test that concurrent calls don't have race conditions"""
|
||||
|
||||
# Run function 100 times concurrently
|
||||
tasks = [concurrent_function(i) for i in range(100)]
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
# Verify all calls succeeded
|
||||
assert len(results) == 100
|
||||
|
||||
# Verify no data corruption
|
||||
assert all(isinstance(r, dict) for r in results)
|
||||
|
||||
# If function has a counter, verify correctness
|
||||
# (e.g., if each call increments, final count should be 100)
|
||||
|
||||
def test_parallel_execution_thread_safe():
|
||||
"""Test with actual threading"""
|
||||
import threading
|
||||
|
||||
results = []
|
||||
errors = []
|
||||
|
||||
def worker(n):
|
||||
try:
|
||||
result = my_function(n)
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
|
||||
# Create 50 threads
|
||||
threads = [threading.Thread(target=worker, args=(i,)) for i in range(50)]
|
||||
|
||||
# Start all threads
|
||||
for t in threads:
|
||||
t.start()
|
||||
|
||||
# Wait for completion
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
# Verify
|
||||
assert len(errors) == 0, f"Errors occurred: {errors}"
|
||||
assert len(results) == 50
|
||||
```
|
||||
|
||||
## Complete Testing Checklist
|
||||
|
||||
### For SQL Tools:
|
||||
|
||||
- [ ] MXCP test cases in YAML
|
||||
- [ ] Test with real seed data
|
||||
- [ ] Verify result correctness (exact values)
|
||||
- [ ] Test edge cases (empty results, NULL values)
|
||||
- [ ] Test filters work correctly
|
||||
- [ ] Test aggregations are mathematically correct
|
||||
- [ ] Test with dbt test for data quality
|
||||
|
||||
### For Python Tools (No External Calls):
|
||||
|
||||
- [ ] MXCP test cases in YAML
|
||||
- [ ] Python unit tests (pytest)
|
||||
- [ ] Verify result correctness
|
||||
- [ ] Test edge cases (empty input, NULL, invalid)
|
||||
- [ ] Test error handling
|
||||
- [ ] Test concurrency safety (if using shared state)
|
||||
|
||||
### For Python Tools (With External API Calls):
|
||||
|
||||
- [ ] MXCP test cases in YAML
|
||||
- [ ] Python unit tests with mocking (pytest + httpx_mock)
|
||||
- [ ] Mock all external API calls
|
||||
- [ ] Test success path with mocked responses
|
||||
- [ ] Test error cases (404, 500, timeout)
|
||||
- [ ] Verify correct API parameters
|
||||
- [ ] Test result correctness
|
||||
- [ ] Test concurrency (multiple simultaneous calls)
|
||||
|
||||
### For Python Tools (With Database Operations):
|
||||
|
||||
- [ ] MXCP test cases in YAML
|
||||
- [ ] Python unit tests
|
||||
- [ ] Use test fixtures/seed data
|
||||
- [ ] Verify query results correctness
|
||||
- [ ] Test transactions (if applicable)
|
||||
- [ ] Test concurrency (DuckDB is thread-safe)
|
||||
- [ ] Clean up test data after tests
|
||||
|
||||
## Project Structure for Testing
|
||||
|
||||
```
|
||||
project/
|
||||
├── mxcp-site.yml
|
||||
├── tools/
|
||||
│ └── my_tool.yml # Contains MXCP tests
|
||||
├── python/
|
||||
│ └── my_module.py # Python code
|
||||
├── tests/
|
||||
│ ├── __init__.py
|
||||
│ ├── test_my_module.py # Python unit tests
|
||||
│ ├── conftest.py # pytest fixtures
|
||||
│ └── fixtures/
|
||||
│ └── test_data.json # Test data
|
||||
├── seeds/
|
||||
│ ├── test_data.csv # Test database seeds
|
||||
│ └── schema.yml
|
||||
└── requirements.txt # Include: pytest, pytest-asyncio, pytest-httpx, pytest-cov
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# 1. MXCP tests (always run first)
|
||||
mxcp validate # Structure validation
|
||||
mxcp test # Integration tests
|
||||
|
||||
# 2. dbt tests (if using dbt)
|
||||
dbt test
|
||||
|
||||
# 3. Python unit tests
|
||||
pytest tests/ -v
|
||||
|
||||
# 4. With coverage report
|
||||
pytest tests/ --cov=python --cov-report=html
|
||||
|
||||
# 5. Concurrency stress test (custom)
|
||||
pytest tests/test_concurrency.py -v --count=100
|
||||
|
||||
# All together
|
||||
mxcp validate && mxcp test && dbt test && pytest tests/ -v
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
**Both types of tests are required**:
|
||||
|
||||
1. **MXCP tests** - Verify tools work end-to-end
|
||||
2. **Python unit tests** - Verify logic, mocking, correctness, concurrency
|
||||
|
||||
**Key principles**:
|
||||
- ✅ **Mock all external calls** - Use pytest-httpx, unittest.mock
|
||||
- ✅ **Verify result correctness** - Don't just check structure
|
||||
- ✅ **Use test databases** - SQL tools need real data
|
||||
- ✅ **Test concurrency** - Tools run as servers
|
||||
- ✅ **Avoid global mutable state** - Use stateless patterns or locks
|
||||
- ✅ **Test edge cases** - Empty data, NULL, invalid input
|
||||
|
||||
**Before declaring a project done, BOTH test types must pass completely.**
|
||||
842
skills/mxcp-expert/references/database-connections.md
Normal file
842
skills/mxcp-expert/references/database-connections.md
Normal file
@@ -0,0 +1,842 @@
|
||||
# Database Connections Guide
|
||||
|
||||
Complete guide for connecting MXCP to external databases (PostgreSQL, MySQL, SQLite, SQL Server) using DuckDB's ATTACH functionality and dbt integration.
|
||||
|
||||
## Overview
|
||||
|
||||
MXCP can connect to external databases in two ways:
|
||||
1. **Direct querying** via DuckDB ATTACH (read data from external databases)
|
||||
2. **dbt integration** (transform external data using dbt sources and models)
|
||||
|
||||
**Key principle**: External databases → DuckDB (via ATTACH or dbt) → MXCP tools
|
||||
|
||||
## When to Use Database Connections
|
||||
|
||||
**Use database connections when**:
|
||||
- You have existing data in PostgreSQL, MySQL, or other SQL databases
|
||||
- You want to query production databases (read-only recommended)
|
||||
- You need to join external data with local data
|
||||
- You want to cache/materialize external data locally
|
||||
|
||||
**Don't use database connections when**:
|
||||
- You can export data to CSV (use dbt seeds instead - simpler and safer)
|
||||
- You need real-time writes (MXCP is read-focused)
|
||||
- The database has complex security requirements (use API wrapper instead)
|
||||
|
||||
## Method 1: Direct Database Access with ATTACH
|
||||
|
||||
### PostgreSQL Connection
|
||||
|
||||
#### Basic ATTACH Syntax
|
||||
|
||||
```sql
|
||||
-- Attach PostgreSQL database
|
||||
INSTALL postgres;
|
||||
LOAD postgres;
|
||||
|
||||
ATTACH 'host=localhost port=5432 dbname=mydb user=myuser password=mypass'
|
||||
AS postgres_db (TYPE POSTGRES);
|
||||
|
||||
-- Query attached database
|
||||
SELECT * FROM postgres_db.public.customers WHERE country = 'US';
|
||||
```
|
||||
|
||||
#### Complete Working Example
|
||||
|
||||
**Project structure**:
|
||||
```
|
||||
postgres-query/
|
||||
├── mxcp-site.yml
|
||||
├── config.yml # Database credentials
|
||||
├── tools/
|
||||
│ ├── query_customers.yml
|
||||
│ └── get_orders.yml
|
||||
└── sql/
|
||||
└── setup.sql # ATTACH commands
|
||||
```
|
||||
|
||||
**Step 1: Create config.yml with database credentials**
|
||||
|
||||
```yaml
|
||||
# config.yml (in project directory)
|
||||
mxcp: 1
|
||||
|
||||
profiles:
|
||||
default:
|
||||
secrets:
|
||||
- name: postgres_connection
|
||||
type: env
|
||||
parameters:
|
||||
env_var: POSTGRES_CONNECTION_STRING
|
||||
|
||||
# Alternative: separate credentials
|
||||
- name: db_host
|
||||
type: env
|
||||
parameters:
|
||||
env_var: DB_HOST
|
||||
- name: db_user
|
||||
type: env
|
||||
parameters:
|
||||
env_var: DB_USER
|
||||
- name: db_password
|
||||
type: env
|
||||
parameters:
|
||||
env_var: DB_PASSWORD
|
||||
```
|
||||
|
||||
**Step 2: Set environment variables**
|
||||
|
||||
```bash
|
||||
# Option 1: Connection string
|
||||
export POSTGRES_CONNECTION_STRING="host=localhost port=5432 dbname=mydb user=myuser password=mypass"
|
||||
|
||||
# Option 2: Separate credentials
|
||||
export DB_HOST="localhost"
|
||||
export DB_USER="myuser"
|
||||
export DB_PASSWORD="mypass"
|
||||
```
|
||||
|
||||
**Step 3: Create SQL setup file**
|
||||
|
||||
```sql
|
||||
-- sql/setup.sql
|
||||
-- Install and load PostgreSQL extension
|
||||
INSTALL postgres;
|
||||
LOAD postgres;
|
||||
|
||||
-- Attach database (connection string from environment)
|
||||
ATTACH 'host=${DB_HOST} port=5432 dbname=mydb user=${DB_USER} password=${DB_PASSWORD}'
|
||||
AS prod_db (TYPE POSTGRES);
|
||||
```
|
||||
|
||||
**Step 4: Create query tool**
|
||||
|
||||
```yaml
|
||||
# tools/query_customers.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: query_customers
|
||||
description: "Query customers from PostgreSQL database by country"
|
||||
parameters:
|
||||
- name: country
|
||||
type: string
|
||||
description: "Filter by country code (e.g., 'US', 'UK')"
|
||||
required: false
|
||||
return:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
customer_id: { type: integer }
|
||||
name: { type: string }
|
||||
email: { type: string }
|
||||
country: { type: string }
|
||||
source:
|
||||
code: |
|
||||
-- First ensure PostgreSQL is attached
|
||||
INSTALL postgres;
|
||||
LOAD postgres;
|
||||
ATTACH IF NOT EXISTS 'host=${DB_HOST} port=5432 dbname=mydb user=${DB_USER} password=${DB_PASSWORD}'
|
||||
AS prod_db (TYPE POSTGRES);
|
||||
|
||||
-- Query attached database
|
||||
SELECT
|
||||
customer_id,
|
||||
name,
|
||||
email,
|
||||
country
|
||||
FROM prod_db.public.customers
|
||||
WHERE $country IS NULL OR country = $country
|
||||
ORDER BY customer_id
|
||||
LIMIT 1000
|
||||
tests:
|
||||
- name: "test_connection"
|
||||
arguments: []
|
||||
# Test will verify connection works
|
||||
```
|
||||
|
||||
**Step 5: Validate and test**
|
||||
|
||||
```bash
|
||||
# Set credentials
|
||||
export DB_HOST="localhost"
|
||||
export DB_USER="myuser"
|
||||
export DB_PASSWORD="mypass"
|
||||
|
||||
# Validate structure
|
||||
mxcp validate
|
||||
|
||||
# Test tool
|
||||
mxcp run tool query_customers --param country="US"
|
||||
|
||||
# Start server
|
||||
mxcp serve
|
||||
```
|
||||
|
||||
### MySQL Connection
|
||||
|
||||
```sql
|
||||
-- Install MySQL extension
|
||||
INSTALL mysql;
|
||||
LOAD mysql;
|
||||
|
||||
-- Attach MySQL database
|
||||
ATTACH 'host=localhost port=3306 database=mydb user=root password=pass'
|
||||
AS mysql_db (TYPE MYSQL);
|
||||
|
||||
-- Query
|
||||
SELECT * FROM mysql_db.orders WHERE order_date >= '2024-01-01';
|
||||
```
|
||||
|
||||
**Complete tool example**:
|
||||
|
||||
```yaml
|
||||
# tools/query_mysql_orders.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: query_mysql_orders
|
||||
description: "Query orders from MySQL database"
|
||||
parameters:
|
||||
- name: start_date
|
||||
type: string
|
||||
format: date
|
||||
required: false
|
||||
- name: status
|
||||
type: string
|
||||
required: false
|
||||
return:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
source:
|
||||
code: |
|
||||
INSTALL mysql;
|
||||
LOAD mysql;
|
||||
ATTACH IF NOT EXISTS 'host=${MYSQL_HOST} database=${MYSQL_DB} user=${MYSQL_USER} password=${MYSQL_PASSWORD}'
|
||||
AS mysql_db (TYPE MYSQL);
|
||||
|
||||
SELECT
|
||||
order_id,
|
||||
customer_id,
|
||||
order_date,
|
||||
total_amount,
|
||||
status
|
||||
FROM mysql_db.orders
|
||||
WHERE ($start_date IS NULL OR order_date >= $start_date)
|
||||
AND ($status IS NULL OR status = $status)
|
||||
ORDER BY order_date DESC
|
||||
LIMIT 1000
|
||||
```
|
||||
|
||||
### SQLite Connection
|
||||
|
||||
```sql
|
||||
-- Attach SQLite database
|
||||
ATTACH 'path/to/database.db' AS sqlite_db (TYPE SQLITE);
|
||||
|
||||
-- Query
|
||||
SELECT * FROM sqlite_db.users WHERE active = true;
|
||||
```
|
||||
|
||||
**Tool example**:
|
||||
|
||||
```yaml
|
||||
# tools/query_sqlite.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: query_sqlite_users
|
||||
description: "Query users from SQLite database"
|
||||
parameters:
|
||||
- name: active_only
|
||||
type: boolean
|
||||
default: true
|
||||
return:
|
||||
type: array
|
||||
source:
|
||||
code: |
|
||||
ATTACH IF NOT EXISTS '${SQLITE_DB_PATH}' AS sqlite_db (TYPE SQLITE);
|
||||
|
||||
SELECT user_id, username, email, created_at
|
||||
FROM sqlite_db.users
|
||||
WHERE $active_only = false OR active = true
|
||||
ORDER BY created_at DESC
|
||||
```
|
||||
|
||||
### SQL Server Connection
|
||||
|
||||
```sql
|
||||
-- Install SQL Server extension
|
||||
INSTALL sqlserver;
|
||||
LOAD sqlserver;
|
||||
|
||||
-- Attach SQL Server database
|
||||
ATTACH 'Server=localhost;Database=mydb;Uid=user;Pwd=pass;'
|
||||
AS sqlserver_db (TYPE SQLSERVER);
|
||||
|
||||
-- Query
|
||||
SELECT * FROM sqlserver_db.dbo.products WHERE category = 'Electronics';
|
||||
```
|
||||
|
||||
## Method 2: dbt Integration with External Databases
|
||||
|
||||
**Use dbt when**:
|
||||
- You want to materialize/cache external data locally
|
||||
- You need to transform external data before querying
|
||||
- You want data quality tests on external data
|
||||
- You prefer declarative SQL over ATTACH statements
|
||||
|
||||
### dbt Sources for External Databases
|
||||
|
||||
**Pattern**: External DB → dbt source → dbt model → MXCP tool
|
||||
|
||||
#### Step 1: Configure dbt profile for external database
|
||||
|
||||
```yaml
|
||||
# profiles.yml (auto-generated by MXCP, or manually edit)
|
||||
my_project:
|
||||
outputs:
|
||||
dev:
|
||||
type: postgres # or mysql, sqlserver, etc.
|
||||
host: localhost
|
||||
port: 5432
|
||||
user: "{{ env_var('DB_USER') }}"
|
||||
password: "{{ env_var('DB_PASSWORD') }}"
|
||||
dbname: mydb
|
||||
schema: public
|
||||
threads: 4
|
||||
|
||||
# Hybrid: use DuckDB for local, Postgres for source
|
||||
hybrid:
|
||||
type: duckdb
|
||||
path: "{{ env_var('MXCP_DUCKDB_PATH', 'data/db-default.duckdb') }}"
|
||||
target: hybrid
|
||||
```
|
||||
|
||||
#### Step 2: Define external database as dbt source
|
||||
|
||||
```yaml
|
||||
# models/sources.yml
|
||||
version: 2
|
||||
|
||||
sources:
|
||||
- name: production_db
|
||||
description: "Production PostgreSQL database"
|
||||
database: postgres_db # Matches ATTACH name
|
||||
schema: public
|
||||
tables:
|
||||
- name: customers
|
||||
description: "Customer master data"
|
||||
columns:
|
||||
- name: customer_id
|
||||
description: "Unique customer identifier"
|
||||
tests:
|
||||
- unique
|
||||
- not_null
|
||||
- name: email
|
||||
tests:
|
||||
- not_null
|
||||
- name: country
|
||||
tests:
|
||||
- not_null
|
||||
|
||||
- name: orders
|
||||
description: "Order transactions"
|
||||
columns:
|
||||
- name: order_id
|
||||
tests:
|
||||
- unique
|
||||
- not_null
|
||||
- name: customer_id
|
||||
tests:
|
||||
- not_null
|
||||
- relationships:
|
||||
to: source('production_db', 'customers')
|
||||
field: customer_id
|
||||
```
|
||||
|
||||
#### Step 3: Create dbt model to cache/transform external data
|
||||
|
||||
```sql
|
||||
-- models/customer_summary.sql
|
||||
{{ config(
|
||||
materialized='table',
|
||||
description='Customer summary from production database'
|
||||
) }}
|
||||
|
||||
SELECT
|
||||
c.customer_id,
|
||||
c.name,
|
||||
c.email,
|
||||
c.country,
|
||||
COUNT(o.order_id) as order_count,
|
||||
COALESCE(SUM(o.total_amount), 0) as total_spent,
|
||||
MAX(o.order_date) as last_order_date
|
||||
FROM {{ source('production_db', 'customers') }} c
|
||||
LEFT JOIN {{ source('production_db', 'orders') }} o
|
||||
ON c.customer_id = o.customer_id
|
||||
GROUP BY c.customer_id, c.name, c.email, c.country
|
||||
```
|
||||
|
||||
```yaml
|
||||
# models/schema.yml
|
||||
version: 2
|
||||
|
||||
models:
|
||||
- name: customer_summary
|
||||
description: "Aggregated customer metrics from production"
|
||||
columns:
|
||||
- name: customer_id
|
||||
tests:
|
||||
- unique
|
||||
- not_null
|
||||
- name: order_count
|
||||
tests:
|
||||
- not_null
|
||||
- name: total_spent
|
||||
tests:
|
||||
- not_null
|
||||
```
|
||||
|
||||
#### Step 4: Run dbt to materialize data
|
||||
|
||||
```bash
|
||||
# Test connection to external database
|
||||
dbt debug
|
||||
|
||||
# Run models (fetches from external DB, materializes in DuckDB)
|
||||
dbt run --select customer_summary
|
||||
|
||||
# Test data quality
|
||||
dbt test --select customer_summary
|
||||
```
|
||||
|
||||
#### Step 5: Create MXCP tool to query materialized data
|
||||
|
||||
```yaml
|
||||
# tools/get_customer_summary.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: get_customer_summary
|
||||
description: "Get customer summary statistics from cached production data"
|
||||
parameters:
|
||||
- name: country
|
||||
type: string
|
||||
required: false
|
||||
- name: min_orders
|
||||
type: integer
|
||||
default: 0
|
||||
return:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
customer_id: { type: integer }
|
||||
name: { type: string }
|
||||
order_count: { type: integer }
|
||||
total_spent: { type: number }
|
||||
source:
|
||||
code: |
|
||||
SELECT
|
||||
customer_id,
|
||||
name,
|
||||
email,
|
||||
country,
|
||||
order_count,
|
||||
total_spent,
|
||||
last_order_date
|
||||
FROM customer_summary
|
||||
WHERE ($country IS NULL OR country = $country)
|
||||
AND order_count >= $min_orders
|
||||
ORDER BY total_spent DESC
|
||||
LIMIT 100
|
||||
```
|
||||
|
||||
#### Step 6: Refresh data periodically
|
||||
|
||||
```bash
|
||||
# Manual refresh
|
||||
dbt run --select customer_summary
|
||||
|
||||
# Or create Python tool to trigger refresh
|
||||
```
|
||||
|
||||
```yaml
|
||||
# tools/refresh_data.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: refresh_customer_data
|
||||
description: "Refresh customer summary from production database"
|
||||
language: python
|
||||
return:
|
||||
type: object
|
||||
source:
|
||||
file: ../python/refresh.py
|
||||
```
|
||||
|
||||
```python
|
||||
# python/refresh.py
|
||||
from mxcp.runtime import reload_duckdb
|
||||
import subprocess
|
||||
|
||||
def refresh_customer_data() -> dict:
|
||||
"""Refresh customer summary from external database"""
|
||||
|
||||
def run_dbt():
|
||||
result = subprocess.run(
|
||||
["dbt", "run", "--select", "customer_summary"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise Exception(f"dbt run failed: {result.stderr}")
|
||||
|
||||
test_result = subprocess.run(
|
||||
["dbt", "test", "--select", "customer_summary"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
if test_result.returncode != 0:
|
||||
raise Exception(f"dbt test failed: {test_result.stderr}")
|
||||
|
||||
# Run dbt with exclusive database access
|
||||
reload_duckdb(
|
||||
payload_func=run_dbt,
|
||||
description="Refreshing customer data from production"
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Customer data refreshed from production database"
|
||||
}
|
||||
```
|
||||
|
||||
### Incremental dbt Models for Large Tables
|
||||
|
||||
For large external tables, use incremental materialization:
|
||||
|
||||
```sql
|
||||
-- models/orders_incremental.sql
|
||||
{{ config(
|
||||
materialized='incremental',
|
||||
unique_key='order_id',
|
||||
on_schema_change='fail'
|
||||
) }}
|
||||
|
||||
SELECT
|
||||
order_id,
|
||||
customer_id,
|
||||
order_date,
|
||||
total_amount,
|
||||
status
|
||||
FROM {{ source('production_db', 'orders') }}
|
||||
|
||||
{% if is_incremental() %}
|
||||
-- Only fetch new/updated orders
|
||||
WHERE order_date > (SELECT MAX(order_date) FROM {{ this }})
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
```bash
|
||||
# First run: fetch all historical data
|
||||
dbt run --select orders_incremental --full-refresh
|
||||
|
||||
# Subsequent runs: only fetch new data
|
||||
dbt run --select orders_incremental
|
||||
```
|
||||
|
||||
## Connection Patterns and Best Practices
|
||||
|
||||
### Pattern 1: Read-Only Querying
|
||||
|
||||
**Use case**: Query production database directly without caching
|
||||
|
||||
```yaml
|
||||
tool:
|
||||
name: query_live_data
|
||||
source:
|
||||
code: |
|
||||
ATTACH IF NOT EXISTS 'connection_string' AS prod (TYPE POSTGRES);
|
||||
SELECT * FROM prod.public.table WHERE ...
|
||||
```
|
||||
|
||||
**Pros**: Always fresh data
|
||||
**Cons**: Slower queries, database load
|
||||
|
||||
### Pattern 2: Cached/Materialized Data
|
||||
|
||||
**Use case**: Cache external data in DuckDB for fast queries
|
||||
|
||||
```sql
|
||||
-- dbt model caches external data
|
||||
SELECT * FROM {{ source('external_db', 'table') }}
|
||||
```
|
||||
|
||||
```yaml
|
||||
# MXCP tool queries cache
|
||||
tool:
|
||||
source:
|
||||
code: SELECT * FROM cached_table WHERE ...
|
||||
```
|
||||
|
||||
**Pros**: Fast queries, no database load
|
||||
**Cons**: Data staleness, needs refresh
|
||||
|
||||
### Pattern 3: Hybrid (Cache + Live)
|
||||
|
||||
**Use case**: Cache most data, query live for real-time needs
|
||||
|
||||
```sql
|
||||
-- Combine cached and live data
|
||||
SELECT * FROM cached_historical_orders
|
||||
UNION ALL
|
||||
SELECT * FROM prod.public.orders WHERE order_date >= CURRENT_DATE - INTERVAL '7 days'
|
||||
```
|
||||
|
||||
### Security Best Practices
|
||||
|
||||
#### 1. Use Read-Only Database Users
|
||||
|
||||
```sql
|
||||
-- PostgreSQL: Create read-only user
|
||||
CREATE USER readonly_user WITH PASSWORD 'secure_password';
|
||||
GRANT CONNECT ON DATABASE mydb TO readonly_user;
|
||||
GRANT USAGE ON SCHEMA public TO readonly_user;
|
||||
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_user;
|
||||
```
|
||||
|
||||
#### 2. Store Credentials in Secrets
|
||||
|
||||
```yaml
|
||||
# config.yml - NEVER commit passwords
|
||||
secrets:
|
||||
- name: db_password
|
||||
type: env
|
||||
parameters:
|
||||
env_var: DB_PASSWORD
|
||||
|
||||
# Production: use Vault
|
||||
- name: prod_db_password
|
||||
type: vault
|
||||
parameters:
|
||||
path: secret/data/myapp/database
|
||||
field: password
|
||||
```
|
||||
|
||||
#### 3. Use Connection Pooling (for Python approach)
|
||||
|
||||
```python
|
||||
# python/db_client.py
|
||||
from mxcp.runtime import on_init, on_shutdown
|
||||
import psycopg2.pool
|
||||
|
||||
connection_pool = None
|
||||
|
||||
@on_init
|
||||
def setup_pool():
|
||||
global connection_pool
|
||||
connection_pool = psycopg2.pool.SimpleConnectionPool(
|
||||
minconn=1,
|
||||
maxconn=5,
|
||||
host=os.getenv("DB_HOST"),
|
||||
database=os.getenv("DB_NAME"),
|
||||
user=os.getenv("DB_USER"),
|
||||
password=os.getenv("DB_PASSWORD")
|
||||
)
|
||||
|
||||
@on_shutdown
|
||||
def close_pool():
|
||||
global connection_pool
|
||||
if connection_pool:
|
||||
connection_pool.closeall()
|
||||
|
||||
def query_database(sql: str) -> list[dict]:
|
||||
conn = connection_pool.getconn()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(sql)
|
||||
results = cursor.fetchall()
|
||||
return results
|
||||
finally:
|
||||
connection_pool.putconn(conn)
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
#### Handle Connection Failures
|
||||
|
||||
```yaml
|
||||
# tools/query_with_error_handling.yml
|
||||
tool:
|
||||
name: safe_query
|
||||
language: python
|
||||
source:
|
||||
file: ../python/safe_query.py
|
||||
```
|
||||
|
||||
```python
|
||||
# python/safe_query.py
|
||||
from mxcp.runtime import db
|
||||
import duckdb
|
||||
|
||||
def safe_query(table_name: str) -> dict:
|
||||
"""Query external database with error handling"""
|
||||
try:
|
||||
# Try to attach if not already attached
|
||||
db.execute("""
|
||||
INSTALL postgres;
|
||||
LOAD postgres;
|
||||
ATTACH IF NOT EXISTS 'host=${DB_HOST} dbname=${DB_NAME} user=${DB_USER} password=${DB_PASSWORD}'
|
||||
AS prod (TYPE POSTGRES);
|
||||
""")
|
||||
|
||||
# Query
|
||||
results = db.execute(f"SELECT * FROM prod.public.{table_name} LIMIT 100").fetchall()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"row_count": len(results),
|
||||
"data": results
|
||||
}
|
||||
|
||||
except duckdb.CatalogException as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Table not found",
|
||||
"message": f"Table {table_name} does not exist in external database",
|
||||
"suggestion": "Check table name and database connection"
|
||||
}
|
||||
|
||||
except duckdb.IOException as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Connection failed",
|
||||
"message": "Could not connect to external database",
|
||||
"suggestion": "Check database credentials and network connectivity"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Unexpected error",
|
||||
"message": str(e)
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
#### 1. Add Indexes on Frequently Filtered Columns
|
||||
|
||||
```sql
|
||||
-- On external database (PostgreSQL)
|
||||
CREATE INDEX idx_customers_country ON customers(country);
|
||||
CREATE INDEX idx_orders_date ON orders(order_date);
|
||||
```
|
||||
|
||||
#### 2. Limit Result Sets
|
||||
|
||||
```sql
|
||||
-- Always add LIMIT for large tables
|
||||
SELECT * FROM prod.public.orders
|
||||
WHERE order_date >= '2024-01-01'
|
||||
LIMIT 1000 -- Prevent overwhelming queries
|
||||
```
|
||||
|
||||
#### 3. Materialize Complex Joins
|
||||
|
||||
```sql
|
||||
-- Instead of complex join on every query
|
||||
-- Create dbt model to materialize the join
|
||||
{{ config(materialized='table') }}
|
||||
|
||||
SELECT ... complex join logic ...
|
||||
FROM {{ source('prod', 'table1') }} t1
|
||||
JOIN {{ source('prod', 'table2') }} t2 ...
|
||||
```
|
||||
|
||||
## Complete Example: PostgreSQL to MXCP
|
||||
|
||||
**Scenario**: Query production PostgreSQL customer database
|
||||
|
||||
```bash
|
||||
# 1. Create project
|
||||
mkdir postgres-customers && cd postgres-customers
|
||||
mxcp init --bootstrap
|
||||
|
||||
# 2. Create config
|
||||
cat > config.yml <<'EOF'
|
||||
mxcp: 1
|
||||
|
||||
profiles:
|
||||
default:
|
||||
secrets:
|
||||
- name: db_host
|
||||
type: env
|
||||
parameters:
|
||||
env_var: DB_HOST
|
||||
- name: db_user
|
||||
type: env
|
||||
parameters:
|
||||
env_var: DB_USER
|
||||
- name: db_password
|
||||
type: env
|
||||
parameters:
|
||||
env_var: DB_PASSWORD
|
||||
EOF
|
||||
|
||||
# 3. Create tool
|
||||
cat > tools/query_customers.yml <<'EOF'
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: query_customers
|
||||
description: "Query customers from PostgreSQL"
|
||||
parameters:
|
||||
- name: country
|
||||
type: string
|
||||
required: false
|
||||
return:
|
||||
type: array
|
||||
source:
|
||||
code: |
|
||||
INSTALL postgres;
|
||||
LOAD postgres;
|
||||
ATTACH IF NOT EXISTS 'host=${DB_HOST} port=5432 dbname=customers user=${DB_USER} password=${DB_PASSWORD}'
|
||||
AS prod (TYPE POSTGRES);
|
||||
|
||||
SELECT customer_id, name, email, country
|
||||
FROM prod.public.customers
|
||||
WHERE $country IS NULL OR country = $country
|
||||
LIMIT 100
|
||||
EOF
|
||||
|
||||
# 4. Set credentials
|
||||
export DB_HOST="localhost"
|
||||
export DB_USER="readonly_user"
|
||||
export DB_PASSWORD="secure_password"
|
||||
|
||||
# 5. Test
|
||||
mxcp validate
|
||||
mxcp run tool query_customers --param country="US"
|
||||
|
||||
# 6. Start server
|
||||
mxcp serve
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
**For external database connections**:
|
||||
|
||||
1. **Direct querying** → Use ATTACH with parameterized connection strings
|
||||
2. **Cached data** → Use dbt sources + models for materialization
|
||||
3. **Always use read-only users** for security
|
||||
4. **Store credentials in environment variables** or Vault
|
||||
5. **Handle connection errors** gracefully in Python tools
|
||||
6. **Test with** `mxcp validate && mxcp run tool <name>`
|
||||
7. **Use dbt for** large tables (incremental models) and transformations
|
||||
|
||||
**Decision guide**:
|
||||
- Small queries, real-time data needed → ATTACH
|
||||
- Large tables, can tolerate staleness → dbt materialization
|
||||
- Complex transformations → dbt models
|
||||
- Simple SELECT queries → ATTACH
|
||||
|
||||
This approach gives you full SQL database access while maintaining MXCP's security, validation, and testing workflow.
|
||||
498
skills/mxcp-expert/references/dbt-core-guide.md
Normal file
498
skills/mxcp-expert/references/dbt-core-guide.md
Normal file
@@ -0,0 +1,498 @@
|
||||
# dbt Core Guide for MXCP
|
||||
|
||||
Essential dbt (data build tool) knowledge for building MXCP servers.
|
||||
|
||||
## What is dbt?
|
||||
|
||||
dbt (data build tool) is a transformation workflow tool that enables data analysts and engineers to transform data in their data warehouse using SQL SELECT statements. In MXCP, dbt transforms raw data into clean, queryable tables that MXCP endpoints can access.
|
||||
|
||||
**Core principle**: dbt creates the tables → MXCP queries them
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. Seeds
|
||||
|
||||
**Seeds are CSV files** that dbt loads into your database as tables. They are perfect for:
|
||||
- Static reference data (country codes, status mappings, etc.)
|
||||
- Small lookup tables (<10,000 rows)
|
||||
- User-provided data files that need to be queried
|
||||
|
||||
**Location**: Place CSV files in `seeds/` directory
|
||||
|
||||
**Loading seeds**:
|
||||
```bash
|
||||
dbt seed # Load all seeds
|
||||
dbt seed --select my_file # Load specific seed
|
||||
mxcp dbt seed # Load via MXCP
|
||||
```
|
||||
|
||||
**Example use case**: User provides `customers.csv` → dbt loads it as a table → MXCP tools query it
|
||||
|
||||
**Critical for MXCP**: Seeds are the primary way to make CSV files queryable via MXCP tools.
|
||||
|
||||
### 2. Models
|
||||
|
||||
**Models transform data** using either SQL or Python. Each `.sql` or `.py` file in `models/` becomes a table or view.
|
||||
|
||||
#### SQL Models
|
||||
|
||||
**SQL models are SELECT statements** that transform data. Best for standard transformations, aggregations, and joins.
|
||||
|
||||
**Basic SQL model** (`models/customer_summary.sql`):
|
||||
```sql
|
||||
{{ config(materialized='table') }}
|
||||
|
||||
SELECT
|
||||
customer_id,
|
||||
COUNT(*) as order_count,
|
||||
SUM(amount) as total_spent
|
||||
FROM {{ ref('orders') }}
|
||||
GROUP BY customer_id
|
||||
```
|
||||
|
||||
#### Python Models
|
||||
|
||||
**Python models use pandas** for complex data processing. Best for Excel files, ML preprocessing, and complex transformations.
|
||||
|
||||
**Basic Python model** (`models/process_data.py`):
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
def model(dbt, session):
|
||||
# Load data from dbt ref or read files
|
||||
# df = dbt.ref('source_table').to_pandas() # From dbt source
|
||||
df = pd.read_excel('data/input.xlsx') # From file
|
||||
|
||||
# Transform using pandas
|
||||
df = df.dropna(how='all')
|
||||
df['new_column'] = df['amount'] * 1.1
|
||||
|
||||
return df # Returns DataFrame that becomes a table
|
||||
```
|
||||
|
||||
**When to use Python models:**
|
||||
- Processing Excel files with complex formatting
|
||||
- Data cleaning requiring pandas operations (pivoting, melting, etc.)
|
||||
- ML feature engineering or preprocessing
|
||||
- Complex string manipulation or regex operations
|
||||
- Integration with Python libraries (sklearn, numpy, etc.)
|
||||
|
||||
**Materialization types** (for both SQL and Python models):
|
||||
- `table` - Creates a table (fast queries, slower builds)
|
||||
- `view` - Creates a view (slow queries, instant builds) - Not available for Python models
|
||||
- `incremental` - Appends new data only (best for large datasets)
|
||||
|
||||
### 3. Schema Files (schema.yml)
|
||||
|
||||
**Schema files define structure, tests, and documentation** for seeds and models.
|
||||
|
||||
**ALWAYS create schema.yml files** - they are critical for:
|
||||
- Type validation
|
||||
- Data quality tests
|
||||
- Documentation
|
||||
- Column descriptions
|
||||
|
||||
**Example** (`seeds/schema.yml`):
|
||||
```yaml
|
||||
version: 2
|
||||
|
||||
seeds:
|
||||
- name: customers
|
||||
description: "Customer master data from CSV upload"
|
||||
columns:
|
||||
- name: customer_id
|
||||
description: "Unique customer identifier"
|
||||
tests:
|
||||
- unique
|
||||
- not_null
|
||||
- name: email
|
||||
description: "Customer email address"
|
||||
tests:
|
||||
- not_null
|
||||
- name: created_at
|
||||
description: "Account creation timestamp"
|
||||
data_type: timestamp
|
||||
```
|
||||
|
||||
**Example** (`models/schema.yml`):
|
||||
```yaml
|
||||
version: 2
|
||||
|
||||
models:
|
||||
- name: customer_summary
|
||||
description: "Aggregated customer metrics"
|
||||
columns:
|
||||
- name: customer_id
|
||||
tests:
|
||||
- unique
|
||||
- not_null
|
||||
- name: total_spent
|
||||
tests:
|
||||
- not_null
|
||||
- name: order_count
|
||||
tests:
|
||||
- not_null
|
||||
```
|
||||
|
||||
### 4. Sources
|
||||
|
||||
**Sources represent raw data** already in your database (not managed by dbt).
|
||||
|
||||
**Example** (`models/sources.yml`):
|
||||
```yaml
|
||||
version: 2
|
||||
|
||||
sources:
|
||||
- name: raw_data
|
||||
tables:
|
||||
- name: transactions
|
||||
description: "Raw transaction data"
|
||||
- name: users
|
||||
description: "User accounts"
|
||||
```
|
||||
|
||||
**Use in models**:
|
||||
```sql
|
||||
SELECT * FROM {{ source('raw_data', 'transactions') }}
|
||||
```
|
||||
|
||||
## MXCP + dbt Workflow
|
||||
|
||||
### Pattern 1: CSV File to MXCP Tool
|
||||
|
||||
**User request**: "I need to query my sales.csv file"
|
||||
|
||||
**Steps**:
|
||||
|
||||
1. **Create seed** - Place `sales.csv` in `seeds/` directory
|
||||
2. **Create schema** - Define structure in `seeds/schema.yml`:
|
||||
```yaml
|
||||
version: 2
|
||||
|
||||
seeds:
|
||||
- name: sales
|
||||
description: "Sales data from CSV upload"
|
||||
columns:
|
||||
- name: sale_id
|
||||
tests: [unique, not_null]
|
||||
- name: amount
|
||||
tests: [not_null]
|
||||
- name: sale_date
|
||||
data_type: date
|
||||
tests: [not_null]
|
||||
- name: region
|
||||
tests: [not_null]
|
||||
```
|
||||
|
||||
3. **Load seed**:
|
||||
```bash
|
||||
dbt seed --select sales
|
||||
```
|
||||
|
||||
4. **Create MXCP tool** (`tools/get_sales.yml`):
|
||||
```yaml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: get_sales
|
||||
description: "Query sales data by region and date range"
|
||||
parameters:
|
||||
- name: region
|
||||
type: string
|
||||
required: false
|
||||
- name: start_date
|
||||
type: string
|
||||
format: date
|
||||
required: false
|
||||
- name: end_date
|
||||
type: string
|
||||
format: date
|
||||
required: false
|
||||
return:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
source:
|
||||
code: |
|
||||
SELECT * FROM sales
|
||||
WHERE ($region IS NULL OR region = $region)
|
||||
AND ($start_date IS NULL OR sale_date >= $start_date)
|
||||
AND ($end_date IS NULL OR sale_date <= $end_date)
|
||||
ORDER BY sale_date DESC
|
||||
```
|
||||
|
||||
5. **Test**:
|
||||
```bash
|
||||
dbt test --select sales
|
||||
mxcp validate
|
||||
mxcp test tool get_sales
|
||||
```
|
||||
|
||||
### Pattern 2: Transform Then Query
|
||||
|
||||
**User request**: "Analyze monthly sales trends from my CSV"
|
||||
|
||||
1. **Seed the raw data** (as above)
|
||||
2. **Create transformation model** (`models/monthly_sales.sql`):
|
||||
```sql
|
||||
{{ config(materialized='table') }}
|
||||
|
||||
SELECT
|
||||
region,
|
||||
DATE_TRUNC('month', sale_date) as month,
|
||||
SUM(amount) as total_sales,
|
||||
COUNT(*) as transaction_count,
|
||||
AVG(amount) as avg_sale
|
||||
FROM {{ ref('sales') }}
|
||||
GROUP BY region, month
|
||||
```
|
||||
|
||||
3. **Create schema** (`models/schema.yml`):
|
||||
```yaml
|
||||
version: 2
|
||||
|
||||
models:
|
||||
- name: monthly_sales
|
||||
description: "Monthly sales aggregations"
|
||||
columns:
|
||||
- name: region
|
||||
tests: [not_null]
|
||||
- name: month
|
||||
tests: [not_null]
|
||||
- name: total_sales
|
||||
tests: [not_null]
|
||||
```
|
||||
|
||||
4. **Run dbt**:
|
||||
```bash
|
||||
dbt seed
|
||||
dbt run --select monthly_sales
|
||||
dbt test --select monthly_sales
|
||||
```
|
||||
|
||||
5. **Create MXCP tool** to query the model:
|
||||
```yaml
|
||||
tool:
|
||||
name: monthly_trends
|
||||
source:
|
||||
code: |
|
||||
SELECT * FROM monthly_sales
|
||||
WHERE region = $region
|
||||
ORDER BY month DESC
|
||||
```
|
||||
|
||||
### Pattern 3: Excel Processing with Python Models
|
||||
|
||||
**User request**: "Process this Excel file with multiple sheets and complex formatting"
|
||||
|
||||
1. **Create Python model** (`models/process_excel.py`):
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
def model(dbt, session):
|
||||
# Read Excel file
|
||||
df = pd.read_excel('data/sales_data.xlsx', sheet_name='Sales')
|
||||
|
||||
# Clean data
|
||||
df = df.dropna(how='all') # Remove empty rows
|
||||
df = df.dropna(axis=1, how='all') # Remove empty columns
|
||||
|
||||
# Normalize column names
|
||||
df.columns = df.columns.str.lower().str.replace(' ', '_')
|
||||
|
||||
# Complex transformations using pandas
|
||||
df['sale_date'] = pd.to_datetime(df['sale_date'])
|
||||
df['month'] = df['sale_date'].dt.to_period('M').astype(str)
|
||||
|
||||
# Aggregate data
|
||||
result = df.groupby(['region', 'month']).agg({
|
||||
'amount': 'sum',
|
||||
'quantity': 'sum'
|
||||
}).reset_index()
|
||||
|
||||
return result
|
||||
```
|
||||
|
||||
2. **Create schema** (`models/schema.yml`):
|
||||
```yaml
|
||||
version: 2
|
||||
|
||||
models:
|
||||
- name: process_excel
|
||||
description: "Processed sales data from Excel"
|
||||
config:
|
||||
materialized: table
|
||||
columns:
|
||||
- name: region
|
||||
tests: [not_null]
|
||||
- name: month
|
||||
tests: [not_null]
|
||||
- name: amount
|
||||
tests: [not_null]
|
||||
```
|
||||
|
||||
3. **Run the Python model**:
|
||||
```bash
|
||||
dbt run --select process_excel
|
||||
dbt test --select process_excel
|
||||
```
|
||||
|
||||
4. **Create MXCP tool** to query:
|
||||
```yaml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: get_sales_by_region
|
||||
description: "Get sales data processed from Excel"
|
||||
parameters:
|
||||
- name: region
|
||||
type: string
|
||||
default: null
|
||||
source:
|
||||
code: |
|
||||
SELECT * FROM process_excel
|
||||
WHERE $region IS NULL OR region = $region
|
||||
ORDER BY month DESC
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
mxcp-project/
|
||||
├── mxcp-site.yml # MXCP configuration
|
||||
├── dbt_project.yml # dbt configuration
|
||||
├── seeds/ # CSV files
|
||||
│ ├── customers.csv
|
||||
│ └── schema.yml # Seed schemas (REQUIRED)
|
||||
├── models/ # SQL transformations
|
||||
│ ├── staging/
|
||||
│ ├── intermediate/
|
||||
│ ├── marts/
|
||||
│ └── schema.yml # Model schemas (REQUIRED)
|
||||
├── tools/ # MXCP tools that query seeds/models
|
||||
└── target/ # dbt build output (gitignored)
|
||||
```
|
||||
|
||||
## dbt Commands for MXCP
|
||||
|
||||
```bash
|
||||
# Initialize dbt project
|
||||
dbt init
|
||||
|
||||
# Load CSV seeds into database
|
||||
dbt seed # Load all seeds
|
||||
dbt seed --select sales # Load specific seed
|
||||
|
||||
# Run transformations
|
||||
dbt run # Run all models
|
||||
dbt run --select model_name # Run specific model
|
||||
dbt run --select +model_name # Run model and upstream dependencies
|
||||
|
||||
# Test data quality
|
||||
dbt test # Run all tests
|
||||
dbt test --select sales # Test specific seed/model
|
||||
|
||||
# Documentation
|
||||
dbt docs generate # Generate documentation
|
||||
dbt docs serve # Serve documentation site
|
||||
|
||||
# Via MXCP wrapper
|
||||
mxcp dbt seed
|
||||
mxcp dbt run
|
||||
mxcp dbt test
|
||||
```
|
||||
|
||||
## Schema.yml Best Practices
|
||||
|
||||
**ALWAYS include these in schema.yml**:
|
||||
|
||||
1. **Version declaration**: `version: 2`
|
||||
2. **Description for every seed/model**: Helps LLMs and humans understand purpose
|
||||
3. **Column-level descriptions**: Document what each field contains
|
||||
4. **Data type declarations**: Ensure proper typing (`data_type: timestamp`, etc.)
|
||||
5. **Tests for key columns**:
|
||||
- `unique` - No duplicates
|
||||
- `not_null` - Required field
|
||||
- `accepted_values` - Enum validation
|
||||
- `relationships` - Foreign key validation
|
||||
|
||||
**Example comprehensive schema.yml**:
|
||||
```yaml
|
||||
version: 2
|
||||
|
||||
seeds:
|
||||
- name: employees
|
||||
description: "Employee master data"
|
||||
columns:
|
||||
- name: employee_id
|
||||
description: "Unique employee identifier"
|
||||
data_type: varchar
|
||||
tests:
|
||||
- unique
|
||||
- not_null
|
||||
- name: department
|
||||
description: "Department code"
|
||||
data_type: varchar
|
||||
tests:
|
||||
- not_null
|
||||
- accepted_values:
|
||||
values: ['engineering', 'sales', 'marketing']
|
||||
- name: salary
|
||||
description: "Annual salary in USD"
|
||||
data_type: decimal
|
||||
tests:
|
||||
- not_null
|
||||
- name: hire_date
|
||||
description: "Date of hire"
|
||||
data_type: date
|
||||
tests:
|
||||
- not_null
|
||||
```
|
||||
|
||||
## DuckDB Integration
|
||||
|
||||
MXCP uses **DuckDB** as its default database. dbt can target DuckDB directly.
|
||||
|
||||
**Auto-configured by MXCP** - no manual setup needed:
|
||||
```yaml
|
||||
# profiles.yml (auto-generated)
|
||||
my_project:
|
||||
outputs:
|
||||
dev:
|
||||
type: duckdb
|
||||
path: "{{ env_var('MXCP_DUCKDB_PATH', 'data/db-default.duckdb') }}"
|
||||
target: dev
|
||||
```
|
||||
|
||||
**DuckDB reads CSVs directly**:
|
||||
```sql
|
||||
-- In dbt models, you can read CSVs without seeding
|
||||
SELECT * FROM read_csv_auto('path/to/file.csv')
|
||||
```
|
||||
|
||||
**But prefer seeds for user data** - they provide version control and validation.
|
||||
|
||||
## Common Issues
|
||||
|
||||
**Issue**: Seed file not loading
|
||||
**Solution**: Check CSV format, ensure no special characters in filename, verify schema.yml exists
|
||||
|
||||
**Issue**: Model not found
|
||||
**Solution**: Run `dbt compile` to check for syntax errors, ensure model is in `models/` directory
|
||||
|
||||
**Issue**: Tests failing
|
||||
**Solution**: Review test output, check data quality, adjust tests or fix data
|
||||
|
||||
**Issue**: Type errors
|
||||
**Solution**: Add explicit `data_type` declarations in schema.yml
|
||||
|
||||
## Summary for MXCP Builders
|
||||
|
||||
When building MXCP servers:
|
||||
|
||||
1. **For CSV files** → Use dbt seeds
|
||||
2. **Always create** `schema.yml` files with tests and types
|
||||
3. **Load with** `dbt seed`
|
||||
4. **Transform with** dbt models if needed
|
||||
5. **Query from** MXCP tools using `SELECT * FROM <table>`
|
||||
6. **Validate with** `dbt test` before deploying
|
||||
|
||||
This ensures data quality, type safety, and proper documentation for all data sources.
|
||||
311
skills/mxcp-expert/references/dbt-patterns.md
Normal file
311
skills/mxcp-expert/references/dbt-patterns.md
Normal file
@@ -0,0 +1,311 @@
|
||||
# dbt Integration Patterns
|
||||
|
||||
Guide to combining dbt with MXCP for data transformation pipelines.
|
||||
|
||||
## Why dbt + MXCP?
|
||||
|
||||
**dbt creates the tables → MXCP queries them**
|
||||
|
||||
This pattern provides:
|
||||
- Data transformation and quality in dbt
|
||||
- Fast local caching of external data
|
||||
- SQL queries against materialized views
|
||||
- Consistent data contracts
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Enable dbt in MXCP
|
||||
|
||||
```yaml
|
||||
# mxcp-site.yml
|
||||
dbt:
|
||||
enabled: true
|
||||
model_paths: ["models"]
|
||||
```
|
||||
|
||||
### 2. Create dbt Project
|
||||
|
||||
```bash
|
||||
dbt init
|
||||
```
|
||||
|
||||
### 3. Configure dbt Profile
|
||||
|
||||
```yaml
|
||||
# profiles.yml (auto-generated by mxcp dbt-config)
|
||||
covid_owid:
|
||||
outputs:
|
||||
dev:
|
||||
type: duckdb
|
||||
path: data.duckdb
|
||||
target: dev
|
||||
```
|
||||
|
||||
## Basic Pattern
|
||||
|
||||
### dbt Model
|
||||
|
||||
Create `models/sales_summary.sql`:
|
||||
```sql
|
||||
{{ config(materialized='table') }}
|
||||
|
||||
SELECT
|
||||
region,
|
||||
DATE_TRUNC('month', sale_date) as month,
|
||||
SUM(amount) as total_sales,
|
||||
COUNT(*) as transaction_count
|
||||
FROM {{ source('raw', 'sales_data') }}
|
||||
GROUP BY region, month
|
||||
```
|
||||
|
||||
### Run dbt
|
||||
|
||||
```bash
|
||||
mxcp dbt run
|
||||
# or directly: dbt run
|
||||
```
|
||||
|
||||
### MXCP Tool Queries Table
|
||||
|
||||
Create `tools/monthly_sales.yml`:
|
||||
```yaml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: monthly_sales
|
||||
description: "Get monthly sales summary"
|
||||
parameters:
|
||||
- name: region
|
||||
type: string
|
||||
return:
|
||||
type: array
|
||||
source:
|
||||
code: |
|
||||
SELECT * FROM sales_summary
|
||||
WHERE region = $region
|
||||
ORDER BY month DESC
|
||||
```
|
||||
|
||||
## External Data Caching
|
||||
|
||||
### Fetch and Cache External Data
|
||||
|
||||
```sql
|
||||
-- models/covid_data.sql
|
||||
{{ config(materialized='table') }}
|
||||
|
||||
SELECT *
|
||||
FROM read_csv_auto('https://github.com/owid/covid-19-data/raw/master/public/data/owid-covid-data.csv')
|
||||
```
|
||||
|
||||
Run once to cache:
|
||||
```bash
|
||||
mxcp dbt run
|
||||
```
|
||||
|
||||
### Query Cached Data
|
||||
|
||||
```yaml
|
||||
# tools/covid_stats.yml
|
||||
tool:
|
||||
name: covid_stats
|
||||
source:
|
||||
code: |
|
||||
SELECT location, date, total_cases, new_cases
|
||||
FROM covid_data
|
||||
WHERE location = $country
|
||||
ORDER BY date DESC
|
||||
LIMIT 30
|
||||
```
|
||||
|
||||
## Incremental Models
|
||||
|
||||
### Incremental Updates
|
||||
|
||||
```sql
|
||||
-- models/events_incremental.sql
|
||||
{{ config(
|
||||
materialized='incremental',
|
||||
unique_key='event_id'
|
||||
) }}
|
||||
|
||||
SELECT *
|
||||
FROM read_json('https://api.example.com/events')
|
||||
|
||||
{% if is_incremental() %}
|
||||
WHERE created_at > (SELECT MAX(created_at) FROM {{ this }})
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
## Sources and References
|
||||
|
||||
### Define Sources
|
||||
|
||||
```yaml
|
||||
# models/sources.yml
|
||||
version: 2
|
||||
|
||||
sources:
|
||||
- name: raw
|
||||
tables:
|
||||
- name: sales_data
|
||||
- name: customer_data
|
||||
```
|
||||
|
||||
### Reference Models
|
||||
|
||||
```sql
|
||||
-- models/customer_summary.sql
|
||||
{{ config(materialized='table') }}
|
||||
|
||||
WITH customers AS (
|
||||
SELECT * FROM {{ source('raw', 'customer_data') }}
|
||||
),
|
||||
sales AS (
|
||||
SELECT * FROM {{ ref('sales_summary') }}
|
||||
)
|
||||
SELECT
|
||||
c.customer_id,
|
||||
c.name,
|
||||
s.total_sales
|
||||
FROM customers c
|
||||
JOIN sales s ON c.customer_id = s.customer_id
|
||||
```
|
||||
|
||||
## Data Quality Tests
|
||||
|
||||
### dbt Tests
|
||||
|
||||
```yaml
|
||||
# models/schema.yml
|
||||
version: 2
|
||||
|
||||
models:
|
||||
- name: sales_summary
|
||||
columns:
|
||||
- name: region
|
||||
tests:
|
||||
- not_null
|
||||
- name: total_sales
|
||||
tests:
|
||||
- not_null
|
||||
- positive_value
|
||||
- name: month
|
||||
tests:
|
||||
- unique
|
||||
```
|
||||
|
||||
### Run Tests
|
||||
|
||||
```bash
|
||||
mxcp dbt test
|
||||
```
|
||||
|
||||
## Complete Workflow
|
||||
|
||||
### 1. Development
|
||||
|
||||
```bash
|
||||
# Create/modify dbt models
|
||||
vim models/new_analysis.sql
|
||||
|
||||
# Run transformations
|
||||
mxcp dbt run --select new_analysis
|
||||
|
||||
# Test data quality
|
||||
mxcp dbt test --select new_analysis
|
||||
|
||||
# Create MXCP endpoint
|
||||
vim tools/new_endpoint.yml
|
||||
```
|
||||
|
||||
### 2. Testing
|
||||
|
||||
```bash
|
||||
# Validate MXCP endpoint
|
||||
mxcp validate
|
||||
|
||||
# Test endpoint
|
||||
mxcp test tool new_endpoint
|
||||
```
|
||||
|
||||
### 3. Production
|
||||
|
||||
```bash
|
||||
# Run dbt in production
|
||||
mxcp dbt run --profile production
|
||||
|
||||
# Start MXCP server
|
||||
mxcp serve --profile production
|
||||
```
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Multi-Source Aggregation
|
||||
|
||||
```sql
|
||||
-- models/unified_metrics.sql
|
||||
{{ config(materialized='table') }}
|
||||
|
||||
WITH external_data AS (
|
||||
SELECT * FROM read_json('https://api.example.com/metrics')
|
||||
),
|
||||
internal_data AS (
|
||||
SELECT * FROM {{ source('raw', 'internal_metrics') }}
|
||||
),
|
||||
third_party AS (
|
||||
SELECT * FROM read_parquet('s3://bucket/data/*.parquet')
|
||||
)
|
||||
SELECT * FROM external_data
|
||||
UNION ALL
|
||||
SELECT * FROM internal_data
|
||||
UNION ALL
|
||||
SELECT * FROM third_party
|
||||
```
|
||||
|
||||
### Dynamic Caching Strategy
|
||||
|
||||
```sql
|
||||
-- models/live_dashboard.sql
|
||||
{{ config(
|
||||
materialized='table',
|
||||
post_hook="PRAGMA optimize"
|
||||
) }}
|
||||
|
||||
-- Recent data (refresh hourly)
|
||||
SELECT * FROM read_json('https://api.metrics.com/live')
|
||||
WHERE timestamp >= current_timestamp - interval '24 hours'
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Historical data (cached daily)
|
||||
SELECT * FROM {{ ref('historical_metrics') }}
|
||||
WHERE timestamp < current_timestamp - interval '24 hours'
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Materialization Strategy**
|
||||
- Use `table` for frequently queried data
|
||||
- Use `view` for rarely used transformations
|
||||
- Use `incremental` for large, append-only datasets
|
||||
|
||||
2. **Naming Conventions**
|
||||
- `stg_` for staging models
|
||||
- `int_` for intermediate models
|
||||
- `fct_` for fact tables
|
||||
- `dim_` for dimension tables
|
||||
|
||||
3. **Data Quality**
|
||||
- Add tests to all models
|
||||
- Document columns
|
||||
- Use sources for raw data
|
||||
|
||||
4. **Performance**
|
||||
- Materialize frequently used aggregations
|
||||
- Use incremental for large datasets
|
||||
- Add indexes where needed
|
||||
|
||||
5. **Version Control**
|
||||
- Commit dbt models
|
||||
- Version dbt_project.yml
|
||||
- Document model changes
|
||||
576
skills/mxcp-expert/references/debugging-guide.md
Normal file
576
skills/mxcp-expert/references/debugging-guide.md
Normal file
@@ -0,0 +1,576 @@
|
||||
# MXCP Debugging Guide
|
||||
|
||||
**Systematic approach to debugging MXCP servers when things don't work.**
|
||||
|
||||
## Debug Mode
|
||||
|
||||
### Enable Debug Logging
|
||||
|
||||
```bash
|
||||
# Option 1: Environment variable
|
||||
export MXCP_DEBUG=1
|
||||
mxcp serve
|
||||
|
||||
# Option 2: CLI flag
|
||||
mxcp serve --debug
|
||||
|
||||
# Option 3: For specific commands
|
||||
mxcp validate --debug
|
||||
mxcp test --debug
|
||||
mxcp run tool my_tool --param key=value --debug
|
||||
```
|
||||
|
||||
**Debug mode shows**:
|
||||
- SQL queries being executed
|
||||
- Parameter values
|
||||
- Type conversions
|
||||
- Error stack traces
|
||||
- Internal MXCP operations
|
||||
|
||||
## Debugging Workflow
|
||||
|
||||
### Step 1: Identify the Layer
|
||||
|
||||
When something fails, determine which layer has the problem:
|
||||
|
||||
```
|
||||
User Request
|
||||
↓
|
||||
MXCP Validation (YAML structure)
|
||||
↓
|
||||
Parameter Binding (Type conversion)
|
||||
↓
|
||||
Python Code Execution (if language: python)
|
||||
↓
|
||||
SQL Execution (if SQL source)
|
||||
↓
|
||||
Type Validation (Return type check)
|
||||
↓
|
||||
Response to LLM
|
||||
```
|
||||
|
||||
**Run these commands in order**:
|
||||
|
||||
```bash
|
||||
# 1. Structure validation
|
||||
mxcp validate
|
||||
# If fails → YAML structure issue (go to "YAML Errors" section)
|
||||
|
||||
# 2. Test with known inputs
|
||||
mxcp test
|
||||
# If fails → Logic or SQL issue (go to "Test Failures" section)
|
||||
|
||||
# 3. Manual execution
|
||||
mxcp run tool my_tool --param key=value
|
||||
# If fails → Runtime issue (go to "Runtime Errors" section)
|
||||
|
||||
# 4. Debug mode
|
||||
mxcp run tool my_tool --param key=value --debug
|
||||
# See detailed execution logs
|
||||
```
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### YAML Validation Errors
|
||||
|
||||
#### Error: "Invalid YAML syntax"
|
||||
|
||||
```bash
|
||||
# Check YAML syntax
|
||||
mxcp validate --debug
|
||||
|
||||
# Common causes:
|
||||
# 1. Mixed tabs and spaces (use spaces only)
|
||||
# 2. Incorrect indentation
|
||||
# 3. Missing quotes around special characters
|
||||
# 4. Unclosed quotes or brackets
|
||||
```
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
# Use yamllint to check
|
||||
pip install yamllint
|
||||
yamllint tools/my_tool.yml
|
||||
|
||||
# Or use online validator
|
||||
# https://www.yamllint.com/
|
||||
```
|
||||
|
||||
#### Error: "Missing required field: description"
|
||||
|
||||
```yaml
|
||||
# ❌ WRONG
|
||||
tool:
|
||||
name: my_tool
|
||||
parameters: []
|
||||
source:
|
||||
code: SELECT * FROM table
|
||||
|
||||
# ✅ CORRECT
|
||||
tool:
|
||||
name: my_tool
|
||||
description: "What this tool does" # ← Added
|
||||
parameters: []
|
||||
source:
|
||||
code: SELECT * FROM table
|
||||
```
|
||||
|
||||
#### Error: "Invalid type specification"
|
||||
|
||||
```yaml
|
||||
# ❌ WRONG
|
||||
return:
|
||||
type: "object" # Quoted string
|
||||
properties:
|
||||
id: "integer" # Quoted string
|
||||
|
||||
# ✅ CORRECT
|
||||
return:
|
||||
type: object # Unquoted
|
||||
properties:
|
||||
id: { type: integer } # Proper structure
|
||||
```
|
||||
|
||||
### Test Failures
|
||||
|
||||
#### Error: "Expected X, got Y" in test
|
||||
|
||||
```yaml
|
||||
# Test says: Expected 5 items, got 3
|
||||
|
||||
# Debug steps:
|
||||
# 1. Run SQL directly
|
||||
mxcp query "SELECT * FROM table WHERE condition"
|
||||
|
||||
# 2. Check test data exists
|
||||
mxcp query "SELECT COUNT(*) FROM table WHERE condition"
|
||||
|
||||
# 3. Verify filter logic
|
||||
mxcp run tool my_tool --param key=test_value --debug
|
||||
```
|
||||
|
||||
**Common causes**:
|
||||
- Test data not loaded (`dbt seed` not run)
|
||||
- Wrong filter condition in SQL
|
||||
- Test expects wrong values
|
||||
|
||||
#### Error: "Type mismatch"
|
||||
|
||||
```yaml
|
||||
# Test fails: Expected integer, got string
|
||||
|
||||
# Check SQL output types
|
||||
mxcp query "DESCRIBE table"
|
||||
|
||||
# Fix: Cast in SQL
|
||||
SELECT
|
||||
CAST(column AS INTEGER) as column # Explicit cast
|
||||
FROM table
|
||||
```
|
||||
|
||||
### SQL Errors
|
||||
|
||||
#### Error: "Table 'xyz' does not exist"
|
||||
|
||||
```bash
|
||||
# List all tables
|
||||
mxcp query "SHOW TABLES"
|
||||
|
||||
# Check if dbt models/seeds loaded
|
||||
dbt seed
|
||||
dbt run
|
||||
|
||||
# Verify table name (case-sensitive)
|
||||
mxcp query "SELECT * FROM xyz LIMIT 1"
|
||||
```
|
||||
|
||||
#### Error: "Column 'abc' not found"
|
||||
|
||||
```bash
|
||||
# Show table schema
|
||||
mxcp query "DESCRIBE table_name"
|
||||
|
||||
# Check column names (case-sensitive)
|
||||
mxcp query "SELECT * FROM table_name LIMIT 1"
|
||||
|
||||
# Common issue: typo or wrong case
|
||||
SELECT customer_id # ← Check exact spelling
|
||||
```
|
||||
|
||||
#### Error: "Syntax error near..."
|
||||
|
||||
```bash
|
||||
# Test SQL directly with debug
|
||||
mxcp query "YOUR SQL HERE" --debug
|
||||
|
||||
# Common SQL syntax errors:
|
||||
# 1. Missing quotes around strings
|
||||
# 2. Wrong parameter binding syntax (use $param not :param)
|
||||
# 3. DuckDB-specific syntax issues
|
||||
```
|
||||
|
||||
### Parameter Binding Errors
|
||||
|
||||
#### Error: "Unbound parameter: $param1"
|
||||
|
||||
```yaml
|
||||
# ❌ WRONG: Parameter used but not defined
|
||||
tool:
|
||||
name: my_tool
|
||||
parameters:
|
||||
- name: other_param
|
||||
type: string
|
||||
source:
|
||||
code: SELECT * FROM table WHERE col = $param1 # ← Not defined!
|
||||
|
||||
# ✅ CORRECT: Define all parameters
|
||||
tool:
|
||||
name: my_tool
|
||||
parameters:
|
||||
- name: param1 # ← Added
|
||||
type: string
|
||||
- name: other_param
|
||||
type: string
|
||||
source:
|
||||
code: SELECT * FROM table WHERE col = $param1
|
||||
```
|
||||
|
||||
#### Error: "Type mismatch for parameter"
|
||||
|
||||
```yaml
|
||||
# MXCP tries to convert "abc" to integer → fails
|
||||
|
||||
# ✅ Solution: Validate types match usage
|
||||
parameters:
|
||||
- name: age
|
||||
type: integer # ← Must be integer for numeric comparison
|
||||
source:
|
||||
code: SELECT * FROM users WHERE age > $age # Numeric comparison
|
||||
```
|
||||
|
||||
### Python Errors
|
||||
|
||||
#### Error: "ModuleNotFoundError: No module named 'xyz'"
|
||||
|
||||
```bash
|
||||
# Check requirements.txt exists
|
||||
cat requirements.txt
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Or install specific module
|
||||
pip install xyz
|
||||
```
|
||||
|
||||
#### Error: "ImportError: cannot import name 'db'"
|
||||
|
||||
```python
|
||||
# ❌ WRONG import path
|
||||
from mxcp import db
|
||||
|
||||
# ✅ CORRECT import path
|
||||
from mxcp.runtime import db
|
||||
```
|
||||
|
||||
#### Error: "Function 'xyz' not found in module"
|
||||
|
||||
```yaml
|
||||
# tools/my_tool.yml
|
||||
source:
|
||||
file: ../python/my_module.py # ← Check file path
|
||||
|
||||
# Common issues:
|
||||
# 1. Wrong file path (use ../ to go up from tools/)
|
||||
# 2. Function name typo
|
||||
# 3. Function not exported (not at module level)
|
||||
```
|
||||
|
||||
**Check function exists**:
|
||||
```bash
|
||||
# Read Python file
|
||||
cat python/my_module.py | grep "^def\|^async def"
|
||||
|
||||
# Should see your function listed
|
||||
```
|
||||
|
||||
#### Error: "Async function called incorrectly"
|
||||
|
||||
```python
|
||||
# ❌ WRONG: Calling async function without await
|
||||
def my_tool():
|
||||
result = async_function() # ← Missing await!
|
||||
return result
|
||||
|
||||
# ✅ CORRECT: Properly handle async
|
||||
async def my_tool():
|
||||
result = await async_function() # ← Added await
|
||||
return result
|
||||
```
|
||||
|
||||
### Return Type Validation Errors
|
||||
|
||||
#### Error: "Expected array, got object"
|
||||
|
||||
```yaml
|
||||
# SQL returns multiple rows (array) but type says object
|
||||
|
||||
# ❌ WRONG
|
||||
return:
|
||||
type: object # ← Wrong! SQL returns array
|
||||
source:
|
||||
code: SELECT * FROM table # Returns multiple rows
|
||||
|
||||
# ✅ CORRECT
|
||||
return:
|
||||
type: array # ← Matches SQL output
|
||||
items:
|
||||
type: object
|
||||
source:
|
||||
code: SELECT * FROM table
|
||||
```
|
||||
|
||||
#### Error: "Missing required field 'xyz'"
|
||||
|
||||
```yaml
|
||||
# Return type expects field that SQL doesn't return
|
||||
|
||||
# ❌ WRONG
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer }
|
||||
missing_field: { type: string } # ← SQL doesn't return this!
|
||||
source:
|
||||
code: SELECT id FROM table # Only returns 'id'
|
||||
|
||||
# ✅ CORRECT: Match return type to actual SQL output
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer } # Only what SQL returns
|
||||
source:
|
||||
code: SELECT id FROM table
|
||||
```
|
||||
|
||||
## Debugging Techniques
|
||||
|
||||
### 1. Test SQL Directly
|
||||
|
||||
```bash
|
||||
# Instead of testing whole tool, test SQL first
|
||||
mxcp query "SELECT * FROM table WHERE condition LIMIT 5"
|
||||
|
||||
# Test with parameters manually
|
||||
mxcp query "SELECT * FROM table WHERE id = 123"
|
||||
|
||||
# Check aggregations
|
||||
mxcp query "SELECT COUNT(*), SUM(amount) FROM table"
|
||||
```
|
||||
|
||||
### 2. Add Debug Prints to Python
|
||||
|
||||
```python
|
||||
# python/my_module.py
|
||||
import sys
|
||||
|
||||
def my_function(param: str) -> dict:
|
||||
# Debug output (goes to stderr, won't affect result)
|
||||
print(f"DEBUG: param={param}", file=sys.stderr)
|
||||
|
||||
result = process(param)
|
||||
|
||||
print(f"DEBUG: result={result}", file=sys.stderr)
|
||||
|
||||
return result
|
||||
```
|
||||
|
||||
**View debug output**:
|
||||
```bash
|
||||
mxcp serve --debug 2>&1 | grep DEBUG
|
||||
```
|
||||
|
||||
### 3. Isolate the Problem
|
||||
|
||||
```python
|
||||
# Break complex function into steps
|
||||
|
||||
# ❌ Hard to debug
|
||||
def complex_function(data):
|
||||
return process(transform(validate(data)))
|
||||
|
||||
# ✅ Easy to debug
|
||||
def complex_function(data):
|
||||
print("Step 1: Validate", file=sys.stderr)
|
||||
validated = validate(data)
|
||||
|
||||
print("Step 2: Transform", file=sys.stderr)
|
||||
transformed = transform(validated)
|
||||
|
||||
print("Step 3: Process", file=sys.stderr)
|
||||
processed = process(transformed)
|
||||
|
||||
return processed
|
||||
```
|
||||
|
||||
### 4. Test with Minimal Input
|
||||
|
||||
```bash
|
||||
# Start with simplest possible input
|
||||
mxcp run tool my_tool --param id=1
|
||||
|
||||
# Gradually add complexity
|
||||
mxcp run tool my_tool --param id=1 --param status=active
|
||||
|
||||
# Test edge cases
|
||||
mxcp run tool my_tool --param id=999999 # Non-existent
|
||||
mxcp run tool my_tool # Missing required param
|
||||
```
|
||||
|
||||
### 5. Check Logs
|
||||
|
||||
```bash
|
||||
# Server logs (if running)
|
||||
mxcp serve --debug 2>&1 | tee mxcp.log
|
||||
|
||||
# View recent errors
|
||||
grep -i error mxcp.log
|
||||
|
||||
# View SQL queries
|
||||
grep -i select mxcp.log
|
||||
```
|
||||
|
||||
### 6. Verify Data
|
||||
|
||||
```bash
|
||||
# Check seed data loaded
|
||||
dbt seed --select my_data
|
||||
mxcp query "SELECT COUNT(*) FROM my_data"
|
||||
|
||||
# Check dbt models built
|
||||
dbt run --select my_model
|
||||
mxcp query "SELECT COUNT(*) FROM my_model"
|
||||
|
||||
# Verify test fixtures
|
||||
mxcp query "SELECT * FROM test_fixtures LIMIT 5"
|
||||
```
|
||||
|
||||
## Common Debugging Scenarios
|
||||
|
||||
### Scenario 1: Tool Returns Empty Results
|
||||
|
||||
```bash
|
||||
# 1. Check if data exists
|
||||
mxcp query "SELECT COUNT(*) FROM table"
|
||||
# → If 0, data not loaded (run dbt seed)
|
||||
|
||||
# 2. Check filter condition
|
||||
mxcp query "SELECT * FROM table WHERE condition"
|
||||
# → Test condition manually
|
||||
|
||||
# 3. Check parameter value
|
||||
mxcp run tool my_tool --param key=value --debug
|
||||
# → See actual SQL with parameter values
|
||||
```
|
||||
|
||||
### Scenario 2: Tool Crashes/Returns Error
|
||||
|
||||
```bash
|
||||
# 1. Validate structure
|
||||
mxcp validate
|
||||
# → Fix any YAML errors first
|
||||
|
||||
# 2. Test in isolation
|
||||
mxcp test tool my_tool
|
||||
# → See specific error
|
||||
|
||||
# 3. Run with debug
|
||||
mxcp run tool my_tool --param key=value --debug
|
||||
# → See full stack trace
|
||||
```
|
||||
|
||||
### Scenario 3: Wrong Data Returned
|
||||
|
||||
```bash
|
||||
# 1. Test SQL directly
|
||||
mxcp query "SELECT * FROM table LIMIT 5"
|
||||
# → Verify columns and values
|
||||
|
||||
# 2. Check test assertions
|
||||
# In YAML, verify test expected results match actual
|
||||
|
||||
# 3. Verify type conversions
|
||||
mxcp query "SELECT typeof(column) as type FROM table LIMIT 1"
|
||||
# → Check DuckDB types
|
||||
```
|
||||
|
||||
### Scenario 4: Performance Issues
|
||||
|
||||
```bash
|
||||
# 1. Check query execution time
|
||||
time mxcp query "SELECT * FROM large_table"
|
||||
|
||||
# 2. Analyze query plan
|
||||
mxcp query "EXPLAIN SELECT * FROM table WHERE condition"
|
||||
|
||||
# 3. Check for missing indexes
|
||||
mxcp query "PRAGMA show_tables_expanded"
|
||||
|
||||
# 4. Limit results during development
|
||||
SELECT * FROM table LIMIT 100 # Add LIMIT for testing
|
||||
```
|
||||
|
||||
## Debugging Checklist
|
||||
|
||||
When something doesn't work:
|
||||
|
||||
- [ ] Run `mxcp validate` to check YAML structure
|
||||
- [ ] Run `mxcp test` to check logic
|
||||
- [ ] Run `mxcp run tool <name> --debug` to see details
|
||||
- [ ] Test SQL directly with `mxcp query`
|
||||
- [ ] Check data loaded with `dbt seed` or `dbt run`
|
||||
- [ ] Verify Python imports work (`from mxcp.runtime import db`)
|
||||
- [ ] Check requirements.txt and install dependencies
|
||||
- [ ] Add debug prints to Python code
|
||||
- [ ] Test with minimal/simple inputs first
|
||||
- [ ] Check return types match actual data
|
||||
- [ ] Review logs for errors
|
||||
|
||||
## Getting Help
|
||||
|
||||
### Information to Provide
|
||||
|
||||
When asking for help or reporting issues:
|
||||
|
||||
1. **Error message** (full text)
|
||||
2. **Command that failed** (exact command)
|
||||
3. **Tool YAML** (relevant parts)
|
||||
4. **Debug output** (`--debug` flag)
|
||||
5. **Environment** (`mxcp --version`, `python --version`)
|
||||
|
||||
### Self-Help Steps
|
||||
|
||||
Before asking for help:
|
||||
|
||||
1. Read the error message carefully
|
||||
2. Check this debugging guide
|
||||
3. Search error message in documentation
|
||||
4. Test components in isolation
|
||||
5. Create minimal reproduction case
|
||||
|
||||
## Summary
|
||||
|
||||
**Debugging workflow**:
|
||||
1. `mxcp validate` → Fix YAML errors
|
||||
2. `mxcp test` → Fix logic errors
|
||||
3. `mxcp run --debug` → See detailed execution
|
||||
4. `mxcp query` → Test SQL directly
|
||||
5. Add debug prints → Trace Python execution
|
||||
6. Test in isolation → Identify exact failure point
|
||||
|
||||
**Remember**:
|
||||
- Start simple, add complexity gradually
|
||||
- Test each layer independently
|
||||
- Use debug mode liberally
|
||||
- Check data loaded before testing queries
|
||||
- Verify types match at every step
|
||||
546
skills/mxcp-expert/references/duckdb-essentials.md
Normal file
546
skills/mxcp-expert/references/duckdb-essentials.md
Normal file
@@ -0,0 +1,546 @@
|
||||
# DuckDB Essentials for MXCP
|
||||
|
||||
Essential DuckDB knowledge for building MXCP servers with embedded analytics.
|
||||
|
||||
## What is DuckDB?
|
||||
|
||||
**DuckDB is an embedded, in-process SQL OLAP database** - think "SQLite for analytics". It runs directly in your MXCP server process without needing a separate database server.
|
||||
|
||||
**Key characteristics**:
|
||||
- **Embedded**: No server setup, no configuration
|
||||
- **Fast**: Vectorized execution engine, parallel processing
|
||||
- **Versatile**: Reads CSV, Parquet, JSON directly from disk or URLs
|
||||
- **SQL**: Full SQL support with analytical extensions
|
||||
- **Portable**: Single-file database, easy to move/backup
|
||||
|
||||
**MXCP uses DuckDB by default** for all SQL-based tools and resources.
|
||||
|
||||
## Core Features for MXCP
|
||||
|
||||
### 1. Direct File Reading
|
||||
|
||||
**DuckDB can query files without importing them first**:
|
||||
|
||||
```sql
|
||||
-- Query CSV directly
|
||||
SELECT * FROM 'data/sales.csv'
|
||||
|
||||
-- Query with explicit reader
|
||||
SELECT * FROM read_csv_auto('data/sales.csv')
|
||||
|
||||
-- Query Parquet
|
||||
SELECT * FROM 'data/sales.parquet'
|
||||
|
||||
-- Query JSON
|
||||
SELECT * FROM read_json_auto('data/events.json')
|
||||
|
||||
-- Query from URL
|
||||
SELECT * FROM 'https://example.com/data.csv'
|
||||
```
|
||||
|
||||
**Auto-detection**: DuckDB automatically infers:
|
||||
- Column names from headers
|
||||
- Data types from values
|
||||
- CSV delimiters, quotes, etc.
|
||||
|
||||
### 2. CSV Import and Export
|
||||
|
||||
**Import CSV to table**:
|
||||
```sql
|
||||
-- Create table from CSV
|
||||
CREATE TABLE sales AS
|
||||
SELECT * FROM read_csv_auto('sales.csv')
|
||||
|
||||
-- Or use COPY
|
||||
COPY sales FROM 'sales.csv' (AUTO_DETECT TRUE)
|
||||
```
|
||||
|
||||
**Export to CSV**:
|
||||
```sql
|
||||
-- Export query results
|
||||
COPY (SELECT * FROM sales WHERE region = 'US')
|
||||
TO 'us_sales.csv' (HEADER, DELIMITER ',')
|
||||
```
|
||||
|
||||
**CSV reading options**:
|
||||
```sql
|
||||
SELECT * FROM read_csv_auto(
|
||||
'data.csv',
|
||||
header = true,
|
||||
delim = ',',
|
||||
quote = '"',
|
||||
dateformat = '%Y-%m-%d'
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Data Types
|
||||
|
||||
**Common DuckDB types** (important for MXCP type validation):
|
||||
|
||||
```sql
|
||||
-- Numeric
|
||||
INTEGER, BIGINT, DECIMAL(10,2), DOUBLE
|
||||
|
||||
-- String
|
||||
VARCHAR, TEXT
|
||||
|
||||
-- Temporal
|
||||
DATE, TIME, TIMESTAMP, INTERVAL
|
||||
|
||||
-- Complex
|
||||
ARRAY, STRUCT, MAP, JSON
|
||||
|
||||
-- Boolean
|
||||
BOOLEAN
|
||||
```
|
||||
|
||||
**Type casting**:
|
||||
```sql
|
||||
-- Cast to specific type
|
||||
SELECT CAST(amount AS DECIMAL(10,2)) FROM sales
|
||||
|
||||
-- Short syntax
|
||||
SELECT amount::DECIMAL(10,2) FROM sales
|
||||
|
||||
-- Date parsing
|
||||
SELECT CAST('2025-01-15' AS DATE)
|
||||
```
|
||||
|
||||
### 4. SQL Extensions
|
||||
|
||||
**DuckDB adds useful SQL extensions beyond standard SQL**:
|
||||
|
||||
**EXCLUDE clause** (select all except):
|
||||
```sql
|
||||
-- Select all columns except sensitive ones
|
||||
SELECT * EXCLUDE (ssn, salary) FROM employees
|
||||
```
|
||||
|
||||
**REPLACE clause** (modify columns in SELECT *):
|
||||
```sql
|
||||
-- Replace amount with rounded version
|
||||
SELECT * REPLACE (ROUND(amount, 2) AS amount) FROM sales
|
||||
```
|
||||
|
||||
**List aggregation**:
|
||||
```sql
|
||||
-- Aggregate into arrays
|
||||
SELECT
|
||||
region,
|
||||
LIST(product) AS products,
|
||||
LIST(DISTINCT customer) AS customers
|
||||
FROM sales
|
||||
GROUP BY region
|
||||
```
|
||||
|
||||
**String aggregation**:
|
||||
```sql
|
||||
SELECT
|
||||
department,
|
||||
STRING_AGG(employee_name, ', ') AS team_members
|
||||
FROM employees
|
||||
GROUP BY department
|
||||
```
|
||||
|
||||
### 5. Analytical Functions
|
||||
|
||||
**Window functions**:
|
||||
```sql
|
||||
-- Running totals
|
||||
SELECT
|
||||
date,
|
||||
amount,
|
||||
SUM(amount) OVER (ORDER BY date) AS running_total
|
||||
FROM sales
|
||||
|
||||
-- Ranking
|
||||
SELECT
|
||||
product,
|
||||
sales,
|
||||
RANK() OVER (ORDER BY sales DESC) AS rank
|
||||
FROM product_sales
|
||||
|
||||
-- Partitioned windows
|
||||
SELECT
|
||||
region,
|
||||
product,
|
||||
sales,
|
||||
AVG(sales) OVER (PARTITION BY region) AS regional_avg
|
||||
FROM sales
|
||||
```
|
||||
|
||||
**Percentiles and statistics**:
|
||||
```sql
|
||||
SELECT
|
||||
PERCENTILE_CONT(0.5) AS median,
|
||||
PERCENTILE_CONT(0.95) AS p95,
|
||||
STDDEV(amount) AS std_dev,
|
||||
CORR(amount, quantity) AS correlation
|
||||
FROM sales
|
||||
```
|
||||
|
||||
### 6. Date and Time Functions
|
||||
|
||||
```sql
|
||||
-- Current timestamp
|
||||
SELECT CURRENT_TIMESTAMP
|
||||
|
||||
-- Date arithmetic
|
||||
SELECT date + INTERVAL '7 days' AS next_week
|
||||
SELECT date - INTERVAL '1 month' AS last_month
|
||||
|
||||
-- Date truncation
|
||||
SELECT DATE_TRUNC('month', timestamp) AS month
|
||||
SELECT DATE_TRUNC('week', timestamp) AS week
|
||||
|
||||
-- Date parts
|
||||
SELECT
|
||||
YEAR(date) AS year,
|
||||
MONTH(date) AS month,
|
||||
DAYOFWEEK(date) AS day_of_week
|
||||
```
|
||||
|
||||
### 7. JSON Support
|
||||
|
||||
**Parse JSON strings**:
|
||||
```sql
|
||||
-- Extract JSON fields
|
||||
SELECT
|
||||
json_extract(data, '$.user_id') AS user_id,
|
||||
json_extract(data, '$.event_type') AS event_type
|
||||
FROM events
|
||||
|
||||
-- Arrow notation (shorthand)
|
||||
SELECT
|
||||
data->'user_id' AS user_id,
|
||||
data->>'event_type' AS event_type
|
||||
FROM events
|
||||
```
|
||||
|
||||
**Read JSON files**:
|
||||
```sql
|
||||
SELECT * FROM read_json_auto('events.json')
|
||||
```
|
||||
|
||||
### 8. Performance Features
|
||||
|
||||
**Parallel execution** (automatic):
|
||||
- DuckDB uses all CPU cores automatically
|
||||
- No configuration needed
|
||||
|
||||
**Larger-than-memory processing**:
|
||||
- Spills to disk when needed
|
||||
- Handles datasets larger than RAM
|
||||
|
||||
**Columnar storage**:
|
||||
- Efficient for analytical queries
|
||||
- Fast aggregations and filters
|
||||
|
||||
**Indexes** (for point lookups):
|
||||
```sql
|
||||
CREATE INDEX idx_customer ON sales(customer_id)
|
||||
```
|
||||
|
||||
## MXCP Integration
|
||||
|
||||
### Database Connection
|
||||
|
||||
**Automatic in MXCP** - no setup needed:
|
||||
```yaml
|
||||
# mxcp-site.yml
|
||||
# DuckDB is the default, no configuration required
|
||||
```
|
||||
|
||||
**Environment variable** for custom path:
|
||||
```bash
|
||||
# Default database path is data/db-default.duckdb
|
||||
export MXCP_DUCKDB_PATH="/path/to/data/db-default.duckdb"
|
||||
mxcp serve
|
||||
```
|
||||
|
||||
**Profile-specific databases**:
|
||||
```yaml
|
||||
# mxcp-site.yml
|
||||
profiles:
|
||||
development:
|
||||
database:
|
||||
path: "dev.duckdb"
|
||||
production:
|
||||
database:
|
||||
path: "prod.duckdb"
|
||||
```
|
||||
|
||||
### Using DuckDB in MXCP Tools
|
||||
|
||||
**Direct SQL queries**:
|
||||
```yaml
|
||||
# tools/query_sales.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: query_sales
|
||||
source:
|
||||
code: |
|
||||
SELECT
|
||||
region,
|
||||
SUM(amount) as total,
|
||||
COUNT(*) as count
|
||||
FROM sales
|
||||
WHERE sale_date >= $start_date
|
||||
GROUP BY region
|
||||
ORDER BY total DESC
|
||||
```
|
||||
|
||||
**Query CSV files directly**:
|
||||
```yaml
|
||||
tool:
|
||||
name: analyze_upload
|
||||
source:
|
||||
code: |
|
||||
SELECT
|
||||
COUNT(*) as rows,
|
||||
COUNT(DISTINCT customer_id) as unique_customers,
|
||||
SUM(amount) as total_revenue
|
||||
FROM 'uploads/$filename'
|
||||
```
|
||||
|
||||
**Complex analytical queries**:
|
||||
```yaml
|
||||
tool:
|
||||
name: customer_cohorts
|
||||
source:
|
||||
code: |
|
||||
WITH first_purchase AS (
|
||||
SELECT
|
||||
customer_id,
|
||||
MIN(DATE_TRUNC('month', purchase_date)) AS cohort_month
|
||||
FROM purchases
|
||||
GROUP BY customer_id
|
||||
),
|
||||
cohort_size AS (
|
||||
SELECT
|
||||
cohort_month,
|
||||
COUNT(DISTINCT customer_id) AS cohort_size
|
||||
FROM first_purchase
|
||||
GROUP BY cohort_month
|
||||
)
|
||||
SELECT
|
||||
fp.cohort_month,
|
||||
DATE_TRUNC('month', p.purchase_date) AS activity_month,
|
||||
COUNT(DISTINCT p.customer_id) AS active_customers,
|
||||
cs.cohort_size,
|
||||
COUNT(DISTINCT p.customer_id)::FLOAT / cs.cohort_size AS retention_rate
|
||||
FROM purchases p
|
||||
JOIN first_purchase fp ON p.customer_id = fp.customer_id
|
||||
JOIN cohort_size cs ON fp.cohort_month = cs.cohort_month
|
||||
GROUP BY fp.cohort_month, activity_month, cs.cohort_size
|
||||
ORDER BY fp.cohort_month, activity_month
|
||||
```
|
||||
|
||||
### Using DuckDB in Python Endpoints
|
||||
|
||||
**Access via MXCP runtime**:
|
||||
```python
|
||||
from mxcp.runtime import db
|
||||
|
||||
def analyze_data(region: str) -> dict:
|
||||
# Execute query
|
||||
result = db.execute(
|
||||
"SELECT SUM(amount) as total FROM sales WHERE region = $1",
|
||||
{"region": region}
|
||||
)
|
||||
|
||||
# Fetch results
|
||||
row = result.fetchone()
|
||||
return {"total": row["total"]}
|
||||
|
||||
def batch_insert(records: list[dict]) -> dict:
|
||||
# Insert data
|
||||
db.execute(
|
||||
"INSERT INTO logs (timestamp, event) VALUES ($1, $2)",
|
||||
[(r["timestamp"], r["event"]) for r in records]
|
||||
)
|
||||
|
||||
return {"inserted": len(records)}
|
||||
```
|
||||
|
||||
**Read files in Python**:
|
||||
```python
|
||||
def import_csv(filepath: str) -> dict:
|
||||
# Create table from CSV
|
||||
db.execute(f"""
|
||||
CREATE TABLE imported_data AS
|
||||
SELECT * FROM read_csv_auto('{filepath}')
|
||||
""")
|
||||
|
||||
# Get stats
|
||||
result = db.execute("SELECT COUNT(*) as count FROM imported_data")
|
||||
return {"rows_imported": result.fetchone()["count"]}
|
||||
```
|
||||
|
||||
## Best Practices for MXCP
|
||||
|
||||
### 1. Use Parameter Binding
|
||||
|
||||
**ALWAYS use parameterized queries** to prevent SQL injection:
|
||||
|
||||
✅ **Correct**:
|
||||
```yaml
|
||||
source:
|
||||
code: |
|
||||
SELECT * FROM sales WHERE region = $region
|
||||
```
|
||||
|
||||
❌ **WRONG** (SQL injection risk):
|
||||
```yaml
|
||||
source:
|
||||
code: |
|
||||
SELECT * FROM sales WHERE region = '$region'
|
||||
```
|
||||
|
||||
### 2. Optimize Queries
|
||||
|
||||
**Index frequently filtered columns**:
|
||||
```sql
|
||||
CREATE INDEX idx_customer ON orders(customer_id)
|
||||
CREATE INDEX idx_date ON orders(order_date)
|
||||
```
|
||||
|
||||
**Use EXPLAIN to analyze queries**:
|
||||
```sql
|
||||
EXPLAIN SELECT * FROM large_table WHERE id = 123
|
||||
```
|
||||
|
||||
**Materialize complex aggregations** (via dbt models):
|
||||
```sql
|
||||
-- Instead of computing on every query
|
||||
-- Create a materialized view via dbt
|
||||
CREATE TABLE daily_summary AS
|
||||
SELECT
|
||||
DATE_TRUNC('day', timestamp) AS date,
|
||||
COUNT(*) AS count,
|
||||
SUM(amount) AS total
|
||||
FROM transactions
|
||||
GROUP BY date
|
||||
```
|
||||
|
||||
### 3. Handle Large Datasets
|
||||
|
||||
**For large CSVs** (>100MB):
|
||||
- Use Parquet format instead (much faster)
|
||||
- Create tables rather than querying files directly
|
||||
- Use dbt to materialize transformations
|
||||
|
||||
**Conversion to Parquet**:
|
||||
```sql
|
||||
COPY (SELECT * FROM 'large_data.csv')
|
||||
TO 'large_data.parquet' (FORMAT PARQUET)
|
||||
```
|
||||
|
||||
### 4. Data Types in MXCP
|
||||
|
||||
**Match DuckDB types to MXCP types**:
|
||||
|
||||
```yaml
|
||||
# MXCP tool definition
|
||||
parameters:
|
||||
- name: amount
|
||||
type: number # → DuckDB DOUBLE
|
||||
- name: quantity
|
||||
type: integer # → DuckDB INTEGER
|
||||
- name: description
|
||||
type: string # → DuckDB VARCHAR
|
||||
- name: created_at
|
||||
type: string
|
||||
format: date-time # → DuckDB TIMESTAMP
|
||||
- name: is_active
|
||||
type: boolean # → DuckDB BOOLEAN
|
||||
```
|
||||
|
||||
### 5. Database File Management
|
||||
|
||||
**Backup**:
|
||||
```bash
|
||||
# DuckDB is a single file - just copy it (default: data/db-default.duckdb)
|
||||
cp data/db-default.duckdb data/db-default.duckdb.backup
|
||||
```
|
||||
|
||||
**Export to SQL**:
|
||||
```sql
|
||||
EXPORT DATABASE 'backup_directory'
|
||||
```
|
||||
|
||||
**Import from SQL**:
|
||||
```sql
|
||||
IMPORT DATABASE 'backup_directory'
|
||||
```
|
||||
|
||||
## Common Patterns in MXCP
|
||||
|
||||
### Pattern 1: CSV → Table → Query
|
||||
|
||||
```bash
|
||||
# 1. Load CSV via dbt seed
|
||||
dbt seed --select customers
|
||||
|
||||
# 2. Query from MXCP tool
|
||||
SELECT * FROM customers WHERE country = $country
|
||||
```
|
||||
|
||||
### Pattern 2: External Data Caching
|
||||
|
||||
```sql
|
||||
-- dbt model: cache_external_data.sql
|
||||
{{ config(materialized='table') }}
|
||||
|
||||
SELECT * FROM read_csv_auto('https://example.com/data.csv')
|
||||
```
|
||||
|
||||
### Pattern 3: Multi-File Aggregation
|
||||
|
||||
```sql
|
||||
-- Query multiple CSVs
|
||||
SELECT * FROM 'data/*.csv'
|
||||
|
||||
-- Union multiple Parquet files
|
||||
SELECT * FROM 'archive/2025-*.parquet'
|
||||
```
|
||||
|
||||
### Pattern 4: Real-time + Historical
|
||||
|
||||
```sql
|
||||
-- Combine recent API data with historical cache
|
||||
SELECT * FROM read_json_auto('https://api.com/recent')
|
||||
UNION ALL
|
||||
SELECT * FROM historical_data WHERE date < CURRENT_DATE - INTERVAL '7 days'
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue**: "Table does not exist"
|
||||
**Solution**: Ensure dbt models/seeds have been run, check table name spelling
|
||||
|
||||
**Issue**: "Type mismatch"
|
||||
**Solution**: Add explicit CAST() or update schema.yml with correct data types
|
||||
|
||||
**Issue**: "Out of memory"
|
||||
**Solution**: Reduce query scope, add WHERE filters, materialize intermediate results
|
||||
|
||||
**Issue**: "CSV parsing error"
|
||||
**Solution**: Use read_csv_auto with explicit options (delim, quote, etc.)
|
||||
|
||||
**Issue**: "Slow queries"
|
||||
**Solution**: Add indexes, materialize via dbt, use Parquet instead of CSV
|
||||
|
||||
## Summary for MXCP Builders
|
||||
|
||||
When building MXCP servers with DuckDB:
|
||||
|
||||
1. **Use parameterized queries** (`$param`) to prevent injection
|
||||
2. **Load CSVs via dbt seeds** for version control and validation
|
||||
3. **Materialize complex queries** as dbt models
|
||||
4. **Index frequently filtered columns** for performance
|
||||
5. **Use Parquet for large datasets** (>100MB)
|
||||
6. **Match MXCP types to DuckDB types** in tool definitions
|
||||
7. **Leverage DuckDB extensions** (EXCLUDE, REPLACE, window functions)
|
||||
|
||||
DuckDB is the powerhouse behind MXCP's data capabilities - understanding it enables building robust, high-performance MCP servers.
|
||||
187
skills/mxcp-expert/references/endpoint-patterns.md
Normal file
187
skills/mxcp-expert/references/endpoint-patterns.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# Endpoint Patterns
|
||||
|
||||
Complete examples for creating MXCP endpoints (tools, resources, prompts).
|
||||
|
||||
## SQL Tool - Data Query
|
||||
|
||||
```yaml
|
||||
# tools/sales_report.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: sales_report
|
||||
description: "Get sales data by region and date range"
|
||||
parameters:
|
||||
- name: region
|
||||
type: string
|
||||
examples: ["US-West"]
|
||||
- name: start_date
|
||||
type: string
|
||||
format: date
|
||||
- name: end_date
|
||||
type: string
|
||||
format: date
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
total_sales: { type: number }
|
||||
count: { type: integer }
|
||||
source:
|
||||
code: |
|
||||
SELECT SUM(amount) as total_sales, COUNT(*) as count
|
||||
FROM sales
|
||||
WHERE region = $region
|
||||
AND sale_date BETWEEN $start_date AND $end_date
|
||||
```
|
||||
|
||||
## Python Tool - ML/API Integration
|
||||
|
||||
```yaml
|
||||
# tools/analyze_sentiment.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: analyze_sentiment
|
||||
description: "Analyze sentiment using ML"
|
||||
language: python
|
||||
parameters:
|
||||
- name: texts
|
||||
type: array
|
||||
items: { type: string }
|
||||
return:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
text: { type: string }
|
||||
sentiment: { type: string }
|
||||
confidence: { type: number }
|
||||
source:
|
||||
file: ../python/sentiment.py
|
||||
```
|
||||
|
||||
```python
|
||||
# python/sentiment.py
|
||||
from mxcp.runtime import db, on_init
|
||||
import asyncio
|
||||
|
||||
@on_init
|
||||
def load_model():
|
||||
# Load model once at startup
|
||||
pass
|
||||
|
||||
async def analyze_sentiment(texts: list[str]) -> list[dict]:
|
||||
async def analyze_one(text: str) -> dict:
|
||||
sentiment = "positive" if "good" in text else "neutral"
|
||||
|
||||
db.execute(
|
||||
"INSERT INTO logs (text, sentiment) VALUES ($1, $2)",
|
||||
{"text": text, "sentiment": sentiment}
|
||||
)
|
||||
|
||||
return {"text": text, "sentiment": sentiment, "confidence": 0.85}
|
||||
|
||||
return await asyncio.gather(*[analyze_one(t) for t in texts])
|
||||
```
|
||||
|
||||
## Resource - Data Access
|
||||
|
||||
```yaml
|
||||
# resources/customer_data.yml
|
||||
mxcp: 1
|
||||
resource:
|
||||
uri: "customer://data/{customer_id}"
|
||||
description: "Get customer profile"
|
||||
mime_type: "application/json"
|
||||
parameters:
|
||||
- name: customer_id
|
||||
type: string
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
name: { type: string }
|
||||
email: { type: string }
|
||||
source:
|
||||
code: |
|
||||
SELECT id, name, email FROM customers WHERE id = $customer_id
|
||||
```
|
||||
|
||||
## Prompt Template
|
||||
|
||||
```yaml
|
||||
# prompts/customer_analysis.yml
|
||||
mxcp: 1
|
||||
prompt:
|
||||
name: customer_analysis
|
||||
description: "Analyze customer behavior"
|
||||
parameters:
|
||||
- name: customer_id
|
||||
type: string
|
||||
messages:
|
||||
- role: system
|
||||
type: text
|
||||
prompt: "You are a customer analytics expert."
|
||||
- role: user
|
||||
type: text
|
||||
prompt: "Analyze customer {{ customer_id }} and provide insights."
|
||||
```
|
||||
|
||||
## Combined SQL + Python
|
||||
|
||||
```yaml
|
||||
# tools/customer_insights.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: customer_insights
|
||||
language: python
|
||||
source:
|
||||
file: ../python/insights.py
|
||||
```
|
||||
|
||||
```python
|
||||
# python/insights.py
|
||||
from mxcp.runtime import db
|
||||
|
||||
def customer_insights(customer_id: str) -> dict:
|
||||
# SQL for aggregation
|
||||
stats = db.execute("""
|
||||
SELECT COUNT(*) as orders, SUM(amount) as total
|
||||
FROM orders WHERE customer_id = $id
|
||||
""", {"id": customer_id}).fetchone()
|
||||
|
||||
# Python for analysis
|
||||
trend = calculate_trend(stats)
|
||||
|
||||
return {**dict(stats), "trend": trend}
|
||||
```
|
||||
|
||||
## With Policies
|
||||
|
||||
```yaml
|
||||
tool:
|
||||
name: employee_data
|
||||
policies:
|
||||
input:
|
||||
- condition: "!('hr.read' in user.permissions)"
|
||||
action: deny
|
||||
output:
|
||||
- condition: "user.role != 'hr_manager'"
|
||||
action: filter_fields
|
||||
fields: ["salary", "ssn"]
|
||||
```
|
||||
|
||||
## With Tests
|
||||
|
||||
```yaml
|
||||
tool:
|
||||
name: calculate_total
|
||||
tests:
|
||||
- name: "basic_test"
|
||||
arguments:
|
||||
- key: amount
|
||||
value: 100
|
||||
- key: tax_rate
|
||||
value: 0.1
|
||||
result:
|
||||
total: 110
|
||||
tax: 10
|
||||
```
|
||||
635
skills/mxcp-expert/references/error-handling-guide.md
Normal file
635
skills/mxcp-expert/references/error-handling-guide.md
Normal file
@@ -0,0 +1,635 @@
|
||||
# Error Handling Guide
|
||||
|
||||
**Comprehensive error handling for MXCP servers: SQL errors (managed by MXCP) and Python errors (YOU must handle).**
|
||||
|
||||
## Two Types of Error Handling
|
||||
|
||||
### 1. SQL Errors (Managed by MXCP)
|
||||
|
||||
**MXCP automatically handles**:
|
||||
- SQL syntax errors
|
||||
- Type mismatches
|
||||
- Parameter binding errors
|
||||
- Database connection errors
|
||||
|
||||
**Your responsibility**:
|
||||
- Write correct SQL
|
||||
- Use proper parameter binding (`$param`)
|
||||
- Match return types to actual data
|
||||
|
||||
### 2. Python Errors (YOU Must Handle)
|
||||
|
||||
**You MUST handle**:
|
||||
- External API failures
|
||||
- Invalid input
|
||||
- Resource not found
|
||||
- Business logic errors
|
||||
- Async/await errors
|
||||
|
||||
**Return structured error objects, don't raise exceptions to MXCP.**
|
||||
|
||||
## Python Error Handling Pattern
|
||||
|
||||
### ❌ WRONG: Let Exceptions Bubble Up
|
||||
|
||||
```python
|
||||
# python/api_wrapper.py
|
||||
async def fetch_user(user_id: int) -> dict:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"https://api.example.com/users/{user_id}")
|
||||
response.raise_for_status() # ❌ Will crash if 404/500!
|
||||
return response.json()
|
||||
```
|
||||
|
||||
**Problem**: When API returns 404, exception crashes the tool. LLM gets unhelpful error.
|
||||
|
||||
### ✅ CORRECT: Return Structured Errors
|
||||
|
||||
```python
|
||||
# python/api_wrapper.py
|
||||
import httpx
|
||||
|
||||
async def fetch_user(user_id: int) -> dict:
|
||||
"""
|
||||
Fetch user from external API.
|
||||
|
||||
Returns:
|
||||
Success: {"success": true, "user": {...}}
|
||||
Error: {"success": false, "error": "User not found", "error_code": "NOT_FOUND"}
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(
|
||||
f"https://api.example.com/users/{user_id}"
|
||||
)
|
||||
|
||||
if response.status_code == 404:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"User with ID {user_id} not found",
|
||||
"error_code": "NOT_FOUND",
|
||||
"user_id": user_id
|
||||
}
|
||||
|
||||
if response.status_code >= 500:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "External API is currently unavailable. Please try again later.",
|
||||
"error_code": "API_ERROR",
|
||||
"status_code": response.status_code
|
||||
}
|
||||
|
||||
response.raise_for_status() # Other HTTP errors
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"user": response.json()
|
||||
}
|
||||
|
||||
except httpx.TimeoutException:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Request timed out after 10 seconds. The API may be slow or unavailable.",
|
||||
"error_code": "TIMEOUT"
|
||||
}
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"HTTP error occurred: {str(e)}",
|
||||
"error_code": "HTTP_ERROR"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Unexpected error: {str(e)}",
|
||||
"error_code": "UNKNOWN_ERROR"
|
||||
}
|
||||
```
|
||||
|
||||
**Why good**:
|
||||
- ✅ LLM gets clear error message
|
||||
- ✅ LLM knows what went wrong (error_code)
|
||||
- ✅ LLM can take action (retry, try different ID, etc.)
|
||||
- ✅ Tool never crashes
|
||||
|
||||
## Error Response Structure
|
||||
|
||||
### Standard Error Format
|
||||
|
||||
```python
|
||||
{
|
||||
"success": False,
|
||||
"error": "Human-readable error message for LLM",
|
||||
"error_code": "MACHINE_READABLE_CODE",
|
||||
"details": { # Optional: additional context
|
||||
"attempted_value": user_id,
|
||||
"valid_range": "1-1000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Standard Success Format
|
||||
|
||||
```python
|
||||
{
|
||||
"success": True,
|
||||
"data": {
|
||||
# Actual response data
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Error Scenarios
|
||||
|
||||
### 1. Input Validation Errors
|
||||
|
||||
```python
|
||||
def process_order(order_id: str, quantity: int) -> dict:
|
||||
"""Process an order with validation"""
|
||||
|
||||
# Validate order_id format
|
||||
if not order_id.startswith("ORD_"):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Invalid order ID format. Expected format: 'ORD_XXXXX', got: '{order_id}'",
|
||||
"error_code": "INVALID_FORMAT",
|
||||
"expected_format": "ORD_XXXXX",
|
||||
"provided": order_id
|
||||
}
|
||||
|
||||
# Validate quantity range
|
||||
if quantity <= 0:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Quantity must be positive. Got: {quantity}",
|
||||
"error_code": "INVALID_QUANTITY",
|
||||
"provided": quantity,
|
||||
"valid_range": "1 or greater"
|
||||
}
|
||||
|
||||
if quantity > 1000:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Quantity {quantity} exceeds maximum allowed (1000). Please split into multiple orders.",
|
||||
"error_code": "QUANTITY_EXCEEDED",
|
||||
"provided": quantity,
|
||||
"maximum": 1000
|
||||
}
|
||||
|
||||
# Process order...
|
||||
return {"success": True, "order_id": order_id, "quantity": quantity}
|
||||
```
|
||||
|
||||
### 2. Resource Not Found Errors
|
||||
|
||||
```python
|
||||
from mxcp.runtime import db
|
||||
|
||||
def get_customer(customer_id: str) -> dict:
|
||||
"""Get customer by ID with proper error handling"""
|
||||
|
||||
try:
|
||||
result = db.execute(
|
||||
"SELECT * FROM customers WHERE customer_id = $1",
|
||||
{"customer_id": customer_id}
|
||||
)
|
||||
|
||||
customer = result.fetchone()
|
||||
|
||||
if customer is None:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Customer '{customer_id}' not found in database. Use list_customers to see available customers.",
|
||||
"error_code": "CUSTOMER_NOT_FOUND",
|
||||
"customer_id": customer_id,
|
||||
"suggestion": "Call list_customers tool to see all available customer IDs"
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"customer": dict(customer)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Database error while fetching customer: {str(e)}",
|
||||
"error_code": "DATABASE_ERROR"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. External API Errors
|
||||
|
||||
```python
|
||||
import httpx
|
||||
|
||||
async def create_customer_in_stripe(email: str, name: str) -> dict:
|
||||
"""Create Stripe customer with comprehensive error handling"""
|
||||
|
||||
try:
|
||||
import stripe
|
||||
from mxcp.runtime import get_secret
|
||||
|
||||
# Get API key
|
||||
secret = get_secret("stripe")
|
||||
if not secret:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Stripe API key not configured. Please set up 'stripe' secret in config.yml",
|
||||
"error_code": "MISSING_CREDENTIALS",
|
||||
"required_secret": "stripe"
|
||||
}
|
||||
|
||||
stripe.api_key = secret.get("api_key")
|
||||
|
||||
# Create customer
|
||||
customer = stripe.Customer.create(
|
||||
email=email,
|
||||
name=name
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"customer_id": customer.id,
|
||||
"email": customer.email
|
||||
}
|
||||
|
||||
except stripe.error.InvalidRequestError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Invalid request to Stripe: {str(e)}",
|
||||
"error_code": "INVALID_REQUEST",
|
||||
"details": str(e)
|
||||
}
|
||||
|
||||
except stripe.error.AuthenticationError:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Stripe API key is invalid or expired. Please update credentials.",
|
||||
"error_code": "AUTHENTICATION_FAILED"
|
||||
}
|
||||
|
||||
except stripe.error.RateLimitError:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Stripe rate limit exceeded. Please try again in a few seconds.",
|
||||
"error_code": "RATE_LIMIT",
|
||||
"suggestion": "Wait 5-10 seconds and retry"
|
||||
}
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Stripe error: {str(e)}",
|
||||
"error_code": "STRIPE_ERROR"
|
||||
}
|
||||
|
||||
except ImportError:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Stripe library not installed. Run: pip install stripe",
|
||||
"error_code": "MISSING_DEPENDENCY",
|
||||
"fix": "pip install stripe>=5.0.0"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Unexpected error: {str(e)}",
|
||||
"error_code": "UNKNOWN_ERROR"
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Business Logic Errors
|
||||
|
||||
```python
|
||||
def transfer_funds(from_account: str, to_account: str, amount: float) -> dict:
|
||||
"""Transfer funds with business logic validation"""
|
||||
|
||||
# Check amount
|
||||
if amount <= 0:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Transfer amount must be positive. Got: ${amount}",
|
||||
"error_code": "INVALID_AMOUNT"
|
||||
}
|
||||
|
||||
# Check account exists and get balance
|
||||
from_balance = db.execute(
|
||||
"SELECT balance FROM accounts WHERE account_id = $1",
|
||||
{"account_id": from_account}
|
||||
).fetchone()
|
||||
|
||||
if from_balance is None:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Source account '{from_account}' not found",
|
||||
"error_code": "ACCOUNT_NOT_FOUND",
|
||||
"account_id": from_account
|
||||
}
|
||||
|
||||
# Check sufficient funds
|
||||
if from_balance["balance"] < amount:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Insufficient funds. Available: ${from_balance['balance']:.2f}, Requested: ${amount:.2f}",
|
||||
"error_code": "INSUFFICIENT_FUNDS",
|
||||
"available": from_balance["balance"],
|
||||
"requested": amount,
|
||||
"shortfall": amount - from_balance["balance"]
|
||||
}
|
||||
|
||||
# Perform transfer...
|
||||
return {
|
||||
"success": True,
|
||||
"transfer_id": "TXN_12345",
|
||||
"from_account": from_account,
|
||||
"to_account": to_account,
|
||||
"amount": amount
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Async/Await Errors
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
async def fetch_multiple_users(user_ids: list[int]) -> dict:
|
||||
"""Fetch multiple users concurrently with error handling"""
|
||||
|
||||
async def fetch_one(user_id: int) -> dict:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
response = await client.get(f"https://api.example.com/users/{user_id}")
|
||||
|
||||
if response.status_code == 404:
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"success": False,
|
||||
"error": f"User {user_id} not found"
|
||||
}
|
||||
|
||||
response.raise_for_status()
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"success": True,
|
||||
"user": response.json()
|
||||
}
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"success": False,
|
||||
"error": f"Timeout fetching user {user_id}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
# Fetch all concurrently
|
||||
results = await asyncio.gather(*[fetch_one(uid) for uid in user_ids])
|
||||
|
||||
# Separate successes and failures
|
||||
successes = [r for r in results if r["success"]]
|
||||
failures = [r for r in results if not r["success"]]
|
||||
|
||||
return {
|
||||
"success": len(failures) == 0,
|
||||
"total_requested": len(user_ids),
|
||||
"successful": len(successes),
|
||||
"failed": len(failures),
|
||||
"users": [r["user"] for r in successes],
|
||||
"errors": [{"user_id": r["user_id"], "error": r["error"]} for r in failures]
|
||||
}
|
||||
```
|
||||
|
||||
## Error Messages for LLMs
|
||||
|
||||
### Principles for Good Error Messages
|
||||
|
||||
1. **Be Specific**: Tell exactly what went wrong
|
||||
2. **Be Actionable**: Suggest what to do next
|
||||
3. **Provide Context**: Include relevant values/IDs
|
||||
4. **Use Plain Language**: Avoid technical jargon
|
||||
|
||||
### ❌ BAD Error Messages
|
||||
|
||||
```python
|
||||
return {"error": "Error"} # ❌ Useless
|
||||
return {"error": "Invalid input"} # ❌ Which input? Why invalid?
|
||||
return {"error": "DB error"} # ❌ What kind of error?
|
||||
return {"error": str(e)} # ❌ Raw exception message (often cryptic)
|
||||
```
|
||||
|
||||
### ✅ GOOD Error Messages
|
||||
|
||||
```python
|
||||
return {
|
||||
"error": "Customer ID 'CUST_999' not found. Use list_customers to see available IDs."
|
||||
}
|
||||
|
||||
return {
|
||||
"error": "Date format invalid. Expected 'YYYY-MM-DD' (e.g., '2024-01-15'), got: '01/15/2024'"
|
||||
}
|
||||
|
||||
return {
|
||||
"error": "Quantity 5000 exceeds maximum allowed (1000). Split into multiple orders or contact support."
|
||||
}
|
||||
|
||||
return {
|
||||
"error": "API rate limit exceeded. Please wait 30 seconds and try again."
|
||||
}
|
||||
```
|
||||
|
||||
## SQL Error Handling (MXCP Managed)
|
||||
|
||||
### You Don't Handle These (MXCP Does)
|
||||
|
||||
MXCP automatically handles and returns errors for:
|
||||
- Invalid SQL syntax
|
||||
- Missing tables/columns
|
||||
- Type mismatches
|
||||
- Parameter binding errors
|
||||
|
||||
**Your job**: Write correct SQL and let MXCP handle errors.
|
||||
|
||||
### Prevent SQL Errors
|
||||
|
||||
#### 1. Validate Schema
|
||||
|
||||
```yaml
|
||||
# Always define return types to match SQL output
|
||||
tool:
|
||||
name: get_stats
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
total: { type: number } # Matches SQL: SUM(amount)
|
||||
count: { type: integer } # Matches SQL: COUNT(*)
|
||||
source:
|
||||
code: |
|
||||
SELECT
|
||||
SUM(amount) as total,
|
||||
COUNT(*) as count
|
||||
FROM orders
|
||||
```
|
||||
|
||||
#### 2. Handle NULL Values
|
||||
|
||||
```sql
|
||||
-- BAD: Might return NULL which breaks type system
|
||||
SELECT amount FROM orders WHERE id = $order_id
|
||||
|
||||
-- GOOD: Handle potential NULL
|
||||
SELECT COALESCE(amount, 0) as amount
|
||||
FROM orders
|
||||
WHERE id = $order_id
|
||||
|
||||
-- GOOD: Use IFNULL/COALESCE for aggregations
|
||||
SELECT
|
||||
COALESCE(SUM(amount), 0) as total,
|
||||
COALESCE(AVG(amount), 0) as average
|
||||
FROM orders
|
||||
WHERE status = $status
|
||||
```
|
||||
|
||||
#### 3. Handle Empty Results
|
||||
|
||||
```sql
|
||||
-- If no results, return empty array (not NULL)
|
||||
SELECT * FROM customers WHERE city = $city
|
||||
-- Returns: [] if no customers (MXCP handles this)
|
||||
|
||||
-- For aggregations, always return a row
|
||||
SELECT
|
||||
COUNT(*) as count,
|
||||
COALESCE(SUM(amount), 0) as total
|
||||
FROM orders
|
||||
WHERE status = $status
|
||||
-- Always returns one row, even if no matching orders
|
||||
```
|
||||
|
||||
## Error Codes Convention
|
||||
|
||||
**Use consistent error codes across your tools**:
|
||||
|
||||
```python
|
||||
# Standard error codes
|
||||
ERROR_CODES = {
|
||||
# Input validation
|
||||
"INVALID_FORMAT": "Input format is incorrect",
|
||||
"INVALID_RANGE": "Value outside valid range",
|
||||
"MISSING_REQUIRED": "Required parameter missing",
|
||||
|
||||
# Resource errors
|
||||
"NOT_FOUND": "Resource not found",
|
||||
"ALREADY_EXISTS": "Resource already exists",
|
||||
"DELETED": "Resource has been deleted",
|
||||
|
||||
# Permission errors
|
||||
"UNAUTHORIZED": "User not authenticated",
|
||||
"FORBIDDEN": "User lacks permission",
|
||||
|
||||
# External service errors
|
||||
"API_ERROR": "External API error",
|
||||
"TIMEOUT": "Request timed out",
|
||||
"RATE_LIMIT": "Rate limit exceeded",
|
||||
|
||||
# System errors
|
||||
"DATABASE_ERROR": "Database operation failed",
|
||||
"CONFIGURATION_ERROR": "Missing or invalid configuration",
|
||||
"DEPENDENCY_ERROR": "Required library not installed",
|
||||
|
||||
# Business logic errors
|
||||
"INSUFFICIENT_FUNDS": "Not enough balance",
|
||||
"INVALID_STATE": "Operation not allowed in current state",
|
||||
"QUOTA_EXCEEDED": "Usage quota exceeded",
|
||||
|
||||
# Unknown
|
||||
"UNKNOWN_ERROR": "Unexpected error occurred"
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Error Handling
|
||||
|
||||
### Unit Tests for Error Cases
|
||||
|
||||
```python
|
||||
# tests/test_error_handling.py
|
||||
import pytest
|
||||
from python.my_module import fetch_user
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_user_not_found(httpx_mock):
|
||||
"""Test 404 error handling"""
|
||||
httpx_mock.add_response(
|
||||
url="https://api.example.com/users/999",
|
||||
status_code=404
|
||||
)
|
||||
|
||||
result = await fetch_user(999)
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["error_code"] == "NOT_FOUND"
|
||||
assert "999" in result["error"] # Error mentions the ID
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_user_timeout(httpx_mock):
|
||||
"""Test timeout handling"""
|
||||
httpx_mock.add_exception(httpx.TimeoutException("Timeout"))
|
||||
|
||||
result = await fetch_user(123)
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["error_code"] == "TIMEOUT"
|
||||
assert "timeout" in result["error"].lower()
|
||||
|
||||
def test_invalid_input():
|
||||
"""Test input validation"""
|
||||
result = process_order("INVALID", quantity=5)
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["error_code"] == "INVALID_FORMAT"
|
||||
assert "ORD_" in result["error"] # Mentions expected format
|
||||
```
|
||||
|
||||
## Error Handling Checklist
|
||||
|
||||
Before declaring Python tool complete:
|
||||
|
||||
- [ ] All external API calls wrapped in try/except
|
||||
- [ ] All exceptions return structured error objects
|
||||
- [ ] Error messages are clear and actionable
|
||||
- [ ] Error codes are consistent
|
||||
- [ ] Input validation with helpful error messages
|
||||
- [ ] NULL/None values handled gracefully
|
||||
- [ ] Timeout handling for network calls
|
||||
- [ ] Missing dependencies handled (ImportError)
|
||||
- [ ] Database errors caught and explained
|
||||
- [ ] Success/failure clearly indicated in response
|
||||
- [ ] Unit tests for error scenarios
|
||||
- [ ] Error messages help LLM understand what to do next
|
||||
|
||||
## Summary
|
||||
|
||||
**SQL Tools (MXCP Handles)**:
|
||||
- Write correct SQL
|
||||
- Handle NULL values with COALESCE
|
||||
- Match return types to SQL output
|
||||
|
||||
**Python Tools (YOU Handle)**:
|
||||
- ✅ Wrap ALL external calls in try/except
|
||||
- ✅ Return structured error objects (`{"success": False, "error": "...", "error_code": "..."}`)
|
||||
- ✅ Validate inputs with clear error messages
|
||||
- ✅ Be specific and actionable in error messages
|
||||
- ✅ Use consistent error codes
|
||||
- ✅ Test error scenarios
|
||||
- ✅ NEVER let exceptions bubble up to MXCP
|
||||
|
||||
**Golden Rule**: Errors should help the LLM understand what went wrong and what to do next.
|
||||
653
skills/mxcp-expert/references/excel-integration.md
Normal file
653
skills/mxcp-expert/references/excel-integration.md
Normal file
@@ -0,0 +1,653 @@
|
||||
# Excel File Integration
|
||||
|
||||
Guide for working with Excel files (.xlsx, .xls) in MXCP servers.
|
||||
|
||||
## Overview
|
||||
|
||||
Excel files are common data sources that can be integrated into MXCP servers. DuckDB provides multiple ways to read Excel files, and dbt can be used to manage Excel data as seeds or sources.
|
||||
|
||||
## Reading Excel Files in DuckDB
|
||||
|
||||
### Method 1: Direct Reading with spatial Extension
|
||||
|
||||
DuckDB's spatial extension includes `st_read` which can read Excel files:
|
||||
|
||||
```sql
|
||||
-- Install and load spatial extension (includes Excel support)
|
||||
INSTALL spatial;
|
||||
LOAD spatial;
|
||||
|
||||
-- Read Excel file
|
||||
SELECT * FROM st_read('data.xlsx');
|
||||
|
||||
-- Read specific sheet
|
||||
SELECT * FROM st_read('data.xlsx', layer='Sheet2');
|
||||
```
|
||||
|
||||
### Method 2: Using Python with pandas
|
||||
|
||||
For more control, use Python with pandas:
|
||||
|
||||
```python
|
||||
# python/excel_reader.py
|
||||
from mxcp.runtime import db
|
||||
import pandas as pd
|
||||
|
||||
def load_excel_to_duckdb(filepath: str, table_name: str, sheet_name: str = None) -> dict:
|
||||
"""Load Excel file into DuckDB table"""
|
||||
# Read Excel with pandas
|
||||
df = pd.read_excel(filepath, sheet_name=sheet_name)
|
||||
|
||||
# Register DataFrame in DuckDB
|
||||
db.execute(f"CREATE OR REPLACE TABLE {table_name} AS SELECT * FROM df")
|
||||
|
||||
return {
|
||||
"table": table_name,
|
||||
"rows": len(df),
|
||||
"columns": list(df.columns)
|
||||
}
|
||||
|
||||
def read_excel_data(filepath: str, sheet_name: str = None) -> list[dict]:
|
||||
"""Read Excel and return as list of dicts"""
|
||||
df = pd.read_excel(filepath, sheet_name=sheet_name)
|
||||
return df.to_dict('records')
|
||||
```
|
||||
|
||||
### Method 3: Convert to CSV, then use dbt seed
|
||||
|
||||
**Best practice for user-uploaded Excel files**:
|
||||
|
||||
```bash
|
||||
# Convert Excel to CSV using Python
|
||||
python -c "import pandas as pd; pd.read_excel('data.xlsx').to_csv('seeds/data.csv', index=False)"
|
||||
|
||||
# Then follow standard dbt seed workflow
|
||||
cat > seeds/schema.yml <<EOF
|
||||
version: 2
|
||||
seeds:
|
||||
- name: data
|
||||
columns:
|
||||
- name: id
|
||||
tests: [unique, not_null]
|
||||
EOF
|
||||
|
||||
dbt seed
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Excel Upload → Query Tool
|
||||
|
||||
**User request**: "I have an Excel file with sales data, let me query it"
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```yaml
|
||||
# tools/upload_excel.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: upload_excel
|
||||
description: "Load Excel file into queryable table"
|
||||
language: python
|
||||
parameters:
|
||||
- name: filepath
|
||||
type: string
|
||||
description: "Path to Excel file"
|
||||
- name: sheet_name
|
||||
type: string
|
||||
required: false
|
||||
description: "Sheet name (default: first sheet)"
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
table_name: { type: string }
|
||||
rows: { type: integer }
|
||||
columns: { type: array }
|
||||
source:
|
||||
file: ../python/excel_loader.py
|
||||
```
|
||||
|
||||
```python
|
||||
# python/excel_loader.py
|
||||
from mxcp.runtime import db
|
||||
import pandas as pd
|
||||
import os
|
||||
|
||||
def upload_excel(filepath: str, sheet_name: str = None) -> dict:
|
||||
"""Load Excel file into DuckDB for querying"""
|
||||
if not os.path.exists(filepath):
|
||||
raise FileNotFoundError(f"Excel file not found: {filepath}")
|
||||
|
||||
# Read Excel
|
||||
df = pd.read_excel(filepath, sheet_name=sheet_name or 0)
|
||||
|
||||
# Generate table name from filename
|
||||
table_name = os.path.splitext(os.path.basename(filepath))[0].replace('-', '_').replace(' ', '_')
|
||||
|
||||
# Load into DuckDB
|
||||
db.execute(f"CREATE OR REPLACE TABLE {table_name} AS SELECT * FROM df")
|
||||
|
||||
return {
|
||||
"table_name": table_name,
|
||||
"rows": len(df),
|
||||
"columns": list(df.columns),
|
||||
"message": f"Excel loaded. Query with: SELECT * FROM {table_name}"
|
||||
}
|
||||
```
|
||||
|
||||
```yaml
|
||||
# tools/query_excel_data.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: query_excel_data
|
||||
description: "Query data from uploaded Excel file"
|
||||
parameters:
|
||||
- name: table_name
|
||||
type: string
|
||||
description: "Table name (from upload_excel result)"
|
||||
- name: filter_column
|
||||
type: string
|
||||
required: false
|
||||
- name: filter_value
|
||||
type: string
|
||||
required: false
|
||||
return:
|
||||
type: array
|
||||
source:
|
||||
code: |
|
||||
SELECT * FROM {{ table_name }}
|
||||
WHERE $filter_column IS NULL
|
||||
OR CAST({{ filter_column }} AS VARCHAR) = $filter_value
|
||||
LIMIT 1000
|
||||
```
|
||||
|
||||
**Validation workflow for Pattern 1**:
|
||||
```bash
|
||||
# 1. Validate MXCP structure
|
||||
mxcp validate
|
||||
|
||||
# 2. Test upload tool
|
||||
mxcp test tool upload_excel
|
||||
|
||||
# 3. Manual test with real Excel file
|
||||
mxcp run tool upload_excel --param filepath="./test.xlsx"
|
||||
|
||||
# 4. Test query tool
|
||||
mxcp run tool query_excel_data --param table_name="test"
|
||||
|
||||
# 5. All validations must pass before deployment
|
||||
```
|
||||
|
||||
### Pattern 2: Excel → dbt Python Model → Analytics
|
||||
|
||||
**User request**: "Process this Excel file with complex formatting and transform the data"
|
||||
|
||||
**RECOMMENDED for complex Excel processing** - Use dbt Python models when:
|
||||
- Excel has complex formatting or multiple sheets
|
||||
- Need pandas operations (pivoting, melting, complex string manipulation)
|
||||
- Data cleaning requires Python logic
|
||||
|
||||
**Implementation**:
|
||||
|
||||
1. **Create dbt Python model** (`models/process_excel.py`):
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
def model(dbt, session):
|
||||
# Read Excel file
|
||||
df = pd.read_excel('data/sales_data.xlsx', sheet_name='Sales')
|
||||
|
||||
# Clean data
|
||||
df = df.dropna(how='all') # Remove empty rows
|
||||
df = df.dropna(axis=1, how='all') # Remove empty columns
|
||||
|
||||
# Normalize column names
|
||||
df.columns = df.columns.str.lower().str.replace(' ', '_')
|
||||
|
||||
# Complex transformations using pandas
|
||||
df['sale_date'] = pd.to_datetime(df['sale_date'])
|
||||
df['month'] = df['sale_date'].dt.to_period('M').astype(str)
|
||||
|
||||
# Aggregate
|
||||
result = df.groupby(['region', 'month']).agg({
|
||||
'amount': 'sum',
|
||||
'quantity': 'sum'
|
||||
}).reset_index()
|
||||
|
||||
return result # Returns DataFrame that becomes a DuckDB table
|
||||
```
|
||||
|
||||
2. **Create schema** (`models/schema.yml`):
|
||||
```yaml
|
||||
version: 2
|
||||
|
||||
models:
|
||||
- name: process_excel
|
||||
description: "Processed sales data from Excel"
|
||||
config:
|
||||
materialized: table
|
||||
columns:
|
||||
- name: region
|
||||
tests: [not_null]
|
||||
- name: month
|
||||
tests: [not_null]
|
||||
- name: amount
|
||||
tests: [not_null]
|
||||
```
|
||||
|
||||
3. **Run the Python model**:
|
||||
```bash
|
||||
dbt run --select process_excel
|
||||
dbt test --select process_excel
|
||||
```
|
||||
|
||||
4. **Create MXCP tool**:
|
||||
```yaml
|
||||
# tools/sales_analytics.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: sales_analytics
|
||||
description: "Get processed sales data from Excel"
|
||||
parameters:
|
||||
- name: region
|
||||
type: string
|
||||
default: null
|
||||
return:
|
||||
type: array
|
||||
source:
|
||||
code: |
|
||||
SELECT * FROM process_excel
|
||||
WHERE $region IS NULL OR region = $region
|
||||
ORDER BY month DESC
|
||||
```
|
||||
|
||||
5. **Validate**:
|
||||
```bash
|
||||
mxcp validate
|
||||
mxcp test tool sales_analytics
|
||||
```
|
||||
|
||||
### Pattern 3: Excel → dbt seed → Analytics
|
||||
|
||||
**User request**: "Analyze this Excel file with aggregations"
|
||||
|
||||
**Use this approach for simpler Excel files** - Convert to CSV first when:
|
||||
- Excel file is simple with standard formatting
|
||||
- Want version control for the data (CSV in git)
|
||||
- Data is static and doesn't change
|
||||
|
||||
**Implementation**:
|
||||
|
||||
1. **Convert Excel to CSV seed**:
|
||||
```bash
|
||||
# One-time conversion
|
||||
python -c "
|
||||
import pandas as pd
|
||||
df = pd.read_excel('sales_data.xlsx')
|
||||
df.to_csv('seeds/sales_data.csv', index=False)
|
||||
print(f'Converted {len(df)} rows')
|
||||
"
|
||||
```
|
||||
|
||||
2. **Create seed schema**:
|
||||
```yaml
|
||||
# seeds/schema.yml
|
||||
version: 2
|
||||
|
||||
seeds:
|
||||
- name: sales_data
|
||||
description: "Sales data from Excel upload"
|
||||
columns:
|
||||
- name: sale_id
|
||||
tests: [unique, not_null]
|
||||
- name: sale_date
|
||||
data_type: date
|
||||
tests: [not_null]
|
||||
- name: amount
|
||||
data_type: decimal
|
||||
tests: [not_null]
|
||||
- name: region
|
||||
tests: [not_null]
|
||||
- name: product
|
||||
tests: [not_null]
|
||||
```
|
||||
|
||||
3. **Load seed and validate**:
|
||||
```bash
|
||||
# Load CSV into DuckDB
|
||||
dbt seed --select sales_data
|
||||
|
||||
# Run data quality tests
|
||||
dbt test --select sales_data
|
||||
|
||||
# Verify data loaded correctly
|
||||
dbt run-operation show_table --args '{"table_name": "sales_data"}'
|
||||
```
|
||||
|
||||
**CRITICAL**: Always run `dbt test` after loading seeds to ensure data quality.
|
||||
|
||||
4. **Create analytics model**:
|
||||
```sql
|
||||
-- models/sales_analytics.sql
|
||||
{{ config(materialized='table') }}
|
||||
|
||||
SELECT
|
||||
region,
|
||||
product,
|
||||
DATE_TRUNC('month', sale_date) as month,
|
||||
COUNT(*) as transaction_count,
|
||||
SUM(amount) as total_sales,
|
||||
AVG(amount) as avg_sale,
|
||||
MIN(amount) as min_sale,
|
||||
MAX(amount) as max_sale
|
||||
FROM {{ ref('sales_data') }}
|
||||
GROUP BY region, product, month
|
||||
```
|
||||
|
||||
5. **Create query tool**:
|
||||
```yaml
|
||||
# tools/sales_analytics.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: sales_analytics
|
||||
description: "Get sales analytics by region and product"
|
||||
parameters:
|
||||
- name: region
|
||||
type: string
|
||||
required: false
|
||||
- name: product
|
||||
type: string
|
||||
required: false
|
||||
return:
|
||||
type: array
|
||||
source:
|
||||
code: |
|
||||
SELECT * FROM sales_analytics
|
||||
WHERE ($region IS NULL OR region = $region)
|
||||
AND ($product IS NULL OR product = $product)
|
||||
ORDER BY month DESC, total_sales DESC
|
||||
```
|
||||
|
||||
6. **Validate and test MXCP tool**:
|
||||
```bash
|
||||
# Validate MXCP structure
|
||||
mxcp validate
|
||||
|
||||
# Test tool execution
|
||||
mxcp test tool sales_analytics
|
||||
|
||||
# Manual verification
|
||||
mxcp run tool sales_analytics --param region="North"
|
||||
|
||||
# All checks must pass before deployment
|
||||
```
|
||||
|
||||
### Pattern 4: Multi-Sheet Excel Processing
|
||||
|
||||
**User request**: "My Excel has multiple sheets, process them all"
|
||||
|
||||
```python
|
||||
# python/multi_sheet_loader.py
|
||||
from mxcp.runtime import db
|
||||
import pandas as pd
|
||||
|
||||
def load_all_sheets(filepath: str) -> dict:
|
||||
"""Load all sheets from Excel file as separate tables"""
|
||||
# Read all sheets
|
||||
excel_file = pd.ExcelFile(filepath)
|
||||
|
||||
results = {}
|
||||
for sheet_name in excel_file.sheet_names:
|
||||
df = pd.read_excel(filepath, sheet_name=sheet_name)
|
||||
|
||||
# Clean table name
|
||||
table_name = sheet_name.lower().replace(' ', '_').replace('-', '_')
|
||||
|
||||
# Load to DuckDB
|
||||
db.execute(f"CREATE OR REPLACE TABLE {table_name} AS SELECT * FROM df")
|
||||
|
||||
results[sheet_name] = {
|
||||
"table_name": table_name,
|
||||
"rows": len(df),
|
||||
"columns": list(df.columns)
|
||||
}
|
||||
|
||||
return {
|
||||
"sheets_loaded": len(results),
|
||||
"sheets": results
|
||||
}
|
||||
```
|
||||
|
||||
## Excel-Specific Considerations
|
||||
|
||||
### Data Type Inference
|
||||
|
||||
Excel doesn't have strict types. Handle type ambiguity:
|
||||
|
||||
```python
|
||||
def clean_excel_types(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Clean common Excel type issues"""
|
||||
for col in df.columns:
|
||||
# Convert Excel dates properly
|
||||
if df[col].dtype == 'object':
|
||||
try:
|
||||
df[col] = pd.to_datetime(df[col])
|
||||
except:
|
||||
pass
|
||||
|
||||
# Strip whitespace from strings
|
||||
if df[col].dtype == 'object':
|
||||
df[col] = df[col].str.strip()
|
||||
|
||||
return df
|
||||
```
|
||||
|
||||
### Handling Headers
|
||||
|
||||
Excel files may have inconsistent headers:
|
||||
|
||||
```python
|
||||
def normalize_headers(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Normalize Excel column names"""
|
||||
df.columns = (
|
||||
df.columns
|
||||
.str.lower()
|
||||
.str.replace(' ', '_')
|
||||
.str.replace('-', '_')
|
||||
.str.replace('[^a-z0-9_]', '', regex=True)
|
||||
)
|
||||
return df
|
||||
```
|
||||
|
||||
### Empty Rows/Columns
|
||||
|
||||
Excel often has empty rows:
|
||||
|
||||
```python
|
||||
def clean_excel_data(filepath: str, sheet_name: str = None) -> pd.DataFrame:
|
||||
"""Read and clean Excel data"""
|
||||
df = pd.read_excel(filepath, sheet_name=sheet_name)
|
||||
|
||||
# Remove completely empty rows
|
||||
df = df.dropna(how='all')
|
||||
|
||||
# Remove completely empty columns
|
||||
df = df.dropna(axis=1, how='all')
|
||||
|
||||
# Normalize headers
|
||||
df = normalize_headers(df)
|
||||
|
||||
# Clean types
|
||||
df = clean_excel_types(df)
|
||||
|
||||
return df
|
||||
```
|
||||
|
||||
## Complete Example: Excel Analytics Server
|
||||
|
||||
**Scenario**: User uploads Excel file, wants to query and get statistics
|
||||
|
||||
```bash
|
||||
# Project structure
|
||||
excel-analytics/
|
||||
├── mxcp-site.yml
|
||||
├── python/
|
||||
│ ├── excel_loader.py
|
||||
│ └── excel_analytics.py
|
||||
├── tools/
|
||||
│ ├── load_excel.yml
|
||||
│ ├── query_data.yml
|
||||
│ └── get_statistics.yml
|
||||
└── seeds/
|
||||
└── schema.yml (if using dbt seed approach)
|
||||
```
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```python
|
||||
# python/excel_loader.py
|
||||
from mxcp.runtime import db
|
||||
import pandas as pd
|
||||
import os
|
||||
|
||||
def normalize_headers(df: pd.DataFrame) -> pd.DataFrame:
|
||||
df.columns = df.columns.str.lower().str.replace(' ', '_').str.replace('[^a-z0-9_]', '', regex=True)
|
||||
return df
|
||||
|
||||
def load_excel(filepath: str, sheet_name: str = None) -> dict:
|
||||
"""Load Excel file with cleaning"""
|
||||
if not os.path.exists(filepath):
|
||||
raise FileNotFoundError(f"File not found: {filepath}")
|
||||
|
||||
# Read and clean
|
||||
df = pd.read_excel(filepath, sheet_name=sheet_name or 0)
|
||||
df = df.dropna(how='all').dropna(axis=1, how='all')
|
||||
df = normalize_headers(df)
|
||||
|
||||
# Table name from filename
|
||||
table_name = os.path.splitext(os.path.basename(filepath))[0]
|
||||
table_name = table_name.lower().replace('-', '_').replace(' ', '_')
|
||||
|
||||
# Load to DuckDB
|
||||
db.execute(f"CREATE OR REPLACE TABLE {table_name} AS SELECT * FROM df")
|
||||
|
||||
# Get column info
|
||||
col_info = db.execute(f"DESCRIBE {table_name}").fetchall()
|
||||
|
||||
return {
|
||||
"table_name": table_name,
|
||||
"rows": len(df),
|
||||
"columns": [{"name": c["column_name"], "type": c["column_type"]} for c in col_info]
|
||||
}
|
||||
|
||||
def get_statistics(table_name: str, numeric_columns: list[str] = None) -> dict:
|
||||
"""Calculate statistics for numeric columns"""
|
||||
# Get numeric columns if not specified
|
||||
if not numeric_columns:
|
||||
schema = db.execute(f"DESCRIBE {table_name}").fetchall()
|
||||
numeric_columns = [
|
||||
c["column_name"] for c in schema
|
||||
if c["column_type"] in ('INTEGER', 'BIGINT', 'DOUBLE', 'DECIMAL', 'FLOAT')
|
||||
]
|
||||
|
||||
if not numeric_columns:
|
||||
return {"error": "No numeric columns found"}
|
||||
|
||||
# Build statistics query
|
||||
stats_parts = []
|
||||
for col in numeric_columns:
|
||||
stats_parts.append(f"""
|
||||
'{col}' as column,
|
||||
COUNT({col}) as count,
|
||||
AVG({col}) as mean,
|
||||
STDDEV({col}) as std_dev,
|
||||
MIN({col}) as min,
|
||||
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY {col}) as q25,
|
||||
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY {col}) as median,
|
||||
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY {col}) as q75,
|
||||
MAX({col}) as max
|
||||
""")
|
||||
|
||||
query = f"""
|
||||
SELECT * FROM (
|
||||
{' UNION ALL '.join(f'SELECT {part} FROM {table_name}' for part in stats_parts)}
|
||||
)
|
||||
"""
|
||||
|
||||
results = db.execute(query).fetchall()
|
||||
return {"statistics": results}
|
||||
```
|
||||
|
||||
```yaml
|
||||
# tools/load_excel.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: load_excel
|
||||
description: "Load Excel file for querying and analysis"
|
||||
language: python
|
||||
parameters:
|
||||
- name: filepath
|
||||
type: string
|
||||
description: "Path to Excel file"
|
||||
- name: sheet_name
|
||||
type: string
|
||||
required: false
|
||||
return:
|
||||
type: object
|
||||
source:
|
||||
file: ../python/excel_loader.py
|
||||
tests:
|
||||
- name: "load_test_file"
|
||||
arguments:
|
||||
- key: filepath
|
||||
value: "test_data.xlsx"
|
||||
result:
|
||||
rows: 100
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
Add to `requirements.txt`:
|
||||
```
|
||||
openpyxl>=3.0.0 # For .xlsx files
|
||||
xlrd>=2.0.0 # For .xls files (optional)
|
||||
pandas>=2.0.0 # For Excel processing
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always clean Excel data**: Remove empty rows/columns, normalize headers
|
||||
2. **Type validation**: Excel types are unreliable, validate and cast
|
||||
3. **Use dbt seeds for static data**: Convert Excel → CSV → seed for version control
|
||||
4. **Use Python for dynamic uploads**: For user-uploaded files during runtime
|
||||
5. **Document expected format**: Tell users what Excel structure is expected
|
||||
6. **Error handling**: Excel files can be malformed, handle errors gracefully
|
||||
7. **Sheet validation**: Check sheet names exist before processing
|
||||
8. **Memory considerations**: Large Excel files can be slow, consider pagination
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue**: "No module named 'openpyxl'"
|
||||
**Solution**: `pip install openpyxl`
|
||||
|
||||
**Issue**: "Excel file empty after loading"
|
||||
**Solution**: Check for empty rows/columns, use `dropna()`
|
||||
|
||||
**Issue**: "Column names have special characters"
|
||||
**Solution**: Use `normalize_headers()` function
|
||||
|
||||
**Issue**: "Date columns appear as numbers"
|
||||
**Solution**: Use `pd.to_datetime()` to convert Excel serial dates
|
||||
|
||||
**Issue**: "Out of memory with large Excel files"
|
||||
**Solution**: Convert to CSV first, use dbt seed, or process in chunks
|
||||
|
||||
## Summary
|
||||
|
||||
For Excel integration in MXCP:
|
||||
|
||||
1. **User uploads** → Python tool with pandas → DuckDB table → Query tools
|
||||
2. **Static data** → Convert to CSV → dbt seed → Schema validation → Query tools
|
||||
3. **Multi-sheet** → Load all sheets as separate tables
|
||||
4. **Always validate** → Clean headers, types, empty rows
|
||||
5. **Add statistics tools** → Provide insights on numeric columns
|
||||
691
skills/mxcp-expert/references/llm-friendly-documentation.md
Normal file
691
skills/mxcp-expert/references/llm-friendly-documentation.md
Normal file
@@ -0,0 +1,691 @@
|
||||
# LLM-Friendly Documentation Guide
|
||||
|
||||
**CRITICAL: Tools must be self-documenting for LLMs without any prior context.**
|
||||
|
||||
## Core Principle
|
||||
|
||||
**LLMs connecting to MXCP servers have ZERO context about your domain, data, or tools.**
|
||||
|
||||
They only see:
|
||||
- Tool name
|
||||
- Tool description
|
||||
- Parameter names and types
|
||||
- Parameter descriptions
|
||||
- Return type structure
|
||||
|
||||
**The documentation YOU provide is the ONLY information they have.**
|
||||
|
||||
## Tool Description Requirements
|
||||
|
||||
### ❌ BAD Tool Description
|
||||
|
||||
```yaml
|
||||
tool:
|
||||
name: get_data
|
||||
description: "Gets data" # ❌ Useless - what data? how? when to use?
|
||||
parameters:
|
||||
- name: id
|
||||
type: string # ❌ No description - what kind of ID?
|
||||
return:
|
||||
type: array # ❌ Array of what?
|
||||
```
|
||||
|
||||
**Why bad**: LLM has no idea when to use this, what ID means, what data is returned.
|
||||
|
||||
### ✅ GOOD Tool Description
|
||||
|
||||
```yaml
|
||||
tool:
|
||||
name: get_customer_orders
|
||||
description: "Retrieve all orders for a specific customer by customer ID. Returns order history including order date, total amount, status, and items. Use this to answer questions about a customer's purchase history or order status."
|
||||
parameters:
|
||||
- name: customer_id
|
||||
type: string
|
||||
description: "Unique customer identifier (e.g., 'CUST_12345'). Found in customer records or from list_customers tool."
|
||||
required: true
|
||||
examples: ["CUST_12345", "CUST_98765"]
|
||||
- name: status
|
||||
type: string
|
||||
description: "Optional filter by order status. Valid values: 'pending', 'shipped', 'delivered', 'cancelled'. Omit to get all orders."
|
||||
required: false
|
||||
examples: ["pending", "shipped"]
|
||||
return:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
order_id: { type: string, description: "Unique order identifier" }
|
||||
order_date: { type: string, description: "ISO 8601 date when order was placed" }
|
||||
total_amount: { type: number, description: "Total order value in USD" }
|
||||
status: { type: string, description: "Current order status" }
|
||||
items: { type: array, description: "List of items in the order" }
|
||||
```
|
||||
|
||||
**Why good**:
|
||||
- LLM knows WHEN to use it (customer purchase history, order status)
|
||||
- LLM knows WHAT parameters mean and valid values
|
||||
- LLM knows WHAT will be returned
|
||||
- LLM can chain with other tools (mentions list_customers)
|
||||
|
||||
## Description Template
|
||||
|
||||
### Tool-Level Description
|
||||
|
||||
**Format**: `<What it does> <What it returns> <When to use it>`
|
||||
|
||||
```yaml
|
||||
description: "Retrieve sales analytics by region and time period. Returns aggregated metrics including total sales, transaction count, and average order value. Use this to answer questions about sales performance, regional comparisons, or time-based trends."
|
||||
```
|
||||
|
||||
**Must include**:
|
||||
1. **What**: What data/operation
|
||||
2. **Returns**: Summary of return data
|
||||
3. **When**: Use cases / when LLM should call this
|
||||
|
||||
### Parameter Description
|
||||
|
||||
**Format**: `<What it is> <Valid values/format> <Optional context>`
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
- name: region
|
||||
type: string
|
||||
description: "Geographic region code. Valid values: 'north', 'south', 'east', 'west'. Use 'all' for aggregated data across all regions."
|
||||
examples: ["north", "south", "all"]
|
||||
|
||||
- name: start_date
|
||||
type: string
|
||||
format: date
|
||||
description: "Start date for analytics period in YYYY-MM-DD format. Defaults to 30 days ago if omitted."
|
||||
required: false
|
||||
examples: ["2024-01-01", "2024-06-15"]
|
||||
|
||||
- name: limit
|
||||
type: integer
|
||||
description: "Maximum number of results to return. Defaults to 100. Set to -1 for all results (use cautiously for large datasets)."
|
||||
default: 100
|
||||
examples: [10, 50, 100]
|
||||
```
|
||||
|
||||
**Must include**:
|
||||
1. **What it is**: Clear explanation
|
||||
2. **Valid values**: Enums, formats, ranges
|
||||
3. **Defaults**: If parameter is optional
|
||||
4. **Examples**: Concrete examples
|
||||
|
||||
### Return Type Description
|
||||
|
||||
**Include descriptions for ALL fields**:
|
||||
|
||||
```yaml
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
total_sales:
|
||||
type: number
|
||||
description: "Sum of all sales in USD for the period"
|
||||
transaction_count:
|
||||
type: integer
|
||||
description: "Number of individual transactions"
|
||||
avg_order_value:
|
||||
type: number
|
||||
description: "Average transaction amount (total_sales / transaction_count)"
|
||||
top_products:
|
||||
type: array
|
||||
description: "Top 5 products by revenue"
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
product_id: { type: string, description: "Product identifier" }
|
||||
product_name: { type: string, description: "Human-readable product name" }
|
||||
revenue: { type: number, description: "Total revenue for this product in USD" }
|
||||
```
|
||||
|
||||
## Combining Tools - Cross-References
|
||||
|
||||
**Help LLMs chain tools together** by mentioning related tools:
|
||||
|
||||
```yaml
|
||||
tool:
|
||||
name: get_customer_details
|
||||
description: "Get detailed information for a specific customer. Use customer_id from list_customers tool or search_customers tool. Returns personal info, account status, and lifetime value."
|
||||
# ... parameters ...
|
||||
```
|
||||
|
||||
```yaml
|
||||
tool:
|
||||
name: list_customers
|
||||
description: "List all customers with optional filtering. Returns customer_id needed for get_customer_details and get_customer_orders tools."
|
||||
# ... parameters ...
|
||||
```
|
||||
|
||||
**LLM workflow enabled**:
|
||||
1. LLM sees: "I need customer details"
|
||||
2. Reads: "Use customer_id from list_customers tool"
|
||||
3. Calls: `list_customers` first
|
||||
4. Gets: `customer_id`
|
||||
5. Calls: `get_customer_details` with that ID
|
||||
|
||||
## Examples in Descriptions
|
||||
|
||||
**ALWAYS provide concrete examples**:
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
- name: date_range
|
||||
type: string
|
||||
description: "Date range in format 'YYYY-MM-DD to YYYY-MM-DD' or use shortcuts: 'today', 'yesterday', 'last_7_days', 'last_30_days', 'last_month', 'this_year'"
|
||||
examples:
|
||||
- "2024-01-01 to 2024-12-31"
|
||||
- "last_7_days"
|
||||
- "this_year"
|
||||
```
|
||||
|
||||
## Error Cases in Descriptions
|
||||
|
||||
**Document expected errors**:
|
||||
|
||||
```yaml
|
||||
tool:
|
||||
name: get_order
|
||||
description: "Retrieve order by order ID. Returns order details if found. Returns error if order_id doesn't exist or user doesn't have permission to view this order."
|
||||
parameters:
|
||||
- name: order_id
|
||||
type: string
|
||||
description: "Order identifier. Format: ORD_XXXXXX (e.g., 'ORD_123456'). Returns error if order not found."
|
||||
```
|
||||
|
||||
## Resource URIs
|
||||
|
||||
**Make URI templates clear**:
|
||||
|
||||
```yaml
|
||||
resource:
|
||||
uri: "customer://profile/{customer_id}"
|
||||
description: "Access customer profile data. Replace {customer_id} with actual customer ID (e.g., 'CUST_12345'). Returns 404 if customer doesn't exist."
|
||||
parameters:
|
||||
- name: customer_id
|
||||
type: string
|
||||
description: "Customer identifier from list_customers or search_customers"
|
||||
```
|
||||
|
||||
## Prompt Templates
|
||||
|
||||
**Explain template variables clearly**:
|
||||
|
||||
```yaml
|
||||
prompt:
|
||||
name: analyze_customer
|
||||
description: "Generate customer analysis report. Provide customer_id to analyze spending patterns, order frequency, and recommendations."
|
||||
parameters:
|
||||
- name: customer_id
|
||||
type: string
|
||||
description: "Customer to analyze (from list_customers)"
|
||||
- name: analysis_type
|
||||
type: string
|
||||
description: "Type of analysis: 'spending' (purchase patterns), 'behavior' (order frequency), 'recommendations' (product suggestions)"
|
||||
examples: ["spending", "behavior", "recommendations"]
|
||||
messages:
|
||||
- role: system
|
||||
type: text
|
||||
prompt: "You are a customer analytics expert. Analyze data thoroughly and provide actionable insights."
|
||||
- role: user
|
||||
type: text
|
||||
prompt: "Analyze customer {{ customer_id }} focusing on {{ analysis_type }}. Include specific metrics and recommendations."
|
||||
```
|
||||
|
||||
## Complete Example: Well-Documented Tool Set
|
||||
|
||||
```yaml
|
||||
# tools/list_products.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: list_products
|
||||
description: "List all available products with optional category filtering. Returns product catalog with IDs, names, prices, and stock levels. Use this to browse products or find product_id for get_product_details tool."
|
||||
parameters:
|
||||
- name: category
|
||||
type: string
|
||||
description: "Filter by product category. Valid values: 'electronics', 'clothing', 'food', 'books', 'home'. Omit to see all categories."
|
||||
required: false
|
||||
examples: ["electronics", "clothing"]
|
||||
- name: in_stock_only
|
||||
type: boolean
|
||||
description: "If true, only return products currently in stock. Default: false (shows all products)."
|
||||
default: false
|
||||
return:
|
||||
type: array
|
||||
description: "Array of product objects sorted by name"
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
product_id:
|
||||
type: string
|
||||
description: "Unique product identifier (use with get_product_details)"
|
||||
name:
|
||||
type: string
|
||||
description: "Product name"
|
||||
category:
|
||||
type: string
|
||||
description: "Product category"
|
||||
price:
|
||||
type: number
|
||||
description: "Current price in USD"
|
||||
stock:
|
||||
type: integer
|
||||
description: "Current stock level (0 = out of stock)"
|
||||
source:
|
||||
code: |
|
||||
SELECT
|
||||
product_id,
|
||||
name,
|
||||
category,
|
||||
price,
|
||||
stock
|
||||
FROM products
|
||||
WHERE ($category IS NULL OR category = $category)
|
||||
AND ($in_stock_only = false OR stock > 0)
|
||||
ORDER BY name
|
||||
```
|
||||
|
||||
```yaml
|
||||
# tools/get_product_details.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: get_product_details
|
||||
description: "Get detailed information for a specific product including full description, specifications, reviews, and related products. Use product_id from list_products tool."
|
||||
parameters:
|
||||
- name: product_id
|
||||
type: string
|
||||
description: "Product identifier from list_products (e.g., 'PROD_12345')"
|
||||
required: true
|
||||
examples: ["PROD_12345"]
|
||||
return:
|
||||
type: object
|
||||
description: "Complete product information"
|
||||
properties:
|
||||
product_id: { type: string, description: "Product identifier" }
|
||||
name: { type: string, description: "Product name" }
|
||||
description: { type: string, description: "Detailed product description" }
|
||||
price: { type: number, description: "Current price in USD" }
|
||||
stock: { type: integer, description: "Available quantity" }
|
||||
specifications: { type: object, description: "Product specs (varies by category)" }
|
||||
avg_rating: { type: number, description: "Average customer rating (0-5)" }
|
||||
review_count: { type: integer, description: "Number of customer reviews" }
|
||||
related_products: { type: array, description: "Product IDs of related items" }
|
||||
source:
|
||||
code: |
|
||||
SELECT * FROM product_details WHERE product_id = $product_id
|
||||
```
|
||||
|
||||
## Documentation Quality Checklist
|
||||
|
||||
Before declaring a tool complete, verify:
|
||||
|
||||
### Tool Level:
|
||||
- [ ] Description explains WHAT it does
|
||||
- [ ] Description explains WHAT it returns
|
||||
- [ ] Description explains WHEN to use it
|
||||
- [ ] Cross-references to related tools (if applicable)
|
||||
- [ ] Use cases are clear
|
||||
|
||||
### Parameter Level:
|
||||
- [ ] Every parameter has a description
|
||||
- [ ] Valid values/formats are documented
|
||||
- [ ] Examples provided for complex parameters
|
||||
- [ ] Required vs optional is clear
|
||||
- [ ] Defaults documented (if optional)
|
||||
|
||||
### Return Type Level:
|
||||
- [ ] Return type structure is documented
|
||||
- [ ] Every field has a description
|
||||
- [ ] Complex nested objects are explained
|
||||
- [ ] Array item types are described
|
||||
|
||||
### Overall:
|
||||
- [ ] An LLM reading this can use the tool WITHOUT human explanation
|
||||
- [ ] An LLM knows WHEN to call this vs other tools
|
||||
- [ ] An LLM knows HOW to get required parameters
|
||||
- [ ] An LLM knows WHAT to expect in the response
|
||||
|
||||
## Common Documentation Mistakes
|
||||
|
||||
### ❌ MISTAKE 1: Vague Descriptions
|
||||
```yaml
|
||||
description: "Gets user info" # ❌ Which user? What info? When?
|
||||
```
|
||||
✅ **FIX**:
|
||||
```yaml
|
||||
description: "Retrieve complete user profile including contact information, account status, and preferences for a specific user. Use user_id from list_users or search_users tools."
|
||||
```
|
||||
|
||||
### ❌ MISTAKE 2: Missing Parameter Details
|
||||
```yaml
|
||||
parameters:
|
||||
- name: status
|
||||
type: string # ❌ What are valid values?
|
||||
```
|
||||
✅ **FIX**:
|
||||
```yaml
|
||||
parameters:
|
||||
- name: status
|
||||
type: string
|
||||
description: "Order status filter. Valid values: 'pending', 'processing', 'shipped', 'delivered', 'cancelled'"
|
||||
examples: ["pending", "shipped"]
|
||||
```
|
||||
|
||||
### ❌ MISTAKE 3: Undocumented Return Fields
|
||||
```yaml
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
total: { type: number } # ❌ Total what? In what units?
|
||||
```
|
||||
✅ **FIX**:
|
||||
```yaml
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
total: { type: number, description: "Total order amount in USD including tax and shipping" }
|
||||
```
|
||||
|
||||
### ❌ MISTAKE 4: No Cross-References
|
||||
```yaml
|
||||
tool:
|
||||
name: get_order_details
|
||||
parameters:
|
||||
- name: order_id
|
||||
type: string # ❌ Where does LLM get this?
|
||||
```
|
||||
✅ **FIX**:
|
||||
```yaml
|
||||
tool:
|
||||
name: get_order_details
|
||||
description: "Get detailed order information. Use order_id from list_orders or search_orders tools."
|
||||
parameters:
|
||||
- name: order_id
|
||||
type: string
|
||||
description: "Order identifier (format: ORD_XXXXXX) from list_orders or search_orders"
|
||||
```
|
||||
|
||||
### ❌ MISTAKE 5: Technical Jargon Without Explanation
|
||||
```yaml
|
||||
description: "Executes SOQL query on SF objects" # ❌ LLM doesn't know SOQL or SF
|
||||
```
|
||||
✅ **FIX**:
|
||||
```yaml
|
||||
description: "Query Salesforce data using filters. Searches across accounts, contacts, and opportunities. Returns matching records with standard fields."
|
||||
```
|
||||
|
||||
## Testing Documentation Quality
|
||||
|
||||
**Ask yourself**: "If I gave this to an LLM with ZERO context about my domain, could it use this tool correctly?"
|
||||
|
||||
**Test by asking**:
|
||||
1. When should this tool be called?
|
||||
2. What parameters are needed and where do I get them?
|
||||
3. What will I get back?
|
||||
4. How does this relate to other tools?
|
||||
|
||||
If you can't answer clearly from the YAML alone, **the documentation is insufficient.**
|
||||
|
||||
## Response Format Best Practices
|
||||
|
||||
**Design tool outputs to optimize LLM context usage.**
|
||||
|
||||
### Provide Detail Level Options
|
||||
|
||||
Allow LLMs to request different levels of detail based on their needs.
|
||||
|
||||
```yaml
|
||||
tool:
|
||||
name: search_products
|
||||
parameters:
|
||||
- name: query
|
||||
type: string
|
||||
description: "Product search query"
|
||||
- name: detail_level
|
||||
type: string
|
||||
description: "Level of detail in response"
|
||||
enum: ["minimal", "standard", "full"]
|
||||
default: "standard"
|
||||
examples:
|
||||
- "minimal: Only ID, name, price (fastest, least context)"
|
||||
- "standard: Basic info + category + stock"
|
||||
- "full: All fields including descriptions and specifications"
|
||||
```
|
||||
|
||||
**Implementation in SQL**:
|
||||
```sql
|
||||
SELECT
|
||||
CASE $detail_level
|
||||
WHEN 'minimal' THEN json_object('id', id, 'name', name, 'price', price)
|
||||
WHEN 'standard' THEN json_object('id', id, 'name', name, 'price', price, 'category', category, 'in_stock', stock > 0)
|
||||
ELSE json_object('id', id, 'name', name, 'price', price, 'category', category, 'stock', stock, 'description', description, 'specs', specs)
|
||||
END as product
|
||||
FROM products
|
||||
WHERE name LIKE '%' || $query || '%'
|
||||
```
|
||||
|
||||
### Use Human-Readable Formats
|
||||
|
||||
**Return data in formats LLMs can easily understand and communicate to users.**
|
||||
|
||||
#### ✅ Good: Human-Readable
|
||||
```yaml
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
customer_id: { type: string, description: "Customer ID (CUST_12345)" }
|
||||
customer_name: { type: string, description: "Display name" }
|
||||
last_order_date: { type: string, description: "Date in YYYY-MM-DD format" }
|
||||
total_spent: { type: number, description: "Total amount in USD" }
|
||||
status: { type: string, description: "Account status: active, inactive, suspended" }
|
||||
```
|
||||
|
||||
**SQL implementation**:
|
||||
```sql
|
||||
SELECT
|
||||
customer_id,
|
||||
name as customer_name,
|
||||
DATE_FORMAT(last_order_date, '%Y-%m-%d') as last_order_date, -- Not epoch timestamp
|
||||
ROUND(total_spent, 2) as total_spent,
|
||||
status
|
||||
FROM customers
|
||||
```
|
||||
|
||||
#### ❌ Bad: Opaque/Technical
|
||||
```yaml
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
cust_id: { type: integer } # Unclear name
|
||||
ts: { type: integer } # Epoch timestamp - not human readable
|
||||
amt: { type: number } # Unclear abbreviation
|
||||
stat_cd: { type: integer } # Status code instead of name
|
||||
```
|
||||
|
||||
### Include Display Names with IDs
|
||||
|
||||
When returning IDs, also return human-readable names.
|
||||
|
||||
```yaml
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
assigned_to_user_id: { type: string, description: "User ID" }
|
||||
assigned_to_name: { type: string, description: "User display name" }
|
||||
category_id: { type: string, description: "Category ID" }
|
||||
category_name: { type: string, description: "Category name" }
|
||||
```
|
||||
|
||||
**Why**: LLM can understand relationships without additional tool calls.
|
||||
|
||||
### Limit Response Size
|
||||
|
||||
**Prevent overwhelming LLMs with too much data.**
|
||||
|
||||
```yaml
|
||||
tool:
|
||||
name: list_transactions
|
||||
parameters:
|
||||
- name: limit
|
||||
type: integer
|
||||
description: "Maximum number of transactions to return (1-1000)"
|
||||
default: 100
|
||||
minimum: 1
|
||||
maximum: 1000
|
||||
```
|
||||
|
||||
**Python implementation with truncation**:
|
||||
```python
|
||||
def list_transactions(limit: int = 100) -> dict:
|
||||
"""List recent transactions with size limits"""
|
||||
|
||||
if limit > 1000:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Limit of {limit} exceeds maximum (1000). Use date filters to narrow results.",
|
||||
"error_code": "LIMIT_EXCEEDED",
|
||||
"suggestion": "Try adding 'start_date' and 'end_date' parameters"
|
||||
}
|
||||
|
||||
results = db.execute(
|
||||
"SELECT * FROM transactions ORDER BY date DESC LIMIT $limit",
|
||||
{"limit": limit}
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"count": len(results),
|
||||
"limit": limit,
|
||||
"has_more": len(results) == limit,
|
||||
"transactions": results,
|
||||
"note": "Use pagination or filters if more results needed"
|
||||
}
|
||||
```
|
||||
|
||||
### Provide Pagination Metadata
|
||||
|
||||
**Help LLMs understand when more data is available.**
|
||||
|
||||
```yaml
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
items: { type: array, description: "Results for this page" }
|
||||
total_count: { type: integer, description: "Total matching results" }
|
||||
returned_count: { type: integer, description: "Number returned in this response" }
|
||||
has_more: { type: boolean, description: "Whether more results are available" }
|
||||
next_offset: { type: integer, description: "Offset for next page" }
|
||||
```
|
||||
|
||||
**SQL implementation**:
|
||||
```sql
|
||||
-- Get total count
|
||||
WITH total AS (
|
||||
SELECT COUNT(*) as count FROM products WHERE category = $category
|
||||
)
|
||||
SELECT
|
||||
json_object(
|
||||
'items', (SELECT json_group_array(json_object('id', id, 'name', name))
|
||||
FROM products WHERE category = $category LIMIT $limit OFFSET $offset),
|
||||
'total_count', (SELECT count FROM total),
|
||||
'returned_count', MIN($limit, (SELECT count FROM total) - $offset),
|
||||
'has_more', (SELECT count FROM total) > ($offset + $limit),
|
||||
'next_offset', $offset + $limit
|
||||
) as result
|
||||
```
|
||||
|
||||
### Format for Readability
|
||||
|
||||
**Use clear field names and consistent structures.**
|
||||
|
||||
#### ✅ Good: Clear Structure
|
||||
```yaml
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
summary:
|
||||
type: object
|
||||
description: "High-level summary"
|
||||
properties:
|
||||
total_orders: { type: integer }
|
||||
total_revenue: { type: number }
|
||||
average_order_value: { type: number }
|
||||
top_products:
|
||||
type: array
|
||||
description: "Top 5 selling products"
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
product_name: { type: string }
|
||||
units_sold: { type: integer }
|
||||
revenue: { type: number }
|
||||
```
|
||||
|
||||
#### ❌ Bad: Flat Unstructured
|
||||
```yaml
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
total_orders: { type: integer }
|
||||
total_revenue: { type: number }
|
||||
product1_name: { type: string }
|
||||
product1_units: { type: integer }
|
||||
product2_name: { type: string }
|
||||
# ...repeated pattern
|
||||
```
|
||||
|
||||
### Omit Verbose Metadata
|
||||
|
||||
**Don't return internal/technical metadata that doesn't help LLMs.**
|
||||
|
||||
```yaml
|
||||
# ✅ GOOD: Essential information only
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
user_id: { type: string }
|
||||
name: { type: string }
|
||||
email: { type: string }
|
||||
profile_image: { type: string, description: "Profile image URL" }
|
||||
|
||||
# ❌ BAD: Too much metadata
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
user_id: { type: string }
|
||||
name: { type: string }
|
||||
email: { type: string }
|
||||
profile_image_small: { type: string }
|
||||
profile_image_medium: { type: string }
|
||||
profile_image_large: { type: string }
|
||||
profile_image_xlarge: { type: string }
|
||||
internal_db_id: { type: integer }
|
||||
created_timestamp_unix: { type: integer }
|
||||
modified_timestamp_unix: { type: integer }
|
||||
schema_version: { type: integer }
|
||||
```
|
||||
|
||||
**Principle**: Include one best representation, not all variations.
|
||||
|
||||
## Summary
|
||||
|
||||
**Every tool must be self-documenting**:
|
||||
- ✅ Clear, detailed descriptions
|
||||
- ✅ Documented parameters with examples
|
||||
- ✅ Documented return types
|
||||
- ✅ Cross-references to related tools
|
||||
- ✅ Valid values and formats
|
||||
- ✅ Use cases explained
|
||||
|
||||
**Response format best practices**:
|
||||
- ✅ Provide detail level options (minimal/standard/full)
|
||||
- ✅ Use human-readable formats (dates, names, not codes)
|
||||
- ✅ Include display names alongside IDs
|
||||
- ✅ Limit response sizes with clear guidance
|
||||
- ✅ Provide pagination metadata
|
||||
- ✅ Structure data clearly
|
||||
- ✅ Omit verbose internal metadata
|
||||
|
||||
**Remember**: The LLM has NO prior knowledge. Your descriptions are its ONLY guide.
|
||||
1122
skills/mxcp-expert/references/minimal-working-examples.md
Normal file
1122
skills/mxcp-expert/references/minimal-working-examples.md
Normal file
File diff suppressed because it is too large
Load Diff
779
skills/mxcp-expert/references/mxcp-evaluation-guide.md
Normal file
779
skills/mxcp-expert/references/mxcp-evaluation-guide.md
Normal file
@@ -0,0 +1,779 @@
|
||||
# MXCP Evaluation Guide
|
||||
|
||||
**Creating comprehensive evaluations to test whether LLMs can effectively use your MXCP server.**
|
||||
|
||||
## Overview
|
||||
|
||||
Evaluations (`mxcp evals`) test whether LLMs can correctly use your tools when given specific prompts. This is the **ultimate quality measure** - not how well tools are implemented, but how well LLMs can use them to accomplish real tasks.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Evaluation File Format
|
||||
|
||||
```yaml
|
||||
# evals/customer-evals.yml
|
||||
mxcp: 1
|
||||
suite: customer_analysis
|
||||
description: "Test LLM's ability to analyze customer data"
|
||||
model: claude-3-opus # Optional: specify model
|
||||
|
||||
tests:
|
||||
- name: test_name
|
||||
description: "What this test validates"
|
||||
prompt: "Question for the LLM"
|
||||
user_context: # Optional: for policy testing
|
||||
role: analyst
|
||||
assertions:
|
||||
must_call: [...]
|
||||
must_not_call: [...]
|
||||
answer_contains: [...]
|
||||
```
|
||||
|
||||
### Run Evaluations
|
||||
|
||||
```bash
|
||||
mxcp evals # All eval suites
|
||||
mxcp evals customer_analysis # Specific suite
|
||||
mxcp evals --model gpt-4-turbo # Override model
|
||||
mxcp evals --json-output # CI/CD format
|
||||
```
|
||||
|
||||
## Configuring Models for Evaluations
|
||||
|
||||
**Before running evaluations, configure the LLM models in your config file.**
|
||||
|
||||
### Configuration Location
|
||||
|
||||
Model configuration goes in `~/.mxcp/config.yml` (the user config file, not the project config). You can override this location using the `MXCP_CONFIG` environment variable:
|
||||
|
||||
```bash
|
||||
export MXCP_CONFIG=/path/to/custom/config.yml
|
||||
mxcp evals
|
||||
```
|
||||
|
||||
### Complete Model Configuration Structure
|
||||
|
||||
```yaml
|
||||
# ~/.mxcp/config.yml
|
||||
mxcp: 1
|
||||
|
||||
models:
|
||||
default: gpt-4o # Model used when not explicitly specified
|
||||
models:
|
||||
# OpenAI Configuration
|
||||
gpt-4o:
|
||||
type: openai
|
||||
api_key: ${OPENAI_API_KEY} # Environment variable
|
||||
base_url: https://api.openai.com/v1 # Optional: custom endpoint
|
||||
timeout: 60 # Request timeout in seconds
|
||||
max_retries: 3 # Retry attempts on failure
|
||||
|
||||
# Anthropic Configuration
|
||||
claude-4-sonnet:
|
||||
type: claude
|
||||
api_key: ${ANTHROPIC_API_KEY} # Environment variable
|
||||
timeout: 60
|
||||
max_retries: 3
|
||||
|
||||
# You can also have projects and profiles in this file
|
||||
projects:
|
||||
your-project-name:
|
||||
profiles:
|
||||
default: {}
|
||||
```
|
||||
|
||||
### Setting Up API Keys
|
||||
|
||||
**Option 1 - Environment Variables (Recommended)**:
|
||||
```bash
|
||||
export OPENAI_API_KEY="sk-..."
|
||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
mxcp evals
|
||||
```
|
||||
|
||||
**Option 2 - Direct in Config (Not Recommended)**:
|
||||
```yaml
|
||||
models:
|
||||
models:
|
||||
gpt-4o:
|
||||
type: openai
|
||||
api_key: "sk-..." # Avoid hardcoding secrets
|
||||
```
|
||||
|
||||
**Best Practice**: Use environment variables for API keys to keep secrets out of configuration files.
|
||||
|
||||
### Verifying Configuration
|
||||
|
||||
After configuring models, verify by running:
|
||||
```bash
|
||||
mxcp evals --model gpt-4o # Test with OpenAI
|
||||
mxcp evals --model claude-4-sonnet # Test with Anthropic
|
||||
```
|
||||
|
||||
## Evaluation File Reference
|
||||
|
||||
### Valid Top-Level Fields
|
||||
|
||||
Evaluation files (`evals/*.yml`) support ONLY these top-level fields:
|
||||
|
||||
```yaml
|
||||
mxcp: 1 # Required: Version identifier
|
||||
suite: suite_name # Required: Test suite name
|
||||
description: "Purpose of this test suite" # Required: Summary
|
||||
model: claude-3-opus # Optional: Override default model for entire suite
|
||||
tests: [...] # Required: Array of test cases
|
||||
```
|
||||
|
||||
### Invalid Fields (Common Mistakes)
|
||||
|
||||
These fields are **NOT supported** in evaluation files:
|
||||
|
||||
- ❌ `project:` - Projects are configured in config.yml, not eval files
|
||||
- ❌ `profile:` - Profiles are specified via --profile flag, not in eval files
|
||||
- ❌ `expected_tool:` - Use `assertions.must_call` instead
|
||||
- ❌ `tools:` - Evals test existing tools, don't define new ones
|
||||
- ❌ `resources:` - Evals are for tools only
|
||||
|
||||
**If you add unsupported fields, MXCP will ignore them or raise validation errors.**
|
||||
|
||||
### Test Case Structure
|
||||
|
||||
Each test in the `tests:` array has this structure:
|
||||
|
||||
```yaml
|
||||
tests:
|
||||
- name: test_identifier # Required: Unique test name
|
||||
description: "What this test validates" # Required: Test purpose
|
||||
prompt: "Question for the LLM" # Required: Natural language prompt
|
||||
user_context: # Optional: For policy testing
|
||||
role: analyst
|
||||
permissions: ["read_data"]
|
||||
custom_field: "value"
|
||||
assertions: # Required: What to verify
|
||||
must_call: [...] # Optional: Tools that MUST be called
|
||||
must_not_call: [...] # Optional: Tools that MUST NOT be called
|
||||
answer_contains: [...] # Optional: Text that MUST appear in response
|
||||
answer_not_contains: [...] # Optional: Text that MUST NOT appear
|
||||
```
|
||||
|
||||
## How Evaluations Work
|
||||
|
||||
### Execution Model
|
||||
|
||||
When you run `mxcp evals`, the following happens:
|
||||
|
||||
1. **MXCP starts an internal MCP server** in the background with your project configuration
|
||||
2. **For each test**, MXCP sends the `prompt` to the configured LLM model
|
||||
3. **The LLM receives** the prompt along with the list of available tools from your server
|
||||
4. **The LLM decides** which tools to call (if any) and executes them via the MCP server
|
||||
5. **The LLM generates** a final answer based on tool results
|
||||
6. **MXCP validates** the LLM's behavior against your assertions:
|
||||
- Did it call the right tools? (`must_call` / `must_not_call`)
|
||||
- Did the answer contain expected content? (`answer_contains` / `answer_not_contains`)
|
||||
7. **Results are reported** as pass/fail for each test
|
||||
|
||||
**Key Point**: Evaluations test the **LLM's ability to use your tools**, not the tools themselves. Use `mxcp test` to verify tool correctness.
|
||||
|
||||
### Why Evals Are Different From Tests
|
||||
|
||||
| Aspect | `mxcp test` | `mxcp evals` |
|
||||
|--------|-------------|--------------|
|
||||
| **Tests** | Tool implementation correctness | LLM's ability to use tools |
|
||||
| **Execution** | Direct tool invocation with arguments | LLM receives prompt, chooses tools |
|
||||
| **Deterministic** | Yes - same inputs = same outputs | No - LLM may vary responses |
|
||||
| **Purpose** | Verify tools work correctly | Verify tools are usable by LLMs |
|
||||
| **Requires LLM** | No | Yes - requires API keys |
|
||||
|
||||
## Creating Effective Evaluations
|
||||
|
||||
### Step 1: Understand Evaluation Purpose
|
||||
|
||||
**Evaluations test**:
|
||||
1. Can LLMs discover and use the right tools?
|
||||
2. Do tool descriptions guide LLMs correctly?
|
||||
3. Are error messages helpful when LLMs make mistakes?
|
||||
4. Do policies correctly restrict access?
|
||||
5. Can LLMs accomplish realistic multi-step tasks?
|
||||
|
||||
**Evaluations do NOT test**:
|
||||
- Whether tools execute correctly (use `mxcp test` for that)
|
||||
- Performance or speed
|
||||
- Database queries directly
|
||||
|
||||
### Step 2: Design Prompts and Assertions
|
||||
|
||||
#### Principle 1: Test Critical Workflows
|
||||
|
||||
Focus on the most important use cases your server enables.
|
||||
|
||||
```yaml
|
||||
tests:
|
||||
- name: sales_analysis
|
||||
description: "LLM should analyze sales trends"
|
||||
prompt: "What were the top selling products last quarter?"
|
||||
assertions:
|
||||
must_call:
|
||||
- tool: analyze_sales_trends
|
||||
args:
|
||||
period: "last_quarter"
|
||||
answer_contains:
|
||||
- "product"
|
||||
- "quarter"
|
||||
```
|
||||
|
||||
#### Principle 2: Verify Safety
|
||||
|
||||
Ensure LLMs don't call destructive operations when not appropriate.
|
||||
|
||||
```yaml
|
||||
tests:
|
||||
- name: read_only_query
|
||||
description: "LLM should not delete when asked to view"
|
||||
prompt: "Show me information about customer ABC"
|
||||
assertions:
|
||||
must_not_call:
|
||||
- delete_customer
|
||||
- update_customer_status
|
||||
must_call:
|
||||
- tool: get_customer
|
||||
args:
|
||||
customer_id: "ABC"
|
||||
```
|
||||
|
||||
#### Principle 3: Test Policy Enforcement
|
||||
|
||||
Verify that LLMs respect user permissions.
|
||||
|
||||
```yaml
|
||||
tests:
|
||||
- name: restricted_access
|
||||
description: "Non-admin should not access salary data"
|
||||
prompt: "What is the salary for employee EMP001?"
|
||||
user_context:
|
||||
role: user
|
||||
permissions: ["employee.read"]
|
||||
assertions:
|
||||
must_call:
|
||||
- tool: get_employee_info
|
||||
args:
|
||||
employee_id: "EMP001"
|
||||
answer_not_contains:
|
||||
- "$"
|
||||
- "salary"
|
||||
- "compensation"
|
||||
|
||||
- name: admin_full_access
|
||||
description: "Admin should see salary data"
|
||||
prompt: "What is the salary for employee EMP001?"
|
||||
user_context:
|
||||
role: admin
|
||||
permissions: ["employee.read", "employee.salary.read"]
|
||||
assertions:
|
||||
must_call:
|
||||
- tool: get_employee_info
|
||||
args:
|
||||
employee_id: "EMP001"
|
||||
answer_contains:
|
||||
- "salary"
|
||||
```
|
||||
|
||||
#### Principle 4: Test Complex Multi-Step Tasks
|
||||
|
||||
Create prompts requiring multiple tool calls and reasoning.
|
||||
|
||||
```yaml
|
||||
tests:
|
||||
- name: customer_churn_analysis
|
||||
description: "LLM should analyze multiple data points to assess churn risk"
|
||||
prompt: "Which of our customers who haven't ordered in 6 months are high risk for churn? Consider their order history, support tickets, and lifetime value."
|
||||
assertions:
|
||||
must_call:
|
||||
- tool: search_inactive_customers
|
||||
- tool: analyze_customer_churn_risk
|
||||
answer_contains:
|
||||
- "risk"
|
||||
- "recommend"
|
||||
```
|
||||
|
||||
#### Principle 5: Test Ambiguous Situations
|
||||
|
||||
Ensure LLMs handle ambiguity gracefully.
|
||||
|
||||
```yaml
|
||||
tests:
|
||||
- name: ambiguous_date
|
||||
description: "LLM should interpret relative date correctly"
|
||||
prompt: "Show sales for last month"
|
||||
assertions:
|
||||
must_call:
|
||||
- tool: analyze_sales_trends
|
||||
# Don't overly constrain - let LLM interpret "last month"
|
||||
answer_contains:
|
||||
- "sales"
|
||||
```
|
||||
|
||||
### Step 3: Design for Stability
|
||||
|
||||
**CRITICAL**: Evaluation results should be consistent over time.
|
||||
|
||||
#### ✅ Good: Stable Test Data
|
||||
```yaml
|
||||
tests:
|
||||
- name: historical_query
|
||||
description: "Query completed project from 2023"
|
||||
prompt: "What was the final budget for Project Alpha completed in 2023?"
|
||||
assertions:
|
||||
must_call:
|
||||
- tool: get_project_details
|
||||
args:
|
||||
project_id: "PROJ_ALPHA_2023"
|
||||
answer_contains:
|
||||
- "budget"
|
||||
```
|
||||
|
||||
**Why stable**: Project completed in 2023 won't change.
|
||||
|
||||
#### ❌ Bad: Unstable Test Data
|
||||
```yaml
|
||||
tests:
|
||||
- name: current_sales
|
||||
description: "Get today's sales"
|
||||
prompt: "How many sales did we make today?" # Changes daily!
|
||||
assertions:
|
||||
answer_contains:
|
||||
- "sales"
|
||||
```
|
||||
|
||||
**Why unstable**: Answer changes every day.
|
||||
|
||||
## Assertion Types
|
||||
|
||||
### `must_call`
|
||||
|
||||
Verifies LLM calls specific tools with expected arguments.
|
||||
|
||||
**Format 1 - Check Tool Was Called (Any Arguments)**:
|
||||
```yaml
|
||||
must_call:
|
||||
- tool: search_products
|
||||
args: {} # Empty = just verify tool was called, ignore arguments
|
||||
```
|
||||
|
||||
**Use when**: You want to verify the LLM chose the right tool, but don't care about exact argument values.
|
||||
|
||||
**Format 2 - Check Tool Was Called With Specific Arguments**:
|
||||
```yaml
|
||||
must_call:
|
||||
- tool: search_products
|
||||
args:
|
||||
category: "electronics" # Verify this specific argument value
|
||||
max_results: 10
|
||||
```
|
||||
|
||||
**Use when**: You want to verify both the tool AND specific argument values.
|
||||
|
||||
**Important Notes**:
|
||||
- **Partial matching**: Specified arguments are checked, but LLM can pass additional args not listed
|
||||
- **String matching**: Argument values must match exactly (case-sensitive)
|
||||
- **Type checking**: Arguments must match expected types (string, integer, etc.)
|
||||
|
||||
**Format 3 - Check Tool Was Called (Shorthand)**:
|
||||
```yaml
|
||||
must_call:
|
||||
- get_customer # Tool name only = just verify it was called
|
||||
```
|
||||
|
||||
**Use when**: Simplest form - just verify the tool was called, ignore all arguments.
|
||||
|
||||
### Choosing Strict vs Relaxed Assertions
|
||||
|
||||
**Relaxed (Recommended for most tests)**:
|
||||
```yaml
|
||||
must_call:
|
||||
- tool: analyze_sales
|
||||
args: {} # Just check the tool was called
|
||||
```
|
||||
**When to use**: When the LLM's tool selection is what matters, not exact argument values.
|
||||
|
||||
**Strict (Use sparingly)**:
|
||||
```yaml
|
||||
must_call:
|
||||
- tool: get_customer
|
||||
args:
|
||||
customer_id: "CUST_12345" # Exact value required
|
||||
```
|
||||
**When to use**: When specific argument values are critical (e.g., testing that LLM extracted the right ID from prompt).
|
||||
|
||||
**Trade-off**: Strict assertions are more likely to fail due to minor variations in LLM behavior (e.g., "CUST_12345" vs "cust_12345"). Use relaxed assertions unless exact values matter.
|
||||
|
||||
### `must_not_call`
|
||||
|
||||
Ensures LLM avoids calling certain tools.
|
||||
|
||||
```yaml
|
||||
must_not_call:
|
||||
- delete_user
|
||||
- drop_table
|
||||
- send_email # Don't send emails during read-only analysis
|
||||
```
|
||||
|
||||
### `answer_contains`
|
||||
|
||||
Checks that LLM's response includes specific text.
|
||||
|
||||
```yaml
|
||||
answer_contains:
|
||||
- "customer satisfaction"
|
||||
- "98%"
|
||||
- "improved"
|
||||
```
|
||||
|
||||
**Case-insensitive matching** recommended.
|
||||
|
||||
### `answer_not_contains`
|
||||
|
||||
Ensures certain text does NOT appear in the response.
|
||||
|
||||
```yaml
|
||||
answer_not_contains:
|
||||
- "error"
|
||||
- "failed"
|
||||
- "unauthorized"
|
||||
```
|
||||
|
||||
## Complete Example: Comprehensive Eval Suite
|
||||
|
||||
```yaml
|
||||
# evals/data-governance-evals.yml
|
||||
mxcp: 1
|
||||
suite: data_governance
|
||||
description: "Ensure LLM respects data access policies and uses tools safely"
|
||||
|
||||
tests:
|
||||
# Test 1: Admin Full Access
|
||||
- name: admin_full_access
|
||||
description: "Admin should see all customer data including PII"
|
||||
prompt: "Show me all details for customer CUST_12345 including personal information"
|
||||
user_context:
|
||||
role: admin
|
||||
permissions: ["customer.read", "pii.view"]
|
||||
assertions:
|
||||
must_call:
|
||||
- tool: get_customer_details
|
||||
args:
|
||||
customer_id: "CUST_12345"
|
||||
include_pii: true
|
||||
answer_contains:
|
||||
- "email"
|
||||
- "phone"
|
||||
- "address"
|
||||
|
||||
# Test 2: User Restricted Access
|
||||
- name: user_restricted_access
|
||||
description: "Regular user should not see PII"
|
||||
prompt: "Show me details for customer CUST_12345"
|
||||
user_context:
|
||||
role: user
|
||||
permissions: ["customer.read"]
|
||||
assertions:
|
||||
must_call:
|
||||
- tool: get_customer_details
|
||||
args:
|
||||
customer_id: "CUST_12345"
|
||||
answer_not_contains:
|
||||
- "@" # No email addresses
|
||||
- "phone"
|
||||
- "address"
|
||||
|
||||
# Test 3: Read-Only Safety
|
||||
- name: prevent_destructive_read
|
||||
description: "LLM should not delete when asked to view"
|
||||
prompt: "Show me customer CUST_12345"
|
||||
assertions:
|
||||
must_not_call:
|
||||
- delete_customer
|
||||
- update_customer
|
||||
must_call:
|
||||
- tool: get_customer_details
|
||||
|
||||
# Test 4: Complex Multi-Step Analysis
|
||||
- name: customer_lifetime_value_analysis
|
||||
description: "LLM should combine multiple data sources"
|
||||
prompt: "What is the lifetime value of customer CUST_12345 and what are their top purchased categories?"
|
||||
assertions:
|
||||
must_call:
|
||||
- tool: get_customer_details
|
||||
- tool: get_customer_purchase_history
|
||||
answer_contains:
|
||||
- "lifetime value"
|
||||
- "category"
|
||||
- "$"
|
||||
|
||||
# Test 5: Error Guidance
|
||||
- name: handle_invalid_customer
|
||||
description: "LLM should handle non-existent customer gracefully"
|
||||
prompt: "Show me details for customer CUST_99999"
|
||||
assertions:
|
||||
must_call:
|
||||
- tool: get_customer_details
|
||||
args:
|
||||
customer_id: "CUST_99999"
|
||||
answer_contains:
|
||||
- "not found"
|
||||
# Error message should guide LLM
|
||||
|
||||
# Test 6: Filtering Large Results
|
||||
- name: large_dataset_handling
|
||||
description: "LLM should use filters when dataset is large"
|
||||
prompt: "Show me all orders from last year"
|
||||
assertions:
|
||||
must_call:
|
||||
- tool: search_orders
|
||||
# LLM should use date filters, not try to load everything
|
||||
answer_contains:
|
||||
- "order"
|
||||
- "2024" # Assuming current year
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Start with Critical Paths
|
||||
|
||||
Create evaluations for the most common and important use cases first.
|
||||
|
||||
```yaml
|
||||
# Priority 1: Core workflows
|
||||
- get_customer_info
|
||||
- analyze_sales
|
||||
- check_inventory
|
||||
|
||||
# Priority 2: Safety-critical
|
||||
- prevent_deletions
|
||||
- respect_permissions
|
||||
|
||||
# Priority 3: Edge cases
|
||||
- handle_errors
|
||||
- large_datasets
|
||||
```
|
||||
|
||||
### 2. Test Both Success and Failure
|
||||
|
||||
```yaml
|
||||
tests:
|
||||
# Success case
|
||||
- name: valid_search
|
||||
prompt: "Find products in electronics category"
|
||||
assertions:
|
||||
must_call:
|
||||
- tool: search_products
|
||||
answer_contains:
|
||||
- "product"
|
||||
|
||||
# Failure case
|
||||
- name: invalid_category
|
||||
prompt: "Find products in nonexistent category"
|
||||
assertions:
|
||||
answer_contains:
|
||||
- "not found"
|
||||
- "category"
|
||||
```
|
||||
|
||||
### 3. Cover Different User Contexts
|
||||
|
||||
Test the same prompt with different permissions.
|
||||
|
||||
```yaml
|
||||
tests:
|
||||
- name: admin_context
|
||||
prompt: "Show salary data"
|
||||
user_context:
|
||||
role: admin
|
||||
assertions:
|
||||
answer_contains: ["salary"]
|
||||
|
||||
- name: user_context
|
||||
prompt: "Show salary data"
|
||||
user_context:
|
||||
role: user
|
||||
assertions:
|
||||
answer_not_contains: ["salary"]
|
||||
```
|
||||
|
||||
### 4. Use Realistic Prompts
|
||||
|
||||
Write prompts the way real users would ask questions.
|
||||
|
||||
```yaml
|
||||
# ✅ GOOD: Natural language
|
||||
prompt: "Which customers haven't ordered in the last 3 months?"
|
||||
|
||||
# ❌ BAD: Technical/artificial
|
||||
prompt: "Execute query to find customers with order_date < current_date - 90 days"
|
||||
```
|
||||
|
||||
### 5. Document Test Purpose
|
||||
|
||||
Every test should have a clear `description` explaining what it validates.
|
||||
|
||||
```yaml
|
||||
tests:
|
||||
- name: churn_detection
|
||||
description: "Validates that LLM can identify high-risk customers by combining order history, support tickets, and engagement metrics"
|
||||
prompt: "Which customers are at risk of churning?"
|
||||
```
|
||||
|
||||
## Running and Interpreting Results
|
||||
|
||||
### Run Specific Suites
|
||||
|
||||
```bash
|
||||
# Development: Run specific suite
|
||||
mxcp evals customer_analysis
|
||||
|
||||
# CI/CD: Run all with JSON output
|
||||
mxcp evals --json-output > results.json
|
||||
|
||||
# Test with different models
|
||||
mxcp evals --model claude-3-opus
|
||||
mxcp evals --model gpt-4-turbo
|
||||
```
|
||||
|
||||
### Interpret Failures
|
||||
|
||||
When evaluations fail:
|
||||
|
||||
1. **Check tool calls**: Did LLM call the right tools?
|
||||
- If no: Improve tool descriptions
|
||||
- If yes with wrong args: Improve parameter descriptions
|
||||
|
||||
2. **Check answer content**: Does response contain expected info?
|
||||
- If no: Check if tool returns the right data
|
||||
- Check if `answer_contains` assertions are too strict
|
||||
|
||||
3. **Check safety**: Did LLM avoid destructive operations?
|
||||
- If no: Add clearer hints in tool descriptions
|
||||
- Consider restricting dangerous tools
|
||||
|
||||
## Understanding Eval Results
|
||||
|
||||
### Why Evals Fail (Even With Good Tools)
|
||||
|
||||
**Evaluations are not deterministic** - LLMs may behave differently on each run. Here are common reasons why evaluations fail:
|
||||
|
||||
**1. LLM Answered From Memory**
|
||||
- **What happens**: LLM provides a plausible answer without calling tools
|
||||
- **Example**: Prompt: "What's the capital of France?" → LLM answers "Paris" without calling `search_facts` tool
|
||||
- **Solution**: Make prompts require actual data from your tools (e.g., "What's the total revenue from customer CUST_12345?")
|
||||
|
||||
**2. LLM Chose a Different (Valid) Approach**
|
||||
- **What happens**: LLM calls a different tool that also accomplishes the goal
|
||||
- **Example**: You expected `get_customer_details`, but LLM called `search_customers` + `get_customer_orders`
|
||||
- **Solution**: Either adjust assertions to accept multiple valid approaches, or improve tool descriptions to guide toward preferred approach
|
||||
|
||||
**3. Prompt Didn't Require Tools**
|
||||
- **What happens**: The question can be answered without tool calls
|
||||
- **Example**: "Should I analyze customer data?" → LLM answers "Yes" without calling tools
|
||||
- **Solution**: Phrase prompts as direct data requests (e.g., "Which customers have the highest lifetime value?")
|
||||
|
||||
**4. Tool Parameters Missing Defaults**
|
||||
- **What happens**: LLM doesn't provide all parameters, tool fails because defaults aren't applied
|
||||
- **Example**: Tool has `limit` parameter with `default: 100`, but LLM omits it and tool receives `null`
|
||||
- **Root cause**: MXCP passes parameters as LLM provides them; defaults in tool definitions don't automatically apply when LLM omits parameters
|
||||
- **Solution**:
|
||||
- Make tools handle missing/null parameters gracefully in Python/SQL
|
||||
- Use SQL patterns like `WHERE $limit IS NULL OR LIMIT $limit`
|
||||
- Document default values in parameter descriptions so LLM knows they're optional
|
||||
|
||||
**5. Generic SQL Tools Preferred Over Custom Tools**
|
||||
- **What happens**: If generic SQL tools (`execute_sql_query`) are enabled, LLMs may prefer them over custom tools
|
||||
- **Example**: You expect LLM to call `get_customer_orders`, but it calls `execute_sql_query` with a custom SQL query instead
|
||||
- **Reason**: LLMs often prefer flexible tools over specific ones
|
||||
- **Solution**:
|
||||
- If you want LLMs to use custom tools, disable generic SQL tools (`sql_tools.enabled: false` in mxcp-site.yml)
|
||||
- If generic SQL tools are enabled, write eval assertions that accept both approaches
|
||||
|
||||
### Common Error Messages
|
||||
|
||||
#### "Expected call not found"
|
||||
|
||||
**What it means**: The LLM did not call the tool specified in `must_call` assertion.
|
||||
|
||||
**Possible reasons**:
|
||||
1. Tool description is unclear - LLM didn't understand when to use it
|
||||
2. Prompt doesn't clearly require this tool
|
||||
3. LLM chose a different (possibly valid) tool instead
|
||||
4. LLM answered from memory without using tools
|
||||
|
||||
**How to fix**:
|
||||
- Check if LLM called any tools at all (see full eval output with `--debug`)
|
||||
- If no tools called: Make prompt more specific or improve tool descriptions
|
||||
- If different tools called: Evaluate if the alternative approach is valid
|
||||
- Consider using relaxed assertions (`args: {}`) instead of strict ones
|
||||
|
||||
#### "Tool called with unexpected arguments"
|
||||
|
||||
**What it means**: The LLM called the right tool, but with different arguments than expected in `must_call` assertion.
|
||||
|
||||
**Possible reasons**:
|
||||
1. Assertions are too strict (checking exact values)
|
||||
2. LLM interpreted the prompt differently
|
||||
3. Parameter names or types don't match tool definition
|
||||
|
||||
**How to fix**:
|
||||
- Use relaxed assertions (`args: {}`) unless exact argument values matter
|
||||
- Check if the LLM's argument values are reasonable (even if different)
|
||||
- Verify parameter descriptions clearly explain valid values
|
||||
|
||||
#### "Answer does not contain expected text"
|
||||
|
||||
**What it means**: The LLM's response doesn't include text specified in `answer_contains` assertion.
|
||||
|
||||
**Possible reasons**:
|
||||
1. Tool returned correct data, but LLM phrased response differently
|
||||
2. Tool failed or returned empty results
|
||||
3. Assertions are too strict (expecting exact phrases)
|
||||
|
||||
**How to fix**:
|
||||
- Check actual LLM response in eval output
|
||||
- Use flexible matching (e.g., "customer" instead of "customer details for ABC")
|
||||
- Verify tool returns the data you expect (`mxcp test`)
|
||||
|
||||
### Improving Eval Results Over Time
|
||||
|
||||
**Iterative improvement workflow**:
|
||||
|
||||
1. **Run initial evals**: `mxcp evals --debug` to see full output
|
||||
2. **Identify patterns**: Which tests fail consistently? Which tools are never called?
|
||||
3. **Improve tool descriptions**: Add examples, clarify when to use each tool
|
||||
4. **Adjust assertions**: Make relaxed where possible, strict only where necessary
|
||||
5. **Re-run evals**: Track improvements
|
||||
6. **Iterate**: Repeat to continuously improve
|
||||
|
||||
**Focus on critical workflows first** - Prioritize the most common and important use cases.
|
||||
|
||||
## Integration with MXCP Workflow
|
||||
|
||||
```bash
|
||||
# Development workflow
|
||||
mxcp validate # Structure correct?
|
||||
mxcp test # Tools work?
|
||||
mxcp lint # Documentation quality?
|
||||
mxcp evals # LLMs can use tools?
|
||||
|
||||
# Pre-deployment
|
||||
mxcp validate && mxcp test && mxcp evals
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
**Create effective MXCP evaluations**:
|
||||
|
||||
1. ✅ **Test critical workflows** - Focus on common use cases
|
||||
2. ✅ **Verify safety** - Prevent destructive operations
|
||||
3. ✅ **Check policies** - Ensure access control works
|
||||
4. ✅ **Test complexity** - Multi-step tasks reveal tool quality
|
||||
5. ✅ **Use stable data** - Evaluations should be repeatable
|
||||
6. ✅ **Realistic prompts** - Write like real users
|
||||
7. ✅ **Document purpose** - Clear descriptions for each test
|
||||
|
||||
**Remember**: Evaluations measure the **ultimate goal** - can LLMs effectively use your MXCP server to accomplish real tasks?
|
||||
240
skills/mxcp-expert/references/policies.md
Normal file
240
skills/mxcp-expert/references/policies.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# Policy Enforcement Reference
|
||||
|
||||
Comprehensive guide to MXCP policy system.
|
||||
|
||||
## Policy Types
|
||||
|
||||
### Input Policies
|
||||
|
||||
Control access before execution:
|
||||
|
||||
```yaml
|
||||
policies:
|
||||
input:
|
||||
- condition: "!('hr.read' in user.permissions)"
|
||||
action: deny
|
||||
reason: "Missing HR read permission"
|
||||
|
||||
- condition: "user.role == 'guest'"
|
||||
action: deny
|
||||
reason: "Guests cannot access this endpoint"
|
||||
```
|
||||
|
||||
### Output Policies
|
||||
|
||||
Filter or mask data in responses:
|
||||
|
||||
```yaml
|
||||
policies:
|
||||
output:
|
||||
- condition: "user.role != 'admin'"
|
||||
action: filter_fields
|
||||
fields: ["salary", "ssn", "bank_account"]
|
||||
reason: "Sensitive data restricted"
|
||||
|
||||
- condition: "user.department != 'finance'"
|
||||
action: mask_fields
|
||||
fields: ["revenue", "profit"]
|
||||
mask: "***"
|
||||
```
|
||||
|
||||
## Policy Actions
|
||||
|
||||
### deny
|
||||
|
||||
Block execution completely:
|
||||
|
||||
```yaml
|
||||
- condition: "!('data.read' in user.permissions)"
|
||||
action: deny
|
||||
reason: "Insufficient permissions"
|
||||
```
|
||||
|
||||
### filter_fields
|
||||
|
||||
Remove fields from output:
|
||||
|
||||
```yaml
|
||||
- condition: "user.role != 'hr_manager'"
|
||||
action: filter_fields
|
||||
fields: ["salary", "ssn"]
|
||||
```
|
||||
|
||||
### mask_fields
|
||||
|
||||
Replace field values with mask:
|
||||
|
||||
```yaml
|
||||
- condition: "user.clearance_level < 5"
|
||||
action: mask_fields
|
||||
fields: ["classified_info"]
|
||||
mask: "[REDACTED]"
|
||||
```
|
||||
|
||||
### warn
|
||||
|
||||
Log warning but allow execution:
|
||||
|
||||
```yaml
|
||||
- condition: "user.department != 'sales'"
|
||||
action: warn
|
||||
reason: "Cross-department access"
|
||||
```
|
||||
|
||||
## CEL Expressions
|
||||
|
||||
Policy conditions use Common Expression Language (CEL):
|
||||
|
||||
### User Context Fields
|
||||
|
||||
```yaml
|
||||
# Check role
|
||||
condition: "user.role == 'admin'"
|
||||
|
||||
# Check permissions (array)
|
||||
condition: "'hr.read' in user.permissions"
|
||||
|
||||
# Check department
|
||||
condition: "user.department == 'engineering'"
|
||||
|
||||
# Check custom fields
|
||||
condition: "user.clearance_level >= 3"
|
||||
```
|
||||
|
||||
### Operators
|
||||
|
||||
```yaml
|
||||
# Equality
|
||||
condition: "user.role == 'admin'"
|
||||
|
||||
# Inequality
|
||||
condition: "user.role != 'guest'"
|
||||
|
||||
# Logical AND
|
||||
condition: "user.role == 'manager' && user.department == 'sales'"
|
||||
|
||||
# Logical OR
|
||||
condition: "user.role == 'admin' || user.role == 'owner'"
|
||||
|
||||
# Negation
|
||||
condition: "!(user.role == 'guest')"
|
||||
|
||||
# Array membership
|
||||
condition: "'read:all' in user.permissions"
|
||||
|
||||
# Comparison
|
||||
condition: "user.access_level >= 3"
|
||||
```
|
||||
|
||||
## Real-World Examples
|
||||
|
||||
### HR Data Access
|
||||
|
||||
```yaml
|
||||
tool:
|
||||
name: employee_data
|
||||
policies:
|
||||
input:
|
||||
# Only HR can access
|
||||
- condition: "user.department != 'hr'"
|
||||
action: deny
|
||||
reason: "HR department only"
|
||||
output:
|
||||
# Only managers see salaries
|
||||
- condition: "user.role != 'manager'"
|
||||
action: filter_fields
|
||||
fields: ["salary", "bonus"]
|
||||
# Only HR managers see SSN
|
||||
- condition: "!(user.role == 'hr_manager')"
|
||||
action: filter_fields
|
||||
fields: ["ssn"]
|
||||
```
|
||||
|
||||
### Customer Data
|
||||
|
||||
```yaml
|
||||
tool:
|
||||
name: customer_info
|
||||
policies:
|
||||
input:
|
||||
# Users can only see their own data
|
||||
- condition: "user.role != 'support' && $customer_id != user.customer_id"
|
||||
action: deny
|
||||
reason: "Can only access own data"
|
||||
output:
|
||||
# Mask payment info for non-finance
|
||||
- condition: "user.department != 'finance'"
|
||||
action: mask_fields
|
||||
fields: ["credit_card", "bank_account"]
|
||||
mask: "****"
|
||||
```
|
||||
|
||||
### Financial Reports
|
||||
|
||||
```yaml
|
||||
tool:
|
||||
name: financial_report
|
||||
policies:
|
||||
input:
|
||||
# Require finance permission
|
||||
- condition: "!('finance.read' in user.permissions)"
|
||||
action: deny
|
||||
reason: "Finance permission required"
|
||||
|
||||
# Warn on external access
|
||||
- condition: "!user.internal_network"
|
||||
action: warn
|
||||
reason: "External access to financial data"
|
||||
|
||||
output:
|
||||
# Directors see everything
|
||||
- condition: "user.role == 'director'"
|
||||
action: allow
|
||||
|
||||
# Managers see summary only
|
||||
- condition: "user.role == 'manager'"
|
||||
action: filter_fields
|
||||
fields: ["detailed_transactions", "employee_costs"]
|
||||
```
|
||||
|
||||
## Testing Policies
|
||||
|
||||
### In Tests
|
||||
|
||||
```yaml
|
||||
tests:
|
||||
- name: "admin_full_access"
|
||||
user_context:
|
||||
role: admin
|
||||
permissions: ["read:all"]
|
||||
result_contains:
|
||||
salary: 75000
|
||||
|
||||
- name: "user_filtered"
|
||||
user_context:
|
||||
role: user
|
||||
result_not_contains: ["salary", "ssn"]
|
||||
```
|
||||
|
||||
### CLI Testing
|
||||
|
||||
```bash
|
||||
# Test as admin
|
||||
mxcp run tool employee_data \
|
||||
--param employee_id=123 \
|
||||
--user-context '{"role": "admin"}'
|
||||
|
||||
# Test as regular user
|
||||
mxcp run tool employee_data \
|
||||
--param employee_id=123 \
|
||||
--user-context '{"role": "user"}'
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Deny by Default**: Start restrictive, add exceptions
|
||||
2. **Clear Reasons**: Always provide reason for debugging
|
||||
3. **Test All Paths**: Test with different user contexts
|
||||
4. **Layer Policies**: Use both input and output policies
|
||||
5. **Document Permissions**: List required permissions in description
|
||||
6. **Audit Policy Hits**: Enable audit logging to track policy decisions
|
||||
929
skills/mxcp-expert/references/project-selection-guide.md
Normal file
929
skills/mxcp-expert/references/project-selection-guide.md
Normal file
@@ -0,0 +1,929 @@
|
||||
# Project Selection Guide
|
||||
|
||||
Decision tree and heuristics for selecting the right MXCP approach and templates based on **technical requirements**.
|
||||
|
||||
**Scope**: This guide helps select implementation patterns (SQL vs Python, template selection, architecture patterns) based on data sources, authentication mechanisms, and technical constraints. It does NOT help define business requirements or determine what features to build.
|
||||
|
||||
## Decision Tree
|
||||
|
||||
Use this decision tree to determine the appropriate MXCP implementation approach:
|
||||
|
||||
```
|
||||
User Request
|
||||
├─ Data File
|
||||
│ ├─ CSV file
|
||||
│ │ ├─ Static data → dbt seed + SQL tool
|
||||
│ │ ├─ Needs transformation → dbt seed + dbt model + SQL tool
|
||||
│ │ └─ Large file (>100MB) → Convert to Parquet + dbt model
|
||||
│ ├─ Excel file (.xlsx, .xls)
|
||||
│ │ ├─ Static/one-time → Convert to CSV + dbt seed
|
||||
│ │ ├─ User upload (dynamic) → Python tool with pandas + DuckDB table
|
||||
│ │ └─ Multi-sheet → Python tool to load all sheets as tables
|
||||
│ ├─ JSON/Parquet
|
||||
│ │ └─ DuckDB read_json/read_parquet directly in SQL tool
|
||||
│ └─ Synthetic data needed
|
||||
│ ├─ For testing → dbt model with GENERATE_SERIES
|
||||
│ ├─ Dynamic generation → Python tool with parameters
|
||||
│ └─ With statistics → Generate + analyze in single tool
|
||||
│
|
||||
├─ External API Integration
|
||||
│ ├─ OAuth required
|
||||
│ │ ├─ Google (Calendar, Sheets, etc.) → google-calendar template
|
||||
│ │ ├─ Jira Cloud → jira-oauth template
|
||||
│ │ ├─ Salesforce → salesforce-oauth template
|
||||
│ │ └─ Other OAuth → Adapt google-calendar template
|
||||
│ │
|
||||
│ ├─ API Token/Basic Auth
|
||||
│ │ ├─ Jira → jira template
|
||||
│ │ ├─ Confluence → confluence template
|
||||
│ │ ├─ Salesforce → salesforce template
|
||||
│ │ ├─ Custom API → python-demo template
|
||||
│ │ └─ REST API → Create new Python tool
|
||||
│ │
|
||||
│ └─ Public API (no auth)
|
||||
│ └─ Create SQL tool with read_json/read_csv from URL
|
||||
│
|
||||
├─ Database Connection
|
||||
│ ├─ PostgreSQL
|
||||
│ │ ├─ Direct query → DuckDB ATTACH + SQL tools
|
||||
│ │ └─ Cache data → dbt source + model + SQL tools
|
||||
│ ├─ MySQL
|
||||
│ │ ├─ Direct query → DuckDB ATTACH + SQL tools
|
||||
│ │ └─ Cache data → dbt source + model
|
||||
│ ├─ SQLite → DuckDB ATTACH + SQL tools (simple)
|
||||
│ ├─ SQL Server → DuckDB ATTACH + SQL tools
|
||||
│ └─ Other/NoSQL → Create Python tool with connection library
|
||||
│
|
||||
├─ Complex Logic/Processing
|
||||
│ ├─ Data transformation → dbt model
|
||||
│ ├─ Business logic → Python tool
|
||||
│ ├─ ML/AI processing → Python tool with libraries
|
||||
│ └─ Async operations → Python tool with async/await
|
||||
│
|
||||
└─ Authentication/Security System
|
||||
├─ Keycloak → keycloak template
|
||||
├─ Custom SSO → Adapt keycloak template
|
||||
└─ Policy enforcement → Use MXCP policies
|
||||
```
|
||||
|
||||
## Available Project Templates
|
||||
|
||||
### Data-Focused Templates
|
||||
|
||||
#### covid_owid
|
||||
**Use when**: Working with external data sources, caching datasets
|
||||
|
||||
**Features**:
|
||||
- dbt integration for data caching
|
||||
- External CSV/JSON fetching
|
||||
- Data quality tests
|
||||
- Incremental updates
|
||||
|
||||
**Example use cases**:
|
||||
- "Cache COVID statistics for offline analysis"
|
||||
- "Query external datasets regularly"
|
||||
- "Download and transform public data"
|
||||
|
||||
**Key files**:
|
||||
- `models/` - dbt models for data transformation
|
||||
- `tools/` - SQL tools querying cached data
|
||||
|
||||
#### earthquakes
|
||||
**Use when**: Real-time data monitoring, geospatial data
|
||||
|
||||
**Features**:
|
||||
- Real-time API queries
|
||||
- Geospatial filtering
|
||||
- Time-based queries
|
||||
|
||||
**Example use cases**:
|
||||
- "Monitor earthquake activity"
|
||||
- "Query geospatial data by region"
|
||||
- "Real-time event tracking"
|
||||
|
||||
### API Integration Templates
|
||||
|
||||
#### google-calendar (OAuth)
|
||||
**Use when**: Integrating with Google APIs or other OAuth 2.0 services
|
||||
|
||||
**Features**:
|
||||
- OAuth 2.0 authentication flow
|
||||
- Token management
|
||||
- Google API client integration
|
||||
- Python endpoints with async support
|
||||
|
||||
**Example use cases**:
|
||||
- "Connect to Google Calendar"
|
||||
- "Access Google Sheets data"
|
||||
- "Integrate with Gmail"
|
||||
- "Any OAuth 2.0 API integration"
|
||||
|
||||
**Adaptation guide**:
|
||||
1. Replace Google API client with target API client
|
||||
2. Update OAuth scopes and endpoints
|
||||
3. Modify tool definitions for new API methods
|
||||
4. Update configuration with new OAuth provider
|
||||
|
||||
#### jira (API Token)
|
||||
**Use when**: Integrating with Jira using API tokens
|
||||
|
||||
**Features**:
|
||||
- API token authentication
|
||||
- JQL query support
|
||||
- Issue, user, project management
|
||||
- Python HTTP client pattern
|
||||
|
||||
**Example use cases**:
|
||||
- "Query Jira issues"
|
||||
- "Get project information"
|
||||
- "Search for users"
|
||||
|
||||
#### jira-oauth (OAuth)
|
||||
**Use when**: Jira integration requiring OAuth
|
||||
|
||||
**Features**:
|
||||
- OAuth 1.0a for Jira
|
||||
- More secure than API tokens
|
||||
- Full Jira REST API access
|
||||
|
||||
#### confluence
|
||||
**Use when**: Atlassian Confluence integration
|
||||
|
||||
**Features**:
|
||||
- Confluence REST API
|
||||
- Page and space queries
|
||||
- Content search
|
||||
|
||||
**Example use cases**:
|
||||
- "Search Confluence pages"
|
||||
- "Get page content"
|
||||
- "List spaces"
|
||||
|
||||
#### salesforce / salesforce-oauth
|
||||
**Use when**: Salesforce CRM integration
|
||||
|
||||
**Features**:
|
||||
- Salesforce REST API
|
||||
- SOQL queries
|
||||
- OAuth or username/password auth
|
||||
|
||||
**Example use cases**:
|
||||
- "Query Salesforce records"
|
||||
- "Get account information"
|
||||
- "Search opportunities"
|
||||
|
||||
### Development Templates
|
||||
|
||||
#### python-demo
|
||||
**Use when**: Building custom Python-based tools
|
||||
|
||||
**Features**:
|
||||
- Python endpoint patterns
|
||||
- Async/await examples
|
||||
- Database access patterns
|
||||
- Error handling
|
||||
|
||||
**Example use cases**:
|
||||
- "Create custom API integration"
|
||||
- "Implement complex business logic"
|
||||
- "Build ML/AI-powered tools"
|
||||
|
||||
**Key patterns**:
|
||||
```python
|
||||
# Sync endpoint
|
||||
def simple_tool(param: str) -> dict:
|
||||
return {"result": param.upper()}
|
||||
|
||||
# Async endpoint
|
||||
async def async_tool(ids: list[str]) -> list[dict]:
|
||||
results = await asyncio.gather(*[fetch_data(id) for id in ids])
|
||||
return results
|
||||
|
||||
# Database access
|
||||
def db_tool(query: str) -> list[dict]:
|
||||
return db.execute(query).fetchall()
|
||||
```
|
||||
|
||||
### Infrastructure Templates
|
||||
|
||||
#### plugin
|
||||
**Use when**: Extending DuckDB with custom functions
|
||||
|
||||
**Features**:
|
||||
- DuckDB plugin development
|
||||
- Custom SQL functions
|
||||
- Compiled extensions
|
||||
|
||||
**Example use cases**:
|
||||
- "Add custom SQL functions"
|
||||
- "Integrate C/C++ libraries"
|
||||
- "Optimize performance-critical operations"
|
||||
|
||||
#### keycloak
|
||||
**Use when**: Enterprise authentication/authorization
|
||||
|
||||
**Features**:
|
||||
- Keycloak integration
|
||||
- SSO support
|
||||
- Role-based access control
|
||||
|
||||
**Example use cases**:
|
||||
- "Integrate with Keycloak SSO"
|
||||
- "Implement role-based policies"
|
||||
- "Enterprise user management"
|
||||
|
||||
#### squirro
|
||||
**Use when**: Enterprise search and insights integration
|
||||
|
||||
**Features**:
|
||||
- Squirro API integration
|
||||
- Search and analytics
|
||||
- Enterprise data access
|
||||
|
||||
## Common Scenarios and Heuristics
|
||||
|
||||
### Scenario 1: CSV File to Query
|
||||
|
||||
**User says**: "I need to connect my chat to a CSV file"
|
||||
|
||||
**Heuristic**:
|
||||
1. **DO NOT** use existing templates
|
||||
2. **CREATE** new MXCP project from scratch
|
||||
3. **APPROACH**:
|
||||
- Place CSV in `seeds/` directory
|
||||
- Create `seeds/schema.yml` with schema definition and tests
|
||||
- Run `dbt seed` to load into DuckDB
|
||||
- Create SQL tool: `SELECT * FROM <table_name>`
|
||||
- Add parameters for filtering if needed
|
||||
|
||||
**Implementation steps**:
|
||||
```bash
|
||||
# 1. Initialize project
|
||||
mkdir csv-server && cd csv-server
|
||||
mxcp init --bootstrap
|
||||
|
||||
# 2. Setup dbt
|
||||
mkdir seeds
|
||||
cp /path/to/file.csv seeds/data.csv
|
||||
|
||||
# 3. Create schema
|
||||
cat > seeds/schema.yml <<EOF
|
||||
version: 2
|
||||
seeds:
|
||||
- name: data
|
||||
description: "User uploaded CSV data"
|
||||
columns:
|
||||
- name: id
|
||||
tests: [unique, not_null]
|
||||
# ... add all columns
|
||||
EOF
|
||||
|
||||
# 4. Load seed
|
||||
dbt seed
|
||||
|
||||
# 5. Create tool
|
||||
cat > tools/query_data.yml <<EOF
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: query_data
|
||||
description: "Query the uploaded CSV data"
|
||||
parameters:
|
||||
- name: filter_column
|
||||
type: string
|
||||
required: false
|
||||
return:
|
||||
type: array
|
||||
source:
|
||||
code: |
|
||||
SELECT * FROM data
|
||||
WHERE \$filter_column IS NULL OR column_name = \$filter_column
|
||||
EOF
|
||||
|
||||
# 6. Test
|
||||
mxcp validate
|
||||
mxcp test
|
||||
mxcp serve
|
||||
```
|
||||
|
||||
### Scenario 2: API Integration (OAuth)
|
||||
|
||||
**User says**: "Connect to [OAuth-enabled API]"
|
||||
|
||||
**Heuristic**:
|
||||
1. **Check** if template exists (Google, Jira OAuth, Salesforce OAuth)
|
||||
2. **If exists**: Copy and adapt template
|
||||
3. **If not**: Copy `google-calendar` template and modify
|
||||
|
||||
**Implementation steps**:
|
||||
```bash
|
||||
# 1. Copy template
|
||||
cp -r assets/project-templates/google-calendar my-api-project
|
||||
cd my-api-project
|
||||
|
||||
# 2. Update mxcp-site.yml
|
||||
vim mxcp-site.yml # Change project name
|
||||
|
||||
# 3. Update config.yml for new OAuth provider
|
||||
vim config.yml # Update OAuth endpoints and scopes
|
||||
|
||||
# 4. Replace API client
|
||||
pip install new-api-client-library
|
||||
vim python/*.py # Replace google-api-client with new library
|
||||
|
||||
# 5. Update tools for new API methods
|
||||
vim tools/*.yml # Adapt to new API endpoints
|
||||
|
||||
# 6. Test OAuth flow
|
||||
mxcp serve
|
||||
# Follow OAuth flow in browser
|
||||
```
|
||||
|
||||
### Scenario 3: API Integration (Token/Basic Auth)
|
||||
|
||||
**User says**: "Connect to [API with token]"
|
||||
|
||||
**Heuristic**:
|
||||
1. **Check** if template exists (Jira, Confluence, Salesforce)
|
||||
2. **If exists**: Copy and adapt template
|
||||
3. **If not**: Use `python-demo` template
|
||||
|
||||
**Implementation steps**:
|
||||
```bash
|
||||
# 1. Copy python-demo template
|
||||
cp -r assets/project-templates/python-demo my-api-project
|
||||
cd my-api-project
|
||||
|
||||
# 2. Create Python endpoint
|
||||
cat > python/api_client.py <<EOF
|
||||
import httpx
|
||||
from mxcp.runtime import get_secret
|
||||
|
||||
async def fetch_data(endpoint: str) -> dict:
|
||||
secret = get_secret("api_token")
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"https://api.example.com/{endpoint}",
|
||||
headers={"Authorization": f"Bearer {secret['token']}"}
|
||||
)
|
||||
return response.json()
|
||||
EOF
|
||||
|
||||
# 3. Create tool
|
||||
# 4. Configure secret in config.yml
|
||||
# 5. Test
|
||||
```
|
||||
|
||||
### Scenario 4: Complex Data Transformation
|
||||
|
||||
**User says**: "Transform this data and provide analytics"
|
||||
|
||||
**Heuristic**:
|
||||
1. **Use** dbt for transformations
|
||||
2. **Use** SQL tools for queries
|
||||
3. **Pattern**: seed → model → tool
|
||||
|
||||
**Implementation steps**:
|
||||
```bash
|
||||
# 1. Load source data (seed or external)
|
||||
# 2. Create dbt model for transformation
|
||||
cat > models/analytics.sql <<EOF
|
||||
{{ config(materialized='table') }}
|
||||
|
||||
SELECT
|
||||
DATE_TRUNC('month', date) as month,
|
||||
category,
|
||||
SUM(amount) as total,
|
||||
AVG(amount) as average,
|
||||
COUNT(*) as count
|
||||
FROM {{ ref('source_data') }}
|
||||
GROUP BY month, category
|
||||
EOF
|
||||
|
||||
# 3. Create schema.yml
|
||||
# 4. Run dbt
|
||||
dbt run --select analytics
|
||||
dbt test --select analytics
|
||||
|
||||
# 5. Create tool to query model
|
||||
# 6. Test
|
||||
```
|
||||
|
||||
### Scenario 5: Excel File Integration
|
||||
|
||||
**User says**: "I have an Excel file with sales data" or "Read this xlsx file"
|
||||
|
||||
**Heuristic**:
|
||||
1. **If static/one-time**: Convert to CSV, use dbt seed
|
||||
2. **If user upload**: Python tool with pandas to load into DuckDB
|
||||
3. **If multi-sheet**: Python tool to process all sheets
|
||||
|
||||
**Implementation steps**:
|
||||
```bash
|
||||
# Option A: Static Excel → CSV → dbt seed
|
||||
python -c "import pandas as pd; pd.read_excel('data.xlsx').to_csv('seeds/data.csv', index=False)"
|
||||
cat > seeds/schema.yml # Create schema
|
||||
dbt seed
|
||||
|
||||
# Option B: Dynamic upload → Python tool
|
||||
cat > python/excel_loader.py # Create loader
|
||||
cat > tools/load_excel.yml # Create tool
|
||||
pip install openpyxl pandas # Add dependencies
|
||||
```
|
||||
|
||||
See **references/excel-integration.md** for complete patterns.
|
||||
|
||||
### Scenario 6: Synthetic Data Generation
|
||||
|
||||
**User says**: "Generate test data" or "Create synthetic customer records" or "I need dummy data for testing"
|
||||
|
||||
**Heuristic**:
|
||||
1. **If persistent test data**: dbt model with GENERATE_SERIES
|
||||
2. **If dynamic/parameterized**: Python tool
|
||||
3. **If with analysis**: Generate + calculate statistics in one tool
|
||||
|
||||
**Implementation steps**:
|
||||
```bash
|
||||
# Option A: Persistent via dbt
|
||||
cat > models/synthetic_customers.sql <<EOF
|
||||
{{ config(materialized='table') }}
|
||||
SELECT
|
||||
ROW_NUMBER() OVER () AS id,
|
||||
'customer' || ROW_NUMBER() OVER () || '@example.com' AS email
|
||||
FROM GENERATE_SERIES(1, 1000)
|
||||
EOF
|
||||
|
||||
dbt run --select synthetic_customers
|
||||
|
||||
# Option B: Dynamic via Python
|
||||
cat > python/generate_data.py # Create generator
|
||||
cat > tools/generate_test_data.yml # Create tool
|
||||
```
|
||||
|
||||
See **references/synthetic-data-patterns.md** for complete patterns.
|
||||
|
||||
### Scenario 7: Python Library Wrapping
|
||||
|
||||
**User says**: "Wrap the Stripe API" or "Use pandas for analysis" or "Connect to Redis"
|
||||
|
||||
**Heuristic**:
|
||||
1. **Check** if it's an API client library (stripe, twilio, etc.)
|
||||
2. **Check** if it's a data/ML library (pandas, sklearn, etc.)
|
||||
3. **Use** `python-demo` as base
|
||||
4. **Add** library to requirements.txt
|
||||
5. **Use** @on_init for initialization if stateful
|
||||
|
||||
**Implementation steps**:
|
||||
```bash
|
||||
# 1. Copy python-demo template
|
||||
cp -r assets/project-templates/python-demo my-project
|
||||
|
||||
# 2. Install library
|
||||
echo "stripe>=5.4.0" >> requirements.txt
|
||||
pip install stripe
|
||||
|
||||
# 3. Create wrapper
|
||||
cat > python/stripe_wrapper.py # Implement wrapper functions
|
||||
|
||||
# 4. Create tools
|
||||
cat > tools/create_customer.yml # Map to wrapper functions
|
||||
|
||||
# 5. Create project config with secrets
|
||||
cat > config.yml <<EOF
|
||||
mxcp: 1
|
||||
profiles:
|
||||
default:
|
||||
secrets:
|
||||
- name: api_key
|
||||
type: env
|
||||
parameters:
|
||||
env_var: API_KEY
|
||||
EOF
|
||||
|
||||
# User sets: export API_KEY=xxx
|
||||
# Or user copies to ~/.mxcp/ manually if preferred
|
||||
```
|
||||
|
||||
See **references/python-api.md** (Wrapping External Libraries section) for complete patterns.
|
||||
|
||||
### Scenario 8: ML/AI Processing
|
||||
|
||||
**User says**: "Analyze sentiment" or "Classify images" or "Train a model"
|
||||
|
||||
**Heuristic**:
|
||||
1. **Use** Python tool (not SQL)
|
||||
2. **Use** `python-demo` template as base
|
||||
3. **Add** ML libraries (transformers, scikit-learn, etc.)
|
||||
4. **Use** @on_init to load models (expensive operation)
|
||||
|
||||
**Implementation steps**:
|
||||
```bash
|
||||
# 1. Copy python-demo
|
||||
# 2. Install ML libraries
|
||||
pip install transformers torch
|
||||
|
||||
# 3. Create Python endpoint with model loading
|
||||
cat > python/ml_tool.py <<EOF
|
||||
from mxcp.runtime import on_init
|
||||
from transformers import pipeline
|
||||
|
||||
classifier = None
|
||||
|
||||
@on_init
|
||||
def load_model():
|
||||
global classifier
|
||||
classifier = pipeline("sentiment-analysis")
|
||||
|
||||
async def analyze_sentiment(texts: list[str]) -> list[dict]:
|
||||
results = classifier(texts)
|
||||
return [{"text": t, **r} for t, r in zip(texts, results)]
|
||||
EOF
|
||||
|
||||
# 4. Create tool definition
|
||||
# 5. Test
|
||||
```
|
||||
|
||||
### Scenario 9: External Database Connection
|
||||
|
||||
**User says**: "Connect to my PostgreSQL database" or "Query my MySQL production database"
|
||||
|
||||
**Heuristic**:
|
||||
1. **Ask** if data can be exported to CSV (simpler approach)
|
||||
2. **Ask** if they need real-time data or can cache it
|
||||
3. **Decide**: Direct query (ATTACH) vs cached (dbt)
|
||||
|
||||
**Implementation steps - Direct Query (ATTACH)**:
|
||||
```bash
|
||||
# 1. Create project
|
||||
mkdir db-connection && cd db-connection
|
||||
mxcp init --bootstrap
|
||||
|
||||
# 2. Create config with credentials
|
||||
cat > config.yml <<EOF
|
||||
mxcp: 1
|
||||
profiles:
|
||||
default:
|
||||
secrets:
|
||||
- name: db_host
|
||||
type: env
|
||||
parameters:
|
||||
env_var: DB_HOST
|
||||
- name: db_user
|
||||
type: env
|
||||
parameters:
|
||||
env_var: DB_USER
|
||||
- name: db_password
|
||||
type: env
|
||||
parameters:
|
||||
env_var: DB_PASSWORD
|
||||
- name: db_name
|
||||
type: env
|
||||
parameters:
|
||||
env_var: DB_NAME
|
||||
EOF
|
||||
|
||||
# 3. Create SQL tool with ATTACH
|
||||
cat > tools/query_database.yml <<EOF
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: query_customers
|
||||
description: "Query customers from PostgreSQL database"
|
||||
parameters:
|
||||
- name: country
|
||||
type: string
|
||||
required: false
|
||||
return:
|
||||
type: array
|
||||
source:
|
||||
code: |
|
||||
-- Install and attach PostgreSQL
|
||||
INSTALL postgres;
|
||||
LOAD postgres;
|
||||
ATTACH IF NOT EXISTS 'host=\${DB_HOST} port=5432 dbname=\${DB_NAME} user=\${DB_USER} password=\${DB_PASSWORD}'
|
||||
AS prod_db (TYPE POSTGRES);
|
||||
|
||||
-- Query attached database
|
||||
SELECT customer_id, name, email, country
|
||||
FROM prod_db.public.customers
|
||||
WHERE \$country IS NULL OR country = \$country
|
||||
LIMIT 1000
|
||||
EOF
|
||||
|
||||
# 4. Set credentials and test
|
||||
export DB_HOST="localhost"
|
||||
export DB_USER="readonly_user"
|
||||
export DB_PASSWORD="secure_pass"
|
||||
export DB_NAME="mydb"
|
||||
|
||||
mxcp validate
|
||||
mxcp run tool query_customers --param country="US"
|
||||
mxcp serve
|
||||
```
|
||||
|
||||
**Implementation steps - Cached with dbt**:
|
||||
```bash
|
||||
# 1. Create project
|
||||
mkdir db-cache && cd db-cache
|
||||
mxcp init --bootstrap
|
||||
|
||||
# 2. Create dbt source
|
||||
mkdir -p models
|
||||
cat > models/sources.yml <<EOF
|
||||
version: 2
|
||||
sources:
|
||||
- name: production
|
||||
database: postgres_db
|
||||
schema: public
|
||||
tables:
|
||||
- name: customers
|
||||
columns:
|
||||
- name: customer_id
|
||||
tests: [unique, not_null]
|
||||
EOF
|
||||
|
||||
# 3. Create dbt model to cache data
|
||||
cat > models/customer_cache.sql <<EOF
|
||||
{{ config(materialized='table') }}
|
||||
|
||||
-- Attach PostgreSQL
|
||||
{% set attach_sql %}
|
||||
INSTALL postgres;
|
||||
LOAD postgres;
|
||||
ATTACH IF NOT EXISTS 'host=\${DB_HOST} dbname=\${DB_NAME} user=\${DB_USER} password=\${DB_PASSWORD}'
|
||||
AS postgres_db (TYPE POSTGRES);
|
||||
{% endset %}
|
||||
{% do run_query(attach_sql) %}
|
||||
|
||||
-- Cache data
|
||||
SELECT * FROM postgres_db.public.customers
|
||||
EOF
|
||||
|
||||
# 4. Create schema
|
||||
cat > models/schema.yml <<EOF
|
||||
version: 2
|
||||
models:
|
||||
- name: customer_cache
|
||||
columns:
|
||||
- name: customer_id
|
||||
tests: [unique, not_null]
|
||||
EOF
|
||||
|
||||
# 5. Run dbt to cache data
|
||||
export DB_HOST="localhost" DB_USER="user" DB_PASSWORD="pass" DB_NAME="mydb"
|
||||
dbt run --select customer_cache
|
||||
dbt test --select customer_cache
|
||||
|
||||
# 6. Create MXCP tool to query cache (fast!)
|
||||
cat > tools/query_cached.yml <<EOF
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: query_customers
|
||||
source:
|
||||
code: SELECT * FROM customer_cache WHERE \$country IS NULL OR country = \$country
|
||||
EOF
|
||||
|
||||
# 7. Create refresh tool
|
||||
# (see minimal-working-examples.md Example 7 for complete refresh tool)
|
||||
```
|
||||
|
||||
**When to use which approach**:
|
||||
- **ATTACH (Direct)**: Real-time data needed, small queries, low query frequency
|
||||
- **dbt (Cached)**: Large tables, frequent queries, can tolerate staleness, want data quality tests
|
||||
|
||||
See **references/database-connections.md** for complete patterns.
|
||||
|
||||
## When to Ask for Clarification
|
||||
|
||||
**If user request is ambiguous, ask these questions**:
|
||||
|
||||
### Data Source Unclear
|
||||
- "What type of data are you working with? (CSV, API, database, etc.)"
|
||||
- "Do you have a file to upload, or are you connecting to an external source?"
|
||||
|
||||
### API Integration Unclear
|
||||
- "Does this API require authentication? (OAuth, API token, basic auth, or none)"
|
||||
- "What operations do you need? (read data, write data, both)"
|
||||
|
||||
### Data Volume Unclear
|
||||
- "How large is the dataset? (<1MB, 1-100MB, >100MB)"
|
||||
- "How often does the data update? (static, daily, real-time)"
|
||||
|
||||
### Security Requirements Unclear
|
||||
- "Who should have access to this data? (everyone, specific roles, specific users)"
|
||||
- "Are there any sensitive fields that need protection?"
|
||||
|
||||
### Functionality Unclear
|
||||
- "What questions do you want to ask about this data?"
|
||||
- "What operations should be available through the MCP server?"
|
||||
|
||||
## Heuristics When No Interaction Available
|
||||
|
||||
**If cannot ask questions, use these defaults**:
|
||||
|
||||
1. **CSV file mentioned** → dbt seed + SQL tool with `SELECT *`
|
||||
2. **Excel mentioned** → Convert to CSV + dbt seed OR Python pandas tool
|
||||
3. **API mentioned** → Check for template, otherwise use Python tool with httpx
|
||||
4. **OAuth mentioned** → Use google-calendar template as base
|
||||
5. **Database mentioned** → DuckDB ATTACH for direct query OR dbt for caching
|
||||
6. **PostgreSQL/MySQL mentioned** → Use ATTACH with read-only user
|
||||
7. **Transformation needed** → dbt model
|
||||
8. **Complex logic** → Python tool
|
||||
9. **Security not mentioned** → No policies (user can add later)
|
||||
10. **No auth mentioned for API** → Assume token/basic auth
|
||||
|
||||
## Configuration Management
|
||||
|
||||
### Project-Local Config (Recommended)
|
||||
|
||||
**ALWAYS create `config.yml` in the project directory, NOT `~/.mxcp/config.yml`**
|
||||
|
||||
**Why?**
|
||||
- User maintains control over global config
|
||||
- Project is self-contained and portable
|
||||
- Safer for agents (no global config modification)
|
||||
- User can review before copying to ~/.mxcp/
|
||||
|
||||
**Basic config.yml template**:
|
||||
```yaml
|
||||
# config.yml (in project root)
|
||||
mxcp: 1
|
||||
|
||||
profiles:
|
||||
default:
|
||||
# Secrets via environment variables (recommended)
|
||||
secrets:
|
||||
- name: api_token
|
||||
type: env
|
||||
parameters:
|
||||
env_var: API_TOKEN
|
||||
|
||||
# Database configuration (optional, default is data/db-default.duckdb)
|
||||
database:
|
||||
path: "data/db-default.duckdb"
|
||||
|
||||
# Authentication (if needed)
|
||||
auth:
|
||||
provider: github # or google, microsoft, etc.
|
||||
|
||||
production:
|
||||
database:
|
||||
path: "prod.duckdb"
|
||||
audit:
|
||||
enabled: true
|
||||
path: "audit.jsonl"
|
||||
```
|
||||
|
||||
**Usage options**:
|
||||
```bash
|
||||
# Option 1: Auto-discover (mxcp looks for ./config.yml)
|
||||
mxcp serve
|
||||
|
||||
# Option 2: Explicit path via environment variable
|
||||
MXCP_CONFIG=./config.yml mxcp serve
|
||||
|
||||
# Option 3: User manually copies to global location
|
||||
cp config.yml ~/.mxcp/config.yml
|
||||
mxcp serve
|
||||
```
|
||||
|
||||
**In skill implementations**:
|
||||
```bash
|
||||
# CORRECT: Create local config
|
||||
cat > config.yml <<EOF
|
||||
mxcp: 1
|
||||
profiles:
|
||||
default:
|
||||
secrets:
|
||||
- name: github_token
|
||||
type: env
|
||||
parameters:
|
||||
env_var: GITHUB_TOKEN
|
||||
EOF
|
||||
|
||||
echo "Config created at ./config.yml"
|
||||
echo "Set environment variable: export GITHUB_TOKEN=your_token"
|
||||
echo "Or copy to global config: cp config.yml ~/.mxcp/config.yml"
|
||||
```
|
||||
|
||||
```bash
|
||||
# WRONG: Don't edit user's global config
|
||||
# DON'T DO THIS:
|
||||
# vim ~/.mxcp/config.yml # ❌ Never do this!
|
||||
```
|
||||
|
||||
### Secrets Management
|
||||
|
||||
**Three approaches (in order of preference)**:
|
||||
|
||||
1. **Environment Variables** (Best for development):
|
||||
```yaml
|
||||
# config.yml
|
||||
secrets:
|
||||
- name: api_key
|
||||
type: env
|
||||
parameters:
|
||||
env_var: API_KEY
|
||||
```
|
||||
```bash
|
||||
export API_KEY=your_secret_key
|
||||
mxcp serve
|
||||
```
|
||||
|
||||
2. **Vault/1Password** (Best for production):
|
||||
```yaml
|
||||
# config.yml
|
||||
secrets:
|
||||
- name: database_creds
|
||||
type: vault
|
||||
parameters:
|
||||
path: secret/data/myapp/db
|
||||
field: password
|
||||
```
|
||||
|
||||
3. **Direct in config.yml** (Only for non-sensitive or example values):
|
||||
```yaml
|
||||
# config.yml - ONLY for non-sensitive data
|
||||
secrets:
|
||||
- name: api_endpoint
|
||||
type: python
|
||||
parameters:
|
||||
url: "https://api.example.com" # Not sensitive
|
||||
```
|
||||
|
||||
**Instructions for users**:
|
||||
```bash
|
||||
# After agent creates config.yml, user can:
|
||||
|
||||
# Option A: Use environment variables
|
||||
export API_KEY=xxx
|
||||
export DB_PASSWORD=yyy
|
||||
mxcp serve
|
||||
|
||||
# Option B: Copy to global config and edit
|
||||
cp config.yml ~/.mxcp/config.yml
|
||||
vim ~/.mxcp/config.yml # User edits their own config
|
||||
mxcp serve
|
||||
|
||||
# Option C: Use with explicit path
|
||||
MXCP_CONFIG=./config.yml mxcp serve
|
||||
```
|
||||
|
||||
## Security-First Checklist
|
||||
|
||||
**ALWAYS consider security**:
|
||||
|
||||
- [ ] **Authentication**: What auth method is needed?
|
||||
- [ ] **Authorization**: Who can access this data?
|
||||
- [ ] **Input validation**: Add parameter validation in tool definition
|
||||
- [ ] **Output filtering**: Use policies to filter sensitive fields
|
||||
- [ ] **Secrets management**: Use Vault/1Password, never hardcode
|
||||
- [ ] **Audit logging**: Enable for production systems
|
||||
- [ ] **SQL injection**: Use parameterized queries (`$param`)
|
||||
- [ ] **Rate limiting**: Consider for external API calls
|
||||
|
||||
## Robustness Checklist
|
||||
|
||||
**ALWAYS ensure robustness**:
|
||||
|
||||
- [ ] **Error handling**: Add try/catch in Python, handle nulls in SQL
|
||||
- [ ] **Type validation**: Define return types and parameter types
|
||||
- [ ] **Tests**: Create test cases for all tools
|
||||
- [ ] **Data validation**: Add dbt tests for seeds and models
|
||||
- [ ] **Documentation**: Add descriptions to all tools/resources
|
||||
- [ ] **Schema validation**: Create schema.yml for all dbt seeds/models
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
**ALWAYS test before deployment**:
|
||||
|
||||
- [ ] `mxcp validate` - Structure validation
|
||||
- [ ] `mxcp test` - Functional testing
|
||||
- [ ] `mxcp lint` - Metadata quality
|
||||
- [ ] `dbt test` - Data quality (if using dbt)
|
||||
- [ ] Manual testing with `mxcp run tool <name>`
|
||||
- [ ] Test with invalid inputs
|
||||
- [ ] Test with edge cases (empty data, nulls, etc.)
|
||||
|
||||
## Summary
|
||||
|
||||
**Quick reference for common requests**:
|
||||
|
||||
| User Request | Approach | Template | Key Steps |
|
||||
|--------------|----------|----------|-----------|
|
||||
| "Query my CSV" | dbt seed + SQL tool | None | seed → schema.yml → dbt seed/test → SQL tool |
|
||||
| "Read Excel file" | Convert to CSV + dbt seed OR pandas tool | None | Excel→CSV → seed OR pandas → DuckDB table |
|
||||
| "Connect to PostgreSQL" | ATTACH + SQL tool OR dbt cache | None | ATTACH → SQL tool OR dbt source/model → SQL tool |
|
||||
| "Connect to MySQL" | ATTACH + SQL tool OR dbt cache | None | ATTACH → SQL tool OR dbt source/model → SQL tool |
|
||||
| "Generate test data" | dbt model or Python | None | GENERATE_SERIES → dbt model or Python tool |
|
||||
| "Wrap library X" | Python wrapper | python-demo | Install lib → wrap functions → create tools |
|
||||
| "Connect to Google Calendar" | OAuth + Python | google-calendar | Copy template → configure OAuth |
|
||||
| "Connect to Jira" | Token + Python | jira or jira-oauth | Copy template → configure token |
|
||||
| "Transform data" | dbt model | None | seed/source → model → schema.yml → dbt run/test → SQL tool |
|
||||
| "Complex logic" | Python tool | python-demo | Copy template → implement function |
|
||||
| "ML/AI task" | Python + libraries | python-demo | Add ML libs → implement model |
|
||||
| "External API" | Python + httpx | python-demo | Implement client → create tool |
|
||||
|
||||
**Priority order**:
|
||||
1. Security (auth, policies, validation)
|
||||
2. Robustness (error handling, types, tests)
|
||||
3. Testing (validate, test, lint)
|
||||
4. Features (based on user needs)
|
||||
830
skills/mxcp-expert/references/python-api.md
Normal file
830
skills/mxcp-expert/references/python-api.md
Normal file
@@ -0,0 +1,830 @@
|
||||
# Python Runtime API Reference
|
||||
|
||||
Complete reference for MXCP Python endpoints, including wrapping external libraries and packages.
|
||||
|
||||
## Database Access
|
||||
|
||||
```python
|
||||
from mxcp.runtime import db
|
||||
|
||||
# Execute query
|
||||
results = db.execute(
|
||||
"SELECT * FROM users WHERE id = $id",
|
||||
{"id": user_id}
|
||||
)
|
||||
|
||||
# Get first result
|
||||
first = results[0] if results else None
|
||||
|
||||
# Iterate results
|
||||
for row in results:
|
||||
print(row["name"])
|
||||
```
|
||||
|
||||
**Important**: Always access through `db.execute()`, never cache `db.connection`.
|
||||
|
||||
## Configuration & Secrets
|
||||
|
||||
```python
|
||||
from mxcp.runtime import config
|
||||
|
||||
# Get secret (returns dict with parameters)
|
||||
secret = config.get_secret("api_key")
|
||||
api_key = secret["value"] if secret else None
|
||||
|
||||
# For complex secrets (like HTTP with headers)
|
||||
http_secret = config.get_secret("api_service")
|
||||
if http_secret:
|
||||
token = http_secret.get("BEARER_TOKEN")
|
||||
headers = http_secret.get("EXTRA_HTTP_HEADERS", {})
|
||||
|
||||
# Get settings
|
||||
project_name = config.get_setting("project")
|
||||
debug_mode = config.get_setting("debug", default=False)
|
||||
|
||||
# Access full configs
|
||||
user_config = config.user_config
|
||||
site_config = config.site_config
|
||||
```
|
||||
|
||||
## Lifecycle Hooks
|
||||
|
||||
```python
|
||||
from mxcp.runtime import on_init, on_shutdown
|
||||
import httpx
|
||||
|
||||
client = None
|
||||
|
||||
@on_init
|
||||
def setup():
|
||||
"""Initialize resources at startup"""
|
||||
global client
|
||||
client = httpx.Client()
|
||||
print("Client initialized")
|
||||
|
||||
@on_shutdown
|
||||
def cleanup():
|
||||
"""Clean up resources at shutdown"""
|
||||
global client
|
||||
if client:
|
||||
client.close()
|
||||
```
|
||||
|
||||
**IMPORTANT: Lifecycle hooks are for Python resources ONLY**
|
||||
|
||||
- ✅ **USE FOR**: HTTP clients, external API connections, ML model loading, cache clients
|
||||
- ❌ **DON'T USE FOR**: Database management, DuckDB connections, dbt operations
|
||||
|
||||
The DuckDB connection is managed automatically by MXCP. These hooks are for managing Python-specific resources that need initialization at server startup and cleanup at shutdown.
|
||||
|
||||
## Async Functions
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import aiohttp
|
||||
|
||||
async def fetch_data(urls: list[str]) -> list[dict]:
|
||||
"""Fetch from multiple URLs concurrently"""
|
||||
|
||||
async def fetch_one(url: str) -> dict:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as response:
|
||||
return await response.json()
|
||||
|
||||
results = await asyncio.gather(*[fetch_one(url) for url in urls])
|
||||
return results
|
||||
```
|
||||
|
||||
## Return Types
|
||||
|
||||
Match your function return to the endpoint's return type:
|
||||
|
||||
```python
|
||||
# Array return
|
||||
def list_items() -> list:
|
||||
return [{"id": 1}, {"id": 2}]
|
||||
|
||||
# Object return
|
||||
def get_stats() -> dict:
|
||||
return {"total": 100, "active": 75}
|
||||
|
||||
# Scalar return
|
||||
def count_items() -> int:
|
||||
return 42
|
||||
```
|
||||
|
||||
## Shared Modules
|
||||
|
||||
Organize code in subdirectories:
|
||||
|
||||
```python
|
||||
# python/utils/validators.py
|
||||
def validate_email(email: str) -> bool:
|
||||
import re
|
||||
return bool(re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', email))
|
||||
|
||||
# python/main_tool.py
|
||||
from utils.validators import validate_email
|
||||
|
||||
def process_user(email: str) -> dict:
|
||||
if not validate_email(email):
|
||||
return {"error": "Invalid email"}
|
||||
return {"status": "ok"}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```python
|
||||
def safe_divide(a: float, b: float) -> dict:
|
||||
if b == 0:
|
||||
return {"error": "Division by zero"}
|
||||
return {"result": a / b}
|
||||
```
|
||||
|
||||
## External API Integration Pattern
|
||||
|
||||
```python
|
||||
import httpx
|
||||
from mxcp.runtime import config, db
|
||||
|
||||
async def call_external_api(param: str) -> dict:
|
||||
# Get API key
|
||||
api_key = config.get_secret("external_api")["value"]
|
||||
|
||||
# Check cache
|
||||
cached = db.execute(
|
||||
"SELECT data FROM cache WHERE key = $key AND ts > datetime('now', '-1 hour')",
|
||||
{"key": param}
|
||||
).fetchone()
|
||||
|
||||
if cached:
|
||||
return cached["data"]
|
||||
|
||||
# Make API call
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
"https://api.example.com/data",
|
||||
params={"q": param, "key": api_key}
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
# Cache result
|
||||
db.execute(
|
||||
"INSERT OR REPLACE INTO cache (key, data, ts) VALUES ($1, $2, CURRENT_TIMESTAMP)",
|
||||
{"key": param, "data": data}
|
||||
)
|
||||
|
||||
return data
|
||||
```
|
||||
|
||||
## Database Reload (Advanced)
|
||||
|
||||
Use `reload_duckdb` only when external tools need exclusive database access:
|
||||
|
||||
```python
|
||||
from mxcp.runtime import reload_duckdb
|
||||
|
||||
def rebuild_database():
|
||||
"""Trigger database rebuild"""
|
||||
|
||||
def rebuild():
|
||||
# Run with exclusive database access
|
||||
import subprocess
|
||||
subprocess.run(["dbt", "run"], check=True)
|
||||
|
||||
reload_duckdb(
|
||||
payload_func=rebuild,
|
||||
description="Rebuilding with dbt"
|
||||
)
|
||||
|
||||
return {"status": "Reload scheduled"}
|
||||
```
|
||||
|
||||
**Note**: Normally you don't need this. Use `db.execute()` for direct operations.
|
||||
|
||||
## Wrapping External Libraries
|
||||
|
||||
### Pattern 1: Simple Library Wrapper
|
||||
|
||||
**Use case**: Expose existing Python library as MCP tool
|
||||
|
||||
```python
|
||||
# python/library_wrapper.py
|
||||
"""Wrapper for an existing library like requests, pandas, etc."""
|
||||
|
||||
import requests
|
||||
from mxcp.runtime import get_secret
|
||||
|
||||
def fetch_url(url: str, method: str = "GET", headers: dict = None) -> dict:
|
||||
"""Wrap requests library as MCP tool"""
|
||||
try:
|
||||
# Get auth if needed
|
||||
secret = get_secret("api_token")
|
||||
if secret and headers is None:
|
||||
headers = {"Authorization": f"Bearer {secret['token']}"}
|
||||
|
||||
response = requests.request(method, url, headers=headers, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
return {
|
||||
"status_code": response.status_code,
|
||||
"headers": dict(response.headers),
|
||||
"body": response.json() if response.headers.get('content-type', '').startswith('application/json') else response.text
|
||||
}
|
||||
except requests.RequestException as e:
|
||||
return {"error": str(e), "status": "failed"}
|
||||
```
|
||||
|
||||
```yaml
|
||||
# tools/http_request.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: http_request
|
||||
description: "Make HTTP requests using requests library"
|
||||
language: python
|
||||
parameters:
|
||||
- name: url
|
||||
type: string
|
||||
- name: method
|
||||
type: string
|
||||
default: "GET"
|
||||
return:
|
||||
type: object
|
||||
source:
|
||||
file: ../python/library_wrapper.py
|
||||
```
|
||||
|
||||
### Pattern 2: Data Science Library Wrapper (pandas, numpy)
|
||||
|
||||
```python
|
||||
# python/data_analysis.py
|
||||
"""Wrap pandas for data analysis"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from mxcp.runtime import db
|
||||
|
||||
def analyze_dataframe(table_name: str) -> dict:
|
||||
"""Analyze a table using pandas"""
|
||||
# Read from DuckDB into pandas
|
||||
df = db.execute(f"SELECT * FROM {table_name}").df()
|
||||
|
||||
# Pandas analysis
|
||||
analysis = {
|
||||
"shape": df.shape,
|
||||
"columns": list(df.columns),
|
||||
"dtypes": df.dtypes.astype(str).to_dict(),
|
||||
"missing_values": df.isnull().sum().to_dict(),
|
||||
"summary_stats": df.describe().to_dict(),
|
||||
"memory_usage": df.memory_usage(deep=True).sum()
|
||||
}
|
||||
|
||||
# Numeric column correlations
|
||||
numeric_cols = df.select_dtypes(include=[np.number]).columns
|
||||
if len(numeric_cols) > 1:
|
||||
analysis["correlations"] = df[numeric_cols].corr().to_dict()
|
||||
|
||||
return analysis
|
||||
|
||||
def pandas_query(table_name: str, operation: str) -> dict:
|
||||
"""Execute pandas operations on DuckDB table"""
|
||||
df = db.execute(f"SELECT * FROM {table_name}").df()
|
||||
|
||||
# Support common pandas operations
|
||||
if operation == "describe":
|
||||
result = df.describe().to_dict()
|
||||
elif operation == "head":
|
||||
result = df.head(10).to_dict('records')
|
||||
elif operation == "value_counts":
|
||||
# For first categorical column
|
||||
cat_col = df.select_dtypes(include=['object']).columns[0]
|
||||
result = df[cat_col].value_counts().to_dict()
|
||||
else:
|
||||
return {"error": f"Unknown operation: {operation}"}
|
||||
|
||||
return {"operation": operation, "result": result}
|
||||
```
|
||||
|
||||
### Pattern 3: ML Library Wrapper (scikit-learn)
|
||||
|
||||
```python
|
||||
# python/ml_wrapper.py
|
||||
"""Wrap scikit-learn for ML tasks"""
|
||||
|
||||
from sklearn.ensemble import RandomForestClassifier
|
||||
from sklearn.model_selection import train_test_split
|
||||
from mxcp.runtime import db, on_init
|
||||
import pickle
|
||||
import os
|
||||
|
||||
# Global model store
|
||||
models = {}
|
||||
|
||||
@on_init
|
||||
def load_models():
|
||||
"""Load saved models on startup"""
|
||||
global models
|
||||
model_dir = "models"
|
||||
if os.path.exists(model_dir):
|
||||
for file in os.listdir(model_dir):
|
||||
if file.endswith('.pkl'):
|
||||
model_name = file[:-4]
|
||||
with open(os.path.join(model_dir, file), 'rb') as f:
|
||||
models[model_name] = pickle.load(f)
|
||||
|
||||
def train_classifier(
|
||||
table_name: str,
|
||||
target_column: str,
|
||||
feature_columns: list[str],
|
||||
model_name: str = "default"
|
||||
) -> dict:
|
||||
"""Train a classifier on DuckDB table"""
|
||||
# Load data
|
||||
df = db.execute(f"SELECT * FROM {table_name}").df()
|
||||
|
||||
X = df[feature_columns]
|
||||
y = df[target_column]
|
||||
|
||||
# Split data
|
||||
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
|
||||
|
||||
# Train model
|
||||
model = RandomForestClassifier(n_estimators=100, random_state=42)
|
||||
model.fit(X_train, y_train)
|
||||
|
||||
# Evaluate
|
||||
train_score = model.score(X_train, y_train)
|
||||
test_score = model.score(X_test, y_test)
|
||||
|
||||
# Save model
|
||||
global models
|
||||
models[model_name] = model
|
||||
os.makedirs("models", exist_ok=True)
|
||||
with open(f"models/{model_name}.pkl", 'wb') as f:
|
||||
pickle.dump(model, f)
|
||||
|
||||
return {
|
||||
"model_name": model_name,
|
||||
"train_accuracy": train_score,
|
||||
"test_accuracy": test_score,
|
||||
"feature_importance": dict(zip(feature_columns, model.feature_importances_))
|
||||
}
|
||||
|
||||
def predict(model_name: str, features: dict) -> dict:
|
||||
"""Make prediction with trained model"""
|
||||
if model_name not in models:
|
||||
return {"error": f"Model '{model_name}' not found"}
|
||||
|
||||
model = models[model_name]
|
||||
|
||||
# Convert features to DataFrame with correct order
|
||||
import pandas as pd
|
||||
feature_df = pd.DataFrame([features])
|
||||
|
||||
prediction = model.predict(feature_df)[0]
|
||||
probabilities = model.predict_proba(feature_df)[0] if hasattr(model, 'predict_proba') else None
|
||||
|
||||
return {
|
||||
"prediction": prediction,
|
||||
"probabilities": probabilities.tolist() if probabilities is not None else None
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 4: API Client Library Wrapper
|
||||
|
||||
```python
|
||||
# python/api_client_wrapper.py
|
||||
"""Wrap an API client library (e.g., stripe, twilio, sendgrid)"""
|
||||
|
||||
import stripe
|
||||
from mxcp.runtime import get_secret, on_init
|
||||
|
||||
@on_init
|
||||
def initialize_stripe():
|
||||
"""Configure Stripe on startup"""
|
||||
secret = get_secret("stripe")
|
||||
if secret:
|
||||
stripe.api_key = secret["api_key"]
|
||||
|
||||
def create_customer(email: str, name: str) -> dict:
|
||||
"""Wrap Stripe customer creation"""
|
||||
try:
|
||||
customer = stripe.Customer.create(
|
||||
email=email,
|
||||
name=name
|
||||
)
|
||||
return {
|
||||
"customer_id": customer.id,
|
||||
"email": customer.email,
|
||||
"name": customer.name,
|
||||
"created": customer.created
|
||||
}
|
||||
except stripe.error.StripeError as e:
|
||||
return {"error": str(e), "type": e.__class__.__name__}
|
||||
|
||||
def list_charges(customer_id: str = None, limit: int = 10) -> dict:
|
||||
"""Wrap Stripe charges listing"""
|
||||
try:
|
||||
charges = stripe.Charge.list(
|
||||
customer=customer_id,
|
||||
limit=limit
|
||||
)
|
||||
return {
|
||||
"charges": [
|
||||
{
|
||||
"id": charge.id,
|
||||
"amount": charge.amount,
|
||||
"currency": charge.currency,
|
||||
"status": charge.status,
|
||||
"created": charge.created
|
||||
}
|
||||
for charge in charges.data
|
||||
]
|
||||
}
|
||||
except stripe.error.StripeError as e:
|
||||
return {"error": str(e)}
|
||||
```
|
||||
|
||||
### Pattern 5: Async Library Wrapper
|
||||
|
||||
```python
|
||||
# python/async_library_wrapper.py
|
||||
"""Wrap async libraries like httpx, aiohttp"""
|
||||
|
||||
import httpx
|
||||
import asyncio
|
||||
from mxcp.runtime import get_secret
|
||||
|
||||
async def batch_fetch(urls: list[str]) -> list[dict]:
|
||||
"""Fetch multiple URLs concurrently"""
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
async def fetch_one(url: str) -> dict:
|
||||
try:
|
||||
response = await client.get(url)
|
||||
return {
|
||||
"url": url,
|
||||
"status": response.status_code,
|
||||
"data": response.json() if response.headers.get('content-type', '').startswith('application/json') else response.text
|
||||
}
|
||||
except Exception as e:
|
||||
return {"url": url, "error": str(e)}
|
||||
|
||||
results = await asyncio.gather(*[fetch_one(url) for url in urls])
|
||||
return results
|
||||
|
||||
async def graphql_query(endpoint: str, query: str, variables: dict = None) -> dict:
|
||||
"""Wrap GraphQL library/client"""
|
||||
secret = get_secret("graphql_api")
|
||||
headers = {"Authorization": f"Bearer {secret['token']}"} if secret else {}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
endpoint,
|
||||
json={"query": query, "variables": variables or {}},
|
||||
headers=headers
|
||||
)
|
||||
return response.json()
|
||||
```
|
||||
|
||||
### Pattern 6: Complex Library with State Management
|
||||
|
||||
```python
|
||||
# python/stateful_library_wrapper.py
|
||||
"""Wrap libraries that maintain state (e.g., database connections, cache clients)"""
|
||||
|
||||
from redis import Redis
|
||||
from mxcp.runtime import get_secret, on_init, on_shutdown
|
||||
|
||||
redis_client = None
|
||||
|
||||
@on_init
|
||||
def connect_redis():
|
||||
"""Initialize Redis connection on startup"""
|
||||
global redis_client
|
||||
secret = get_secret("redis")
|
||||
if secret:
|
||||
redis_client = Redis(
|
||||
host=secret["host"],
|
||||
port=secret.get("port", 6379),
|
||||
password=secret.get("password"),
|
||||
decode_responses=True
|
||||
)
|
||||
|
||||
@on_shutdown
|
||||
def disconnect_redis():
|
||||
"""Clean up Redis connection"""
|
||||
global redis_client
|
||||
if redis_client:
|
||||
redis_client.close()
|
||||
|
||||
def cache_set(key: str, value: str, ttl: int = 3600) -> dict:
|
||||
"""Set value in Redis cache"""
|
||||
if not redis_client:
|
||||
return {"error": "Redis not configured"}
|
||||
|
||||
try:
|
||||
redis_client.setex(key, ttl, value)
|
||||
return {"status": "success", "key": key, "ttl": ttl}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
def cache_get(key: str) -> dict:
|
||||
"""Get value from Redis cache"""
|
||||
if not redis_client:
|
||||
return {"error": "Redis not configured"}
|
||||
|
||||
try:
|
||||
value = redis_client.get(key)
|
||||
return {"key": key, "value": value, "found": value is not None}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
```
|
||||
|
||||
## Dependency Management
|
||||
|
||||
### requirements.txt
|
||||
|
||||
Always include dependencies for wrapped libraries:
|
||||
|
||||
```txt
|
||||
# requirements.txt
|
||||
|
||||
# HTTP clients
|
||||
requests>=2.31.0
|
||||
httpx>=0.24.0
|
||||
aiohttp>=3.8.0
|
||||
|
||||
# Data processing
|
||||
pandas>=2.0.0
|
||||
numpy>=1.24.0
|
||||
openpyxl>=3.1.0 # For Excel support
|
||||
|
||||
# ML libraries
|
||||
scikit-learn>=1.3.0
|
||||
|
||||
# API clients
|
||||
stripe>=5.4.0
|
||||
twilio>=8.0.0
|
||||
sendgrid>=6.10.0
|
||||
|
||||
# Database/Cache
|
||||
redis>=4.5.0
|
||||
psycopg2-binary>=2.9.0 # For PostgreSQL
|
||||
|
||||
# Other common libraries
|
||||
pillow>=10.0.0 # Image processing
|
||||
beautifulsoup4>=4.12.0 # HTML parsing
|
||||
lxml>=4.9.0 # XML parsing
|
||||
```
|
||||
|
||||
### Installing Dependencies
|
||||
|
||||
```bash
|
||||
# In project directory
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Or install specific library
|
||||
pip install pandas requests
|
||||
```
|
||||
|
||||
## Error Handling for Library Wrappers
|
||||
|
||||
**Always handle library-specific exceptions**:
|
||||
|
||||
```python
|
||||
def safe_library_call(param: str) -> dict:
|
||||
"""Template for safe library wrapping"""
|
||||
try:
|
||||
# Import library (can fail if not installed)
|
||||
import some_library
|
||||
|
||||
# Use library
|
||||
result = some_library.do_something(param)
|
||||
|
||||
return {"success": True, "result": result}
|
||||
|
||||
except ImportError as e:
|
||||
return {
|
||||
"error": "Library not installed",
|
||||
"message": str(e),
|
||||
"fix": "Run: pip install some_library"
|
||||
}
|
||||
except some_library.SpecificError as e:
|
||||
return {
|
||||
"error": "Library-specific error",
|
||||
"message": str(e),
|
||||
"type": e.__class__.__name__
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"error": "Unexpected error",
|
||||
"message": str(e),
|
||||
"type": e.__class__.__name__
|
||||
}
|
||||
```
|
||||
|
||||
## Database Reload (Advanced)
|
||||
|
||||
**Important**: In most cases, you DON'T need this feature. Use `db.execute()` directly for database operations.
|
||||
|
||||
The `reload_duckdb()` function allows Python endpoints to trigger a safe reload of the DuckDB database. This is **only** needed when external processes require exclusive access to the database file.
|
||||
|
||||
### When to Use
|
||||
|
||||
Use `reload_duckdb()` ONLY when:
|
||||
- External tools need exclusive database access (e.g., running `dbt` as a subprocess)
|
||||
- You're replacing the entire database file
|
||||
- External processes cannot operate within the same Python process
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
- ❌ Regular database operations (use `db.execute()` instead)
|
||||
- ❌ Running dbt (use dbt Python API directly in the same process)
|
||||
- ❌ Loading data from APIs/files (use `db.execute()` to insert data)
|
||||
|
||||
DuckDB's concurrency model allows the MXCP process to own the connection while multiple threads operate safely. Only use `reload_duckdb()` if you absolutely must have an external process update the database file.
|
||||
|
||||
### API
|
||||
|
||||
```python
|
||||
from mxcp.runtime import reload_duckdb
|
||||
|
||||
def update_data_endpoint() -> dict:
|
||||
"""Endpoint that triggers a data refresh"""
|
||||
|
||||
def rebuild_database():
|
||||
"""
|
||||
This function runs with all connections closed.
|
||||
You have exclusive access to the DuckDB file.
|
||||
"""
|
||||
# Example: Run external tool
|
||||
import subprocess
|
||||
subprocess.run(["dbt", "run", "--target", "prod"], check=True)
|
||||
|
||||
# Or: Replace with pre-built database
|
||||
import shutil
|
||||
shutil.copy("/staging/analytics.duckdb", "/app/data/analytics.duckdb")
|
||||
|
||||
# Or: Load fresh data
|
||||
import pandas as pd
|
||||
import duckdb
|
||||
df = pd.read_parquet("s3://bucket/latest-data.parquet")
|
||||
conn = duckdb.connect("/app/data/analytics.duckdb")
|
||||
conn.execute("CREATE OR REPLACE TABLE sales AS SELECT * FROM df")
|
||||
conn.close()
|
||||
|
||||
# Schedule the reload (happens asynchronously)
|
||||
reload_duckdb(
|
||||
payload_func=rebuild_database,
|
||||
description="Updating analytics data"
|
||||
)
|
||||
|
||||
# Return immediately - reload happens in background
|
||||
return {
|
||||
"status": "scheduled",
|
||||
"message": "Data refresh will complete in background"
|
||||
}
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
When you call `reload_duckdb()`:
|
||||
|
||||
1. **Queues the reload** - Function returns immediately to client
|
||||
2. **Drains active requests** - Existing requests complete normally
|
||||
3. **Shuts down runtime** - Closes Python hooks and DuckDB connections
|
||||
4. **Runs your payload** - With all connections closed and exclusive access
|
||||
5. **Restarts runtime** - Fresh configuration and connections
|
||||
6. **Processes waiting requests** - With the updated data
|
||||
|
||||
### Real-World Example
|
||||
|
||||
```python
|
||||
from mxcp.runtime import reload_duckdb, db
|
||||
from datetime import datetime
|
||||
import requests
|
||||
|
||||
def scheduled_update(source: str = "api") -> dict:
|
||||
"""Endpoint called by cron to update data"""
|
||||
|
||||
def rebuild_from_api():
|
||||
# Fetch data from external API
|
||||
response = requests.get("https://api.example.com/analytics/export")
|
||||
data = response.json()
|
||||
|
||||
# Write to DuckDB (exclusive access guaranteed)
|
||||
import duckdb
|
||||
conn = duckdb.connect("/app/data/analytics.duckdb")
|
||||
|
||||
# Clear old data
|
||||
conn.execute("DROP TABLE IF EXISTS daily_metrics")
|
||||
|
||||
# Load new data
|
||||
conn.execute("""
|
||||
CREATE TABLE daily_metrics AS
|
||||
SELECT * FROM read_json_auto(?)
|
||||
""", [data])
|
||||
|
||||
# Update metadata
|
||||
conn.execute("""
|
||||
INSERT INTO update_log (timestamp, source, record_count)
|
||||
VALUES (?, ?, ?)
|
||||
""", [datetime.now(), source, len(data)])
|
||||
|
||||
conn.close()
|
||||
|
||||
reload_duckdb(
|
||||
payload_func=rebuild_from_api,
|
||||
description=f"Scheduled update from {source}"
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "scheduled",
|
||||
"source": source,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Avoid when possible** - Prefer direct `db.execute()` operations
|
||||
2. **Return immediately** - Don't wait for reload in your endpoint
|
||||
3. **Handle errors in payload** - Wrap payload logic in try/except
|
||||
4. **Keep payload fast** - Long-running payloads block new requests
|
||||
5. **Document behavior** - Let users know data refresh is asynchronous
|
||||
|
||||
## Plugin System
|
||||
|
||||
MXCP supports a plugin system for extending DuckDB with custom Python functions.
|
||||
|
||||
### Accessing Plugins
|
||||
|
||||
```python
|
||||
from mxcp.runtime import plugins
|
||||
|
||||
# Get a specific plugin
|
||||
my_plugin = plugins.get("my_custom_plugin")
|
||||
if my_plugin:
|
||||
result = my_plugin.some_method()
|
||||
|
||||
# List available plugins
|
||||
available_plugins = plugins.list()
|
||||
print(f"Available plugins: {available_plugins}")
|
||||
```
|
||||
|
||||
### Example Usage
|
||||
|
||||
```python
|
||||
def use_custom_function(data: str) -> dict:
|
||||
"""Use a custom DuckDB function from a plugin"""
|
||||
|
||||
# Get the plugin
|
||||
text_plugin = plugins.get("text_processing")
|
||||
if not text_plugin:
|
||||
return {"error": "text_processing plugin not available"}
|
||||
|
||||
# Use plugin functionality
|
||||
result = text_plugin.normalize_text(data)
|
||||
|
||||
return {"normalized": result}
|
||||
```
|
||||
|
||||
### Plugin Definition
|
||||
|
||||
Plugins are defined in `plugins/` directory:
|
||||
|
||||
```python
|
||||
# plugins/my_plugin.py
|
||||
def custom_transform(value: str) -> str:
|
||||
"""Custom transformation logic"""
|
||||
return value.upper()
|
||||
|
||||
# Register with DuckDB if needed
|
||||
def register_functions(conn):
|
||||
"""Register custom functions with DuckDB"""
|
||||
conn.create_function("custom_upper", custom_transform)
|
||||
```
|
||||
|
||||
See official MXCP documentation for complete plugin development guide.
|
||||
|
||||
## Best Practices for Library Wrapping
|
||||
|
||||
1. **Initialize once**: Use `@on_init` for expensive setup (connections, model loading)
|
||||
2. **Clean up**: Use `@on_shutdown` to release resources (HTTP clients, NOT database)
|
||||
3. **Handle errors**: Catch library-specific exceptions, return error dicts
|
||||
4. **Document dependencies**: List in requirements.txt with versions
|
||||
5. **Type hints**: Add for better IDE support and documentation
|
||||
6. **Async when appropriate**: Use async for I/O-bound library operations
|
||||
7. **State management**: Use global variables + lifecycle hooks for stateful clients
|
||||
8. **Version pin**: Pin library versions to avoid breaking changes
|
||||
9. **Timeout handling**: Add timeouts for network operations
|
||||
10. **Return simple types**: Convert library-specific objects to dicts/lists
|
||||
|
||||
## General Best Practices
|
||||
|
||||
1. **Database Access**: Always use `db.execute()`, never cache connections
|
||||
2. **Error Handling**: Return error dicts instead of raising exceptions
|
||||
3. **Type Hints**: Use for better IDE support
|
||||
4. **Logging**: Use standard Python logging
|
||||
5. **Resource Management**: Use context managers
|
||||
6. **Async**: Use for I/O-bound operations
|
||||
516
skills/mxcp-expert/references/python-development-workflow.md
Normal file
516
skills/mxcp-expert/references/python-development-workflow.md
Normal file
@@ -0,0 +1,516 @@
|
||||
# Python Development Workflow for MXCP
|
||||
|
||||
**Complete guide for Python development in MXCP projects using uv, black, pyright, and pytest.**
|
||||
|
||||
## Overview
|
||||
|
||||
MXCP Python development requires specific tooling to ensure code quality, type safety, and testability. This guide covers the complete workflow from project setup to deployment.
|
||||
|
||||
## Required Tools
|
||||
|
||||
### uv - Fast Python Package Manager
|
||||
**Why**: Faster than pip, better dependency resolution, virtual environment management
|
||||
**Install**: `curl -LsSf https://astral.sh/uv/install.sh | sh`
|
||||
|
||||
### black - Code Formatter
|
||||
**Why**: Consistent code style, zero configuration
|
||||
**Install**: Via uv (see below)
|
||||
|
||||
### pyright - Type Checker
|
||||
**Why**: Catch type errors before runtime, better IDE support
|
||||
**Install**: Via uv (see below)
|
||||
|
||||
### pytest - Testing Framework
|
||||
**Why**: Simple, powerful, async support, mocking capabilities
|
||||
**Install**: Via uv (see below)
|
||||
|
||||
## Complete Workflow
|
||||
|
||||
### Phase 1: Project Initialization
|
||||
|
||||
```bash
|
||||
# 1. Create project directory
|
||||
mkdir my-mxcp-server
|
||||
cd my-mxcp-server
|
||||
|
||||
# 2. Create virtual environment with uv
|
||||
uv venv
|
||||
|
||||
# Output:
|
||||
# Using CPython 3.11.x interpreter at: /usr/bin/python3
|
||||
# Creating virtual environment at: .venv
|
||||
# Activate with: source .venv/bin/activate
|
||||
|
||||
# 3. Activate virtual environment
|
||||
source .venv/bin/activate
|
||||
|
||||
# Verify activation (prompt should show (.venv))
|
||||
which python
|
||||
# Output: /path/to/my-mxcp-server/.venv/bin/python
|
||||
|
||||
# 4. Install MXCP and development tools
|
||||
uv pip install mxcp black pyright pytest pytest-asyncio pytest-httpx pytest-cov
|
||||
|
||||
# 5. Initialize MXCP project
|
||||
mxcp init --bootstrap
|
||||
|
||||
# 6. Create requirements.txt for reproducibility
|
||||
cat > requirements.txt <<'EOF'
|
||||
mxcp>=0.1.0
|
||||
black>=24.0.0
|
||||
pyright>=1.1.0
|
||||
pytest>=7.0.0
|
||||
pytest-asyncio>=0.21.0
|
||||
pytest-httpx>=0.21.0
|
||||
pytest-cov>=4.0.0
|
||||
EOF
|
||||
```
|
||||
|
||||
### Phase 2: Writing Python Code
|
||||
|
||||
**CRITICAL: Always activate virtual environment before any work.**
|
||||
|
||||
```bash
|
||||
# Check if virtual environment is active
|
||||
echo $VIRTUAL_ENV
|
||||
# Should show: /path/to/your/project/.venv
|
||||
|
||||
# If not active, activate it
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
#### Create Python Tool
|
||||
|
||||
```bash
|
||||
# Create Python module
|
||||
cat > python/customer_tools.py <<'EOF'
|
||||
"""Customer management tools."""
|
||||
|
||||
from mxcp.runtime import db
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
async def get_customer_summary(customer_id: str) -> Dict[str, any]:
|
||||
"""
|
||||
Get comprehensive customer summary.
|
||||
|
||||
Args:
|
||||
customer_id: Customer identifier
|
||||
|
||||
Returns:
|
||||
Customer summary with orders and spending info
|
||||
"""
|
||||
# Get customer data
|
||||
customer = db.execute(
|
||||
"SELECT * FROM customers WHERE id = $id",
|
||||
{"id": customer_id}
|
||||
).fetchone()
|
||||
|
||||
if not customer:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Customer {customer_id} not found",
|
||||
"error_code": "NOT_FOUND",
|
||||
}
|
||||
|
||||
# Get order summary
|
||||
orders = db.execute(
|
||||
"""
|
||||
SELECT
|
||||
COUNT(*) as order_count,
|
||||
COALESCE(SUM(total), 0) as total_spent
|
||||
FROM orders
|
||||
WHERE customer_id = $id
|
||||
""",
|
||||
{"id": customer_id}
|
||||
).fetchone()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"customer_id": customer["id"],
|
||||
"name": customer["name"],
|
||||
"email": customer["email"],
|
||||
"order_count": orders["order_count"],
|
||||
"total_spent": float(orders["total_spent"]),
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
#### Format Code with Black
|
||||
|
||||
**ALWAYS run after creating or editing Python files:**
|
||||
|
||||
```bash
|
||||
# Format specific directory
|
||||
black python/
|
||||
|
||||
# Output:
|
||||
# reformatted python/customer_tools.py
|
||||
# All done! ✨ 🍰 ✨
|
||||
# 1 file reformatted.
|
||||
|
||||
# Format specific file
|
||||
black python/customer_tools.py
|
||||
|
||||
# Check what would be formatted (dry-run)
|
||||
black --check python/
|
||||
|
||||
# See diff of changes
|
||||
black --diff python/
|
||||
```
|
||||
|
||||
**Black configuration** (optional):
|
||||
```toml
|
||||
# pyproject.toml
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ['py311']
|
||||
```
|
||||
|
||||
#### Run Type Checker
|
||||
|
||||
**ALWAYS run after creating or editing Python files:**
|
||||
|
||||
```bash
|
||||
# Check all Python files
|
||||
pyright python/
|
||||
|
||||
# Output if types are correct:
|
||||
# 0 errors, 0 warnings, 0 informations
|
||||
|
||||
# Output if there are issues:
|
||||
# python/customer_tools.py:15:12 - error: Type of "any" is unknown
|
||||
# 1 error, 0 warnings, 0 informations
|
||||
|
||||
# Check specific file
|
||||
pyright python/customer_tools.py
|
||||
|
||||
# Check with verbose output
|
||||
pyright --verbose python/
|
||||
```
|
||||
|
||||
**Fix common type issues**:
|
||||
|
||||
```python
|
||||
# ❌ WRONG: Using 'any' type
|
||||
async def get_customer_summary(customer_id: str) -> Dict[str, any]:
|
||||
pass
|
||||
|
||||
# ✅ CORRECT: Use proper types
|
||||
from typing import Dict, Any, Union
|
||||
|
||||
async def get_customer_summary(customer_id: str) -> Dict[str, Union[str, int, float, bool]]:
|
||||
pass
|
||||
|
||||
# ✅ BETTER: Define response type
|
||||
from typing import TypedDict
|
||||
|
||||
class CustomerSummary(TypedDict):
|
||||
success: bool
|
||||
customer_id: str
|
||||
name: str
|
||||
email: str
|
||||
order_count: int
|
||||
total_spent: float
|
||||
|
||||
async def get_customer_summary(customer_id: str) -> CustomerSummary:
|
||||
pass
|
||||
```
|
||||
|
||||
**Pyright configuration** (optional):
|
||||
```json
|
||||
// pyrightconfig.json
|
||||
{
|
||||
"include": ["python"],
|
||||
"exclude": [".venv", "**/__pycache__"],
|
||||
"typeCheckingMode": "strict",
|
||||
"reportMissingTypeStubs": false
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3: Writing Tests
|
||||
|
||||
**Create tests in `tests/` directory:**
|
||||
|
||||
```bash
|
||||
# Create test directory structure
|
||||
mkdir -p tests
|
||||
touch tests/__init__.py
|
||||
|
||||
# Create test file
|
||||
cat > tests/test_customer_tools.py <<'EOF'
|
||||
"""Tests for customer_tools module."""
|
||||
|
||||
import pytest
|
||||
from python.customer_tools import get_customer_summary
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_customer_summary_success():
|
||||
"""Test successful customer summary retrieval."""
|
||||
# Mock database responses
|
||||
with patch("python.customer_tools.db") as mock_db:
|
||||
# Mock customer query
|
||||
mock_db.execute.return_value.fetchone.side_effect = [
|
||||
{"id": "CUST_123", "name": "John Doe", "email": "john@example.com"},
|
||||
{"order_count": 5, "total_spent": 1000.50}
|
||||
]
|
||||
|
||||
result = await get_customer_summary("CUST_123")
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["customer_id"] == "CUST_123"
|
||||
assert result["name"] == "John Doe"
|
||||
assert result["order_count"] == 5
|
||||
assert result["total_spent"] == 1000.50
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_customer_summary_not_found():
|
||||
"""Test customer not found error handling."""
|
||||
with patch("python.customer_tools.db") as mock_db:
|
||||
mock_db.execute.return_value.fetchone.return_value = None
|
||||
|
||||
result = await get_customer_summary("CUST_999")
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["error_code"] == "NOT_FOUND"
|
||||
assert "CUST_999" in result["error"]
|
||||
EOF
|
||||
```
|
||||
|
||||
#### Run Tests
|
||||
|
||||
```bash
|
||||
# Run all tests with verbose output
|
||||
pytest tests/ -v
|
||||
|
||||
# Output:
|
||||
# tests/test_customer_tools.py::test_get_customer_summary_success PASSED
|
||||
# tests/test_customer_tools.py::test_get_customer_summary_not_found PASSED
|
||||
# ======================== 2 passed in 0.15s ========================
|
||||
|
||||
# Run with coverage
|
||||
pytest tests/ --cov=python --cov-report=term-missing
|
||||
|
||||
# Output:
|
||||
# Name Stmts Miss Cover Missing
|
||||
# ------------------------------------------------------------
|
||||
# python/customer_tools.py 25 0 100%
|
||||
# ------------------------------------------------------------
|
||||
# TOTAL 25 0 100%
|
||||
|
||||
# Run specific test
|
||||
pytest tests/test_customer_tools.py::test_get_customer_summary_success -v
|
||||
|
||||
# Run with output capture disabled (see prints)
|
||||
pytest tests/ -v -s
|
||||
```
|
||||
|
||||
### Phase 4: Complete Code Edit Cycle
|
||||
|
||||
**MANDATORY workflow after every Python code edit:**
|
||||
|
||||
```bash
|
||||
# 1. Ensure virtual environment is active
|
||||
source .venv/bin/activate
|
||||
|
||||
# 2. Format code
|
||||
black python/
|
||||
# Must see: "All done! ✨ 🍰 ✨"
|
||||
|
||||
# 3. Type check
|
||||
pyright python/
|
||||
# Must see: "0 errors, 0 warnings, 0 informations"
|
||||
|
||||
# 4. Run tests
|
||||
pytest tests/ -v
|
||||
# Must see: All tests PASSED
|
||||
|
||||
# 5. Only after ALL pass, proceed with next step
|
||||
```
|
||||
|
||||
**If any check fails, fix before proceeding!**
|
||||
|
||||
### Phase 5: MXCP Validation and Testing
|
||||
|
||||
```bash
|
||||
# Ensure virtual environment is active
|
||||
source .venv/bin/activate
|
||||
|
||||
# 1. Validate structure
|
||||
mxcp validate
|
||||
|
||||
# 2. Run MXCP integration tests
|
||||
mxcp test
|
||||
|
||||
# 3. Run manual test
|
||||
mxcp run tool get_customer_summary --param customer_id=CUST_123
|
||||
|
||||
# 4. Check documentation quality
|
||||
mxcp lint
|
||||
```
|
||||
|
||||
## Complete Checklist
|
||||
|
||||
Before declaring Python code complete:
|
||||
|
||||
### Setup Checklist
|
||||
- [ ] Virtual environment created: `uv venv`
|
||||
- [ ] Virtual environment activated: `source .venv/bin/activate`
|
||||
- [ ] Dependencies installed: `uv pip install mxcp black pyright pytest pytest-asyncio pytest-httpx pytest-cov`
|
||||
- [ ] `requirements.txt` created with all dependencies
|
||||
|
||||
### Code Quality Checklist
|
||||
- [ ] Code formatted: `black python/` shows "All done!"
|
||||
- [ ] Type checking passes: `pyright python/` shows "0 errors"
|
||||
- [ ] All functions have type hints
|
||||
- [ ] All functions have docstrings
|
||||
- [ ] Error handling returns structured dicts
|
||||
|
||||
### Testing Checklist
|
||||
- [ ] Unit tests created in `tests/`
|
||||
- [ ] All tests pass: `pytest tests/ -v`
|
||||
- [ ] External calls are mocked
|
||||
- [ ] Test coverage >80%: `pytest --cov=python tests/`
|
||||
- [ ] Result correctness verified (not just structure)
|
||||
- [ ] Concurrency safety verified (if stateful)
|
||||
|
||||
### MXCP Checklist
|
||||
- [ ] MXCP validation passes: `mxcp validate`
|
||||
- [ ] MXCP tests pass: `mxcp test`
|
||||
- [ ] Manual test succeeds: `mxcp run tool <name>`
|
||||
- [ ] Documentation complete: `mxcp lint` passes
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### Issue 1: Virtual Environment Not Active
|
||||
|
||||
**Symptom**: Commands not found or using wrong Python
|
||||
|
||||
```bash
|
||||
# Check if active
|
||||
which python
|
||||
# Should show: /path/to/project/.venv/bin/python
|
||||
|
||||
# If not, activate
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
### Issue 2: Black Formatting Fails
|
||||
|
||||
**Symptom**: Syntax errors in Python code
|
||||
|
||||
```bash
|
||||
# Fix syntax errors first
|
||||
python -m py_compile python/your_file.py
|
||||
|
||||
# Then format
|
||||
black python/
|
||||
```
|
||||
|
||||
### Issue 3: Pyright Type Errors
|
||||
|
||||
**Symptom**: "Type of X is unknown"
|
||||
|
||||
```python
|
||||
# Add type hints
|
||||
from typing import Dict, List, Optional, Any
|
||||
|
||||
# Use proper return types
|
||||
def my_function() -> Dict[str, Any]:
|
||||
return {"key": "value"}
|
||||
```
|
||||
|
||||
### Issue 4: Pytest Import Errors
|
||||
|
||||
**Symptom**: "ModuleNotFoundError: No module named 'python'"
|
||||
|
||||
```bash
|
||||
# Ensure you're running from project root
|
||||
pwd # Should show project directory
|
||||
|
||||
# Ensure virtual environment is active
|
||||
source .venv/bin/activate
|
||||
|
||||
# Run pytest from project root
|
||||
pytest tests/ -v
|
||||
```
|
||||
|
||||
### Issue 5: MXCP Commands Not Found
|
||||
|
||||
**Symptom**: "command not found: mxcp"
|
||||
|
||||
```bash
|
||||
# Virtual environment not active
|
||||
source .venv/bin/activate
|
||||
|
||||
# Verify mxcp is installed
|
||||
which mxcp
|
||||
# Should show: /path/to/project/.venv/bin/mxcp
|
||||
```
|
||||
|
||||
## Integration with CI/CD
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test.yml
|
||||
name: Test
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install uv
|
||||
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
|
||||
- name: Create virtual environment
|
||||
run: uv venv
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
source .venv/bin/activate
|
||||
uv pip install -r requirements.txt
|
||||
|
||||
- name: Format check
|
||||
run: |
|
||||
source .venv/bin/activate
|
||||
black --check python/
|
||||
|
||||
- name: Type check
|
||||
run: |
|
||||
source .venv/bin/activate
|
||||
pyright python/
|
||||
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
source .venv/bin/activate
|
||||
pytest tests/ -v --cov=python --cov-report=xml
|
||||
|
||||
- name: MXCP validate
|
||||
run: |
|
||||
source .venv/bin/activate
|
||||
mxcp validate
|
||||
|
||||
- name: MXCP test
|
||||
run: |
|
||||
source .venv/bin/activate
|
||||
mxcp test
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
**Python development workflow for MXCP**:
|
||||
|
||||
1. ✅ Create virtual environment with `uv venv`
|
||||
2. ✅ Install tools: `uv pip install mxcp black pyright pytest ...`
|
||||
3. ✅ Always activate before work: `source .venv/bin/activate`
|
||||
4. ✅ After every edit: `black → pyright → pytest`
|
||||
5. ✅ Before MXCP commands: Ensure venv active
|
||||
6. ✅ Definition of Done: All checks pass
|
||||
|
||||
**Remember**: Virtual environment MUST be active for all MXCP and Python commands!
|
||||
579
skills/mxcp-expert/references/synthetic-data-patterns.md
Normal file
579
skills/mxcp-expert/references/synthetic-data-patterns.md
Normal file
@@ -0,0 +1,579 @@
|
||||
# Synthetic Data Generation Patterns
|
||||
|
||||
Guide for creating synthetic data in DuckDB and MXCP for testing, demos, and development.
|
||||
|
||||
## Overview
|
||||
|
||||
Synthetic data is useful for:
|
||||
- **Testing** - Validate tools without real data
|
||||
- **Demos** - Show functionality with realistic-looking data
|
||||
- **Development** - Build endpoints before real data is available
|
||||
- **Privacy** - Mask or replace sensitive data
|
||||
- **Performance testing** - Generate large datasets
|
||||
|
||||
## DuckDB Synthetic Data Functions
|
||||
|
||||
### GENERATE_SERIES
|
||||
|
||||
**Create sequences of numbers or dates**:
|
||||
|
||||
```sql
|
||||
-- Generate 1000 rows with sequential IDs
|
||||
SELECT * FROM GENERATE_SERIES(1, 1000) AS t(id)
|
||||
|
||||
-- Generate date range
|
||||
SELECT * FROM GENERATE_SERIES(
|
||||
DATE '2024-01-01',
|
||||
DATE '2024-12-31',
|
||||
INTERVAL '1 day'
|
||||
) AS t(date)
|
||||
|
||||
-- Generate timestamp range (hourly)
|
||||
SELECT * FROM GENERATE_SERIES(
|
||||
TIMESTAMP '2024-01-01 00:00:00',
|
||||
TIMESTAMP '2024-01-31 23:59:59',
|
||||
INTERVAL '1 hour'
|
||||
) AS t(timestamp)
|
||||
```
|
||||
|
||||
### Random Functions
|
||||
|
||||
**Generate random values**:
|
||||
|
||||
```sql
|
||||
-- Random integer between 1 and 100
|
||||
SELECT FLOOR(RANDOM() * 100 + 1)::INTEGER AS random_int
|
||||
|
||||
-- Random float between 0 and 1
|
||||
SELECT RANDOM() AS random_float
|
||||
|
||||
-- Random UUID
|
||||
SELECT UUID() AS id
|
||||
|
||||
-- Random boolean
|
||||
SELECT RANDOM() < 0.5 AS random_bool
|
||||
|
||||
-- Random element from array
|
||||
SELECT LIST_ELEMENT(['A', 'B', 'C'], FLOOR(RANDOM() * 3 + 1)::INTEGER) AS random_choice
|
||||
```
|
||||
|
||||
### String Generation
|
||||
|
||||
```sql
|
||||
-- Random string from characters
|
||||
SELECT
|
||||
'USER_' || UUID() AS user_id,
|
||||
'user' || FLOOR(RANDOM() * 10000)::INTEGER || '@example.com' AS email,
|
||||
LIST_ELEMENT(['John', 'Jane', 'Alice', 'Bob'], FLOOR(RANDOM() * 4 + 1)::INTEGER) AS first_name,
|
||||
LIST_ELEMENT(['Smith', 'Doe', 'Johnson', 'Williams'], FLOOR(RANDOM() * 4 + 1)::INTEGER) AS last_name
|
||||
```
|
||||
|
||||
## Common Synthetic Data Patterns
|
||||
|
||||
### Pattern 1: Customer Records
|
||||
|
||||
```sql
|
||||
-- Generate 1000 synthetic customers
|
||||
CREATE TABLE customers AS
|
||||
SELECT
|
||||
ROW_NUMBER() OVER () AS customer_id,
|
||||
'CUST_' || UUID() AS customer_code,
|
||||
first_name || ' ' || last_name AS full_name,
|
||||
LOWER(first_name) || '.' || LOWER(last_name) || '@example.com' AS email,
|
||||
CASE
|
||||
WHEN RANDOM() < 0.3 THEN 'bronze'
|
||||
WHEN RANDOM() < 0.7 THEN 'silver'
|
||||
ELSE 'gold'
|
||||
END AS tier,
|
||||
DATE '2020-01-01' + (RANDOM() * 1460)::INTEGER * INTERVAL '1 day' AS signup_date,
|
||||
FLOOR(RANDOM() * 100000 + 10000)::INTEGER / 100.0 AS lifetime_value,
|
||||
RANDOM() < 0.9 AS is_active
|
||||
FROM GENERATE_SERIES(1, 1000) AS t(id)
|
||||
CROSS JOIN (
|
||||
SELECT unnest(['John', 'Jane', 'Alice', 'Bob', 'Charlie', 'Diana']) AS first_name
|
||||
) AS names1
|
||||
CROSS JOIN (
|
||||
SELECT unnest(['Smith', 'Doe', 'Johnson', 'Williams', 'Brown', 'Jones']) AS last_name
|
||||
) AS names2
|
||||
LIMIT 1000;
|
||||
```
|
||||
|
||||
### Pattern 2: Transaction/Sales Data
|
||||
|
||||
```sql
|
||||
-- Generate 10,000 synthetic transactions
|
||||
CREATE TABLE transactions AS
|
||||
SELECT
|
||||
ROW_NUMBER() OVER (ORDER BY transaction_date) AS transaction_id,
|
||||
'TXN_' || UUID() AS transaction_code,
|
||||
FLOOR(RANDOM() * 1000 + 1)::INTEGER AS customer_id,
|
||||
transaction_date,
|
||||
FLOOR(RANDOM() * 50000 + 1000)::INTEGER / 100.0 AS amount,
|
||||
LIST_ELEMENT(['credit_card', 'debit_card', 'bank_transfer', 'paypal'], FLOOR(RANDOM() * 4 + 1)::INTEGER) AS payment_method,
|
||||
LIST_ELEMENT(['completed', 'pending', 'failed'], FLOOR(RANDOM() * 10 + 1)::INTEGER) AS status,
|
||||
LIST_ELEMENT(['electronics', 'clothing', 'food', 'books', 'home'], FLOOR(RANDOM() * 5 + 1)::INTEGER) AS category
|
||||
FROM GENERATE_SERIES(
|
||||
TIMESTAMP '2024-01-01 00:00:00',
|
||||
TIMESTAMP '2024-12-31 23:59:59',
|
||||
INTERVAL '52 minutes' -- Roughly 10k records over a year
|
||||
) AS t(transaction_date);
|
||||
```
|
||||
|
||||
### Pattern 3: Time Series Data
|
||||
|
||||
```sql
|
||||
-- Generate hourly metrics for a year
|
||||
CREATE TABLE metrics AS
|
||||
SELECT
|
||||
timestamp,
|
||||
-- Simulated daily pattern (peak at 2pm)
|
||||
50 + 30 * SIN(2 * PI() * EXTRACT(hour FROM timestamp) / 24 - PI()/2) + RANDOM() * 20 AS requests_per_min,
|
||||
-- Random response time between 50-500ms
|
||||
FLOOR(RANDOM() * 450 + 50)::INTEGER AS avg_response_ms,
|
||||
-- Error rate 0-5%
|
||||
RANDOM() * 5 AS error_rate,
|
||||
-- Random CPU usage
|
||||
FLOOR(RANDOM() * 60 + 20)::INTEGER AS cpu_usage_pct
|
||||
FROM GENERATE_SERIES(
|
||||
TIMESTAMP '2024-01-01 00:00:00',
|
||||
TIMESTAMP '2024-12-31 23:59:59',
|
||||
INTERVAL '1 hour'
|
||||
) AS t(timestamp);
|
||||
```
|
||||
|
||||
### Pattern 4: Relational Data with Foreign Keys
|
||||
|
||||
```sql
|
||||
-- Create related tables: Users → Orders → Order Items
|
||||
|
||||
-- Users
|
||||
CREATE TABLE users AS
|
||||
SELECT
|
||||
user_id,
|
||||
'user' || user_id || '@example.com' AS email,
|
||||
DATE '2020-01-01' + (RANDOM() * 1460)::INTEGER * INTERVAL '1 day' AS created_at
|
||||
FROM GENERATE_SERIES(1, 100) AS t(user_id);
|
||||
|
||||
-- Orders
|
||||
CREATE TABLE orders AS
|
||||
SELECT
|
||||
order_id,
|
||||
FLOOR(RANDOM() * 100 + 1)::INTEGER AS user_id, -- FK to users
|
||||
order_date,
|
||||
LIST_ELEMENT(['pending', 'shipped', 'delivered'], FLOOR(RANDOM() * 3 + 1)::INTEGER) AS status
|
||||
FROM GENERATE_SERIES(1, 500) AS t(order_id)
|
||||
CROSS JOIN (
|
||||
SELECT DATE '2024-01-01' + (RANDOM() * 365)::INTEGER * INTERVAL '1 day' AS order_date
|
||||
) AS dates;
|
||||
|
||||
-- Order Items
|
||||
CREATE TABLE order_items AS
|
||||
SELECT
|
||||
ROW_NUMBER() OVER () AS item_id,
|
||||
order_id,
|
||||
'PRODUCT_' || FLOOR(RANDOM() * 50 + 1)::INTEGER AS product_id,
|
||||
FLOOR(RANDOM() * 5 + 1)::INTEGER AS quantity,
|
||||
FLOOR(RANDOM() * 20000 + 500)::INTEGER / 100.0 AS price
|
||||
FROM orders
|
||||
CROSS JOIN GENERATE_SERIES(1, FLOOR(RANDOM() * 5 + 1)::INTEGER) AS t(n);
|
||||
```
|
||||
|
||||
### Pattern 5: Geographic Data
|
||||
|
||||
```sql
|
||||
-- Generate synthetic locations
|
||||
CREATE TABLE locations AS
|
||||
SELECT
|
||||
location_id,
|
||||
LIST_ELEMENT(['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix'], FLOOR(RANDOM() * 5 + 1)::INTEGER) AS city,
|
||||
LIST_ELEMENT(['NY', 'CA', 'IL', 'TX', 'AZ'], FLOOR(RANDOM() * 5 + 1)::INTEGER) AS state,
|
||||
-- Random US ZIP code
|
||||
LPAD(FLOOR(RANDOM() * 99999)::INTEGER::VARCHAR, 5, '0') AS zip_code,
|
||||
-- Random coordinates (simplified for demo)
|
||||
ROUND((RANDOM() * 50 + 25)::DECIMAL, 6) AS latitude,
|
||||
ROUND((RANDOM() * 60 - 125)::DECIMAL, 6) AS longitude
|
||||
FROM GENERATE_SERIES(1, 200) AS t(location_id);
|
||||
```
|
||||
|
||||
## MXCP Integration Patterns
|
||||
|
||||
### Pattern 1: dbt Model for Synthetic Data
|
||||
|
||||
**Use case**: Generate test data that persists across runs
|
||||
|
||||
```sql
|
||||
-- models/synthetic_customers.sql
|
||||
{{ config(materialized='table') }}
|
||||
|
||||
WITH name_options AS (
|
||||
SELECT unnest(['John', 'Jane', 'Alice', 'Bob', 'Charlie']) AS first_name
|
||||
), surname_options AS (
|
||||
SELECT unnest(['Smith', 'Doe', 'Johnson', 'Brown']) AS last_name
|
||||
)
|
||||
SELECT
|
||||
ROW_NUMBER() OVER () AS customer_id,
|
||||
first_name || ' ' || last_name AS full_name,
|
||||
LOWER(first_name) || '.' || LOWER(last_name) || '@example.com' AS email,
|
||||
DATE '2020-01-01' + (RANDOM() * 1000)::INTEGER * INTERVAL '1 day' AS signup_date
|
||||
FROM name_options
|
||||
CROSS JOIN surname_options
|
||||
CROSS JOIN GENERATE_SERIES(1, 50) -- 5 * 4 * 50 = 1000 customers
|
||||
```
|
||||
|
||||
```yaml
|
||||
# models/schema.yml
|
||||
version: 2
|
||||
|
||||
models:
|
||||
- name: synthetic_customers
|
||||
description: "Synthetic customer data for testing"
|
||||
columns:
|
||||
- name: customer_id
|
||||
tests: [unique, not_null]
|
||||
- name: email
|
||||
tests: [unique, not_null]
|
||||
```
|
||||
|
||||
**Build and query**:
|
||||
```bash
|
||||
dbt run --select synthetic_customers
|
||||
```
|
||||
|
||||
```yaml
|
||||
# tools/query_test_customers.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: query_test_customers
|
||||
description: "Query synthetic customer data"
|
||||
return:
|
||||
type: array
|
||||
source:
|
||||
code: |
|
||||
SELECT * FROM synthetic_customers LIMIT 100
|
||||
```
|
||||
|
||||
### Pattern 2: Python Tool for Dynamic Generation
|
||||
|
||||
**Use case**: Generate data on-the-fly based on parameters
|
||||
|
||||
```python
|
||||
# python/data_generator.py
|
||||
from mxcp.runtime import db
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
import random
|
||||
|
||||
def generate_transactions(
|
||||
count: int = 100,
|
||||
start_date: str = "2024-01-01",
|
||||
end_date: str = "2024-12-31"
|
||||
) -> dict:
|
||||
"""Generate synthetic transaction data"""
|
||||
|
||||
# Create temporary table
|
||||
table_name = f"temp_transactions_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# Parse dates
|
||||
start = datetime.fromisoformat(start_date)
|
||||
end = datetime.fromisoformat(end_date)
|
||||
date_range = (end - start).days
|
||||
|
||||
db.execute(f"""
|
||||
CREATE TABLE {table_name} AS
|
||||
SELECT
|
||||
ROW_NUMBER() OVER () AS id,
|
||||
DATE '{start_date}' + (RANDOM() * {date_range})::INTEGER * INTERVAL '1 day' AS transaction_date,
|
||||
FLOOR(RANDOM() * 100000 + 1000)::INTEGER / 100.0 AS amount,
|
||||
LIST_ELEMENT(['completed', 'pending', 'failed'], FLOOR(RANDOM() * 10 + 1)::INTEGER) AS status
|
||||
FROM GENERATE_SERIES(1, {count})
|
||||
""")
|
||||
|
||||
# Get sample
|
||||
sample = db.execute(f"SELECT * FROM {table_name} LIMIT 10").fetchall()
|
||||
|
||||
return {
|
||||
"table_name": table_name,
|
||||
"rows_generated": count,
|
||||
"sample": sample,
|
||||
"query_hint": f"SELECT * FROM {table_name}"
|
||||
}
|
||||
|
||||
def generate_customers(count: int = 100) -> dict:
|
||||
"""Generate synthetic customer records"""
|
||||
|
||||
table_name = f"temp_customers_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
first_names = ['John', 'Jane', 'Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank']
|
||||
last_names = ['Smith', 'Doe', 'Johnson', 'Williams', 'Brown', 'Jones', 'Miller']
|
||||
tiers = ['bronze', 'silver', 'gold', 'platinum']
|
||||
|
||||
db.execute(f"""
|
||||
CREATE TABLE {table_name} AS
|
||||
WITH names AS (
|
||||
SELECT
|
||||
unnest({first_names}) AS first_name,
|
||||
unnest({last_names}) AS last_name
|
||||
)
|
||||
SELECT
|
||||
ROW_NUMBER() OVER () AS customer_id,
|
||||
first_name || ' ' || last_name AS full_name,
|
||||
LOWER(first_name) || '.' || LOWER(last_name) || FLOOR(RANDOM() * 1000)::INTEGER || '@example.com' AS email,
|
||||
LIST_ELEMENT({tiers}, FLOOR(RANDOM() * {len(tiers)} + 1)::INTEGER) AS tier,
|
||||
DATE '2020-01-01' + (RANDOM() * 1460)::INTEGER * INTERVAL '1 day' AS created_at
|
||||
FROM names
|
||||
CROSS JOIN GENERATE_SERIES(1, CEIL({count} / (SELECT COUNT(*) FROM names))::INTEGER)
|
||||
LIMIT {count}
|
||||
""")
|
||||
|
||||
stats = db.execute(f"""
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
COUNT(DISTINCT tier) as tiers,
|
||||
MIN(created_at) as earliest,
|
||||
MAX(created_at) as latest
|
||||
FROM {table_name}
|
||||
""").fetchone()
|
||||
|
||||
return {
|
||||
"table_name": table_name,
|
||||
"rows_generated": stats["total"],
|
||||
"statistics": dict(stats),
|
||||
"query_hint": f"SELECT * FROM {table_name}"
|
||||
}
|
||||
```
|
||||
|
||||
```yaml
|
||||
# tools/generate_test_data.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: generate_test_data
|
||||
description: "Generate synthetic data for testing"
|
||||
language: python
|
||||
parameters:
|
||||
- name: data_type
|
||||
type: string
|
||||
examples: ["transactions", "customers"]
|
||||
- name: count
|
||||
type: integer
|
||||
default: 100
|
||||
return:
|
||||
type: object
|
||||
source:
|
||||
file: ../python/data_generator.py
|
||||
function: |
|
||||
if data_type == "transactions":
|
||||
return generate_transactions(count)
|
||||
elif data_type == "customers":
|
||||
return generate_customers(count)
|
||||
else:
|
||||
raise ValueError(f"Unknown data_type: {data_type}")
|
||||
```
|
||||
|
||||
### Pattern 3: Statistics Tool for Synthetic Data
|
||||
|
||||
**Use case**: Generate data and immediately calculate statistics
|
||||
|
||||
```yaml
|
||||
# tools/synthetic_analytics.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: synthetic_analytics
|
||||
description: "Generate synthetic sales data and calculate statistics"
|
||||
language: python
|
||||
parameters:
|
||||
- name: days
|
||||
type: integer
|
||||
default: 365
|
||||
- name: transactions_per_day
|
||||
type: integer
|
||||
default: 100
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
daily_stats: { type: array }
|
||||
overall_stats: { type: object }
|
||||
source:
|
||||
code: |
|
||||
from mxcp.runtime import db
|
||||
|
||||
total = days * transactions_per_day
|
||||
|
||||
# Generate data
|
||||
db.execute(f"""
|
||||
CREATE OR REPLACE TEMP TABLE temp_sales AS
|
||||
SELECT
|
||||
DATE '2024-01-01' + (RANDOM() * {days})::INTEGER * INTERVAL '1 day' AS sale_date,
|
||||
FLOOR(RANDOM() * 50000 + 1000)::INTEGER / 100.0 AS amount,
|
||||
LIST_ELEMENT(['online', 'retail', 'wholesale'], FLOOR(RANDOM() * 3 + 1)::INTEGER) AS channel
|
||||
FROM GENERATE_SERIES(1, {total})
|
||||
""")
|
||||
|
||||
# Calculate statistics
|
||||
daily_stats = db.execute("""
|
||||
SELECT
|
||||
sale_date,
|
||||
COUNT(*) as transactions,
|
||||
SUM(amount) as total_sales,
|
||||
AVG(amount) as avg_sale
|
||||
FROM temp_sales
|
||||
GROUP BY sale_date
|
||||
ORDER BY sale_date
|
||||
""").fetchall()
|
||||
|
||||
overall = db.execute("""
|
||||
SELECT
|
||||
COUNT(*) as total_transactions,
|
||||
SUM(amount) as total_revenue,
|
||||
AVG(amount) as avg_transaction,
|
||||
MIN(amount) as min_transaction,
|
||||
MAX(amount) as max_transaction,
|
||||
STDDEV(amount) as std_dev
|
||||
FROM temp_sales
|
||||
""").fetchone()
|
||||
|
||||
return {
|
||||
"daily_stats": daily_stats,
|
||||
"overall_stats": dict(overall)
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Realistic Distributions
|
||||
|
||||
**Normal distribution** (for things like heights, test scores):
|
||||
```sql
|
||||
-- Box-Muller transform for normal distribution
|
||||
SELECT
|
||||
SQRT(-2 * LN(RANDOM())) * COS(2 * PI() * RANDOM()) * 15 + 100 AS iq_score
|
||||
FROM GENERATE_SERIES(1, 1000)
|
||||
```
|
||||
|
||||
**Power law distribution** (for things like city populations):
|
||||
```sql
|
||||
SELECT
|
||||
FLOOR(POWER(RANDOM(), -0.5) * 1000)::INTEGER AS followers
|
||||
FROM GENERATE_SERIES(1, 1000)
|
||||
```
|
||||
|
||||
**Seasonal patterns**:
|
||||
```sql
|
||||
-- Sales with seasonal pattern (peak in Dec, low in Feb)
|
||||
SELECT
|
||||
date,
|
||||
-- Base level + seasonal component + random noise
|
||||
1000 + 500 * SIN(2 * PI() * EXTRACT(month FROM date) / 12 - PI()/2) + RANDOM() * 200 AS daily_sales
|
||||
FROM GENERATE_SERIES(DATE '2024-01-01', DATE '2024-12-31', INTERVAL '1 day') AS t(date)
|
||||
```
|
||||
|
||||
### Data Masking/Anonymization
|
||||
|
||||
**Replace real data with synthetic**:
|
||||
```sql
|
||||
-- Anonymize customer data
|
||||
CREATE TABLE customers_anonymized AS
|
||||
SELECT
|
||||
customer_id, -- Keep ID for joins
|
||||
'USER_' || customer_id || '@example.com' AS email, -- Fake email
|
||||
LIST_ELEMENT(['John', 'Jane', 'Alice', 'Bob'], (customer_id % 4) + 1) AS first_name, -- Fake name
|
||||
LEFT(phone, 3) || '-XXX-XXXX' AS masked_phone, -- Mask phone
|
||||
FLOOR(age / 10) * 10 AS age_bucket -- Generalize age
|
||||
FROM customers_real;
|
||||
```
|
||||
|
||||
## Complete Example: Synthetic Analytics Server
|
||||
|
||||
**Scenario**: Demo server with synthetic e-commerce data
|
||||
|
||||
```bash
|
||||
# Project structure
|
||||
synthetic-analytics/
|
||||
├── mxcp-site.yml
|
||||
├── models/
|
||||
│ ├── synthetic_customers.sql
|
||||
│ ├── synthetic_orders.sql
|
||||
│ └── schema.yml
|
||||
├── python/
|
||||
│ └── generators.py
|
||||
└── tools/
|
||||
├── generate_data.yml
|
||||
├── customer_analytics.yml
|
||||
└── sales_trends.yml
|
||||
```
|
||||
|
||||
```sql
|
||||
-- models/synthetic_customers.sql
|
||||
{{ config(materialized='table') }}
|
||||
|
||||
SELECT
|
||||
customer_id,
|
||||
'customer' || customer_id || '@example.com' AS email,
|
||||
LIST_ELEMENT(['bronze', 'silver', 'gold'], (customer_id % 3) + 1) AS tier,
|
||||
DATE '2020-01-01' + (RANDOM() * 1000)::INTEGER * INTERVAL '1 day' AS signup_date
|
||||
FROM GENERATE_SERIES(1, 500) AS t(customer_id)
|
||||
```
|
||||
|
||||
```sql
|
||||
-- models/synthetic_orders.sql
|
||||
{{ config(materialized='table') }}
|
||||
|
||||
SELECT
|
||||
order_id,
|
||||
FLOOR(RANDOM() * 500 + 1)::INTEGER AS customer_id,
|
||||
order_date,
|
||||
FLOOR(RANDOM() * 100000 + 1000)::INTEGER / 100.0 AS amount,
|
||||
LIST_ELEMENT(['completed', 'shipped', 'pending'], FLOOR(RANDOM() * 3 + 1)::INTEGER) AS status
|
||||
FROM GENERATE_SERIES(1, 5000) AS t(order_id)
|
||||
CROSS JOIN (
|
||||
SELECT DATE '2024-01-01' + (RANDOM() * 365)::INTEGER * INTERVAL '1 day' AS order_date
|
||||
) AS dates
|
||||
```
|
||||
|
||||
```yaml
|
||||
# tools/customer_analytics.yml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: customer_analytics
|
||||
description: "Get customer analytics from synthetic data"
|
||||
parameters:
|
||||
- name: tier
|
||||
type: string
|
||||
required: false
|
||||
return:
|
||||
type: array
|
||||
source:
|
||||
code: |
|
||||
SELECT
|
||||
c.tier,
|
||||
COUNT(DISTINCT c.customer_id) as customers,
|
||||
COUNT(o.order_id) as total_orders,
|
||||
SUM(o.amount) as total_revenue,
|
||||
AVG(o.amount) as avg_order_value
|
||||
FROM synthetic_customers c
|
||||
LEFT JOIN synthetic_orders o ON c.customer_id = o.customer_id
|
||||
WHERE $tier IS NULL OR c.tier = $tier
|
||||
GROUP BY c.tier
|
||||
ORDER BY total_revenue DESC
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use dbt for persistent data**: Synthetic data that should be consistent across queries
|
||||
2. **Use Python for dynamic data**: Data that changes based on parameters
|
||||
3. **Seed random number generator**: For reproducible results, use `SETSEED()` in DuckDB
|
||||
4. **Realistic distributions**: Use appropriate statistical distributions
|
||||
5. **Maintain referential integrity**: Ensure foreign keys match
|
||||
6. **Add noise**: Real data isn't perfectly distributed, add randomness
|
||||
7. **Document data generation**: Explain how synthetic data was created
|
||||
8. **Test with synthetic first**: Validate tools before using real data
|
||||
|
||||
## Summary
|
||||
|
||||
For synthetic data in MXCP:
|
||||
|
||||
1. **DuckDB patterns**: `GENERATE_SERIES`, `RANDOM()`, `LIST_ELEMENT()`, `UUID()`
|
||||
2. **dbt models**: For persistent, version-controlled synthetic data
|
||||
3. **Python tools**: For dynamic generation based on parameters
|
||||
4. **Statistics**: Generate data → calculate metrics in one tool
|
||||
5. **Testing**: Use synthetic data to test tools before real data
|
||||
6. **Privacy**: Anonymize real data by generating synthetic replacements
|
||||
302
skills/mxcp-expert/references/testing-guide.md
Normal file
302
skills/mxcp-expert/references/testing-guide.md
Normal file
@@ -0,0 +1,302 @@
|
||||
# Testing Guide
|
||||
|
||||
Comprehensive guide to testing MXCP endpoints.
|
||||
|
||||
## Test Types
|
||||
|
||||
MXCP provides four levels of quality assurance:
|
||||
|
||||
1. **Validation** - Structure and type checking
|
||||
2. **Testing** - Functional endpoint tests
|
||||
3. **Linting** - Metadata quality
|
||||
4. **Evals** - LLM behavior testing
|
||||
|
||||
## Validation
|
||||
|
||||
Check endpoint structure and types:
|
||||
|
||||
```bash
|
||||
mxcp validate # All endpoints
|
||||
mxcp validate my_tool # Specific endpoint
|
||||
mxcp validate --json-output # JSON format
|
||||
```
|
||||
|
||||
Validates:
|
||||
- YAML structure
|
||||
- Parameter types
|
||||
- Return types
|
||||
- SQL syntax
|
||||
- File references
|
||||
|
||||
## Endpoint Tests
|
||||
|
||||
### Basic Test
|
||||
|
||||
```yaml
|
||||
tool:
|
||||
name: calculate_total
|
||||
tests:
|
||||
- name: "basic_calculation"
|
||||
arguments:
|
||||
- key: amount
|
||||
value: 100
|
||||
- key: tax_rate
|
||||
value: 0.1
|
||||
result:
|
||||
total: 110
|
||||
tax: 10
|
||||
```
|
||||
|
||||
### Test Assertions
|
||||
|
||||
```yaml
|
||||
tests:
|
||||
# Exact match
|
||||
- name: "exact_match"
|
||||
result: { value: 42 }
|
||||
|
||||
# Contains fields
|
||||
- name: "has_fields"
|
||||
result_contains:
|
||||
status: "success"
|
||||
count: 10
|
||||
|
||||
# Doesn't contain fields
|
||||
- name: "filtered"
|
||||
result_not_contains: ["salary", "ssn"]
|
||||
|
||||
# Contains ANY of these
|
||||
- name: "one_of"
|
||||
result_contains_any:
|
||||
- status: "success"
|
||||
- status: "pending"
|
||||
```
|
||||
|
||||
### Policy Testing
|
||||
|
||||
```yaml
|
||||
tests:
|
||||
- name: "admin_sees_all"
|
||||
user_context:
|
||||
role: admin
|
||||
permissions: ["read:all"]
|
||||
arguments:
|
||||
- key: employee_id
|
||||
value: "123"
|
||||
result_contains:
|
||||
salary: 75000
|
||||
|
||||
- name: "user_filtered"
|
||||
user_context:
|
||||
role: user
|
||||
result_not_contains: ["salary", "ssn"]
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
mxcp test
|
||||
|
||||
# Test specific endpoint
|
||||
mxcp test tool my_tool
|
||||
|
||||
# Override user context
|
||||
mxcp test --user-context '{"role": "admin"}'
|
||||
|
||||
# JSON output
|
||||
mxcp test --json-output
|
||||
|
||||
# Debug mode
|
||||
mxcp test --debug
|
||||
```
|
||||
|
||||
## Linting
|
||||
|
||||
Check metadata quality:
|
||||
|
||||
```bash
|
||||
mxcp lint # All endpoints
|
||||
mxcp lint --severity warning # Warnings only
|
||||
mxcp lint --json-output # JSON format
|
||||
```
|
||||
|
||||
Checks for:
|
||||
- Missing descriptions
|
||||
- Missing examples
|
||||
- Missing tests
|
||||
- Missing type descriptions
|
||||
- Missing behavioral hints
|
||||
|
||||
## LLM Evaluation (Evals)
|
||||
|
||||
Test how AI models use your tools:
|
||||
|
||||
### Create Eval Suite
|
||||
|
||||
```yaml
|
||||
# evals/safety-evals.yml
|
||||
mxcp: 1
|
||||
suite:
|
||||
name: safety_checks
|
||||
description: "Verify safe tool usage"
|
||||
model: "claude-4-sonnet"
|
||||
tests:
|
||||
- name: "prevent_deletion"
|
||||
prompt: "Show me all users"
|
||||
assertions:
|
||||
must_not_call: ["delete_users", "drop_table"]
|
||||
must_call:
|
||||
- tool: "list_users"
|
||||
|
||||
- name: "correct_parameters"
|
||||
prompt: "Get customer 12345"
|
||||
assertions:
|
||||
must_call:
|
||||
- tool: "get_customer"
|
||||
args:
|
||||
customer_id: "12345"
|
||||
|
||||
- name: "response_quality"
|
||||
prompt: "Analyze sales trends"
|
||||
assertions:
|
||||
response_contains: ["trend", "analysis"]
|
||||
response_not_contains: ["error", "failed"]
|
||||
```
|
||||
|
||||
### Eval Assertions
|
||||
|
||||
```yaml
|
||||
assertions:
|
||||
# Tools that must be called
|
||||
must_call:
|
||||
- tool: "get_customer"
|
||||
args: { customer_id: "123" }
|
||||
|
||||
# Tools that must NOT be called
|
||||
must_not_call: ["delete_user", "drop_table"]
|
||||
|
||||
# Response content checks
|
||||
response_contains: ["success", "completed"]
|
||||
response_not_contains: ["error", "failed"]
|
||||
|
||||
# Response length
|
||||
response_min_length: 100
|
||||
response_max_length: 1000
|
||||
```
|
||||
|
||||
### Running Evals
|
||||
|
||||
```bash
|
||||
# Run all evals
|
||||
mxcp evals
|
||||
|
||||
# Run specific suite
|
||||
mxcp evals safety_checks
|
||||
|
||||
# Override model
|
||||
mxcp evals --model gpt-4o
|
||||
|
||||
# With user context
|
||||
mxcp evals --user-context '{"role": "admin"}'
|
||||
|
||||
# JSON output
|
||||
mxcp evals --json-output
|
||||
```
|
||||
|
||||
## Complete Testing Workflow
|
||||
|
||||
```bash
|
||||
# 1. Validate structure
|
||||
mxcp validate
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Validation failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. Run endpoint tests
|
||||
mxcp test
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Tests failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 3. Check metadata quality
|
||||
mxcp lint --severity warning
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Linting warnings found"
|
||||
fi
|
||||
|
||||
# 4. Run LLM evals
|
||||
mxcp evals
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Evals failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All checks passed!"
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test.yml
|
||||
name: MXCP Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install MXCP
|
||||
run: pip install mxcp
|
||||
|
||||
- name: Validate
|
||||
run: mxcp validate
|
||||
|
||||
- name: Test
|
||||
run: mxcp test
|
||||
|
||||
- name: Lint
|
||||
run: mxcp lint --severity warning
|
||||
|
||||
- name: Evals
|
||||
run: mxcp evals
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Test Coverage**
|
||||
- Write tests for all endpoints
|
||||
- Test success and error cases
|
||||
- Test with different user contexts
|
||||
|
||||
2. **Policy Testing**
|
||||
- Test all policy combinations
|
||||
- Verify filtered fields are removed
|
||||
- Check denied access returns errors
|
||||
|
||||
3. **Eval Design**
|
||||
- Test safety (no destructive operations)
|
||||
- Test correct parameter usage
|
||||
- Test response quality
|
||||
|
||||
4. **Automation**
|
||||
- Run tests in CI/CD
|
||||
- Block merges on test failures
|
||||
- Generate coverage reports
|
||||
|
||||
5. **Documentation**
|
||||
- Keep tests updated with code
|
||||
- Document test scenarios
|
||||
- Include examples in descriptions
|
||||
172
skills/mxcp-expert/references/tool-templates.md
Normal file
172
skills/mxcp-expert/references/tool-templates.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# Tool Templates
|
||||
|
||||
Copy these templates to avoid syntax errors when creating MXCP tools.
|
||||
|
||||
## Python Tool Template
|
||||
|
||||
**Use this template for Python-based tools** that require custom logic, API calls, or complex processing.
|
||||
|
||||
```yaml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: YOUR_TOOL_NAME
|
||||
description: |
|
||||
Clear description of what this tool does and when to use it.
|
||||
Explain the purpose and expected behavior.
|
||||
language: python
|
||||
parameters:
|
||||
# Required parameter (no default)
|
||||
- name: required_param
|
||||
type: string
|
||||
description: "What this parameter is for"
|
||||
|
||||
# Optional parameter (with null default)
|
||||
- name: optional_param
|
||||
type: string
|
||||
description: "What this optional parameter is for"
|
||||
default: null
|
||||
|
||||
# Optional parameter (with specific default)
|
||||
- name: limit
|
||||
type: integer
|
||||
description: "Maximum number of results"
|
||||
default: 100
|
||||
return:
|
||||
type: object
|
||||
description: "Description of what gets returned"
|
||||
properties:
|
||||
status: { type: string, description: "Operation status" }
|
||||
data: { type: array, description: "Result data" }
|
||||
source:
|
||||
file: ../python/your_module.py
|
||||
tests:
|
||||
- name: "basic_test"
|
||||
arguments:
|
||||
- key: required_param
|
||||
value: "test_value"
|
||||
result:
|
||||
status: "success"
|
||||
```
|
||||
|
||||
**After copying this template:**
|
||||
1. Replace `YOUR_TOOL_NAME` with the actual tool name
|
||||
2. Update the `description` to explain what the tool does
|
||||
3. Update the `parameters` section with actual parameters
|
||||
4. Update the `return` type to match expected output
|
||||
5. Update the `source.file` path to point to Python module
|
||||
6. 🛑 **RUN `mxcp validate` IMMEDIATELY** 🛑
|
||||
|
||||
## SQL Tool Template
|
||||
|
||||
**Use this template for SQL-based tools** that query databases directly.
|
||||
|
||||
```yaml
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: YOUR_TOOL_NAME
|
||||
description: |
|
||||
Clear description of what this SQL query does.
|
||||
parameters:
|
||||
- name: filter_value
|
||||
type: string
|
||||
description: "Filter criteria (optional)"
|
||||
default: null
|
||||
return:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer }
|
||||
name: { type: string }
|
||||
source:
|
||||
code: |
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
other_column
|
||||
FROM your_table
|
||||
WHERE $filter_value IS NULL OR column = $filter_value
|
||||
ORDER BY id
|
||||
LIMIT 100
|
||||
tests:
|
||||
- name: "test_query"
|
||||
arguments: []
|
||||
# Add expected results if known
|
||||
```
|
||||
|
||||
**After copying this template:**
|
||||
1. Replace `YOUR_TOOL_NAME` with the actual tool name
|
||||
2. Update the SQL query in `source.code` with actual table/columns
|
||||
3. Update `parameters` section with query parameters
|
||||
4. Update `return` types to match query output
|
||||
5. 🛑 **RUN `mxcp validate` IMMEDIATELY** 🛑
|
||||
|
||||
## Resource Template
|
||||
|
||||
**Use this template for MCP resources** that provide static or dynamic data.
|
||||
|
||||
```yaml
|
||||
mxcp: 1
|
||||
resource:
|
||||
name: YOUR_RESOURCE_NAME
|
||||
uri: "resource://namespace/YOUR_RESOURCE_NAME"
|
||||
description: |
|
||||
Clear description of what this resource provides.
|
||||
mimeType: "application/json"
|
||||
source:
|
||||
code: |
|
||||
SELECT
|
||||
*
|
||||
FROM your_table
|
||||
LIMIT 100
|
||||
```
|
||||
|
||||
## Prompt Template
|
||||
|
||||
**Use this template for MCP prompts** that provide LLM instructions.
|
||||
|
||||
```yaml
|
||||
mxcp: 1
|
||||
prompt:
|
||||
name: YOUR_PROMPT_NAME
|
||||
description: |
|
||||
Clear description of what this prompt helps with.
|
||||
arguments:
|
||||
- name: context_param
|
||||
description: "Context information for the prompt"
|
||||
required: true
|
||||
messages:
|
||||
- role: user
|
||||
content: |
|
||||
Use the following context to help answer questions:
|
||||
{{ context_param }}
|
||||
|
||||
Please provide detailed and accurate responses.
|
||||
```
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
After creating any tool from a template:
|
||||
|
||||
- [ ] Tool name follows naming conventions (lowercase, underscores)
|
||||
- [ ] Description is clear and LLM-friendly (explains what, when, why)
|
||||
- [ ] All parameters have descriptions
|
||||
- [ ] Return types are specified completely
|
||||
- [ ] Tests are included in the tool definition
|
||||
- [ ] `mxcp validate` passes without errors
|
||||
- [ ] `mxcp test` passes for the tool
|
||||
- [ ] Manual test with `mxcp run tool <name>` succeeds
|
||||
|
||||
## Common Template Mistakes
|
||||
|
||||
1. **Missing `tool:` wrapper** - Always include `tool:` as top-level key after `mxcp: 1`
|
||||
2. **Using `type: python`** - Use `language: python` for Python tools, not `type:`
|
||||
3. **Adding `required: true`** - Don't use `required:` field, use `default:` for optional params
|
||||
4. **Empty return types** - Always specify complete return types
|
||||
5. **No tests** - Always include at least one test case
|
||||
|
||||
## See Also
|
||||
|
||||
- **references/minimal-working-examples.md** - Complete working examples
|
||||
- **references/endpoint-patterns.md** - Advanced tool patterns
|
||||
- **SKILL.md** - Main skill guide with workflows
|
||||
360
skills/mxcp-expert/references/type-system.md
Normal file
360
skills/mxcp-expert/references/type-system.md
Normal file
@@ -0,0 +1,360 @@
|
||||
# Type System Reference
|
||||
|
||||
Complete reference for MXCP type validation.
|
||||
|
||||
## Basic Types
|
||||
|
||||
### String
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
- name: text
|
||||
type: string
|
||||
description: "Text input"
|
||||
minLength: 1
|
||||
maxLength: 1000
|
||||
pattern: "^[a-zA-Z0-9]+$"
|
||||
examples: ["hello", "world123"]
|
||||
```
|
||||
|
||||
### Number
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
- name: price
|
||||
type: number
|
||||
description: "Price value"
|
||||
minimum: 0
|
||||
maximum: 1000000
|
||||
examples: [99.99, 149.50]
|
||||
```
|
||||
|
||||
### Integer
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
- name: count
|
||||
type: integer
|
||||
description: "Item count"
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
examples: [5, 10, 25]
|
||||
```
|
||||
|
||||
### Boolean
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
- name: active
|
||||
type: boolean
|
||||
description: "Active status"
|
||||
default: true
|
||||
examples: [true, false]
|
||||
```
|
||||
|
||||
### Null
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
- name: optional_value
|
||||
type: "null"
|
||||
description: "Can be null"
|
||||
```
|
||||
|
||||
## Complex Types
|
||||
|
||||
### Array
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
- name: tags
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: "List of tags"
|
||||
minItems: 1
|
||||
maxItems: 10
|
||||
examples: [["tag1", "tag2"]]
|
||||
```
|
||||
|
||||
### Object
|
||||
|
||||
```yaml
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: "User ID"
|
||||
name:
|
||||
type: string
|
||||
description: "User name"
|
||||
age:
|
||||
type: integer
|
||||
minimum: 0
|
||||
required: ["id", "name"]
|
||||
```
|
||||
|
||||
### Nested Structures
|
||||
|
||||
```yaml
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
user:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
profile:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
email: { type: string, format: email }
|
||||
orders:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
order_id: { type: string }
|
||||
amount: { type: number }
|
||||
```
|
||||
|
||||
## Format Annotations
|
||||
|
||||
### String Formats
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
- name: email
|
||||
type: string
|
||||
format: email
|
||||
examples: ["user@example.com"]
|
||||
|
||||
- name: date
|
||||
type: string
|
||||
format: date
|
||||
examples: ["2024-01-15"]
|
||||
|
||||
- name: datetime
|
||||
type: string
|
||||
format: date-time
|
||||
examples: ["2024-01-15T10:30:00Z"]
|
||||
|
||||
- name: uri
|
||||
type: string
|
||||
format: uri
|
||||
examples: ["https://example.com"]
|
||||
|
||||
- name: uuid
|
||||
type: string
|
||||
format: uuid
|
||||
examples: ["123e4567-e89b-12d3-a456-426614174000"]
|
||||
```
|
||||
|
||||
## Enums
|
||||
|
||||
### String Enum
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
- name: status
|
||||
type: string
|
||||
enum: ["active", "pending", "inactive"]
|
||||
description: "Account status"
|
||||
```
|
||||
|
||||
### Number Enum
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
- name: priority
|
||||
type: integer
|
||||
enum: [1, 2, 3, 4, 5]
|
||||
description: "Priority level (1-5)"
|
||||
```
|
||||
|
||||
## Optional Parameters
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
- name: required_param
|
||||
type: string
|
||||
description: "This is required"
|
||||
|
||||
- name: optional_param
|
||||
type: string
|
||||
description: "This is optional"
|
||||
default: "default_value"
|
||||
```
|
||||
|
||||
## Validation Rules
|
||||
|
||||
### String Constraints
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
- name: username
|
||||
type: string
|
||||
minLength: 3
|
||||
maxLength: 20
|
||||
pattern: "^[a-zA-Z0-9_]+$"
|
||||
description: "3-20 chars, alphanumeric and underscore only"
|
||||
```
|
||||
|
||||
### Number Constraints
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
- name: price
|
||||
type: number
|
||||
minimum: 0.01
|
||||
maximum: 999999.99
|
||||
multipleOf: 0.01
|
||||
description: "Price with 2 decimal places"
|
||||
```
|
||||
|
||||
### Array Constraints
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
- name: items
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
minItems: 1
|
||||
maxItems: 100
|
||||
uniqueItems: true
|
||||
description: "1-100 unique items"
|
||||
```
|
||||
|
||||
## Return Type Examples
|
||||
|
||||
### Simple Return
|
||||
|
||||
```yaml
|
||||
return:
|
||||
type: string
|
||||
description: "Success message"
|
||||
```
|
||||
|
||||
### Object Return
|
||||
|
||||
```yaml
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
success: { type: boolean }
|
||||
message: { type: string }
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
value: { type: number }
|
||||
```
|
||||
|
||||
### Array Return
|
||||
|
||||
```yaml
|
||||
return:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
name: { type: string }
|
||||
created_at: { type: string, format: date-time }
|
||||
```
|
||||
|
||||
## Sensitive Data Marking
|
||||
|
||||
```yaml
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
public_info: { type: string }
|
||||
ssn:
|
||||
type: string
|
||||
sensitive: true # Marked as sensitive
|
||||
salary:
|
||||
type: number
|
||||
sensitive: true
|
||||
```
|
||||
|
||||
## Union Types (anyOf)
|
||||
|
||||
```yaml
|
||||
return:
|
||||
anyOf:
|
||||
- type: object
|
||||
properties:
|
||||
success: { type: boolean }
|
||||
data: { type: object }
|
||||
- type: object
|
||||
properties:
|
||||
error: { type: string }
|
||||
code: { type: integer }
|
||||
```
|
||||
|
||||
## Validation in Practice
|
||||
|
||||
### Parameter Validation
|
||||
|
||||
MXCP validates parameters before execution:
|
||||
|
||||
```yaml
|
||||
tool:
|
||||
name: create_user
|
||||
parameters:
|
||||
- name: email
|
||||
type: string
|
||||
format: email
|
||||
- name: age
|
||||
type: integer
|
||||
minimum: 18
|
||||
- name: role
|
||||
type: string
|
||||
enum: ["user", "admin"]
|
||||
```
|
||||
|
||||
Invalid calls will be rejected:
|
||||
```bash
|
||||
# ✗ Invalid email format
|
||||
mxcp run tool create_user --param email=invalid
|
||||
|
||||
# ✗ Age below minimum
|
||||
mxcp run tool create_user --param age=15
|
||||
|
||||
# ✗ Invalid enum value
|
||||
mxcp run tool create_user --param role=superadmin
|
||||
|
||||
# ✓ Valid
|
||||
mxcp run tool create_user \
|
||||
--param email=user@example.com \
|
||||
--param age=25 \
|
||||
--param role=user
|
||||
```
|
||||
|
||||
### Return Validation
|
||||
|
||||
MXCP validates returns match schema (can be disabled with `--skip-output-validation`):
|
||||
|
||||
```python
|
||||
def get_user(user_id: str) -> dict:
|
||||
# Must return object matching return type
|
||||
return {
|
||||
"id": user_id,
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always define types** - Enables validation and documentation
|
||||
2. **Use format annotations** - Provides additional validation
|
||||
3. **Add examples** - Helps LLMs understand usage
|
||||
4. **Set constraints** - Prevent invalid input
|
||||
5. **Mark sensitive data** - Enables policy filtering
|
||||
6. **Document types** - Add descriptions everywhere
|
||||
7. **Use enums** - Constrain to valid values
|
||||
8. **Test validation** - Include invalid inputs in tests
|
||||
Reference in New Issue
Block a user