Skip to main content
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

Then scrape it:
The Content-Type is the canonical Prometheus text/plain; version=0.0.4; charset=utf-8. The Web-standard handler serves the same endpoint: FrontMcpInstance.createFetchHandler(config) (Bun, Deno, a Node fetch server) answers GET /metrics (or metrics.path) with the same body, auth and headers as the Express listener. Like the Express endpoint, it reads the counters through @frontmcp/observability, which must be installed. On Cloudflare Workers, require() cannot reach bundled modules, so frontmcp build --target cloudflare imports @frontmcp/observability in the generated worker entry and hands it to the SDK with registerOptionalModule() whenever the @FrontMcp source names metrics or observability and the package is installed, including an env-gated block that is off at build time (the build warns when the config enables them and the package is not installed). Process gauges the runtime does not implement (CPU, event-loop lag, handles) are left out of the scrape instead of failing it.

What the endpoint exposes

When enabled, every scrape returns: A gauge the runtime cannot measure is left out rather than reported as 0: a Cloudflare Worker, which has no process, serves none of the process gauges. Up to 1.9.2 a Worker listed five frontmcp_process_* gauges, all 0. createCounter() from @frontmcp/observability writes into the same store the scrape reads from — no extra wiring needed.

Configuration

format: 'json'

Returns a { counters, gauges } envelope at Content-Type application/json — useful for tooling that prefers JSON over text parsing:

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:
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:

include[] filter

Each category maps to a counter-name prefix: Omit include to emit every category. FrontMCP itself counts only the skills counters above: it records no request, tool-call, error or duration counters, so the other categories match the counters you create with those prefixes (createCounter('frontmcp_tool_my_calls_total')).

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:
createFetchHandler() also refuses a metrics.path equal to its MCP entry path (http.entryPath, / when unset), with the same MetricsPathConflictError, because the metrics route would otherwise answer the GET that MCP clients use to open the event stream. For the same reason it refuses a metrics.path at or under the path of a splitByApp or standalone app (<entryPath>/<appId>), such as /metrics with an app whose id is metrics.

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:
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