gogcli-mcp 2.23.2 → 2.24.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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
10
- "version": "2.23.2"
10
+ "version": "2.24.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "gogcli",
16
16
  "source": "./",
17
17
  "description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
18
- "version": "2.23.2",
18
+ "version": "2.24.0",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gogcli-mcp",
3
3
  "displayName": "gogcli",
4
- "version": "2.23.2",
4
+ "version": "2.24.0",
5
5
  "description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/dist/index.js CHANGED
@@ -31827,6 +31827,7 @@ function registerApiTools(server) {
31827
31827
  // src/tools/auth.ts
31828
31828
  function registerAuthToolsWith(server, defaultServices) {
31829
31829
  const servicesDescribe = `Services to authorize: "all" or comma-separated list (e.g. "sheets,gmail,calendar"). Default: "${defaultServices}". Prefer the narrowest set you need \u2014 requesting a service whose Google API is not enabled on the OAuth client's project makes Google reject the WHOLE request with invalid_scope.`;
31830
+ const extraScopesDescribe = "Additional raw OAuth scope URIs to request, comma-separated, on top of the ones `services` implies. Use for scopes no service covers \u2014 e.g. https://www.googleapis.com/auth/bigquery.readonly, required before gog_sheets_datasource_* can read BigQuery-backed Connected Sheets. Leave unset otherwise: an extra scope whose API is not enabled on the OAuth client project makes Google reject the WHOLE authorization with invalid_scope.";
31830
31831
  server.registerTool("gog_auth_list", {
31831
31832
  description: "List the Google accounts stored in gogcli, with their scopes. This reads local configuration only \u2014 it does not contact Google and does NOT tell you whether an account still works: a signed-out account whose refresh token expired or was revoked is listed here exactly like a healthy one, scopes and all. Use gog_auth_health to check whether an account can actually authenticate.",
31832
31833
  annotations: { readOnlyHint: true },
@@ -31876,11 +31877,14 @@ function registerAuthToolsWith(server, defaultServices) {
31876
31877
  annotations: { destructiveHint: true },
31877
31878
  inputSchema: {
31878
31879
  email: external_exports.string().describe("Google account email to authorize"),
31879
- services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe)
31880
+ services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe),
31881
+ extraScopes: external_exports.string().optional().describe(extraScopesDescribe)
31880
31882
  }
31881
- }, async ({ email: email3, services = defaultServices }) => {
31883
+ }, async ({ email: email3, services = defaultServices, extraScopes }) => {
31882
31884
  try {
31883
- return rawTextResult(await run(["auth", "add", email3, "--services", services], {
31885
+ const args = ["auth", "add", email3, "--services", services];
31886
+ if (extraScopes) args.push(`--extra-scopes=${extraScopes}`, "--force-consent");
31887
+ return rawTextResult(await run(args, {
31884
31888
  interactive: true,
31885
31889
  timeout: 3e5
31886
31890
  }));
@@ -31892,14 +31896,14 @@ function registerAuthToolsWith(server, defaultServices) {
31892
31896
  description: "Begin REMOTE/headless Google authorization (step 1 of 2). Returns a sign-in URL to open in any browser \u2014 no local server or terminal on the gogcli host is needed, so this works over the hosted connector where the interactive gog_auth_add cannot. Hand the URL to the user; after they sign in, the browser is redirected to a localhost URL that fails to load \u2014 that is expected. They copy that full redirected URL (from the address bar) and you pass it to gog_auth_add_complete. The link is valid for 10 minutes. If you pass a custom `services` here, pass the SAME value to gog_auth_add_complete or the second step will not match this one.",
31893
31897
  inputSchema: {
31894
31898
  email: external_exports.string().describe("Google account email to authorize"),
31895
- services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe)
31899
+ services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe),
31900
+ extraScopes: external_exports.string().optional().describe(`${extraScopesDescribe} Pass the SAME value to gog_auth_add_complete.`)
31896
31901
  }
31897
- }, async ({ email: email3, services = defaultServices }) => {
31902
+ }, async ({ email: email3, services = defaultServices, extraScopes }) => {
31898
31903
  try {
31899
- return rawTextResult(await run(
31900
- ["auth", "add", email3, "--remote", "--step", "1", "--services", services, "--force-consent"],
31901
- { redactMode: "tokens" }
31902
- ));
31904
+ const args = ["auth", "add", email3, "--remote", "--step", "1", "--services", services, "--force-consent"];
31905
+ if (extraScopes) args.push(`--extra-scopes=${extraScopes}`);
31906
+ return rawTextResult(await run(args, { redactMode: "tokens" }));
31903
31907
  } catch (err) {
31904
31908
  return errorResult(errorText(err));
31905
31909
  }
@@ -31914,25 +31918,28 @@ function registerAuthToolsWith(server, defaultServices) {
31914
31918
  ),
31915
31919
  services: external_exports.string().optional().default(defaultServices).describe(
31916
31920
  `Services authorized \u2014 MUST match the value passed to gog_auth_add_url. Default: "${defaultServices}".`
31921
+ ),
31922
+ extraScopes: external_exports.string().optional().describe(
31923
+ "Extra OAuth scope URIs \u2014 MUST match the value passed to gog_auth_add_url, for the same reason `services` must: the two steps have to describe the same grant."
31917
31924
  )
31918
31925
  }
31919
- }, async ({ email: email3, redirectUrl, services = defaultServices }) => {
31926
+ }, async ({ email: email3, redirectUrl, services = defaultServices, extraScopes }) => {
31920
31927
  try {
31921
- return rawTextResult(await run(
31922
- [
31923
- "auth",
31924
- "add",
31925
- email3,
31926
- "--remote",
31927
- "--step",
31928
- "2",
31929
- "--auth-url",
31930
- redirectUrl,
31931
- "--services",
31932
- services,
31933
- "--force-consent"
31934
- ]
31935
- ));
31928
+ const args = [
31929
+ "auth",
31930
+ "add",
31931
+ email3,
31932
+ "--remote",
31933
+ "--step",
31934
+ "2",
31935
+ "--auth-url",
31936
+ redirectUrl,
31937
+ "--services",
31938
+ services,
31939
+ "--force-consent"
31940
+ ];
31941
+ if (extraScopes) args.push(`--extra-scopes=${extraScopes}`);
31942
+ return rawTextResult(await run(args));
31936
31943
  } catch (err) {
31937
31944
  return errorResult(errorText(err));
31938
31945
  }
@@ -31951,13 +31958,19 @@ function registerAuthTools(server) {
31951
31958
  // src/tools/calendar.ts
31952
31959
  function registerCalendarTools(server) {
31953
31960
  server.registerTool("gog_calendar_events", {
31954
- description: 'List calendar events. Filters can be combined (e.g. --from + --to for a range, or --today for just today). gog returns only 10 events by default, so a wide date range is USUALLY INCOMPLETE: raise max, or page with pageToken until the response carries no nextPageToken. A response carrying "truncated": true is an incomplete view \u2014 never conclude an event does not exist from one.',
31961
+ description: 'List calendar events. Describe the window ONE way and one way only (gog >= 0.36.0 rejects the rest as ambiguous rather than silently discarding a flag): today on its own; or from + to; or from + days; or days on its own (a window of that many days starting today). today cannot be combined with from, to or days, and days cannot be combined with to. gog returns only 10 events by default, so a wide date range is USUALLY INCOMPLETE: raise max, or page with pageToken until the response carries no nextPageToken. A response carrying "truncated": true is an incomplete view \u2014 never conclude an event does not exist from one.',
31955
31962
  annotations: { readOnlyHint: true },
31956
31963
  inputSchema: {
31957
31964
  calendarId: external_exports.string().optional().describe("Calendar ID (default: primary calendar)"),
31958
31965
  from: external_exports.string().optional().describe("Start time filter (RFC3339, date, or natural language)"),
31959
- to: external_exports.string().optional().describe("End time filter (RFC3339, date, or natural language)"),
31960
- today: external_exports.boolean().optional().describe("Only show today's events"),
31966
+ to: external_exports.string().optional().describe("End time filter (RFC3339, date, or natural language). Mutually exclusive with today and with days."),
31967
+ // gog >= 0.36.0 (openclaw/gogcli#981). Before that release --days sat in
31968
+ // a switch arm evaluated ahead of --from, so `--from 2026-09-25 --days 5`
31969
+ // silently threw --from away and answered for today instead — at exit 0,
31970
+ // in a well-formed table. It is only exposed here now that it means what
31971
+ // it says.
31972
+ days: external_exports.number().int().positive().optional().describe('Window LENGTH in days (calendar days, DST-aware), measured from `from` when one is given and from today otherwise. Use from + days for "the week of the 25th"; days alone for "the next N days". Mutually exclusive with to and with today.'),
31973
+ today: external_exports.boolean().optional().describe("Only show today's events. A complete window on its own \u2014 mutually exclusive with from, to and days."),
31961
31974
  query: external_exports.string().optional().describe("Free text search within events"),
31962
31975
  max: external_exports.number().int().optional().describe("Max events to return. gog defaults to 10, which silently hides the rest \u2014 raise it, or page with pageToken."),
31963
31976
  pageToken: pageTokenParam,
@@ -31967,11 +31980,12 @@ function registerCalendarTools(server) {
31967
31980
  timezone: external_exports.string().optional().describe(`Display timezone for event times (IANA name, e.g. America/New_York, or "local" for the system timezone). Default: each event's timezone, then its calendar's timezone.`),
31968
31981
  account: accountParam
31969
31982
  }
31970
- }, async ({ calendarId, from, to, today, query, max, pageToken, page, all, eventTypes, timezone, account }) => {
31983
+ }, async ({ calendarId, from, to, days, today, query, max, pageToken, page, all, eventTypes, timezone, account }) => {
31971
31984
  const args = ["calendar", "events"];
31972
31985
  if (calendarId) args.push(calendarId);
31973
31986
  if (from) args.push(`--from=${from}`);
31974
31987
  if (to) args.push(`--to=${to}`);
31988
+ if (days !== void 0) args.push(`--days=${days}`);
31975
31989
  if (today) args.push("--today");
31976
31990
  if (query) args.push(`--query=${query}`);
31977
31991
  if (max !== void 0) args.push(`--max=${max}`);
@@ -32970,16 +32984,22 @@ function registerGmailTools(server) {
32970
32984
  });
32971
32985
  });
32972
32986
  server.registerTool("gog_gmail_get", {
32973
- description: "Get a Gmail message by ID.",
32987
+ description: "Get a Gmail message by ID. For a long message, sanitizeContent is the cheapest way to keep it in context: it drops the raw MIME payload and the HTML part, which are usually the bulk of the response.",
32974
32988
  annotations: { readOnlyHint: true },
32975
32989
  inputSchema: {
32976
32990
  messageId: external_exports.string().describe("Message ID"),
32977
32991
  format: external_exports.enum(["full", "metadata", "raw"]).optional().describe("Message format (default: full)"),
32992
+ // Requires gog >= 0.37.0. Before that (openclaw/gogcli#992) the JSON
32993
+ // carried the headers and body TWICE — once inside `message`, once
32994
+ // copied to the top level — so the flag meant to shrink the payload
32995
+ // enlarged it. MIN_GOG_VERSION is the guard; there is no runtime check.
32996
+ sanitizeContent: external_exports.boolean().optional().describe("Return agent-oriented sanitized content: HTML stripped, HTTP(S) URLs removed, raw Gmail payloads omitted from the JSON. The largest payload-size reduction available here. Note the URL removal is lossy \u2014 omit this when you need to follow a link out of the message."),
32978
32997
  account: accountParam
32979
32998
  }
32980
- }, async ({ messageId, format, account }) => {
32999
+ }, async ({ messageId, format, sanitizeContent, account }) => {
32981
33000
  const args = ["gmail", "get", messageId];
32982
33001
  if (format) args.push(`--format=${format}`);
33002
+ if (sanitizeContent) args.push("--sanitize-content");
32983
33003
  return runOrDiagnose(args, { account });
32984
33004
  });
32985
33005
  server.registerTool("gog_gmail_send", {
@@ -33332,7 +33352,7 @@ function registerTasksTools(server) {
33332
33352
  }
33333
33353
 
33334
33354
  // src/server.ts
33335
- var VERSION = true ? "2.23.2" : "0.0.0";
33355
+ var VERSION = true ? "2.24.0" : "0.0.0";
33336
33356
  var BASE_TOOL_REGISTRARS = [
33337
33357
  registerApiTools,
33338
33358
  registerAuthTools,
package/dist/lib.js CHANGED
@@ -23042,7 +23042,7 @@ function activeExecutor() {
23042
23042
  return runExecutor.getStore() ?? defaultExecutor;
23043
23043
  }
23044
23044
  var TIMEOUT_MS = 3e4;
23045
- var MIN_GOG_VERSION = "0.35.0";
23045
+ var MIN_GOG_VERSION = "0.37.0";
23046
23046
  function readonlyEnvEnabled() {
23047
23047
  return readEnvVar("GOG_READONLY") !== void 0 && parseBoolEnv("GOG_READONLY", { default: true });
23048
23048
  }
@@ -23712,6 +23712,7 @@ function registerApiTools(server) {
23712
23712
  // src/tools/auth.ts
23713
23713
  function registerAuthToolsWith(server, defaultServices) {
23714
23714
  const servicesDescribe = `Services to authorize: "all" or comma-separated list (e.g. "sheets,gmail,calendar"). Default: "${defaultServices}". Prefer the narrowest set you need \u2014 requesting a service whose Google API is not enabled on the OAuth client's project makes Google reject the WHOLE request with invalid_scope.`;
23715
+ const extraScopesDescribe = "Additional raw OAuth scope URIs to request, comma-separated, on top of the ones `services` implies. Use for scopes no service covers \u2014 e.g. https://www.googleapis.com/auth/bigquery.readonly, required before gog_sheets_datasource_* can read BigQuery-backed Connected Sheets. Leave unset otherwise: an extra scope whose API is not enabled on the OAuth client project makes Google reject the WHOLE authorization with invalid_scope.";
23715
23716
  server.registerTool("gog_auth_list", {
23716
23717
  description: "List the Google accounts stored in gogcli, with their scopes. This reads local configuration only \u2014 it does not contact Google and does NOT tell you whether an account still works: a signed-out account whose refresh token expired or was revoked is listed here exactly like a healthy one, scopes and all. Use gog_auth_health to check whether an account can actually authenticate.",
23717
23718
  annotations: { readOnlyHint: true },
@@ -23761,11 +23762,14 @@ function registerAuthToolsWith(server, defaultServices) {
23761
23762
  annotations: { destructiveHint: true },
23762
23763
  inputSchema: {
23763
23764
  email: external_exports.string().describe("Google account email to authorize"),
23764
- services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe)
23765
+ services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe),
23766
+ extraScopes: external_exports.string().optional().describe(extraScopesDescribe)
23765
23767
  }
23766
- }, async ({ email: email3, services = defaultServices }) => {
23768
+ }, async ({ email: email3, services = defaultServices, extraScopes }) => {
23767
23769
  try {
23768
- return rawTextResult(await run(["auth", "add", email3, "--services", services], {
23770
+ const args = ["auth", "add", email3, "--services", services];
23771
+ if (extraScopes) args.push(`--extra-scopes=${extraScopes}`, "--force-consent");
23772
+ return rawTextResult(await run(args, {
23769
23773
  interactive: true,
23770
23774
  timeout: 3e5
23771
23775
  }));
@@ -23777,14 +23781,14 @@ function registerAuthToolsWith(server, defaultServices) {
23777
23781
  description: "Begin REMOTE/headless Google authorization (step 1 of 2). Returns a sign-in URL to open in any browser \u2014 no local server or terminal on the gogcli host is needed, so this works over the hosted connector where the interactive gog_auth_add cannot. Hand the URL to the user; after they sign in, the browser is redirected to a localhost URL that fails to load \u2014 that is expected. They copy that full redirected URL (from the address bar) and you pass it to gog_auth_add_complete. The link is valid for 10 minutes. If you pass a custom `services` here, pass the SAME value to gog_auth_add_complete or the second step will not match this one.",
23778
23782
  inputSchema: {
23779
23783
  email: external_exports.string().describe("Google account email to authorize"),
23780
- services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe)
23784
+ services: external_exports.string().optional().default(defaultServices).describe(servicesDescribe),
23785
+ extraScopes: external_exports.string().optional().describe(`${extraScopesDescribe} Pass the SAME value to gog_auth_add_complete.`)
23781
23786
  }
23782
- }, async ({ email: email3, services = defaultServices }) => {
23787
+ }, async ({ email: email3, services = defaultServices, extraScopes }) => {
23783
23788
  try {
23784
- return rawTextResult(await run(
23785
- ["auth", "add", email3, "--remote", "--step", "1", "--services", services, "--force-consent"],
23786
- { redactMode: "tokens" }
23787
- ));
23789
+ const args = ["auth", "add", email3, "--remote", "--step", "1", "--services", services, "--force-consent"];
23790
+ if (extraScopes) args.push(`--extra-scopes=${extraScopes}`);
23791
+ return rawTextResult(await run(args, { redactMode: "tokens" }));
23788
23792
  } catch (err) {
23789
23793
  return errorResult(errorText(err));
23790
23794
  }
@@ -23799,25 +23803,28 @@ function registerAuthToolsWith(server, defaultServices) {
23799
23803
  ),
23800
23804
  services: external_exports.string().optional().default(defaultServices).describe(
23801
23805
  `Services authorized \u2014 MUST match the value passed to gog_auth_add_url. Default: "${defaultServices}".`
23806
+ ),
23807
+ extraScopes: external_exports.string().optional().describe(
23808
+ "Extra OAuth scope URIs \u2014 MUST match the value passed to gog_auth_add_url, for the same reason `services` must: the two steps have to describe the same grant."
23802
23809
  )
23803
23810
  }
23804
- }, async ({ email: email3, redirectUrl, services = defaultServices }) => {
23811
+ }, async ({ email: email3, redirectUrl, services = defaultServices, extraScopes }) => {
23805
23812
  try {
23806
- return rawTextResult(await run(
23807
- [
23808
- "auth",
23809
- "add",
23810
- email3,
23811
- "--remote",
23812
- "--step",
23813
- "2",
23814
- "--auth-url",
23815
- redirectUrl,
23816
- "--services",
23817
- services,
23818
- "--force-consent"
23819
- ]
23820
- ));
23813
+ const args = [
23814
+ "auth",
23815
+ "add",
23816
+ email3,
23817
+ "--remote",
23818
+ "--step",
23819
+ "2",
23820
+ "--auth-url",
23821
+ redirectUrl,
23822
+ "--services",
23823
+ services,
23824
+ "--force-consent"
23825
+ ];
23826
+ if (extraScopes) args.push(`--extra-scopes=${extraScopes}`);
23827
+ return rawTextResult(await run(args));
23821
23828
  } catch (err) {
23822
23829
  return errorResult(errorText(err));
23823
23830
  }
@@ -23839,13 +23846,19 @@ function authToolsFor(defaultServices) {
23839
23846
  // src/tools/calendar.ts
23840
23847
  function registerCalendarTools(server) {
23841
23848
  server.registerTool("gog_calendar_events", {
23842
- description: 'List calendar events. Filters can be combined (e.g. --from + --to for a range, or --today for just today). gog returns only 10 events by default, so a wide date range is USUALLY INCOMPLETE: raise max, or page with pageToken until the response carries no nextPageToken. A response carrying "truncated": true is an incomplete view \u2014 never conclude an event does not exist from one.',
23849
+ description: 'List calendar events. Describe the window ONE way and one way only (gog >= 0.36.0 rejects the rest as ambiguous rather than silently discarding a flag): today on its own; or from + to; or from + days; or days on its own (a window of that many days starting today). today cannot be combined with from, to or days, and days cannot be combined with to. gog returns only 10 events by default, so a wide date range is USUALLY INCOMPLETE: raise max, or page with pageToken until the response carries no nextPageToken. A response carrying "truncated": true is an incomplete view \u2014 never conclude an event does not exist from one.',
23843
23850
  annotations: { readOnlyHint: true },
23844
23851
  inputSchema: {
23845
23852
  calendarId: external_exports.string().optional().describe("Calendar ID (default: primary calendar)"),
23846
23853
  from: external_exports.string().optional().describe("Start time filter (RFC3339, date, or natural language)"),
23847
- to: external_exports.string().optional().describe("End time filter (RFC3339, date, or natural language)"),
23848
- today: external_exports.boolean().optional().describe("Only show today's events"),
23854
+ to: external_exports.string().optional().describe("End time filter (RFC3339, date, or natural language). Mutually exclusive with today and with days."),
23855
+ // gog >= 0.36.0 (openclaw/gogcli#981). Before that release --days sat in
23856
+ // a switch arm evaluated ahead of --from, so `--from 2026-09-25 --days 5`
23857
+ // silently threw --from away and answered for today instead — at exit 0,
23858
+ // in a well-formed table. It is only exposed here now that it means what
23859
+ // it says.
23860
+ days: external_exports.number().int().positive().optional().describe('Window LENGTH in days (calendar days, DST-aware), measured from `from` when one is given and from today otherwise. Use from + days for "the week of the 25th"; days alone for "the next N days". Mutually exclusive with to and with today.'),
23861
+ today: external_exports.boolean().optional().describe("Only show today's events. A complete window on its own \u2014 mutually exclusive with from, to and days."),
23849
23862
  query: external_exports.string().optional().describe("Free text search within events"),
23850
23863
  max: external_exports.number().int().optional().describe("Max events to return. gog defaults to 10, which silently hides the rest \u2014 raise it, or page with pageToken."),
23851
23864
  pageToken: pageTokenParam,
@@ -23855,11 +23868,12 @@ function registerCalendarTools(server) {
23855
23868
  timezone: external_exports.string().optional().describe(`Display timezone for event times (IANA name, e.g. America/New_York, or "local" for the system timezone). Default: each event's timezone, then its calendar's timezone.`),
23856
23869
  account: accountParam
23857
23870
  }
23858
- }, async ({ calendarId, from, to, today, query, max, pageToken, page, all, eventTypes, timezone, account }) => {
23871
+ }, async ({ calendarId, from, to, days, today, query, max, pageToken, page, all, eventTypes, timezone, account }) => {
23859
23872
  const args = ["calendar", "events"];
23860
23873
  if (calendarId) args.push(calendarId);
23861
23874
  if (from) args.push(`--from=${from}`);
23862
23875
  if (to) args.push(`--to=${to}`);
23876
+ if (days !== void 0) args.push(`--days=${days}`);
23863
23877
  if (today) args.push("--today");
23864
23878
  if (query) args.push(`--query=${query}`);
23865
23879
  if (max !== void 0) args.push(`--max=${max}`);
@@ -24858,16 +24872,22 @@ function registerGmailTools(server) {
24858
24872
  });
24859
24873
  });
24860
24874
  server.registerTool("gog_gmail_get", {
24861
- description: "Get a Gmail message by ID.",
24875
+ description: "Get a Gmail message by ID. For a long message, sanitizeContent is the cheapest way to keep it in context: it drops the raw MIME payload and the HTML part, which are usually the bulk of the response.",
24862
24876
  annotations: { readOnlyHint: true },
24863
24877
  inputSchema: {
24864
24878
  messageId: external_exports.string().describe("Message ID"),
24865
24879
  format: external_exports.enum(["full", "metadata", "raw"]).optional().describe("Message format (default: full)"),
24880
+ // Requires gog >= 0.37.0. Before that (openclaw/gogcli#992) the JSON
24881
+ // carried the headers and body TWICE — once inside `message`, once
24882
+ // copied to the top level — so the flag meant to shrink the payload
24883
+ // enlarged it. MIN_GOG_VERSION is the guard; there is no runtime check.
24884
+ sanitizeContent: external_exports.boolean().optional().describe("Return agent-oriented sanitized content: HTML stripped, HTTP(S) URLs removed, raw Gmail payloads omitted from the JSON. The largest payload-size reduction available here. Note the URL removal is lossy \u2014 omit this when you need to follow a link out of the message."),
24866
24885
  account: accountParam
24867
24886
  }
24868
- }, async ({ messageId, format, account }) => {
24887
+ }, async ({ messageId, format, sanitizeContent, account }) => {
24869
24888
  const args = ["gmail", "get", messageId];
24870
24889
  if (format) args.push(`--format=${format}`);
24890
+ if (sanitizeContent) args.push("--sanitize-content");
24871
24891
  return runOrDiagnose(args, { account });
24872
24892
  });
24873
24893
  server.registerTool("gog_gmail_send", {
@@ -25220,7 +25240,7 @@ function registerTasksTools(server) {
25220
25240
  }
25221
25241
 
25222
25242
  // src/server.ts
25223
- var VERSION = true ? "2.23.2" : "0.0.0";
25243
+ var VERSION = true ? "2.24.0" : "0.0.0";
25224
25244
  var BASE_TOOL_REGISTRARS = [
25225
25245
  registerApiTools,
25226
25246
  registerAuthTools,
package/manifest.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "manifest_version": "0.3",
4
4
  "name": "gogcli-mcp",
5
5
  "display_name": "gogcli",
6
- "version": "2.23.2",
6
+ "version": "2.24.0",
7
7
  "description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
8
8
  "author": {
9
9
  "name": "Chris Hall",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp",
3
- "version": "2.23.2",
3
+ "version": "2.24.0",
4
4
  "mcpName": "io.github.chrischall/gogcli-mcp",
5
5
  "description": "MCP server wrapping gogcli for Google service access",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "source": "github",
8
8
  "subfolder": "packages/gogcli-mcp"
9
9
  },
10
- "version": "2.23.2",
10
+ "version": "2.24.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "gogcli-mcp",
15
- "version": "2.23.2",
15
+ "version": "2.24.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
@@ -71,10 +71,21 @@ const COUNT_PROBE_PAGE_SIZE = 500;
71
71
  // a caller is most likely to draw a false negative from. A full page with more
72
72
  // behind it yields an honest lower bound instead.
73
73
  //
74
- // gog cannot supply this itself: `gmail search` and `gmail messages search`
75
- // build their JSON by hand from the items plus nextPageToken, so a direct
76
- // Discovery call is the only route. It is only ever spent on a result set
77
- // already known to be truncated.
74
+ // gog CAN now supply this itself — `--count` on `gmail search` / `gmail
75
+ // messages search` (gog >= 0.36.0, openclaw/gogcli#985) is this same probe,
76
+ // upstreamed, down to the page size and the exact/lower-bound split. The probe
77
+ // stays here anyway, for two differences that both matter at this seam:
78
+ //
79
+ // * It is spent ONLY on a result set already known to be truncated. --count
80
+ // is decided before the search runs, so adopting it would cost every
81
+ // search an extra Gmail request to answer a question most of them never
82
+ // raise.
83
+ // * It is best-effort. gog returns the count probe's error from the whole
84
+ // command, so a failed count would turn a search that DID succeed into an
85
+ // error — trading a missing warning for a missing answer.
86
+ //
87
+ // Keep the two in step: a change to what "exact" means on either side should
88
+ // be made on both.
78
89
  //
79
90
  // Best-effort by construction: any failure must degrade the warning, never the
80
91
  // search.
package/src/runner.ts CHANGED
@@ -167,7 +167,7 @@ const TIMEOUT_MS = 30_000;
167
167
  // so the requirement change is surfaced in the release notes (see
168
168
  // .github/release.yml). This is the single source of truth for the required
169
169
  // version; keep the README/CLAUDE.md mention in sync.
170
- export const MIN_GOG_VERSION = '0.35.0';
170
+ export const MIN_GOG_VERSION = '0.37.0';
171
171
 
172
172
  // Interpret the GOG_READONLY kill-switch. `readEnvVar` already treats blank
173
173
  // values, 'undefined'/'null' sentinels, and unresolved .mcpb placeholders
package/src/tools/auth.ts CHANGED
@@ -13,6 +13,20 @@ function registerAuthToolsWith(server: McpServer, defaultServices: string): void
13
13
  `Default: "${defaultServices}". Prefer the narrowest set you need — requesting a service whose ` +
14
14
  `Google API is not enabled on the OAuth client's project makes Google reject the WHOLE request ` +
15
15
  `with invalid_scope.`;
16
+ // Additional raw OAuth scope URIs, appended after the service scopes gog
17
+ // derives from `services`. This exists for scopes no service selection can
18
+ // ask for: notably bigquery.readonly, which Google demands whenever a Sheets
19
+ // response CONTAINS BigQuery Connected Sheets data (gog_sheets_datasource_*)
20
+ // and which ordinary `sheets` authorization deliberately does not request.
21
+ // Same invalid_scope caveat as `services`: Google rejects the WHOLE request
22
+ // if the scope's API is not enabled on the OAuth client's project, and that
23
+ // happens in the user's browser, so this wrapper cannot catch it.
24
+ const extraScopesDescribe =
25
+ 'Additional raw OAuth scope URIs to request, comma-separated, on top of the ones `services` implies. ' +
26
+ 'Use for scopes no service covers — e.g. https://www.googleapis.com/auth/bigquery.readonly, required ' +
27
+ 'before gog_sheets_datasource_* can read BigQuery-backed Connected Sheets. Leave unset otherwise: an ' +
28
+ 'extra scope whose API is not enabled on the OAuth client project makes Google reject the WHOLE ' +
29
+ 'authorization with invalid_scope.';
16
30
  server.registerTool('gog_auth_list', {
17
31
  description:
18
32
  'List the Google accounts stored in gogcli, with their scopes. This reads local ' +
@@ -91,10 +105,19 @@ function registerAuthToolsWith(server: McpServer, defaultServices: string): void
91
105
  inputSchema: {
92
106
  email: z.string().describe('Google account email to authorize'),
93
107
  services: z.string().optional().default(defaultServices).describe(servicesDescribe),
108
+ extraScopes: z.string().optional().describe(extraScopesDescribe),
94
109
  },
95
- }, async ({ email, services = defaultServices }) => {
110
+ }, async ({ email, services = defaultServices, extraScopes }) => {
96
111
  try {
97
- return rawTextResult(await run(['auth', 'add', email, '--services', services], {
112
+ const args = ['auth', 'add', email, '--services', services];
113
+ // --force-consent rides along with extraScopes and only with them. Google
114
+ // re-prompts for a NEW scope only when consent is forced; without it the
115
+ // account can come back still missing the scope, with a success message —
116
+ // the exact shape of failure the caller cannot see. The other two tools
117
+ // force consent unconditionally; this one does not, so it must be added
118
+ // here rather than assumed.
119
+ if (extraScopes) args.push(`--extra-scopes=${extraScopes}`, '--force-consent');
120
+ return rawTextResult(await run(args, {
98
121
  interactive: true,
99
122
  timeout: 300_000,
100
123
  }));
@@ -115,17 +138,17 @@ function registerAuthToolsWith(server: McpServer, defaultServices: string): void
115
138
  inputSchema: {
116
139
  email: z.string().describe('Google account email to authorize'),
117
140
  services: z.string().optional().default(defaultServices).describe(servicesDescribe),
141
+ extraScopes: z.string().optional().describe(`${extraScopesDescribe} Pass the SAME value to gog_auth_add_complete.`),
118
142
  },
119
- }, async ({ email, services = defaultServices }) => {
143
+ }, async ({ email, services = defaultServices, extraScopes }) => {
120
144
  try {
121
145
  // --force-consent guarantees a refresh token even if a prior grant exists
122
146
  // (the whole point when recovering from a dead one). redactMode 'tokens'
123
147
  // keeps the consent URL's scope names intact (the shared redactor mangles
124
148
  // them) while still stripping any real token — a step-1 URL carries none.
125
- return rawTextResult(await run(
126
- ['auth', 'add', email, '--remote', '--step', '1', '--services', services, '--force-consent'],
127
- { redactMode: 'tokens' },
128
- ));
149
+ const args = ['auth', 'add', email, '--remote', '--step', '1', '--services', services, '--force-consent'];
150
+ if (extraScopes) args.push(`--extra-scopes=${extraScopes}`);
151
+ return rawTextResult(await run(args, { redactMode: 'tokens' }));
129
152
  } catch (err) {
130
153
  return errorResult(errorText(err));
131
154
  }
@@ -147,13 +170,17 @@ function registerAuthToolsWith(server: McpServer, defaultServices: string): void
147
170
  services: z.string().optional().default(defaultServices).describe(
148
171
  `Services authorized — MUST match the value passed to gog_auth_add_url. Default: "${defaultServices}".`,
149
172
  ),
173
+ extraScopes: z.string().optional().describe(
174
+ 'Extra OAuth scope URIs — MUST match the value passed to gog_auth_add_url, for the same reason `services` must: ' +
175
+ 'the two steps have to describe the same grant.',
176
+ ),
150
177
  },
151
- }, async ({ email, redirectUrl, services = defaultServices }) => {
178
+ }, async ({ email, redirectUrl, services = defaultServices, extraScopes }) => {
152
179
  try {
153
- return rawTextResult(await run(
154
- ['auth', 'add', email, '--remote', '--step', '2', '--auth-url', redirectUrl,
155
- '--services', services, '--force-consent'],
156
- ));
180
+ const args = ['auth', 'add', email, '--remote', '--step', '2', '--auth-url', redirectUrl,
181
+ '--services', services, '--force-consent'];
182
+ if (extraScopes) args.push(`--extra-scopes=${extraScopes}`);
183
+ return rawTextResult(await run(args));
157
184
  } catch (err) {
158
185
  return errorResult(errorText(err));
159
186
  }
@@ -5,15 +5,22 @@ import { annotateTruncatedList } from '../pagination.js';
5
5
 
6
6
  export function registerCalendarTools(server: McpServer): void {
7
7
  server.registerTool('gog_calendar_events', {
8
- description: 'List calendar events. Filters can be combined (e.g. --from + --to for a range, or --today for just today). '
8
+ description: 'List calendar events. Describe the window ONE way and one way only (gog >= 0.36.0 rejects the rest as ambiguous rather than silently discarding a flag): '
9
+ + 'today on its own; or from + to; or from + days; or days on its own (a window of that many days starting today). today cannot be combined with from, to or days, and days cannot be combined with to. '
9
10
  + 'gog returns only 10 events by default, so a wide date range is USUALLY INCOMPLETE: raise max, or page with pageToken until the response carries no nextPageToken. '
10
11
  + 'A response carrying "truncated": true is an incomplete view — never conclude an event does not exist from one.',
11
12
  annotations: { readOnlyHint: true },
12
13
  inputSchema: {
13
14
  calendarId: z.string().optional().describe('Calendar ID (default: primary calendar)'),
14
15
  from: z.string().optional().describe('Start time filter (RFC3339, date, or natural language)'),
15
- to: z.string().optional().describe('End time filter (RFC3339, date, or natural language)'),
16
- today: z.boolean().optional().describe('Only show today\'s events'),
16
+ to: z.string().optional().describe('End time filter (RFC3339, date, or natural language). Mutually exclusive with today and with days.'),
17
+ // gog >= 0.36.0 (openclaw/gogcli#981). Before that release --days sat in
18
+ // a switch arm evaluated ahead of --from, so `--from 2026-09-25 --days 5`
19
+ // silently threw --from away and answered for today instead — at exit 0,
20
+ // in a well-formed table. It is only exposed here now that it means what
21
+ // it says.
22
+ days: z.number().int().positive().optional().describe('Window LENGTH in days (calendar days, DST-aware), measured from `from` when one is given and from today otherwise. Use from + days for "the week of the 25th"; days alone for "the next N days". Mutually exclusive with to and with today.'),
23
+ today: z.boolean().optional().describe('Only show today\'s events. A complete window on its own — mutually exclusive with from, to and days.'),
17
24
  query: z.string().optional().describe('Free text search within events'),
18
25
  max: z.number().int().optional().describe('Max events to return. gog defaults to 10, which silently hides the rest — raise it, or page with pageToken.'),
19
26
  pageToken: pageTokenParam,
@@ -23,11 +30,12 @@ export function registerCalendarTools(server: McpServer): void {
23
30
  timezone: z.string().optional().describe('Display timezone for event times (IANA name, e.g. America/New_York, or "local" for the system timezone). Default: each event\'s timezone, then its calendar\'s timezone.'),
24
31
  account: accountParam,
25
32
  },
26
- }, async ({ calendarId, from, to, today, query, max, pageToken, page, all, eventTypes, timezone, account }) => {
33
+ }, async ({ calendarId, from, to, days, today, query, max, pageToken, page, all, eventTypes, timezone, account }) => {
27
34
  const args = ['calendar', 'events'];
28
35
  if (calendarId) args.push(calendarId);
29
36
  if (from) args.push(`--from=${from}`);
30
37
  if (to) args.push(`--to=${to}`);
38
+ if (days !== undefined) args.push(`--days=${days}`);
31
39
  if (today) args.push('--today');
32
40
  if (query) args.push(`--query=${query}`);
33
41
  if (max !== undefined) args.push(`--max=${max}`);
@@ -46,16 +46,22 @@ export function registerGmailTools(server: McpServer): void {
46
46
  });
47
47
 
48
48
  server.registerTool('gog_gmail_get', {
49
- description: 'Get a Gmail message by ID.',
49
+ description: 'Get a Gmail message by ID. For a long message, sanitizeContent is the cheapest way to keep it in context: it drops the raw MIME payload and the HTML part, which are usually the bulk of the response.',
50
50
  annotations: { readOnlyHint: true },
51
51
  inputSchema: {
52
52
  messageId: z.string().describe('Message ID'),
53
53
  format: z.enum(['full', 'metadata', 'raw']).optional().describe('Message format (default: full)'),
54
+ // Requires gog >= 0.37.0. Before that (openclaw/gogcli#992) the JSON
55
+ // carried the headers and body TWICE — once inside `message`, once
56
+ // copied to the top level — so the flag meant to shrink the payload
57
+ // enlarged it. MIN_GOG_VERSION is the guard; there is no runtime check.
58
+ sanitizeContent: z.boolean().optional().describe('Return agent-oriented sanitized content: HTML stripped, HTTP(S) URLs removed, raw Gmail payloads omitted from the JSON. The largest payload-size reduction available here. Note the URL removal is lossy — omit this when you need to follow a link out of the message.'),
54
59
  account: accountParam,
55
60
  },
56
- }, async ({ messageId, format, account }) => {
61
+ }, async ({ messageId, format, sanitizeContent, account }) => {
57
62
  const args = ['gmail', 'get', messageId];
58
63
  if (format) args.push(`--format=${format}`);
64
+ if (sanitizeContent) args.push('--sanitize-content');
59
65
  return runOrDiagnose(args, { account });
60
66
  });
61
67
 
package/src/worker.ts CHANGED
@@ -38,7 +38,7 @@ import { gogAuth, CONNECTOR_INSTRUCTIONS, type GogProps } from './connector-auth
38
38
  // connector with all ~360 tools at once. Add whichever paths you want as separate
39
39
  // connectors in claude.ai (each authorizes with the same connector key).
40
40
 
41
- const VERSION = '2.23.2'; // x-release-please-version
41
+ const VERSION = '2.24.0'; // x-release-please-version
42
42
 
43
43
  // Build an McpAgent subclass whose init() registers `registrars` onto its server,
44
44
  // each handler wrapped in the ALS scope carrying the per-session Fly executor.
@@ -124,6 +124,35 @@ describe('gog_auth_add', () => {
124
124
  );
125
125
  });
126
126
 
127
+ // gog 0.37.0's Connected Sheets reads need bigquery.readonly, which no
128
+ // `services` selection covers. --force-consent is not optional alongside it:
129
+ // Google re-prompts for a NEW scope only when consent is forced, so without
130
+ // it the grant can come back missing the scope AND reporting success.
131
+ it('passes --extra-scopes with --force-consent', async () => {
132
+ vi.mocked(runner.run).mockResolvedValue('Authorization successful');
133
+ const harness = await setupHandlers();
134
+ await harness.callTool('gog_auth_add', {
135
+ email: 'user@gmail.com',
136
+ services: 'sheets',
137
+ extraScopes: 'https://www.googleapis.com/auth/bigquery.readonly',
138
+ });
139
+ expect(runner.run).toHaveBeenCalledWith(
140
+ ['auth', 'add', 'user@gmail.com', '--services', 'sheets',
141
+ '--extra-scopes=https://www.googleapis.com/auth/bigquery.readonly', '--force-consent'],
142
+ { interactive: true, timeout: 300_000 },
143
+ );
144
+ });
145
+
146
+ it('does not force consent when no extra scopes are asked for', async () => {
147
+ vi.mocked(runner.run).mockResolvedValue('Authorization successful');
148
+ const harness = await setupHandlers();
149
+ await harness.callTool('gog_auth_add', { email: 'user@gmail.com' });
150
+ expect(runner.run).toHaveBeenCalledWith(
151
+ ['auth', 'add', 'user@gmail.com', '--services', 'all'],
152
+ { interactive: true, timeout: 300_000 },
153
+ );
154
+ });
155
+
127
156
  it('returns error text on failure', async () => {
128
157
  vi.mocked(runner.run).mockRejectedValue(new Error('Auth cancelled by user'));
129
158
  const harness = await setupHandlers();
@@ -215,6 +244,21 @@ describe('gog_auth_add_url', () => {
215
244
  );
216
245
  });
217
246
 
247
+ it('appends --extra-scopes after the service scopes', async () => {
248
+ vi.mocked(runner.run).mockResolvedValue('{"auth_url":"https://x"}');
249
+ const harness = await setupHandlers();
250
+ await harness.callTool('gog_auth_add_url', {
251
+ email: 'user@gmail.com',
252
+ services: 'sheets',
253
+ extraScopes: 'https://www.googleapis.com/auth/bigquery.readonly',
254
+ });
255
+ expect(runner.run).toHaveBeenCalledWith(
256
+ ['auth', 'add', 'user@gmail.com', '--remote', '--step', '1', '--services', 'sheets', '--force-consent',
257
+ '--extra-scopes=https://www.googleapis.com/auth/bigquery.readonly'],
258
+ { redactMode: 'tokens' },
259
+ );
260
+ });
261
+
218
262
  it('returns error text on failure', async () => {
219
263
  vi.mocked(runner.run).mockRejectedValue(new Error('client not configured'));
220
264
  const harness = await setupHandlers();
@@ -252,6 +296,22 @@ describe('gog_auth_add_complete', () => {
252
296
  );
253
297
  });
254
298
 
299
+ it('carries the same --extra-scopes as step 1', async () => {
300
+ vi.mocked(runner.run).mockResolvedValue('{"stored":true}');
301
+ const harness = await setupHandlers();
302
+ await harness.callTool('gog_auth_add_complete', {
303
+ email: 'user@gmail.com',
304
+ redirectUrl: 'http://127.0.0.1/cb?code=c&state=s',
305
+ services: 'sheets',
306
+ extraScopes: 'https://www.googleapis.com/auth/bigquery.readonly',
307
+ });
308
+ expect(runner.run).toHaveBeenCalledWith(
309
+ ['auth', 'add', 'user@gmail.com', '--remote', '--step', '2', '--auth-url',
310
+ 'http://127.0.0.1/cb?code=c&state=s', '--services', 'sheets', '--force-consent',
311
+ '--extra-scopes=https://www.googleapis.com/auth/bigquery.readonly'],
312
+ );
313
+ });
314
+
255
315
  it('returns error text on failure (e.g. expired state)', async () => {
256
316
  vi.mocked(runner.run).mockRejectedValue(new Error('no matching manual auth state'));
257
317
  const harness = await setupHandlers();
@@ -17,23 +17,50 @@ describe('gog_calendar_events', () => {
17
17
  expect(runner.run).toHaveBeenCalledWith(['calendar', 'events'], { account: undefined });
18
18
  });
19
19
 
20
- it('appends calendarId and filters when provided', async () => {
20
+ // The window flags are pinned as SEPARATE cases on purpose: gog >= 0.36.0
21
+ // (openclaw/gogcli#981) rejects a fixed preset combined with from/to/days,
22
+ // and days combined with to, so one test passing them all at once would
23
+ // assert an arg array gog refuses to run.
24
+ it('appends calendarId and an explicit from/to range', async () => {
21
25
  vi.mocked(runner.run).mockResolvedValue('{}');
22
26
  const harness = await setupHandlers();
23
27
  await harness.callTool('gog_calendar_events', {
24
28
  calendarId: 'primary',
25
29
  from: '2026-01-01',
26
30
  to: '2026-01-31',
27
- today: true,
28
31
  query: 'standup',
29
32
  all: true,
30
33
  });
31
34
  expect(runner.run).toHaveBeenCalledWith(
32
- ['calendar', 'events', 'primary', '--from=2026-01-01', '--to=2026-01-31', '--today', '--query=standup', '--all'],
35
+ ['calendar', 'events', 'primary', '--from=2026-01-01', '--to=2026-01-31', '--query=standup', '--all'],
33
36
  { account: undefined },
34
37
  );
35
38
  });
36
39
 
40
+ it('appends --today on its own', async () => {
41
+ vi.mocked(runner.run).mockResolvedValue('{}');
42
+ const harness = await setupHandlers();
43
+ await harness.callTool('gog_calendar_events', { today: true });
44
+ expect(runner.run).toHaveBeenCalledWith(['calendar', 'events', '--today'], { account: undefined });
45
+ });
46
+
47
+ it('anchors --days at --from when both are given', async () => {
48
+ vi.mocked(runner.run).mockResolvedValue('{}');
49
+ const harness = await setupHandlers();
50
+ await harness.callTool('gog_calendar_events', { from: '2026-09-25', days: 5 });
51
+ expect(runner.run).toHaveBeenCalledWith(
52
+ ['calendar', 'events', '--from=2026-09-25', '--days=5'],
53
+ { account: undefined },
54
+ );
55
+ });
56
+
57
+ it('passes --days alone as a today-anchored window', async () => {
58
+ vi.mocked(runner.run).mockResolvedValue('{}');
59
+ const harness = await setupHandlers();
60
+ await harness.callTool('gog_calendar_events', { days: 7 });
61
+ expect(runner.run).toHaveBeenCalledWith(['calendar', 'events', '--days=7'], { account: undefined });
62
+ });
63
+
37
64
  it('repeats --event-types for each requested type', async () => {
38
65
  vi.mocked(runner.run).mockResolvedValue('{}');
39
66
  const harness = await setupHandlers();
@@ -201,6 +201,23 @@ describe('gog_gmail_get', () => {
201
201
  expect(runner.run).toHaveBeenCalledWith(['gmail', 'get', 'msg1', '--format=metadata'], { account: undefined });
202
202
  });
203
203
 
204
+ // gog >= 0.37.0 (openclaw/gogcli#992): before that release the sanitized
205
+ // JSON repeated the headers and body at the top level, so this flag grew the
206
+ // payload it exists to shrink. Pinned here as the flag spelling gog expects.
207
+ it('appends --sanitize-content when asked', async () => {
208
+ vi.mocked(runner.run).mockResolvedValue('{}');
209
+ const harness = await setupHandlers();
210
+ await harness.callTool('gog_gmail_get', { messageId: 'msg1', sanitizeContent: true });
211
+ expect(runner.run).toHaveBeenCalledWith(['gmail', 'get', 'msg1', '--sanitize-content'], { account: undefined });
212
+ });
213
+
214
+ it('omits --sanitize-content when false', async () => {
215
+ vi.mocked(runner.run).mockResolvedValue('{}');
216
+ const harness = await setupHandlers();
217
+ await harness.callTool('gog_gmail_get', { messageId: 'msg1', sanitizeContent: false });
218
+ expect(runner.run).toHaveBeenCalledWith(['gmail', 'get', 'msg1'], { account: undefined });
219
+ });
220
+
204
221
  it('returns error text on failure', async () => {
205
222
  vi.mocked(runner.run).mockRejectedValue(new Error('Not found'));
206
223
  const harness = await setupHandlers();