ergzone-mcp 1.1.0 → 1.2.1
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 +15 -3
- package/package.json +1 -1
- package/src/client.mjs +21 -1
- package/src/tools.mjs +6 -9
package/README.md
CHANGED
|
@@ -16,9 +16,9 @@ You need a Concept2 Logbook account (the one you use on log.concept2.com).
|
|
|
16
16
|
|
|
17
17
|
1. Download `ergzone-mcp.mcpb` from the [latest release](https://github.com/malveo/ergzone-mcp/releases/latest).
|
|
18
18
|
2. Double-click it (or drag it into Claude Desktop → Settings → Extensions).
|
|
19
|
-
3. Type your Logbook email and password in the form
|
|
19
|
+
3. Type your Logbook email and password in the form, and make sure the extension is **enabled**. Done.
|
|
20
20
|
|
|
21
|
-
Nothing to install — no Node, no terminal, no config files.
|
|
21
|
+
Nothing to install — no Node, no terminal, no config files. Full walkthrough with screenshots: [docs/claude-desktop.md](docs/claude-desktop.md).
|
|
22
22
|
|
|
23
23
|
### Claude Code (terminal)
|
|
24
24
|
|
|
@@ -63,7 +63,19 @@ Prefer not to store your password? Use `ERGZONE_SESSION_TOKEN` instead (copied f
|
|
|
63
63
|
| `list_my_results` / `get_result` | your sessions + telemetry |
|
|
64
64
|
| `my_stats` / `analyze_result` | totals, HR zones, pace/SPI per interval |
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
## Example prompts
|
|
67
|
+
|
|
68
|
+
Just talk to Claude in plain language:
|
|
69
|
+
|
|
70
|
+
- **Check it works** — *"Am I connected to ErgZone? Who am I?"*
|
|
71
|
+
- **Browse** — *"List my ErgZone workouts."*
|
|
72
|
+
- **Build (preview, nothing saved)** — *"Show me an SPM ladder from 16 to 30 spm, 1 min each."*
|
|
73
|
+
- **Create** — *"Create a workout 'Tuesday tempo' with 3 × 2 min at 22 spm, 1 min rest."*
|
|
74
|
+
- **Progressive intensity** — *"Build a workout: 2 blocks of 1-2-1-3-1-4 minutes, each interval 0.1s/500m faster than the previous, 4 min rest between blocks."*
|
|
75
|
+
- **Results** — *"Show my last few ErgZone results."*
|
|
76
|
+
- **Analysis** — *"Analyze my last result: pace, SPI and HR zone per interval."*
|
|
77
|
+
- **Stats** — *"How many meters did I row this month?"*
|
|
78
|
+
- **Tidy up** — *"Delete the workout 'Tuesday tempo'."*
|
|
67
79
|
|
|
68
80
|
## Settings
|
|
69
81
|
|
package/package.json
CHANGED
package/src/client.mjs
CHANGED
|
@@ -14,7 +14,6 @@ const LOGBOOK = {
|
|
|
14
14
|
password: process.env.ERGZONE_LOGBOOK_PASSWORD || null,
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
export const DEFAULT_TRACK_ID = process.env.ERGZONE_TRACK_ID || '';
|
|
18
17
|
export const WRITE_ENABLED = process.env.ERGZONE_ALLOW_WRITE !== 'false';
|
|
19
18
|
|
|
20
19
|
// Normalized error: kind = config | network | auth | infra | graphql
|
|
@@ -109,3 +108,24 @@ export function todayISO() {
|
|
|
109
108
|
const p = (n) => String(n).padStart(2, '0');
|
|
110
109
|
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
111
110
|
}
|
|
111
|
+
|
|
112
|
+
// Resolve the track to use, in order: explicit arg -> ERGZONE_TRACK_ID -> auto-discover
|
|
113
|
+
// the user's personal "My Workouts" track. Discovery result is cached in memory.
|
|
114
|
+
let trackIdCache = null;
|
|
115
|
+
export async function resolveTrackId(explicit) {
|
|
116
|
+
if (explicit) return explicit;
|
|
117
|
+
if (process.env.ERGZONE_TRACK_ID) return process.env.ERGZONE_TRACK_ID;
|
|
118
|
+
if (trackIdCache) return trackIdCache;
|
|
119
|
+
|
|
120
|
+
const data = await gql('query{ tracks(onlyAdmin:true){ id name trackMode type isOwner } }');
|
|
121
|
+
const tracks = data.tracks || [];
|
|
122
|
+
const personal =
|
|
123
|
+
tracks.find((t) => t.trackMode === 'single-user' && t.isOwner) ||
|
|
124
|
+
tracks.find((t) => t.type === 'private') ||
|
|
125
|
+
tracks[0];
|
|
126
|
+
if (!personal) {
|
|
127
|
+
throw new ErgzoneError('No personal track found. Set ERGZONE_TRACK_ID.', { kind: 'config' });
|
|
128
|
+
}
|
|
129
|
+
trackIdCache = personal.id;
|
|
130
|
+
return trackIdCache;
|
|
131
|
+
}
|
package/src/tools.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// MCP tool definitions (Tier 1: core training + builder + basic analysis).
|
|
2
2
|
// Each tool: { name, description, inputSchema, handler, write?, destructive? }
|
|
3
3
|
|
|
4
|
-
import { gql, todayISO,
|
|
4
|
+
import { gql, todayISO, resolveTrackId } from './client.mjs';
|
|
5
5
|
import { resolveIntervals, validateIntervals } from './intervals.mjs';
|
|
6
6
|
|
|
7
7
|
// ---- analysis helpers ----
|
|
@@ -47,18 +47,17 @@ export const TOOLS = [
|
|
|
47
47
|
|
|
48
48
|
{
|
|
49
49
|
name: 'list_workouts',
|
|
50
|
-
description: 'List the workouts in a track (
|
|
50
|
+
description: 'List the workouts in a track (defaults to your "My Workouts" track, auto-detected). Optional filters: search, limit.',
|
|
51
51
|
inputSchema: {
|
|
52
52
|
type: 'object',
|
|
53
53
|
properties: {
|
|
54
|
-
trackId: { type: 'string', description: 'Track ID (default
|
|
54
|
+
trackId: { type: 'string', description: 'Track ID (default: auto-detected My Workouts)' },
|
|
55
55
|
search: { type: 'string' },
|
|
56
56
|
limit: { type: 'number', default: 50 },
|
|
57
57
|
},
|
|
58
58
|
},
|
|
59
59
|
async handler({ trackId, search, limit = 50 }) {
|
|
60
|
-
const t = trackId
|
|
61
|
-
if (!t) throw new Error('No trackId (pass trackId or set ERGZONE_TRACK_ID).');
|
|
60
|
+
const t = await resolveTrackId(trackId);
|
|
62
61
|
const d = await gql(
|
|
63
62
|
`query($t:[ID],$s:String){ workouts(trackIds:$t, search:$s){ id title status publishedAt intervalsLength workoutResultsCount } }`,
|
|
64
63
|
{ t: [t], s: search || null },
|
|
@@ -127,8 +126,7 @@ export const TOOLS = [
|
|
|
127
126
|
required: ['title'],
|
|
128
127
|
},
|
|
129
128
|
async handler(args) {
|
|
130
|
-
const trackId = args.trackId
|
|
131
|
-
if (!trackId) throw new Error('No trackId (pass trackId or set ERGZONE_TRACK_ID).');
|
|
129
|
+
const trackId = await resolveTrackId(args.trackId);
|
|
132
130
|
const intervals = resolveIntervals(args);
|
|
133
131
|
const errors = validateIntervals(intervals);
|
|
134
132
|
if (errors.length) throw new Error('Invalid intervals: ' + errors.join('; '));
|
|
@@ -173,8 +171,7 @@ export const TOOLS = [
|
|
|
173
171
|
required: ['id'],
|
|
174
172
|
},
|
|
175
173
|
async handler(args) {
|
|
176
|
-
const trackId = args.trackId
|
|
177
|
-
if (!trackId) throw new Error('No trackId.');
|
|
174
|
+
const trackId = await resolveTrackId(args.trackId);
|
|
178
175
|
|
|
179
176
|
// Fetch the current state for the fields that are not provided.
|
|
180
177
|
const cur = await gql(`query($id:ID!){ workout(id:$id){ title status publishedAt workoutType description } }`, { id: args.id });
|