gogcli-mcp 2.25.0 → 2.26.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/README.md +9 -4
- package/SKILL.md +10 -5
- package/dist/index.js +483 -124
- package/dist/lib.js +407 -85
- package/manifest.json +81 -1
- package/mint.yaml +106 -0
- package/package.json +5 -5
- package/server.json +2 -2
- package/src/gmail-results.ts +29 -2
- package/src/lib.ts +2 -0
- package/src/runner.ts +1 -1
- package/src/server.ts +6 -0
- package/src/tools/appscript.ts +173 -0
- package/src/tools/calendar.ts +56 -2
- package/src/tools/chat.ts +253 -0
- package/src/worker.ts +1 -1
- package/tests/gmail-results.test.ts +47 -0
- package/tests/server.test.ts +2 -0
- package/tests/tools/appscript.test.ts +159 -0
- package/tests/tools/calendar.test.ts +121 -0
- package/tests/tools/chat.test.ts +284 -0
package/manifest.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"manifest_version": "0.3",
|
|
4
4
|
"name": "gogcli-mcp",
|
|
5
5
|
"display_name": "gogcli",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.26.0",
|
|
7
7
|
"description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
|
@@ -423,6 +423,86 @@
|
|
|
423
423
|
{
|
|
424
424
|
"name": "gog_slides_run",
|
|
425
425
|
"description": "Run any gog slides subcommand (escape hatch)"
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
"name": "gog_chat_spaces_list",
|
|
429
|
+
"description": "List the Google Chat spaces the account belongs to, with their resource names"
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
"name": "gog_chat_spaces_find",
|
|
433
|
+
"description": "Find Chat spaces by display name (substring by default, or exact)"
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
"name": "gog_chat_spaces_create",
|
|
437
|
+
"description": "Create a named Chat space, optionally seeding its membership"
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
"name": "gog_chat_threads_list",
|
|
441
|
+
"description": "List the threads in a Chat space"
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
"name": "gog_chat_messages_list",
|
|
445
|
+
"description": "Read messages in a space, with mentions and reaction summaries"
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
"name": "gog_chat_messages_send",
|
|
449
|
+
"description": "Post a message to a Chat space (immediately visible; supports attachments)"
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
"name": "gog_chat_dm_send",
|
|
453
|
+
"description": "Send a direct message to one person by email address"
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
"name": "gog_chat_dm_space",
|
|
457
|
+
"description": "Resolve the DM space resource name for an email address"
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
"name": "gog_chat_reactions_list",
|
|
461
|
+
"description": "List the emoji reactions on a message, with who reacted"
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
"name": "gog_chat_reactions_create",
|
|
465
|
+
"description": "React to a Chat message with an emoji"
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
"name": "gog_chat_reactions_delete",
|
|
469
|
+
"description": "Remove one emoji reaction by its resource name"
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
"name": "gog_chat_run",
|
|
473
|
+
"description": "Run any gog chat subcommand not covered by the other tools"
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
"name": "gog_appscript_get",
|
|
477
|
+
"description": "Get an Apps Script project's metadata"
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
"name": "gog_appscript_content",
|
|
481
|
+
"description": "Read a project's source files and manifest inline"
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
"name": "gog_appscript_pull",
|
|
485
|
+
"description": "Write a project's files into a directory on the machine where gog runs"
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
"name": "gog_appscript_create",
|
|
489
|
+
"description": "Create a new, empty Apps Script project (optionally bound to a Drive file)"
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
"name": "gog_appscript_deployments",
|
|
493
|
+
"description": "List a project's deployments"
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
"name": "gog_appscript_versions",
|
|
497
|
+
"description": "List a project's saved versions"
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
"name": "gog_appscript_run_function",
|
|
501
|
+
"description": "Execute a function in a deployed Apps Script project"
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
"name": "gog_appscript_run",
|
|
505
|
+
"description": "Run any gog appscript subcommand not covered by the other tools"
|
|
426
506
|
}
|
|
427
507
|
],
|
|
428
508
|
"compatibility": {
|
package/mint.yaml
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
name: gogcli (Google Workspace)
|
|
3
|
+
slug: gogcli-mcp
|
|
4
|
+
summary: >-
|
|
5
|
+
MCP server wrapping gogcli for Google service access
|
|
6
|
+
command:
|
|
7
|
+
# This package publishes a single bin; naming it keeps the install
|
|
8
|
+
# unambiguous alongside its eight sibling packages.
|
|
9
|
+
bin: gogcli-mcp
|
|
10
|
+
env:
|
|
11
|
+
- name: GOG_CLIENT_ID
|
|
12
|
+
required: false
|
|
13
|
+
help: >-
|
|
14
|
+
Google OAuth client id. NOTE: on the local-spawn path the child `gog`
|
|
15
|
+
receives a sanitized env — runner.ts drops GOG_ACCESS_TOKEN and every
|
|
16
|
+
*_TOKEN / *_SECRET / *_API_KEY / *_PRIVATE_KEY variable — so the CLI
|
|
17
|
+
authenticates from its own stored credentials under $HOME (see
|
|
18
|
+
state.dataDir), not from these variables being passed through.
|
|
19
|
+
- name: GOG_CLIENT_SECRET
|
|
20
|
+
secret: true
|
|
21
|
+
required: false
|
|
22
|
+
help: >-
|
|
23
|
+
Google OAuth client secret. Stripped from the spawned CLI's environment
|
|
24
|
+
by runner.ts's *_SECRET rule — see GOG_CLIENT_ID.
|
|
25
|
+
- name: GOG_REFRESH_TOKEN
|
|
26
|
+
secret: true
|
|
27
|
+
required: false
|
|
28
|
+
help: >-
|
|
29
|
+
Google OAuth refresh token. Stripped from the spawned CLI's environment
|
|
30
|
+
by runner.ts's *_TOKEN rule — see GOG_CLIENT_ID. A hosted deployment must
|
|
31
|
+
therefore carry gog's authorised-account state in its persisted data dir,
|
|
32
|
+
or drive a remote runner via GOG_RUNNER_URL.
|
|
33
|
+
- name: GOG_ACCESS_TOKEN
|
|
34
|
+
secret: true
|
|
35
|
+
required: false
|
|
36
|
+
help: >-
|
|
37
|
+
Deliberately removed from the spawned CLI's environment by runner.ts, so
|
|
38
|
+
that a stale directly-passed token cannot shadow gog's stored refresh
|
|
39
|
+
credential. Leave unset.
|
|
40
|
+
- name: GOG_ACCOUNT
|
|
41
|
+
required: false
|
|
42
|
+
help: >-
|
|
43
|
+
Which configured Google account to act as, when more than one is
|
|
44
|
+
authorised. Defaults to the single/most recent account.
|
|
45
|
+
- name: GOG_READONLY
|
|
46
|
+
required: false
|
|
47
|
+
help: >-
|
|
48
|
+
Set to 1 to refuse every mutating operation. Recommended when the
|
|
49
|
+
connector is shared or you only need reads.
|
|
50
|
+
- name: GOG_PATH
|
|
51
|
+
required: false
|
|
52
|
+
help: >-
|
|
53
|
+
Path to the `gog` binary. Leave unset when the dependency below supplies
|
|
54
|
+
it; set it only to point at a binary you manage yourself.
|
|
55
|
+
- name: GOG_RUNNER_URL
|
|
56
|
+
required: false
|
|
57
|
+
help: >-
|
|
58
|
+
URL of a remote gog runner to execute against instead of spawning the
|
|
59
|
+
local binary. If you set this, add its host to egress.allow.
|
|
60
|
+
- name: GOG_RUNNER_KEY
|
|
61
|
+
secret: true
|
|
62
|
+
required: false
|
|
63
|
+
help: >-
|
|
64
|
+
Shared key authenticating calls to GOG_RUNNER_URL. Required whenever that
|
|
65
|
+
is set.
|
|
66
|
+
- name: GOG_TIMEZONE
|
|
67
|
+
required: false
|
|
68
|
+
help: >-
|
|
69
|
+
The IANA zone gog itself formats its naive timestamps in. The wrapper
|
|
70
|
+
reads it (naiveSourceTimeZone) to re-attach the correct offset, so it
|
|
71
|
+
should match gog's own configuration. Falls back to DISPLAY_TZ, then to
|
|
72
|
+
America/New_York.
|
|
73
|
+
- name: DISPLAY_TZ
|
|
74
|
+
required: false
|
|
75
|
+
help: >-
|
|
76
|
+
The IANA zone every rendered *Display field uses (displayTimeZone,
|
|
77
|
+
default America/New_York). It does not read GOG_TIMEZONE — the fallback
|
|
78
|
+
runs the other way, so this is also what GOG_TIMEZONE falls back to. An
|
|
79
|
+
invalid value degrades to the default rather than throwing.
|
|
80
|
+
dependencies:
|
|
81
|
+
# Every tool shells out to the `gog` CLI; without it the server starts and
|
|
82
|
+
# then fails on the first call. This tag must track
|
|
83
|
+
# packages/gogcli-mcp/src/runner.ts's MIN_GOG_VERSION (the floor the tools
|
|
84
|
+
# assume) and the fly-gog-runner/Dockerfile GOG_VERSION build arg. See
|
|
85
|
+
# CLAUDE.md "Required gog version" — bumping the floor means bumping these
|
|
86
|
+
# nine pins too.
|
|
87
|
+
- kind: github-release
|
|
88
|
+
repo: openclaw/gogcli
|
|
89
|
+
tag: v0.38.1
|
|
90
|
+
asset: "gogcli_*_linux_amd64.tar.gz"
|
|
91
|
+
bin: [gog]
|
|
92
|
+
state:
|
|
93
|
+
dataDir: true
|
|
94
|
+
reason: >-
|
|
95
|
+
`gog` keeps its authorised-account state and token cache under $HOME.
|
|
96
|
+
Without a persistent data dir every cold start has no account configured and
|
|
97
|
+
every tool call fails until the credentials are re-supplied.
|
|
98
|
+
egress:
|
|
99
|
+
allow:
|
|
100
|
+
# Google OAuth token exchange and the Google REST APIs the CLI calls.
|
|
101
|
+
- oauth2.googleapis.com
|
|
102
|
+
- www.googleapis.com
|
|
103
|
+
- googleapis.com
|
|
104
|
+
#
|
|
105
|
+
# NOTE: if you set GOG_RUNNER_URL to run against a remote gog runner, add
|
|
106
|
+
# that host here too — it is supplied at runtime and cannot be declared.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gogcli-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.26.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/gogcli-mcp",
|
|
5
5
|
"description": "MCP server wrapping gogcli for Google service access",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -41,15 +41,15 @@
|
|
|
41
41
|
"test:coverage": "vitest run --coverage"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@chrischall/mcp-utils": "^0.
|
|
44
|
+
"@chrischall/mcp-utils": "^0.15.0",
|
|
45
45
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
46
46
|
"zod": "^4.4.3"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@types/node": "^26.
|
|
49
|
+
"@types/node": "^26.2.0",
|
|
50
50
|
"@vitest/coverage-v8": "^4.1.8",
|
|
51
|
-
"esbuild": "^0.28.
|
|
51
|
+
"esbuild": "^0.28.2",
|
|
52
52
|
"typescript": "^7.0.2",
|
|
53
|
-
"vitest": "^4.1.
|
|
53
|
+
"vitest": "^4.1.11"
|
|
54
54
|
}
|
|
55
55
|
}
|
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"source": "github",
|
|
8
8
|
"subfolder": "packages/gogcli-mcp"
|
|
9
9
|
},
|
|
10
|
-
"version": "2.
|
|
10
|
+
"version": "2.26.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "gogcli-mcp",
|
|
15
|
-
"version": "2.
|
|
15
|
+
"version": "2.26.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|
package/src/gmail-results.ts
CHANGED
|
@@ -177,6 +177,16 @@ export async function finalizeGmailSearch(
|
|
|
177
177
|
// The merged payload keeps a nextPageToken only if pages remain when the cap is
|
|
178
178
|
// hit, so finalizeGmailSearch still marks it truncated — running out of budget
|
|
179
179
|
// is not the same as reaching the end, and must not read like it.
|
|
180
|
+
//
|
|
181
|
+
// A REPEATED CURSOR ENDS THE WALK TOO. Google can hand back a nextPageToken it
|
|
182
|
+
// has already issued, and gog 0.38.0 fixed exactly that for its own `--all`
|
|
183
|
+
// (openclaw/gogcli#1004) — but this walk is not gog's: it makes N separate
|
|
184
|
+
// single-page calls, so a repeated cursor arrives here untouched. Following it
|
|
185
|
+
// re-fetches a page already merged, so the caller gets the same threads twice
|
|
186
|
+
// over, presented as more results and looking perfectly well-formed. The token
|
|
187
|
+
// is KEPT when that happens, for the same reason the cap keeps it: the walk
|
|
188
|
+
// never established an end, and a set that reads complete is precisely how mail
|
|
189
|
+
// that exists gets reported as missing.
|
|
180
190
|
export async function fetchGmailPages(
|
|
181
191
|
runPage: (token: string | undefined) => Promise<CallToolResult>,
|
|
182
192
|
itemsKey: 'threads' | 'messages',
|
|
@@ -186,6 +196,10 @@ export async function fetchGmailPages(
|
|
|
186
196
|
const merged: unknown[] = [];
|
|
187
197
|
let base: Record<string, unknown> | undefined;
|
|
188
198
|
let token = startToken;
|
|
199
|
+
// Every cursor this walk has already fetched with, so one Google repeats is
|
|
200
|
+
// recognised rather than followed. Seeded with the caller's own cursor: a
|
|
201
|
+
// response echoing that back would re-fetch the page just merged.
|
|
202
|
+
const fetched = new Set<string>(startToken === undefined ? [] : [startToken]);
|
|
189
203
|
|
|
190
204
|
for (let pages = 0; pages < maxPages; pages++) {
|
|
191
205
|
const result = await runPage(token);
|
|
@@ -199,10 +213,23 @@ export async function fetchGmailPages(
|
|
|
199
213
|
}
|
|
200
214
|
base = parsed;
|
|
201
215
|
merged.push(...(parsed[itemsKey] as unknown[]));
|
|
202
|
-
|
|
216
|
+
const next = typeof parsed.nextPageToken === 'string' && parsed.nextPageToken !== ''
|
|
203
217
|
? parsed.nextPageToken
|
|
204
218
|
: undefined;
|
|
205
|
-
if (
|
|
219
|
+
if (next === undefined) {
|
|
220
|
+
token = undefined;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
// Checked BEFORE the next fetch, so the duplicate page is never requested
|
|
224
|
+
// and never merged. `token` keeps the repeated cursor so the result still
|
|
225
|
+
// reads as truncated — it is the honest answer, and handing it back lets a
|
|
226
|
+
// caller retry later rather than concluding there is nothing more.
|
|
227
|
+
if (fetched.has(next)) {
|
|
228
|
+
token = next;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
fetched.add(next);
|
|
232
|
+
token = next;
|
|
206
233
|
}
|
|
207
234
|
|
|
208
235
|
return finish(base as Record<string, unknown>, itemsKey, merged, token);
|
package/src/lib.ts
CHANGED
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.
|
|
220
|
+
export const MIN_GOG_VERSION = '0.38.1';
|
|
221
221
|
|
|
222
222
|
// Interpret the GOG_READONLY kill-switch. `readEnvVar` already treats blank
|
|
223
223
|
// values, 'undefined'/'null' sentinels, and unresolved .mcpb placeholders
|
package/src/server.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ToolRegistrar } from '@chrischall/mcp-utils';
|
|
2
2
|
import { registerApiTools } from './tools/api.js';
|
|
3
|
+
import { registerAppScriptTools } from './tools/appscript.js';
|
|
3
4
|
import { registerAuthTools, authToolsFor } from './tools/auth.js';
|
|
4
5
|
import { registerCalendarTools } from './tools/calendar.js';
|
|
6
|
+
import { registerChatTools } from './tools/chat.js';
|
|
5
7
|
import { registerClassroomTools } from './tools/classroom.js';
|
|
6
8
|
import { registerContactsTools } from './tools/contacts.js';
|
|
7
9
|
import { registerDocsTools } from './tools/docs.js';
|
|
@@ -23,8 +25,10 @@ export const VERSION = typeof GOGCLI_VERSION !== 'undefined' ? GOGCLI_VERSION :
|
|
|
23
25
|
// re-exported below.
|
|
24
26
|
export const BASE_TOOL_REGISTRARS: ToolRegistrar[] = [
|
|
25
27
|
registerApiTools,
|
|
28
|
+
registerAppScriptTools,
|
|
26
29
|
registerAuthTools,
|
|
27
30
|
registerCalendarTools,
|
|
31
|
+
registerChatTools,
|
|
28
32
|
registerClassroomTools,
|
|
29
33
|
registerContactsTools,
|
|
30
34
|
registerDocsTools,
|
|
@@ -37,9 +41,11 @@ export const BASE_TOOL_REGISTRARS: ToolRegistrar[] = [
|
|
|
37
41
|
|
|
38
42
|
export {
|
|
39
43
|
registerApiTools,
|
|
44
|
+
registerAppScriptTools,
|
|
40
45
|
registerAuthTools,
|
|
41
46
|
authToolsFor,
|
|
42
47
|
registerCalendarTools,
|
|
48
|
+
registerChatTools,
|
|
43
49
|
registerClassroomTools,
|
|
44
50
|
registerContactsTools,
|
|
45
51
|
registerDocsTools,
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import {
|
|
4
|
+
accountParam,
|
|
5
|
+
runOrDiagnose,
|
|
6
|
+
registerRunTool,
|
|
7
|
+
paginationParams,
|
|
8
|
+
pushPaginationFlags,
|
|
9
|
+
} from './utils.js';
|
|
10
|
+
|
|
11
|
+
// Google Apps Script (gog >= 0.38.0 for pull/deployments/versions).
|
|
12
|
+
//
|
|
13
|
+
// This wraps the READ and RUN halves of the Apps Script API. gog has no push,
|
|
14
|
+
// so nothing here can change a project's code: `create` makes an empty project
|
|
15
|
+
// and `pull`/`content` only copy code outwards. The one tool with real reach is
|
|
16
|
+
// gog_appscript_run_function, which executes somebody's script under this
|
|
17
|
+
// account's authority — see its description.
|
|
18
|
+
//
|
|
19
|
+
// The Apps Script API is OFF by default on a Google Cloud project, so the first
|
|
20
|
+
// call on a fresh OAuth client fails with "Apps Script API is not enabled for
|
|
21
|
+
// this OAuth project" and a console URL. That error is gog's, it names the
|
|
22
|
+
// exact project, and it is not a scope failure — do not answer it with a
|
|
23
|
+
// re-auth.
|
|
24
|
+
export function registerAppScriptTools(server: McpServer): void {
|
|
25
|
+
const scriptIdParam = z.string().describe(
|
|
26
|
+
'Apps Script project ID — the long ID in script.google.com/…/projects/<scriptId>/…, NOT the Drive file ID of a '
|
|
27
|
+
+ 'container document',
|
|
28
|
+
);
|
|
29
|
+
const apiEnableNote =
|
|
30
|
+
' Needs the Apps Script API enabled on the OAuth client\'s Google Cloud project; if it is not, gog says so and prints '
|
|
31
|
+
+ 'the console URL to enable it. That is a project setting, not a missing scope — re-authorizing will not fix it.';
|
|
32
|
+
|
|
33
|
+
server.registerTool('gog_appscript_get', {
|
|
34
|
+
description:
|
|
35
|
+
'Get an Apps Script project\'s metadata: title, creator, create/update times, and the parent Drive file when the '
|
|
36
|
+
+ 'project is bound to a Sheet, Doc or Form. Use gog_appscript_content to read the actual code.' + apiEnableNote,
|
|
37
|
+
annotations: { readOnlyHint: true },
|
|
38
|
+
inputSchema: {
|
|
39
|
+
scriptId: scriptIdParam,
|
|
40
|
+
account: accountParam,
|
|
41
|
+
},
|
|
42
|
+
}, async ({ scriptId, account }) => {
|
|
43
|
+
return runOrDiagnose(['appscript', 'get', scriptId], { account });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
server.registerTool('gog_appscript_content', {
|
|
47
|
+
description:
|
|
48
|
+
'Read a project\'s source — every .gs file and its appsscript.json manifest — INLINE in the response. This is the '
|
|
49
|
+
+ 'tool to reach for when the question is "what does this script do"; it needs no filesystem, so it works the same '
|
|
50
|
+
+ 'on a hosted deployment as it does locally, unlike gog_appscript_pull.' + apiEnableNote,
|
|
51
|
+
annotations: { readOnlyHint: true },
|
|
52
|
+
inputSchema: {
|
|
53
|
+
scriptId: scriptIdParam,
|
|
54
|
+
account: accountParam,
|
|
55
|
+
},
|
|
56
|
+
}, async ({ scriptId, account }) => {
|
|
57
|
+
return runOrDiagnose(['appscript', 'content', scriptId], { account });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
server.registerTool('gog_appscript_pull', {
|
|
61
|
+
description:
|
|
62
|
+
'Write a project\'s files into a local directory, for editing a script as ordinary files. '
|
|
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 '
|
|
65
|
+
+ 'reach them. Use gog_appscript_content there instead — it returns the same source in the response. Existing files '
|
|
66
|
+
+ 'are left alone unless overwrite is set. Read-only as far as Google is concerned: nothing is pushed back.'
|
|
67
|
+
+ apiEnableNote,
|
|
68
|
+
inputSchema: {
|
|
69
|
+
scriptId: scriptIdParam,
|
|
70
|
+
dir: z.string().describe('Destination directory, resolved on the machine where gog runs'),
|
|
71
|
+
overwrite: z.boolean().optional().describe('Overwrite files that already exist in dir'),
|
|
72
|
+
account: accountParam,
|
|
73
|
+
},
|
|
74
|
+
}, async ({ scriptId, dir, overwrite, account }) => {
|
|
75
|
+
const args = ['appscript', 'pull', scriptId, dir];
|
|
76
|
+
if (overwrite) args.push('--overwrite');
|
|
77
|
+
return runOrDiagnose(args, { account });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
server.registerTool('gog_appscript_create', {
|
|
81
|
+
description:
|
|
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
|
+
+ 'what makes the script a container-bound script with access to that document; omit it for a standalone project. '
|
|
84
|
+
+ 'gog cannot upload code, so the project starts empty either way.' + apiEnableNote,
|
|
85
|
+
inputSchema: {
|
|
86
|
+
title: z.string().describe('Project title'),
|
|
87
|
+
parentId: z.string().optional().describe('Drive file ID to bind the project to (Sheet, Doc or Form). Omit for a standalone project.'),
|
|
88
|
+
account: accountParam,
|
|
89
|
+
},
|
|
90
|
+
}, async ({ title, parentId, account }) => {
|
|
91
|
+
const args = ['appscript', 'create', `--title=${title}`];
|
|
92
|
+
if (parentId) args.push(`--parent-id=${parentId}`);
|
|
93
|
+
return runOrDiagnose(args, { account });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
server.registerTool('gog_appscript_deployments', {
|
|
97
|
+
description:
|
|
98
|
+
'List a project\'s deployments — the published web apps, add-ons and API executables, each pinned to a version. A '
|
|
99
|
+
+ 'deployment ID from here is what gog_appscript_run_function needs when a script is not running in dev mode.'
|
|
100
|
+
+ apiEnableNote,
|
|
101
|
+
annotations: { readOnlyHint: true },
|
|
102
|
+
inputSchema: {
|
|
103
|
+
scriptId: scriptIdParam,
|
|
104
|
+
...paginationParams,
|
|
105
|
+
account: accountParam,
|
|
106
|
+
},
|
|
107
|
+
}, async ({ scriptId, max, pageToken, page, all, account }) => {
|
|
108
|
+
const args = ['appscript', 'deployments', scriptId];
|
|
109
|
+
pushPaginationFlags(args, { max, pageToken, page, all });
|
|
110
|
+
return runOrDiagnose(args, { account });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
server.registerTool('gog_appscript_versions', {
|
|
114
|
+
description:
|
|
115
|
+
'List a project\'s saved versions — the immutable snapshots deployments point at, with their numbers and '
|
|
116
|
+
+ 'descriptions. Useful for answering "what is actually deployed" next to gog_appscript_deployments.' + apiEnableNote,
|
|
117
|
+
annotations: { readOnlyHint: true },
|
|
118
|
+
inputSchema: {
|
|
119
|
+
scriptId: scriptIdParam,
|
|
120
|
+
...paginationParams,
|
|
121
|
+
account: accountParam,
|
|
122
|
+
},
|
|
123
|
+
}, async ({ scriptId, max, pageToken, page, all, account }) => {
|
|
124
|
+
const args = ['appscript', 'versions', scriptId];
|
|
125
|
+
pushPaginationFlags(args, { max, pageToken, page, all });
|
|
126
|
+
return runOrDiagnose(args, { account });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
server.registerTool('gog_appscript_run_function', {
|
|
130
|
+
description:
|
|
131
|
+
'Execute a function in a deployed Apps Script project. TREAT THIS AS ARBITRARY CODE EXECUTION: the script runs with '
|
|
132
|
+
+ 'this Google account\'s authority and can send mail, edit Drive files or call external services, and the wrapper '
|
|
133
|
+
+ 'cannot tell a read from a write — read the code with gog_appscript_content first if you did not write it. '
|
|
134
|
+
+ 'Requires the project to be deployed as an API executable and to share the OAuth client with the calling '
|
|
135
|
+
+ 'credentials, otherwise Google refuses regardless of scopes. devMode runs the latest saved code instead of the '
|
|
136
|
+
+ 'deployed version, and only works if the account owns the script. '
|
|
137
|
+
+ 'This is NOT the escape hatch — gog_appscript_run is that.' + apiEnableNote,
|
|
138
|
+
annotations: { destructiveHint: true },
|
|
139
|
+
inputSchema: {
|
|
140
|
+
scriptId: scriptIdParam,
|
|
141
|
+
functionName: z.string().describe('Name of the function to call, e.g. "doWork"'),
|
|
142
|
+
params: z.string().optional().describe('Function parameters as a JSON ARRAY of positional arguments, e.g. \'["a", 1]\' — not an object'),
|
|
143
|
+
devMode: z.boolean().optional().describe('Run the latest saved code rather than the deployed version (owner only)'),
|
|
144
|
+
account: accountParam,
|
|
145
|
+
},
|
|
146
|
+
}, async ({ scriptId, functionName, params, devMode, account }) => {
|
|
147
|
+
// gog passes --params through to the API as-is, so a malformed value comes
|
|
148
|
+
// back as a Google error about the request body rather than about the
|
|
149
|
+
// argument the caller actually got wrong. Checking the shape here is what
|
|
150
|
+
// turns "invalid argument" into "params must be a JSON array".
|
|
151
|
+
if (params !== undefined) {
|
|
152
|
+
let parsed: unknown;
|
|
153
|
+
try {
|
|
154
|
+
parsed = JSON.parse(params);
|
|
155
|
+
} catch {
|
|
156
|
+
throw new Error(`params must be a JSON array of positional arguments, e.g. '["a", 1]'. Received: ${params}`);
|
|
157
|
+
}
|
|
158
|
+
if (!Array.isArray(parsed)) {
|
|
159
|
+
throw new Error(`params must be a JSON ARRAY of positional arguments, e.g. '["a", 1]' — Apps Script takes positional arguments, not named ones. Received: ${params}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const args = ['appscript', 'run', scriptId, functionName];
|
|
163
|
+
if (params !== undefined) args.push(`--params=${params}`);
|
|
164
|
+
if (devMode) args.push('--dev-mode');
|
|
165
|
+
return runOrDiagnose(args, { account });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
registerRunTool(server, {
|
|
169
|
+
service: 'appscript',
|
|
170
|
+
examples: '"get", "content", "deployments"',
|
|
171
|
+
note: 'To execute a function, use gog_appscript_run_function — this tool is the generic escape hatch.',
|
|
172
|
+
});
|
|
173
|
+
}
|
package/src/tools/calendar.ts
CHANGED
|
@@ -3,6 +3,56 @@ import { z } from 'zod';
|
|
|
3
3
|
import { accountParam, runOrDiagnose, registerRunTool, pageTokenParam, pageAliasParam, resolvePageToken } from './utils.js';
|
|
4
4
|
import { annotateTruncatedList } from '../pagination.js';
|
|
5
5
|
|
|
6
|
+
// Reminder params, shared by create and update (gog >= 0.38.0 for
|
|
7
|
+
// --no-reminders). An event's reminders are one of THREE states, and the two
|
|
8
|
+
// params below have to spell all three because gog does:
|
|
9
|
+
//
|
|
10
|
+
// reminders: ['popup:30m'] → --reminder=popup:30m custom overrides
|
|
11
|
+
// noReminders: true → --no-reminders no reminder at all
|
|
12
|
+
// reminders: [] → --reminder= back to the calendar's defaults
|
|
13
|
+
//
|
|
14
|
+
// That last one is the subtle one and it only means anything on update: gog
|
|
15
|
+
// reads an EMPTY --reminder as "clear the overrides and use the calendar
|
|
16
|
+
// default" (openclaw/gogcli#1016), which is a different outcome from omitting
|
|
17
|
+
// the flag (leave whatever the event already has) and from --no-reminders
|
|
18
|
+
// (override the calendar with silence). An empty array is how a JSON caller
|
|
19
|
+
// says it, since there is no way to send a bare flag with no value.
|
|
20
|
+
const reminderParams = {
|
|
21
|
+
reminders: z.array(z.string()).max(5).optional().describe(
|
|
22
|
+
'Reminders as method:duration, e.g. ["popup:30m", "email:1d"]. Method is popup or email; duration accepts m/h/d '
|
|
23
|
+
+ '(max 40320 minutes = 4 weeks). Google allows at most 5. These REPLACE the event\'s reminders — on update, pass an '
|
|
24
|
+
+ 'EMPTY array to drop custom reminders and go back to the calendar\'s defaults. Cannot be combined with noReminders.',
|
|
25
|
+
),
|
|
26
|
+
noReminders: z.boolean().optional().describe(
|
|
27
|
+
'Give the event no reminders at all, overriding the calendar\'s defaults. Different from an empty reminders array, '
|
|
28
|
+
+ 'which RESTORES those defaults. Cannot be combined with reminders.',
|
|
29
|
+
),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// The one place the three states become argv. Kept together so create and
|
|
33
|
+
// update cannot drift apart on the empty-array case.
|
|
34
|
+
function pushReminderFlags(
|
|
35
|
+
args: string[],
|
|
36
|
+
p: { reminders?: string[]; noReminders?: boolean },
|
|
37
|
+
): void {
|
|
38
|
+
if (p.noReminders) {
|
|
39
|
+
// gog's own flags are `xor:"reminders"`, so it would reject this too — but
|
|
40
|
+
// only after a spawn, and with kong's wording rather than the tool's.
|
|
41
|
+
if (p.reminders !== undefined) {
|
|
42
|
+
throw new Error('reminders and noReminders are mutually exclusive: pass reminders to set custom ones, noReminders for none, or an empty reminders array to restore the calendar defaults.');
|
|
43
|
+
}
|
|
44
|
+
args.push('--no-reminders');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (p.reminders === undefined) return;
|
|
48
|
+
// Empty array → one empty --reminder, which is gog's "restore defaults".
|
|
49
|
+
if (p.reminders.length === 0) {
|
|
50
|
+
args.push('--reminder=');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
for (const reminder of p.reminders) args.push(`--reminder=${reminder}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
6
56
|
export function registerCalendarTools(server: McpServer): void {
|
|
7
57
|
server.registerTool('gog_calendar_events', {
|
|
8
58
|
description: 'List calendar events. Describe the window ONE way and one way only (gog >= 0.36.0 rejects the rest as ambiguous rather than silently discarding a flag): '
|
|
@@ -80,9 +130,10 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
80
130
|
allDay: z.boolean().optional().describe('All-day event (use date-only in from/to)'),
|
|
81
131
|
timezone: z.string().optional().describe('IANA timezone metadata applied to from/to (e.g. America/New_York). Sets both start and end timezone unless start/end timezone are overridden.'),
|
|
82
132
|
withZoom: z.boolean().optional().describe('Create a Zoom video conference for this event (requires Zoom S2S OAuth setup)'),
|
|
133
|
+
...reminderParams,
|
|
83
134
|
account: accountParam,
|
|
84
135
|
},
|
|
85
|
-
}, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, account }) => {
|
|
136
|
+
}, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, reminders, noReminders, account }) => {
|
|
86
137
|
const args = ['calendar', 'create', calendarId, `--summary=${summary}`, `--from=${from}`, `--to=${to}`];
|
|
87
138
|
if (description) args.push(`--description=${description}`);
|
|
88
139
|
if (location) args.push(`--location=${location}`);
|
|
@@ -90,6 +141,7 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
90
141
|
if (allDay) args.push('--all-day');
|
|
91
142
|
if (timezone) args.push(`--timezone=${timezone}`);
|
|
92
143
|
if (withZoom) args.push('--with-zoom');
|
|
144
|
+
pushReminderFlags(args, { reminders, noReminders });
|
|
93
145
|
return runOrDiagnose(args, { account });
|
|
94
146
|
});
|
|
95
147
|
|
|
@@ -111,9 +163,10 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
111
163
|
regenerateZoom: z.boolean().optional().describe('Replace the event\'s existing Zoom video conference'),
|
|
112
164
|
removeZoom: z.boolean().optional().describe('Remove the event\'s Zoom video conference'),
|
|
113
165
|
removeMeet: z.boolean().optional().describe('Remove the event\'s Google Meet video conference (clears conference data only)'),
|
|
166
|
+
...reminderParams,
|
|
114
167
|
account: accountParam,
|
|
115
168
|
},
|
|
116
|
-
}, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, account }) => {
|
|
169
|
+
}, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, reminders, noReminders, account }) => {
|
|
117
170
|
const args = ['calendar', 'update', calendarId, eventId];
|
|
118
171
|
if (summary !== undefined) args.push(`--summary=${summary}`);
|
|
119
172
|
if (from !== undefined) args.push(`--from=${from}`);
|
|
@@ -127,6 +180,7 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
127
180
|
if (regenerateZoom) args.push('--regenerate-zoom');
|
|
128
181
|
if (removeZoom) args.push('--remove-zoom');
|
|
129
182
|
if (removeMeet) args.push('--remove-meet');
|
|
183
|
+
pushReminderFlags(args, { reminders, noReminders });
|
|
130
184
|
return runOrDiagnose(args, { account });
|
|
131
185
|
});
|
|
132
186
|
|