gogcli-mcp 2.21.1 → 2.22.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 +119 -8
- package/dist/lib.js +119 -8
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/auth-log.ts +59 -1
- package/src/connector-auth.ts +265 -8
- package/src/connector-runtime.ts +186 -1
- package/src/google-probe.ts +113 -0
- package/src/timestamps.ts +7 -0
- package/src/tools/auth.ts +8 -2
- package/src/worker.ts +25 -6
- package/tests/auth-log.test.ts +24 -2
- package/tests/connector-auth.test.ts +539 -8
- package/tests/connector-runtime.test.ts +447 -1
- package/tests/google-probe.test.ts +116 -0
- package/tests/timestamps.test.ts +52 -0
- package/tests/tools/auth.test.ts +28 -0
- package/tests/worker.test.ts +33 -8
package/src/tools/auth.ts
CHANGED
|
@@ -31,7 +31,10 @@ function registerAuthToolsWith(server: McpServer, defaultServices: string): void
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
server.registerTool('gog_auth_status', {
|
|
34
|
-
description:
|
|
34
|
+
description:
|
|
35
|
+
'Show gogcli auth CONFIGURATION: keyring backend, credential files, and auth setup. Despite the ' +
|
|
36
|
+
'name this is not a health check — it reads local setup and does not contact Google, so it says ' +
|
|
37
|
+
'nothing about whether an account can still authenticate. Use gog_auth_health for that.',
|
|
35
38
|
annotations: { readOnlyHint: true },
|
|
36
39
|
inputSchema: {},
|
|
37
40
|
}, async () => {
|
|
@@ -50,7 +53,10 @@ function registerAuthToolsWith(server: McpServer, defaultServices: string): void
|
|
|
50
53
|
'service. Reports per account: whether the token is currently valid, the mapped cause when it is ' +
|
|
51
54
|
'not, how long ago it was authorized, and a warning as it approaches the 7-day refresh-token limit ' +
|
|
52
55
|
'that applies to OAuth apps whose consent screen is still in "Testing" mode. Run it proactively to ' +
|
|
53
|
-
're-authorize on your own schedule instead of mid-task.'
|
|
56
|
+
're-authorize on your own schedule instead of mid-task. On the hosted connector this is the ONLY ' +
|
|
57
|
+
'check that measures Google: a connector showing "connected" or "refreshed" has verified the ' +
|
|
58
|
+
'connector key that reaches the gog machine, and nothing else — the Google credential lives on ' +
|
|
59
|
+
'that machine and can be dead while the connection looks perfectly healthy.',
|
|
54
60
|
annotations: { readOnlyHint: true },
|
|
55
61
|
inputSchema: {},
|
|
56
62
|
}, async () => {
|
package/src/worker.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { registerExtraGmailTools } from '../../gogcli-mcp-gmail/src/tools/gmail-
|
|
|
16
16
|
import { registerExtraDriveTools } from '../../gogcli-mcp-drive/src/tools/drive-extra.js';
|
|
17
17
|
import { registerExtraDocsTools } from '../../gogcli-mcp-docs/src/tools/docs-extra.js';
|
|
18
18
|
import { makeFlyExecutor, wrapServer } from './connector-runtime.js';
|
|
19
|
-
import { gogAuth, type GogProps } from './connector-auth.js';
|
|
19
|
+
import { gogAuth, CONNECTOR_INSTRUCTIONS, type GogProps } from './connector-auth.js';
|
|
20
20
|
|
|
21
21
|
// The Cloudflare remote-connector entrypoint for gogcli-mcp.
|
|
22
22
|
//
|
|
@@ -38,7 +38,7 @@ import { gogAuth, type GogProps } from './connector-auth.js';
|
|
|
38
38
|
// connector with all ~360 tools at once. Add whichever paths you want as separate
|
|
39
39
|
// connectors in claude.ai (each authorizes with the same connector key).
|
|
40
40
|
|
|
41
|
-
const VERSION = '2.
|
|
41
|
+
const VERSION = '2.22.0'; // x-release-please-version
|
|
42
42
|
|
|
43
43
|
// Build an McpAgent subclass whose init() registers `registrars` onto its server,
|
|
44
44
|
// each handler wrapped in the ALS scope carrying the per-session Fly executor.
|
|
@@ -46,7 +46,14 @@ const VERSION = '2.21.1'; // x-release-please-version
|
|
|
46
46
|
// `agents` runtime; the node-testable helpers stay in connector-runtime.ts.)
|
|
47
47
|
function makeAgent(registrars: ToolRegistrar[]): typeof McpAgent {
|
|
48
48
|
class GogAgent extends McpAgent<unknown, unknown, GogProps> {
|
|
49
|
-
|
|
49
|
+
// `instructions` is the connector's only channel to the model that is not a
|
|
50
|
+
// tool description, and it carries the one thing the client UI gets wrong:
|
|
51
|
+
// "connected"/"refreshed" is a statement about the connector key, not about
|
|
52
|
+
// Google. See CONNECTOR_INSTRUCTIONS for why that has to be said out loud.
|
|
53
|
+
server = new McpServer(
|
|
54
|
+
{ name: 'gogcli-mcp', version: VERSION },
|
|
55
|
+
{ instructions: CONNECTOR_INSTRUCTIONS },
|
|
56
|
+
);
|
|
50
57
|
async init() {
|
|
51
58
|
// NO third argument, deliberately: the hosted connector supplies no
|
|
52
59
|
// per-caller access token, so `gog` runs as the Fly volume's own identity
|
|
@@ -56,9 +63,21 @@ function makeAgent(registrars: ToolRegistrar[]): typeof McpAgent {
|
|
|
56
63
|
// 401 on this path stops at the `no access token was supplied` guard and
|
|
57
64
|
// logs `replay.declined`. That record is the expected outcome for a
|
|
58
65
|
// hosted connector, not a bug; the transport-failure classification and
|
|
59
|
-
// the auth log itself do apply here.
|
|
60
|
-
//
|
|
61
|
-
//
|
|
66
|
+
// the auth log itself do apply here.
|
|
67
|
+
//
|
|
68
|
+
// Inert is not the same as unobserved. Because `gog` is spawned fresh per
|
|
69
|
+
// /run and re-reads the keyring each time, a Google 401 here means the
|
|
70
|
+
// STORED credential was refused — which no retry can repair, so no retry
|
|
71
|
+
// is built. Instead that same guard first takes one live reading of the
|
|
72
|
+
// Google layer (`GET /health/google` on the runner) and records it as
|
|
73
|
+
// `refusal.google-ok` / `-unhealthy` / `-unmeasured`. It is throttled,
|
|
74
|
+
// deadline-bounded, cannot throw, and leaves the caller's error
|
|
75
|
+
// byte-identical; its whole job is to answer, in the log, the question
|
|
76
|
+
// that could not be answered after the incident: at the moment Google
|
|
77
|
+
// refused, was the refresh token on the volume alive or dead?
|
|
78
|
+
// (docs/DEPLOY-CONNECTOR.md, "Reading the auth log" and "Why a hosted
|
|
79
|
+
// Google 401 is measured rather than retried", says this for whoever is
|
|
80
|
+
// reading logs rather than code.)
|
|
62
81
|
const executor = makeFlyExecutor((this.env as { FLY_ENDPOINT: string }).FLY_ENDPOINT, this.props.key);
|
|
63
82
|
const wrapped = wrapServer(this.server, executor);
|
|
64
83
|
for (const register of registrars) register(wrapped);
|
package/tests/auth-log.test.ts
CHANGED
|
@@ -129,6 +129,20 @@ describe('logAuthTransition', () => {
|
|
|
129
129
|
expect(log.emitted.map((e) => e.method)).toEqual(['warn', 'error']);
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
it('separates a MEASURED dead Google layer from one nobody could measure', () => {
|
|
133
|
+
// The connect-time probe has three honest answers, and conflating the last
|
|
134
|
+
// two is exactly the defect it exists to remove: "I asked Google and it said
|
|
135
|
+
// no" is a failure, while "I could not ask" is not evidence of anything.
|
|
136
|
+
const log = captureLog();
|
|
137
|
+
logAuthTransition('connect.google-ok', { endpoint: 'https://runner.example' });
|
|
138
|
+
logAuthTransition('connect.google-unhealthy', { endpoint: 'https://runner.example', reason: 'invalid_grant' });
|
|
139
|
+
logAuthTransition('connect.google-unmeasured', { endpoint: 'https://runner.example', reason: 'HTTP 404' });
|
|
140
|
+
|
|
141
|
+
expect(log.events()).toEqual(['connect.google-ok', 'connect.google-unhealthy', 'connect.google-unmeasured']);
|
|
142
|
+
expect(log.emitted.map((e) => e.method)).toEqual(['warn', 'error', 'warn']);
|
|
143
|
+
expect(log.toStdout).toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
|
|
132
146
|
it('omits absent context rather than writing nulls', () => {
|
|
133
147
|
const log = captureLog();
|
|
134
148
|
logAuthTransition('token.evicted', { credential: 'abc' });
|
|
@@ -370,8 +384,16 @@ describe('connector-runtime records the transitions the runner path authors', ()
|
|
|
370
384
|
vi.stubGlobal('fetch', vi.fn(async () => gogFailed(stderr)));
|
|
371
385
|
const log = captureLog();
|
|
372
386
|
await expect(invoke()).rejects.toThrow();
|
|
373
|
-
|
|
374
|
-
|
|
387
|
+
// The 'no token' case — the HOSTED shape — now writes a second record
|
|
388
|
+
// BEFORE its decision: the live reading of the Google layer this branch
|
|
389
|
+
// added, which here reports `refusal.google-unmeasured` because the stub
|
|
390
|
+
// answers /health/google with the same non-2xx it answers /run with. The
|
|
391
|
+
// decision is still the LAST word in every case, which is what this test
|
|
392
|
+
// is about.
|
|
393
|
+
const records = log.records();
|
|
394
|
+
expect(records.length, name).toBe(name === 'no token' ? 2 : 1);
|
|
395
|
+
if (name === 'no token') expect(records[0].event).toBe('refusal.google-unmeasured');
|
|
396
|
+
const record = records[records.length - 1];
|
|
375
397
|
expect(record.event, name).toBe(name === 'invalid_grant' ? 'grant.dead' : 'replay.declined');
|
|
376
398
|
expect(record.reason as string, name).toMatch(expected);
|
|
377
399
|
}
|