A production-ready Model Context Protocol (MCP) server implemented in Go that provides a generic SQL interface for an acoustic monitoring database, plus time utilities for AI assistants.
This MCP server provides AI assistants with direct SQL query access to an acoustic monitoring database containing recordings, locations, species data, and classifications. Built using the official MCP Go SDK, it follows MCP's three-primitive architecture with an LLM-friendly design focused on providing schema context rather than rigid tool APIs.
# Clone or navigate to the project directory
cd /home/david/go/src/skraak_mcp
# Download dependencies
go mod download
# Build the server
go build -o skraak_mcp
The server communicates over stdio (standard input/output) as per MCP specification and requires a DuckDB database path as an argument:
./skraak_mcp /path/to/database.duckdb
Database Options:
./db/skraak.duckdb - Real data, use for Claude Desktop./db/test.duckdb - Test data, use for development/testing⚠️ IMPORTANT: Always use test.duckdb for testing and development to avoid corrupting production data!
Examples:
# Production use (Claude Desktop)
./skraak_mcp ./db/skraak.duckdb
# Testing (development, shell scripts)
./skraak_mcp ./db/test.duckdb
Add to your Claude Desktop MCP configuration file:
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
Linux: ~/.config/Claude/claude_desktop_config.json
{
"mcpServers": {
"skraak_mcp": {
"command": "/home/david/go/src/skraak_mcp/skraak_mcp",
"args": ["/path/to/database.duckdb"]
}
}
}
Execute arbitrary SQL SELECT queries against the acoustic monitoring database.
Input:
{
"query": "SELECT id, name FROM dataset WHERE active = true",
"parameters": [],
"limit": 1000
}
Parameters:
query (required): SQL query (must start with SELECT or WITH)parameters (optional): Array of values for ? placeholderslimit (optional): Row limit (default 1000, max 10000)Output:
{
"rows": [...],
"row_count": 42,
"columns": ["id", "name", "active"]
}
Security:
Example Queries:
-- Basic query
SELECT * FROM dataset WHERE active = true
-- Parameterized query
SELECT * FROM location WHERE dataset_id = ? AND active = true
-- JOIN with aggregates
SELECT d.name, COUNT(l.id) as locations
FROM dataset d
LEFT JOIN location l ON d.id = l.dataset_id
WHERE d.active = true
GROUP BY d.name
Returns the current system time with comprehensive timezone information.
Input: None
Output:
{
"time": "2024-01-25T10:30:45Z",
"timezone": "UTC",
"unix": 1706181045
}
Fields:
time: Current system time in RFC3339 format (ISO 8601 compatible)timezone: System timezone identifierunix: Unix timestamp in seconds since epochBatch import WAV files from a folder into the database.
Input:
{
"folder_path": "/absolute/path/to/recordings",
"dataset_id": "abc123xyz789",
"location_id": "def456uvw012",
"cluster_id": "ghi789rst345",
"recursive": true
}
Parameters:
folder_path (required): Absolute path to folder containing WAV filesdataset_id (required): Dataset ID (12 characters)location_id (required): Location ID (12 characters)cluster_id (required): Cluster ID (12 characters)recursive (optional): Scan subfolders recursively (default: true)Features:
Import a single WAV file into the database.
Input:
{
"file_path": "/absolute/path/to/recording.wav",
"dataset_id": "abc123xyz789",
"location_id": "def456uvw012",
"cluster_id": "ghi789rst345"
}
Output:
{
"file_id": "nB3xK8pLm9qR5sT7uV2wX",
"file_name": "recording.wav",
"hash": "a1b2c3d4e5f6g7h8",
"duration_seconds": 60.0,
"sample_rate": 250000,
"timestamp_local": "2024-01-15T20:30:00+13:00",
"is_audiomoth": true,
"is_duplicate": false,
"processing_time": "250ms"
}
Use Cases:
Import ML-detected kiwi call selections from folder structure.
Input:
{
"folder_path": "/path/to/Clips_filter_date",
"dataset_id": "abc123xyz789",
"cluster_id": "def456uvw012"
}
Features:
Clips_{filter_name}_{date}/Species/CallType/*.wav+.png{base}-{start}-{end}.wav formatBatch import WAV files across multiple locations/clusters using a CSV file.
Input:
{
"dataset_id": "abc123xyz789",
"csv_path": "/absolute/path/to/import.csv",
"log_file_path": "/absolute/path/to/progress.log"
}
CSV Format:
location_name,location_id,directory_path,date_range,sample_rate,file_count
Site A,loc123456789,/path/to/siteA,2024-01,48000,150
Site B,loc987654321,/path/to/siteB,2024-02,48000,200
Required CSV Columns:
location_name: Human-readable location namelocation_id: 12-character location ID from databasedirectory_path: Absolute path to folder containing WAV filesdate_range: Cluster name (typically date range like "2024-01" or "Jan-2024")sample_rate: Sample rate in Hz (e.g., 48000, 250000)file_count: Expected number of files (for validation)Features:
tail -f)Output:
{
"total_locations": 10,
"clusters_created": 5,
"clusters_existing": 5,
"total_files_scanned": 1500,
"files_imported": 1200,
"files_duplicate": 250,
"files_error": 50,
"processing_time": "5m30s",
"errors": []
}
Use Cases:
Create a new dataset.
Input:
{
"name": "My Dataset",
"description": "Description of dataset",
"type": "organise"
}
Parameters:
name (required): Dataset name (max 255 characters)description (optional): Dataset description (max 255 characters)type (optional): Dataset type - "organise", "test", or "train" (default: "organise")Create a new location within a dataset.
Input:
{
"dataset_id": "abc123xyz789",
"name": "Recording Site A",
"latitude": -41.2865,
"longitude": 174.7762,
"timezone_id": "Pacific/Auckland",
"description": "Forest recording site"
}
Parameters:
dataset_id (required): Parent dataset ID (12 characters)name (required): Location name (max 140 characters)latitude (required): Latitude in decimal degrees (-90 to 90)longitude (required): Longitude in decimal degrees (-180 to 180)timezone_id (required): IANA timezone ID (e.g., "Pacific/Auckland")description (optional): Location description (max 255 characters)Create a new cluster within a location.
Input:
{
"dataset_id": "abc123xyz789",
"location_id": "def456uvw012",
"name": "2024-01",
"sample_rate": 48000,
"description": "January 2024 recordings",
"cyclic_recording_pattern_id": "pat123456789"
}
Parameters:
dataset_id (required): Parent dataset ID (12 characters)location_id (required): Parent location ID (12 characters)name (required): Cluster name (max 140 characters)sample_rate (required): Sample rate in Hz (must be positive)description (optional): Cluster description (max 255 characters)cyclic_recording_pattern_id (optional): Recording pattern ID (12 characters)Create a reusable recording pattern with record/sleep cycle.
Input:
{
"record_seconds": 60,
"sleep_seconds": 540
}
Parameters:
record_seconds (required): Number of seconds to record (must be positive)sleep_seconds (required): Number of seconds to sleep between recordings (must be positive)Best Practice: Query existing patterns first with execute_sql to reuse matching patterns:
SELECT id FROM cyclic_recording_pattern
WHERE record_s = ? AND sleep_s = ? AND active = true
Update metadata for existing entities. See tool descriptions for parameters.
skraak_mcp/
├── go.mod # Go module definition
├── go.sum # Dependency checksums
├── main.go # Server entry point, tool registration
├── README.md # This file
├── CLAUDE.md # Development notes and best practices
├── db/ # Database files
│ ├── db.go # Database connection (read-only)
│ ├── types.go # Type definitions
│ ├── schema.sql # Database schema (348 lines)
│ ├── skraak.duckdb # Production database ⚠️
│ └── test.duckdb # Test database ✅
├── tools/ # MCP tool implementations
│ ├── time.go # get_current_time
│ ├── sql.go # execute_sql
│ ├── import_files.go # import_audio_files (batch WAV import)
│ ├── import_file.go # import_file (single WAV file)
│ ├── import_ml_selections.go # import_ml_selections (ML detections)
│ ├── bulk_file_import.go # bulk_file_import (CSV-based bulk import)
│ ├── write_dataset.go # create_dataset
│ ├── write_location.go # create_location
│ ├── write_cluster.go # create_cluster
│ ├── write_pattern.go # create_cyclic_recording_pattern
│ ├── update_dataset.go # update_dataset
│ ├── update_location.go # update_location
│ ├── update_cluster.go # update_cluster
│ └── update_pattern.go # update_pattern
├── resources/ # MCP resources
│ └── schema.go # Database schema resources
├── prompts/ # MCP prompts
│ └── examples.go # SQL workflow templates
├── utils/ # Utility functions
│ ├── astronomical.go # Solar/civil night, moon phase
│ ├── astronomical_test.go # Tests (11 cases)
│ ├── audiomoth_parser.go # AudioMoth WAV parsing
│ ├── audiomoth_parser_test.go # Tests (36 cases)
│ ├── filename_parser.go # Filename timestamp parsing
│ ├── filename_parser_test.go # Tests (60 cases)
│ ├── wav_metadata.go # WAV header parsing
│ ├── wav_metadata_test.go # Tests (22 cases)
│ ├── xxh64.go # File hashing
│ └── xxh64_test.go # Tests (6 cases)
└── shell_scripts/ # End-to-end test scripts
├── test_sql.sh # SQL tool tests
├── test_resources_prompts.sh # Resources/prompts tests
├── test_all_prompts.sh # All prompts tests
└── get_time.sh # Time tool test
The acoustic monitoring database contains:
Core Tables:
Annotation Tables:
Taxonomy Tables:
Key Fields:
active boolean for soft deletestimestamp_local (with timezone) and may include timestamp_utcmaybe_solar_night, maybe_civil_night, moon_phaserecorder_id, gain, battery_v, temp_cFull schema available via schema://full resource.
The server follows MCP best practices with type-safe tool handlers:
Create tool file in tools/ package (e.g., tools/analysis.go)
Define input/output structures with jsonschema tags:
type AnalysisInput struct {
ClusterID string `json:"cluster_id" jsonschema:"Cluster to analyze"`
Metric string `json:"metric" jsonschema:"Metric to calculate"`
}
type AnalysisOutput struct {
Value float64 `json:"value" jsonschema:"Calculated metric value"`
Unit string `json:"unit" jsonschema:"Unit of measurement"`
}
func Analyze(ctx context.Context, req *mcp.CallToolRequest, input AnalysisInput) (
*mcp.CallToolResult,
AnalysisOutput,
error,
) {
// Implementation
return &mcp.CallToolResult{}, output, nil
}
err := mcp.AddTool(
server,
"analyze_cluster",
"Calculate metrics for a recording cluster",
tools.Analyze,
)
tools/analysis_test.goComprehensive test suite with 91.5% code coverage:
# Run all tests
go test ./...
# Run specific package tests
go test ./utils/
# Verbose output with test names
go test -v ./utils/
# Run specific test
go test -v ./utils/ -run TestParseFilenameTimestamps
# Coverage report
go test -cover ./utils/
# Generate HTML coverage report
go test -coverprofile=coverage.out ./utils/
go tool cover -html=coverage.out
Test Coverage:
Shell scripts test the MCP protocol integration:
# Navigate to test scripts
cd shell_scripts
# ⚠️ ALWAYS use test.duckdb for testing!
# Test SQL tool (pipe to file to avoid token overflow)
./test_sql.sh ../db/test.duckdb > test.txt 2>&1
rg '"result":' test.txt | wc -l # Count successful queries
# Test resources and prompts
./test_resources_prompts.sh ../db/test.duckdb > test_resources.txt 2>&1
cat test_resources.txt | jq '.'
# Test all prompts
./test_all_prompts.sh ../db/test.duckdb > test_prompts.txt 2>&1
# Test time tool (no database needed)
./get_time.sh
Important: Always use test.duckdb for testing to avoid corrupting production data!
This server implements:
MIT
Contributions welcome! Please ensure:
Server won't start:
go versiongo build -o skraak_mcpTool not appearing in client:
Time format issues: