gogcli-mcp 1.0.4 → 1.0.6
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 +23 -125
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5342 -5394
- package/dist/lib.d.ts +6 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +30837 -0
- package/dist/runner.d.ts +12 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/tools/auth.d.ts +3 -0
- package/dist/tools/auth.d.ts.map +1 -0
- package/dist/tools/calendar.d.ts +3 -0
- package/dist/tools/calendar.d.ts.map +1 -0
- package/dist/tools/contacts.d.ts +3 -0
- package/dist/tools/contacts.d.ts.map +1 -0
- package/dist/tools/docs.d.ts +3 -0
- package/dist/tools/docs.d.ts.map +1 -0
- package/dist/tools/drive.d.ts +3 -0
- package/dist/tools/drive.d.ts.map +1 -0
- package/dist/tools/gmail.d.ts +3 -0
- package/dist/tools/gmail.d.ts.map +1 -0
- package/dist/tools/sheets.d.ts +3 -0
- package/dist/tools/sheets.d.ts.map +1 -0
- package/dist/tools/tasks.d.ts +3 -0
- package/dist/tools/tasks.d.ts.map +1 -0
- package/dist/tools/utils.d.ts +14 -0
- package/dist/tools/utils.d.ts.map +1 -0
- package/manifest.json +2 -10
- package/package.json +6 -2
- package/src/index.ts +2 -23
- package/src/lib.ts +5 -0
- package/src/runner.ts +4 -1
- package/src/server.ts +31 -0
- package/src/tools/docs.ts +10 -76
- package/tests/runner.test.ts +17 -0
- package/tests/tools/docs.test.ts +19 -163
- package/tsconfig.json +2 -8
- package/.github/workflows/ci.yml +0 -22
- package/.github/workflows/release.yml +0 -76
- package/.github/workflows/tag-and-bump.yml +0 -67
- package/.mcp.json +0 -14
- package/CLAUDE.md +0 -61
- package/docs/superpowers/plans/2026-04-12-gogcli-mcp.md +0 -758
- package/docs/superpowers/plans/2026-04-13-browser-auth.md +0 -450
- package/docs/superpowers/specs/2026-04-12-gogcli-mcp-design.md +0 -102
- package/docs/superpowers/specs/2026-04-13-browser-auth-design.md +0 -88
- package/gogcli-mcp-1.0.4.skill +0 -0
|
@@ -1,450 +0,0 @@
|
|
|
1
|
-
# Browser-Based Auth Flow Implementation Plan
|
|
2
|
-
|
|
3
|
-
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
-
|
|
5
|
-
**Goal:** Add a `gog_auth_add` MCP tool that opens the user's browser for Google OAuth and blocks until completion.
|
|
6
|
-
|
|
7
|
-
**Architecture:** Extend `RunOptions` with `interactive` and `timeout` fields. Add a dedicated `gog_auth_add` tool that uses these options to run the blocking browser auth flow. Existing runner behavior is unchanged for all other tools.
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** TypeScript, vitest, zod, @modelcontextprotocol/sdk
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
### Task 1: Runner — add `interactive` option (skip `--no-input`)
|
|
14
|
-
|
|
15
|
-
**Files:**
|
|
16
|
-
- Modify: `src/runner.ts:4-5` (RunOptions interface)
|
|
17
|
-
- Modify: `src/runner.ts:17-22` (run function)
|
|
18
|
-
- Test: `tests/runner.test.ts`
|
|
19
|
-
|
|
20
|
-
- [ ] **Step 1: Write the failing test for `interactive: true` omitting `--no-input`**
|
|
21
|
-
|
|
22
|
-
Add to `tests/runner.test.ts` inside the existing `describe('run', ...)`:
|
|
23
|
-
|
|
24
|
-
```ts
|
|
25
|
-
it('omits --no-input when interactive is true', async () => {
|
|
26
|
-
const spawner = makeSpawner(0, '{"ok":true}');
|
|
27
|
-
await run(['auth', 'add', 'user@gmail.com'], { spawner, interactive: true });
|
|
28
|
-
const callArgs = (spawner as ReturnType<typeof vi.fn>).mock.calls[0][1] as string[];
|
|
29
|
-
expect(callArgs).toContain('--json');
|
|
30
|
-
expect(callArgs).toContain('--color=never');
|
|
31
|
-
expect(callArgs).not.toContain('--no-input');
|
|
32
|
-
expect(callArgs).toContain('auth');
|
|
33
|
-
});
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
- [ ] **Step 2: Run test to verify it fails**
|
|
37
|
-
|
|
38
|
-
Run: `npm test -- --reporter=verbose -t "omits --no-input when interactive is true"`
|
|
39
|
-
Expected: FAIL — `interactive` is not a recognized option yet, `--no-input` is still present.
|
|
40
|
-
|
|
41
|
-
- [ ] **Step 3: Write the regression test for default (non-interactive) still including `--no-input`**
|
|
42
|
-
|
|
43
|
-
Add to `tests/runner.test.ts`:
|
|
44
|
-
|
|
45
|
-
```ts
|
|
46
|
-
it('includes --no-input when interactive is not set', async () => {
|
|
47
|
-
const spawner = makeSpawner(0, '{"ok":true}');
|
|
48
|
-
await run(['sheets', 'get', 'id1', 'A1'], { spawner });
|
|
49
|
-
const callArgs = (spawner as ReturnType<typeof vi.fn>).mock.calls[0][1] as string[];
|
|
50
|
-
expect(callArgs).toContain('--no-input');
|
|
51
|
-
});
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
- [ ] **Step 4: Implement `interactive` in runner**
|
|
55
|
-
|
|
56
|
-
In `src/runner.ts`, update `RunOptions`:
|
|
57
|
-
|
|
58
|
-
```ts
|
|
59
|
-
export interface RunOptions {
|
|
60
|
-
account?: string;
|
|
61
|
-
spawner?: Spawner;
|
|
62
|
-
interactive?: boolean;
|
|
63
|
-
timeout?: number;
|
|
64
|
-
}
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
In the `run()` function, change the `fullArgs` construction:
|
|
68
|
-
|
|
69
|
-
```ts
|
|
70
|
-
const { account, spawner = spawn as unknown as Spawner, interactive = false, timeout } = options;
|
|
71
|
-
|
|
72
|
-
const effectiveAccount = account ?? process.env.GOG_ACCOUNT;
|
|
73
|
-
|
|
74
|
-
const fullArgs = ['--json', '--color=never'];
|
|
75
|
-
if (!interactive) {
|
|
76
|
-
fullArgs.push('--no-input');
|
|
77
|
-
}
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
- [ ] **Step 5: Run tests to verify both pass**
|
|
81
|
-
|
|
82
|
-
Run: `npm test -- --reporter=verbose`
|
|
83
|
-
Expected: Both new tests PASS, all existing tests PASS.
|
|
84
|
-
|
|
85
|
-
- [ ] **Step 6: Commit**
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
git add src/runner.ts tests/runner.test.ts
|
|
89
|
-
git commit -m "feat(runner): add interactive option to skip --no-input"
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
---
|
|
93
|
-
|
|
94
|
-
### Task 2: Runner — add custom `timeout` option
|
|
95
|
-
|
|
96
|
-
**Files:**
|
|
97
|
-
- Modify: `src/runner.ts:17-18` (run function, timeout handling)
|
|
98
|
-
- Test: `tests/runner.test.ts`
|
|
99
|
-
|
|
100
|
-
- [ ] **Step 1: Write the failing test for custom timeout**
|
|
101
|
-
|
|
102
|
-
Add to `tests/runner.test.ts`:
|
|
103
|
-
|
|
104
|
-
```ts
|
|
105
|
-
it('uses custom timeout when provided', async () => {
|
|
106
|
-
vi.useFakeTimers();
|
|
107
|
-
const spawner = vi.fn(() => {
|
|
108
|
-
const proc = new EventEmitter() as ReturnType<Spawner>;
|
|
109
|
-
(proc as unknown as { stdout: EventEmitter; stderr: EventEmitter }).stdout = new EventEmitter();
|
|
110
|
-
(proc as unknown as { stdout: EventEmitter; stderr: EventEmitter }).stderr = new EventEmitter();
|
|
111
|
-
proc.kill = vi.fn();
|
|
112
|
-
return proc;
|
|
113
|
-
}) as unknown as Spawner;
|
|
114
|
-
|
|
115
|
-
const promise = run(['auth', 'add', 'user@gmail.com'], { spawner, timeout: 300_000 });
|
|
116
|
-
// Should NOT have timed out at 30s
|
|
117
|
-
vi.advanceTimersByTime(30_000);
|
|
118
|
-
// Advance to custom timeout
|
|
119
|
-
vi.advanceTimersByTime(270_000);
|
|
120
|
-
await expect(promise).rejects.toThrow('gog timed out after 300000ms (5 minutes)');
|
|
121
|
-
vi.useRealTimers();
|
|
122
|
-
});
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
- [ ] **Step 2: Write test that default timeout still works**
|
|
126
|
-
|
|
127
|
-
Add to `tests/runner.test.ts`:
|
|
128
|
-
|
|
129
|
-
```ts
|
|
130
|
-
it('includes human-readable duration in timeout error for default timeout', async () => {
|
|
131
|
-
vi.useFakeTimers();
|
|
132
|
-
const spawner = vi.fn(() => {
|
|
133
|
-
const proc = new EventEmitter() as ReturnType<Spawner>;
|
|
134
|
-
(proc as unknown as { stdout: EventEmitter; stderr: EventEmitter }).stdout = new EventEmitter();
|
|
135
|
-
(proc as unknown as { stdout: EventEmitter; stderr: EventEmitter }).stderr = new EventEmitter();
|
|
136
|
-
proc.kill = vi.fn();
|
|
137
|
-
return proc;
|
|
138
|
-
}) as unknown as Spawner;
|
|
139
|
-
|
|
140
|
-
const promise = run(['sheets', 'get', 'id', 'A1'], { spawner });
|
|
141
|
-
vi.advanceTimersByTime(30_000);
|
|
142
|
-
await expect(promise).rejects.toThrow('gog timed out after 30000ms');
|
|
143
|
-
vi.useRealTimers();
|
|
144
|
-
});
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
- [ ] **Step 3: Run tests to verify they fail**
|
|
148
|
-
|
|
149
|
-
Run: `npm test -- --reporter=verbose`
|
|
150
|
-
Expected: Custom timeout test FAIL (times out at 30s instead of 300s). Default timeout message test may also fail if message format changed.
|
|
151
|
-
|
|
152
|
-
- [ ] **Step 4: Implement custom timeout in runner**
|
|
153
|
-
|
|
154
|
-
In `src/runner.ts`, update the `run()` function:
|
|
155
|
-
|
|
156
|
-
```ts
|
|
157
|
-
const effectiveTimeout = timeout ?? TIMEOUT_MS;
|
|
158
|
-
|
|
159
|
-
// Helper for human-readable duration
|
|
160
|
-
function formatTimeout(ms: number): string {
|
|
161
|
-
const seconds = Math.round(ms / 1000);
|
|
162
|
-
if (seconds >= 60) {
|
|
163
|
-
const minutes = Math.round(seconds / 60);
|
|
164
|
-
return `${ms}ms (${minutes} minute${minutes !== 1 ? 's' : ''})`;
|
|
165
|
-
}
|
|
166
|
-
return `${ms}ms`;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const timer = setTimeout(() => {
|
|
170
|
-
settled = true;
|
|
171
|
-
child.kill();
|
|
172
|
-
reject(new Error(`gog timed out after ${formatTimeout(effectiveTimeout)}`));
|
|
173
|
-
}, effectiveTimeout);
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
Note: `formatTimeout` should be defined inside `run()` or as a module-level helper. Since it's small, define it at module level above the `run` function.
|
|
177
|
-
|
|
178
|
-
- [ ] **Step 5: Run tests to verify they pass**
|
|
179
|
-
|
|
180
|
-
Run: `npm test -- --reporter=verbose`
|
|
181
|
-
Expected: All tests PASS.
|
|
182
|
-
|
|
183
|
-
- [ ] **Step 6: Verify existing timeout test still passes**
|
|
184
|
-
|
|
185
|
-
The existing test `'rejects with timeout error when gog does not respond'` uses `.toThrow('gog timed out after 30000ms')` which is a substring match. The new message `"gog timed out after 30000ms"` still contains this substring (30s doesn't reach the 60s threshold for the minutes suffix). Verify it passes with no changes needed.
|
|
186
|
-
|
|
187
|
-
Run: `npm test -- --reporter=verbose -t "does not respond"`
|
|
188
|
-
Expected: PASS with no changes.
|
|
189
|
-
|
|
190
|
-
- [ ] **Step 7: Run full test suite**
|
|
191
|
-
|
|
192
|
-
Run: `npm test -- --reporter=verbose`
|
|
193
|
-
Expected: All tests PASS.
|
|
194
|
-
|
|
195
|
-
- [ ] **Step 8: Commit**
|
|
196
|
-
|
|
197
|
-
```bash
|
|
198
|
-
git add src/runner.ts tests/runner.test.ts
|
|
199
|
-
git commit -m "feat(runner): add custom timeout option with human-readable error"
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
---
|
|
203
|
-
|
|
204
|
-
### Task 3: Runner — append stderr on interactive success
|
|
205
|
-
|
|
206
|
-
**Files:**
|
|
207
|
-
- Modify: `src/runner.ts` (close handler)
|
|
208
|
-
- Test: `tests/runner.test.ts`
|
|
209
|
-
|
|
210
|
-
- [ ] **Step 1: Write the failing test for stderr appended on interactive success**
|
|
211
|
-
|
|
212
|
-
Add to `tests/runner.test.ts`:
|
|
213
|
-
|
|
214
|
-
```ts
|
|
215
|
-
it('appends stderr to stdout on success when interactive is true', async () => {
|
|
216
|
-
const spawner = vi.fn(() => {
|
|
217
|
-
const proc = new EventEmitter() as ReturnType<Spawner>;
|
|
218
|
-
(proc as unknown as { stdout: EventEmitter; stderr: EventEmitter }).stdout = new EventEmitter();
|
|
219
|
-
(proc as unknown as { stdout: EventEmitter; stderr: EventEmitter }).stderr = new EventEmitter();
|
|
220
|
-
setTimeout(() => {
|
|
221
|
-
(proc as unknown as { stdout: EventEmitter }).stdout.emit('data', Buffer.from('{"success":true}'));
|
|
222
|
-
(proc as unknown as { stderr: EventEmitter }).stderr.emit('data', Buffer.from('Opening browser...\nIf the browser doesn\'t open, visit this URL:\nhttps://accounts.google.com/auth?...'));
|
|
223
|
-
proc.emit('close', 0);
|
|
224
|
-
}, 0);
|
|
225
|
-
return proc;
|
|
226
|
-
}) as unknown as Spawner;
|
|
227
|
-
|
|
228
|
-
const result = await run(['auth', 'add', 'user@gmail.com'], { spawner, interactive: true });
|
|
229
|
-
expect(result).toContain('{"success":true}');
|
|
230
|
-
expect(result).toContain('Opening browser...');
|
|
231
|
-
expect(result).toContain('https://accounts.google.com/auth?...');
|
|
232
|
-
});
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
- [ ] **Step 2: Write test that non-interactive success does NOT include stderr**
|
|
236
|
-
|
|
237
|
-
Add to `tests/runner.test.ts`:
|
|
238
|
-
|
|
239
|
-
```ts
|
|
240
|
-
it('does not append stderr to stdout on success when interactive is false', async () => {
|
|
241
|
-
const spawner = vi.fn(() => {
|
|
242
|
-
const proc = new EventEmitter() as ReturnType<Spawner>;
|
|
243
|
-
(proc as unknown as { stdout: EventEmitter; stderr: EventEmitter }).stdout = new EventEmitter();
|
|
244
|
-
(proc as unknown as { stdout: EventEmitter; stderr: EventEmitter }).stderr = new EventEmitter();
|
|
245
|
-
setTimeout(() => {
|
|
246
|
-
(proc as unknown as { stdout: EventEmitter }).stdout.emit('data', Buffer.from('{"ok":true}'));
|
|
247
|
-
(proc as unknown as { stderr: EventEmitter }).stderr.emit('data', Buffer.from('some warning'));
|
|
248
|
-
proc.emit('close', 0);
|
|
249
|
-
}, 0);
|
|
250
|
-
return proc;
|
|
251
|
-
}) as unknown as Spawner;
|
|
252
|
-
|
|
253
|
-
const result = await run(['sheets', 'get', 'id', 'A1'], { spawner });
|
|
254
|
-
expect(result).toBe('{"ok":true}');
|
|
255
|
-
expect(result).not.toContain('some warning');
|
|
256
|
-
});
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
- [ ] **Step 3: Run tests to verify they fail**
|
|
260
|
-
|
|
261
|
-
Run: `npm test -- --reporter=verbose`
|
|
262
|
-
Expected: Interactive stderr test FAIL (stderr not included in result).
|
|
263
|
-
|
|
264
|
-
- [ ] **Step 4: Implement stderr append for interactive mode**
|
|
265
|
-
|
|
266
|
-
In `src/runner.ts`, update the close handler:
|
|
267
|
-
|
|
268
|
-
```ts
|
|
269
|
-
child.on('close', (code: number | null) => {
|
|
270
|
-
clearTimeout(timer);
|
|
271
|
-
if (settled) return;
|
|
272
|
-
settled = true;
|
|
273
|
-
if (code === 0) {
|
|
274
|
-
if (interactive && stderr.trim()) {
|
|
275
|
-
resolve(stdout + '\n' + stderr);
|
|
276
|
-
} else {
|
|
277
|
-
resolve(stdout);
|
|
278
|
-
}
|
|
279
|
-
} else {
|
|
280
|
-
reject(new Error(stderr.trim() || `gog exited with code ${code}`));
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
- [ ] **Step 5: Run tests to verify they pass**
|
|
286
|
-
|
|
287
|
-
Run: `npm test -- --reporter=verbose`
|
|
288
|
-
Expected: All tests PASS.
|
|
289
|
-
|
|
290
|
-
- [ ] **Step 6: Commit**
|
|
291
|
-
|
|
292
|
-
```bash
|
|
293
|
-
git add src/runner.ts tests/runner.test.ts
|
|
294
|
-
git commit -m "feat(runner): append stderr on interactive success for fallback URL"
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
---
|
|
298
|
-
|
|
299
|
-
### Task 4: Auth tool — add `gog_auth_add`
|
|
300
|
-
|
|
301
|
-
**Files:**
|
|
302
|
-
- Modify: `src/tools/auth.ts:6-56` (add new tool, update `gog_auth_run` description)
|
|
303
|
-
- Test: `tests/tools/auth.test.ts`
|
|
304
|
-
|
|
305
|
-
- [ ] **Step 1: Write the failing test for `gog_auth_add` with default services**
|
|
306
|
-
|
|
307
|
-
Add to `tests/tools/auth.test.ts`:
|
|
308
|
-
|
|
309
|
-
```ts
|
|
310
|
-
describe('gog_auth_add', () => {
|
|
311
|
-
it('calls run with correct args, interactive true, and 5-minute timeout', async () => {
|
|
312
|
-
vi.mocked(runner.run).mockResolvedValue('Authorization successful for user@gmail.com');
|
|
313
|
-
const handlers = setupHandlers();
|
|
314
|
-
const result = await handlers.get('gog_auth_add')!({ email: 'user@gmail.com' });
|
|
315
|
-
expect(runner.run).toHaveBeenCalledWith(
|
|
316
|
-
['auth', 'add', 'user@gmail.com', '--services', 'all'],
|
|
317
|
-
{ interactive: true, timeout: 300_000 },
|
|
318
|
-
);
|
|
319
|
-
expect(result.content[0].text).toBe('Authorization successful for user@gmail.com');
|
|
320
|
-
});
|
|
321
|
-
});
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
- [ ] **Step 2: Run test to verify it fails**
|
|
325
|
-
|
|
326
|
-
Run: `npm test -- --reporter=verbose -t "gog_auth_add"`
|
|
327
|
-
Expected: FAIL — handler `gog_auth_add` does not exist.
|
|
328
|
-
|
|
329
|
-
- [ ] **Step 3: Write additional tests for `gog_auth_add`**
|
|
330
|
-
|
|
331
|
-
Add inside the `describe('gog_auth_add', ...)`:
|
|
332
|
-
|
|
333
|
-
```ts
|
|
334
|
-
it('passes custom services when provided', async () => {
|
|
335
|
-
vi.mocked(runner.run).mockResolvedValue('Authorization successful');
|
|
336
|
-
const handlers = setupHandlers();
|
|
337
|
-
await handlers.get('gog_auth_add')!({ email: 'user@gmail.com', services: 'sheets,gmail' });
|
|
338
|
-
expect(runner.run).toHaveBeenCalledWith(
|
|
339
|
-
['auth', 'add', 'user@gmail.com', '--services', 'sheets,gmail'],
|
|
340
|
-
{ interactive: true, timeout: 300_000 },
|
|
341
|
-
);
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
it('returns error text on failure', async () => {
|
|
345
|
-
vi.mocked(runner.run).mockRejectedValue(new Error('Auth cancelled by user'));
|
|
346
|
-
const handlers = setupHandlers();
|
|
347
|
-
const result = await handlers.get('gog_auth_add')!({ email: 'user@gmail.com' });
|
|
348
|
-
expect(result.content[0].text).toBe('Error: Auth cancelled by user');
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
it('returns error text on timeout', async () => {
|
|
352
|
-
vi.mocked(runner.run).mockRejectedValue(new Error('gog timed out after 300000ms (5 minutes)'));
|
|
353
|
-
const handlers = setupHandlers();
|
|
354
|
-
const result = await handlers.get('gog_auth_add')!({ email: 'user@gmail.com' });
|
|
355
|
-
expect(result.content[0].text).toContain('timed out');
|
|
356
|
-
expect(result.content[0].text).toContain('5 minutes');
|
|
357
|
-
});
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
- [ ] **Step 4: Implement `gog_auth_add` tool**
|
|
361
|
-
|
|
362
|
-
Add to `src/tools/auth.ts`, before the `gog_auth_run` registration:
|
|
363
|
-
|
|
364
|
-
```ts
|
|
365
|
-
server.registerTool('gog_auth_add', {
|
|
366
|
-
description:
|
|
367
|
-
'Authorize a Google account via browser-based OAuth. ' +
|
|
368
|
-
'Opens a browser window where the user must sign in and grant access. ' +
|
|
369
|
-
'Blocks for up to 5 minutes waiting for the user to complete authorization. ' +
|
|
370
|
-
'If the browser does not open automatically, a fallback URL is included in the response. ' +
|
|
371
|
-
'Use gog_auth_list to check which accounts are already configured.',
|
|
372
|
-
annotations: { destructiveHint: true },
|
|
373
|
-
inputSchema: {
|
|
374
|
-
email: z.string().describe('Google account email to authorize'),
|
|
375
|
-
services: z.string().optional().default('all').describe(
|
|
376
|
-
'Services to authorize: "all" or comma-separated list (e.g. "sheets,gmail,calendar"). Default: "all"',
|
|
377
|
-
),
|
|
378
|
-
},
|
|
379
|
-
}, async ({ email, services }) => {
|
|
380
|
-
try {
|
|
381
|
-
return toText(await run(['auth', 'add', email, '--services', services], {
|
|
382
|
-
interactive: true,
|
|
383
|
-
timeout: 300_000,
|
|
384
|
-
}));
|
|
385
|
-
} catch (err) {
|
|
386
|
-
return toError(err);
|
|
387
|
-
}
|
|
388
|
-
});
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
- [ ] **Step 5: Update `gog_auth_run` description**
|
|
392
|
-
|
|
393
|
-
Change the `gog_auth_run` description from:
|
|
394
|
-
|
|
395
|
-
```ts
|
|
396
|
-
description: 'Run any gog auth subcommand. Run `gog auth --help` to see all available subcommands and flags. Note: gog auth add requires interactive browser auth and cannot be completed over MCP — run it in your terminal instead: gog auth add <email> --services <service>',
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
to:
|
|
400
|
-
|
|
401
|
-
```ts
|
|
402
|
-
description: 'Run any gog auth subcommand. Run `gog auth --help` to see all available subcommands and flags. Note: for browser-based authorization, use gog_auth_add instead.',
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
- [ ] **Step 6: Run all tests**
|
|
406
|
-
|
|
407
|
-
Run: `npm test -- --reporter=verbose`
|
|
408
|
-
Expected: All tests PASS.
|
|
409
|
-
|
|
410
|
-
- [ ] **Step 7: Commit**
|
|
411
|
-
|
|
412
|
-
```bash
|
|
413
|
-
git add src/tools/auth.ts tests/tools/auth.test.ts
|
|
414
|
-
git commit -m "feat(auth): add gog_auth_add tool for browser-based OAuth"
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
---
|
|
418
|
-
|
|
419
|
-
### Task 5: Manifest update
|
|
420
|
-
|
|
421
|
-
**Files:**
|
|
422
|
-
- Modify: `manifest.json`
|
|
423
|
-
|
|
424
|
-
- [ ] **Step 1: Add `gog_auth_add` to manifest tools list**
|
|
425
|
-
|
|
426
|
-
Add a new entry in the `tools` array in `manifest.json`, after the existing `gog_auth_list` entry:
|
|
427
|
-
|
|
428
|
-
```json
|
|
429
|
-
{
|
|
430
|
-
"name": "gog_auth_add",
|
|
431
|
-
"description": "Authorize a Google account via browser-based OAuth"
|
|
432
|
-
},
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
- [ ] **Step 2: Run build to verify no issues**
|
|
436
|
-
|
|
437
|
-
Run: `npm run build`
|
|
438
|
-
Expected: Build succeeds.
|
|
439
|
-
|
|
440
|
-
- [ ] **Step 3: Run full test suite**
|
|
441
|
-
|
|
442
|
-
Run: `npm test -- --reporter=verbose`
|
|
443
|
-
Expected: All tests PASS.
|
|
444
|
-
|
|
445
|
-
- [ ] **Step 4: Commit**
|
|
446
|
-
|
|
447
|
-
```bash
|
|
448
|
-
git add manifest.json
|
|
449
|
-
git commit -m "chore(manifest): add gog_auth_add tool"
|
|
450
|
-
```
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
# gogcli-mcp Design Spec
|
|
2
|
-
|
|
3
|
-
**Date:** 2026-04-12
|
|
4
|
-
**Status:** Approved
|
|
5
|
-
|
|
6
|
-
## Overview
|
|
7
|
-
|
|
8
|
-
An MCP server that wraps the `gog` CLI (gogcli), exposing Google service operations as typed MCP tools. Sheets is implemented first; the architecture is scaffolded for easy addition of other services (Gmail, Calendar, Drive, etc.).
|
|
9
|
-
|
|
10
|
-
## Architecture
|
|
11
|
-
|
|
12
|
-
### Project Structure
|
|
13
|
-
|
|
14
|
-
```
|
|
15
|
-
gogcli-mcp/
|
|
16
|
-
├── src/
|
|
17
|
-
│ ├── index.ts — MCP server entry point; registers all service tools
|
|
18
|
-
│ ├── runner.ts — Core executor: spawns gog, handles auth & errors
|
|
19
|
-
│ └── tools/
|
|
20
|
-
│ └── sheets.ts — Sheets curated tools + escape hatch
|
|
21
|
-
├── package.json
|
|
22
|
-
├── tsconfig.json
|
|
23
|
-
└── .mcp.json — MCP server config for Claude Code
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
### Key Principle
|
|
27
|
-
|
|
28
|
-
No service tool file touches `child_process` directly. All execution goes through `runner.ts`. Adding a new service means creating `src/tools/<service>.ts` and registering it in `index.ts`.
|
|
29
|
-
|
|
30
|
-
## Components
|
|
31
|
-
|
|
32
|
-
### `runner.ts`
|
|
33
|
-
|
|
34
|
-
Exports a single `run(service, subcommand, args, options)` function:
|
|
35
|
-
|
|
36
|
-
- Prepends global flags: `--json`, `--no-input`, `--color=never`
|
|
37
|
-
- Injects `--account <email>` from `options.account` → `GOG_ACCOUNT` env var → omit
|
|
38
|
-
- Spawns `gog` as a child process, capturing stdout and stderr separately
|
|
39
|
-
- On exit code 0: returns parsed JSON (or raw stdout if not valid JSON)
|
|
40
|
-
- On non-zero exit: throws an error with stderr content as the message (gogcli stderr is human-readable)
|
|
41
|
-
|
|
42
|
-
### `src/tools/sheets.ts`
|
|
43
|
-
|
|
44
|
-
Exports `registerSheetsTools(server: McpServer)`. Implements:
|
|
45
|
-
|
|
46
|
-
| Tool | gog command | Notes |
|
|
47
|
-
|------|-------------|-------|
|
|
48
|
-
| `gog_sheets_get` | `gog sheets get <id> <range>` | Read-only hint |
|
|
49
|
-
| `gog_sheets_update` | `gog sheets update <id> <range> <values>` | Values as JSON array of arrays |
|
|
50
|
-
| `gog_sheets_append` | `gog sheets append <id> <range> <values>` | Values as JSON array of arrays |
|
|
51
|
-
| `gog_sheets_clear` | `gog sheets clear <id> <range>` | Destructive hint |
|
|
52
|
-
| `gog_sheets_metadata` | `gog sheets metadata <id>` | Returns tabs, named ranges, etc. |
|
|
53
|
-
| `gog_sheets_create` | `gog sheets create <title>` | Returns new spreadsheet ID |
|
|
54
|
-
| `gog_sheets_find_replace` | `gog sheets find-replace <id> <find> <replace>` | |
|
|
55
|
-
| `gog_sheets_run` | `gog sheets <subcommand> [args...]` | Escape hatch for any other sheets op |
|
|
56
|
-
|
|
57
|
-
All tools accept an optional `account` string parameter to override the default account.
|
|
58
|
-
|
|
59
|
-
### `src/index.ts`
|
|
60
|
-
|
|
61
|
-
Creates the `McpServer`, calls `registerSheetsTools(server)` (and future service registrations), connects via `StdioServerTransport`.
|
|
62
|
-
|
|
63
|
-
### `.mcp.json`
|
|
64
|
-
|
|
65
|
-
Documents `GOG_ACCOUNT` as the environment variable for the default Google account. Used by Claude Code to configure the server.
|
|
66
|
-
|
|
67
|
-
## Auth Flow
|
|
68
|
-
|
|
69
|
-
Account resolution order (most specific wins):
|
|
70
|
-
|
|
71
|
-
1. Tool-level `account` parameter (per-call override)
|
|
72
|
-
2. `GOG_ACCOUNT` environment variable (session default)
|
|
73
|
-
3. No `--account` flag (gogcli uses its own configured default)
|
|
74
|
-
|
|
75
|
-
## Error Handling
|
|
76
|
-
|
|
77
|
-
- Runner captures stdout and stderr as separate streams
|
|
78
|
-
- Non-zero exit: throw `Error(stderr)` — gogcli error messages are already user-readable
|
|
79
|
-
- The MCP tool catches the error and returns it as text content so the model sees it
|
|
80
|
-
|
|
81
|
-
## Testing
|
|
82
|
-
|
|
83
|
-
- **Unit tests** (vitest): mock `child_process.spawn` to assert correct argument construction for each tool without executing gogcli
|
|
84
|
-
- **No integration tests**: real-world validation is done by running the server against live gogcli
|
|
85
|
-
|
|
86
|
-
## Adding Future Services
|
|
87
|
-
|
|
88
|
-
To add a new service (e.g., Gmail):
|
|
89
|
-
|
|
90
|
-
1. Create `src/tools/gmail.ts` with `registerGmailTools(server)`
|
|
91
|
-
2. Implement curated tools for common operations + a `gog_gmail_run` escape hatch
|
|
92
|
-
3. Import and call `registerGmailTools(server)` in `src/index.ts`
|
|
93
|
-
4. No changes to `runner.ts` required
|
|
94
|
-
|
|
95
|
-
## Stack
|
|
96
|
-
|
|
97
|
-
- **Language:** TypeScript (ES2022, NodeNext modules)
|
|
98
|
-
- **MCP SDK:** `@modelcontextprotocol/sdk`
|
|
99
|
-
- **Schema validation:** Zod
|
|
100
|
-
- **Build:** esbuild (single bundled output)
|
|
101
|
-
- **Tests:** vitest
|
|
102
|
-
- **Pattern:** Mirrors ofw-mcp structure
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
# Browser-Based Auth Flow for gogcli-mcp
|
|
2
|
-
|
|
3
|
-
## Summary
|
|
4
|
-
|
|
5
|
-
Add a `gog_auth_add` MCP tool that runs the default `gog auth add` browser flow, opening the user's browser for Google OAuth and blocking until completion. This gives users a one-click auth experience without leaving Claude.
|
|
6
|
-
|
|
7
|
-
## Runner Changes
|
|
8
|
-
|
|
9
|
-
### `RunOptions` additions (`src/runner.ts`)
|
|
10
|
-
|
|
11
|
-
Add two optional fields to the existing `RunOptions` interface:
|
|
12
|
-
|
|
13
|
-
```ts
|
|
14
|
-
export interface RunOptions {
|
|
15
|
-
account?: string;
|
|
16
|
-
spawner?: Spawner;
|
|
17
|
-
interactive?: boolean; // NEW: when true, omit --no-input
|
|
18
|
-
timeout?: number; // NEW: override default 30s timeout (ms)
|
|
19
|
-
}
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
### Behavior changes in `run()`
|
|
23
|
-
|
|
24
|
-
- When `interactive` is `true`, do NOT inject `--no-input` into `fullArgs`.
|
|
25
|
-
- When `timeout` is provided, use it instead of the `TIMEOUT_MS` constant.
|
|
26
|
-
- When `interactive` is `true` and the process exits successfully (code 0), append any captured stderr to the stdout result. This ensures the fallback URL ("If the browser doesn't open, visit this URL") reaches Claude even on success.
|
|
27
|
-
- When `timeout` is provided, the timeout error message includes the human-readable duration: e.g., `"gog timed out after 300000ms (5 minutes)"` so both the raw value and friendly form are visible.
|
|
28
|
-
|
|
29
|
-
### Default behavior
|
|
30
|
-
|
|
31
|
-
Unchanged. Existing calls without `interactive` or `timeout` behave exactly as before.
|
|
32
|
-
|
|
33
|
-
## New Tool: `gog_auth_add`
|
|
34
|
-
|
|
35
|
-
### Registration (`src/tools/auth.ts`)
|
|
36
|
-
|
|
37
|
-
- **Name:** `gog_auth_add`
|
|
38
|
-
- **Annotations:** `{ destructiveHint: true }`
|
|
39
|
-
- **Input schema:**
|
|
40
|
-
- `email` — required `z.string()`, Google account email to authorize
|
|
41
|
-
- `services` — optional `z.string()`, default `"all"`, comma-separated services or `"all"`
|
|
42
|
-
- **Handler:** Calls `run(['auth', 'add', email, '--services', services], { interactive: true, timeout: 300_000 })`
|
|
43
|
-
- **Error handling:** Standard `toError()` wrapping, same as other auth tools.
|
|
44
|
-
|
|
45
|
-
### Tool description
|
|
46
|
-
|
|
47
|
-
The description tells Claude:
|
|
48
|
-
|
|
49
|
-
1. This opens a browser window for Google OAuth
|
|
50
|
-
2. The user must complete authorization in the browser
|
|
51
|
-
3. The tool blocks for up to 5 minutes waiting for completion
|
|
52
|
-
4. If the browser doesn't open automatically, a fallback URL is included in the response
|
|
53
|
-
|
|
54
|
-
### `gog_auth_run` description update
|
|
55
|
-
|
|
56
|
-
Remove the "gog auth add requires interactive browser auth and cannot be completed over MCP" caveat. Replace with a note pointing to `gog_auth_add` for browser-based authorization.
|
|
57
|
-
|
|
58
|
-
## Manifest update
|
|
59
|
-
|
|
60
|
-
Add `gog_auth_add` to the `manifest.json` tools list.
|
|
61
|
-
|
|
62
|
-
## Error Handling
|
|
63
|
-
|
|
64
|
-
| Scenario | Behavior |
|
|
65
|
-
|---|---|
|
|
66
|
-
| User completes auth in browser | Tool returns success with gogcli output |
|
|
67
|
-
| User doesn't complete within 5 min | Process killed, error: "gog auth timed out after 5 minutes" |
|
|
68
|
-
| User cancels/denies in browser | gogcli exits non-zero, stderr returned as error |
|
|
69
|
-
| Browser doesn't open | Fallback URL included in response via stderr capture |
|
|
70
|
-
| Already authorized account | gogcli re-authorizes (updates scopes), no special handling |
|
|
71
|
-
|
|
72
|
-
## Testing
|
|
73
|
-
|
|
74
|
-
### Runner tests (`tests/runner.test.ts`)
|
|
75
|
-
|
|
76
|
-
- `interactive: true` omits `--no-input` from args
|
|
77
|
-
- Custom `timeout` is respected (process killed at custom time, not 30s)
|
|
78
|
-
- `interactive: true` on success appends stderr to stdout result
|
|
79
|
-
- `interactive: false` (or omitted) still includes `--no-input` (regression)
|
|
80
|
-
- Timeout error message includes human-readable duration
|
|
81
|
-
|
|
82
|
-
### Auth tool tests (`tests/tools/auth.test.ts`)
|
|
83
|
-
|
|
84
|
-
- `gog_auth_add` passes correct args to runner with `interactive: true` and `timeout: 300_000`
|
|
85
|
-
- `gog_auth_add` defaults services to `"all"` when omitted
|
|
86
|
-
- `gog_auth_add` passes custom services when provided
|
|
87
|
-
- `gog_auth_add` returns error text on failure
|
|
88
|
-
- `gog_auth_add` returns error text on timeout
|
package/gogcli-mcp-1.0.4.skill
DELETED
|
Binary file
|