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