Guide · MCP API Gateway Integration
MCP Server Traefik — Router inspection, middleware management, service health, and /ping monitoring
Traefik v3 is a cloud-native reverse proxy that dynamically discovers routes from Docker labels, Kubernetes IngressRoutes, and file providers. Its REST API exposes the full current routing state — which makes it a rich target for MCP tools that need to inspect active routes, diagnose routing errors, check backend service health, and audit middleware chains. This guide covers Traefik API setup, building list_routers, list_services, get_overview, and list_middlewares tools, understanding the critical distinction between Traefik's own health (/ping) and backend server health, and registering the right endpoint with AliveMCP.
TL;DR
Enable the Traefik dashboard API with --api.insecure=true (dev) or via an IngressRoute with auth middleware (prod). Call GET /api/http/routers, GET /api/http/services, and GET /api/overview to inspect routing state. The critical difference: GET /ping only tells you Traefik is alive — backend server health lives in the serverStatus field of /api/http/services. Traefik's API is read-only for dynamic config: you cannot add routes via API, only inspect them. Monitor /ping with AliveMCP and separately alert on service.serverStatus containing any DOWN servers.
Enabling and accessing the Traefik API
Traefik's API and dashboard are disabled by default. Enable them in the static configuration (traefik.yaml or CLI flags). The API is read-only — it exposes current routing state but cannot modify dynamic config.
# traefik.yaml — static configuration
api:
dashboard: true
insecure: true # Exposes API on port 8080 without auth — dev only
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
providers:
docker:
exposedByDefault: false
file:
directory: /etc/traefik/dynamic
watch: true
# Production: expose API behind auth middleware
# In a separate dynamic config file (dynamic/api.yaml):
http:
routers:
traefik-api:
rule: "Host(`traefik.example.com`)"
service: api@internal
middlewares:
- basic-auth
tls: {}
middlewares:
basic-auth:
basicAuth:
users:
- "admin:$apr1$xyz..." # htpasswd format
// TypeScript client for Traefik REST API
const TRAEFIK_API_URL = process.env.TRAEFIK_API_URL ?? 'http://localhost:8080';
const TRAEFIK_USERNAME = process.env.TRAEFIK_USERNAME;
const TRAEFIK_PASSWORD = process.env.TRAEFIK_PASSWORD;
async function traefikRequest(path: string): Promise<any> {
const headers: Record<string, string> = { 'Accept': 'application/json' };
// Basic auth for production instances
if (TRAEFIK_USERNAME && TRAEFIK_PASSWORD) {
const credentials = Buffer.from(`${TRAEFIK_USERNAME}:${TRAEFIK_PASSWORD}`).toString('base64');
headers['Authorization'] = `Basic ${credentials}`;
}
const res = await fetch(`${TRAEFIK_API_URL}${path}`, { headers });
if (!res.ok) {
throw new Error(`Traefik API ${path} → ${res.status} ${res.statusText}`);
}
return res.json();
}
export { traefikRequest };
| API endpoint | What it returns | Useful for |
|---|---|---|
GET /api/overview |
Router/service/middleware counts, Traefik version | Quick health summary, error counts |
GET /api/http/routers |
All HTTP routers with rule, service, middlewares, TLS | Route auditing, finding misconfigured rules |
GET /api/http/services |
All HTTP services with server URLs and status | Backend health — serverStatus per server URL |
GET /api/http/middlewares |
All middlewares with type and configuration | Security audit, rate limit inspection |
GET /ping |
200 OK if Traefik process is alive | Process liveness — not backend health |
GET /api/tcp/routers |
TCP routers (non-HTTP protocols) | Debugging TCP passthrough routes |
Core Traefik MCP tool patterns
import { z } from 'zod';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { traefikRequest } from './traefik-client.js';
// ---- get_overview ----
server.tool(
'get_overview',
{},
async () => {
const overview = await traefikRequest('/api/overview');
return {
content: [{
type: 'text',
text: JSON.stringify({
version: overview.version,
http: {
routers: overview.http?.routers,
services: overview.http?.services,
middlewares: overview.http?.middlewares
},
tcp: overview.tcp,
udp: overview.udp,
features: overview.features
}, null, 2)
}]
};
}
);
// ---- list_routers ----
server.tool(
'list_routers',
{
protocol: z.enum(['http', 'tcp', 'udp']).default('http'),
status_filter: z.enum(['enabled', 'disabled', 'all']).default('all')
},
async ({ protocol, status_filter }) => {
const routers = await traefikRequest(`/api/${protocol}/routers`);
const filtered = status_filter === 'all'
? routers
: routers.filter((r: any) => r.status === status_filter);
return {
content: [{
type: 'text',
text: JSON.stringify(filtered.map((r: any) => ({
name: r.name,
rule: r.rule,
service: r.service,
middlewares: r.middlewares ?? [],
tls: r.tls ? { enabled: true, certResolver: r.tls.certResolver } : { enabled: false },
status: r.status,
provider: r.provider,
priority: r.priority ?? 0,
// 'error' field appears when the router has a configuration problem
error: r.error ?? null
})), null, 2)
}]
};
}
);
// ---- list_services ----
// The 'serverStatus' field shows per-server health — DOWN means backend is unreachable
server.tool(
'list_services',
{
protocol: z.enum(['http', 'tcp', 'udp']).default('http'),
show_unhealthy_only: z.boolean().default(false)
},
async ({ protocol, show_unhealthy_only }) => {
const services = await traefikRequest(`/api/${protocol}/services`);
let filtered = services as any[];
if (show_unhealthy_only) {
filtered = services.filter((s: any) => {
const serverStatus = s.loadBalancer?.serverStatus ?? {};
return Object.values(serverStatus).some(v => v === 'DOWN');
});
}
return {
content: [{
type: 'text',
text: JSON.stringify(filtered.map((s: any) => ({
name: s.name,
type: s.type,
provider: s.provider,
status: s.status,
// serverStatus is a map of server URL → 'UP' | 'DOWN'
serverStatus: s.loadBalancer?.serverStatus ?? {},
servers: (s.loadBalancer?.servers ?? []).map((srv: any) => ({
url: srv.url,
health: s.loadBalancer?.serverStatus?.[srv.url] ?? 'UNKNOWN'
})),
healthCheck: s.loadBalancer?.healthCheck ?? null,
error: s.error ?? null
})), null, 2)
}]
};
}
);
// ---- list_middlewares ----
server.tool(
'list_middlewares',
{ type_filter: z.string().optional().describe('Filter by middleware type, e.g. rateLimit, basicAuth, headers') },
async ({ type_filter }) => {
const middlewares = await traefikRequest('/api/http/middlewares');
const filtered = type_filter
? (middlewares as any[]).filter(m => Object.keys(m).some(k => k === type_filter))
: middlewares as any[];
return {
content: [{
type: 'text',
text: JSON.stringify(filtered.map((m: any) => ({
name: m.name,
provider: m.provider,
status: m.status,
type: Object.keys(m).find(k => !['name', 'provider', 'status', 'usedBy', 'error'].includes(k)) ?? 'unknown',
usedBy: m.usedBy ?? [],
error: m.error ?? null
})), null, 2)
}]
};
}
);
// ---- diagnose_router ----
// Find why a specific hostname/path isn't routing as expected
server.tool(
'diagnose_router',
{ host: z.string(), path: z.string().default('/') },
async ({ host, path }) => {
const routers = await traefikRequest('/api/http/routers') as any[];
// Find routers whose rules mention the host
const candidates = routers.filter(r =>
r.rule?.includes(host) ||
r.rule?.toLowerCase().includes(host.toLowerCase())
);
if (candidates.length === 0) {
return {
content: [{
type: 'text',
text: JSON.stringify({
host,
path,
matched_routers: [],
diagnosis: `No router rules found mentioning host "${host}". Check provider config (Docker labels, IngressRoute, file provider).`
})
}]
};
}
return {
content: [{
type: 'text',
text: JSON.stringify({
host,
path,
matched_routers: candidates.map(r => ({
name: r.name,
rule: r.rule,
service: r.service,
status: r.status,
error: r.error ?? null,
tls: !!r.tls,
priority: r.priority ?? 0
})),
highest_priority: candidates.reduce((a, b) => (a.priority ?? 0) > (b.priority ?? 0) ? a : b).name
}, null, 2)
}]
};
}
);
The serverStatus field on /api/http/services is Traefik's per-server health report. It's a map of server URL to either UP or DOWN. This field only appears when Traefik has health check configuration on the service — without health checks, all servers are assumed UP. Configure health checks in your dynamic config to make serverStatus meaningful.
Configuring health checks for serverStatus
Traefik health checks are configured per service in dynamic config, not globally. Without this config, serverStatus is empty and the only way to detect backend failures is through passive circuit-breaker behavior.
# dynamic/services.yaml
http:
services:
my-api:
loadBalancer:
healthCheck:
path: /health
interval: "10s"
timeout: "3s"
# Optional: check a specific hostname header
hostname: "api.example.com"
# Optional: only mark UP if status code is in this range
# Default: 2xx is healthy
servers:
- url: "http://backend1:8080"
- url: "http://backend2:8080"
// Docker label equivalent:
// traefik.http.services.my-api.loadbalancer.healthcheck.path=/health
// traefik.http.services.my-api.loadbalancer.healthcheck.interval=10s
// Kubernetes IngressRoute equivalent:
// Create a TraefikService with health check:
// apiVersion: traefik.io/v1alpha1
// kind: TraefikService
// spec:
// weighted:
// services:
// - name: my-api
// port: 8080
// healthCheck:
// path: /health
// intervalSeconds: 10
| Config method | Health check location | Hot-reload |
|---|---|---|
| Docker labels | Container label | Yes — on container start/stop |
| File provider | Dynamic config YAML/TOML | Yes — file watcher triggers reload |
| Kubernetes IngressRoute | TraefikService CRD | Yes — k8s watch events |
| Consul Catalog | Consul service health checks | Yes — consul change notifications |
Health endpoint: /health/traefik
Traefik provides two built-in health endpoints. Understanding which to use for AliveMCP monitoring is critical:
import express from 'express';
import { traefikRequest } from './traefik-client.js';
const app = express();
const TRAEFIK_PING_URL = process.env.TRAEFIK_PING_URL ?? 'http://localhost:8080/ping';
app.get('/health/traefik', async (_req, res) => {
const start = Date.now();
try {
// Layer 1: Traefik process liveness via /ping
const pingRes = await fetch(TRAEFIK_PING_URL);
const pingOk = pingRes.status === 200;
// Layer 2: Router and service health via API
const [overview, services] = await Promise.all([
traefikRequest('/api/overview'),
traefikRequest('/api/http/services')
]);
// Count services with any DOWN servers
const degradedServices = (services as any[]).filter(s => {
const serverStatus = s.loadBalancer?.serverStatus ?? {};
return Object.values(serverStatus).some(v => v === 'DOWN');
});
// Count routers with errors
const errorRouters = (await traefikRequest('/api/http/routers') as any[])
.filter(r => r.error || r.status === 'disabled');
const latencyMs = Date.now() - start;
const ok = pingOk && degradedServices.length === 0;
res.status(ok ? 200 : 503).json({
status: ok ? 'ok' : 'degraded',
traefik_alive: pingOk,
version: overview.version,
router_errors: errorRouters.length,
router_error_names: errorRouters.map(r => r.name),
degraded_services: degradedServices.length,
degraded_service_names: degradedServices.map(s => s.name),
latency_ms: latencyMs
});
} catch (err: any) {
res.status(503).json({
status: 'error',
error: err.message,
elapsed_ms: Date.now() - start
});
}
});
app.listen(3001);
| Endpoint | What it checks | Traefik docs label |
|---|---|---|
/ping |
Traefik process is running and accepting connections | Liveness probe |
/api/http/services serverStatus |
Whether configured backend servers respond to health check path | No official label — runtime state |
/api/overview |
Router/service/middleware counts and error counts | Dashboard summary |
Configure AliveMCP to poll /ping directly on the Traefik entrypoint or admin port. If you expose the API behind an authenticated IngressRoute, your MCP server's /health/traefik endpoint (above) provides the combined view that includes backend server status — register that with AliveMCP instead, since a healthy /ping with all backends DOWN is still a service degradation worth alerting on.
Static vs dynamic configuration: what MCP tools can and cannot change
Traefik's architecture separates static configuration (set at startup) from dynamic configuration (updated at runtime). This distinction is critical for MCP tools: you can only modify dynamic config.
| Configuration type | Examples | Can MCP tools modify it? | How to change |
|---|---|---|---|
| Static | Entry points, providers, TLS certificates, log level | No — requires Traefik restart | Edit traefik.yaml or restart with new CLI flags |
| Dynamic (file provider) | Routers, services, middlewares defined in dynamic YAML files | Yes — write to the watched directory | MCP tool writes/updates YAML files in the provider directory |
| Dynamic (Docker labels) | Routes derived from container labels | Via Docker API only | Update container labels via Docker SDK or docker-compose |
| Dynamic (Kubernetes CRDs) | IngressRoute, Middleware, TraefikService | Via Kubernetes API | kubectl apply / Kubernetes client |
// MCP tool to write a new dynamic route via file provider
server.tool(
'add_file_route',
{
route_name: z.string().regex(/^[a-z0-9-]+$/).describe('Lowercase alphanumeric route name'),
host: z.string(),
service_url: z.string().url(),
middlewares: z.array(z.string()).default([]),
confirm: z.literal(true)
},
async ({ route_name, host, service_url, middlewares }) => {
const { writeFile } = await import('fs/promises');
const path = await import('path');
const DYNAMIC_DIR = process.env.TRAEFIK_DYNAMIC_DIR ?? '/etc/traefik/dynamic';
const config = {
http: {
routers: {
[route_name]: {
rule: `Host(\`${host}\`)`,
service: route_name,
middlewares: middlewares.length > 0 ? middlewares : undefined,
tls: { certResolver: 'letsencrypt' }
}
},
services: {
[route_name]: {
loadBalancer: {
servers: [{ url: service_url }],
healthCheck: { path: '/health', interval: '10s', timeout: '3s' }
}
}
}
}
};
const { dump } = await import('js-yaml');
const yamlContent = dump(config);
const filePath = path.join(DYNAMIC_DIR, `${route_name}.yaml`);
await writeFile(filePath, yamlContent, 'utf-8');
// Traefik watches the directory and picks up the new file automatically
return { content: [{ type: 'text', text: JSON.stringify({ created: filePath, route: route_name, host }) }] };
}
);
Frequently asked questions
Why does /ping return 200 even when all my backends are down?
/ping checks whether the Traefik process is running and accepting HTTP connections on its entrypoint — not whether backends are reachable. Traefik's liveness probe (/ping) and backend health (serverStatus in /api/http/services) are intentionally separate. If all backends are down, Traefik is still alive and will return 502 Bad Gateway to incoming requests — but /ping returns 200. For AliveMCP monitoring, poll your MCP server's composite /health/traefik endpoint (which checks both /ping AND serverStatus), not /ping directly, if you want to detect backend failures.
How do I debug a router that shows "disabled" status?
A router appears as status: "disabled" in the API when Traefik cannot resolve its configuration — typically because the service it references doesn't exist, the middleware name is misspelled, or there's a TLS certificate resolver that failed. Check the error field on the router object: it contains the specific error message. Common causes: (1) typo in the service name in the router rule — the service referenced by the router must exactly match a service in the same provider; (2) middleware referenced in the router doesn't exist in the same or a global provider; (3) the CertResolver hasn't received a certificate yet for the domain (check /api/http/routers/:name for the TLS status).
How do I handle Traefik running without the API enabled?
If the Traefik API is not enabled (api.insecure not set and no API IngressRoute), all /api/* calls return 404 or connection refused. Your MCP server should fail gracefully: catch the 404, check if /ping responds, and return a clear error explaining that the Traefik API is not enabled. Add instructions in the error message for how to enable it. For production environments that intentionally disable the API for security, your MCP tools should fall back to reading the dynamic config files directly (via file system) or Kubernetes CRDs (via kubectl/client-go) instead of using the REST API.
Can I use Traefik API tools with Traefik v2 (older installations)?
The Traefik REST API structure is largely compatible between v2 and v3, but there are breaking differences: in v3, the default IngressRoute API group changed from traefik.containo.us to traefik.io; ServersTransport moved from the HTTP provider to a top-level resource; and some middleware configurations changed. For API-level MCP tools (routers, services, middlewares), the /api/http/* endpoints work the same in both versions. Test your tools against both versions if you support mixed environments — the /api/overview response includes a version field you can use to branch behavior.
Further reading
- MCP Server Kong Gateway — Admin API tools, upstream health checks, plugin management
- MCP Server Envoy — Admin API cluster health, config dump, and graceful drain
- MCP Server nginx — reverse proxy configuration and health monitoring patterns
- MCP Server Health Check — designing endpoints for uptime monitors
- MCP Server Kubernetes — kubectl client patterns for agent tools