mcp-google-multi 6.0.0-alpha.76 → 6.0.0-alpha.78
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/README.md +14 -0
- package/dist/fs-atomic.js +98 -12
- package/package.json +4 -2
- package/skills/google-workspace/SKILL.md +162 -0
- package/skills/google-workspace/reference/admin.md +105 -0
- package/skills/google-workspace/reference/analytics.md +70 -0
- package/skills/google-workspace/reference/appsmarket.md +5 -0
- package/skills/google-workspace/reference/calendar.md +175 -0
- package/skills/google-workspace/reference/chat.md +59 -0
- package/skills/google-workspace/reference/classroom.md +5 -0
- package/skills/google-workspace/reference/cloudidentity.md +5 -0
- package/skills/google-workspace/reference/cloudsearch.md +5 -0
- package/skills/google-workspace/reference/contacts.md +138 -0
- package/skills/google-workspace/reference/docs.md +395 -0
- package/skills/google-workspace/reference/drive.md +485 -0
- package/skills/google-workspace/reference/driveactivity.md +5 -0
- package/skills/google-workspace/reference/drivelabels.md +5 -0
- package/skills/google-workspace/reference/forms.md +92 -0
- package/skills/google-workspace/reference/gmail.md +305 -0
- package/skills/google-workspace/reference/groupsmigration.md +5 -0
- package/skills/google-workspace/reference/groupssettings.md +5 -0
- package/skills/google-workspace/reference/keep.md +5 -0
- package/skills/google-workspace/reference/licensing.md +5 -0
- package/skills/google-workspace/reference/meet.md +68 -0
- package/skills/google-workspace/reference/postmaster.md +5 -0
- package/skills/google-workspace/reference/reseller.md +5 -0
- package/skills/google-workspace/reference/script.md +5 -0
- package/skills/google-workspace/reference/searchconsole.md +128 -0
- package/skills/google-workspace/reference/sheets.md +430 -0
- package/skills/google-workspace/reference/slides.md +67 -0
- package/skills/google-workspace/reference/tasks.md +167 -0
- package/skills/google-workspace/reference/vault.md +5 -0
- package/skills/google-workspace/reference/workspaceevents.md +5 -0
package/README.md
CHANGED
|
@@ -55,6 +55,20 @@ New to all this? It's written for someone who just installed Claude Code and has
|
|
|
55
55
|
|
|
56
56
|
**Go deeper:** [Configuration reference](./docs/configuration.md) · [What's covered](./COVERAGE.md) · [Features tour](./docs/features.md) · [Remote / HTTP setup](./docs/http-setup.md) · [Secrets in a vault](./docs/secrets.md) · [Local usage metrics](./docs/usage-metrics.md) · [Migrating to v6](./MIGRATION-v6.md) · [Security policy](./SECURITY.md) · [Roadmap](https://github.com/bakissation/mcp-google-multi/milestones)
|
|
57
57
|
|
|
58
|
+
## Agent skill
|
|
59
|
+
|
|
60
|
+
The repo and the npm package both ship an [Agent Skill](https://code.claude.com/docs/en/skills) at `skills/google-workspace/`: a hand-written `SKILL.md` covering account selection, the discover step, incremental Gmail sweeps, keeping results small, write profiles, and the escape hatch, plus a `reference/` folder with a generated argument table per service. Loading it gets an agent calling this server's tools correctly without guessing argument names.
|
|
61
|
+
|
|
62
|
+
To install it for Claude Code, copy or symlink the `skills/google-workspace` folder into `~/.claude/skills/` (every project) or a project's own `.claude/skills/` (that project only):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# from a cloned copy of this repo
|
|
66
|
+
ln -s "$(pwd)/skills/google-workspace" ~/.claude/skills/google-workspace
|
|
67
|
+
|
|
68
|
+
# from the installed npm package
|
|
69
|
+
ln -s "$(npm root -g)/mcp-google-multi/skills/google-workspace" ~/.claude/skills/google-workspace
|
|
70
|
+
```
|
|
71
|
+
|
|
58
72
|
## Maintainer, sponsor & license
|
|
59
73
|
|
|
60
74
|
Built and maintained by **Abdelbaki Berkati** — [berkati.xyz](https://berkati.xyz) · [@bakissation](https://github.com/bakissation). [Read the case study →](https://berkati.xyz/case-studies/mcp-google-multi/)
|
package/dist/fs-atomic.js
CHANGED
|
@@ -10,6 +10,9 @@ const RENAME_RETRY_MS = 20;
|
|
|
10
10
|
const LOCK_LEASE_MS = 60_000;
|
|
11
11
|
// Breaking a lock takes microseconds, so an older breaker file was left by a crash.
|
|
12
12
|
const BREAKER_STALE_MS = 2_000;
|
|
13
|
+
// The no-hard-link fallback writes the lock in two syscalls (create, then the
|
|
14
|
+
// pid); a reader can catch it empty in between. See docs/internals.md.
|
|
15
|
+
const LOCK_WRITE_GRACE_MS = 2_000;
|
|
13
16
|
const PROCESS_START_MS = performance.timeOrigin;
|
|
14
17
|
const OWNER_NAME = /^(\d+)\.(\d+|host)\.(\d+)\.([0-9a-f]{8}|0)\.[0-9a-f]{12}\.owner$/;
|
|
15
18
|
function startOf(pid) {
|
|
@@ -63,11 +66,19 @@ function sameFile(a, b) {
|
|
|
63
66
|
function isEnoent(error) {
|
|
64
67
|
return error.code === 'ENOENT';
|
|
65
68
|
}
|
|
69
|
+
function bindingRecord(id) {
|
|
70
|
+
return `${id.dev}:${id.ino}:${id.mtimeNs}`;
|
|
71
|
+
}
|
|
72
|
+
function matchesBinding(content, id) {
|
|
73
|
+
const m = /^(\d+):(\d+):(\d+)$/.exec(content);
|
|
74
|
+
return m !== null && BigInt(m[1]) === id.dev && BigInt(m[2]) === id.ino && BigInt(m[3]) === id.mtimeNs;
|
|
75
|
+
}
|
|
66
76
|
// Undefined when the lock changed hands while it was being read.
|
|
67
77
|
function readHolder(lock) {
|
|
68
78
|
const before = fs.lstatSync(lock, { bigint: true });
|
|
69
79
|
const body = before.isFile() ? fs.readFileSync(lock, 'utf8') : '';
|
|
70
80
|
let owner;
|
|
81
|
+
let ownerPending = false;
|
|
71
82
|
if (before.isFile()) {
|
|
72
83
|
const dir = path.dirname(lock);
|
|
73
84
|
const prefix = `${path.basename(lock)}.`;
|
|
@@ -79,10 +90,12 @@ function readHolder(lock) {
|
|
|
79
90
|
continue;
|
|
80
91
|
const ownerPath = path.join(dir, name);
|
|
81
92
|
try {
|
|
82
|
-
|
|
93
|
+
const ownerStat = fs.lstatSync(ownerPath, { bigint: true });
|
|
94
|
+
if (sameFile(ownerStat, before) || matchesBinding(fs.readFileSync(ownerPath, 'utf8'), before)) {
|
|
83
95
|
owner = { path: ownerPath, ns: m[2], start: m[3], boot: m[4] };
|
|
84
96
|
break;
|
|
85
97
|
}
|
|
98
|
+
ownerPending = true;
|
|
86
99
|
}
|
|
87
100
|
catch (error) {
|
|
88
101
|
if (!isEnoent(error))
|
|
@@ -93,7 +106,7 @@ function readHolder(lock) {
|
|
|
93
106
|
const after = fs.lstatSync(lock, { bigint: true });
|
|
94
107
|
if (!sameFile(before, after))
|
|
95
108
|
return undefined;
|
|
96
|
-
return { body, ino: after.ino, dev: after.dev, mtimeNs: after.mtimeNs, mtimeMs: Number(after.mtimeNs) / 1e6, owner };
|
|
109
|
+
return { body, ino: after.ino, dev: after.dev, mtimeNs: after.mtimeNs, mtimeMs: Number(after.mtimeNs) / 1e6, owner, ownerPending };
|
|
97
110
|
}
|
|
98
111
|
// The owner file's random name tells two holders apart even when the second
|
|
99
112
|
// lock reuses the first one's freed inode.
|
|
@@ -115,10 +128,21 @@ function probeDead(pid) {
|
|
|
115
128
|
return false;
|
|
116
129
|
}
|
|
117
130
|
}
|
|
131
|
+
// Symmetric: a clock step back must not make a write-in-progress lock look
|
|
132
|
+
// permanently fresh (see docs/internals.md).
|
|
133
|
+
function withinWriteGrace(now, mtimeMs) {
|
|
134
|
+
return Math.abs(now - mtimeMs) < LOCK_WRITE_GRACE_MS;
|
|
135
|
+
}
|
|
118
136
|
function holderDead(holder, now) {
|
|
137
|
+
if (holder.body === '' && withinWriteGrace(now, holder.mtimeMs))
|
|
138
|
+
return false;
|
|
119
139
|
const pid = Number(holder.body);
|
|
120
140
|
if (!(Number.isSafeInteger(pid) && pid > 0))
|
|
121
141
|
return true;
|
|
142
|
+
// A same-pid owner file exists but is not yet bound (the fallback lock's two
|
|
143
|
+
// writes race a reader); give it the same grace as an empty lock.
|
|
144
|
+
if (!holder.owner && holder.ownerPending && withinWriteGrace(now, holder.mtimeMs))
|
|
145
|
+
return false;
|
|
122
146
|
const leaseOver = now - holder.mtimeMs >= LOCK_LEASE_MS;
|
|
123
147
|
const owner = holder.owner;
|
|
124
148
|
// No owner file: an older writer, whose namespace is unknown.
|
|
@@ -196,6 +220,53 @@ function releaseBreaker(breaker, token) {
|
|
|
196
220
|
throw error;
|
|
197
221
|
}
|
|
198
222
|
}
|
|
223
|
+
// No hard links here (Android/Termux denies link() in private storage, some
|
|
224
|
+
// filesystems lack it entirely): the lock is created directly with O_EXCL,
|
|
225
|
+
// carrying the bare pid a linked owner file would carry, and the owner file
|
|
226
|
+
// is bound to it by a recorded identity instead of a hard link. See docs/internals.md.
|
|
227
|
+
function takeFallbackLock(lock, ownerFile) {
|
|
228
|
+
const fd = fs.openSync(lock, 'wx', 0o600);
|
|
229
|
+
let acquired;
|
|
230
|
+
try {
|
|
231
|
+
fs.writeSync(fd, String(process.pid));
|
|
232
|
+
const stat = fs.fstatSync(fd, { bigint: true });
|
|
233
|
+
acquired = { dev: stat.dev, ino: stat.ino, mtimeNs: stat.mtimeNs };
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
// The lock is left empty; remove it only if it is still the one we just
|
|
237
|
+
// created, then rethrow so the caller never retries a write failure.
|
|
238
|
+
const stat = fs.fstatSync(fd, { bigint: true });
|
|
239
|
+
fs.closeSync(fd);
|
|
240
|
+
try {
|
|
241
|
+
if (sameFile(fs.lstatSync(lock, { bigint: true }), stat))
|
|
242
|
+
fs.rmSync(lock, { force: true });
|
|
243
|
+
}
|
|
244
|
+
catch (cleanupError) {
|
|
245
|
+
if (!isEnoent(cleanupError))
|
|
246
|
+
throw cleanupError;
|
|
247
|
+
}
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
fs.closeSync(fd);
|
|
251
|
+
try {
|
|
252
|
+
fs.writeFileSync(ownerFile, bindingRecord(acquired));
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
// The lock already exists holding our live pid; without this cleanup
|
|
256
|
+
// every other writer sees an ownerless holder and waits out the full lease.
|
|
257
|
+
try {
|
|
258
|
+
const current = fs.lstatSync(lock, { bigint: true });
|
|
259
|
+
if (sameFile(current, acquired) && current.mtimeNs === acquired.mtimeNs)
|
|
260
|
+
fs.rmSync(lock, { force: true });
|
|
261
|
+
}
|
|
262
|
+
catch (cleanupError) {
|
|
263
|
+
if (!isEnoent(cleanupError))
|
|
264
|
+
throw cleanupError;
|
|
265
|
+
}
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
return acquired;
|
|
269
|
+
}
|
|
199
270
|
// Hardlink-based advisory lock; the lock body is the bare pid, as older
|
|
200
271
|
// readers expect. See docs/internals.md.
|
|
201
272
|
export function withFileLock(absPath, fn, label = `lock: ${absPath}`) {
|
|
@@ -206,17 +277,31 @@ export function withFileLock(absPath, fn, label = `lock: ${absPath}`) {
|
|
|
206
277
|
fs.writeFileSync(ownerFile, String(process.pid), { mode: 0o600, flag: 'wx' });
|
|
207
278
|
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
208
279
|
let linked = false;
|
|
280
|
+
let acquired;
|
|
209
281
|
try {
|
|
210
282
|
while (true) {
|
|
211
283
|
try {
|
|
212
284
|
fs.linkSync(ownerFile, lock);
|
|
213
285
|
linked = true;
|
|
286
|
+
acquired = fs.lstatSync(lock, { bigint: true });
|
|
214
287
|
break;
|
|
215
288
|
}
|
|
216
289
|
catch (error) {
|
|
217
290
|
const err = error;
|
|
218
|
-
if (err.code
|
|
291
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
292
|
+
try {
|
|
293
|
+
acquired = takeFallbackLock(lock, ownerFile);
|
|
294
|
+
linked = true;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
catch (fbError) {
|
|
298
|
+
if (fbError.code !== 'EEXIST')
|
|
299
|
+
throw fbError;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
else if (err.code !== 'EEXIST') {
|
|
219
303
|
throw error;
|
|
304
|
+
}
|
|
220
305
|
try {
|
|
221
306
|
const holder = readHolder(lock);
|
|
222
307
|
if (holder && holderDead(holder, Date.now()) && breakLock(lock, holder))
|
|
@@ -238,23 +323,24 @@ export function withFileLock(absPath, fn, label = `lock: ${absPath}`) {
|
|
|
238
323
|
if (!linked)
|
|
239
324
|
fs.rmSync(ownerFile, { force: true });
|
|
240
325
|
}
|
|
241
|
-
let bound = false;
|
|
242
326
|
try {
|
|
243
|
-
// Some network and FUSE filesystems give each name of one file its own
|
|
244
|
-
// inode number; there the owner file cannot vouch for the lock.
|
|
245
|
-
bound = sameFile(fs.lstatSync(ownerFile, { bigint: true }), fs.lstatSync(lock, { bigint: true }));
|
|
246
327
|
return fn();
|
|
247
328
|
}
|
|
248
329
|
finally {
|
|
249
|
-
releaseLock(lock, ownerFile,
|
|
330
|
+
releaseLock(lock, ownerFile, acquired);
|
|
250
331
|
}
|
|
251
332
|
}
|
|
252
333
|
// A holder whose lock was broken (its owner file removed with it) must not
|
|
253
|
-
// remove its successor's lock
|
|
254
|
-
|
|
334
|
+
// remove its successor's lock: only drop the lock if it is still the exact
|
|
335
|
+
// file this call created, and our own owner file is still there to vouch for it.
|
|
336
|
+
function releaseLock(lock, ownerFile, acquired) {
|
|
255
337
|
try {
|
|
256
|
-
if (
|
|
257
|
-
fs.
|
|
338
|
+
if (acquired) {
|
|
339
|
+
const current = fs.lstatSync(lock, { bigint: true });
|
|
340
|
+
if (sameFile(current, acquired) && current.mtimeNs === acquired.mtimeNs && fs.existsSync(ownerFile)) {
|
|
341
|
+
fs.rmSync(lock, { force: true });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
258
344
|
}
|
|
259
345
|
catch (error) {
|
|
260
346
|
if (!isEnoent(error))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-google-multi",
|
|
3
|
-
"version": "6.0.0-alpha.
|
|
3
|
+
"version": "6.0.0-alpha.78",
|
|
4
4
|
"mcpName": "io.github.bakissation/mcp-google-multi",
|
|
5
5
|
"description": "Local MCP server for Google Workspace (Gmail, Drive, Calendar, Sheets, Docs, Contacts, Tasks, Meet, Search Console, +Forms/Chat/Admin) across multiple accounts — OAuth-only, encrypted token storage, deny-by-default writes.",
|
|
6
6
|
"type": "module",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"./package.json": "./package.json"
|
|
37
37
|
},
|
|
38
38
|
"files": [
|
|
39
|
-
"dist"
|
|
39
|
+
"dist",
|
|
40
|
+
"skills"
|
|
40
41
|
],
|
|
41
42
|
"engines": {
|
|
42
43
|
"node": ">=22"
|
|
@@ -85,6 +86,7 @@
|
|
|
85
86
|
"gen:discovery": "tsx scripts/fetch-discovery.ts",
|
|
86
87
|
"gen:tools": "tsx scripts/gen-tools.ts",
|
|
87
88
|
"gen:coverage": "tsx scripts/gen-coverage.ts",
|
|
89
|
+
"gen:skill": "tsx scripts/gen-skill.ts",
|
|
88
90
|
"eval:contract": "npx --yes promptfoo@0.123.1 eval -c eval/contract/promptfooconfig.yaml --no-progress-bar",
|
|
89
91
|
"measure:lazy": "node scripts/measure-tools.mjs",
|
|
90
92
|
"pack:mcpb": "mcpb pack . mcp-google-multi.mcpb",
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: google-workspace
|
|
3
|
+
description: Use this skill when working with the mcp-google-multi MCP server to call Gmail, Drive, Calendar, Sheets, Docs, Contacts, Tasks, Meet, Chat, Slides, Forms, Search Console, Admin, or any other Google Workspace tool it exposes. Load it before the first discover or tool call in a session that touches this server, and whenever an account selector, a write-profile refusal, or an unfamiliar argument name needs resolving.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Google Workspace MCP server
|
|
7
|
+
|
|
8
|
+
This server exposes Google Workspace as MCP tools: a small set of curated,
|
|
9
|
+
hand-written tools per service, a long tail of generated tools (one per
|
|
10
|
+
Google API Discovery method), and an escape hatch for anything else. Reading
|
|
11
|
+
this skill before the first call avoids the two most common failures: guessing
|
|
12
|
+
an account when none was configured as default, and guessing an argument name
|
|
13
|
+
instead of using the one the tool actually declares.
|
|
14
|
+
|
|
15
|
+
## 1. Pick the account
|
|
16
|
+
|
|
17
|
+
Every operational tool takes an `account` argument naming one of the
|
|
18
|
+
configured aliases (an alias is a short local name, not the email address
|
|
19
|
+
itself). Before calling anything:
|
|
20
|
+
|
|
21
|
+
1. Call `account_list` first. It returns each alias, its email, admin flag,
|
|
22
|
+
token status, and which alias (if any) is the configured default.
|
|
23
|
+
2. If a default account is set, omit `account` on a single-account call;
|
|
24
|
+
the server injects the default. Passing an alias is always safe too.
|
|
25
|
+
3. If no default is set and more than one account is configured, you must
|
|
26
|
+
pass `account` explicitly. A call that omits it then fails with
|
|
27
|
+
`E_NO_DEFAULT_ACCOUNT` and a hint listing every `alias=email` pair so you
|
|
28
|
+
can match the one the user means by email, name, or context, rather than
|
|
29
|
+
guessing.
|
|
30
|
+
4. Read tools accept `"*"` for the `account` argument to fan out across every
|
|
31
|
+
configured account (bounded concurrency), or a comma-separated subset such
|
|
32
|
+
as `"work,personal"`. Fan-out is read-only by design: write tools, and the
|
|
33
|
+
handful of read tools that save a file to local disk (`drive_download`,
|
|
34
|
+
`drive_export`, `gmail_download_attachment`), take exactly one account.
|
|
35
|
+
A fan-out call returns `{ results: [{ account, ok, data|error }], partial }`
|
|
36
|
+
so one account failing never hides the others.
|
|
37
|
+
|
|
38
|
+
## 2. Discover before calling
|
|
39
|
+
|
|
40
|
+
Tools are hidden from the model's context until revealed, but every tool is
|
|
41
|
+
always callable by exact name (discovery is a listing optimization, not a
|
|
42
|
+
security boundary).
|
|
43
|
+
|
|
44
|
+
1. Call `{service}_discover` (e.g. `gmail_discover`, `drive_discover`) for the
|
|
45
|
+
service you need. It returns that service's catalog: each tool's name, a
|
|
46
|
+
one-line summary, its arguments, and its read/create/update/delete class.
|
|
47
|
+
An optional `query` argument filters the catalog by keyword.
|
|
48
|
+
2. Call the tool **by its exact name** from that catalog. Do not guess a name
|
|
49
|
+
by analogy with another service.
|
|
50
|
+
3. `discover_all` reveals every curated tool across all services at once,
|
|
51
|
+
useful before a session doing substantial cross-service work, and
|
|
52
|
+
`discover_reset` collapses back to the configured default afterward to
|
|
53
|
+
reclaim context.
|
|
54
|
+
4. A client that loads tool schemas on demand (rather than accepting
|
|
55
|
+
everything a server advertises) may need its own tool-search or
|
|
56
|
+
tool-load step before a newly revealed tool is actually callable. If a
|
|
57
|
+
call to a tool you just discovered fails as unknown on such a client,
|
|
58
|
+
look for that client's own load-tool mechanism before retrying the
|
|
59
|
+
Google call itself.
|
|
60
|
+
|
|
61
|
+
## 3. Incremental Gmail sweeps
|
|
62
|
+
|
|
63
|
+
For a recurring "what's new in this mailbox" check, do not re-run the same
|
|
64
|
+
broad `gmail_search` on a timer. Two cheaper patterns:
|
|
65
|
+
|
|
66
|
+
- **`gmail_list_history(startHistoryId, historyTypes)`** with a
|
|
67
|
+
`startHistoryId` you stored from a previous `gmail_get_profile` or
|
|
68
|
+
`gmail_list_history` response. It returns exactly the mailbox changes since that
|
|
69
|
+
point (messages added/deleted, labels added/removed), optionally filtered
|
|
70
|
+
by `historyTypes`. Store the newest history ID you observe and pass it
|
|
71
|
+
next time.
|
|
72
|
+
- **`gmail_search(since, until)`** when you have no stored history ID (e.g.
|
|
73
|
+
the first sweep, or after a gap long enough that Gmail's history window
|
|
74
|
+
has expired). Both accept an ISO 8601 date or a date-time with an explicit
|
|
75
|
+
`Z` or UTC offset, never a bare local time.
|
|
76
|
+
|
|
77
|
+
## 4. Keep results small
|
|
78
|
+
|
|
79
|
+
Large responses cost context and, for list endpoints, risk a page that looks
|
|
80
|
+
complete but isn't. Prefer the per-call controls over reading everything and
|
|
81
|
+
discarding most of it:
|
|
82
|
+
|
|
83
|
+
- On a generated tool or the `google_api_call(queryParams)` escape hatch,
|
|
84
|
+
pass Google's own `fields` partial-response query param to restrict a
|
|
85
|
+
response to the properties you actually need. Curated tools already pick a
|
|
86
|
+
response shape for you, so they don't expose `fields` themselves.
|
|
87
|
+
- Leave `full` unset (it defaults to the trimmed shape) on fat readers such as
|
|
88
|
+
`gmail_read(full)` and `gmail_read_thread(full)`; pass `full: true` only
|
|
89
|
+
when you actually need the untruncated body. `calendar_list_events` and
|
|
90
|
+
`calendar_list_instances` always return a trimmed view with no `full`
|
|
91
|
+
argument to toggle; call `calendar_get_event` for one event's complete
|
|
92
|
+
data.
|
|
93
|
+
- Cap the page on list and search tools (the argument name varies by tool:
|
|
94
|
+
`drive_search(maxResults)` and `contacts_search(pageSize)` are two
|
|
95
|
+
examples; check `{service}_discover` or the reference table for the exact
|
|
96
|
+
name), and follow `nextPageToken` rather than assuming a short list is the
|
|
97
|
+
whole answer. Where a list reports `truncated`, there is more to fetch.
|
|
98
|
+
- On Gmail, pass `headers` on `gmail_read(headers)` or
|
|
99
|
+
`gmail_read_batch(headers)` to fetch only `id`, `threadId`, `labelIds`, and
|
|
100
|
+
the named header values, skipping full body decoding entirely when you
|
|
101
|
+
only need metadata like `Subject` or `From`.
|
|
102
|
+
|
|
103
|
+
## 5. Write profiles
|
|
104
|
+
|
|
105
|
+
Reads are never gated. Writes (create/update/delete) are deny-by-default and
|
|
106
|
+
controlled by the deployment's `GOOGLE_PROFILE`:
|
|
107
|
+
|
|
108
|
+
- **`read-only`** (the default): every write refused.
|
|
109
|
+
- **`safe-writes`**: ordinary creates and updates on your own data are
|
|
110
|
+
allowed, and so is moving an item to or from Trash (`gmail_trash`,
|
|
111
|
+
`drive_trash`, and their untrash equivalents) since that is recoverable.
|
|
112
|
+
Permanent deletes (emptying Trash, a hard Gmail or Drive delete),
|
|
113
|
+
`drive_transfer` with `move: true`, and privileged writes (anything needing
|
|
114
|
+
an admin, Vault, reseller, or script scope, or registering a push channel)
|
|
115
|
+
stay refused under this profile.
|
|
116
|
+
- **`full-writes`**: everything permitted by OAuth scope and the account's
|
|
117
|
+
grants, privileged writes included. An operator's allow and deny patterns
|
|
118
|
+
can widen or narrow any profile.
|
|
119
|
+
|
|
120
|
+
A write a profile refuses comes back as `{ error: "write_disabled", message,
|
|
121
|
+
hint, retriable: false }` rather than a silent no-op or a generic failure.
|
|
122
|
+
The `hint` names the exact rule that refused it (a deny pattern, the
|
|
123
|
+
privileged-write rule, or the active profile) and what would need to change;
|
|
124
|
+
read it rather than retrying the same call. This applies identically to
|
|
125
|
+
curated tools, generated tools, and the `google_api_call` escape hatch.
|
|
126
|
+
|
|
127
|
+
## 6. Argument names are exact
|
|
128
|
+
|
|
129
|
+
The server never rewrites an argument name: if you pass `pageSize` where a
|
|
130
|
+
tool expects `maxResults`, the call fails rather than silently mapping one to
|
|
131
|
+
the other. Always use the exact name shown by `{service}_discover` or the
|
|
132
|
+
service's reference table in `reference/` next to this file. When in doubt,
|
|
133
|
+
discover the tool again and read its declared arguments rather than guessing
|
|
134
|
+
from memory or from another service's convention.
|
|
135
|
+
|
|
136
|
+
Each `reference/<service>.md` file tables every **curated** tool for that
|
|
137
|
+
service: its name, its read/create/update/delete class, a one-line summary,
|
|
138
|
+
and every argument with its type, whether it's required, and its
|
|
139
|
+
description. **Generated** tools (the long tail, one per Google API
|
|
140
|
+
Discovery method) are not tabled individually: they take Google's own
|
|
141
|
+
Discovery parameter names directly, and calling `{service}_discover` lists
|
|
142
|
+
each one with its arguments at call time.
|
|
143
|
+
|
|
144
|
+
## 7. Escape hatch for anything not covered
|
|
145
|
+
|
|
146
|
+
If a service has no curated or generated tool for the operation you need
|
|
147
|
+
(or discovery/`reference/` doesn't cover it):
|
|
148
|
+
|
|
149
|
+
1. Call `google_api_search(query, api, maxResults)` with keywords (optionally
|
|
150
|
+
scoped to one `api` alias) to find the Discovery method id, e.g.
|
|
151
|
+
`drive.revisions.list`.
|
|
152
|
+
2. Call `google_api_call(api, methodId, pathParams, queryParams, body)` with
|
|
153
|
+
that method id and whichever of path params, query params, or a JSON body
|
|
154
|
+
it needs.
|
|
155
|
+
|
|
156
|
+
The escape hatch runs through the same write-control policy as named tools
|
|
157
|
+
(the read/create/update/delete class is derived from the method's HTTP verb
|
|
158
|
+
and name), returns JSON only (binary media is refused with a pointer to
|
|
159
|
+
`drive_download` / `drive_export`), and a response over about 100k
|
|
160
|
+
characters may be cut by the client, so narrow a large request. Prefer a named curated or
|
|
161
|
+
generated tool when one exists: it is more token-lean and better typed than
|
|
162
|
+
building the call by hand.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# admin
|
|
2
|
+
|
|
3
|
+
<!-- GENERATED by scripts/gen-skill.ts, do not edit by hand. Regenerate: npm run gen:skill -->
|
|
4
|
+
|
|
5
|
+
6 curated tools, plus 132 generated tools (Google Discovery parameter names; see `admin_discover`).
|
|
6
|
+
|
|
7
|
+
## `admin_group_members_list`
|
|
8
|
+
|
|
9
|
+
Class: read
|
|
10
|
+
|
|
11
|
+
List members of a Workspace group
|
|
12
|
+
|
|
13
|
+
| Argument | Type | Required | Description |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| `account` | string | optional | Google account alias (must be a Workspace admin); "*" = all accounts; omit for the default account |
|
|
16
|
+
| `groupKey` | string | required | Group email or ID |
|
|
17
|
+
| `roles` | string | optional | Comma-separated roles to include (OWNER, MANAGER, MEMBER) |
|
|
18
|
+
| `includeDerivedMembership` | boolean | optional | |
|
|
19
|
+
| `maxResults` | number | optional | |
|
|
20
|
+
| `pageToken` | string | optional | |
|
|
21
|
+
|
|
22
|
+
## `admin_groups_list`
|
|
23
|
+
|
|
24
|
+
Class: read
|
|
25
|
+
|
|
26
|
+
List groups in the Workspace customer
|
|
27
|
+
|
|
28
|
+
| Argument | Type | Required | Description |
|
|
29
|
+
|---|---|---|---|
|
|
30
|
+
| `account` | string | optional | Google account alias (must be a Workspace admin); "*" = all accounts; omit for the default account |
|
|
31
|
+
| `customer` | string | optional | |
|
|
32
|
+
| `domain` | string | optional | |
|
|
33
|
+
| `userKey` | string | optional | Only return groups containing this user |
|
|
34
|
+
| `query` | string | optional | |
|
|
35
|
+
| `maxResults` | number | optional | |
|
|
36
|
+
| `pageToken` | string | optional | |
|
|
37
|
+
|
|
38
|
+
## `admin_users_get`
|
|
39
|
+
|
|
40
|
+
Class: read
|
|
41
|
+
|
|
42
|
+
Get a single Workspace user by email or user ID
|
|
43
|
+
|
|
44
|
+
| Argument | Type | Required | Description |
|
|
45
|
+
|---|---|---|---|
|
|
46
|
+
| `account` | string | optional | Google account alias (must be a Workspace admin); "*" = all accounts; omit for the default account |
|
|
47
|
+
| `userKey` | string | required | User email or ID |
|
|
48
|
+
| `projection` | enum | optional | |
|
|
49
|
+
|
|
50
|
+
## `admin_users_list`
|
|
51
|
+
|
|
52
|
+
Class: read
|
|
53
|
+
|
|
54
|
+
List users in the Workspace customer. Filter by domain or customer=my_customer.
|
|
55
|
+
|
|
56
|
+
| Argument | Type | Required | Description |
|
|
57
|
+
|---|---|---|---|
|
|
58
|
+
| `account` | string | optional | Google account alias (must be a Workspace admin); "*" = all accounts; omit for the default account |
|
|
59
|
+
| `customer` | string | optional | |
|
|
60
|
+
| `domain` | string | optional | |
|
|
61
|
+
| `query` | string | optional | Directory search query: space-separated clauses of field+operator+value, ANDed together. "=" is exact match, ":" is a contains/startsWith match. Searchable fields include email, name (matches the concatenated givenName+familyName), givenName, familyName, isAdmin, isDelegatedAdmin, orgName, orgTitle, orgDepartment, manager, directManager, addressCountry. Prefix/wildcard matching is a trailing "*" on a ":" clause, e.g. "email:admin*" for every address starting with "admin". Examples: 'name:John', 'isAdmin=true', 'orgTitle:Manager orgName=Engineering'. |
|
|
62
|
+
| `maxResults` | number | optional | |
|
|
63
|
+
| `pageToken` | string | optional | |
|
|
64
|
+
| `orderBy` | enum | optional | |
|
|
65
|
+
| `showDeleted` | boolean | optional | |
|
|
66
|
+
| `projection` | enum | optional | |
|
|
67
|
+
|
|
68
|
+
## `admin_users_update`
|
|
69
|
+
|
|
70
|
+
Class: update
|
|
71
|
+
|
|
72
|
+
Update a Workspace user (PATCH semantics). Gated by write-control (a CUD tool).
|
|
73
|
+
|
|
74
|
+
| Argument | Type | Required | Description |
|
|
75
|
+
|---|---|---|---|
|
|
76
|
+
| `account` | string | optional | Google account alias (must be a Workspace admin) |
|
|
77
|
+
| `userKey` | string | required | User email or ID |
|
|
78
|
+
| `givenName` | string | optional | |
|
|
79
|
+
| `familyName` | string | optional | |
|
|
80
|
+
| `suspended` | boolean | optional | |
|
|
81
|
+
| `password` | string | optional | |
|
|
82
|
+
| `changePasswordAtNextLogin` | boolean | optional | |
|
|
83
|
+
| `orgUnitPath` | string | optional | |
|
|
84
|
+
|
|
85
|
+
## `reports_activities_list`
|
|
86
|
+
|
|
87
|
+
Class: read
|
|
88
|
+
|
|
89
|
+
List Admin Activity audit log entries. Filter by application (login, drive, gmail, admin, token, etc.), user, date range, event. The single most useful admin endpoint for SMB visibility.
|
|
90
|
+
|
|
91
|
+
| Argument | Type | Required | Description |
|
|
92
|
+
|---|---|---|---|
|
|
93
|
+
| `account` | string | optional | Google account alias (must be a Workspace admin); "*" = all accounts; omit for the default account |
|
|
94
|
+
| `applicationName` | enum | required | Which application's activity to query |
|
|
95
|
+
| `userKey` | string | optional | User profile ID, email, or "all" (default) |
|
|
96
|
+
| `startTime` | string | optional | RFC 3339 lower bound |
|
|
97
|
+
| `endTime` | string | optional | RFC 3339 upper bound |
|
|
98
|
+
| `eventName` | string | optional | Specific event name to filter |
|
|
99
|
+
| `actorIpAddress` | string | optional | |
|
|
100
|
+
| `filters` | string | optional | Comma-separated event-parameter filters with relational operators |
|
|
101
|
+
| `orgUnitID` | string | optional | |
|
|
102
|
+
| `groupIdFilter` | string | optional | |
|
|
103
|
+
| `customerId` | string | optional | |
|
|
104
|
+
| `maxResults` | number | optional | |
|
|
105
|
+
| `pageToken` | string | optional | |
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# analytics
|
|
2
|
+
|
|
3
|
+
<!-- GENERATED by scripts/gen-skill.ts, do not edit by hand. Regenerate: npm run gen:skill -->
|
|
4
|
+
|
|
5
|
+
4 curated tools, plus 62 generated tools (Google Discovery parameter names; see `analytics_discover`).
|
|
6
|
+
|
|
7
|
+
## `analytics_account_summaries`
|
|
8
|
+
|
|
9
|
+
Class: read
|
|
10
|
+
|
|
11
|
+
List every Google Analytics (GA4) account and property this Google account can access, with their numeric property IDs. The starting point for any Analytics question ("what properties do I have?").
|
|
12
|
+
|
|
13
|
+
| Argument | Type | Required | Description |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| `account` | string | optional | Google account alias; "*" = all accounts; omit for the default account |
|
|
16
|
+
| `pageSize` | number | optional | Summaries per page (default 50, max 200) |
|
|
17
|
+
| `pageToken` | string | optional | Token from a previous page |
|
|
18
|
+
|
|
19
|
+
## `analytics_get_metadata`
|
|
20
|
+
|
|
21
|
+
Class: read
|
|
22
|
+
|
|
23
|
+
List every valid dimension and metric API name for a GA4 property, including its custom definitions. Call this before analytics_run_report when unsure which names exist. Property "0" returns the standard set without property access.
|
|
24
|
+
|
|
25
|
+
| Argument | Type | Required | Description |
|
|
26
|
+
|---|---|---|---|
|
|
27
|
+
| `account` | string | optional | Google account alias; "*" = all accounts; omit for the default account |
|
|
28
|
+
| `property` | string | required | GA4 property: numeric ID like "213025502" or "properties/213025502" — NOT a "G-..." measurement ID and not "UA-...". Find yours with analytics_account_summaries. |
|
|
29
|
+
|
|
30
|
+
## `analytics_run_realtime_report`
|
|
31
|
+
|
|
32
|
+
Class: read
|
|
33
|
+
|
|
34
|
+
Run a GA4 realtime report: who is on the site right now (last 30 minutes). Realtime supports a restricted set of names, e.g. metrics activeUsers, screenPageViews, eventCount, keyEvents; dimensions country, city, deviceCategory, unifiedScreenName, eventName.
|
|
35
|
+
|
|
36
|
+
| Argument | Type | Required | Description |
|
|
37
|
+
|---|---|---|---|
|
|
38
|
+
| `account` | string | optional | Google account alias; "*" = all accounts; omit for the default account |
|
|
39
|
+
| `property` | string | required | GA4 property: numeric ID like "213025502" or "properties/213025502" — NOT a "G-..." measurement ID and not "UA-...". Find yours with analytics_account_summaries. |
|
|
40
|
+
| `metrics` | array | required | Realtime metric API names, e.g. ["activeUsers"] |
|
|
41
|
+
| `dimensions` | array | optional | Realtime dimension API names, e.g. ["country"] or ["unifiedScreenName"] |
|
|
42
|
+
| `dimensionFilter` | object | optional | FilterExpression on dimensions — same shape as analytics_run_report |
|
|
43
|
+
| `metricFilter` | object | optional | FilterExpression on metric values — same shape as analytics_run_report |
|
|
44
|
+
| `minuteRanges` | array | optional | Up to 2 ranges of minutes-ago, e.g. [{"startMinutesAgo":29,"endMinutesAgo":0}] (default: last 30 minutes) |
|
|
45
|
+
| `limit` | number | optional | Max rows to return |
|
|
46
|
+
| `returnPropertyQuota` | boolean | optional | Include this property's remaining realtime quota tokens |
|
|
47
|
+
|
|
48
|
+
## `analytics_run_report`
|
|
49
|
+
|
|
50
|
+
Class: read
|
|
51
|
+
|
|
52
|
+
Run a Google Analytics (GA4) report: metrics over a date range, optionally grouped by dimensions — the workhorse for questions like "how many users last week, by country". Dates accept YYYY-MM-DD or relative forms ("today", "yesterday", "28daysAgo"). Also accepts the native GA4 request shapes directly: a single dateRanges entry in place of startDate/endDate, and metrics/dimensions as {name} objects. Unsure which dimension/metric names are valid? Call analytics_get_metadata first.
|
|
53
|
+
|
|
54
|
+
| Argument | Type | Required | Description |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| `account` | string | optional | Google account alias; "*" = all accounts; omit for the default account |
|
|
57
|
+
| `property` | string | required | GA4 property: numeric ID like "213025502" or "properties/213025502" — NOT a "G-..." measurement ID and not "UA-...". Find yours with analytics_account_summaries. |
|
|
58
|
+
| `startDate` | string | optional | Start date: YYYY-MM-DD or relative ("today", "yesterday", "NdaysAgo" e.g. "28daysAgo"). Required unless dateRanges is given. |
|
|
59
|
+
| `endDate` | string | optional | End date: YYYY-MM-DD or relative ("today", "yesterday", "NdaysAgo"). Required unless dateRanges is given. |
|
|
60
|
+
| `dateRanges` | array | optional | Native GA4 shape, as an alternative to startDate/endDate: exactly one {startDate, endDate} entry (this tool runs one range per call). |
|
|
61
|
+
| `metrics` | array | required | Metric API names, e.g. ["activeUsers","sessions","screenPageViews"], or the native GA4 [{"name":"activeUsers"}] form. Max 10. Valid names (including custom ones) come from analytics_get_metadata. |
|
|
62
|
+
| `dimensions` | array | optional | Dimension API names to group by, e.g. ["date"] or ["country","deviceCategory"], or the native GA4 {"name":...} form. Max 9. Omit for a single total row. |
|
|
63
|
+
| `dimensionFilter` | object | optional | FilterExpression on dimensions (applies independently of metricFilter). Simple: {"filter":{"fieldName":"country","stringFilter":{"matchType":"EXACT","value":"France"}}}. AND of two: {"andGroup":{"expressions":[{"filter":{"fieldName":"country","stringFilter":{"matchType":"EXACT","value":"France"}}},{"filter":{"fieldName":"deviceCategory","stringFilter":{"matchType":"EXACT","value":"mobile"}}}]}}. matchType: EXACT \| BEGINS_WITH \| ENDS_WITH \| CONTAINS \| FULL_REGEXP (add "caseSensitive":true for case-sensitive). Also available: inListFilter, notExpression, orGroup. |
|
|
64
|
+
| `metricFilter` | object | optional | FilterExpression on metric values, e.g. {"filter":{"fieldName":"sessions","numericFilter":{"operation":"GREATER_THAN","value":{"int64Value":"100"}}}}. operation: EQUAL \| LESS_THAN \| LESS_THAN_OR_EQUAL \| GREATER_THAN \| GREATER_THAN_OR_EQUAL; betweenFilter takes fromValue/toValue. |
|
|
65
|
+
| `orderBys` | array | optional | Sort order, e.g. [{"metric":{"metricName":"sessions"},"desc":true}] or [{"dimension":{"dimensionName":"date"}}]. Default: unordered. |
|
|
66
|
+
| `limit` | number | optional | Max rows to return (API default 10000). Keep small for readable output. |
|
|
67
|
+
| `offset` | number | optional | Zero-based row offset for pagination |
|
|
68
|
+
| `metricAggregations` | array | optional | Also return aggregate rows across all matching data (surfaced as totals/maximums/minimums) |
|
|
69
|
+
| `keepEmptyRows` | boolean | optional | Include rows whose metrics are all zero (default false) |
|
|
70
|
+
| `returnPropertyQuota` | boolean | optional | Include this property's remaining quota tokens in the response |
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# appsmarket
|
|
2
|
+
|
|
3
|
+
<!-- GENERATED by scripts/gen-skill.ts, do not edit by hand. Regenerate: npm run gen:skill -->
|
|
4
|
+
|
|
5
|
+
This service has no curated tools. All 2 `appsmarket` tools are generated from Google's API Discovery document: they take Google's own Discovery parameter names (not a hand-written schema), and `appsmarket_discover` lists each one with its arguments.
|