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
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:
- Missing
Authorizationheader → 401 Authorization: Bearer <wrong>→ 403Authorization: Bearer <correct>→ 200
{ 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
UsecreateCounter() from @frontmcp/observability. The counter is automatically included in the scrape:
Reference
@frontmcp/sdkexports:MetricsService,registerMetricsRoutes,MetricsPathConflictError,MetricsTokenNotConfiguredError@frontmcp/observabilityexports:renderPrometheusExposition,renderJsonExposition,ProcessStatsCollector,PROMETHEUS_CONTENT_TYPE,createCounter,getMetricSnapshot- Type definitions:
MetricsOptionsInterface,MetricsAuth,MetricsCategory,MetricsFormat,MetricsProcessOptionsInterface - Tracked under issue #397