@gehlich/calicomp-mcp 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +200 -20
- package/dist/index.js +1219 -247
- package/package.json +18 -11
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@ CalisthenicsCompanion-MCP/
|
|
|
40
40
|
| `CALICOMP_PAT` | Personal Access Token (prefixed `calicomp_pat_`). Created in the CalisthenicsCompanion app. |
|
|
41
41
|
| `CALICOMP_KEY` | AES-256 encryption key in raw base64 (no prefix). Exported from the app alongside the PAT. |
|
|
42
42
|
| `CALICOMP_SERVER_URL` | *(Optional)* Override the API base URL. Defaults to `https://api.calicompanion.de`. |
|
|
43
|
+
| `CALICOMP_EXPORT_DIR` | *(Optional)* The directory that `outputFile: true` writes into. Absolute, or starting with `~/`. Defaults to a directory under the system temp dir. See [Export directory for outputFile](#export-directory-for-outputfile-since-140). |
|
|
43
44
|
|
|
44
45
|
## Credential Storage
|
|
45
46
|
|
|
@@ -103,9 +104,16 @@ returns two kinds of entries, distinguished by the `origin` field:
|
|
|
103
104
|
muscle-group, equipment and capability-axis ratings.
|
|
104
105
|
- **`"CUSTOM"`** — a user's own exercise, created because they cannot write to the curated
|
|
105
106
|
catalog at all. For the closed test circle this is the normal case, not an exception: a custom
|
|
106
|
-
exercise is real training data
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
exercise is real training data. Its key is `user_exercise:<id>`. Since phase 164 the app
|
|
108
|
+
encrypts an own exercise, and the encrypted payload carries the muscle groups, equipment and
|
|
109
|
+
capability ratings the athlete set; the MCP takes them over with the catalog's own keys, so
|
|
110
|
+
such an exercise counts in the weighted evaluations like a catalog exercise. A link to an id the
|
|
111
|
+
catalog does not know and a capability link without a level are left out, never invented. An
|
|
112
|
+
own exercise without links, or a plaintext row the app has not yet encrypted, has empty lists:
|
|
113
|
+
treat an empty rating as "not rated", never as a measured zero. An own exercise that cannot be
|
|
114
|
+
decrypted is left out of the catalog (`get_exercise_catalog` carries no `unreadable` list; the
|
|
115
|
+
row stands in the `unreadable` list of `get_history` and `get_training_state`) and is named with
|
|
116
|
+
`exerciseUnknown` wherever a tool names an exercise.
|
|
109
117
|
|
|
110
118
|
**This does not replace curating the catalog.** If you have admin access and can run the
|
|
111
119
|
catalog round trip (`tools/exercise.mjs` in the super-repo), your own exercises still belong in
|
|
@@ -184,8 +192,10 @@ The athlete can rename a catalog exercise in the app. Such an **override** is pe
|
|
|
184
192
|
encrypted: it replaces the name, the description or both of one catalog exercise in one language,
|
|
185
193
|
travels through the athlete's encrypted sync, and the server only ever holds ciphertext.
|
|
186
194
|
Since 1.3.0 the coach sees the own name next to the catalog name. Every field below is a **new
|
|
187
|
-
key**; every existing key keeps its value
|
|
188
|
-
|
|
195
|
+
key**; every existing key keeps its value, with one exception: `unreadable` can additionally name
|
|
196
|
+
an unreadable override row (below). Above all, `exerciseName` stays the catalog name, never the
|
|
197
|
+
own name; since phase 166 (protocol 1.50) the catalog name follows the effective language, see
|
|
198
|
+
[Names without `lang`](#names-without-lang).
|
|
189
199
|
|
|
190
200
|
| Tool | New keys |
|
|
191
201
|
|---|---|
|
|
@@ -200,14 +210,17 @@ byte for byte as before.
|
|
|
200
210
|
- **Which language.** An override applies to one language. The effective language is `lang` when
|
|
201
211
|
the tool receives it, otherwise the app language, the synchronized setting `default_language`.
|
|
202
212
|
Language codes match exactly (`de` does not match `de-DE`). `exerciseOwnNameLanguage` and
|
|
203
|
-
`ownNameLanguage` name the language of the own name
|
|
204
|
-
otherwise English)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
213
|
+
`ownNameLanguage` name the language of the own name. Since phase 166 `exerciseName` follows the
|
|
214
|
+
same effective language (its catalog translation, otherwise English), see
|
|
215
|
+
[Names without `lang`](#names-without-lang).
|
|
216
|
+
- **The app language.** `appLanguage` is the value of `default_language`, the athlete's explicit
|
|
217
|
+
choice, when it is a code of two to three lower-case letters, for example `de`; otherwise, since
|
|
218
|
+
phase 166 (protocol 1.50), the value of `effective_language`, a device fact the app writes on
|
|
219
|
+
every sync run (the choice, otherwise the device language, as a code of the app's language list,
|
|
220
|
+
otherwise `en`). Otherwise it is `null`, and `appLanguageUnavailableReason` says why, over both
|
|
221
|
+
rows: `setting_unreadable` (a row arrived but could not be decrypted or decoded), otherwise
|
|
222
|
+
`setting_unrecognized` (readable, but not such a code), otherwise `setting_not_synced` (neither
|
|
223
|
+
row: an older app without a choice). The tool never fails because of it.
|
|
211
224
|
- **Absent, not null.** Without an override in the effective language, with a description-only
|
|
212
225
|
override (see below), for the athlete's own exercises, a deleted or unknown exercise, and
|
|
213
226
|
against a server older than phase 156, the new name fields are missing; nothing else in the
|
|
@@ -216,13 +229,18 @@ byte for byte as before.
|
|
|
216
229
|
version 2 with `name: null`). The name then stays the catalog name and keeps following catalog
|
|
217
230
|
updates: the tools carry no `exerciseOwnName` for that exercise, and `get_exercise_catalog` no
|
|
218
231
|
`ownName`.
|
|
219
|
-
- **An unreadable override is visible
|
|
220
|
-
decoded, one with a blank name included (empty or only
|
|
221
|
-
text", since protocol 1.29), stands in `unreadable`
|
|
222
|
-
`
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
232
|
+
- **An unreadable override is visible in the six tools with an `unreadable` list.** An override
|
|
233
|
+
row that cannot be decrypted or decoded, one with a blank name included (empty or only
|
|
234
|
+
characters of the list in § 1.7 "Blank text", since protocol 1.29), stands in `unreadable` of
|
|
235
|
+
`get_history`, `get_stats`, `get_progress`, `get_training_state`, `get_adherence` and
|
|
236
|
+
`get_planned_workouts` with `entity: "exerciseNameOverride"`, its `id`, its `reason` and (since
|
|
237
|
+
phase 166) its plaintext routing `key` `"<exerciseId>:<languageCode>"`, never its content. It takes away only the own name of that exercise in that language; `exerciseName`,
|
|
238
|
+
`exerciseKey`, the own names of every other exercise and every other key stay complete. The
|
|
239
|
+
descriptions of those six tools say the same (`get_adherence` and `get_planned_workouts` output
|
|
240
|
+
no exercise names, so there it affects nothing). `get_template`, `get_suggestion` and
|
|
241
|
+
`get_exercise_catalog` carry no `unreadable` list: there an unreadable override, or without
|
|
242
|
+
`lang` an unreadable app language, only leaves the own name out. Call `get_history` or
|
|
243
|
+
`get_training_state` to see the entry.
|
|
226
244
|
- **No own description.** An overridden description is decoded but not output by any tool of
|
|
227
245
|
this version; a readable description-only override therefore changes no tool output.
|
|
228
246
|
- **Freshness.** A freshly entered own name can be missing for up to 60 seconds (snapshot cache).
|
|
@@ -231,6 +249,168 @@ The contract — the payload form, the conflict rule between devices, the decodi
|
|
|
231
249
|
language rule and the field names — is § 1.7 "Own names of catalog exercises" of
|
|
232
250
|
`docs/COACH-PLANNING-PROTOCOL.md` (version 1.29) in the CaliCompanion super-repo.
|
|
233
251
|
|
|
252
|
+
## Export directory for outputFile (since 1.4.0)
|
|
253
|
+
|
|
254
|
+
`get_history`, `get_progress` and `get_adherence` accept `outputFile: true`: the MCP then writes
|
|
255
|
+
the full result to a local JSON file and returns only its path, byte size and item count, so the
|
|
256
|
+
numbers never pass through the model (protocol §1.5, "The Local File Channel").
|
|
257
|
+
|
|
258
|
+
Where that file lands is the athlete's choice, never the model's. No tool takes a directory or a
|
|
259
|
+
file name as an argument. The directory comes only from the environment variable
|
|
260
|
+
`CALICOMP_EXPORT_DIR`, which you set yourself in the `env` block of the MCP registration, for
|
|
261
|
+
example:
|
|
262
|
+
|
|
263
|
+
```text
|
|
264
|
+
CALICOMP_EXPORT_DIR=~/training/personal
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
- **Without the variable** the files go to `calicomp-mcp-exports` under the system temp directory,
|
|
268
|
+
as before, and files older than 24 hours are deleted there at every start.
|
|
269
|
+
- **With the variable** the files go into that directory and stay: the 24-hour cleanup never runs
|
|
270
|
+
there. The value must be absolute or start with `~/` (your home directory), and it must name an
|
|
271
|
+
existing directory. It is resolved once at start; a symlinked directory is followed once, so a
|
|
272
|
+
later change of the link does not move the writes. A relative value, a missing directory or a
|
|
273
|
+
file stops the start with an error that names the variable but not the path.
|
|
274
|
+
- Every file is named by a random UUID with `.json`, created new with mode `0600` (owner read and
|
|
275
|
+
write only), and never written through an existing file or symlink of the same name.
|
|
276
|
+
|
|
277
|
+
The file holds decrypted training data in plaintext. With `CALICOMP_EXPORT_DIR` set it stays in
|
|
278
|
+
your directory after the conversation; it is written only when a tool call says `outputFile: true`.
|
|
279
|
+
Keep that directory out of any repository you publish.
|
|
280
|
+
|
|
281
|
+
## New in phase 166 (since 1.4.0)
|
|
282
|
+
|
|
283
|
+
Phase 166 makes the app and the MCP count the same sets by the same rules (§ 1.11 "Counting rules
|
|
284
|
+
shared by the app and the MCP" of `docs/COACH-PLANNING-PROTOCOL.md`, version 1.48, and § 1.6 in
|
|
285
|
+
version 1.49). Released in 1.4.0.
|
|
286
|
+
|
|
287
|
+
### Existing fields that change meaning
|
|
288
|
+
|
|
289
|
+
A corrected number changes the **existing** field in place: there is no parallel field, and the
|
|
290
|
+
name, the place and the form of every field stay as they are; only the sets it counts change. The
|
|
291
|
+
tool texts name the rule of each field below.
|
|
292
|
+
|
|
293
|
+
| Tool | Existing field | Before 166 | Since 166 |
|
|
294
|
+
|---|---|---|---|
|
|
295
|
+
| `get_training_state` | `muscleBalance` | every set of finished sessions, also of exercises whose rated capability axes are only `mobility` or `flexibility` | Rule S over Rule P: such stretch-only exercises are left out, the same number as the app's strength heatmap |
|
|
296
|
+
| `get_training_state` | `personalRecords` | the hold list by the catalog mode only, the repetition list without any mode | Rule M: by the mode the set was trained in (`maxHoldSeconds` only from sets that record a time, `maxReps` only from the others); since protocol 1.52 `maxHoldSeconds` leaves out station sets |
|
|
297
|
+
| `get_stats` | `by: "exercise"`, `by: "muscle"`: every field of the aggregate and `weightedSetCount` | no deletion marks, no `endTime`, no resolution over `workoutExerciseId` | Rule P with finished sessions; `by: "muscle"` still counts stretch-only exercises (no Rule S) |
|
|
298
|
+
| `get_stats` | `by: "capabilities"`: `axes[]` with `setCount`, `weightedSetCount`, `sessionCount`, `contributingExerciseIds`, `lastSessionDate` | the same three gaps | Rule P with finished sessions |
|
|
299
|
+
| `get_adherence` | `matches[]` and `unplannedSessionIds` | of several sessions of the same template on the same day as an occurrence, the one with the smaller `sessionId` won | the session with more live sets wins, then the longer one, then the old tie-break; `matchedCount` and `adherenceRatio` keep their definition |
|
|
300
|
+
| `get_adherence` | `removedWithCompletedSessionCount` and `removedWithCompletedSessionDates[]` | only over the roots with a live occurrence in the window | over every live recurring template root of the snapshot |
|
|
301
|
+
|
|
302
|
+
`removedOccurrenceCount` keeps its definition. On the template side it counts only roots with a
|
|
303
|
+
live occurrence in the window, on the foreign side (`foreignSport.removedOccurrenceCount`) every
|
|
304
|
+
live foreign root; the difference is deliberate and stays.
|
|
305
|
+
|
|
306
|
+
**Comparing across the change.** The comparison at the measuring point 2026-11-16 computes both
|
|
307
|
+
periods with the MCP from phase 166 on: the MCP changes no session data, only how it counts, so
|
|
308
|
+
the earlier period counted again by the new rules is comparable with the later one. A number an
|
|
309
|
+
MCP before phase 166 returned, also one noted down earlier, is not comparable with one counted
|
|
310
|
+
after it.
|
|
311
|
+
|
|
312
|
+
### New fields
|
|
313
|
+
|
|
314
|
+
| Tool | New keys |
|
|
315
|
+
|---|---|
|
|
316
|
+
| `get_training_state` | `capabilityBalance` (windows `last7Days`, `last30Days`, `allTime`, each `{ setCounts, weighted }`, stretching included on purpose) and `dataQuality` (`suspectSetCount`, `byReason`, at most 20 `newest`), both after the existing keys |
|
|
317
|
+
| `get_history` | `suspect` on a set-log that is probably an entry error: a list of `{ reason, explanation }` with `reps_on_time_station`, `zero_set` or `measured_time_missing`; absent on a set without a finding |
|
|
318
|
+
| `get_suggestion` | for an `exercise_update` proposal, the top-level `exerciseId` of the resolved copy is named like every other exercise reference |
|
|
319
|
+
| `get_template`, `get_history`, `get_progress`, `get_training_state`, `get_stats`, `get_suggestion` | `exerciseNameFreeText` beside `exerciseName` of an own exercise (key `user_exercise:<id>`, live or deleted): its shown name in the free-text envelope, because that name is the athlete's free text; `exerciseName` keeps its value |
|
|
320
|
+
| `get_exercise_catalog` | `nameFreeText` on an entry with `origin: "CUSTOM"`: its shown name (`localizedName` when present, else `nameEn`) in the free-text envelope; `nameEn` and `localizedName` keep their values |
|
|
321
|
+
| `get_training_state` | `personalRecords.repsInTime` as the last key of `personalRecords`, a new record kind (protocol 1.52): entries `{ exerciseId, exerciseName, durationSeconds, weightKg, value, achievedOn }` with the name fields of the other two lists, ordered by `exerciseId`, then `durationSeconds`, then `weightKg` (without weight first); one exercise may stand several times |
|
|
322
|
+
| `get_training_state` | `narrativeNames` directly after `narrative`, only when the sentence names an own exercise: `[{ ref, name }]` with `ref` = its `exerciseKey` in the sentence and `name` = its name in the free-text envelope (see [Names without `lang`](#names-without-lang)) |
|
|
323
|
+
| `get_history`, `get_progress`, `get_stats`, `get_training_state`, `get_adherence`, `get_planned_workouts` | `key` on an `unreadable` entry with `entity: "exerciseNameOverride"`: the row's plaintext routing key `"<exerciseId>:<languageCode>"`, so the coach knows whose own name is missing |
|
|
324
|
+
| `get_history`, `get_planned_workouts` | `droppedValues` after `unreadable`, only when a stored foreign `performedTime` or `scheduledTime` is not `HH:mm`: that time reads as `null`, the row stays readable, and the entry names `entity`, `id`, `field` and `reason: "not_hh_mm"`, never the value |
|
|
325
|
+
|
|
326
|
+
Since protocol 1.51 (gap plan 166-21) `reps_on_time_station` marks only repetitions on an exercise
|
|
327
|
+
whose catalog mode is `TIME` (repetitions of a repetition exercise at a `TIME` station are real
|
|
328
|
+
counts, recorded before app plan 165-02), and `zero_set` falls back to the other value when the
|
|
329
|
+
value of the set's kind is absent.
|
|
330
|
+
|
|
331
|
+
Since protocol 1.52 (gap plan 166-22) such a set is a station set: it records a time, the catalog
|
|
332
|
+
mode of its exercise is not `TIME`, and it carries `completedReps`. A station set never counts for
|
|
333
|
+
`maxHoldSeconds` or `maxReps`; `personalRecords.repsInTime` keeps the largest `completedReps` per
|
|
334
|
+
exercise, duration (the set's own `completedTimeSeconds`, greater than 0) and weight class
|
|
335
|
+
(`weightUsed` in hundredths of a kilogram; `null` or `0` is the class without weight, reported as
|
|
336
|
+
`weightKg: null`), compared only at the same duration and weight.
|
|
337
|
+
|
|
338
|
+
The marks are hints only: no tool changes or deletes a set, the athlete decides in the app.
|
|
339
|
+
|
|
340
|
+
### New proposals
|
|
341
|
+
|
|
342
|
+
- **Block ops.** `propose_plan_update` takes the ops `updateBlock` (the rounds of an existing block,
|
|
343
|
+
1 to 100) and `addBlock` (a new block with its exercises in the shared top-level `orderIndex`
|
|
344
|
+
space), for CLASSIC templates only (app 4.5 and later). Every op acts only on rows of the
|
|
345
|
+
proposal's template.
|
|
346
|
+
- **New exercises with links.** `propose_new_exercise` and `newExercises[]` of both plan tools take
|
|
347
|
+
the optional fields `translations`, `muscleGroups`, `capabilities` and `equipment`, keys as
|
|
348
|
+
`get_exercise_catalog` shows them; an unknown capability or equipment key is refused with an
|
|
349
|
+
error that names it. None of the fields is required.
|
|
350
|
+
- **Changing an exercise.** The new tool `propose_exercise_update` (proposal type
|
|
351
|
+
`exercise_update`) proposes `translations`, `muscleGroups` and `capabilities` for an own
|
|
352
|
+
exercise, and only `translations` for a catalog exercise, as the athlete's personal name and
|
|
353
|
+
description; the shared catalog never changes. It is live once the server has been deployed with
|
|
354
|
+
the type.
|
|
355
|
+
- **Hold sizes.** The proposal tools now say how `mode` behaves: `TIME` counts down and is a
|
|
356
|
+
target (a set that ran out stores that target), `MAX` counts up and is a measurement. A maximal
|
|
357
|
+
hold, such as a benchmark hold, is proposed with `MAX`. The reading tools say the same about the
|
|
358
|
+
stored hold times.
|
|
359
|
+
|
|
360
|
+
Nothing is applied by the MCP: the athlete accepts, edits or rejects every proposal in the app.
|
|
361
|
+
|
|
362
|
+
### Names without `lang`
|
|
363
|
+
|
|
364
|
+
Since protocol 1.50 (the athlete's decision E-16: without `lang` the coach gets the language set in
|
|
365
|
+
the app) every name a tool returns follows one effective language: `lang`, otherwise the app
|
|
366
|
+
language `appLanguage` (`default_language`, otherwise the device fact `effective_language` the app
|
|
367
|
+
mirrors on every sync run, so it is known also when the athlete never chose one). This changes the
|
|
368
|
+
meaning of existing fields in place, without parallel fields:
|
|
369
|
+
|
|
370
|
+
- `exerciseName` in `get_template`, `get_history`, `get_progress`, `get_training_state`,
|
|
371
|
+
`get_stats` and `get_suggestion`: a catalog exercise's translation in that language, otherwise its
|
|
372
|
+
English name; an own exercise's translation, otherwise its name.
|
|
373
|
+
- `typeName` of a predefined foreign sport type in `get_history`, `get_training_state`,
|
|
374
|
+
`get_adherence` and `get_planned_workouts` (the last two have no `lang` and take the app
|
|
375
|
+
language): German when the language starts with `de`, otherwise English.
|
|
376
|
+
- `localizedName` of `get_exercise_catalog`, present whenever there is an effective language.
|
|
377
|
+
- The name of a catalog exercise in `get_training_state.narrative`; the prose stays English.
|
|
378
|
+
- Own names, as before, in the same language. When the app language is unknown or unreadable, no
|
|
379
|
+
own name appears and `get_training_state` names the reason in `appLanguageUnavailableReason`; a
|
|
380
|
+
call with `lang` shows the own names of that language.
|
|
381
|
+
|
|
382
|
+
Without `lang` and without an app language every name is English as before. With `lang`, `lang`
|
|
383
|
+
wins. No athlete free text stands unwrapped in a text field: when `narrative` names an own
|
|
384
|
+
exercise, the sentence carries its `exerciseKey` (`user_exercise:<id>`), and `narrativeNames`
|
|
385
|
+
right after it carries its name in the free-text envelope. Every name tool says this in one shared
|
|
386
|
+
sentence of its description. Releases before 1.4.0 keep the earlier behaviour.
|
|
387
|
+
|
|
388
|
+
### Export directory
|
|
389
|
+
|
|
390
|
+
`outputFile: true` writes into the directory set in `CALICOMP_EXPORT_DIR`; see
|
|
391
|
+
[Export directory for outputFile](#export-directory-for-outputfile-since-140).
|
|
392
|
+
|
|
393
|
+
### Tool texts against behaviour
|
|
394
|
+
|
|
395
|
+
No tool claims any longer to read no encrypted data: every tool that loads the snapshot loads it
|
|
396
|
+
decrypted, and `get_template`, `get_suggestion` and `get_exercise_catalog` say which of their
|
|
397
|
+
fields come from encrypted data and when an own name or an own exercise can be missing. A test
|
|
398
|
+
over the import chain of every tool module keeps that claim out.
|
|
399
|
+
|
|
400
|
+
## Changelog
|
|
401
|
+
|
|
402
|
+
### 1.4.0
|
|
403
|
+
|
|
404
|
+
- **Export directory:** `outputFile: true` can write into a directory you choose with
|
|
405
|
+
`CALICOMP_EXPORT_DIR`; see
|
|
406
|
+
[Export directory for outputFile](#export-directory-for-outputfile-since-140).
|
|
407
|
+
- **Counting rules shared with the app:** some existing fields keep their name, form and place but
|
|
408
|
+
count different sets, and names follow the effective language; see
|
|
409
|
+
[New in phase 166](#new-in-phase-166-since-140).
|
|
410
|
+
- **Dependencies:** `@modelcontextprotocol/sdk` 1.32.2 and `zod` 4.6.7. Development tools: Vitest 5
|
|
411
|
+
with v8 coverage 5, tsup 9, ESLint 10.14, typescript-eslint 8.72, Stryker 10.1.
|
|
412
|
+
- **Node:** still `>=22`.
|
|
413
|
+
|
|
234
414
|
## Building
|
|
235
415
|
|
|
236
416
|
```bash
|