@gehlich/calicomp-mcp 1.0.0 → 1.1.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 +53 -0
- package/dist/index.js +32 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -41,6 +41,36 @@ CalisthenicsCompanion-MCP/
|
|
|
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
43
|
|
|
44
|
+
## Credential Storage
|
|
45
|
+
|
|
46
|
+
The "Copy MCP config" button in the app's PAT settings screen produces the JSON block shown
|
|
47
|
+
under [Usage](#usage) below, with `CALICOMP_PAT` and `CALICOMP_KEY` filled in. That block is not
|
|
48
|
+
an ordinary configuration snippet — **both values are plaintext secrets**. `CALICOMP_KEY` in
|
|
49
|
+
particular is what turns the encrypted payload the server stores back into readable training
|
|
50
|
+
data; the server itself only ever sees ciphertext. Treat the whole block the same way you would
|
|
51
|
+
treat a password.
|
|
52
|
+
|
|
53
|
+
**Where it must NOT go:** any file your project's version control tracks. Pasting the block into
|
|
54
|
+
a repo-committed config file — even a "local" one that ends up staged by accident — puts a live
|
|
55
|
+
token and decryption key into git history, which is not something a later `git rm` undoes.
|
|
56
|
+
|
|
57
|
+
**Where it should go instead:** a file outside any repository that only your own user account
|
|
58
|
+
can read (`chmod 600`), or your MCP client's user-scope server registration instead of a
|
|
59
|
+
project-scope one. Concretely, the same approach this project uses for its own recurring MCP
|
|
60
|
+
calls: an env file under your user config directory (e.g. `~/.config/calicomp/mcp.env`, mode
|
|
61
|
+
`600`) that a wrapper script or your client's env-var substitution reads at invocation time — or,
|
|
62
|
+
if your client supports it directly, register the server in its *user* config rather than a
|
|
63
|
+
project's `.mcp.json` (Claude Code, for example, distinguishes `claude mcp add --scope user` from
|
|
64
|
+
a project-scoped registration). Either way, the secret lives in exactly one place your user
|
|
65
|
+
account controls, never in a directory a `git add .` could reach.
|
|
66
|
+
|
|
67
|
+
**What the snippet leaves out:** `CALICOMP_SERVER_URL`. For the hosted default
|
|
68
|
+
(`https://api.calicompanion.de`, see the table above) this is harmless — the server falls back to
|
|
69
|
+
that same default when the variable is absent, so registration works without it. If you run your
|
|
70
|
+
own server, however, the snippet gives you no hint that this variable exists or that you need to
|
|
71
|
+
add it by hand; without it you will silently talk to the wrong server with no error message
|
|
72
|
+
pointing at the cause. Add `CALICOMP_SERVER_URL` to the `env` block yourself in that case.
|
|
73
|
+
|
|
44
74
|
## Transport
|
|
45
75
|
|
|
46
76
|
Runs as a local stdio MCP server. The LLM client communicates via JSON-RPC 2.0
|
|
@@ -64,6 +94,29 @@ exclusive JSON-RPC channel.
|
|
|
64
94
|
}
|
|
65
95
|
```
|
|
66
96
|
|
|
97
|
+
## Exercise Catalog: Curated Entries and Custom Exercises (since 1.1.0)
|
|
98
|
+
|
|
99
|
+
`get_exercise_catalog` — and every training-state calculation that resolves an exercise id —
|
|
100
|
+
returns two kinds of entries, distinguished by the `origin` field:
|
|
101
|
+
|
|
102
|
+
- **`"CATALOG"`** — the curated, admin-maintained catalog (`GET /api/exercises`). These carry
|
|
103
|
+
muscle-group, equipment and capability-axis ratings.
|
|
104
|
+
- **`"CUSTOM"`** — a user's own exercise, created because they cannot write to the curated
|
|
105
|
+
catalog at all. For the closed test circle this is the normal case, not an exception: a custom
|
|
106
|
+
exercise is real training data, but it carries **no** muscle, equipment or capability rating —
|
|
107
|
+
there is nothing to invent, so those fields are always empty. Treat an empty rating as "not
|
|
108
|
+
rated", never as a measured zero.
|
|
109
|
+
|
|
110
|
+
**This does not replace curating the catalog.** If you have admin access and can run the
|
|
111
|
+
catalog round trip (`tools/exercise.mjs` in the super-repo), your own exercises still belong in
|
|
112
|
+
the curated catalog — cleanly, with real ratings. The `CUSTOM` read path exists for everyone who
|
|
113
|
+
cannot do that, so their training data is not silently invisible to the coach. Two user groups,
|
|
114
|
+
two answers; this section covers the one that has no other option.
|
|
115
|
+
|
|
116
|
+
`propose_new_exercise`'s duplicate check only ever matches against `"CATALOG"` entries — a
|
|
117
|
+
proposal for a new catalog exercise can never be blocked by someone else's same-named custom
|
|
118
|
+
exercise.
|
|
119
|
+
|
|
67
120
|
## Building
|
|
68
121
|
|
|
69
122
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -848,9 +848,34 @@ function normalizeCatalog(wireCatalog) {
|
|
|
848
848
|
...ex,
|
|
849
849
|
capabilities: ex.capabilities ?? [],
|
|
850
850
|
equipment: ex.equipment ?? [],
|
|
851
|
-
isSkill: ex.isSkill ?? false
|
|
851
|
+
isSkill: ex.isSkill ?? false,
|
|
852
|
+
origin: "CATALOG"
|
|
852
853
|
}));
|
|
853
854
|
}
|
|
855
|
+
function projectUserExerciseToCatalog(ex, allTranslations) {
|
|
856
|
+
const translations = allTranslations.filter((t) => t.exerciseId === ex.id).map((t) => ({
|
|
857
|
+
languageCode: t.languageCode,
|
|
858
|
+
name: t.name,
|
|
859
|
+
description: t.description
|
|
860
|
+
}));
|
|
861
|
+
return {
|
|
862
|
+
id: ex.id,
|
|
863
|
+
key: `user_exercise:${ex.id}`,
|
|
864
|
+
nameEn: ex.name,
|
|
865
|
+
mode: ex.mode,
|
|
866
|
+
usesWeight: ex.usesWeight,
|
|
867
|
+
description: ex.description,
|
|
868
|
+
lastModifiedAt: ex.updatedAt,
|
|
869
|
+
translations,
|
|
870
|
+
// Never invented — an unrated exercise contributes nothing to a weighted evaluation, and a
|
|
871
|
+
// fabricated non-empty value here would look like a real measurement (T-141-80).
|
|
872
|
+
muscleGroups: [],
|
|
873
|
+
equipment: [],
|
|
874
|
+
isSkill: false,
|
|
875
|
+
capabilities: [],
|
|
876
|
+
origin: "CUSTOM"
|
|
877
|
+
};
|
|
878
|
+
}
|
|
854
879
|
async function doFetch(pat2, keyB642, serverUrl2) {
|
|
855
880
|
const [rawSnapshot, profile, rawCatalog] = await Promise.all([
|
|
856
881
|
fetchSnapshot(pat2, serverUrl2),
|
|
@@ -858,7 +883,10 @@ async function doFetch(pat2, keyB642, serverUrl2) {
|
|
|
858
883
|
fetchCatalog(serverUrl2)
|
|
859
884
|
]);
|
|
860
885
|
const snapshot = decryptMerge(rawSnapshot, keyB642);
|
|
861
|
-
const
|
|
886
|
+
const curatedCatalog = normalizeCatalog(rawCatalog);
|
|
887
|
+
const curatedIds = new Set(curatedCatalog.map((ex) => ex.id));
|
|
888
|
+
const projectedUserExercises = snapshot.exercises.filter((ex) => !ex.deletedAt && !curatedIds.has(ex.id)).map((ex) => projectUserExerciseToCatalog(ex, snapshot.exerciseTranslations));
|
|
889
|
+
const catalog = [...curatedCatalog, ...projectedUserExercises];
|
|
862
890
|
return { snapshot, catalog, profile, fetchedAt: Date.now() };
|
|
863
891
|
}
|
|
864
892
|
async function getSnapshot(pat2, keyB642, serverUrl2) {
|
|
@@ -1437,7 +1465,7 @@ function registerToolGetExerciseCatalog(server2, cfg) {
|
|
|
1437
1465
|
"get_exercise_catalog",
|
|
1438
1466
|
{
|
|
1439
1467
|
title: "Get Exercise Catalog",
|
|
1440
|
-
description: 'Return the full exercise catalog with muscle groups, equipment, and optional localized names. Provide `lang` (e.g. "de", "fr") to add a `localizedName` field (falls back to `nameEn` when no translation exists). Use `muscleGroups[].key` with `get_stats { by: "muscle" }` to query aggregates by muscle group.',
|
|
1468
|
+
description: 'Return the full exercise catalog with muscle groups, equipment, and optional localized names. Provide `lang` (e.g. "de", "fr") to add a `localizedName` field (falls back to `nameEn` when no translation exists). Use `muscleGroups[].key` with `get_stats { by: "muscle" }` to query aggregates by muscle group. The result includes both curated catalog entries AND the user\'s own custom exercises \u2014 check `origin` ("CATALOG" vs "CUSTOM") to tell them apart. A "CUSTOM" entry carries no muscle-group, equipment or capability-axis rating, so it never contributes to a weighted evaluation (muscle balance, capability balance) \u2014 treat that absence as "not rated", never as a real zero.',
|
|
1441
1469
|
inputSchema: GetExerciseCatalogSchema,
|
|
1442
1470
|
annotations: { readOnlyHint: true }
|
|
1443
1471
|
},
|
|
@@ -2534,7 +2562,7 @@ function findCatalogMatch(proposedName, catalog) {
|
|
|
2534
2562
|
const key = normalizeExerciseName(proposedName);
|
|
2535
2563
|
if (!key) return void 0;
|
|
2536
2564
|
return catalog.find(
|
|
2537
|
-
(ex) => normalizeExerciseName(ex.nameEn) === key || ex.translations.some((t) => normalizeExerciseName(t.name) === key)
|
|
2565
|
+
(ex) => ex.origin === "CATALOG" && (normalizeExerciseName(ex.nameEn) === key || ex.translations.some((t) => normalizeExerciseName(t.name) === key))
|
|
2538
2566
|
);
|
|
2539
2567
|
}
|
|
2540
2568
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gehlich/calicomp-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Local stdio MCP for CalisthenicsCompanion",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/Packi1992/calicomp-mcp"
|
|
9
|
+
"url": "git+https://github.com/Packi1992/calicomp-mcp.git"
|
|
10
10
|
},
|
|
11
11
|
"bin": {
|
|
12
12
|
"calicomp-mcp": "dist/index.js"
|