gogcli-mcp 2.30.0 → 4.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 (64) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/dist/index.js +15345 -18417
  4. package/dist/lib.js +13720 -9481
  5. package/manifest.json +2 -2
  6. package/mint.yaml +37 -33
  7. package/package.json +5 -5
  8. package/server.json +2 -2
  9. package/src/attachments.ts +28 -34
  10. package/src/blob-upload.ts +165 -134
  11. package/src/blob-urls.ts +3 -5
  12. package/src/bootstrap-auth.ts +97 -0
  13. package/src/gmail-dispatch-guard.ts +106 -0
  14. package/src/gmail-results.ts +1 -1
  15. package/src/index.ts +3 -4
  16. package/src/lib.ts +24 -9
  17. package/src/pagination.ts +1 -1
  18. package/src/runner.ts +23 -175
  19. package/src/tools/api.ts +7 -7
  20. package/src/tools/appscript.ts +16 -16
  21. package/src/tools/auth.ts +16 -16
  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 +23 -25
  28. package/src/tools/gmail.ts +145 -32
  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 +14 -61
  33. package/tests/attachments.test.ts +11 -14
  34. package/tests/blob-upload.test.ts +235 -160
  35. package/tests/bootstrap-auth.test.ts +245 -0
  36. package/tests/gmail-dispatch-guard.test.ts +132 -0
  37. package/tests/runner-file-args.test.ts +1 -13
  38. package/tests/runner.test.ts +8 -95
  39. package/tests/sdk-single-copy.test.ts +11 -37
  40. package/tests/tools/appscript.test.ts +1 -1
  41. package/tests/tools/auth-401-shapes.test.ts +2 -3
  42. package/tests/tools/auth.test.ts +5 -4
  43. package/tests/tools/chat.test.ts +1 -1
  44. package/tests/tools/drive.test.ts +11 -3
  45. package/tests/tools/gmail.test.ts +244 -14
  46. package/tests/tools/sheets.test.ts +1 -1
  47. package/tests/tools/utils.test.ts +1 -50
  48. package/tests/zod-single-copy.test.ts +8 -16
  49. package/tsconfig.json +1 -4
  50. package/vitest.config.ts +2 -14
  51. package/src/auth-log.ts +0 -205
  52. package/src/connector-auth.ts +0 -303
  53. package/src/connector-runtime.ts +0 -887
  54. package/src/google-probe.ts +0 -113
  55. package/src/google-token.ts +0 -391
  56. package/src/remote-runner.ts +0 -77
  57. package/src/worker.ts +0 -129
  58. package/tests/auth-log.test.ts +0 -530
  59. package/tests/connector-auth.test.ts +0 -559
  60. package/tests/connector-runtime.test.ts +0 -1644
  61. package/tests/google-probe.test.ts +0 -116
  62. package/tests/google-token.test.ts +0 -425
  63. package/tests/remote-runner.test.ts +0 -202
  64. package/tests/worker.test.ts +0 -167
@@ -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
  });
