MCP Metric Surface
Use this workflow for any change that can add, rename, remove, or reinterpret data exposed by MCP.
Read First
Read docs/mcp-server.md. Read docs/training-workspace.md completely for Training or derived-metric work. For a Sports
Lib version or parser change, also use .agent/skills/sports-lib-upgrade-and-reparse/SKILL.md.
Classify the Change
- Sports Lib numeric event stat: the MCP catalog must discover it from the public
DataStore, canonicalize it throughDynamicDataLoader, and expose it only when that canonical stat is actually persisted for the user. Do not add a second hand-maintained metric registry. - First-class body measurement: keep the automatic numeric Sports Lib catalog authoritative for class existence,
canonical type, unit, and numeric validation. Add a deliberately allowlisted semantic entry in
functions/src/mcp/measurement-catalog.tsonly when the value is meaningful and safe as a personal measurement. Preserve identity-free date buckets; never expose exact source timestamps, event/activity identity, names, labels, provider/device metadata, or source provenance. Update consent, Help, Policies, the public MCP page, and focused catalog/query tests in the same change. - Recorded Health metric: extend only the explicit public allowlist in
functions/src/mcp/health.service.ts. Preservehealth:read, the additional Body measurements grant and identity-free date buckets for body composition, and the separate Weight/Sleep contracts. Keep sample parent-revision checks, source-separated semantics, Sports Lib display/unit preferences, read/output bounds, and strict negative leakage fixtures. Never resolve Sleep references through Health permission or infer permission for new sensitive metric families from a shared catalog addition. Native exceptions require an exact provider/metric/unit/semantic allowlist and explicit native-labelled output; Garmin Body Battery points must never be relabelled as a canonical percentage. - Training-derived kind: register it in
shared/derived-metrics.ts, preserve the normal snapshot build lifecycle, and expose only a ready server-side snapshot. Add its exact identity-free payload schema tofunctions/src/mcp/derived-output-schemas.ts; the exhaustive map must fail compilation until the new kind is covered. - Training plans and planned workouts: distinct from Training-derived metrics and completed activities. Review every
planning change for MCP impact. Relevant plan/workout capabilities, lifecycle semantics, recipe fields and safe
delivery-status changes must extend the plan-specific MCP surface in the same feature PR. Coordinate explicit
Firestore projections, strict schemas, independent
training-plans:readconsent, owner/connection-bound references, revision-bound pagination and byte limits, Sports Lib unit formatting, Assistant routing/evidence, bundled Training, Activity and cross-domain guidance, tests and documentation. Keep detailed behavior indocs/mcp-server.mdanddocs/training-workspace.md; distinguish implementation from deployed/registered-client availability. Never forward whole records or automatically expose new stored fields. Preserve registered schemas using the compatible additive-tool lifecycle when a shape cannot safely change. Keep private delivery fields private. Keep every manually mirrored workout recipe discriminant behind an exhaustive compile-time coverage map and fixtures that JSON-round-trip each shared variant through public read and write validation. A shared-model addition must fail closed until its MCP schema, formatting, Assistant/plugin authoring guidance, contract digest and tests are reviewed; never make the coverage map an automatic field-exposure mechanism. Read extensions usetraining-plans:read. New mutation capability is never implied: it must fit the explicit safe Training lifecycle, use the independenttraining-plans:writeortraining-delivery:writechild scope, enter one bounded preview, bind owner/connection/grant/revision/expiry, and expose an idempotent apply as a separately approval-gated write tool with accurate annotations. The external MCP host owns its native approval UI; do not use MCP elicitation solely to reconfirm a fully specified write call, and document that server code cannot detect a client's automatic-approval setting. User guidance must also call out unattended connector modes such as Claude Research and tell users to disable Training write tools there when per-call review is required. The built-in Assistant may expose preview to the model but never apply; app-owned confirmation must recheck its server-owned conversation generation. Provider actions reuse the server delivery command and Pro/readiness gates, never accept credentials or remote IDs, and return independent outcomes without rolling back authored data. Permanent workout deletion, plan deletion, history restore, inferred completion and new provider actions require a new explicit contract decision rather than silently widening the existing union. Record a genuine no-impact rationale in verification notes. If relevant coverage must be deferred, create or reuse a focused #583 subissue, add/verify it in Project 2, and reference it before declaring completion. Existing read/write coverage does not authorize wider consent, additional mutations, provider transport calls or deployment. - Sleep field or provider: update the normalized contract in
shared/sleep.ts, then deliberately decide whether it belongs in the MCP safe projection. Never forward provider user/session identifiers, provider payloads, raw stage intervals, or raw HRV, SpO2, or respiration samples. - Activity-detail field: decide whether it belongs in the explicit activity summary, lap, jump, or swim-length
projection. Never forward whole activity documents, raw streams, creator/device metadata, source keys, names/notes,
internal identifier fields, arbitrary stats, or parser extensions. The separate
get_activity_descriptiontool may read only the parent event description with bothactivity-descriptions:readandactivity-details:read; this never widens normal activity/metric projections. Preserve explicit consent, full-text bounds and untrusted-context semantics. Exact activity start/end and jump coordinates, nearby search, and chart breadcrumbs require dependentactivity-location:readin addition toactivity-details:read. - On-demand activity chart stream: add deliberate aliases and canonical units to
functions/src/mcp/activity-stream.service.ts(re-exported by the chart service), request only the stream and Sports Lib derivation dependencies, and preserve the existing original-file-only workflow. Keep file, raw/decompressed byte, selected-sample, runtime, response, point, and per-connection/user parse budgets. Downsample the complete domain; never crop, persist parsed output, invoke reparse/auto-healing, or return original files, full-resolution streams, absolute sample timestamps, source metadata, or unrequested streams. - Detailed activity samples: preserve the additive
get_activity_samplescontract and existingactivity-details:readgrant. Share the chart catalog, selective original-file parser, identity matcher and source budgets; project only selected canonical numeric arrays on an elapsed-second grid. Keep aligned null gaps, complete range pagination, exact units, whole-result byte bounds, owner/connection/query/source-bound expiring cursors, and source revision and access checks on every page. Cache only the safe selected projection within the documented short-lived process-memory limits; never persist streams, cache raw files or parser objects, expose coordinates, call provider APIs, backfill data or silently truncate a parse. Review external-client versus Assistant routing. - Saved-route field or parser output: decide whether it belongs in the explicit route summary, preview, or waypoint
projection. Never forward original files, raw points/streams, Storage paths, source/delivery provenance, waypoint text,
links, or extensions. Exact route bounds, preview geometry, nearby search, and waypoint coordinates require dependent
route-location:readin addition toroutes:read. - Local plugin or bundled workflow skill: keep
plugins/quantified-self/plugin.template.json, the repo-local marketplace, branding, the three manifest-level starter prompts, and all seven focused/cross-domain workflow skills aligned with the public MCP surface. Review the affected single-domain skill and the cross-domain skill whenever a domain changes. Do not duplicate complete tool names or metric IDs in a skill; make it discover authoritative runtime tools and catalogs. Keep each skill'sagents/openai.yamlprompt, one hosted MCP dependency, and implicit-invocation policy aligned. Keep the ChatGPT technical app ID and generated cache-busted bundle files out of Git. - OAuth consent UI: initialize selected permissions from the complete validated scope list returned for the authorization request. Every requested current and future scope starts checked. Do not maintain per-scope default-off filters. Users must remain able to uncheck independent scopes before approval; removing a parent scope must remove and disable its dependent child scopes. Preselection is presentation state only and never creates or expands a grant without explicit approval.
Implementation Contract
- Keep root and Functions on the same Sports Lib version. Confirm a new numeric class is public, enumerable from
DataStore, has a stable canonicaltype, and round-trips through persisted event JSON. - Determine whether historical event reparsing or a derived schema bump is required. Document and test that transition.
- Preserve the explicit IANA timezone contract for date bucketing and the legacy local-time behavior for existing aggregation callers that omit a timezone.
- Treat
functions/src/mcp/metric-catalog.ts,functions/src/mcp/measurement-catalog.ts, andfunctions/src/mcp/data.service.tsas the MCP projection boundary. Expand allowlists deliberately; do not return whole Firestore documents. - Keep OAuth scopes least-privilege:
metrics:read,measurements:read,sleep:read,activity-details:read, androutes:readremain data grants.activity-location:readdepends on activity details androute-location:readdepends on routes; the two location domains remain independent. Enforce those dependencies in consent, approval, refresh, bearer validation, HTTP prechecks, tool registration, and data reads. First-class measurement types must also be excluded from generic and per-activity metric paths so those tools cannot bypassmeasurements:read. Keep queries bounded, references/cursors UID-and-connection-bound, and ordinary data tools read-only. Training mutation tools are the sole exception and must preserve the strict preview/native-approval/idempotent-apply boundary above. Update OAuth metadata, consent, Settings, Help, policies, anddocs/mcp-server.mdwhen the user-visible contract moves. - For every new Sports Lib detail or route field, update the named MCP allowlist, add a negative leakage test for nearby sensitive fields, confirm historical persistence/reparse expectations, review the Firestore query/index shape, and document units and operational limits. A Sports Lib export alone never authorizes MCP exposure.
- For every new or changed MCP tool or output field, update the matching entry in
functions/src/mcp/tool-output-schemas.ts. Use recursively strict objects. Model optional fields only when the key can be absent; use nullable fields when the key is present with no value. Keep canonical units, opaque references, timestamps/date ranges, pagination cursors, counts, and result arrays explicit. Parent-only activity and route schemas must omit their location fields entirely. Do not widen a public schema from an internal object, and do not useany,unknown, a catch-all object, or an unconstrained dynamic map as a shortcut. - Keep one schema in charge of advertisement and enforcement: the registration wrapper must advertise it, validate the
projected value before serialization, return the validated value as
structuredContent, and emit equivalent JSON text for compatibility. Expected errors remain text-onlyisErrorresults. Update the in-memory contract fixture for every affected tool, every derived kind, optional/nullable and pagination states, and add a negative leakage canary for each sensitive neighboring field. - Preserve the registered MCP contract in
functions/src/mcp/contracts/registered-contract.json. Existing tool names, authorization-profile availability, annotations, security schemes, and input/output schemas are frozen after registration; use an additive tool for a new shape. Runnpm --prefix functions run mcp:contract:checkfor every public MCP change. Compatible new tools or metadata require the digest-bound pending change record and the documented developer refresh or published-version lifecycle; a pending record never overrides a breaking finding. Never edit the registered baseline or append-only transition history directly; use the verified promotion command. - Classify local-plugin follow-up explicitly. Tool names, descriptions, schemas, scopes, or server instructions require
a deployed-server update and a rescan of the registered ChatGPT app. Plugin manifest, starter-prompt, icon, or bundled
skill changes require
npm run plugin:syncafter validation. Server implementation changes that preserve the public contract do not require a plugin rebuild. - Treat the exported bundled-skill registry in
tools/quantified-self-plugin/plugin-tool.mjsas exhaustive. Add a new skill with the official skill scaffolder, then update that registry, source and installed-tree fixtures, per-skill starter prompt, MCP dependency, README, anddocs/mcp-server.mdtogether. The source directory must contain exactly the registered skills, and installed validation must compare every regular file recursively and reject symlinks.
Verify
Add or update focused tests for:
- automatic Sports Lib discovery and alias canonicalization;
- first-class measurement catalog resolution, measurement-specific value validation, timezone/DST bucketing, aggregation, range/work/response limits, missing history, and explicit identity/provenance exclusion;
- persistence availability and any reparse expectation;
- Training ready-state handling and identity redaction;
- planned-workout scope isolation, complete recipe/Unicode validation, field-mask leakage rejection, reference replay, revision/deletion/consent fences, calendar scopes, bounded pagination and complete/incomplete delivery aggregates;
- Training proposal scope dependencies, safe-operation union, opaque proposal replay, stale revision/grant/conversation rejection, native approval annotations on every transport, idempotent apply, standalone-create-plus-send, provider-failure isolation, and the absence of provider transport from preview;
- Assistant optional Training consent reset/retry generation, planned-versus-completed routing and compact untrusted-text evidence. Review three cases: a new workout target needs explicit MCP schema/format tests; presentation-only spacing needs a documented no-wire-impact rationale; provider-internal artifact metadata must stay excluded;
- exhaustive tool/output-schema registration, Ajv validation of every successful
structuredContentresult, JSON-text equivalence, exact Training payload-kind pairing, and generic text-only contract-mismatch errors; - sleep safe projection and explicit raw/provider-field exclusion;
- activity-detail and route allowlists, parent/location authorization matrices, exact-coordinate redaction, opaque-reference binding, selective on-demand parsing, identity ambiguity, complete-domain downsampling, and every source/sample/runtime/point/response/rate limit;
- IANA timezone/DST bucketing;
- scope denial and query limits;
- consent initialization with every requested scope checked, independent-scope unchecking, parent/child removal, and approval as the only grant boundary.
Then run npm --prefix functions test -- src/mcp/tool-output-schemas.spec.ts plus the focused Functions tests,
npm --prefix functions run mcp:contract:check, the affected frontend tests, the Firestore rules suite when access changes,
npm run plugin:tools, the official skill validator for every bundled skill,
npm --prefix tools/quantified-self-plugin test, a fixture-ID npm run plugin:validate when the local package or public
tool contract is affected, and git diff --check. Forward-test representative prompts for every affected workflow,
missing-permission handling, and ambiguous single-domain/cross-domain routing. Do not deploy, publish Sports Lib, start
a production reparse, install the plugin into a real profile, or mutate cloud configuration as part of this workflow.