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

Know when Traefik's backends go DOWN before traffic starts failing

AliveMCP polls your /health/traefik endpoint every 60 seconds and alerts you the moment any backend server status drops to DOWN — before users start hitting 502 errors from your proxy.

Start monitoring free