Skip to main content

Observability Overview

MCPKit provides comprehensive observability features for production MCP servers: metrics, tracing, and health checks. This guide explains how to use each feature and when to apply them.

The Three Pillars

FeaturePurposeWhen to Use
MetricsCount events, measure valuesDashboards, alerts, SLOs
TracingTrack request flowDebugging, performance analysis
Health ChecksMonitor system statusLoad balancers, orchestrators

Quick Start

Minimal Setup

import {
MCPServer,
Tool,
metricsPlugin,
healthPlugin,
tracingPlugin,
createMetricsCollector,
createHealthChecker,
createTracer,
consoleExporter,
} from '@mcpkit-dev/core';

// Create observability components
const metrics = createMetricsCollector({ prefix: 'myserver' });
const health = createHealthChecker();
const tracer = createTracer({
serviceName: 'my-server',
exporters: [consoleExporter()],
});

@MCPServer({
name: 'my-server',
version: '1.0.0',
plugins: [
metricsPlugin({ collector: metrics }),
healthPlugin({ checker: health }),
tracingPlugin({ tracer }),
],
})
class MyServer {
@Tool({ description: 'My tool' })
async myTool() {
return 'result';
}
}

Metrics

Metrics collect numerical data about your server's behavior over time.

Creating a Metrics Collector

import { createMetricsCollector } from '@mcpkit-dev/core';

const metrics = createMetricsCollector({
// Prefix for all metric names
prefix: 'myapp',

// Default labels added to all metrics
defaultLabels: {
service: 'my-server',
environment: 'production',
},
});

Metric Types

Counter - Count events

// Create a counter
const requestCounter = metrics.counter({
name: 'requests_total',
help: 'Total number of requests',
labels: ['method', 'status'],
});

// Increment
requestCounter.inc({ method: 'tool', status: 'success' });
requestCounter.inc({ method: 'tool', status: 'error' });

// Increment by value
requestCounter.inc({ method: 'resource', status: 'success' }, 5);

Gauge - Track current values

// Create a gauge
const activeConnections = metrics.gauge({
name: 'active_connections',
help: 'Number of active connections',
labels: ['transport'],
});

// Set value
activeConnections.set({ transport: 'stdio' }, 1);

// Increment/decrement
activeConnections.inc({ transport: 'http' });
activeConnections.dec({ transport: 'http' });

Histogram - Measure distributions

// Create a histogram
const responseTimes = metrics.histogram({
name: 'response_time_seconds',
help: 'Response time in seconds',
labels: ['tool'],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 5], // bucket boundaries
});

// Record observations
responseTimes.observe({ tool: 'search' }, 0.045);
responseTimes.observe({ tool: 'search' }, 0.123);

// Use timer helper
const timer = responseTimes.startTimer({ tool: 'process' });
await doWork();
timer.end(); // Records elapsed time

Built-in Metrics

The metricsPlugin automatically tracks:

MetricTypeDescription
mcp_tool_calls_totalCounterTotal tool calls by name and status
mcp_tool_duration_secondsHistogramTool execution time
mcp_resource_reads_totalCounterTotal resource reads
mcp_prompt_gets_totalCounterTotal prompt retrievals
mcp_errors_totalCounterTotal errors by type

Exposing Metrics

import { metricsPlugin, createMetricsCollector } from '@mcpkit-dev/core';

const metrics = createMetricsCollector({ prefix: 'myapp' });

// Get Prometheus-format metrics
const prometheusMetrics = metrics.getMetrics();
console.log(prometheusMetrics);
// # HELP myapp_requests_total Total number of requests
// # TYPE myapp_requests_total counter
// myapp_requests_total{method="tool",status="success"} 42

// For HTTP transport, expose at /metrics endpoint
@MCPServer({
name: 'my-server',
version: '1.0.0',
plugins: [
metricsPlugin({
collector: metrics,
endpoint: '/metrics', // Prometheus scrape endpoint
}),
],
})
class MyServer {}

Tracing

Tracing tracks the flow of requests through your system.

Setting Up Tracing

import { createTracer, setGlobalTracer, consoleExporter } from '@mcpkit-dev/core';

// Development: console output
const tracer = createTracer({
serviceName: 'my-server',
serviceVersion: '1.0.0',
exporters: [consoleExporter()],
});

// Production: OTLP exporter (Jaeger, Zipkin, etc.)
const prodTracer = createTracer({
serviceName: 'my-server',
serviceVersion: '1.0.0',
environment: 'production',
exporters: [
{
type: 'otlp',
endpoint: 'http://jaeger:4318/v1/traces',
},
],
sampler: { ratio: 0.1 }, // Sample 10% of requests
});

// Set as global tracer for @Traced decorator
setGlobalTracer(tracer);

Using @Traced Decorator

import { Traced } from '@mcpkit-dev/core';

@Tool({ description: 'Process order' })
@Traced({
name: 'order.process',
attributes: { 'order.type': 'standard' },
extractAttributes: (orderId) => ({ 'order.id': orderId }),
})
async processOrder(@Param({ name: 'orderId' }) orderId: string) {
return { status: 'completed' };
}

Manual Spans

import { withTrace, getGlobalTracer } from '@mcpkit-dev/core';

const tracer = getGlobalTracer();

