Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.agentfront.dev/llms.txt

Use this file to discover all available pages before exploring further.

FrontMCP can expose process metrics (CPU, RSS, heap, event-loop lag) and every framework counter (frontmcp_skills_*_total, plus any counter emitted via createCounter()) as a Prometheus scrape endpoint on the same HTTP listener as /healthz. The endpoint is off by default — turn it on with metrics: { enabled: true }.

Quick Start

import { FrontMcp } from '@frontmcp/sdk';

@FrontMcp({
  info: { name: 'my-server', version: '1.0.0' },
  apps: [MyApp],
  metrics: { enabled: true },
})
class Server {}
Then scrape it:
curl http://localhost:3001/metrics
# HELP frontmcp_process_resident_memory_bytes Resident memory size in bytes
# TYPE frontmcp_process_resident_memory_bytes gauge
frontmcp_process_resident_memory_bytes 50331648
# HELP frontmcp_process_uptime_seconds Time since process start in seconds
# TYPE frontmcp_process_uptime_seconds gauge
frontmcp_process_uptime_seconds 42
# TYPE frontmcp_skills_bundle_pulls_total counter
frontmcp_skills_bundle_pulls_total{source="npm"} 3
...
The Content-Type is the canonical Prometheus text/plain; version=0.0.4; charset=utf-8.

What the endpoint exposes

When enabled, every scrape returns:
CategoryExamplesSource
Process gaugesfrontmcp_process_resident_memory_bytes, frontmcp_process_heap_bytes, frontmcp_process_uptime_seconds, frontmcp_process_cpu_seconds_total{mode="user"|"system"}Built-in ProcessStatsCollector
Node.js gaugesfrontmcp_nodejs_eventloop_lag_seconds{quantile="mean"|"p99"}, frontmcp_nodejs_active_handles, frontmcp_nodejs_active_requests, frontmcp_nodejs_open_fds (Linux only)perf_hooks + process._getActiveHandles()
Framework countersfrontmcp_skills_bundle_pulls_total, frontmcp_skills_signature_*_total, frontmcp_skills_replay_*_total, frontmcp_skills_audit_*_total@frontmcp/observability’s in-memory counter snapshot store
Custom countersAnything emitted via createCounter('my_counter_total').inc()Same snapshot store
createCounter() from @frontmcp/observability writes into the same store the scrape reads from — no extra wiring needed.

Configuration

@FrontMcp({
  metrics: {
    enabled: true,                       // default: false
    path: '/metrics',                    // default: '/metrics'
    format: 'prometheus',                // 'prometheus' | 'json', default: 'prometheus'
    auth: 'public',                      // 'public' | 'token' | { token: string }, default: 'public'
    tokenEnv: 'FRONTMCP_METRICS_TOKEN',  // env var to read the bearer from when auth: 'token'
    include: ['process', 'skills'],      // optional category filter
    process: {
      eventLoopLag: true,                // include event-loop lag gauge
      fdCount: true,                     // include /proc/self/fd count (Linux only)
      activeHandles: true,               // include active-handles / active-requests counts
    },
  },
})

format: 'json'

Returns a { counters, gauges } envelope at Content-Type application/json — useful for tooling that prefers JSON over text parsing:
{
  "counters": [
    { "name": "frontmcp_skills_bundle_pulls_total", "count": 3, "attributes": { "source": "npm" } }
  ],
  "gauges": [
    { "name": "frontmcp_process_uptime_seconds", "value": 42 }
  ]
}

auth: 'token'

Sets Authorization: Bearer <token> as the gate. The token is read from process.env[tokenEnv] (default env var: FRONTMCP_METRICS_TOKEN) at startup. If the env var is unset, the service constructor throws MetricsTokenNotConfiguredError — failing fast so a token-gated endpoint never silently downgrades to public:
metrics: {
  enabled: true,
  auth: 'token',
  tokenEnv: 'FRONTMCP_METRICS_TOKEN',
}
Behaviour:
  • Missing Authorization header → 401
  • Authorization: Bearer <wrong>403
  • Authorization: Bearer <correct>200
For local / non-secret testing, an inline { token: '...' } literal also works but is discouraged in production:
metrics: {
  enabled: true,
  auth: { token: 'dev-only' },
}

include[] filter

Each category maps to a counter-name prefix:
CategoryPrefix matched
processfrontmcp_process_, frontmcp_nodejs_
skillsfrontmcp_skills_
toolsfrontmcp_tool_
resourcesfrontmcp_resource_
httpfrontmcp_http_
storagefrontmcp_storage_
authfrontmcp_auth_
sessionsfrontmcp_session_
Omit include to emit every category.

Path conflicts

metrics.path MUST NOT collide with MCP transport paths (/mcp, /sse, /messages). The service constructor throws MetricsPathConflictError at startup if it detects an overlap:
metrics: { enabled: true, path: '/mcp' }
// → throws MetricsPathConflictError at boot

Off-by-default rationale

The endpoint is opt-in because process metrics, framework counter names, and tool vocabularies can hint at deployment scale and feature usage (e.g. frontmcp_skills_signature_failures_total reveals a signing infra exists, frontmcp_auth_checks_total{result="denied"} correlates to attack attempts). Recommendations:
  • Internet-exposed deployments: use auth: 'token' or terminate the endpoint at a sidecar/ingress with a network ACL.
  • Cluster-local deployments: auth: 'public' matches the Prometheus / Kubernetes convention; ensure the Prometheus pod can reach the port and external traffic cannot.

How it interacts with OpenTelemetry

createCounter() from @frontmcp/observability writes to two places: the in-memory snapshot store (which this endpoint reads) AND any globally configured OTel MeterProvider (which forwards to OTLP / Prometheus exporter / Grafana Cloud). If you’ve already wired an OTel MeterProvider via metrics.setGlobalMeterProvider(), the values in the scrape match the values pushed via OTel — both paths share the same counter handles.

Adding custom counters

Use createCounter() from @frontmcp/observability. The counter is automatically included in the scrape:
import { createCounter } from '@frontmcp/observability';

const cacheHits = createCounter('my_cache_hits_total', 'Cache hits by tier');

// Later, in a tool / resource / provider:
cacheHits.inc(1, { tier: 'l1' });

// Next scrape returns:
// # TYPE my_cache_hits_total counter
// my_cache_hits_total{tier="l1"} 1
Label values should be bounded (status codes, enum members, tool names) — unbounded values (user IDs, URLs, JWTs) blow up the timeseries count.

Reference

  • @frontmcp/sdk exports: MetricsService, registerMetricsRoutes, MetricsPathConflictError, MetricsTokenNotConfiguredError
  • @frontmcp/observability exports: renderPrometheusExposition, renderJsonExposition, ProcessStatsCollector, PROMETHEUS_CONTENT_TYPE, createCounter, getMetricSnapshot
  • Type definitions: MetricsOptionsInterface, MetricsAuth, MetricsCategory, MetricsFormat, MetricsProcessOptionsInterface
  • Tracked under issue #397