gogcli-mcp 2.8.0 → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +20218 -19700
- package/dist/lib.js +15845 -23331
- package/manifest.json +33 -1
- package/package.json +7 -6
- package/server.json +2 -2
- package/src/connector-auth.ts +46 -0
- package/src/connector-runtime.ts +177 -0
- package/src/index.ts +7 -5
- package/src/lib.ts +8 -7
- package/src/runner.ts +225 -30
- package/src/server.ts +21 -25
- package/src/tools/api.ts +65 -0
- package/src/tools/auth.ts +112 -15
- package/src/tools/calendar.ts +24 -9
- package/src/tools/docs.ts +28 -3
- package/src/tools/drive.ts +124 -1
- package/src/tools/gmail.ts +7 -3
- package/src/tools/sheets.ts +6 -3
- package/src/tools/slides.ts +9 -4
- package/src/tools/tasks.ts +3 -1
- package/src/tools/utils.ts +163 -27
- package/src/worker.ts +99 -0
- package/tests/connector-auth.test.ts +28 -0
- package/tests/connector-runtime.test.ts +474 -0
- package/tests/runner-file-args-failure.test.ts +94 -0
- package/tests/runner-file-args.test.ts +232 -0
- package/tests/runner.test.ts +221 -13
- package/tests/server.test.ts +28 -28
- package/tests/tools/api.test.ts +107 -0
- package/tests/tools/auth.test.ts +187 -31
- package/tests/tools/calendar.test.ts +115 -52
- package/tests/tools/classroom.test.ts +77 -77
- package/tests/tools/contacts.test.ts +24 -24
- package/tests/tools/docs.test.ts +84 -46
- package/tests/tools/drive.test.ts +226 -55
- package/tests/tools/gmail.test.ts +61 -28
- package/tests/tools/sheets.test.ts +81 -70
- package/tests/tools/slides.test.ts +56 -36
- package/tests/tools/tasks.test.ts +33 -33
- package/tests/tools/utils.test.ts +116 -2
- package/tests/worker.test.ts +142 -0
- package/tsconfig.json +4 -1
- package/vitest.config.ts +33 -2
- package/tests/helpers/test-harness.ts +0 -27
package/src/tools/utils.ts
CHANGED
|
@@ -1,8 +1,47 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { errorResult, rawTextResult } from '@chrischall/mcp-utils';
|
|
3
5
|
import { run } from '../runner.js';
|
|
6
|
+
import type { GogArg } from '../runner.js';
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
// Byte size at or below which a payload stays on the plain inline flag.
|
|
9
|
+
//
|
|
10
|
+
// Two reasons not to route everything through a file. First, gog's semantics
|
|
11
|
+
// differ between the two forms: reading a body from a file strips ALL trailing
|
|
12
|
+
// newlines (measured on gog 0.34.1 — a 5-byte payload padded to 6/7/8 bytes all
|
|
13
|
+
// came back as 5; tracked upstream at openclaw/gogcli#936), so a body ending in
|
|
14
|
+
// "\n" cannot round-trip byte-for-byte through the file path. Second, the file
|
|
15
|
+
// path costs a temp dir, a write, and a delete per call.
|
|
16
|
+
//
|
|
17
|
+
// The value matches the per-arg byte limit the Fly runner enforced BEFORE large
|
|
18
|
+
// payloads could leave argv (the old MAX_ARG_LEN, 4096). That is deliberate:
|
|
19
|
+
// every body that used to round-trip inline byte-for-byte still does, so this
|
|
20
|
+
// change adds no trailing-newline regression for any body that already worked —
|
|
21
|
+
// only bodies that previously exceeded the cap and hard-failed ("each arg must
|
|
22
|
+
// be at most 4096 chars") now take the file path and its newline trim. The
|
|
23
|
+
// runner's plain-arg cap is now 64 KiB, so a 4096-byte inline value is nowhere
|
|
24
|
+
// near being rejected.
|
|
25
|
+
export const PAYLOAD_INLINE_MAX = 4096;
|
|
26
|
+
|
|
27
|
+
// The ONE place the inline-vs-file decision is made. Every tool that has a
|
|
28
|
+
// gog `--x` / `--x-file` flag pair routes its value through here so the
|
|
29
|
+
// threshold cannot drift between tools.
|
|
30
|
+
//
|
|
31
|
+
// Measures BYTES, not characters: the Fly runner's cap and the Linux kernel's
|
|
32
|
+
// MAX_ARG_STRLEN are both byte-based, so a multibyte-heavy body (CJK, emoji)
|
|
33
|
+
// would slip past a `.length` check at up to 4x its real argv cost.
|
|
34
|
+
export function payloadArg(
|
|
35
|
+
inlineFlag: string,
|
|
36
|
+
fileFlag: string,
|
|
37
|
+
value: string,
|
|
38
|
+
ext?: string,
|
|
39
|
+
): GogArg {
|
|
40
|
+
if (Buffer.byteLength(value, 'utf8') <= PAYLOAD_INLINE_MAX) {
|
|
41
|
+
return `--${inlineFlag}=${value}`;
|
|
42
|
+
}
|
|
43
|
+
return { kind: 'file', flag: fileFlag, contents: value, ext };
|
|
44
|
+
}
|
|
6
45
|
|
|
7
46
|
export const accountParam = z.string().optional().describe(
|
|
8
47
|
'Google account email to use, e.g. you@gmail.com — must be the full address, not a bare username. ' +
|
|
@@ -88,16 +127,21 @@ export function registerRunTool(
|
|
|
88
127
|
});
|
|
89
128
|
}
|
|
90
129
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return toText(err instanceof Error ? `Error: ${err.message}` : String(err));
|
|
130
|
+
// The fleet-standard error text for a thrown value ("Error: <message>").
|
|
131
|
+
// Pair with mcp-utils errorResult (which redacts secrets and sets
|
|
132
|
+
// `isError: true`) when the text is the whole tool result.
|
|
133
|
+
export function errorText(err: unknown): string {
|
|
134
|
+
return err instanceof Error ? `Error: ${err.message}` : String(err);
|
|
97
135
|
}
|
|
98
136
|
|
|
99
137
|
const AUTH_ERROR_PATTERN = /\b(401|unauthorized|token.*(expired|revoked)|invalid_grant)\b/i;
|
|
100
138
|
|
|
139
|
+
// A DEAD refresh token — the whole account is signed out, not just a stale
|
|
140
|
+
// access token that would refresh silently. This is the recurring account-wide
|
|
141
|
+
// failure, so it gets its own plain-English cause + durable fix, distinct from
|
|
142
|
+
// the generic 401/unauthorized case.
|
|
143
|
+
const INVALID_GRANT_PATTERN = /invalid_grant|token has been expired or revoked/i;
|
|
144
|
+
|
|
101
145
|
const TRANSIENT_ERROR_PATTERN =
|
|
102
146
|
/\b429\b|\b5\d\d\b|\bquota\b|rateLimit|\bDEADLINE_EXCEEDED\b/i;
|
|
103
147
|
|
|
@@ -110,6 +154,20 @@ const AUTH_HINT =
|
|
|
110
154
|
'\n\nAuthentication may have expired. Use gog_auth_add to re-authorize the account. ' +
|
|
111
155
|
'Ask the user if they would like to re-authenticate.';
|
|
112
156
|
|
|
157
|
+
// invalid_grant means the stored REFRESH token was rejected, so re-auth is
|
|
158
|
+
// mandatory (no silent recovery) and it will recur unless the root cause is
|
|
159
|
+
// fixed. Name the most common cause (the 7-day limit Google puts on OAuth apps
|
|
160
|
+
// still in "Testing"), offer both re-auth paths (browser + remote/headless),
|
|
161
|
+
// and point at the durable fix. Keeps the literal `gog_auth_add` token so the
|
|
162
|
+
// generic auth-recovery guidance still applies.
|
|
163
|
+
const INVALID_GRANT_HINT =
|
|
164
|
+
'\n\nThe stored refresh token was rejected (invalid_grant): it has expired or been revoked, so the ' +
|
|
165
|
+
'whole account is signed out and re-authorization is required. The most common cause is the 7-day ' +
|
|
166
|
+
'refresh-token limit Google applies to OAuth apps whose consent screen is still in "Testing" mode. ' +
|
|
167
|
+
'Re-authorize with gog_auth_add (opens a browser) or gog_auth_add_url + gog_auth_add_complete (remote/headless). ' +
|
|
168
|
+
'To stop this recurring, publish the OAuth consent screen to "In production" in the Google Cloud project ' +
|
|
169
|
+
'that owns the OAuth client. Ask the user if they would like to re-authenticate.';
|
|
170
|
+
|
|
113
171
|
const TRANSIENT_HINT =
|
|
114
172
|
'\n\nThis error is often transient. Retry the same call before trying a different approach ' +
|
|
115
173
|
'(do not fall back to smaller writes or row-by-row operations).';
|
|
@@ -139,39 +197,117 @@ export function formatAccountList(raw: string): string {
|
|
|
139
197
|
return raw.trim();
|
|
140
198
|
}
|
|
141
199
|
|
|
142
|
-
// Turn a thrown error into a diagnosed
|
|
143
|
-
// actionable hint when the failure class is recognised (auth /
|
|
144
|
-
// off-grid write), and the list of configured accounts. Callers
|
|
145
|
-
// surface a failure without going through runOrDiagnose (e.g. a
|
|
146
|
-
// verification read that must abort) can reuse this so the error
|
|
147
|
-
// same diagnostic quality as everywhere else.
|
|
148
|
-
export async function diagnose(err: unknown): Promise<
|
|
149
|
-
const errText =
|
|
200
|
+
// Turn a thrown error into a diagnosed error result (`isError: true`): the
|
|
201
|
+
// error text, an actionable hint when the failure class is recognised (auth /
|
|
202
|
+
// transient / off-grid write), and the list of configured accounts. Callers
|
|
203
|
+
// that need to surface a failure without going through runOrDiagnose (e.g. a
|
|
204
|
+
// pre-write verification read that must abort) can reuse this so the error
|
|
205
|
+
// keeps the same diagnostic quality as everywhere else.
|
|
206
|
+
export async function diagnose(err: unknown): Promise<CallToolResult> {
|
|
207
|
+
const errText = errorText(err);
|
|
208
|
+
const isInvalidGrant = INVALID_GRANT_PATTERN.test(errText);
|
|
150
209
|
const isAuthError = AUTH_ERROR_PATTERN.test(errText);
|
|
151
210
|
const isTransientError = !isAuthError && TRANSIENT_ERROR_PATTERN.test(errText);
|
|
152
211
|
const isGridLimitError = GRID_LIMIT_ERROR_PATTERN.test(errText);
|
|
153
|
-
const hint =
|
|
154
|
-
?
|
|
155
|
-
:
|
|
156
|
-
?
|
|
157
|
-
:
|
|
158
|
-
?
|
|
159
|
-
:
|
|
212
|
+
const hint = isInvalidGrant
|
|
213
|
+
? INVALID_GRANT_HINT
|
|
214
|
+
: isAuthError
|
|
215
|
+
? AUTH_HINT
|
|
216
|
+
: isTransientError
|
|
217
|
+
? TRANSIENT_HINT
|
|
218
|
+
: isGridLimitError
|
|
219
|
+
? GRID_LIMIT_HINT
|
|
220
|
+
: '';
|
|
160
221
|
try {
|
|
161
222
|
const accounts = formatAccountList(await run(['auth', 'list']));
|
|
162
|
-
return
|
|
223
|
+
return errorResult(`${errText}\n\nConfigured accounts:\n${accounts || '(none)'}${hint}`);
|
|
163
224
|
} catch {
|
|
164
|
-
return
|
|
225
|
+
return errorResult(`${errText}${hint}`);
|
|
165
226
|
}
|
|
166
227
|
}
|
|
167
228
|
|
|
168
229
|
export async function runOrDiagnose(
|
|
169
|
-
args:
|
|
230
|
+
args: GogArg[],
|
|
170
231
|
options: { account?: string },
|
|
171
|
-
): Promise<
|
|
232
|
+
): Promise<CallToolResult> {
|
|
172
233
|
try {
|
|
173
|
-
return
|
|
234
|
+
return rawTextResult(await run(args, options));
|
|
174
235
|
} catch (err) {
|
|
175
236
|
return diagnose(err);
|
|
176
237
|
}
|
|
177
238
|
}
|
|
239
|
+
|
|
240
|
+
// Google puts a 7-day cap on refresh tokens issued by OAuth apps whose consent
|
|
241
|
+
// screen is still in "Testing" — the recurring account-wide sign-out this tool
|
|
242
|
+
// exists to warn about. Once the token is this old and still valid, expiry is
|
|
243
|
+
// imminent, so surface it before it becomes a hard failure mid-task.
|
|
244
|
+
const REFRESH_TOKEN_TESTING_TTL_DAYS = 7;
|
|
245
|
+
const HEALTH_WARN_AGE_DAYS = 6;
|
|
246
|
+
const MS_PER_DAY = 86_400_000;
|
|
247
|
+
|
|
248
|
+
interface AuthHealthAccount {
|
|
249
|
+
email?: string;
|
|
250
|
+
created_at?: string;
|
|
251
|
+
valid?: boolean;
|
|
252
|
+
error?: string;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Days between an ISO timestamp and `now`, or null if unparseable/absent.
|
|
256
|
+
function ageInDays(createdAt: string | undefined, now: number): number | null {
|
|
257
|
+
if (!createdAt) return null;
|
|
258
|
+
const t = Date.parse(createdAt);
|
|
259
|
+
if (Number.isNaN(t)) return null;
|
|
260
|
+
return (now - t) / MS_PER_DAY;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function formatOneAccountHealth(a: AuthHealthAccount, now: number): string {
|
|
264
|
+
const email = a.email ?? '(unknown account)';
|
|
265
|
+
const age = ageInDays(a.created_at, now);
|
|
266
|
+
const ageStr = age === null ? '' : ` Authorized ${age.toFixed(1)} day(s) ago.`;
|
|
267
|
+
|
|
268
|
+
if (a.valid === false) {
|
|
269
|
+
// Map the dead-refresh-token error to a plain cause; fall back to the raw
|
|
270
|
+
// (already token-redacted by run()) error for anything else.
|
|
271
|
+
const cause = INVALID_GRANT_PATTERN.test(a.error ?? '')
|
|
272
|
+
? 'refresh token expired or revoked — commonly the 7-day limit on OAuth consent screens still in "Testing" mode'
|
|
273
|
+
: (a.error?.trim() || 'unknown error');
|
|
274
|
+
return `✗ ${email}: NEEDS RE-AUTH — ${cause}.${ageStr} ` +
|
|
275
|
+
'Re-authorize with gog_auth_add (browser) or gog_auth_add_url + gog_auth_add_complete (remote/headless).';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (a.valid === true) {
|
|
279
|
+
let line = `✓ ${email}: token valid.${ageStr}`;
|
|
280
|
+
if (age !== null && age >= HEALTH_WARN_AGE_DAYS) {
|
|
281
|
+
const est = new Date(Date.parse(a.created_at as string) + REFRESH_TOKEN_TESTING_TTL_DAYS * MS_PER_DAY)
|
|
282
|
+
.toISOString()
|
|
283
|
+
.slice(0, 10);
|
|
284
|
+
line += ` ⚠ Approaching the 7-day refresh-token limit for OAuth apps in "Testing" mode ` +
|
|
285
|
+
`(if that applies, expect expiry around ${est}). Re-authorize soon, or publish the OAuth ` +
|
|
286
|
+
'consent screen to "In production" to stop the weekly expiry.';
|
|
287
|
+
}
|
|
288
|
+
return line;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// valid absent — the caller ran without --check (or gog omitted the field).
|
|
292
|
+
return `? ${email}: token validity unknown.${ageStr} Run this check again to probe it live.`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Turn `gog auth list --check --json` into a per-account health summary:
|
|
296
|
+
// validity, a mapped cause for dead tokens, token age, and a pre-expiry warning
|
|
297
|
+
// near the 7-day testing-mode cliff. Only email/created_at/valid/error are read
|
|
298
|
+
// — scopes/subject never appear in the summary. Falls back to trimmed raw text
|
|
299
|
+
// when the output isn't the expected JSON shape.
|
|
300
|
+
export function formatAuthHealth(raw: string, now: number): string {
|
|
301
|
+
let accounts: AuthHealthAccount[];
|
|
302
|
+
try {
|
|
303
|
+
const parsed = JSON.parse(raw) as { accounts?: unknown };
|
|
304
|
+
if (!Array.isArray(parsed?.accounts)) return raw.trim();
|
|
305
|
+
accounts = parsed.accounts as AuthHealthAccount[];
|
|
306
|
+
} catch {
|
|
307
|
+
return raw.trim();
|
|
308
|
+
}
|
|
309
|
+
if (accounts.length === 0) {
|
|
310
|
+
return 'No Google accounts are configured. Use gog_auth_add to authorize one.';
|
|
311
|
+
}
|
|
312
|
+
return accounts.map((a) => formatOneAccountHealth(a, now)).join('\n\n');
|
|
313
|
+
}
|
package/src/worker.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { McpAgent } from 'agents/mcp';
|
|
3
|
+
import { OAuthProvider } from '@cloudflare/workers-oauth-provider';
|
|
4
|
+
import { handleAuthorize } from '@chrischall/mcp-connector';
|
|
5
|
+
import type { ToolRegistrar } from '@chrischall/mcp-utils';
|
|
6
|
+
import {
|
|
7
|
+
BASE_TOOL_REGISTRARS,
|
|
8
|
+
authToolsFor,
|
|
9
|
+
registerSheetsTools,
|
|
10
|
+
registerGmailTools,
|
|
11
|
+
registerDriveTools,
|
|
12
|
+
registerDocsTools,
|
|
13
|
+
} from './lib.js';
|
|
14
|
+
import { registerExtraSheetsTools } from '../../gogcli-mcp-sheets/src/tools/sheets-extra.js';
|
|
15
|
+
import { registerExtraGmailTools } from '../../gogcli-mcp-gmail/src/tools/gmail-extra.js';
|
|
16
|
+
import { registerExtraDriveTools } from '../../gogcli-mcp-drive/src/tools/drive-extra.js';
|
|
17
|
+
import { registerExtraDocsTools } from '../../gogcli-mcp-docs/src/tools/docs-extra.js';
|
|
18
|
+
import { makeFlyExecutor, wrapServer } from './connector-runtime.js';
|
|
19
|
+
import { gogAuth, type GogProps } from './connector-auth.js';
|
|
20
|
+
|
|
21
|
+
// The Cloudflare remote-connector entrypoint for gogcli-mcp.
|
|
22
|
+
//
|
|
23
|
+
// It reuses the EXISTING transport-neutral tool registrars UNCHANGED and executes
|
|
24
|
+
// every assembled `gog` arg-array by forwarding it to a Fly.io backend (a Worker
|
|
25
|
+
// cannot spawn processes). The bridge is the `runExecutor` AsyncLocalStorage seam
|
|
26
|
+
// in `runner.ts`: `wrapServer` scopes each tool handler in `runExecutor.run(...)`
|
|
27
|
+
// so the handler's `run()` forwards to the per-session Fly executor.
|
|
28
|
+
//
|
|
29
|
+
// One Worker serves several MCP endpoints under one OAuth login, each a distinct
|
|
30
|
+
// tool set backed by its own Durable Object:
|
|
31
|
+
// /mcp all-services base (BASE_TOOL_REGISTRARS)
|
|
32
|
+
// /mcp/sheets auth + Sheets base + Sheets extras
|
|
33
|
+
// /mcp/gmail auth + Gmail base + Gmail extras
|
|
34
|
+
// /mcp/drive auth + Drive base + Drive extras
|
|
35
|
+
// /mcp/docs auth + Docs base + Docs extras
|
|
36
|
+
// Each per-service path exposes the SAME tool set as that sub-package's stdio
|
|
37
|
+
// server, so the ~50-70 extras per service are reachable without swamping one
|
|
38
|
+
// connector with all ~360 tools at once. Add whichever paths you want as separate
|
|
39
|
+
// connectors in claude.ai (each authorizes with the same connector key).
|
|
40
|
+
|
|
41
|
+
const VERSION = '2.18.0'; // x-release-please-version
|
|
42
|
+
|
|
43
|
+
// Build an McpAgent subclass whose init() registers `registrars` onto its server,
|
|
44
|
+
// each handler wrapped in the ALS scope carrying the per-session Fly executor.
|
|
45
|
+
// (Kept in worker.ts, not connector-runtime.ts, because it imports the Worker-only
|
|
46
|
+
// `agents` runtime; the node-testable helpers stay in connector-runtime.ts.)
|
|
47
|
+
function makeAgent(registrars: ToolRegistrar[]): typeof McpAgent {
|
|
48
|
+
class GogAgent extends McpAgent<unknown, unknown, GogProps> {
|
|
49
|
+
server = new McpServer({ name: 'gogcli-mcp', version: VERSION });
|
|
50
|
+
async init() {
|
|
51
|
+
const executor = makeFlyExecutor((this.env as { FLY_ENDPOINT: string }).FLY_ENDPOINT, this.props.key);
|
|
52
|
+
const wrapped = wrapServer(this.server, executor);
|
|
53
|
+
for (const register of registrars) register(wrapped);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return GogAgent as unknown as typeof McpAgent;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// auth + <service> base + <service> extras — the exact set each sub-package's
|
|
60
|
+
// stdio server exposes. The auth registrar is bound to THIS service's default
|
|
61
|
+
// `services` (least-privilege): a per-service connector re-auths requesting only
|
|
62
|
+
// its own scopes, so one unregistered scope for a service it doesn't wrap can't
|
|
63
|
+
// poison its re-auth with invalid_scope. The base /mcp agent keeps 'all'.
|
|
64
|
+
const svc = (service: string, base: ToolRegistrar, extra: ToolRegistrar): ToolRegistrar[] =>
|
|
65
|
+
[authToolsFor(service), base, extra];
|
|
66
|
+
|
|
67
|
+
export class GogcliMcpAgent extends makeAgent(BASE_TOOL_REGISTRARS) {}
|
|
68
|
+
export class GogcliSheetsAgent extends makeAgent(svc('sheets', registerSheetsTools, registerExtraSheetsTools)) {}
|
|
69
|
+
export class GogcliGmailAgent extends makeAgent(svc('gmail', registerGmailTools, registerExtraGmailTools)) {}
|
|
70
|
+
export class GogcliDriveAgent extends makeAgent(svc('drive,driveactivity,drivelabels', registerDriveTools, registerExtraDriveTools)) {}
|
|
71
|
+
export class GogcliDocsAgent extends makeAgent(svc('docs', registerDocsTools, registerExtraDocsTools)) {}
|
|
72
|
+
|
|
73
|
+
const defaultHandler = {
|
|
74
|
+
fetch(request: Request, env: unknown): Response | Promise<Response> {
|
|
75
|
+
const url = new URL(request.url);
|
|
76
|
+
if (url.pathname === '/authorize') return handleAuthorize(request, env, gogAuth);
|
|
77
|
+
return new Response('Not found', { status: 404 });
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// NOTE: OAuthProvider matches apiHandlers by PREFIX and returns the FIRST match,
|
|
82
|
+
// so the specific per-service paths MUST be listed before the base `/mcp`
|
|
83
|
+
// (otherwise `/mcp` greedily swallows `/mcp/sheets`).
|
|
84
|
+
const handler = new OAuthProvider({
|
|
85
|
+
apiHandlers: {
|
|
86
|
+
'/mcp/sheets': GogcliSheetsAgent.serve('/mcp/sheets', { binding: 'SHEETS_MCP' }) as never,
|
|
87
|
+
'/mcp/gmail': GogcliGmailAgent.serve('/mcp/gmail', { binding: 'GMAIL_MCP' }) as never,
|
|
88
|
+
'/mcp/drive': GogcliDriveAgent.serve('/mcp/drive', { binding: 'DRIVE_MCP' }) as never,
|
|
89
|
+
'/mcp/docs': GogcliDocsAgent.serve('/mcp/docs', { binding: 'DOCS_MCP' }) as never,
|
|
90
|
+
'/mcp': GogcliMcpAgent.serve('/mcp') as never,
|
|
91
|
+
'/sse': GogcliMcpAgent.serveSSE('/sse') as never,
|
|
92
|
+
},
|
|
93
|
+
defaultHandler: defaultHandler as never,
|
|
94
|
+
authorizeEndpoint: '/authorize',
|
|
95
|
+
tokenEndpoint: '/token',
|
|
96
|
+
clientRegistrationEndpoint: '/register',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
export default handler;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { gogAuth } from '../src/connector-auth.js';
|
|
3
|
+
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
vi.unstubAllGlobals();
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
describe('gogAuth.login', () => {
|
|
9
|
+
const env = { FLY_ENDPOINT: 'https://runner.example' };
|
|
10
|
+
|
|
11
|
+
it('verifies the key against the backend /health and returns the props', async () => {
|
|
12
|
+
const fetchMock = vi.fn(async () => ({ ok: true }));
|
|
13
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
14
|
+
|
|
15
|
+
const props = await gogAuth.login({ key: 'my-key' }, env);
|
|
16
|
+
expect(props).toEqual({ key: 'my-key' });
|
|
17
|
+
expect(fetchMock).toHaveBeenCalledWith('https://runner.example/health', {
|
|
18
|
+
headers: { Authorization: 'Bearer my-key' },
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('throws when the backend rejects the key', async () => {
|
|
23
|
+
vi.stubGlobal('fetch', vi.fn(async () => ({ ok: false })));
|
|
24
|
+
await expect(gogAuth.login({ key: 'bad' }, env)).rejects.toThrow(
|
|
25
|
+
'Invalid connector key',
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
});
|