Skip to content

Commit 55d8c65

Browse files
authored
Merge pull request #143 from karthikcsq/t3code/review-main-changes-npm-readiness
Pre-publish review of the 2026-09-01 main changes: eight fixes before v3.0.0
2 parents a957677 + 66d4e18 commit 55d8c65

33 files changed

Lines changed: 1628 additions & 278 deletions

.claude/skills/live-agent-loop/SKILL.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,11 @@ failures and every distinct error message.
105105

106106
**Exit code semantics:** a failed mission exits 0, because a mission that failed
107107
is a finding, not a runner error. The runner exits non-zero only when it could
108-
not do its own job: a safety refusal, a stdout leak from the tool code path, or
109-
cleanup leaving something behind. Read the status line, never the exit code
110-
alone.
108+
not do its own job: an undeclared safety refusal, a stdout leak from the tool
109+
code path, cleanup leaving something behind, or a creating call whose result
110+
named no id it could register (`UNTRACKED`). A mission whose purpose is to prove
111+
a guard deny still holds exports `expectsSafetyRefusals = N`; exactly `N` are
112+
forgiven, no more. Read the status line, never the exit code alone.
111113

112114
### 5. Triage the report
113115

@@ -145,9 +147,10 @@ Anything short of that is another iteration.
145147

146148
## Writing a mission
147149

148-
Missions live in `live/missions/` and are the same shape as a `live/` scenario
149-
minus the assertions. See `live/missions/harness-selftest.mjs` for a worked
150-
example.
150+
Missions live in `live/missions/` and are the same shape as a `live/` scenario,
151+
with `ctx.friction()` and `ctx.note()` added. The `ctx.assert*` helpers still
152+
work; a failed assertion ends the mission with status `fail` rather than a
153+
friction entry. See `live/missions/harness-selftest.mjs` for a worked example.
151154

