From 90c87684078411df0b580f26725eb610b60cb788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 27 Jun 2026 17:38:29 +0200 Subject: [PATCH] feat(mcp): add a dedicated MCP measurement source for write provenance Append an MCP value to MeasurementSource (migration 0196, additive enum extension) so a row logged through the remote MCP endpoint is distinguishable from MANUAL/TELEGRAM provenance and gets an isolated idempotency namespace (userId, type, MCP, externalId). The value joins the read enum so existing read/response shapes decode the rows; the REST batch and single-POST write surfaces still reject it (server-resolved only). Regenerate the OpenAPI contract for the additive enum. --- docs/api/openapi.yaml | 116 ++++++++++++++++++ .../migration.sql | 14 +++ prisma/schema.prisma | 9 ++ src/lib/validations/measurement.ts | 7 ++ 4 files changed, 146 insertions(+) create mode 100644 prisma/migrations/0196_mcp_phase5_write_surface/migration.sql diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 586a1f894..edd7a098f 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1745,6 +1745,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP - in: query name: valueMin schema: @@ -2240,6 +2241,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP externalId: type: string maxLength: 128 @@ -3358,6 +3360,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP activeEnergy: maxItems: 8 type: array @@ -3375,6 +3378,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP walkingRunningDistance: maxItems: 8 type: array @@ -3392,6 +3396,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP flightsClimbed: maxItems: 8 type: array @@ -3409,6 +3414,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP sleep: maxItems: 8 type: array @@ -3426,6 +3432,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP weight: maxItems: 8 type: array @@ -3443,6 +3450,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP bloodPressure: maxItems: 8 type: array @@ -3460,6 +3468,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP pulse: maxItems: 8 type: array @@ -3477,6 +3486,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP bodyFat: maxItems: 8 type: array @@ -3494,6 +3504,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP bodyTemperature: maxItems: 8 type: array @@ -3511,6 +3522,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP spo2: maxItems: 8 type: array @@ -3528,6 +3540,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP hrv: maxItems: 8 type: array @@ -3545,6 +3558,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP restingHeartRate: maxItems: 8 type: array @@ -3562,6 +3576,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP vo2Max: maxItems: 8 type: array @@ -3579,6 +3594,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP skinTemperature: maxItems: 8 type: array @@ -3596,6 +3612,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP respiratoryRate: maxItems: 8 type: array @@ -3613,6 +3630,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP recovery: maxItems: 8 type: array @@ -3630,6 +3648,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP stress: maxItems: 8 type: array @@ -3647,6 +3666,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP metricPriority: type: object properties: @@ -3667,6 +3687,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP activeEnergy: maxItems: 8 type: array @@ -3684,6 +3705,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP walkingRunningDistance: maxItems: 8 type: array @@ -3701,6 +3723,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP flightsClimbed: maxItems: 8 type: array @@ -3718,6 +3741,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP sleep: maxItems: 8 type: array @@ -3735,6 +3759,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP weight: maxItems: 8 type: array @@ -3752,6 +3777,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP bloodPressure: maxItems: 8 type: array @@ -3769,6 +3795,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP pulse: maxItems: 8 type: array @@ -3786,6 +3813,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP bodyFat: maxItems: 8 type: array @@ -3803,6 +3831,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP bodyTemperature: maxItems: 8 type: array @@ -3820,6 +3849,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP spo2: maxItems: 8 type: array @@ -3837,6 +3867,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP hrv: maxItems: 8 type: array @@ -3854,6 +3885,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP restingHeartRate: maxItems: 8 type: array @@ -3871,6 +3903,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP vo2Max: maxItems: 8 type: array @@ -3888,6 +3921,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP skinTemperature: maxItems: 8 type: array @@ -3905,6 +3939,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP respiratoryRate: maxItems: 8 type: array @@ -3922,6 +3957,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP recovery: maxItems: 8 type: array @@ -3939,6 +3975,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP stress: maxItems: 8 type: array @@ -3956,6 +3993,7 @@ paths: - POLAR - OURA - TELEGRAM + - MCP deviceTypePriority: type: object properties: @@ -10743,6 +10781,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP notes: anyOf: - type: string @@ -11466,6 +11505,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP notes: anyOf: - type: string @@ -11709,6 +11749,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP - type: "null" start: type: string @@ -11836,6 +11877,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP - type: "null" start: type: string @@ -12209,6 +12251,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP externalId: anyOf: - type: string @@ -12318,6 +12361,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP externalId: anyOf: - type: string @@ -14529,6 +14573,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP activeEnergy: maxItems: 8 type: array @@ -14546,6 +14591,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP walkingRunningDistance: maxItems: 8 type: array @@ -14563,6 +14609,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP flightsClimbed: maxItems: 8 type: array @@ -14580,6 +14627,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP sleep: maxItems: 8 type: array @@ -14597,6 +14645,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP weight: maxItems: 8 type: array @@ -14614,6 +14663,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP bloodPressure: maxItems: 8 type: array @@ -14631,6 +14681,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP pulse: maxItems: 8 type: array @@ -14648,6 +14699,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP bodyFat: maxItems: 8 type: array @@ -14665,6 +14717,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP bodyTemperature: maxItems: 8 type: array @@ -14682,6 +14735,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP spo2: maxItems: 8 type: array @@ -14699,6 +14753,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP hrv: maxItems: 8 type: array @@ -14716,6 +14771,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP restingHeartRate: maxItems: 8 type: array @@ -14733,6 +14789,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP vo2Max: maxItems: 8 type: array @@ -14750,6 +14807,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP skinTemperature: maxItems: 8 type: array @@ -14767,6 +14825,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP respiratoryRate: maxItems: 8 type: array @@ -14784,6 +14843,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP recovery: maxItems: 8 type: array @@ -14801,6 +14861,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP stress: maxItems: 8 type: array @@ -14818,6 +14879,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP metricPriority: type: object properties: @@ -14838,6 +14900,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP activeEnergy: maxItems: 8 type: array @@ -14855,6 +14918,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP walkingRunningDistance: maxItems: 8 type: array @@ -14872,6 +14936,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP flightsClimbed: maxItems: 8 type: array @@ -14889,6 +14954,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP sleep: maxItems: 8 type: array @@ -14906,6 +14972,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP weight: maxItems: 8 type: array @@ -14923,6 +14990,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP bloodPressure: maxItems: 8 type: array @@ -14940,6 +15008,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP pulse: maxItems: 8 type: array @@ -14957,6 +15026,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP bodyFat: maxItems: 8 type: array @@ -14974,6 +15044,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP bodyTemperature: maxItems: 8 type: array @@ -14991,6 +15062,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP spo2: maxItems: 8 type: array @@ -15008,6 +15080,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP hrv: maxItems: 8 type: array @@ -15025,6 +15098,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP restingHeartRate: maxItems: 8 type: array @@ -15042,6 +15116,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP vo2Max: maxItems: 8 type: array @@ -15059,6 +15134,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP skinTemperature: maxItems: 8 type: array @@ -15076,6 +15152,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP respiratoryRate: maxItems: 8 type: array @@ -15093,6 +15170,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP recovery: maxItems: 8 type: array @@ -15110,6 +15188,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP stress: maxItems: 8 type: array @@ -15127,6 +15206,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP additionalProperties: false deviceTypePriority: type: object @@ -15193,6 +15273,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP activeEnergy: maxItems: 8 type: array @@ -15210,6 +15291,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP walkingRunningDistance: maxItems: 8 type: array @@ -15227,6 +15309,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP flightsClimbed: maxItems: 8 type: array @@ -15244,6 +15327,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP sleep: maxItems: 8 type: array @@ -15261,6 +15345,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP weight: maxItems: 8 type: array @@ -15278,6 +15363,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP bloodPressure: maxItems: 8 type: array @@ -15295,6 +15381,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP pulse: maxItems: 8 type: array @@ -15312,6 +15399,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP bodyFat: maxItems: 8 type: array @@ -15329,6 +15417,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP bodyTemperature: maxItems: 8 type: array @@ -15346,6 +15435,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP spo2: maxItems: 8 type: array @@ -15363,6 +15453,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP hrv: maxItems: 8 type: array @@ -15380,6 +15471,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP restingHeartRate: maxItems: 8 type: array @@ -15397,6 +15489,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP vo2Max: maxItems: 8 type: array @@ -15414,6 +15507,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP skinTemperature: maxItems: 8 type: array @@ -15431,6 +15525,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP respiratoryRate: maxItems: 8 type: array @@ -15448,6 +15543,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP recovery: maxItems: 8 type: array @@ -15465,6 +15561,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP stress: maxItems: 8 type: array @@ -15482,6 +15579,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP metricPriority: type: object properties: @@ -15502,6 +15600,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP activeEnergy: maxItems: 8 type: array @@ -15519,6 +15618,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP walkingRunningDistance: maxItems: 8 type: array @@ -15536,6 +15636,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP flightsClimbed: maxItems: 8 type: array @@ -15553,6 +15654,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP sleep: maxItems: 8 type: array @@ -15570,6 +15672,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP weight: maxItems: 8 type: array @@ -15587,6 +15690,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP bloodPressure: maxItems: 8 type: array @@ -15604,6 +15708,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP pulse: maxItems: 8 type: array @@ -15621,6 +15726,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP bodyFat: maxItems: 8 type: array @@ -15638,6 +15744,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP bodyTemperature: maxItems: 8 type: array @@ -15655,6 +15762,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP spo2: maxItems: 8 type: array @@ -15672,6 +15780,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP hrv: maxItems: 8 type: array @@ -15689,6 +15798,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP restingHeartRate: maxItems: 8 type: array @@ -15706,6 +15816,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP vo2Max: maxItems: 8 type: array @@ -15723,6 +15834,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP skinTemperature: maxItems: 8 type: array @@ -15740,6 +15852,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP respiratoryRate: maxItems: 8 type: array @@ -15757,6 +15870,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP recovery: maxItems: 8 type: array @@ -15774,6 +15888,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP stress: maxItems: 8 type: array @@ -15791,6 +15906,7 @@ components: - POLAR - OURA - TELEGRAM + - MCP additionalProperties: false deviceTypePriority: type: object diff --git a/prisma/migrations/0196_mcp_phase5_write_surface/migration.sql b/prisma/migrations/0196_mcp_phase5_write_surface/migration.sql new file mode 100644 index 000000000..c4055770a --- /dev/null +++ b/prisma/migrations/0196_mcp_phase5_write_surface/migration.sql @@ -0,0 +1,14 @@ +-- MCP write provenance. +-- +-- The remote MCP endpoint gains a confirmed write surface (logging a +-- measurement / mood). Rows it writes carry their own `MeasurementSource` +-- value so they are distinguishable from MANUAL / TELEGRAM provenance and +-- get an isolated idempotency namespace `(userId, type, MCP, externalId)`. +-- +-- Purely-additive enum extension; no row touched. `ADD VALUE IF NOT EXISTS` +-- makes a rerun safe. The new value is NOT used elsewhere in this migration, +-- so it is safe to extend the enum in the same step (Postgres only forbids +-- USING a freshly-added value in the same transaction). Mirrors the 0189 +-- source-extension that added TELEGRAM. The enum is `@@map`-ed to the +-- snake_case type name `measurement_source`. +ALTER TYPE "measurement_source" ADD VALUE IF NOT EXISTS 'MCP'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 172aafc9d..a24846b30 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1033,6 +1033,15 @@ enum MeasurementSource { // (server-resolved only); iOS DTO mirrors this spelling so the // /api/sync/changes delta feed decodes the rows. TELEGRAM + // v1.22.0 — measurements logged through the remote MCP write surface. A + // distinct provenance from MANUAL/TELEGRAM: the row was confirmed and + // written by an external assistant over the `/mcp` wire under a + // `health:write`-scoped token, never the cookie/Bearer client write path. + // Its own value gives the MCP writes an isolated idempotency namespace + // `(userId, type, MCP, externalId)`. The client batch + single-POST + // surfaces still reject it (server-resolved only); iOS DTO mirrors this + // spelling so the /api/sync/changes delta feed decodes the rows. + MCP @@map("measurement_source") } diff --git a/src/lib/validations/measurement.ts b/src/lib/validations/measurement.ts index 991094226..2c11ba6f0 100644 --- a/src/lib/validations/measurement.ts +++ b/src/lib/validations/measurement.ts @@ -184,6 +184,13 @@ export const measurementSourceEnum = z.enum([ // decode the rows it surfaces; the client-facing write surfaces still // reject it (see `WRITABLE_MEASUREMENT_SOURCES` + the batch allowlist). "TELEGRAM", + // v1.22.0 — measurements logged through the confirmed remote MCP write + // surface under a `health:write`-scoped token. Server-resolved (the row is + // written in-process over `/mcp`, never through the cookie/Bearer client + // write path), so it is excluded from `WRITABLE_MEASUREMENT_SOURCES`; part + // of this enum so the read/response shapes (and the iOS decoder) can decode + // the rows it surfaces. + "MCP", ]); /**