@dynamicsninja/garminconnect-mcp 0.7.0 → 0.8.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/dist/cli.js CHANGED
@@ -1,13 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { createRequire } from "module";
5
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
5
 
7
6
  // ../src/errors.ts
8
7
  var GarminError = class extends Error {
9
- constructor(message, options) {
10
- super(message, options);
8
+ constructor(message2, options) {
9
+ super(message2, options);
11
10
  this.name = new.target.name;
12
11
  Error.captureStackTrace?.(this, new.target);
13
12
  }
@@ -17,15 +16,15 @@ var GarminAuthError = class extends GarminError {
17
16
  var GarminConnectionError = class extends GarminError {
18
17
  };
19
18
  var GarminRateLimitError = class extends GarminError {
20
- constructor(message, retryAfter, options) {
21
- super(message, options);
19
+ constructor(message2, retryAfter, options) {
20
+ super(message2, options);
22
21
  this.retryAfter = retryAfter;
23
22
  }
24
23
  retryAfter;
25
24
  };
26
25
  var GarminHttpError = class extends GarminError {
27
- constructor(message, status, url, body, options) {
28
- super(message, options);
26
+ constructor(message2, status, url, body, options) {
27
+ super(message2, options);
29
28
  this.status = status;
30
29
  this.url = url;
31
30
  this.body = body;
@@ -354,18 +353,18 @@ var CSRF_RE = /name="_csrf"\s+value="(.+?)"/;
354
353
  var TITLE_RE = /<title>([\s\S]*?)<\/title>/i;
355
354
  var TICKET_RE = /\?ticket=(ST-[^"&\s]+)/;
356
355
  var MFA_VAR_RE = /var\s+(customerGuid|mfaMethod|locale|clientId|codeSentTo)\s*=\s*"([^"]*)"\s*;/g;
357
- function csrfFrom(html) {
358
- return CSRF_RE.exec(html)?.[1] ?? null;
356
+ function csrfFrom(html2) {
357
+ return CSRF_RE.exec(html2)?.[1] ?? null;
359
358
  }
360
- function titleFrom(html) {
361
- return TITLE_RE.exec(html)?.[1]?.trim() ?? "";
359
+ function titleFrom(html2) {
360
+ return TITLE_RE.exec(html2)?.[1]?.trim() ?? "";
362
361
  }
363
- function ticketFrom(html) {
364
- return TICKET_RE.exec(html)?.[1] ?? null;
362
+ function ticketFrom(html2) {
363
+ return TICKET_RE.exec(html2)?.[1] ?? null;
365
364
  }
366
- function mfaVarsFrom(html) {
365
+ function mfaVarsFrom(html2) {
367
366
  const vars = {};
368
- for (const [, name, value] of html.matchAll(MFA_VAR_RE)) {
367
+ for (const [, name, value] of html2.matchAll(MFA_VAR_RE)) {
369
368
  if (name !== void 0 && value !== void 0) vars[name] = value;
370
369
  }
371
370
  return vars;
@@ -428,8 +427,8 @@ async function widgetLogin(email, password, fetcher, domain, opts) {
428
427
  body: new URLSearchParams({ username: email, password, embed: "true", _csrf: csrf }).toString(),
429
428
  timeoutMs: 3e4
430
429
  });
431
- const html = await res.text();
432
- const title = titleFrom(html);
430
+ const html2 = await res.text();
431
+ const title = titleFrom(html2);
433
432
  const lower2 = title.toLowerCase();
434
433
  if (isServerErrorTitle(title)) {
435
434
  throw new GarminConnectionError(`Widget login: server error '${title}'`);
@@ -440,9 +439,9 @@ async function widgetLogin(email, password, fetcher, domain, opts) {
440
439
  if (RESTRICTED_HINTS.some((h) => lower2.includes(h))) {
441
440
  throw new GarminAuthError(`SSO error: ACCOUNT_RESTRICTED: ${title}`);
442
441
  }
443
- const vars = mfaVarsFrom(html);
442
+ const vars = mfaVarsFrom(html2);
444
443
  if (lower2.includes("mfa") || vars.mfaMethod) {
445
- const mfaCsrf = csrfFrom(html);
444
+ const mfaCsrf = csrfFrom(html2);
446
445
  if (!mfaCsrf) throw new GarminConnectionError("Widget login: MFA page has no CSRF token");
447
446
  const mfaReferer = fetcher.lastUrl ?? urls.signin;
448
447
  await requestMfaCode(fetcher, urls.base, vars, mfaReferer);
@@ -462,7 +461,7 @@ async function widgetLogin(email, password, fetcher, domain, opts) {
462
461
  if (title !== "Success") {
463
462
  throw new GarminConnectionError(`Widget login: unexpected page '${title}'`);
464
463
  }
465
- const ticket = ticketFrom(html);
464
+ const ticket = ticketFrom(html2);
466
465
  if (!ticket) throw new GarminAuthError("Widget login: missing service ticket");
467
466
  return opts.complete(ticket, urls.embed);
468
467
  }
@@ -481,13 +480,13 @@ async function widgetResume(state, code, fetcher, complete) {
481
480
  }).toString(),
482
481
  timeoutMs: 3e4
483
482
  });
484
- const html = await res.text();
485
- const title = titleFrom(html);
483
+ const html2 = await res.text();
484
+ const title = titleFrom(html2);
486
485
  if (isServerErrorTitle(title)) {
487
486
  throw new GarminConnectionError(`Widget MFA: server error '${title}'`);
488
487
  }
489
488
  if (title !== "Success") throw new GarminAuthError(`SSO error: INVALID_MFA_CODE: ${title}`);
490
- const ticket = ticketFrom(html);
489
+ const ticket = ticketFrom(html2);
491
490
  if (!ticket) throw new GarminAuthError("Widget login: missing service ticket");
492
491
  return complete(ticket, urls.embed);
493
492
  }
@@ -534,8 +533,8 @@ async function parseJson(res) {
534
533
  function requireSuccess(body) {
535
534
  const type = body.responseStatus?.type ?? "UNKNOWN";
536
535
  if (type !== SSO_SUCCESSFUL) {
537
- const message = body.responseStatus?.message;
538
- throw new GarminAuthError(`SSO error: ${message ? `${type}: ${message}` : type}`);
536
+ const message2 = body.responseStatus?.message;
537
+ throw new GarminAuthError(`SSO error: ${message2 ? `${type}: ${message2}` : type}`);
539
538
  }
540
539
  return body;
541
540
  }
@@ -574,16 +573,16 @@ async function mobileCredentials(email, password, ctx, params) {
574
573
  var WIDGET_CONTEXT = "Mobile login rate limited; ";
575
574
  function explainWidgetFailure(err) {
576
575
  if (!(err instanceof GarminError) || err instanceof GarminRateLimitError) return err;
577
- const message = WIDGET_CONTEXT + err.message;
576
+ const message2 = WIDGET_CONTEXT + err.message;
578
577
  const options = { cause: err };
579
578
  if (err instanceof GarminAuthError) {
580
- return err.message.startsWith("SSO error:") ? err : new GarminAuthError(message, options);
579
+ return err.message.startsWith("SSO error:") ? err : new GarminAuthError(message2, options);
581
580
  }
582
- if (err instanceof GarminConnectionError) return new GarminConnectionError(message, options);
581
+ if (err instanceof GarminConnectionError) return new GarminConnectionError(message2, options);
583
582
  if (err instanceof GarminHttpError) {
584
- return new GarminHttpError(message, err.status, err.url, err.body, options);
583
+ return new GarminHttpError(message2, err.status, err.url, err.body, options);
585
584
  }
586
- return err.constructor === GarminError ? new GarminError(message, options) : err;
585
+ return err.constructor === GarminError ? new GarminError(message2, options) : err;
587
586
  }
588
587
  async function login(email, password, ctx) {
589
588
  const params = loginParamsFor(ctx.domain);
@@ -4274,8 +4273,8 @@ var REPEAT_HINTS = {
4274
4273
  };
4275
4274
  var SpecError = class extends GarminError {
4276
4275
  };
4277
- function fail(path3, message) {
4278
- throw new SpecError(`${path3}: ${message}`);
4276
+ function fail(path3, message2) {
4277
+ throw new SpecError(`${path3}: ${message2}`);
4279
4278
  }
4280
4279
  function show(v) {
4281
4280
  if (v === void 0) return "nothing";
@@ -4620,14 +4619,20 @@ var WORKOUT_SPEC_JSON_SCHEMA = {
4620
4619
  // src/session.ts
4621
4620
  import os from "os";
4622
4621
  import path from "path";
4622
+ var setting = (value) => {
4623
+ const v = value?.trim();
4624
+ return v && !v.includes("${") ? v : void 0;
4625
+ };
4623
4626
  function loadConfig(env = process.env) {
4624
4627
  const home = os.homedir();
4625
- const groups = (env["GARMIN_MCP_GROUPS"] ?? "").split(",").map((g) => g.trim()).filter(Boolean);
4628
+ const groups = (setting(env["GARMIN_MCP_GROUPS"]) ?? "").split(",").map((g) => g.trim()).filter(Boolean);
4629
+ const graphql = (setting(env["GARMIN_MCP_ENABLE_GRAPHQL"]) ?? "").toLowerCase();
4630
+ const downloadDir = setting(env["GARMIN_MCP_DOWNLOAD_DIR"]);
4626
4631
  return {
4627
- tokenDir: env["GARMIN_MCP_TOKEN_DIR"] || path.join(home, ".garminconnect-mcp", "tokens"),
4628
- downloadDir: env["GARMIN_MCP_DOWNLOAD_DIR"] || path.join(home, "Downloads", "garmin"),
4632
+ tokenDir: setting(env["GARMIN_MCP_TOKEN_DIR"]) ?? path.join(home, ".garminconnect-mcp", "tokens"),
4633
+ downloadDir: downloadDir && path.isAbsolute(downloadDir) ? downloadDir : path.join(home, "Downloads", "garmin"),
4629
4634
  groups: groups.length > 0 ? new Set(groups) : null,
4630
- enableGraphql: env["GARMIN_MCP_ENABLE_GRAPHQL"] === "1"
4635
+ enableGraphql: graphql === "1" || graphql === "true"
4631
4636
  };
4632
4637
  }
4633
4638
  var NotLoggedInError = class extends GarminAuthError {
@@ -4653,7 +4658,7 @@ function fileSession(tokenDir, options = {}) {
4653
4658
  }
4654
4659
 
4655
4660
  // src/errors.ts
4656
- var LOGIN_HINT = "run `npx @dynamicsninja/garminconnect-mcp login` in a terminal, then try again";
4661
+ var LOGIN_HINT = "call the `sign_in_to_garmin` tool to sign in, then try again";
4657
4662
  function describeError(error) {
4658
4663
  if (error instanceof NotLoggedInError) return `Not logged in to Garmin: ${LOGIN_HINT}.`;
4659
4664
  if (error instanceof GarminAuthError) {
@@ -4731,6 +4736,16 @@ import {
4731
4736
  CallToolRequestSchema,
4732
4737
  ListToolsRequestSchema
4733
4738
  } from "@modelcontextprotocol/sdk/types.js";
4739
+
4740
+ // src/icon.ts
4741
+ var SERVER_ICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect x="22" y="2" width="20" height="60" rx="6" fill="#1f2937"/><circle cx="32" cy="32" r="20" fill="#0f766e"/><circle cx="32" cy="32" r="15" fill="#f8fafc"/><path d="M32 21v11l7 5" fill="none" stroke="#0f766e" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/><rect x="51" y="27" width="5" height="10" rx="2" fill="#1f2937"/></svg>';
4742
+ var SERVER_ICON = {
4743
+ src: `data:image/svg+xml;base64,${Buffer.from(SERVER_ICON_SVG, "utf8").toString("base64")}`,
4744
+ mimeType: "image/svg+xml",
4745
+ sizes: ["any"]
4746
+ };
4747
+
4748
+ // src/server.ts
4734
4749
  function createServer(deps, factories) {
4735
4750
  const tools = factories.flatMap((factory) => factory(deps));
4736
4751
  const byName = /* @__PURE__ */ new Map();
@@ -4739,7 +4754,13 @@ function createServer(deps, factories) {
4739
4754
  byName.set(def.tool.name, def);
4740
4755
  }
4741
4756
  const server = new Server(
4742
- { name: "garminconnect-mcp", version: deps.version ?? "0.0.0" },
4757
+ {
4758
+ name: "garminconnect-mcp",
4759
+ title: "Garmin Connect (unofficial)",
4760
+ version: deps.version ?? "0.0.0",
4761
+ websiteUrl: "https://github.com/DynamicsNinja/garminconnect-js/tree/main/mcp#readme",
4762
+ icons: [SERVER_ICON]
4763
+ },
4743
4764
  { capabilities: { tools: {} } }
4744
4765
  );
4745
4766
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: tools.map((t) => t.tool) }));
@@ -9555,16 +9576,296 @@ function toolFor(m, deps) {
9555
9576
  }
9556
9577
  };
9557
9578
  }
9579
+ var KNOWN_GROUPS = new Set(GARMIN_METHODS.map((m) => m.category));
9580
+ function resolveGroups(requested) {
9581
+ if (requested === null) return null;
9582
+ const byLower = new Map([...KNOWN_GROUPS].map((g) => [g.toLowerCase(), g]));
9583
+ const resolved = /* @__PURE__ */ new Set();
9584
+ const unknown = [];
9585
+ for (const raw of requested) {
9586
+ const match = byLower.get(raw.trim().toLowerCase());
9587
+ if (match) resolved.add(match);
9588
+ else unknown.push(raw);
9589
+ }
9590
+ if (unknown.length > 0) {
9591
+ process.stderr.write(
9592
+ `garminconnect-mcp: ignoring unknown GARMIN_MCP_GROUPS value(s): ${unknown.join(", ")}. Valid: ${[...KNOWN_GROUPS].sort().join(", ")}
9593
+ `
9594
+ );
9595
+ }
9596
+ if (resolved.size === 0) {
9597
+ process.stderr.write("garminconnect-mcp: no valid GARMIN_MCP_GROUPS value remained; loading all tool groups.\n");
9598
+ return null;
9599
+ }
9600
+ return resolved;
9601
+ }
9558
9602
  var methodTools = (deps) => {
9559
- const known = new Set(GARMIN_METHODS.map((m) => m.category));
9560
- if (deps.config.groups) {
9561
- const unknown = [...deps.config.groups].filter((g) => !known.has(g));
9562
- if (unknown.length > 0) {
9563
- throw new Error(`Unknown GARMIN_MCP_GROUPS value(s): ${unknown.join(", ")}. Valid: ${[...known].sort().join(", ")}`);
9603
+ const groups = resolveGroups(deps.config.groups);
9604
+ return GARMIN_METHODS.filter((m) => !Object.hasOwn(EXCLUDED, m.name)).filter((m) => !Object.hasOwn(OPT_IN, m.name) || deps.config[OPT_IN[m.name]]).filter((m) => groups === null || groups.has(m.category)).map((m) => toolFor(m, deps));
9605
+ };
9606
+
9607
+ // src/signin-tool.ts
9608
+ import { spawn } from "child_process";
9609
+
9610
+ // src/signin-page.ts
9611
+ import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
9612
+ import http from "http";
9613
+ var HEADERS = {
9614
+ "content-type": "text/html; charset=utf-8",
9615
+ "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'",
9616
+ "cache-control": "no-store",
9617
+ "x-frame-options": "DENY",
9618
+ "referrer-policy": "no-referrer"
9619
+ };
9620
+ var MAX_BODY = 16 * 1024;
9621
+ var ENTITIES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
9622
+ var escape = (s) => s.replace(/[&<>"']/g, (c) => ENTITIES[c] ?? c);
9623
+ function html(title, body) {
9624
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escape(title)}</title><style>
9625
+ body{font-family:system-ui,sans-serif;background:#f8fafc;color:#0f172a;display:grid;place-items:center;min-height:100vh;margin:0}
9626
+ main{background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:28px;width:min(360px,90vw)}
9627
+ h1{font-size:1.25rem;margin:0 0 4px}p{color:#475569;margin:0 0 16px}
9628
+ label{display:block;font-size:.9rem;margin:12px 0 4px}input{width:100%;box-sizing:border-box;padding:10px;border:1px solid #cbd5e1;border-radius:8px;font-size:1rem}
9629
+ button{margin-top:18px;width:100%;padding:10px;border:0;border-radius:8px;background:#0f766e;color:#fff;font-size:1rem;cursor:pointer}
9630
+ .error{background:#fef2f2;color:#991b1b;border-radius:8px;padding:10px;margin-bottom:8px}
9631
+ </style></head><body><main>${body}</main></body></html>`;
9632
+ }
9633
+ function credentialsForm(key2, error, email = "") {
9634
+ return html(
9635
+ "Sign in to Garmin",
9636
+ `<h1>Sign in to Garmin Connect</h1><p>For the Garmin tools in Claude. Your password goes only to Garmin.</p>
9637
+ ${error ? `<div class="error">${escape(error)}</div>` : ""}
9638
+ <form method="post"><input type="hidden" name="key" value="${escape(key2)}"><input type="hidden" name="step" value="credentials">
9639
+ <label for="email">Email</label><input id="email" name="email" type="email" autocomplete="username" required value="${escape(email)}">
9640
+ <label for="password">Password</label><input id="password" name="password" type="password" autocomplete="current-password" required>
9641
+ <button type="submit">Sign in</button></form>`
9642
+ );
9643
+ }
9644
+ function mfaForm(key2, error) {
9645
+ return html(
9646
+ "Garmin verification code",
9647
+ `<h1>Enter your verification code</h1><p>Garmin sent a code to your email or phone.</p>
9648
+ ${error ? `<div class="error">${escape(error)}</div>` : ""}
9649
+ <form method="post"><input type="hidden" name="key" value="${escape(key2)}"><input type="hidden" name="step" value="mfa">
9650
+ <label for="code">Code</label><input id="code" name="code" inputmode="numeric" autocomplete="one-time-code" required>
9651
+ <button type="submit">Verify</button></form>`
9652
+ );
9653
+ }
9654
+ var donePage = (name) => html("Signed in", `<h1>Signed in as ${escape(name)}</h1><p>You can close this tab and go back to Claude.</p>`);
9655
+ function readBody(req) {
9656
+ return new Promise((resolve, reject) => {
9657
+ let size = 0;
9658
+ const chunks = [];
9659
+ req.on("data", (c) => {
9660
+ size += c.length;
9661
+ if (size > MAX_BODY) {
9662
+ reject(new Error("Request too large"));
9663
+ req.destroy();
9664
+ } else chunks.push(c);
9665
+ });
9666
+ req.on("end", () => resolve(new URLSearchParams(Buffer.concat(chunks).toString("utf8"))));
9667
+ req.on("error", reject);
9668
+ req.on("close", () => reject(new Error("Request aborted")));
9669
+ });
9670
+ }
9671
+ var message = (e) => e instanceof Error ? e.message : String(e);
9672
+ async function startSignInPage(options) {
9673
+ const key2 = randomBytes2(32).toString("base64url");
9674
+ const keyBytes = Buffer.from(key2);
9675
+ const isKey = (candidate) => {
9676
+ if (candidate == null) return false;
9677
+ const bytes = Buffer.from(candidate);
9678
+ return bytes.length === keyBytes.length && timingSafeEqual(bytes, keyBytes);
9679
+ };
9680
+ const idleMs = options.idleMs ?? 15 * 6e4;
9681
+ let client = null;
9682
+ let mfaState = null;
9683
+ let finished = false;
9684
+ let signedInName = "your Garmin account";
9685
+ let isClosed = false;
9686
+ let idle;
9687
+ let port = 0;
9688
+ let queue = Promise.resolve();
9689
+ const serialized = (step2) => {
9690
+ const run = queue.then(step2);
9691
+ queue = run.catch(() => {
9692
+ });
9693
+ return run;
9694
+ };
9695
+ let resolveClosed;
9696
+ const closed = new Promise((r) => resolveClosed = r);
9697
+ const server = http.createServer((req, res) => {
9698
+ void handle(req, res).catch(() => {
9699
+ if (!res.headersSent) res.writeHead(500, HEADERS);
9700
+ res.end(html("Error", "<h1>Something went wrong</h1><p>Close this tab and ask Claude to sign in again.</p>"));
9701
+ });
9702
+ });
9703
+ let closing;
9704
+ const close = () => closing ??= new Promise((resolve) => {
9705
+ isClosed = true;
9706
+ clearTimeout(idle);
9707
+ server.close(() => resolve());
9708
+ server.closeAllConnections?.();
9709
+ }).then(resolveClosed);
9710
+ const touch = () => {
9711
+ if (isClosed) return;
9712
+ clearTimeout(idle);
9713
+ idle = setTimeout(() => void close(), idleMs);
9714
+ idle.unref();
9715
+ };
9716
+ const closeAfter = (res) => {
9717
+ if (res.destroyed || res.writableFinished) void close();
9718
+ else res.once("close", () => void close());
9719
+ };
9720
+ const sendDone = (res, name = "your Garmin account") => {
9721
+ closeAfter(res);
9722
+ if (res.destroyed) return;
9723
+ res.writeHead(200, HEADERS);
9724
+ res.end(donePage(name));
9725
+ };
9726
+ const notFound = (res) => {
9727
+ res.writeHead(404, { "cache-control": "no-store" });
9728
+ res.end();
9729
+ };
9730
+ async function handle(req, res) {
9731
+ const path3 = req.url ?? "";
9732
+ if (req.headers.host !== `127.0.0.1:${port}` || !path3.startsWith("/") || !isKey(path3.slice(1))) return notFound(res);
9733
+ touch();
9734
+ const send = (body) => {
9735
+ res.writeHead(200, HEADERS);
9736
+ res.end(body);
9737
+ };
9738
+ if (finished) return sendDone(res, signedInName);
9739
+ if (req.method === "GET") return send(mfaState ? mfaForm(key2) : credentialsForm(key2));
9740
+ if (req.method !== "POST") return notFound(res);
9741
+ const form = await readBody(req);
9742
+ if (!isKey(form.get("key"))) return notFound(res);
9743
+ return serialized(() => step(form, res, send));
9744
+ }
9745
+ async function step(form, res, send) {
9746
+ if (finished) return sendDone(res, signedInName);
9747
+ if (res.destroyed) return;
9748
+ if (isClosed) return notFound(res);
9749
+ if (form.get("step") === "mfa") {
9750
+ if (!client || !mfaState) return send(credentialsForm(key2, "Please sign in again."));
9751
+ const code = (form.get("code") ?? "").replace(/\s+/g, "");
9752
+ try {
9753
+ await client.resumeLogin(mfaState, code);
9754
+ } catch (e) {
9755
+ return send(mfaForm(key2, message(e)));
9756
+ }
9757
+ return finish(res, client);
9564
9758
  }
9759
+ const email = (form.get("email") ?? "").trim();
9760
+ const password = form.get("password") ?? "";
9761
+ client = null;
9762
+ mfaState = null;
9763
+ const attempt = options.createClient();
9764
+ let result;
9765
+ try {
9766
+ result = await attempt.login(email, password);
9767
+ } catch (e) {
9768
+ return send(credentialsForm(key2, message(e), email));
9769
+ }
9770
+ client = attempt;
9771
+ if (result.state === "mfa_required") {
9772
+ mfaState = result.mfaState;
9773
+ return send(mfaForm(key2));
9774
+ }
9775
+ return finish(res, attempt);
9565
9776
  }
9566
- return GARMIN_METHODS.filter((m) => !Object.hasOwn(EXCLUDED, m.name)).filter((m) => !Object.hasOwn(OPT_IN, m.name) || deps.config[OPT_IN[m.name]]).filter((m) => deps.config.groups === null || deps.config.groups.has(m.category)).map((m) => toolFor(m, deps));
9567
- };
9777
+ async function finish(res, signedIn) {
9778
+ finished = true;
9779
+ mfaState = null;
9780
+ options.onSignedIn();
9781
+ const name = await signedIn.displayName().catch(() => "your Garmin account");
9782
+ signedInName = name;
9783
+ sendDone(res, name);
9784
+ }
9785
+ let onListenError = () => {
9786
+ };
9787
+ await new Promise((resolve, reject) => {
9788
+ onListenError = reject;
9789
+ server.once("error", onListenError);
9790
+ server.listen(0, "127.0.0.1", () => resolve());
9791
+ });
9792
+ server.off("error", onListenError);
9793
+ server.on("error", () => void close());
9794
+ server.unref();
9795
+ server.on("connection", (socket) => socket.unref());
9796
+ port = server.address().port;
9797
+ touch();
9798
+ return { url: `http://127.0.0.1:${port}/${key2}`, closed, close };
9799
+ }
9800
+
9801
+ // src/signin-tool.ts
9802
+ var SAFE_SIGNIN_URL = /^http:\/\/127\.0\.0\.1:\d+\/[A-Za-z0-9_-]+$/;
9803
+ function openInBrowser(url) {
9804
+ if (!SAFE_SIGNIN_URL.test(url)) return;
9805
+ const [command2, args] = process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
9806
+ try {
9807
+ const child = spawn(command2, args, { detached: true, stdio: "ignore", windowsHide: true });
9808
+ child.on("error", () => {
9809
+ });
9810
+ child.unref();
9811
+ } catch {
9812
+ }
9813
+ }
9814
+ function tokenStoreClient(tokenDir) {
9815
+ const client = new GarminClient({ tokenStore: new FileTokenStore(tokenDir) });
9816
+ return {
9817
+ login: (email, password) => client.login(email, password),
9818
+ resumeLogin: (state, code) => client.resumeLogin(state, code),
9819
+ displayName: async () => (await new Garmin(client).getUserProfile()).displayName
9820
+ };
9821
+ }
9822
+ function signInTools(options = {}) {
9823
+ const openUrl = options.openUrl ?? openInBrowser;
9824
+ const createClient = options.createClient ?? tokenStoreClient;
9825
+ const startPage = options.startPage ?? startSignInPage;
9826
+ let page = null;
9827
+ let starting = null;
9828
+ return ({ config: config2, session }) => [
9829
+ {
9830
+ tool: {
9831
+ name: "sign_in_to_garmin",
9832
+ description: "Sign in to Garmin Connect. Call this when another Garmin tool says you are not logged in or the session expired. It opens a sign-in page in the user's browser; the password and MFA code are entered there, never in the chat. Afterwards, ask the user to tell you when they have finished, then retry.",
9833
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
9834
+ annotations: { title: "Sign in to Garmin", readOnlyHint: false, destructiveHint: false, openWorldHint: true }
9835
+ },
9836
+ async run() {
9837
+ try {
9838
+ const profile = await (await session.get()).getUserProfile();
9839
+ return textResult(`Already signed in to Garmin as ${profile.displayName}.`);
9840
+ } catch (error) {
9841
+ if (!(error instanceof GarminAuthError)) throw error;
9842
+ }
9843
+ if (!page) {
9844
+ starting ??= startPage({
9845
+ createClient: () => createClient(config2.tokenDir),
9846
+ onSignedIn: () => session.reset(),
9847
+ idleMs: options.idleMs
9848
+ });
9849
+ let started;
9850
+ try {
9851
+ started = await starting;
9852
+ } finally {
9853
+ starting = null;
9854
+ }
9855
+ page = started;
9856
+ void started.closed.then(() => {
9857
+ if (page === started) page = null;
9858
+ });
9859
+ }
9860
+ openUrl(page.url);
9861
+ return textResult(
9862
+ `A Garmin sign-in page opened in your browser: ${page.url}
9863
+ Sign in there (including any MFA code), then tell me when you're done.`
9864
+ );
9865
+ }
9866
+ }
9867
+ ];
9868
+ }
9568
9869
 
9569
9870
  // ../src/exercises.ts
9570
9871
  var EXERCISES = {
@@ -11759,10 +12060,10 @@ ${summarizeWorkout(workout)}`);
11759
12060
  ];
11760
12061
 
11761
12062
  // src/tools.ts
11762
- var TOOL_FACTORIES = [workoutTools, methodTools];
12063
+ var TOOL_FACTORIES = [signInTools(), workoutTools, methodTools];
11763
12064
 
11764
12065
  // src/cli.ts
11765
- var { version } = createRequire(import.meta.url)("../package.json");
12066
+ var version = "0.8.0";
11766
12067
  var USAGE = `garminconnect-mcp ${version}
11767
12068
 
11768
12069
  garminconnect-mcp start the MCP server (stdio); Claude Desktop runs this