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 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. Done.
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
- Ask in plain language, e.g. *"create an SPM ladder 16 to 30"* or *"analyze my last result"*.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ergzone-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Unofficial MCP server for ErgZone (Concept2 rowing). Zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "bin": {
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, DEFAULT_TRACK_ID } from './client.mjs';
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 (default: My Workouts). Optional filters: search, limit.',
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 from ERGZONE_TRACK_ID)' },
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 || DEFAULT_TRACK_ID;
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 || DEFAULT_TRACK_ID;
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 || DEFAULT_TRACK_ID;
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 });