An MCP (Model Context Protocol) server that provides LLM-friendly access to curated Axiom queries with intelligent formatting and column statistics.
- 🔍 Dynamic Tool Generation: Automatically creates MCP tools from Axiom starred queries
- 🤖 LLM-Optimized Formatting: Results formatted as markdown with CSV data and comprehensive column statistics
- 📊 Smart Data Analysis: Automatic column stats including unique values, examples, and frequency analysis
- 🚀 Dual MCP Modes: Supports both stdio and HTTP server modes
- ⚡ Response Size Management: Warns when responses exceed 20KB to optimize LLM context usage
- 🛡️ Parameter Validation: Type checking and validation for query parameters
go install github.com/roessland/curated-axiom-mcp@latestSet your Axiom token and URL via environment variables:
export AXIOM_TOKEN="your-axiom-token-here"
export AXIOM_URL="https://api.axiom.co" # or https://api.eu.axiom.co for EUOr create a config file at ~/.config/curated-axiom-mcp/config.yaml (no environment variables needed if using config file):
axiom:
token: "your-axiom-token"
url: "https://api.axiom.co"# Stdio mode (for MCP clients like Claude Desktop)
curated-axiom-mcp --stdio
# HTTP server mode (for testing with mcptools)
curated-axiom-mcp --port 8080Install mcptools for testing:
npm install -g @modelcontextprotocol/cliTest the server:
# List available tools
mcptools tools go run main.go --stdio
# Test a manual query
mcptools call run_query --params '{"apl": "[\"activities\"] | where athlete_id == \"12345\" | where _time > ago(24h) | limit 10"}' go run main.go --stdio
# Test a dynamic tool (if you have starred queries)
mcptools call athlete_activity_summary --params '{"athlete_id": "12345", "time_range": "7d"}' go run main.go --stdioThe server automatically converts Axiom starred queries into MCP tools when they contain special metadata comments:
Create a starred query in Axiom with this APL and metadata:
declare query_parameters ( // CuratedAxiomMCP
athlete_id:string = "12345", ///param='{{.AthleteId}}',
time_range:string = "7d" ///param='{{.TimeRange}}'
);
["strava_activities"]
| where athlete_id == athlete_id
| where _time > ago(time_range)
| summarize
total_activities = count(),
total_distance = sum(distance),
avg_speed = avg(average_speed),
activity_types = dcount(sport_type)
by athlete_id
| extend
total_distance_km = round(total_distance / 1000, 2),
avg_speed_kmh = round(avg_speed * 3.6, 2)
| project athlete_id, total_activities, total_distance_km, avg_speed_kmh, activity_types
| limit 1000
// CuratedAxiomMCP:
// ToolName: athlete_activity_summary
// Description: Get activity summary for a specific athlete
// Params:
// - Name: AthleteId
// Type: string
// Example: "12345"
// Description: The athlete's unique identifier
// - Name: TimeRange
// Type: string
// Example: "7d"
// Description: Time range (e.g., 1h, 24h, 7d, 30d)
// Constraints:
// - Only returns activities from the last 90 days
// - Limited to 1000 results for performanceThe starred query must contain YAML metadata in comments:
// CuratedAxiomMCP:
// ToolName: your_tool_name # Required: MCP tool name
// Description: Tool description # Optional: Tool description
// Params: # Required: Parameter definitions
// - Name: ParamName # Parameter name (Go style)
// Type: string # Type: string, int, float, bool
// Example: "example_value" # Example value
// Description: Parameter desc # Parameter description
// Constraints: # Optional: Usage constraints
// - Constraint descriptionParameters must be declared using declare query_parameters:
declare query_parameters ( // CuratedAxiomMCP
param_name:type = default_value, ///param='{{.ParamName}}',
final_param:type = default_value ///param='{{.FinalParam}}'
);The server returns structured markdown with:
Found 42 records in 0.8 s with 6 fields.
## APL
["strava_activities"] | where athlete_id == "12345" | where _time > ago(7d)## Results
athlete_id,total_activities,total_distance_km,avg_speed_kmh,activity_types
string,int,float,float,int
---
12345,15,247.8,18.5,3
67890,8,156.2,16.7,2## Column Stats
**athlete_id** (string)
- First: "12345", Last: "67890"
- Nulls: 0
- Unique: 2 total
- Top values: "12345" (15), "67890" (8)
- Examples: "12345", "67890"
**total_activities** (int)
- First: "15", Last: "8"
- Nulls: 0
- Unique: 2 total
- Top values: "15" (1), "8" (1)
- Examples: "15", "8"| Variable | Description | Default |
|---|---|---|
AXIOM_TOKEN |
Axiom API token (required) | - |
AXIOM_ORG_ID |
Axiom organization ID | - |
AXIOM_URL |
Axiom base URL | https://api.axiom.co |
PORT |
Server port | 5111 |
LOG_LEVEL |
Logging level | info |
Location: ~/.config/curated-axiom-mcp/config.yaml
axiom:
token: "your-axiom-token"
org_id: "your-org-id" # optional
url: "https://api.axiom.co" # or https://api.eu.axiom.co for EU
server:
host: "127.0.0.1"
port: 5111
queries:
cache_ttl: "5m"
logging:
level: "info"
format: "text"| Region | Base URL | Environment Variable Setting |
|---|---|---|
| US | https://api.axiom.co |
AXIOM_URL=https://api.axiom.co |
| EU | https://api.eu.axiom.co |
AXIOM_URL=https://api.eu.axiom.co |
// Track athlete performance metrics
["strava_activities"]
| where athlete_id == "12345"
| where sport_type == "running"
| where _time > ago(30d)
| summarize avg_pace = avg(average_speed), total_distance = sum(distance)// Monitor application events
["app_events"]
| where user_id == "user123"
| where event_type == "workout_completed"
| where _time >= ago(7d)
| summarize events = count() by bin(1d, _time)// Analyze API response times
["api_logs"]
| where endpoint contains "activities"
| where _time >= ago(1h)
| summarize avg_response_time = avg(response_time_ms), error_rate = countif(status_code >= 400) * 100.0 / count()go build -o curated-axiom-mcp ./main.go# Run tests
go test ./...
# Run with quality checks
just check # requires justfile
# Integration testing (requires Axiom config)
just integration-testDebug logs are written to ~/.config/curated-axiom-mcp/stderr.log. Tool calls and responses are also logged to stdout.log for debugging.
# Monitor debug logs
tail -f ~/.config/curated-axiom-mcp/stderr.log
# Monitor tool execution
tail -f ~/.config/curated-axiom-mcp/stdout.logAdd to your Claude Desktop MCP configuration:
{
"mcpServers": {
"curated-axiom-mcp": {
"command": "curated-axiom-mcp",
"args": ["--stdio"],
"env": {
"AXIOM_TOKEN": "your-token-here",
"AXIOM_URL": "https://api.axiom.co"
}
}
}
}Note: The env section is only needed if you're not using a config file. If you have ~/.config/curated-axiom-mcp/config.yaml with your credentials, you can omit the environment variables.
The server supports standard MCP protocol over both stdio and HTTP transports.
- First/Last Values: Shows first and last values in the dataset
- Null Counts: Tracks empty/null values
- Unique Value Analysis: Counts unique values with frequency analysis
- Smart Sampling: For large datasets, intelligently samples data for statistics
- High Cardinality Handling: Summarizes columns with many unique values
- Warns when responses exceed 20KB to help optimize LLM context usage
- Automatically truncates debug logs to manageable sizes
- Provides row limiting options for large result sets
- Detailed error logging for debugging
- Graceful handling of malformed starred queries
- Clear error messages for missing parameters or invalid queries
MIT License - see LICENSE file for details.