@@ -61,16 +61,16 @@ export function registerAppScriptTools(server: McpServer): void {
61
61
  description:
62
62
  'Write a project\'s files into a local directory, for editing a script as ordinary files. '
63
63
  + 'THE DIRECTORY IS RESOLVED WHERE GOG RUNS, which is the caller\'s own machine only on a local (stdio) deployment: '
64
- + 'on the hosted connector, or any GOG_RUNNER_URL backend, the files land on that server where the caller cannot '
64
+ + 'on a hosted deployment (e.g. mcp-host) the files land on that server where the caller cannot '
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']));
@@ -67,12 +67,12 @@ function registerAuthToolsWith(server: McpServer, defaultServices: string): void
67
67
  'service. Reports per account: whether the token is currently valid, the mapped cause when it is ' +
68
68
  'not, how long ago it was authorized, and a warning as it approaches the 7-day refresh-token limit ' +
69
69
  'that applies to OAuth apps whose consent screen is still in "Testing" mode. Run it proactively to ' +
70
- 're-authorize on your own schedule instead of mid-task. On the hosted connector this is the ONLY ' +
70
+ 're-authorize on your own schedule instead of mid-task. On a hosted deployment this is the ONLY ' +
71
71
  'check that measures Google: a connector showing "connected" or "refreshed" has verified the ' +
72
- 'connector key that reaches the gog machine, and nothing else — the Google credential lives on ' +
73
- 'that machine and can be dead while the connection looks perfectly healthy.',
72
+ 'client\'s connection to the MCP host, and nothing else — the Google credential lives in gog\'s ' +
73
+ 'keyring on that host 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];
@@ -129,17 +129,17 @@ function registerAuthToolsWith(server: McpServer, defaultServices: string): void
129
129
  server.registerTool('gog_auth_add_url', {
130
130
  description:
131
131
  'Begin REMOTE/headless Google authorization (step 1 of 2). Returns a sign-in URL to open in any ' +
132
- 'browser — no local server or terminal on the gogcli host is needed, so this works over the hosted ' +
133
- 'connector where the interactive gog_auth_add cannot. Hand the URL to the user; after they sign in, ' +
132
+ 'browser — no local server or terminal on the gogcli host is needed, so this works on a hosted ' +
133
+ 'deployment where the interactive gog_auth_add cannot. Hand the URL to the user; after they sign in, ' +
134
134
  'the browser is redirected to a localhost URL that fails to load — that is expected. They copy that ' +
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}`);
package/src/tools/chat.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 {
4
4
  accountParam,
@@ -49,10 +49,10 @@ export function registerChatTools(server: McpServer): void {
49
49
  + 'Start here when you do not yet have a space name; gog_chat_spaces_find is faster when you know the room\'s title.'
50
50
  + workspaceOnlyNote,
51
51
  annotations: { readOnlyHint: true },
52
- inputSchema: {
52
+ inputSchema: z.object({
53
53
  ...paginationParams,
54
54
  account: accountParam,
55
- },
55
+ }),
56
56
  }, async ({ max, pageToken, page, all, account }) => {
57
57
  const args = ['chat', 'spaces', 'list'];
58
58
  pushPaginationFlags(args, { max, pageToken, page, all });
@@ -65,12 +65,12 @@ export function registerChatTools(server: McpServer): void {
65
65
  + 'user names a room approximately ("the launch room"); pass exact=true to require the whole title. DMs have no '
66
66
  + 'display name — use gog_chat_dm_space to reach a person.' + workspaceOnlyNote,
67
67
  annotations: { readOnlyHint: true },
68
- inputSchema: {
68
+ inputSchema: z.object({
69
69
  displayName: z.string().describe('Space display name, or part of one'),
70
70
  exact: z.boolean().optional().describe('Require an exact (still case-insensitive) match on the whole display name'),
71
71
  max: z.number().int().optional().describe('Max results per page'),
72
72
  account: accountParam,
73
- },
73
+ }),
74
74
  }, async ({ displayName, exact, max, account }) => {
75
75
  const args = ['chat', 'spaces', 'find', displayName];
76
76
  if (exact) args.push('--exact');
@@ -82,11 +82,11 @@ export function registerChatTools(server: McpServer): void {
82
82
  description:
83
83
  'Create a named Chat space, optionally seeding its membership. Members are added immediately and are notified — this '
84
84
  + 'is visible to other people the moment it runs, so confirm the member list before calling it.' + workspaceOnlyNote,
85
- inputSchema: {
85
+ inputSchema: z.object({
86
86
  displayName: z.string().describe('Display name for the new space'),
87
87
  members: z.array(z.string()).optional().describe('Initial members, as email addresses or "users/..." resource names'),
88
88
  account: accountParam,
89
- },
89
+ }),
90
90
  }, async ({ displayName, members, account }) => {
91
91
  const args = ['chat', 'spaces', 'create', displayName];
92
92
  if (members) for (const member of members) args.push(`--member=${member}`);
@@ -98,11 +98,11 @@ export function registerChatTools(server: McpServer): void {
98
98
  'List the threads in a space, so a reply can be targeted at an existing conversation rather than starting a new one. '
99
99
  + 'Pass a thread name from here as `thread` to gog_chat_messages_send.' + workspaceOnlyNote,
100
100
  annotations: { readOnlyHint: true },
101
- inputSchema: {
101
+ inputSchema: z.object({
102
102
  space: spaceParam,
103
103
  ...paginationParams,
104
104
  account: accountParam,
105
- },
105
+ }),
106
106
  }, async ({ space, max, pageToken, page, all, account }) => {
107
107
  const args = ['chat', 'threads', 'list', space];
108
108
  pushPaginationFlags(args, { max, pageToken, page, all });
@@ -117,7 +117,7 @@ export function registerChatTools(server: McpServer): void {
117
117
  + 'miss". Newest-first needs an explicit order="createTime desc"; Chat\'s own default is oldest-first.'
118
118
  + workspaceOnlyNote,
119
119
  annotations: { readOnlyHint: true },
120
- inputSchema: {
120
+ inputSchema: z.object({
121
121
  space: spaceParam,
122
122
  thread: threadParam,
123
123
  unread: z.boolean().optional().describe('Only messages posted after the account last read this space'),
@@ -125,7 +125,7 @@ export function registerChatTools(server: McpServer): void {
125
125
  .optional().describe('Sort order (Chat default: "createTime asc", i.e. OLDEST first — ask for "createTime desc" when you want the latest messages)'),
126
126
  ...paginationParams,
127
127
  account: accountParam,
128
- },
128
+ }),
129
129
  }, async ({ space, thread, unread, order, max, pageToken, page, all, account }) => {
130
130
  const args = ['chat', 'messages', 'list', space];
131
131
  if (thread) args.push(`--thread=${thread}`);
@@ -146,7 +146,7 @@ export function registerChatTools(server: McpServer): void {
146
146
  + 'which gog\'s chat scope set does NOT request (re-auth with extraScopes to get it). Missing metadata is OMITTED '
147
147
  + 'rather than defaulted, so an absent `read` means unknown while an explicit false means unread.' + workspaceOnlyNote,
148
148
  annotations: { readOnlyHint: true },
149
- inputSchema: {
149
+ inputSchema: z.object({
150
150
  query: z.string().describe('Google Chat filter-syntax query — keywords, or filters such as "from:alice@example.com budget"'),
151
151
  order: z.enum(['create_time desc', 'relevance desc']).optional().describe(
152
152
  'Sort order. NOTE the snake_case, which differs from gog_chat_messages_list\'s camelCase. "relevance desc" needs '
@@ -160,7 +160,7 @@ export function registerChatTools(server: McpServer): void {
160
160
  ...paginationParams,
161
161
  max: z.number().int().min(1).max(100).optional().describe('Max results per page (1-100; Chat search caps a page at 100)'),
162
162
  account: accountParam,
163
- },
163
+ }),
164
164
  }, async ({ query, order, view, markup, max, pageToken, page, all, account }) => {
165
165
  const args = ['chat', 'messages', 'search', query];
166
166
  if (order) args.push(`--order=${order}`);
@@ -176,7 +176,7 @@ export function registerChatTools(server: McpServer): void {
176
176
  + 'this tool, so treat it like sending mail, not like saving a draft. Pass `thread` to reply inside an existing '
177
177
  + 'conversation (from gog_chat_threads_list or a message\'s thread field); omit it to start a new one. Text supports '
178
178
  + 'Chat\'s markdown-ish formatting (*bold*, _italic_, `code`).' + workspaceOnlyNote,
179
- inputSchema: {
179
+ inputSchema: z.object({
180
180
  space: spaceParam,
181
181
  text: z.string().optional().describe('Message text. Optional only when an attachment is supplied.'),
182
182
  thread: threadParam,
@@ -186,7 +186,7 @@ export function registerChatTools(server: McpServer): void {
186
186
  ),
187
187
  attachInline: attachInlineParam,
188
188
  account: accountParam,
189
- },
189
+ }),
190
190
  }, async ({ space, text, thread, attach, attachInline, account }) => {
191
191
  if (text === undefined && !attach?.length && !attachInline?.length) {
192
192
  throw new Error('A Chat message needs text, an attachment, or both.');
@@ -207,12 +207,12 @@ export function registerChatTools(server: McpServer): void {
207
207
  'Send a direct message to one person by email address, creating the DM space if this is the first message. Delivered '
208
208
  + 'immediately and cannot be unsent through this tool. For a room rather than a person, use gog_chat_messages_send.'
209
209
  + workspaceOnlyNote,
210
- inputSchema: {
210
+ inputSchema: z.object({
211
211
  email: z.string().describe('Recipient email address'),
212
212
  text: z.string().describe('Message text'),
213
213
  thread: threadParam,
214
214
  account: accountParam,
215
- },
215
+ }),
216
216
  }, async ({ email, text, thread, account }) => {
217
217
  const args = ['chat', 'dm', 'send', email, `--text=${text}`];
218
218
  if (thread) args.push(`--thread=${thread}`);
@@ -223,10 +223,10 @@ export function registerChatTools(server: McpServer): void {
223
223
  description:
224
224
  'Resolve the DM space for an email address — the bridge from a person to the "spaces/..." name the message tools '
225
225
  + 'want. Creates the space if none exists yet, which is silent: it does not message the person.' + workspaceOnlyNote,
226
- inputSchema: {
226
+ inputSchema: z.object({
227
227
  email: z.string().describe('The other person\'s email address'),
228
228
  account: accountParam,
229
- },
229
+ }),
230
230
  }, async ({ email, account }) => {
231
231
  return runOrDiagnose(['chat', 'dm', 'space', email], { account });
232
232
  });
@@ -237,12 +237,12 @@ export function registerChatTools(server: McpServer): void {
237
237
  + 'SUMMARY per message; come here when you need the individual reactors, or the reaction resource names that '
238
238
  + 'gog_chat_reactions_delete takes.' + workspaceOnlyNote,
239
239
  annotations: { readOnlyHint: true },
240
- inputSchema: {
240
+ inputSchema: z.object({
241
241
  message: z.string().describe('Message resource name ("spaces/AAAA/messages/BBBB"), or a bare message ID together with `space`'),
242
242
  space: z.string().optional().describe('Space resource name — required only when `message` is a bare ID'),
243
243
  ...paginationParams,
244
244
  account: accountParam,
245
- },
245
+ }),
246
246
  }, async ({ message, space, max, pageToken, page, all, account }) => {
247
247
  const args = ['chat', 'messages', 'reactions', 'list', message];
248
248
  if (space) args.push(`--space=${space}`);
@@ -254,12 +254,12 @@ export function registerChatTools(server: McpServer): void {
254
254
  description:
255
255
  'React to a message with an emoji. Visible to the space immediately. Pass the emoji itself ("👍"), not a :shortcode:.'
256
256
  + workspaceOnlyNote,
257
- inputSchema: {
257
+ inputSchema: z.object({
258
258
  message: z.string().describe('Message resource name ("spaces/AAAA/messages/BBBB"), or a bare message ID together with `space`'),
259
259
  emoji: z.string().describe('The emoji character to react with, e.g. "👍"'),
260
260
  space: z.string().optional().describe('Space resource name — required only when `message` is a bare ID'),
261
261
  account: accountParam,
262
- },
262
+ }),
263
263
  }, async ({ message, emoji, space, account }) => {
264
264
  const args = ['chat', 'messages', 'reactions', 'create', message, emoji];
265
265
  if (space) args.push(`--space=${space}`);
@@ -272,10 +272,10 @@ export function registerChatTools(server: McpServer): void {
272
272
  + 'the message\'s and not the emoji — get it from gog_chat_reactions_list. An account can only remove its own reaction.'
273
273
  + workspaceOnlyNote,
274
274
  annotations: { destructiveHint: true },
275
- inputSchema: {
275
+ inputSchema: z.object({
276
276
  reaction: z.string().describe('Reaction resource name, e.g. "spaces/AAAA/messages/BBBB/reactions/CCCC"'),
277
277
  account: accountParam,
278
- },
278
+ }),
279
279
  }, async ({ reaction, account }) => {
280
280
  return runOrDiagnose(['chat', 'messages', 'reactions', 'delete', reaction], { account });
281
281
  });