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.
Files changed (33) hide show
  1. package/README.md +14 -0
  2. package/dist/fs-atomic.js +98 -12
  3. package/package.json +4 -2
  4. package/skills/google-workspace/SKILL.md +162 -0
  5. package/skills/google-workspace/reference/admin.md +105 -0
  6. package/skills/google-workspace/reference/analytics.md +70 -0
  7. package/skills/google-workspace/reference/appsmarket.md +5 -0
  8. package/skills/google-workspace/reference/calendar.md +175 -0
  9. package/skills/google-workspace/reference/chat.md +59 -0
  10. package/skills/google-workspace/reference/classroom.md +5 -0
  11. package/skills/google-workspace/reference/cloudidentity.md +5 -0
  12. package/skills/google-workspace/reference/cloudsearch.md +5 -0
  13. package/skills/google-workspace/reference/contacts.md +138 -0
  14. package/skills/google-workspace/reference/docs.md +395 -0
  15. package/skills/google-workspace/reference/drive.md +485 -0
  16. package/skills/google-workspace/reference/driveactivity.md +5 -0
  17. package/skills/google-workspace/reference/drivelabels.md +5 -0
  18. package/skills/google-workspace/reference/forms.md +92 -0
  19. package/skills/google-workspace/reference/gmail.md +305 -0
  20. package/skills/google-workspace/reference/groupsmigration.md +5 -0
  21. package/skills/google-workspace/reference/groupssettings.md +5 -0
  22. package/skills/google-workspace/reference/keep.md +5 -0
  23. package/skills/google-workspace/reference/licensing.md +5 -0
  24. package/skills/google-workspace/reference/meet.md +68 -0
  25. package/skills/google-workspace/reference/postmaster.md +5 -0
  26. package/skills/google-workspace/reference/reseller.md +5 -0
  27. package/skills/google-workspace/reference/script.md +5 -0
  28. package/skills/google-workspace/reference/searchconsole.md +128 -0
  29. package/skills/google-workspace/reference/sheets.md +430 -0
  30. package/skills/google-workspace/reference/slides.md +67 -0
  31. package/skills/google-workspace/reference/tasks.md +167 -0
  32. package/skills/google-workspace/reference/vault.md +5 -0
  33. 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
- if (sameFile(fs.lstatSync(ownerPath, { bigint: true }), before)) {
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 !== 'EEXIST')
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, bound);
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
- function releaseLock(lock, ownerFile, bound) {
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 (!bound || sameFile(fs.lstatSync(ownerFile, { bigint: true }), fs.lstatSync(lock, { bigint: true })))
257
- fs.rmSync(lock, { force: true });
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.76",
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.