gogcli-mcp 2.29.1 → 3.0.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.
Files changed (47) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/dist/index.js +17105 -18892
  4. package/dist/lib.js +15157 -9562
  5. package/manifest.json +1 -1
  6. package/mint.yaml +2 -2
  7. package/package.json +5 -5
  8. package/server.json +2 -2
  9. package/src/blob-upload.ts +179 -0
  10. package/src/blob-urls.ts +280 -0
  11. package/src/connector-auth.ts +22 -6
  12. package/src/connector-login.ts +87 -0
  13. package/src/connector-runtime.ts +33 -10
  14. package/src/gmail-dispatch-guard.ts +106 -0
  15. package/src/gmail-results.ts +1 -1
  16. package/src/lib.ts +29 -0
  17. package/src/pagination.ts +1 -1
  18. package/src/runner.ts +19 -2
  19. package/src/tools/api.ts +7 -7
  20. package/src/tools/appscript.ts +15 -15
  21. package/src/tools/auth.ts +11 -11
  22. package/src/tools/calendar.ts +13 -13
  23. package/src/tools/chat.ts +25 -25
  24. package/src/tools/classroom.ts +49 -49
  25. package/src/tools/contacts.ts +9 -9
  26. package/src/tools/docs.ts +13 -13
  27. package/src/tools/drive.ts +22 -22
  28. package/src/tools/gmail.ts +136 -23
  29. package/src/tools/sheets.ts +15 -15
  30. package/src/tools/slides.ts +13 -13
  31. package/src/tools/tasks.ts +13 -13
  32. package/src/tools/utils.ts +2 -3
  33. package/src/worker.ts +52 -64
  34. package/tests/blob-upload.test.ts +204 -0
  35. package/tests/blob-urls.test.ts +319 -0
  36. package/tests/connector-login.test.ts +151 -0
  37. package/tests/connector-runtime.test.ts +21 -1
  38. package/tests/gmail-dispatch-guard.test.ts +132 -0
  39. package/tests/runner.test.ts +62 -1
  40. package/tests/sdk-single-copy.test.ts +18 -36
  41. package/tests/tools/appscript.test.ts +1 -1
  42. package/tests/tools/chat.test.ts +1 -1
  43. package/tests/tools/gmail.test.ts +242 -12
  44. package/tests/tools/sheets.test.ts +1 -1
  45. package/tests/zod-single-copy.test.ts +2 -2
  46. package/tsconfig.json +1 -3
  47. package/vitest.config.ts +3 -5
@@ -13,7 +13,7 @@ export type { RunnerFailureKind } from './runner.js';
13
13
 
14
14
  // Runtime helpers for the Cloudflare connector (worker.ts), split out here so
15
15
  // they can be unit-tested under the node pool — worker.ts itself imports the
16
- // Worker-only `@chrischall/mcp-connector`/`agents` runtime and cannot load in
16
+ // Worker-only `agents` runtime and cannot load in
17
17
  // node. These helpers touch only the `runExecutor` seam and global `fetch`.
18
18
 
19
19
  // Mirrors runner.ts's TIMEOUT_MS: the budget the stdio path gives a `gog` call