async function complexOperation() {
// Create a span for a code section
return await withTrace(tracer, 'database.query', async (span) => {
span.setAttribute('db.system', 'postgresql');

const result = await db.query('SELECT * FROM users');

span.setAttribute('db.rows', result.length);
return result;
});
}

Tracing Plugin

Automatically trace all MCP operations:

@MCPServer({
name: 'my-server',
version: '1.0.0',
plugins: [
tracingPlugin({
tracer,
traceTools: true,
traceResources: true,
tracePrompts: true,
}),
],
})
class MyServer {}

Health Checks

Health checks report system status for load balancers and orchestrators.

Creating Health Checks

import { createHealthChecker } from '@mcpkit-dev/core';

const health = createHealthChecker();

// Add checks
health.addCheck('database', async () => {
const connected = await db.ping();
return {
status: connected ? 'healthy' : 'unhealthy',
message: connected ? 'Connected' : 'Connection failed',
};
});

health.addCheck('redis', async () => {
try {
await redis.ping();
return { status: 'healthy' };
} catch (error) {
return {
status: 'unhealthy',
message: error.message,
};
}
}, { critical: true }); // Mark as critical

health.addCheck('external-api', async () => {
const response = await fetch('https://api.example.com/health');
return {
status: response.ok ? 'healthy' : 'degraded',
details: { responseTime: response.headers.get('x-response-time') },
};
}, { critical: false }); // Non-critical

Check Status Values

StatusMeaningHTTP Code
healthyAll systems operational200
degradedSome issues, but functional200
unhealthySystem not functioning503

Running Health Checks

// Run all checks
const result = await health.check();
console.log(result);
// {
// status: 'healthy',
// checks: {
// database: { status: 'healthy', message: 'Connected' },
// redis: { status: 'healthy' },
// 'external-api': { status: 'healthy', details: { responseTime: '45ms' } }
// }
// }

// Run specific check
const dbHealth = await health.checkOne('database');

Kubernetes Probes

import { healthPlugin, createHealthChecker, createHealthHandler } from '@mcpkit-dev/core';

const health = createHealthChecker();

// Liveness: Is the process alive?
health.addCheck('process', async () => ({ status: 'healthy' }));

// Readiness: Can it handle traffic?
health.addCheck('database', async () => {
const ready = await db.isReady();
return { status: ready ? 'healthy' : 'unhealthy' };
}, { critical: true });

// For HTTP transport
@MCPServer({
name: 'my-server',
version: '1.0.0',
plugins: [
healthPlugin({
checker: health,
livePath: '/health/live', // Liveness probe
readyPath: '/health/ready', // Readiness probe
}),
],
})
class MyServer {}

// Kubernetes config:
// livenessProbe:
// httpGet:
// path: /health/live
// port: 3000
// readinessProbe:
// httpGet:
// path: /health/ready
// port: 3000

Combining All Three

A production server with full observability:

import {
MCPServer,
Tool,
Traced,
Monitor,
createMetricsCollector,
createHealthChecker,
createTracer,
metricsPlugin,
healthPlugin,
tracingPlugin,
setGlobalTracer,
} from '@mcpkit-dev/core';

// Initialize observability
const metrics = createMetricsCollector({ prefix: 'orderservice' });
const health = createHealthChecker();
const tracer = createTracer({
serviceName: 'order-service',
serviceVersion: '1.0.0',
exporters: [{ type: 'otlp', endpoint: process.env.OTLP_ENDPOINT! }],
});

setGlobalTracer(tracer);

// Custom metrics
const orderCounter = metrics.counter({
name: 'orders_total',
help: 'Total orders processed',
labels: ['status'],
});

// Health checks
health.addCheck('database', async () => {
const ok = await db.ping();
return { status: ok ? 'healthy' : 'unhealthy' };
}, { critical: true });

health.addCheck('payment-gateway', async () => {
const ok = await paymentGateway.healthCheck();
return { status: ok ? 'healthy' : 'degraded' };
});

@MCPServer({
name: 'order-service',
version: '1.0.0',
plugins: [
metricsPlugin({ collector: metrics, endpoint: '/metrics' }),
healthPlugin({ checker: health, livePath: '/health/live', readyPath: '/health/ready' }),
tracingPlugin({ tracer, traceTools: true }),
],
})
class OrderService {
@Tool({ description: 'Create a new order' })
@Traced({ name: 'order.create' })
@Monitor({ logDuration: true, logErrors: true })
async createOrder(
@Param({ name: 'customerId' }) customerId: string,
@Param({ name: 'items' }) items: string[]
) {
try {
const order = await db.createOrder({ customerId, items });
orderCounter.inc({ status: 'success' });
return order;
} catch (error) {
orderCounter.inc({ status: 'error' });
throw error;
}
}
}

Best Practices

Metrics

  • Use consistent naming: noun_verb_unit (e.g., http_requests_total)
  • Include relevant labels but avoid high cardinality
  • Set appropriate histogram buckets for your latency profile

Tracing

  • Trace at service boundaries, not every function
  • Include relevant attributes for debugging
  • Use sampling in production to reduce overhead

Health Checks

  • Keep checks fast (< 1 second)
  • Mark truly critical dependencies as critical: true
  • Use separate liveness and readiness probes

See Also