gogcli-mcp 2.23.1 → 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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +283 -69
- package/dist/lib.js +293 -71
- package/manifest.json +3 -3
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/gmail-results.ts +239 -0
- package/src/lib.ts +9 -0
- package/src/pagination.ts +108 -0
- package/src/runner.ts +1 -1
- package/src/tools/auth.ts +39 -12
- package/src/tools/calendar.ts +28 -7
- package/src/tools/classroom.ts +46 -28
- package/src/tools/drive.ts +6 -4
- package/src/tools/gmail.ts +37 -6
- package/src/tools/utils.ts +36 -5
- package/src/worker.ts +1 -1
- package/tests/gmail-results.test.ts +285 -0
- package/tests/page-cursor-contract.test.ts +50 -0
- package/tests/pagination.test.ts +102 -0
- package/tests/tools/auth.test.ts +60 -0
- package/tests/tools/calendar.test.ts +81 -3
- package/tests/tools/gmail.test.ts +163 -0
package/src/tools/classroom.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { accountParam, runOrDiagnose, registerRunTool } from './utils.js';
|
|
3
|
+
import { accountParam, runOrDiagnose, registerRunTool, pageTokenParam, pageAliasParam, resolvePageToken} from './utils.js';
|
|
4
4
|
|
|
5
5
|
export function registerClassroomTools(server: McpServer): void {
|
|
6
6
|
server.registerTool('gog_classroom_courses_list', {
|
|
@@ -11,17 +11,19 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
11
11
|
teacher: z.string().optional().describe('Filter by teacher user ID'),
|
|
12
12
|
student: z.string().optional().describe('Filter by student user ID'),
|
|
13
13
|
max: z.number().optional().describe('Max results per page'),
|
|
14
|
-
|
|
14
|
+
pageToken: pageTokenParam,
|
|
15
|
+
page: pageAliasParam,
|
|
15
16
|
all: z.boolean().optional().describe('Fetch all pages'),
|
|
16
17
|
account: accountParam,
|
|
17
18
|
},
|
|
18
|
-
}, async ({ state, teacher, student, max, page, all, account }) => {
|
|
19
|
+
}, async ({ state, teacher, student, max, pageToken, page, all, account }) => {
|
|
19
20
|
const args = ['classroom', 'courses', 'list'];
|
|
20
21
|
if (state) args.push(`--state=${state}`);
|
|
21
22
|
if (teacher) args.push(`--teacher=${teacher}`);
|
|
22
23
|
if (student) args.push(`--student=${student}`);
|
|
23
24
|
if (max !== undefined) args.push(`--max=${max}`);
|
|
24
|
-
|
|
25
|
+
const token = resolvePageToken({ pageToken, page });
|
|
26
|
+
if (token) args.push(`--page=${token}`);
|
|
25
27
|
if (all) args.push('--all');
|
|
26
28
|
return runOrDiagnose(args, { account });
|
|
27
29
|
});
|
|
@@ -43,14 +45,16 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
43
45
|
inputSchema: {
|
|
44
46
|
courseId: z.string().describe('Course ID'),
|
|
45
47
|
max: z.number().optional().describe('Max results per page'),
|
|
46
|
-
|
|
48
|
+
pageToken: pageTokenParam,
|
|
49
|
+
page: pageAliasParam,
|
|
47
50
|
all: z.boolean().optional().describe('Fetch all pages'),
|
|
48
51
|
account: accountParam,
|
|
49
52
|
},
|
|
50
|
-
}, async ({ courseId, max, page, all, account }) => {
|
|
53
|
+
}, async ({ courseId, max, pageToken, page, all, account }) => {
|
|
51
54
|
const args = ['classroom', 'students', 'list', courseId];
|
|
52
55
|
if (max !== undefined) args.push(`--max=${max}`);
|
|
53
|
-
|
|
56
|
+
const token = resolvePageToken({ pageToken, page });
|
|
57
|
+
if (token) args.push(`--page=${token}`);
|
|
54
58
|
if (all) args.push('--all');
|
|
55
59
|
return runOrDiagnose(args, { account });
|
|
56
60
|
});
|
|
@@ -73,14 +77,16 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
73
77
|
inputSchema: {
|
|
74
78
|
courseId: z.string().describe('Course ID'),
|
|
75
79
|
max: z.number().optional().describe('Max results per page'),
|
|
76
|
-
|
|
80
|
+
pageToken: pageTokenParam,
|
|
81
|
+
page: pageAliasParam,
|
|
77
82
|
all: z.boolean().optional().describe('Fetch all pages'),
|
|
78
83
|
account: accountParam,
|
|
79
84
|
},
|
|
80
|
-
}, async ({ courseId, max, page, all, account }) => {
|
|
85
|
+
}, async ({ courseId, max, pageToken, page, all, account }) => {
|
|
81
86
|
const args = ['classroom', 'teachers', 'list', courseId];
|
|
82
87
|
if (max !== undefined) args.push(`--max=${max}`);
|
|
83
|
-
|
|
88
|
+
const token = resolvePageToken({ pageToken, page });
|
|
89
|
+
if (token) args.push(`--page=${token}`);
|
|
84
90
|
if (all) args.push('--all');
|
|
85
91
|
return runOrDiagnose(args, { account });
|
|
86
92
|
});
|
|
@@ -105,16 +111,18 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
105
111
|
students: z.boolean().optional().describe('Include students only'),
|
|
106
112
|
teachers: z.boolean().optional().describe('Include teachers only'),
|
|
107
113
|
max: z.number().optional().describe('Max results per page'),
|
|
108
|
-
|
|
114
|
+
pageToken: pageTokenParam,
|
|
115
|
+
page: pageAliasParam,
|
|
109
116
|
all: z.boolean().optional().describe('Fetch all pages'),
|
|
110
117
|
account: accountParam,
|
|
111
118
|
},
|
|
112
|
-
}, async ({ courseId, students, teachers, max, page, all, account }) => {
|
|
119
|
+
}, async ({ courseId, students, teachers, max, pageToken, page, all, account }) => {
|
|
113
120
|
const args = ['classroom', 'roster', courseId];
|
|
114
121
|
if (students) args.push('--students');
|
|
115
122
|
if (teachers) args.push('--teachers');
|
|
116
123
|
if (max !== undefined) args.push(`--max=${max}`);
|
|
117
|
-
|
|
124
|
+
const token = resolvePageToken({ pageToken, page });
|
|
125
|
+
if (token) args.push(`--page=${token}`);
|
|
118
126
|
if (all) args.push('--all');
|
|
119
127
|
return runOrDiagnose(args, { account });
|
|
120
128
|
});
|
|
@@ -128,18 +136,20 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
128
136
|
topic: z.string().optional().describe('Filter by topic ID'),
|
|
129
137
|
orderBy: z.string().optional().describe('Sort order (e.g. "updateTime desc")'),
|
|
130
138
|
max: z.number().optional().describe('Max results per page'),
|
|
131
|
-
|
|
139
|
+
pageToken: pageTokenParam,
|
|
140
|
+
page: pageAliasParam,
|
|
132
141
|
all: z.boolean().optional().describe('Fetch all pages'),
|
|
133
142
|
scanPages: z.number().optional().describe('Max pages to scan when filtering'),
|
|
134
143
|
account: accountParam,
|
|
135
144
|
},
|
|
136
|
-
}, async ({ courseId, state, topic, orderBy, max, page, all, scanPages, account }) => {
|
|
145
|
+
}, async ({ courseId, state, topic, orderBy, max, pageToken, page, all, scanPages, account }) => {
|
|
137
146
|
const args = ['classroom', 'coursework', 'list', courseId];
|
|
138
147
|
if (state) args.push(`--state=${state}`);
|
|
139
148
|
if (topic) args.push(`--topic=${topic}`);
|
|
140
149
|
if (orderBy) args.push(`--order-by=${orderBy}`);
|
|
141
150
|
if (max !== undefined) args.push(`--max=${max}`);
|
|
142
|
-
|
|
151
|
+
const token = resolvePageToken({ pageToken, page });
|
|
152
|
+
if (token) args.push(`--page=${token}`);
|
|
143
153
|
if (all) args.push('--all');
|
|
144
154
|
if (scanPages !== undefined) args.push(`--scan-pages=${scanPages}`);
|
|
145
155
|
return runOrDiagnose(args, { account });
|
|
@@ -167,17 +177,19 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
167
177
|
late: z.enum(['late', 'not-late']).optional().describe('Filter by late status'),
|
|
168
178
|
user: z.string().optional().describe('Filter by student user ID'),
|
|
169
179
|
max: z.number().optional().describe('Max results per page'),
|
|
170
|
-
|
|
180
|
+
pageToken: pageTokenParam,
|
|
181
|
+
page: pageAliasParam,
|
|
171
182
|
all: z.boolean().optional().describe('Fetch all pages'),
|
|
172
183
|
account: accountParam,
|
|
173
184
|
},
|
|
174
|
-
}, async ({ courseId, courseworkId, state, late, user, max, page, all, account }) => {
|
|
185
|
+
}, async ({ courseId, courseworkId, state, late, user, max, pageToken, page, all, account }) => {
|
|
175
186
|
const args = ['classroom', 'submissions', 'list', courseId, courseworkId];
|
|
176
187
|
if (state) args.push(`--state=${state}`);
|
|
177
188
|
if (late) args.push(`--late=${late}`);
|
|
178
189
|
if (user) args.push(`--user=${user}`);
|
|
179
190
|
if (max !== undefined) args.push(`--max=${max}`);
|
|
180
|
-
|
|
191
|
+
const token = resolvePageToken({ pageToken, page });
|
|
192
|
+
if (token) args.push(`--page=${token}`);
|
|
181
193
|
if (all) args.push('--all');
|
|
182
194
|
return runOrDiagnose(args, { account });
|
|
183
195
|
});
|
|
@@ -260,16 +272,18 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
260
272
|
state: z.string().optional().describe('Filter by announcement state'),
|
|
261
273
|
orderBy: z.string().optional().describe('Sort order'),
|
|
262
274
|
max: z.number().optional().describe('Max results per page'),
|
|
263
|
-
|
|
275
|
+
pageToken: pageTokenParam,
|
|
276
|
+
page: pageAliasParam,
|
|
264
277
|
all: z.boolean().optional().describe('Fetch all pages'),
|
|
265
278
|
account: accountParam,
|
|
266
279
|
},
|
|
267
|
-
}, async ({ courseId, state, orderBy, max, page, all, account }) => {
|
|
280
|
+
}, async ({ courseId, state, orderBy, max, pageToken, page, all, account }) => {
|
|
268
281
|
const args = ['classroom', 'announcements', 'list', courseId];
|
|
269
282
|
if (state) args.push(`--state=${state}`);
|
|
270
283
|
if (orderBy) args.push(`--order-by=${orderBy}`);
|
|
271
284
|
if (max !== undefined) args.push(`--max=${max}`);
|
|
272
|
-
|
|
285
|
+
const token = resolvePageToken({ pageToken, page });
|
|
286
|
+
if (token) args.push(`--page=${token}`);
|
|
273
287
|
if (all) args.push('--all');
|
|
274
288
|
return runOrDiagnose(args, { account });
|
|
275
289
|
});
|
|
@@ -308,14 +322,16 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
308
322
|
inputSchema: {
|
|
309
323
|
courseId: z.string().describe('Course ID'),
|
|
310
324
|
max: z.number().optional().describe('Max results per page'),
|
|
311
|
-
|
|
325
|
+
pageToken: pageTokenParam,
|
|
326
|
+
page: pageAliasParam,
|
|
312
327
|
all: z.boolean().optional().describe('Fetch all pages'),
|
|
313
328
|
account: accountParam,
|
|
314
329
|
},
|
|
315
|
-
}, async ({ courseId, max, page, all, account }) => {
|
|
330
|
+
}, async ({ courseId, max, pageToken, page, all, account }) => {
|
|
316
331
|
const args = ['classroom', 'topics', 'list', courseId];
|
|
317
332
|
if (max !== undefined) args.push(`--max=${max}`);
|
|
318
|
-
|
|
333
|
+
const token = resolvePageToken({ pageToken, page });
|
|
334
|
+
if (token) args.push(`--page=${token}`);
|
|
319
335
|
if (all) args.push('--all');
|
|
320
336
|
return runOrDiagnose(args, { account });
|
|
321
337
|
});
|
|
@@ -339,16 +355,18 @@ export function registerClassroomTools(server: McpServer): void {
|
|
|
339
355
|
course: z.string().optional().describe('Filter by course ID'),
|
|
340
356
|
user: z.string().optional().describe('Filter by user ID'),
|
|
341
357
|
max: z.number().optional().describe('Max results per page'),
|
|
342
|
-
|
|
358
|
+
pageToken: pageTokenParam,
|
|
359
|
+
page: pageAliasParam,
|
|
343
360
|
all: z.boolean().optional().describe('Fetch all pages'),
|
|
344
361
|
account: accountParam,
|
|
345
362
|
},
|
|
346
|
-
}, async ({ course, user, max, page, all, account }) => {
|
|
363
|
+
}, async ({ course, user, max, pageToken, page, all, account }) => {
|
|
347
364
|
const args = ['classroom', 'invitations', 'list'];
|
|
348
365
|
if (course) args.push(`--course=${course}`);
|
|
349
366
|
if (user) args.push(`--user=${user}`);
|
|
350
367
|
if (max !== undefined) args.push(`--max=${max}`);
|
|
351
|
-
|
|
368
|
+
const token = resolvePageToken({ pageToken, page });
|
|
369
|
+
if (token) args.push(`--page=${token}`);
|
|
352
370
|
if (all) args.push('--all');
|
|
353
371
|
return runOrDiagnose(args, { account });
|
|
354
372
|
});
|
package/src/tools/drive.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { rawTextResult } from '@chrischall/mcp-utils';
|
|
5
5
|
import { run, runBinary } from '../runner.js';
|
|
6
|
-
import { accountParam, diagnose, runOrDiagnose, registerRunTool } from './utils.js';
|
|
6
|
+
import { accountParam, diagnose, runOrDiagnose, registerRunTool, pageTokenParam, pageAliasParam, resolvePageToken} from './utils.js';
|
|
7
7
|
|
|
8
8
|
// A native Google Doc exports to text directly; anything else (PDF, image,
|
|
9
9
|
// docx, …) is first copied WITH conversion to this type, which makes Drive run
|
|
@@ -25,16 +25,18 @@ export function registerDriveTools(server: McpServer): void {
|
|
|
25
25
|
inputSchema: {
|
|
26
26
|
folderId: z.string().optional().describe('Folder ID to list (default: root)'),
|
|
27
27
|
max: z.number().optional().describe('Max results (default: 20)'),
|
|
28
|
-
|
|
28
|
+
pageToken: pageTokenParam,
|
|
29
|
+
page: pageAliasParam,
|
|
29
30
|
query: z.string().optional().describe('Drive query filter (e.g. "name contains \'budget\'")'),
|
|
30
31
|
allDrives: z.boolean().optional().describe('Include shared drives (default: true). Set false for My Drive only.'),
|
|
31
32
|
account: accountParam,
|
|
32
33
|
},
|
|
33
|
-
}, async ({ folderId, max, page, query, allDrives, account }) => {
|
|
34
|
+
}, async ({ folderId, max, pageToken, page, query, allDrives, account }) => {
|
|
34
35
|
const args = ['drive', 'ls'];
|
|
35
36
|
if (folderId) args.push(`--parent=${folderId}`);
|
|
36
37
|
if (max !== undefined) args.push(`--max=${max}`);
|
|
37
|
-
|
|
38
|
+
const token = resolvePageToken({ pageToken, page });
|
|
39
|
+
if (token) args.push(`--page=${token}`);
|
|
38
40
|
if (query) args.push(`--query=${query}`);
|
|
39
41
|
if (allDrives === false) args.push('--no-all-drives');
|
|
40
42
|
return runOrDiagnose(args, { account });
|
package/src/tools/gmail.ts
CHANGED
|
@@ -1,36 +1,67 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { accountParam, runOrDiagnose, registerRunTool, payloadArg } from './utils.js';
|
|
3
|
+
import { accountParam, runOrDiagnose, registerRunTool, payloadArg, pageTokenParam, pageAliasParam, resolvePageToken } from './utils.js';
|
|
4
|
+
import { finalizeGmailSearch, fetchGmailPages } from '../gmail-results.js';
|
|
4
5
|
import type { GogArg } from '../runner.js';
|
|
5
6
|
|
|
6
7
|
export function registerGmailTools(server: McpServer): void {
|
|
7
8
|
server.registerTool('gog_gmail_search', {
|
|
8
|
-
description: 'Search Gmail threads using Gmail query syntax (e.g. "from:alice subject:invoice is:unread"). The query is passed verbatim to Gmail; a bare name token (from:alison) matches per Gmail\'s own heuristics, a full address (from:alison@example.com) is exact. To match a contact across several addresses, OR them: from:(a@x.com OR b@y.com).'
|
|
9
|
+
description: 'Search Gmail threads using Gmail query syntax (e.g. "from:alice subject:invoice is:unread"). The query is passed verbatim to Gmail; a bare name token (from:alison) matches per Gmail\'s own heuristics, a full address (from:alison@example.com) is exact. To match a contact across several addresses, OR them: from:(a@x.com OR b@y.com). '
|
|
10
|
+
+ 'Results are ALWAYS newest-first by Gmail\'s internalDate — the wrapper sorts them, so the first result is the most recent match and a recent message can never be buried below older ones. '
|
|
11
|
+
+ 'IMPORTANT — a response carrying "truncated": true is an INCOMPLETE view of the matches: NEVER report that a message does not exist, or that there is no such mail, on the strength of one. Page through it (pass nextPageToken back as `pageToken`), set maxPages to walk several pages in one call, or narrow the query, and only then draw a conclusion. '
|
|
12
|
+
+ 'If you already know the thread, do not search for it at all — read it directly with gog_gmail_thread_get, which returns the whole thread and cannot be truncated or mis-ranked.',
|
|
9
13
|
annotations: { readOnlyHint: true },
|
|
10
14
|
inputSchema: {
|
|
11
15
|
query: z.string().describe('Gmail search query'),
|
|
12
16
|
max: z.number().int().optional().describe('Max results to return (default: 10)'),
|
|
17
|
+
pageToken: pageTokenParam,
|
|
18
|
+
page: pageAliasParam,
|
|
19
|
+
maxPages: z.number().int().positive().max(20).optional().describe('Walk up to this many pages in ONE call and merge the results, instead of returning a single page. Use it for existence questions (\"is there any mail matching X?\"), which a single page cannot answer. Stops early at the last page; if pages remain when the cap is hit the response is still marked truncated. Prefer this over all=true, which is unbounded.'),
|
|
20
|
+
all: z.boolean().optional().describe('Fetch every page instead of one. Removes truncation entirely, at the cost of one API round-trip per page — the reliable way to answer "does any message match?" for a query with few expected hits.'),
|
|
13
21
|
fromContact: z.string().optional().describe('Resolve a Google Contact (name or email) to its addresses and AND a from:(addr OR addr) clause onto the query — saves looking the contact up first when you only know who, not which address.'),
|
|
14
22
|
account: accountParam,
|
|
15
23
|
},
|
|
16
|
-
}, async ({ query, max, fromContact, account }) => {
|
|
24
|
+
}, async ({ query, max, pageToken, page, maxPages, all, fromContact, account }) => {
|
|
17
25
|
const args = ['gmail', 'search', query];
|
|
18
26
|
if (max !== undefined) args.push(`--max=${max}`);
|
|
27
|
+
if (all) args.push('--all');
|
|
19
28
|
if (fromContact) args.push(`--from-contact=${fromContact}`);
|
|
20
|
-
|
|
29
|
+
// The cursor is applied per page rather than baked into args, so the
|
|
30
|
+
// multi-page walk can advance it.
|
|
31
|
+
const runPage = (tok: string | undefined) =>
|
|
32
|
+
runOrDiagnose(tok ? [...args, `--page=${tok}`] : args, { account });
|
|
33
|
+
const token = resolvePageToken({ pageToken, page });
|
|
34
|
+
const result = maxPages !== undefined
|
|
35
|
+
? await fetchGmailPages(runPage, 'threads', maxPages, token)
|
|
36
|
+
: await runPage(token);
|
|
37
|
+
return finalizeGmailSearch(result, {
|
|
38
|
+
itemsKey: 'threads',
|
|
39
|
+
method: 'users.threads.list',
|
|
40
|
+
query,
|
|
41
|
+
account,
|
|
42
|
+
// --from-contact is expanded INSIDE gog, against the People API, so the
|
|
43
|
+
// query Gmail actually saw is not the one we hold here.
|
|
44
|
+
queryIsExact: !fromContact,
|
|
45
|
+
});
|
|
21
46
|
});
|
|
22
47
|
|
|
23
48
|
server.registerTool('gog_gmail_get', {
|
|
24
|
-
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.',
|
|
25
50
|
annotations: { readOnlyHint: true },
|
|
26
51
|
inputSchema: {
|
|
27
52
|
messageId: z.string().describe('Message ID'),
|
|
28
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.'),
|
|
29
59
|
account: accountParam,
|
|
30
60
|
},
|
|
31
|
-
}, async ({ messageId, format, account }) => {
|
|
61
|
+
}, async ({ messageId, format, sanitizeContent, account }) => {
|
|
32
62
|
const args = ['gmail', 'get', messageId];
|
|
33
63
|
if (format) args.push(`--format=${format}`);
|
|
64
|
+
if (sanitizeContent) args.push('--sanitize-content');
|
|
34
65
|
return runOrDiagnose(args, { account });
|
|
35
66
|
});
|
|
36
67
|
|
package/src/tools/utils.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { errorResult, rawTextResult } from '@chrischall/mcp-utils';
|
|
|
5
5
|
import { run, isRunnerTransportError } from '../runner.js';
|
|
6
6
|
import type { GogArg, RunnerFailureKind } from '../runner.js';
|
|
7
7
|
import { normalizeTimestamps } from '../timestamps.js';
|
|
8
|
+
import { stripConsumedPageToken } from '../pagination.js';
|
|
8
9
|
|
|
9
10
|
// Byte size at or below which a payload stays on the plain inline flag.
|
|
10
11
|
//
|
|
@@ -77,10 +78,36 @@ export const ids = {
|
|
|
77
78
|
person: z.string().describe('Person resource name (people/...) or email'),
|
|
78
79
|
};
|
|
79
80
|
|
|
80
|
-
//
|
|
81
|
+
// THE CURSOR IS NAMED AFTER THE FIELD THAT CARRIES IT. Every paginated response
|
|
82
|
+
// reports its cursor as `nextPageToken`, so the request parameter is
|
|
83
|
+
// `pageToken` — a caller reading a response can guess the input name and be
|
|
84
|
+
// right. It used to be `page` (after gog's own `--page` flag), and that
|
|
85
|
+
// mismatch was not cosmetic: MCP tool inputs are zod objects, which SILENTLY
|
|
86
|
+
// STRIP unknown keys, so a client that inferred `pageToken` had it dropped
|
|
87
|
+
// before the handler ran and got page 1 back forever — same items, same token,
|
|
88
|
+
// no error. Two "that email doesn't exist" incidents came from exactly that.
|
|
89
|
+
export const pageTokenParam = z.string().optional().describe(
|
|
90
|
+
'Cursor for the NEXT page. Pass back the nextPageToken from a previous response verbatim, ' +
|
|
91
|
+
'keeping the query and max identical: call once, then call again with pageToken=<that value>. ' +
|
|
92
|
+
'A response with NO nextPageToken is the last page.',
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Kept so anything already sending `page` keeps working. Prefer pageTokenParam.
|
|
96
|
+
export const pageAliasParam = z.string().optional().describe(
|
|
97
|
+
'Deprecated alias for pageToken, accepted so existing callers keep working. Use pageToken — ' +
|
|
98
|
+
'it matches the nextPageToken field in the response.',
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// The one place the alias collapses into a single value.
|
|
102
|
+
export function resolvePageToken(p: { pageToken?: string; page?: string }): string | undefined {
|
|
103
|
+
return p.pageToken ?? p.page;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Pagination params — appear in 30+ tools across base + extras.
|
|
81
107
|
export const paginationParams = {
|
|
82
108
|
max: z.number().int().optional().describe('Max results'),
|
|
83
|
-
|
|
109
|
+
pageToken: pageTokenParam,
|
|
110
|
+
page: pageAliasParam,
|
|
84
111
|
all: z.boolean().optional().describe('Fetch all pages'),
|
|
85
112
|
};
|
|
86
113
|
|
|
@@ -88,10 +115,11 @@ export const paginationParams = {
|
|
|
88
115
|
// paginationParams above. Use together to keep call sites concise.
|
|
89
116
|
export function pushPaginationFlags(
|
|
90
117
|
args: string[],
|
|
91
|
-
p: { max?: number; page?: string; all?: boolean },
|
|
118
|
+
p: { max?: number; pageToken?: string; page?: string; all?: boolean },
|
|
92
119
|
): void {
|
|
93
120
|
if (p.max !== undefined) args.push(`--max=${p.max}`);
|
|
94
|
-
|
|
121
|
+
const token = resolvePageToken(p);
|
|
122
|
+
if (token) args.push(`--page=${token}`);
|
|
95
123
|
if (p.all) args.push('--all');
|
|
96
124
|
}
|
|
97
125
|
|
|
@@ -330,7 +358,10 @@ export async function runOrDiagnose(
|
|
|
330
358
|
// truth would stop telling it. Losslessness wins over presentation there —
|
|
331
359
|
// the friendlier views of the same data are already normalized.
|
|
332
360
|
const raw = await run(args, options);
|
|
333
|
-
|
|
361
|
+
// Same seam, same reason as normalizeTimestamps: doing this per call site
|
|
362
|
+
// would let one paginated tool forget and go on reporting a spent cursor as
|
|
363
|
+
// if it were a live one. `lossless` opts the raw dumps out of both.
|
|
364
|
+
return rawTextResult(options.lossless ? raw : stripConsumedPageToken(normalizeTimestamps(raw)));
|
|
334
365
|
} catch (err) {
|
|
335
366
|
return diagnose(err);
|
|
336
367
|
}
|
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.
|
|
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.
|