152155
```js
153156
export const name = 'meeting-notes';
@@ -169,6 +172,8 @@ Context API:
169172
| `await ctx.call(tool, args)` | throws on failure |
170173
| `await ctx.tryCall(tool, args)` | returns `{ok, result, error}`, never throws |
171174
| `await ctx.createDoc(title, markdown)` | seeded doc in the sandbox |
175+
| `await ctx.createFolder(name, parent?)` | folder inside the sandbox |
176+
| `ctx.describe(name)` | the tool's description string, as a real MCP client sees it in `tools/list` |
172177
| `ctx.title(label)` | label + run id, so concurrent runs cannot collide |
173178
| `ctx.folderId` | the sandbox folder id |
174179
| `ctx.note(text)` | observation, lands in the report |

CHANGELOG.md

Lines changed: 102 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,24 @@ stdio client is unaffected. See
7373
[`.claude/skills/live-agent-loop/SKILL.md`](.claude/skills/live-agent-loop/SKILL.md),
7474
reference in [docs/live-agent-loop.md](docs/live-agent-loop.md).
7575
- `npm run live-coverage` reports which of the 160 registered tools are driven
76-
against the real Google API by checked-in code (28) and which have unit tests
77-
only (132), and exits non-zero if a scenario calls a tool that is no longer
78-
registered. Tools that provably cannot reach Google are reported separately
76+
against the real Google API by checked-in code (29) and which have unit tests
77+
only (131), and exits non-zero if a scenario calls a tool that is no longer
78+
registered, or if it finds no covered tools at all (a silent zero means the
79+
scan broke, not the coverage). Calls through the `ctx.createDoc()` and
80+
`ctx.createFolder()` helpers are credited to `createDocument` and
81+
`createFolder`, any quote style around a tool name counts, and
82+
`live/missions/archive/` is skipped: it holds the frozen iteration-1 and
83+
iteration-2 transcripts, which record what the agent hit at the time and are
84+
not expected to pass on fixed code (loop-2 sends the nested `formatCells`
85+
shape that 3.0 rejects by design). Tools that provably cannot reach Google are reported separately
7986
rather than counted as covered, because a scenario names them only to assert
8087
the refusal holds: `forwardMessage`, which the runner blocks before
8188
`execute()`, and `createPresentation`, which `guard.mjs` denies outright
8289
because the Slides API creates in Drive root whatever parent it is given. It
8390
runs offline, registering every category against a recording
8491
stub. The point is that "all suites pass" and "this tool works" are different
8592
claims, and the gap between them should be a number rather than a feeling.
86-
- Jest `testTimeout` raised to 30s. The four suites that call `registerAllTools`
93+
- Jest `testTimeout` raised to 30s. The seven suites that call `registerAllTools`
8794
dynamically import all 12 tool categories (~180 modules): about 750ms warm, but
8895
measured past Jest's 5s default against a cold filesystem cache. That is
8996
exactly the CI shape, since both workflows run `npm ci` and then the suite, and
@@ -101,6 +108,37 @@ stdio client is unaffected. See
101108
counted what the runner had noticed. One shared extractor now handles every
102109
shape including the prose form, and a creating call whose id cannot be found
103110
is reported as `UNTRACKED` and fails the run rather than passing quietly.
111+
The cleanup loop itself is now one shared implementation
112+
(`scripts/live-smoke/cleanup.mjs`) instead of two copies, because both copies
113+
had the same holes: a file the run had already deleted was reported as left
114+
behind (the containment check ran before the existence check, and an
115+
unreadable file is "not proven inside"); an `invalid_grant` rebuild inside
116+
the run's last call left the raw Drive handle null so every trash failed with
117+
a `TypeError`; a guard refusal was counted as neither pass nor fail; and a
118+
mission that `track()`ed an id the runner had already registered made
119+
cleanup say `2/2` for one file. After cleanup both runners now list the test
120+
folder and any leftover drafts and fail on anything this run left there, and
121+
`--keep` prints each kept id with the `npm run live-call -- --cleanup <ids>`
122+
command that trashes them later.
123+
- The set of tools `live-coverage` reports as "denied by `guard.mjs`" is derived
124+
from the guard's own deny table and read-only deciders (`MUTATING_VERB` is now
125+
exported for that) rather than a hand-maintained list beside it, so the two
126+
cannot drift. The created-resource pin test executes all eight creating tools
127+
against fake clients and feeds their real return values to the shared id
128+
extractor, replacing a test that only compared two hand-written literals.
129+
- `verify-preserve-heading` probe 3 deletes a heading on purpose; its verdict
130+
logic expected the heading to survive and reported false friction on every
131+
run. The probe now states which outcome it expects.
132+
- The post-cleanup audit fails closed. It listed only the top level of the test
133+
folder, so a file inside a folder the run created was hidden behind its
134+
parent; a listing that threw came back as an empty array; a listing the tool
135+
cut short was read as complete; and any `getDraft` error (auth, quota,
136+
network) was taken as "the draft is gone". The scan is now recursive
137+
(`depth: 'all'`, up to 5000 items), and a listing that could not be made, was
138+
truncated, or skipped a folder it could not read is reported as `UNVERIFIED`
139+
and fails the run. Only an error carrying a real 404 status counts as a
140+
deleted draft; anything else is reported per draft and fails the run too.
141+
Covered by `tests/liveHarnessCleanupAudit.test.js`.
104142

105143
### Security
106144

@@ -238,12 +276,63 @@ stdio client is unaffected. See
238276
token fails every in-flight call at once, and each failure nulled the shared
239277
clients out from under the others mid-rebuild. Both now hold the in-flight
240278
Promise, and both release it when it settles, so a declined consent screen is
241-
never replayed to the next caller.
279+
never replayed to the next caller. Each flow releases only the latch it owns:
280+
`logout` drops the latch mid-flow and the next request starts a fresh one,
281+
and when the abandoned flow later fails it must not clear that newer latch,
282+
or a third request would open a third browser window behind the one the
283+
user is already looking at. Nor may the abandoned flow *succeed*: `logout`
284+
bumps an authorization generation, and a flow that started under the old one
285+
throws `Logged out while authorization was in progress` instead of installing
286+
a client the user has just asked to discard, whichever flow finishes last.
287+
A cold request that arrives while a re-authorization is running joins it
288+
rather than opening a second consent screen beside it.
242289
- **`GET /healthz` reported a closed runtime as healthy.** The health branch sat
243290
above the `closed` check, so a drained handler answered `200 {"status":"ok"}`
244291
while `/mcp` already answered `503` — the one probe meant to notice a dead
245292
runtime was the only route that never did. It now answers
246293
`503 {"status":"closed"}`, still behind the same auth gate.
294+
- **`setup` and `doctor` could not finish on any machine that had Codex or
295+
Claude Code installed.** Both clients report a missing registration as a
296+
failed command (`codex mcp get google --json` exits 1 with "No MCP server
297+
named 'google' found."), and every rejection was mapped to `unknown` without
298+
reading it, so setup stopped at Step 5 with "was left unconfigured (unknown).
299+
Setup is incomplete." and doctor said "unrecognized client entry". The Claude
300+
Code probe was worse: `claude mcp get -s user google --json` is answered by
301+
every version with `error: unknown option '-s'`, so inspection was `unknown`
302+
on every machine. A rejection whose text says "missing" now means missing,
303+
and Claude Code's user-scope entry is read from the file `claude mcp add -s
304+
user` writes (`~/.claude.json`, or `$CLAUDE_CONFIG_DIR/.claude.json`), with
305+
its `type: 'stdio'` discriminator and empty `env` stripped so a correct entry
306+
compares equal to the desired one instead of being re-added on every run.
307+
- **`doctor` reported every README-documented registration as a problem.** Its
308+
"recommended" stdio entry is the absolute path of the copy running doctor,
309+
while setup writes `node <global npm root>/google-tools-mcp/dist/index.js`
310+
and the README's own instructions register the bare `google-tools-mcp` bin
311+
or `npx -y google-tools-mcp`. All three were "entry differs from recommended
312+
configuration", exit 1, including `npx -y google-tools-mcp doctor` run right
313+
after a successful setup. An entry that launches this package (its bin, a
314+
Windows shim, `npx google-tools-mcp[@version]`, a `dist/index.js` next to a
315+
`google-tools-mcp` package.json) is now `configured` with a note saying it is
316+
not the entry setup would write, and the npx form additionally says why setup
317+
prefers a direct launch. A Codex entry without `CODEX_MCP_PROTOCOL_VERSION`
318+
in its env stays a problem, and now names the missing variable and value. An
319+
entry that launches something else, a `@latest` target, or an HTTP URL that
320+
differs is still a problem. So is an entry that launches this package with a
321+
subcommand or flag after it (`google-tools-mcp doctor`, `npx google-tools-mcp
322+
setup`, `dist/index.js auth`), because `dist/index.js` dispatches on
323+
`argv[2]` and such an entry never starts the server, and one whose env sets
324+
`GOOGLE_MCP_TRANSPORT` to an HTTP transport.
325+
- **`doctor --json` printed `"args": "[Circular]"` for the second client.** The
326+
diagnostic redactor treated every object it had already seen as a cycle, so
327+
the `args` array the two recommended entries share was replaced on its second
328+
appearance. It now tracks ancestors only: a true cycle still prints
329+
`[Circular]`, a shared reference prints its value.
330+
- **The update check hit the npm registry on every launch.** `checkForUpdate`
331+
read and wrote its cache through injectable `readFile`/`writeFile`/`mkdir`
332+
parameters whose defaults were never set, so outside the unit tests every
333+
cache read threw, was swallowed, and the registry was asked again. The
334+
defaults are the real `node:fs/promises` functions, and a test now proves a
335+
second launch within the TTL makes no network request.
247336
- **`readDocument(format='markdown')` wrapped every run in a colour span nobody
248337
asked for, which made read-back verification impossible.** #14 requires every
249338
run this server writes to carry an explicit `foregroundColor`, and Google's
@@ -292,7 +381,14 @@ stdio client is unaffected. See
292381
agent into skipping discovery and guessing argument names. `help` now accepts
293382
`tool` (returns that tool's description and JSON Schema, ~3,000 characters)
294383
and `listTools` (just the registered names). Calling it with no arguments
295-
still returns the full manual, so nothing that worked before changed.
384+
still returns the full manual, so nothing that worked before changed. The
385+
schema it returns is rendered the same way the SDK renders `tools/list`
386+
(`io: 'input'`, draft 2020-12): the first cut used Zod's bare
387+
`toJSONSchema()`, whose default is the *output* schema, so every
388+
`.optional().default(x)` field came back as `required`. For 50 of the 160
389+
tools that contradicted `tools/list`; `readDocument` alone claimed all seven
390+
of its optional fields were mandatory. A test now pins `help`'s schema to the
391+
SDK's conversion for every registered tool.
296392
- **`formatCells` now names its accepted arguments when none are supplied.**
297393
The tool takes flat options with hex-string colors, not the nested Google
298394
Sheets API `CellFormat` shape, which is what a caller who knows the underlying

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,14 +150,22 @@ npm install -g google-tools-mcp
150150

151151
#### Codex
152152

153+
Codex pins stdio servers to the legacy MCP lifecycle unless told otherwise, so
154+
its registration needs `CODEX_MCP_PROTOCOL_VERSION=2026-07-28` in the env block
155+
(see [Breaking change in 3.0.0](#breaking-change-in-300-http-is-stateless)).
156+
The setup wizard writes it for you; by hand, pass it with `--env`:
157+
153158
```bash
154-
codex mcp add google -- google-tools-mcp
159+
codex mcp add google \
160+
--env CODEX_MCP_PROTOCOL_VERSION=2026-07-28 \
161+
-- google-tools-mcp
155162
```
156163

157164
With env vars (Option C):
158165

159166
```bash
160167
codex mcp add google \
168+
--env CODEX_MCP_PROTOCOL_VERSION=2026-07-28 \
161169
--env GOOGLE_CLIENT_ID=your-client-id \
162170
--env GOOGLE_CLIENT_SECRET=your-client-secret \
163171
-- google-tools-mcp
@@ -421,6 +429,8 @@ These tools require `GOOGLE_MAPS_API_KEY`, a Google Maps Platform API key, separ
421429

422430
The categories above contain 156 service-specific tools. Four general utilities — `help`, `logout`, `troubleshoot`, and `feedback` — bring the default server surface to 160 tools. See [Common Workflows](docs/workflows.md) for practical examples.
423431

432+
`help` with no arguments returns this whole manual. `help` with `tool: "<name>"` returns just that tool's description and the JSON Schema for its arguments, exactly as `tools/list` publishes it, and `help` with `listTools: true` returns the registered tool names one per line. Reach for the per-tool form first: it is a few thousand characters instead of forty thousand, and it states the argument shape the tool actually takes.
433+
424434
## Finding Character Indices in a Doc
425435

426436
Every index-addressed Docs tool (`modifyText`, `batchModifyText`, `replaceRangeWithMarkdown`, `deleteRange`, `insertTable`, `insertTableWithData`, `insertPageBreak`) needs `startIndex`/`endIndex` values. Get them with `readDocument` and `format='index'`:

0 commit comments

Comments
 (0)