@@ -504,14 +504,11 @@ export function makeFlyExecutor(
504
504
  // Throttle state for the refusal probe, held PER EXECUTOR rather than in a
505
505
  // module-level map.
506
506
  //
507
- // That is the scope the thing being throttled actually has: on the Worker one
508
- // executor is built per agent session (`worker.ts` `init()`), on stdio one per
509
- // process (`remote-runner.ts`). So a session that is hammering a refused
510
- // credential rate-limits itself without a second, unrelated session's probe
511
- // being suppressed by it — a module global would let one caller's retry loop
512
- // silence everybody else's first and only measurement. It also means the state
513
- // dies with the session instead of accumulating endpoints for the isolate's
514
- // lifetime.
507
+ // That is the scope the thing being throttled actually has: the stateless
508
+ // Worker caches one executor per endpoint + connector key for the isolate,
509
+ // while stdio builds one per process (`remote-runner.ts`). Callers sharing a
510
+ // backend credential therefore share its throttle, without one credential's
511
+ // retry loop suppressing the first measurement for another credential.
515
512
  let lastProbeAt = Number.NEGATIVE_INFINITY;
516
513
 
517
514
  /**
@@ -860,10 +857,36 @@ async function attempt(
860
857
  return stdout;
861
858
  }
862
859
 
860
+ /**
861
+ * Cache Fly executors by endpoint and connector key for one runtime isolate.
862
+ *
863
+ * `makeFlyExecutor` owns the refusal-probe throttle state, so resolving a new
864
+ * executor for every stateless tool call would reset that state and allow each
865
+ * retry to spawn another keyring-locking Google probe.
866
+ */
867
+ export function createFlyExecutorResolver(
868
+ factory: (endpoint: string, key: string) => GogExecutor = makeFlyExecutor,
869
+ ): (endpoint: string, key: string) => GogExecutor {
870
+ const byEndpoint = new Map<string, Map<string, GogExecutor>>();
871
+ return (endpoint, key) => {
872
+ let byKey = byEndpoint.get(endpoint);
873
+ if (!byKey) {
874
+ byKey = new Map();
875
+ byEndpoint.set(endpoint, byKey);
876
+ }
877
+ let executor = byKey.get(key);
878
+ if (!executor) {
879
+ executor = factory(endpoint, key);
880
+ byKey.set(key, executor);
881
+ }
882
+ return executor;
883
+ };
884
+ }
885
+
863
886
  // Wrap an McpServer in a Proxy whose `registerTool` (and `tool`, if any
864
887
  // registrar uses it) intercepts the tool handler so it runs inside the
865
888
  // `runExecutor` ALS scope. This is the crux of the connector: it lets the
866
- // UNCHANGED base registrars forward every `gog` call to the per-session Fly
889
+ // UNCHANGED base registrars forward every `gog` call to the request's Fly
867
890
  // executor without any change to the registrars or `runner.ts` — when a
868
891
  // handler's `run()` looks up `runExecutor.getStore()` it finds `executor` and
869
892
  // forwards instead of spawning. Everything else proxies through via Reflect.
@@ -0,0 +1,106 @@
1
+ import type { CallToolResult, InputRequiredResult, ServerContext } from '@modelcontextprotocol/server';
2
+ import { readEnvVar, requireConfirmation } from '@chrischall/mcp-utils';
3
+
4
+ // ============================================================================
5
+ // THE SAFETY RAIL. gog_gmail_reply / reply_all / send / forward / autoreply are
6
+ // the only tools in this fleet that put a message irreversibly into someone
7
+ // else's mailbox on the FIRST call. Every other Gmail write either stages
8
+ // something (drafts) or acts on mail already in this account (labels,
9
+ // archive, trash). A caller that meant "save a draft" and picked the wrong
10
+ // tool — or an agent that inherited the wrong reply target — used to find out
11
+ // only after the send API call already succeeded.
12
+ //
13
+ // MCP elicitation makes the first round inert: it returns an input_required
14
+ // result containing the preview, and only the protocol retry carrying the
15
+ // user's accepted confirmation dispatches. The confirmation is never a tool
16
+ // argument, so a model cannot bypass the user by setting a boolean itself.
17
+ // ============================================================================
18
+ /** Apply the shared stateless confirmation flow with Gmail-specific copy. */
19
+ export function requireGmailDispatchConfirmation(
20
+ ctx: ServerContext,
21
+ op: string,
22
+ details: Record<string, unknown>,
23
+ ): InputRequiredResult | CallToolResult | undefined {
24
+ return requireConfirmation(ctx, {
25
+ action: op,
26
+ message: 'Review and confirm this email dispatch:',
27
+ details,
28
+ confirmationLabel: 'Confirm that this email should be sent now.',
29
+ });
30
+ }
31
+
32
+ // Over-inclusive on purpose: this feeds an audit log and a caller-facing
33
+ // preview, neither of which is the enforcement point (the protocol gate is).
34
+ // Missing a real recipient would be the dangerous direction of error; catching
35
+ // an extra email-shaped substring is not.
36
+ const EMAIL_PATTERN = /[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(?:\.[a-z0-9-]+)+/gi;
37
+
38
+ export function extractEmails(...values: Array<string | undefined | null>): string[] {
39
+ const seen = new Set<string>();
40
+ const out: string[] = [];
41
+ for (const value of values) {
42
+ if (!value) continue;
43
+ const matches = value.match(EMAIL_PATTERN);
44
+ if (!matches) continue;
45
+ for (const match of matches) {
46
+ const lower = match.toLowerCase();
47
+ if (!seen.has(lower)) {
48
+ seen.add(lower);
49
+ out.push(lower);
50
+ }
51
+ }
52
+ }
53
+ return out;
54
+ }
55
+
56
+ // GOG_GMAIL_TRUSTED_DOMAINS names domains that are never "external" — by
57
+ // default just the sending account's own domain, so a reply-all that includes
58
+ // the account itself never reads as a surprise. Comma-separated, additive.
59
+ function trustedDomains(account: string | undefined): Set<string> {
60
+ const domains = new Set<string>();
61
+ const raw = readEnvVar('GOG_GMAIL_TRUSTED_DOMAINS');
62
+ if (raw) {
63
+ for (const part of raw.split(',')) {
64
+ const domain = part.trim().toLowerCase();
65
+ if (domain) domains.add(domain);
66
+ }
67
+ }
68
+ const acct = account ?? readEnvVar('GOG_ACCOUNT');
69
+ const at = acct?.indexOf('@') ?? -1;
70
+ if (acct && at > -1) domains.add(acct.slice(at + 1).toLowerCase());
71
+ return domains;
72
+ }
73
+
74
+ // A distinguishable, greppable event for every mail dispatch — recipient
75
+ // count plus whichever recipients fall outside the trusted-domain list — so an
76
+ // unexpected external send (outside counsel, a wrong-number alias) can be
77
+ // caught after the fact even if the confirmation step above is somehow
78
+ // bypassed by a future caller. stdout is the JSON-RPC channel, so this goes to
79
+ // stderr like every other diagnostic in this repo.
80
+ export function logGmailDispatch(tool: string, recipients: string[], account?: string): void {
81
+ const domains = trustedDomains(account);
82
+ const externalRecipients = recipients.filter((recipient) => {
83
+ const at = recipient.indexOf('@');
84
+ const domain = at > -1 ? recipient.slice(at + 1) : '';
85
+ return !domain || !domains.has(domain);
86
+ });
87
+ const event = {
88
+ event: 'gmail_dispatch',
89
+ tool,
90
+ recipientCount: recipients.length,
91
+ externalRecipientCount: externalRecipients.length,
92
+ hasExternalRecipients: externalRecipients.length > 0,
93
+ externalRecipients,
94
+ timestamp: new Date().toISOString(),
95
+ };
96
+ process.stderr.write(`${JSON.stringify(event)}\n`);
97
+ }
98
+
99
+ // The single place a CallToolResult's text is pulled back out, for the tools
100
+ // here that need to read gog's own JSON before deciding what to preview or
101
+ // log. Mirrors the shape every runOrDiagnose result actually returns
102
+ // (content[0].text); never throws on an unexpected shape.
103
+ export function resultText(result: CallToolResult): string {
104
+ const first = result.content[0];
105
+ return first && first.type === 'text' && typeof first.text === 'string' ? first.text : '{}';
106
+ }
@@ -1,4 +1,4 @@
1
- import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
1
+ import type { CallToolResult } from '@modelcontextprotocol/server';
2
2
  import { rawTextResult } from '@chrischall/mcp-utils';
3
3
  import { run } from './runner.js';
4
4
  import { annotateTruncation, hasMorePages } from './pagination.js';
package/src/lib.ts CHANGED
@@ -21,6 +21,16 @@ export {
21
21
  // same tool name from both registrar lists would be a duplicate-name error.
22
22
  export { replySchema, appendReplyFlags } from './tools/gmail.js';
23
23
  export type { ReplyFlags } from './tools/gmail.js';
24
+ // The gmail confirmation gate — gog_gmail_reply/send/forward/autoreply are
25
+ // the only tools that dispatch mail irreversibly on the first call. The
26
+ // gmail sub-package's send-side forward/autoreply tools reuse these directly
27
+ // rather than re-declaring the gate; the draft-side twins never import them.
28
+ export {
29
+ extractEmails,
30
+ logGmailDispatch,
31
+ requireGmailDispatchConfirmation,
32
+ resultText,
33
+ } from './gmail-dispatch-guard.js';
24
34
  export { run, runBinary, runExecutor, isGogFileArg, MIN_GOG_VERSION } from './runner.js';
25
35
  // Sub-package tools that read gog JSON through bare `run()` (rather than the
26
36
  // `runOrDiagnose` seam) must still apply this, or their timestamps skip the
@@ -64,3 +74,22 @@ export {
64
74
  registerRunTool,
65
75
  assertNotBoth,
66
76
  } from './tools/utils.js';
77
+ // Signed URLs for mcp-host's per-registration blob store — the only way a
78
+ // hosted child can hand an agent BYTES it can fetch with `curl`. Exported from
79
+ // the base package so the gmail sub-package (and any later one) shares ONE
80
+ // implementation of the signing; see src/blob-urls.ts for why that matters.
81
+ // Deliberately NOT re-exporting the payload builders: a caller outside this
82
+ // module has no business assembling a payload and signing it by hand, which is
83
+ // the mistake the shared minter exists to prevent.
84
+ export {
85
+ blobStoreFromEnv,
86
+ createBlobUrlMinter,
87
+ BLOB_URL_MAX_TTL_MS,
88
+ BLOB_URL_DEFAULT_TTL_MS,
89
+ } from './blob-urls.js';
90
+ export type { BlobStoreConfig, BlobUrlMinter, MintOptions } from './blob-urls.js';
91
+ // The other half of that hop: under the hosted connector the bytes are on the
92
+ // RUNNER's disk and this child never sees them, so the runner is asked to
93
+ // stream them to the URL this process minted. See src/blob-upload.ts.
94
+ export { uploadToBlobStore, RUNNER_UPLOAD_TIMEOUT_MS } from './blob-upload.js';
95
+ export type { BlobUploadRequest, BlobUploadOutcome, BlobUploadOptions } from './blob-upload.js';
package/src/pagination.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
1
+ import type { CallToolResult } from '@modelcontextprotocol/server';
2
2
  import { rawTextResult } from '@chrischall/mcp-utils';
3
3
 
4
4
  // gog reports an exhausted cursor as `"nextPageToken": ""` rather than omitting
package/src/runner.ts CHANGED
@@ -217,7 +217,7 @@ const TIMEOUT_MS = 30_000;
217
217
  // so the requirement change is surfaced in the release notes (see
218
218
  // .github/release.yml). This is the single source of truth for the required
219
219
  // version; keep the README/CLAUDE.md mention in sync.
220
- export const MIN_GOG_VERSION = '0.39.1';
220
+ export const MIN_GOG_VERSION = '0.40.0';
221
221
 
222
222
  // Interpret the GOG_READONLY kill-switch. `readEnvVar` already treats blank
223
223
  // values, 'undefined'/'null' sentinels, and unresolved .mcpb placeholders
@@ -236,12 +236,29 @@ function readonlyEnvEnabled(): boolean {
236
236
  // instead of the stored refresh token. The broader patterns are
237
237
  // defense-in-depth — the parent process's shell may have other Google /
238
238
  // cloud / API secrets in scope that the child has no business seeing.
239
+ //
240
+ // `_KEY`, not `_API_KEY|_PRIVATE_KEY`: those were four spellings of "a key"
241
+ // with the bare one missing, and TWO credentials this repo hands its own
242
+ // process fell in that gap. `MCP_BLOB_SIGNING_KEY` mints the signed blob URLs
243
+ // a `deliver="url"` download is uploaded to — a signature IS the whole access
244
+ // control on that store — and `GOG_RUNNER_KEY` is the bearer for the Fly
245
+ // backend, where `POST /run` is arbitrary `gog` argv. Neither is read by the
246
+ // child: both are spent HERE, and when `GOG_RUNNER_URL` is set nothing is
247
+ // spawned at all. `_CREDENTIALS` generalises the named
248
+ // GOOGLE_APPLICATION_CREDENTIALS above, which stays named because it is the
249
+ // one gog itself would act on.
250
+ //
251
+ // The list is bounded by what the child LEGITIMATELY READS, which is why
252
+ // `_PASSWORD` is deliberately NOT on it: `GOG_KEYRING_PASSWORD` decrypts gog's
253
+ // own file keyring (`GOG_KEYRING_BACKEND=file`), so that rule would strip the
254
+ // one credential the child needs and turn every call into an auth failure.
255
+ // Both directions are tested — a widening with no control case is a guess.
239
256
  function sanitizedEnv(): NodeJS.ProcessEnv {
240
257
  const result: NodeJS.ProcessEnv = {};
241
258
  for (const [key, value] of Object.entries(process.env)) {
242
259
  if (key === 'GOG_ACCESS_TOKEN') continue;
243
260
  if (key === 'GOOGLE_APPLICATION_CREDENTIALS') continue;
244
- if (/(_TOKEN|_SECRET|_API_KEY|_PRIVATE_KEY)$/.test(key)) continue;
261
+ if (/(_TOKEN|_SECRET|_KEY|_CREDENTIALS)$/.test(key)) continue;
245
262
  result[key] = value;
246
263
  }
247
264
  return result;
package/src/tools/api.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import { McpServer } from '@modelcontextprotocol/server';
2
2
  import { z } from 'zod';
3
3
  import { accountParam, runOrDiagnose } from './utils.js';
4
4
 
@@ -10,10 +10,10 @@ export function registerApiTools(server: McpServer): void {
10
10
  server.registerTool('gog_api_list', {
11
11
  description: 'List the Google Discovery APIs available for gog_api_call / gog_api_describe (name + version + title).',
12
12
  annotations: { readOnlyHint: true },
13
- inputSchema: {
13
+ inputSchema: z.object({
14
14
  all: z.boolean().optional().describe('Include every Discovery API (including preview/less-common ones) instead of the curated default set'),
15
15
  account: accountParam,
16
- },
16
+ }),
17
17
  }, async ({ all, account }) => {
18
18
  const args = ['api', 'list'];
19
19
  if (all) args.push('--all');
@@ -23,12 +23,12 @@ export function registerApiTools(server: McpServer): void {
23
23
  server.registerTool('gog_api_describe', {
24
24
  description: 'Describe a Google Discovery API, or a single method within it — its parameters, request/response schema, and required OAuth scopes. Use this to discover the exact api/version/method and params before calling gog_api_call.',
25
25
  annotations: { readOnlyHint: true },
26
- inputSchema: {
26
+ inputSchema: z.object({
27
27
  api: z.string().describe('Discovery API name (e.g. drive, gmail, calendar)'),
28
28
  version: z.string().describe('API version (e.g. v3, v1)'),
29
29
  method: z.string().optional().describe('Optional method id to describe a single method (e.g. files.list); omit to describe the whole API'),
30
30
  account: accountParam,
31
- },
31
+ }),
32
32
  }, async ({ api, version, method, account }) => {
33
33
  const args = ['api', 'describe', api, version];
34
34
  if (method) args.push(method);
@@ -38,7 +38,7 @@ export function registerApiTools(server: McpServer): void {
38
38
  server.registerTool('gog_api_call', {
39
39
  description: 'Call any Discovery-described Google API method directly — an escape hatch for endpoints gog has no dedicated tool for. Find the exact api/version/method/params with gog_api_describe first. Read methods (GET/LIST) run as-is. Mutating methods (POST/PUT/PATCH/DELETE) are refused unless you set allowWrite=true — keep it false to preview, or set dryRun=true to print the intended request without sending it.',
40
40
  annotations: { destructiveHint: true },
41
- inputSchema: {
41
+ inputSchema: z.object({
42
42
  api: z.string().describe('Discovery API name (e.g. drive, gmail, calendar)'),
43
43
  version: z.string().describe('API version (e.g. v3, v1)'),
44
44
  method: z.string().describe('Method id to call (e.g. files.list, files.create)'),
@@ -48,7 +48,7 @@ export function registerApiTools(server: McpServer): void {
48
48
  allowWrite: z.boolean().optional().describe('Required to invoke a mutating method (POST/PUT/PATCH/DELETE). Without it, gog refuses write methods. Leave unset for read-only calls.'),
49
49
  dryRun: z.boolean().optional().describe('Print the intended request and exit without sending it (no changes made)'),
50
50
  account: accountParam,
51
- },
51
+ }),
52
52
  }, async ({ api, version, method, params, body, scope, allowWrite, dryRun, account }) => {
53
53
  const args = ['api', 'call', api, version, method];
54
54
  if (params) args.push(`--params=${params}`);
@@ -1,4 +1,4 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import { McpServer } from '@modelcontextprotocol/server';
2
2
  import { z } from 'zod';
3
3
  import {
4
4
  accountParam,
@@ -35,10 +35,10 @@ export function registerAppScriptTools(server: McpServer): void {
35
35
  'Get an Apps Script project\'s metadata: title, creator, create/update times, and the parent Drive file when the '
36
36
  + 'project is bound to a Sheet, Doc or Form. Use gog_appscript_content to read the actual code.' + apiEnableNote,
37
37
  annotations: { readOnlyHint: true },
38
- inputSchema: {
38
+ inputSchema: z.object({
39
39
  scriptId: scriptIdParam,
40
40
  account: accountParam,
41
- },
41
+ }),
42
42
  }, async ({ scriptId, account }) => {
43
43
  return runOrDiagnose(['appscript', 'get', scriptId], { account });
44
44
  });
@@ -49,10 +49,10 @@ export function registerAppScriptTools(server: McpServer): void {
49
49
  + 'tool to reach for when the question is "what does this script do"; it needs no filesystem, so it works the same '
50
50
  + 'on a hosted deployment as it does locally, unlike gog_appscript_pull.' + apiEnableNote,
51
51
  annotations: { readOnlyHint: true },
52
- inputSchema: {
52
+ inputSchema: z.object({
53
53
  scriptId: scriptIdParam,
54
54
  account: accountParam,
55
- },
55
+ }),
56
56
  }, async ({ scriptId, account }) => {
57
57
  return runOrDiagnose(['appscript', 'content', scriptId], { account });
58
58
  });
@@ -65,12 +65,12 @@ export function registerAppScriptTools(server: McpServer): void {
65
65
  + 'reach them. Use gog_appscript_content there instead — it returns the same source in the response. Existing files '
66
66
  + 'are left alone unless overwrite is set. Read-only as far as Google is concerned: nothing is pushed back.'
67
67
  + apiEnableNote,
68
- inputSchema: {
68
+ inputSchema: z.object({
69
69
  scriptId: scriptIdParam,
70
70
  dir: z.string().describe('Destination directory, resolved on the machine where gog runs'),
71
71
  overwrite: z.boolean().optional().describe('Overwrite files that already exist in dir'),
72
72
  account: accountParam,
73
- },
73
+ }),
74
74
  }, async ({ scriptId, dir, overwrite, account }) => {
75
75
  const args = ['appscript', 'pull', scriptId, dir];
76
76
  if (overwrite) args.push('--overwrite');
@@ -82,11 +82,11 @@ export function registerAppScriptTools(server: McpServer): void {
82
82
  'Create a new, empty Apps Script project. Pass parentId to bind it to a Drive file (a Sheet, Doc or Form), which is '
83
83
  + 'what makes the script a container-bound script with access to that document; omit it for a standalone project. '
84
84
  + 'gog cannot upload code, so the project starts empty either way.' + apiEnableNote,
85
- inputSchema: {
85
+ inputSchema: z.object({
86
86
  title: z.string().describe('Project title'),
87
87
  parentId: z.string().optional().describe('Drive file ID to bind the project to (Sheet, Doc or Form). Omit for a standalone project.'),
88
88
  account: accountParam,
89
- },
89
+ }),
90
90
  }, async ({ title, parentId, account }) => {
91
91
  const args = ['appscript', 'create', `--title=${title}`];
92
92
  if (parentId) args.push(`--parent-id=${parentId}`);
@@ -99,11 +99,11 @@ export function registerAppScriptTools(server: McpServer): void {
99
99
  + 'deployment ID from here is what gog_appscript_run_function needs when a script is not running in dev mode.'
100
100
  + apiEnableNote,
101
101
  annotations: { readOnlyHint: true },
102
- inputSchema: {
102
+ inputSchema: z.object({
103
103
  scriptId: scriptIdParam,
104
104
  ...paginationParams,
105
105
  account: accountParam,
106
- },
106
+ }),
107
107
  }, async ({ scriptId, max, pageToken, page, all, account }) => {
108
108
  const args = ['appscript', 'deployments', scriptId];
109
109
  pushPaginationFlags(args, { max, pageToken, page, all });
@@ -115,11 +115,11 @@ export function registerAppScriptTools(server: McpServer): void {
115
115
  'List a project\'s saved versions — the immutable snapshots deployments point at, with their numbers and '
116
116
  + 'descriptions. Useful for answering "what is actually deployed" next to gog_appscript_deployments.' + apiEnableNote,
117
117
  annotations: { readOnlyHint: true },
118
- inputSchema: {
118
+ inputSchema: z.object({
119
119
  scriptId: scriptIdParam,
120
120
  ...paginationParams,
121
121
  account: accountParam,
122
- },
122
+ }),
123
123
  }, async ({ scriptId, max, pageToken, page, all, account }) => {
124
124
  const args = ['appscript', 'versions', scriptId];
125
125
  pushPaginationFlags(args, { max, pageToken, page, all });
@@ -136,13 +136,13 @@ export function registerAppScriptTools(server: McpServer): void {
136
136
  + 'deployed version, and only works if the account owns the script. '
137
137
  + 'This is NOT the escape hatch — gog_appscript_run is that.' + apiEnableNote,
138
138
  annotations: { destructiveHint: true },
139
- inputSchema: {
139
+ inputSchema: z.object({
140
140
  scriptId: scriptIdParam,
141
141
  functionName: z.string().describe('Name of the function to call, e.g. "doWork"'),
142
142
  params: z.string().optional().describe('Function parameters as a JSON ARRAY of positional arguments, e.g. \'["a", 1]\' — not an object'),
143
143
  devMode: z.boolean().optional().describe('Run the latest saved code rather than the deployed version (owner only)'),
144
144
  account: accountParam,
145
- },
145
+ }),
146
146
  }, async ({ scriptId, functionName, params, devMode, account }) => {
147
147
  // gog passes --params through to the API as-is, so a malformed value comes
148
148
  // back as a Google error about the request body rather than about the
package/src/tools/auth.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import { McpServer } from '@modelcontextprotocol/server';
2
2
  import { z } from 'zod';
3
3
  import { run } from '../runner.js';
4
4
  import { errorResult, rawTextResult } from '@chrischall/mcp-utils';
@@ -35,7 +35,7 @@ function registerAuthToolsWith(server: McpServer, defaultServices: string): void
35
35
  'exactly like a healthy one, scopes and all. Use gog_auth_health to check whether an account ' +
36
36
  'can actually authenticate.',
37
37
  annotations: { readOnlyHint: true },
38
- inputSchema: {},
38
+ inputSchema: z.object({}),
39
39
  }, async () => {
40
40
  try {
41
41
  return rawTextResult(await run(['auth', 'list']));
@@ -50,7 +50,7 @@ function registerAuthToolsWith(server: McpServer, defaultServices: string): void
50
50
  'name this is not a health check — it reads local setup and does not contact Google, so it says ' +
51
51
  'nothing about whether an account can still authenticate. Use gog_auth_health for that.',
52
52
  annotations: { readOnlyHint: true },
53
- inputSchema: {},
53
+ inputSchema: z.object({}),
54
54
  }, async () => {
55
55
  try {
56
56
  return rawTextResult(await run(['auth', 'status']));
@@ -72,7 +72,7 @@ function registerAuthToolsWith(server: McpServer, defaultServices: string): void
72
72
  'connector key that reaches the gog machine, and nothing else — the Google credential lives on ' +
73
73
  'that machine and can be dead while the connection looks perfectly healthy.',
74
74
  annotations: { readOnlyHint: true },
75
- inputSchema: {},
75
+ inputSchema: z.object({}),
76
76
  }, async () => {
77
77
  try {
78
78
  // `run` injects --json; --check makes gog probe each token live.
@@ -85,7 +85,7 @@ function registerAuthToolsWith(server: McpServer, defaultServices: string): void
85
85
  server.registerTool('gog_auth_services', {
86
86
  description: 'List all Google services supported by gogcli and the OAuth scopes each requires.',
87
87
  annotations: { readOnlyHint: true },
88
- inputSchema: {},
88
+ inputSchema: z.object({}),
89
89
  }, async () => {
90
90
  try {
91
91
  return rawTextResult(await run(['auth', 'services']));
@@ -102,11 +102,11 @@ function registerAuthToolsWith(server: McpServer, defaultServices: string): void
102
102
  'If the browser does not open automatically, a fallback URL is included in the response. ' +
103
103
  'Use gog_auth_list to check which accounts are already configured.',
104
104
  annotations: { destructiveHint: true },
105
- inputSchema: {
105
+ inputSchema: z.object({
106
106
  email: z.string().describe('Google account email to authorize'),
107
107
  services: z.string().optional().default(defaultServices).describe(servicesDescribe),
108
108
  extraScopes: z.string().optional().describe(extraScopesDescribe),
109
- },
109
+ }),
110
110
  }, async ({ email, services = defaultServices, extraScopes }) => {
111
111
  try {
112
112
  const args = ['auth', 'add', email, '--services', services];
@@ -135,11 +135,11 @@ function registerAuthToolsWith(server: McpServer, defaultServices: string): void
135
135
  'full redirected URL (from the address bar) and you pass it to gog_auth_add_complete. The link is ' +
136
136
  'valid for 10 minutes. If you pass a custom `services` here, pass the SAME value to ' +
137
137
  'gog_auth_add_complete or the second step will not match this one.',
138
- inputSchema: {
138
+ inputSchema: z.object({
139
139
  email: z.string().describe('Google account email to authorize'),
140
140
  services: z.string().optional().default(defaultServices).describe(servicesDescribe),
141
141
  extraScopes: z.string().optional().describe(`${extraScopesDescribe} Pass the SAME value to gog_auth_add_complete.`),
142
- },
142
+ }),
143
143
  }, async ({ email, services = defaultServices, extraScopes }) => {
144
144
  try {
145
145
  // --force-consent guarantees a refresh token even if a prior grant exists
@@ -161,7 +161,7 @@ function registerAuthToolsWith(server: McpServer, defaultServices: string): void
161
161
  'it. Use the SAME `services` value you passed to gog_auth_add_url. Must run within 10 minutes of ' +
162
162
  'step 1 and against the same gogcli host.',
163
163
  annotations: { destructiveHint: true },
164
- inputSchema: {
164
+ inputSchema: z.object({
165
165
  email: z.string().describe('Google account email being authorized (same as step 1)'),
166
166
  redirectUrl: z.string().describe(
167
167
  'The full localhost redirect URL the user copied from the browser address bar after signing in ' +
@@ -174,7 +174,7 @@ function registerAuthToolsWith(server: McpServer, defaultServices: string): void
174
174
  'Extra OAuth scope URIs — MUST match the value passed to gog_auth_add_url, for the same reason `services` must: ' +
175
175
  'the two steps have to describe the same grant.',
176
176
  ),
177
- },
177
+ }),
178
178
  }, async ({ email, redirectUrl, services = defaultServices, extraScopes }) => {
179
179
  try {
180
180
  const args = ['auth', 'add', email, '--remote', '--step', '2', '--auth-url', redirectUrl,
@@ -1,4 +1,4 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import { McpServer } from '@modelcontextprotocol/server';
2
2
  import { z } from 'zod';
3
3
  import { viewParam, resolveView } from '@chrischall/mcp-utils';
4
4
  import { accountParam, runOrDiagnose, registerRunTool, pageTokenParam, pageAliasParam, resolvePageToken } from './utils.js';
@@ -82,7 +82,7 @@ export function registerCalendarTools(server: McpServer): void {
82
82
  + '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. '
83
83
  + 'A response carrying "truncated": true is an incomplete view — never conclude an event does not exist from one.',
84
84
  annotations: { readOnlyHint: true },
85
- inputSchema: {
85
+ inputSchema: z.object({
86
86
  calendarId: z.string().optional().describe('Calendar ID (default: primary calendar)'),
87
87
  from: z.string().optional().describe('Start time filter (RFC3339, date, or natural language)'),
88
88
  to: z.string().optional().describe('End time filter (RFC3339, date, or natural language). Mutually exclusive with today and with days.'),
@@ -105,7 +105,7 @@ export function registerCalendarTools(server: McpServer): void {
105
105
  + 'listing\'s bytes — plus etag/iCalUID/kind. Ask for full when you need a body or a guest list.',
106
106
  }),
107
107
  account: accountParam,
108
- },
108
+ }),
109
109
  }, async ({ calendarId, from, to, days, today, query, max, pageToken, page, all, eventTypes, timezone, view, account }) => {
110
110
  const args = ['calendar', 'events'];
111
111
  if (calendarId) args.push(calendarId);
@@ -134,12 +134,12 @@ export function registerCalendarTools(server: McpServer): void {
134
134
  server.registerTool('gog_calendar_get', {
135
135
  description: 'Get a specific calendar event by ID.',
136
136
  annotations: { readOnlyHint: true },
137
- inputSchema: {
137
+ inputSchema: z.object({
138
138
  calendarId: z.string().describe('Calendar ID'),
139
139
  eventId: z.string().describe('Event ID'),
140
140
  timezone: z.string().optional().describe('Display timezone for event times (IANA name, e.g. America/New_York, or "local" for the system timezone). Default: the event\'s timezone, then its calendar\'s timezone.'),
141
141
  account: accountParam,
142
- },
142
+ }),
143
143
  }, async ({ calendarId, eventId, timezone, account }) => {
144
144
  const args = ['calendar', 'event', calendarId, eventId];
145
145
  if (timezone) args.push(`--timezone=${timezone}`);
@@ -149,7 +149,7 @@ export function registerCalendarTools(server: McpServer): void {
149
149
  server.registerTool('gog_calendar_create', {
150
150
  description: 'Create a calendar event. Set withZoom=true to attach a Zoom meeting (requires Zoom S2S OAuth setup via gog_zoom_auth_setup; the join URL + meeting ID + passcode are appended to the event description — Google rejects native conference card writes from non-Workspace-Marketplace OAuth clients).',
151
151
  annotations: { destructiveHint: false },
152
- inputSchema: {
152
+ inputSchema: z.object({
153
153
  calendarId: z.string().describe('Calendar ID (use "primary" for the default calendar)'),
154
154
  summary: z.string().describe('Event title'),
155
155
  from: z.string().describe('Start time (RFC3339 or date for all-day events)'),
@@ -162,7 +162,7 @@ export function registerCalendarTools(server: McpServer): void {
162
162
  withZoom: z.boolean().optional().describe('Create a Zoom video conference for this event (requires Zoom S2S OAuth setup)'),
163
163
  ...reminderParams,
164
164
  account: accountParam,
165
- },
165
+ }),
166
166
  }, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, reminders, noReminders, account }) => {
167
167
  const args = ['calendar', 'create', calendarId, `--summary=${summary}`, `--from=${from}`, `--to=${to}`];
168
168
  if (description) args.push(`--description=${description}`);
@@ -178,7 +178,7 @@ export function registerCalendarTools(server: McpServer): void {
178
178
  server.registerTool('gog_calendar_update', {
179
179
  description: 'Update an existing calendar event. Zoom: withZoom adds a Zoom meeting, regenerateZoom replaces the existing one, removeZoom strips it. removeMeet clears the event\'s Google Meet conference data (e.g. before attaching another provider). Conference flags are independent — use one per call.',
180
180
  annotations: { destructiveHint: false },
181
- inputSchema: {
181
+ inputSchema: z.object({
182
182
  calendarId: z.string().describe('Calendar ID'),
183
183
  eventId: z.string().describe('Event ID'),
184
184
  summary: z.string().optional().describe('New event title'),
@@ -195,7 +195,7 @@ export function registerCalendarTools(server: McpServer): void {
195
195
  removeMeet: z.boolean().optional().describe('Remove the event\'s Google Meet video conference (clears conference data only)'),
196
196
  ...reminderParams,
197
197
  account: accountParam,
198
- },
198
+ }),
199
199
  }, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, reminders, noReminders, account }) => {
200
200
  const args = ['calendar', 'update', calendarId, eventId];
201
201
  if (summary !== undefined) args.push(`--summary=${summary}`);
@@ -217,11 +217,11 @@ export function registerCalendarTools(server: McpServer): void {
217
217
  server.registerTool('gog_calendar_delete', {
218
218
  description: 'Delete a calendar event.',
219
219
  annotations: { destructiveHint: true },
220
- inputSchema: {
220
+ inputSchema: z.object({
221
221
  calendarId: z.string().describe('Calendar ID'),
222
222
  eventId: z.string().describe('Event ID'),
223
223
  account: accountParam,
224
- },
224
+ }),
225
225
  }, async ({ calendarId, eventId, account }) => {
226
226
  // gog gates this delete behind a confirmation; the runner injects
227
227
  // --no-input, so without --force it refuses at runtime.
@@ -231,13 +231,13 @@ export function registerCalendarTools(server: McpServer): void {
231
231
  server.registerTool('gog_calendar_respond', {
232
232
  description: 'Respond to a calendar event invitation.',
233
233
  annotations: { destructiveHint: true },
234
- inputSchema: {
234
+ inputSchema: z.object({
235
235
  calendarId: z.string().describe('Calendar ID'),
236
236
  eventId: z.string().describe('Event ID'),
237
237
  status: z.enum(['accepted', 'declined', 'tentative']).describe('Response status'),
238
238
  comment: z.string().optional().describe('Optional comment to include with response'),
239
239
  account: accountParam,
240
- },
240
+ }),
241
241
  }, async ({ calendarId, eventId, status, comment, account }) => {
242
242
  const args = ['calendar', 'respond', calendarId, eventId, `--status=${status}`];
243
243
  if (comment) args.push(`--comment=${comment}`);