loading…
Search for a command to run...
loading…
Federated MCP server that joins relational metadata (MySQL) with timeseries values (InfluxDB) behind a single, LLM-friendly entity layer.
Federated MCP server that joins relational metadata (MySQL) with timeseries values (InfluxDB) behind a single, LLM-friendly entity layer.
Federated MCP server that joins relational metadata (MySQL) with timeseries values (InfluxDB) behind a single, LLM-friendly entity layer.
When an LLM needs to answer a question like:
"show me last-7-day temperature trends for active sensors in zone A"
it has to traverse two stores:
Plugging in two independent MCP servers forces the LLM to:
LLMs reliably stumble at step 3 — especially when the join key is composite (e.g. an InfluxDB tag is the concatenation of two relational fields).
This server collapses all three steps into one tool call.
Five tools, no SQL or Flux in the prompt:
| Tool | Purpose |
|---|---|
list_entities |
Filter business entities by their fields |
get_entity |
Look up a single entity by primary key |
list_related |
Walk a configured relation between entities |
get_entity_timeseries |
Federated — metadata + timeseries in one call |
compare_timeseries |
Compare 2–20 specific entities over the same window |
Every tool ships with rich JSON-Schema descriptions so the LLM picks the right arguments without prompt tricks.
| Scenario | Two independent MCP servers | mcp-federated-data |
|---|---|---|
| "trends for sensors in zone A" | LLM: query MySQL → extract ids → push into Flux → join in its head | one get_entity_timeseries call |
Composite tag like {deviceId}.{signalId} |
LLM composes strings in-prompt — error-prone | composer: "{deviceId}.{signalId}" in YAML, server does it |
| Metadata ↔ timeseries alignment | LLM does the join, frequently mis-pairs | server joins by configured key |
| Data-volume blow-up | unprotected | enforced max entities + auto-downsampling + per-entity point cap |
| Business semantics for the LLM | raw CREATE TABLE only |
YAML fields with description |
| Audit trail | none | structured per-call audit log |
git clone https://github.com/baller-coder/mcp-federated-data.git
cd mcp-federated-data
pnpm install
# Sample environment (MySQL + InfluxDB in Docker, with seeded data)
docker compose -f examples/industrial-monitoring/docker-compose.yml up -d
pnpm seed
# Start the MCP server over stdio
pnpm dev -- --config examples/industrial-monitoring/config.yaml
Connect from any MCP client (Claude Desktop, Cursor, mcp-inspector).
// list active sites
{
"name": "list_entities",
"arguments": {
"entity": "site",
"filters": [{ "field": "status", "op": "eq", "value": "active" }]
}
}
// list sensors attached to site 1
{
"name": "list_related",
"arguments": {
"source_entity": "site",
"source_id": 1,
"target_entity": "sensor"
}
}
// federated query — metadata + timeseries in ONE call
{
"name": "get_entity_timeseries",
"arguments": {
"entity": "sensor",
"filters": [
{ "field": "site_id", "op": "eq", "value": 1 },
{ "field": "kind", "op": "eq", "value": "temperature" }
],
"time_range": { "start": "-7d" },
"aggregation": { "window": "1h", "fn": "mean" }
}
}
// compare 3 specific sensors over the same window
{
"name": "compare_timeseries",
"arguments": {
"entity": "sensor",
"ids": [101, 201, 301],
"time_range": { "start": "-24h" },
"aggregation": { "window": "10m", "fn": "mean" }
}
}
The whole server behavior is defined in a single YAML file. Three sections.
datasources:
- name: business
type: mysql
host: localhost
port: 3306
database: my_db
username: readonly_user
password: secret
- name: timeseries
type: influxdb
url: http://localhost:8086
token: my-token
org: my_org
bucket: my_bucket
Each entity binds to a relational table or view, with optional relations and an optional timeseries binding.
entities:
- name: site
description: Physical monitoring location.
source:
datasource: business
table: sites
primary_key: id
fields:
- { name: id, type: number }
- { name: name, type: string, description: Display name }
- { name: region, type: string }
- { name: status, type: string, description: "active / inactive / maintenance" }
- name: sensor
description: A sensor attached to a site.
source:
datasource: business
table: sensors
primary_key: id
fields:
- { name: id, type: number }
- { name: site_id, type: number }
- { name: name, type: string }
- { name: kind, type: string, description: "temperature / humidity / voltage / ..." }
- { name: unit, type: string }
relations:
- target: site
type: many-to-one
local_key: site_id
foreign_key: id
timeseries:
datasource: timeseries
measurement: sensor_data
value_field: value
join_key:
local: id
remote_tag: sensor_id
defaults:
max_entities_per_query: 50
max_points_per_entity: 500
query_timeout_ms: 15000
When the InfluxDB tag value is a composite of multiple relational fields — common in IoT / industrial systems where a tag like 400001240.438000066 encodes {deviceId}.{signalId} — declare it like this:
timeseries:
measurement: sensor_data
value_field: value
join_key:
local: [device_id, signal_id]
remote_tag: signal_id
composer: "{device_id}.{signal_id}"
The server:
SELECT).Single-field bindings (the v0.1 form) keep working unchanged — the server treats them as a one-element composite, so all paths stay uniform.
Every federated tool follows the same three steps:
WHERE. Allowed filter fields are restricted to those declared in the entity config.No SQL parser. No cross-store query planner. By design.
┌──────────────────────────────────┐
│ MCP client (Claude / Cursor) │
└────────────┬─────────────────────┘
│ stdio (JSON-RPC)
▼
┌──────────────────────────────────┐
│ mcp-federated-data │
│ ┌────────────────────────────┐ │
│ │ Tools (5 tools) │ │
│ ├────────────────────────────┤ │
│ │ Entity registry │ │
│ │ Join-key normalizer │ │
│ │ Composer engine │ │
│ │ Guards (limits/timeout) │ │
│ │ Audit logger │ │
│ ├────────────────────────────┤ │
│ │ Datasource adapters │ │
│ └─────────┬────────┬─────────┘ │
└────────────┼────────┼────────────┘
▼ ▼
┌──────┐ ┌──────────┐
│MySQL │ │InfluxDB │
└──────┘ └──────────┘
mcp-federated-data is schema-driven, not domain-specific. It applies anywhere business metadata lives in a relational store and observed values live in a timeseries store:
If your stack is MySQL + InfluxDB and you want LLMs to reason over it — this server is for you.
| mcp-federated-data | mcp-server-mysql | mcp-server-influxdb | Wren AI / Vanna | |
|---|---|---|---|---|
| Cross-store join | ✅ | ❌ | ❌ | partial (own engine) |
| Composite tag composer | ✅ | n/a | ❌ | ❌ |
| LLM-safe SQL guards | ✅ | varies | n/a | ✅ |
| Schema as semantic layer | ✅ | ❌ | ❌ | ✅ |
| Configuration | YAML | env / args | env / args | dedicated DSL |
| Scope | narrow but deep | thin wrapper | thin wrapper | full BI platform |
Issues and PRs welcome. Before opening one:
Добавь это в claude_desktop_config.json и перезапусти Claude Desktop.
{
"mcpServers": {
"mcp-federated-data": {
"command": "npx",
"args": []
}
}
}