mcp-google-multi 6.0.0-alpha.66 → 6.0.0-alpha.68
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/dist/http-transport.d.ts +1 -0
- package/dist/http-transport.js +17 -4
- package/dist/mcp-token.d.ts +20 -0
- package/dist/mcp-token.js +53 -3
- package/dist/oauth-as.d.ts +4 -0
- package/dist/oauth-as.js +3 -3
- package/dist/token-store.js +0 -0
- package/dist/usage-metrics.d.ts +21 -4
- package/dist/usage-metrics.js +43 -31
- package/package.json +3 -1
package/dist/http-transport.d.ts
CHANGED
package/dist/http-transport.js
CHANGED
|
@@ -70,6 +70,10 @@ const MAX_GRACE_MS = 2 ** 31 - 1;
|
|
|
70
70
|
function endAfterFlush(socket) {
|
|
71
71
|
if (socket.destroyed || socket.writableEnded)
|
|
72
72
|
return;
|
|
73
|
+
// Node arms a keep-alive timer when a response finishes and destroys the
|
|
74
|
+
// socket when it fires, which would cut a slow reader's unread tail just as
|
|
75
|
+
// an early destroy does. Only the client's FIN or the grace ends it now.
|
|
76
|
+
socket.setTimeout(0);
|
|
73
77
|
socket.end();
|
|
74
78
|
}
|
|
75
79
|
// A request that arrives while closing is never answered, so Node keeps it
|
|
@@ -109,6 +113,9 @@ export class HttpTransportHost {
|
|
|
109
113
|
// every handle() that has not settled.
|
|
110
114
|
sockets = new Set();
|
|
111
115
|
pending = new Map();
|
|
116
|
+
// Sockets that have accepted a request: nothing pending means the last
|
|
117
|
+
// response finished writing, but its tail can still sit in kernel buffers.
|
|
118
|
+
served = new WeakSet();
|
|
112
119
|
handling = new Set();
|
|
113
120
|
closing = false;
|
|
114
121
|
graceful;
|
|
@@ -143,6 +150,7 @@ export class HttpTransportHost {
|
|
|
143
150
|
stopReading(socket);
|
|
144
151
|
return;
|
|
145
152
|
}
|
|
153
|
+
this.served.add(socket);
|
|
146
154
|
const entry = this.pending.get(socket);
|
|
147
155
|
if (entry) {
|
|
148
156
|
entry.count++;
|
|
@@ -232,7 +240,8 @@ export class HttpTransportHost {
|
|
|
232
240
|
this.httpServer = undefined;
|
|
233
241
|
return;
|
|
234
242
|
}
|
|
235
|
-
// Starting over would
|
|
243
|
+
// Starting over would arm a second grace, and a shorter one would cut
|
|
244
|
+
// what the first still lets finish.
|
|
236
245
|
this.graceful ??= this.closeGracefully(s, graceMs).finally(() => (this.graceful = undefined));
|
|
237
246
|
return this.graceful;
|
|
238
247
|
}
|
|
@@ -249,15 +258,19 @@ export class HttpTransportHost {
|
|
|
249
258
|
// after the last of them: that one says so if it has not started, and the
|
|
250
259
|
// settle hook in start() ends the socket once it finishes. Node itself
|
|
251
260
|
// destroys a socket once a Connection: close response is flushed, which
|
|
252
|
-
// resets it just as endAfterFlush explains, so that end lingers too.
|
|
261
|
+
// resets it just as endAfterFlush explains, so that end lingers too. A
|
|
262
|
+
// socket with nothing pending may still be delivering the tail of its
|
|
263
|
+
// last response, so only one that never accepted a request is destroyed.
|
|
253
264
|
for (const socket of this.sockets) {
|
|
254
265
|
const entry = this.pending.get(socket);
|
|
255
|
-
if (!entry) {
|
|
266
|
+
if (!entry && !this.served.has(socket)) {
|
|
256
267
|
socket.destroy();
|
|
257
268
|
continue;
|
|
258
269
|
}
|
|
259
270
|
socket.destroySoon = () => endAfterFlush(socket);
|
|
260
|
-
if (!entry
|
|
271
|
+
if (!entry)
|
|
272
|
+
endAfterFlush(socket);
|
|
273
|
+
else if (!entry.last.headersSent)
|
|
261
274
|
entry.last.setHeader('Connection', 'close');
|
|
262
275
|
}
|
|
263
276
|
let timer;
|
package/dist/mcp-token.d.ts
CHANGED
|
@@ -123,6 +123,14 @@ export interface RefreshStoreOptions {
|
|
|
123
123
|
maxAgeSec?: number;
|
|
124
124
|
/** Idle lifetime in seconds, counted from the last issue or rotation. Unset: unlimited. */
|
|
125
125
|
idleSec?: number;
|
|
126
|
+
/** Max live refresh-token families per subject (both formats). Unset:
|
|
127
|
+
* unlimited (default; the single-owner server passes none). When issuing a
|
|
128
|
+
* new family would put a subject over this, that subject's own
|
|
129
|
+
* least-recently-used family is dropped (by its last issue or rotation
|
|
130
|
+
* time) until it fits; a dropped family behaves exactly like a revoked
|
|
131
|
+
* one. Only the subject signing in ever triggers it, and only its own
|
|
132
|
+
* families are ever touched. */
|
|
133
|
+
maxFamiliesPerSub?: number;
|
|
126
134
|
/** Server-side log for a revoked, dropped or expired family. Never given a token or a subject. */
|
|
127
135
|
log?: (line: string) => void;
|
|
128
136
|
}
|
|
@@ -141,6 +149,7 @@ export declare class RefreshStore {
|
|
|
141
149
|
private readonly log;
|
|
142
150
|
private readonly maxAgeMs;
|
|
143
151
|
private readonly idleMs;
|
|
152
|
+
private readonly maxFamiliesPerSub;
|
|
144
153
|
private macKey;
|
|
145
154
|
private legacyEmpty;
|
|
146
155
|
constructor(path: string, masterKey: string, options?: RefreshStoreOptions);
|
|
@@ -156,6 +165,17 @@ export declare class RefreshStore {
|
|
|
156
165
|
private load;
|
|
157
166
|
private save;
|
|
158
167
|
private emit;
|
|
168
|
+
/** Evict `sub`'s own least-recently-used sessions (both formats) so at
|
|
169
|
+
* most `limit` of its live sessions remain; dropping one here is the same
|
|
170
|
+
* as deleting it from `data.families` / `data.active` on a revoke, so it
|
|
171
|
+
* can never be presented again (reuse detection for every OTHER family is
|
|
172
|
+
* untouched). Only `sub`'s own entries are ever inspected.
|
|
173
|
+
*
|
|
174
|
+
* `keepKey` is the family just issued: it is excluded from the ranking so
|
|
175
|
+
* a clock step back, or a concurrent rotate's `usedAt = max(usedAt, nowMs)`
|
|
176
|
+
* racing ahead of it under the lock, can never make the brand-new family
|
|
177
|
+
* look like the least recently used one and drop itself. */
|
|
178
|
+
private enforceFamilyCap;
|
|
159
179
|
private newFamilyId;
|
|
160
180
|
issue(nowMs: number, sub: string): string;
|
|
161
181
|
/** Rotate a presented refresh token; null if it is unknown, malformed, or
|
package/dist/mcp-token.js
CHANGED
|
@@ -363,12 +363,16 @@ export function assertRefreshStoreReadable(file, masterKey) {
|
|
|
363
363
|
}
|
|
364
364
|
checkFormat(file, d);
|
|
365
365
|
}
|
|
366
|
-
function
|
|
366
|
+
function positiveInt(name, v) {
|
|
367
367
|
if (v === undefined)
|
|
368
368
|
return undefined;
|
|
369
369
|
if (typeof v !== 'number' || !Number.isSafeInteger(v) || v <= 0)
|
|
370
370
|
throw new Error(`E_REFRESH_OPTION_INVALID: ${name} must be a positive whole number`);
|
|
371
|
-
return v
|
|
371
|
+
return v;
|
|
372
|
+
}
|
|
373
|
+
function lifetimeMs(name, v) {
|
|
374
|
+
const n = positiveInt(name, v);
|
|
375
|
+
return n === undefined ? undefined : n * 1000;
|
|
372
376
|
}
|
|
373
377
|
/**
|
|
374
378
|
* Persisted (encrypted under MASTER_KEY) refresh-token store. Rotates on every
|
|
@@ -385,6 +389,7 @@ export class RefreshStore {
|
|
|
385
389
|
log;
|
|
386
390
|
maxAgeMs;
|
|
387
391
|
idleMs;
|
|
392
|
+
maxFamiliesPerSub;
|
|
388
393
|
macKey;
|
|
389
394
|
// Set once a load finds no earlier-release record. Nothing here adds one,
|
|
390
395
|
// so from then on a legacy-shaped token is refused before the lock.
|
|
@@ -394,6 +399,7 @@ export class RefreshStore {
|
|
|
394
399
|
this.masterKey = masterKey;
|
|
395
400
|
this.maxAgeMs = lifetimeMs('maxAgeSec', options.maxAgeSec);
|
|
396
401
|
this.idleMs = lifetimeMs('idleSec', options.idleSec);
|
|
402
|
+
this.maxFamiliesPerSub = positiveInt('maxFamiliesPerSub', options.maxFamiliesPerSub);
|
|
397
403
|
this.log = options.log ?? (() => undefined);
|
|
398
404
|
}
|
|
399
405
|
get timed() {
|
|
@@ -502,6 +508,47 @@ export class RefreshStore {
|
|
|
502
508
|
}
|
|
503
509
|
}
|
|
504
510
|
}
|
|
511
|
+
/** Evict `sub`'s own least-recently-used sessions (both formats) so at
|
|
512
|
+
* most `limit` of its live sessions remain; dropping one here is the same
|
|
513
|
+
* as deleting it from `data.families` / `data.active` on a revoke, so it
|
|
514
|
+
* can never be presented again (reuse detection for every OTHER family is
|
|
515
|
+
* untouched). Only `sub`'s own entries are ever inspected.
|
|
516
|
+
*
|
|
517
|
+
* `keepKey` is the family just issued: it is excluded from the ranking so
|
|
518
|
+
* a clock step back, or a concurrent rotate's `usedAt = max(usedAt, nowMs)`
|
|
519
|
+
* racing ahead of it under the lock, can never make the brand-new family
|
|
520
|
+
* look like the least recently used one and drop itself. */
|
|
521
|
+
enforceFamilyCap(data, sub, limit, lines, keepKey) {
|
|
522
|
+
const entries = [];
|
|
523
|
+
for (const [k, f] of Object.entries(data.families)) {
|
|
524
|
+
if (f.sub !== sub || k === keepKey)
|
|
525
|
+
continue;
|
|
526
|
+
entries.push({
|
|
527
|
+
at: f.usedAt,
|
|
528
|
+
drop: () => {
|
|
529
|
+
delete data.families[k];
|
|
530
|
+
lines.push(`refresh family dropped: cap tag=${familyTag(Buffer.from(k, 'base64url'))}`);
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
for (const [t, r] of Object.entries(data.active)) {
|
|
535
|
+
if (r.sub !== sub)
|
|
536
|
+
continue;
|
|
537
|
+
entries.push({
|
|
538
|
+
at: r.issuedAt,
|
|
539
|
+
drop: () => {
|
|
540
|
+
delete data.active[t];
|
|
541
|
+
lines.push(`refresh family dropped: cap tag=${familyTag(r.family)}`);
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
// +1 accounts for the kept family, which stays live but is never ranked.
|
|
546
|
+
if (entries.length + 1 <= limit)
|
|
547
|
+
return;
|
|
548
|
+
entries.sort((a, b) => a.at - b.at);
|
|
549
|
+
for (const e of entries.slice(0, entries.length + 1 - limit))
|
|
550
|
+
e.drop();
|
|
551
|
+
}
|
|
505
552
|
newFamilyId(data) {
|
|
506
553
|
let fid;
|
|
507
554
|
do
|
|
@@ -517,8 +564,11 @@ export class RefreshStore {
|
|
|
517
564
|
const lines = [];
|
|
518
565
|
const { data } = this.load(nowMs, lines);
|
|
519
566
|
const fid = this.newFamilyId(data);
|
|
567
|
+
const fidKey = fid.toString('base64url');
|
|
520
568
|
const { token, cur } = mintR1(key, fid, 0);
|
|
521
|
-
data.families[
|
|
569
|
+
data.families[fidKey] = { sub, gen: 0, cur, createdAt: nowMs, usedAt: nowMs };
|
|
570
|
+
if (this.maxFamiliesPerSub !== undefined)
|
|
571
|
+
this.enforceFamilyCap(data, sub, this.maxFamiliesPerSub, lines, fidKey);
|
|
522
572
|
this.save(data);
|
|
523
573
|
this.emit(lines);
|
|
524
574
|
return token;
|
package/dist/oauth-as.d.ts
CHANGED
|
@@ -26,6 +26,10 @@ export interface AuthServerConfig {
|
|
|
26
26
|
/** `idleSec` of that store: sessions end this long after their last
|
|
27
27
|
* refresh. Unset: unlimited. */
|
|
28
28
|
refreshIdleSec?: number;
|
|
29
|
+
/** `maxFamiliesPerSub` of that store: bounds how many live refresh-token
|
|
30
|
+
* families one subject may hold, dropping its least-recently-used one to
|
|
31
|
+
* make room. Unset: unlimited. */
|
|
32
|
+
refreshMaxFamiliesPerSub?: number;
|
|
29
33
|
/** Default true. The clientless `flow=alias_reauth` link names an alias with
|
|
30
34
|
* no tenant binding, so a multi-tenant deployment sets false: with several
|
|
31
35
|
* tenants an alias name alone is ambiguous and the branch becomes a
|
package/dist/oauth-as.js
CHANGED
|
@@ -200,10 +200,10 @@ export function buildAuthServer(config, deps = {}) {
|
|
|
200
200
|
const now = deps.now ?? (() => Date.now());
|
|
201
201
|
const log = deps.log ?? (() => undefined);
|
|
202
202
|
// Silently dropping a lifetime the caller asked for would leave sessions unlimited.
|
|
203
|
-
if (deps.refreshStore && (config.refreshMaxAgeSec !== undefined || config.refreshIdleSec !== undefined)) {
|
|
204
|
-
throw new Error('E_REFRESH_OPTION_CONFLICT: refreshMaxAgeSec and
|
|
203
|
+
if (deps.refreshStore && (config.refreshMaxAgeSec !== undefined || config.refreshIdleSec !== undefined || config.refreshMaxFamiliesPerSub !== undefined)) {
|
|
204
|
+
throw new Error('E_REFRESH_OPTION_CONFLICT: refreshMaxAgeSec, refreshIdleSec and refreshMaxFamiliesPerSub configure the refresh store buildAuthServer creates; set them on the injected refreshStore instead');
|
|
205
205
|
}
|
|
206
|
-
const refresh = deps.refreshStore ?? new RefreshStore(config.refreshStorePath, config.masterKey, { maxAgeSec: config.refreshMaxAgeSec, idleSec: config.refreshIdleSec, log });
|
|
206
|
+
const refresh = deps.refreshStore ?? new RefreshStore(config.refreshStorePath, config.masterKey, { maxAgeSec: config.refreshMaxAgeSec, idleSec: config.refreshIdleSec, maxFamiliesPerSub: config.refreshMaxFamiliesPerSub, log });
|
|
207
207
|
const nowSec = () => Math.floor(now() / 1000);
|
|
208
208
|
// A full guard is refused at whatever rate the caller likes, so its line is
|
|
209
209
|
// written at most once a minute (or at once after the clock stepped back).
|
package/dist/token-store.js
CHANGED
|
Binary file
|
package/dist/usage-metrics.d.ts
CHANGED
|
@@ -86,11 +86,26 @@ export interface InitOptions {
|
|
|
86
86
|
now?: () => number;
|
|
87
87
|
monotonic?: () => number;
|
|
88
88
|
log?: (line: string) => void;
|
|
89
|
-
/** Default true.
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
89
|
+
/** Default true. Three chains correlate CONSECUTIVE calls, which is only
|
|
90
|
+
* meaningful when one caller owns the whole process: the retry and bigram
|
|
91
|
+
* chains, and pendingMid (the escape hatch's resolved methodId, attached to
|
|
92
|
+
* the next google_api_call event's `mid`). A host serving several callers
|
|
93
|
+
* sets false so none of the three ever cross-link a call between tenants.
|
|
94
|
+
* DayAgg shape is unchanged (retries/bigrams stay empty maps);
|
|
95
|
+
* escape.methods still counts every resolved call either way. */
|
|
93
96
|
sequencingEnabled?: boolean;
|
|
97
|
+
/** Default true. When false, no events.jsonl / events.1.jsonl is ever
|
|
98
|
+
* created or appended to; only agg/ exists. A host serving several callers
|
|
99
|
+
* sets false: each event line carries a minute timestamp, the boot id and a
|
|
100
|
+
* per-process sequence number, which can be joined against a per-caller log
|
|
101
|
+
* such as an audit trail; the day aggregate keeps the same counters without
|
|
102
|
+
* order. */
|
|
103
|
+
eventLog?: boolean;
|
|
104
|
+
/** Default none. A host's own closed slug set, unioned with
|
|
105
|
+
* KNOWN_ERROR_SLUGS for this instance only; each entry must match
|
|
106
|
+
* /^[A-Za-z][A-Za-z0-9_]{0,63}$/ or it is dropped silently. Absent,
|
|
107
|
+
* behavior is byte-identical to today. */
|
|
108
|
+
extraErrorSlugs?: readonly string[];
|
|
94
109
|
}
|
|
95
110
|
/** Deep-add b into a for the DayAgg shape (numbers add, maps union). */
|
|
96
111
|
export declare function mergeDay(a: DayAgg, b: DayAgg): DayAgg;
|
|
@@ -114,6 +129,8 @@ export declare class Metrics {
|
|
|
114
129
|
private failures;
|
|
115
130
|
private disabled;
|
|
116
131
|
private readonly sequencing;
|
|
132
|
+
private readonly eventLog;
|
|
133
|
+
private readonly errorSlugs;
|
|
117
134
|
constructor(opts: InitOptions);
|
|
118
135
|
private day;
|
|
119
136
|
private initStorage;
|
package/dist/usage-metrics.js
CHANGED
|
@@ -33,6 +33,7 @@ const TOOL_NAME_RE = /^[a-z][a-z0-9_]{0,63}$/;
|
|
|
33
33
|
// Declared argument keys are camelCase identifiers, some dotted
|
|
34
34
|
// (groupKey.id, debugOptions.enableDebugging).
|
|
35
35
|
const ARG_KEY_RE = /^[A-Za-z][A-Za-z0-9_.]{0,63}$/;
|
|
36
|
+
const EXTRA_ERROR_SLUG_RE = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
|
|
36
37
|
/** Union of every `error:` slug literal emitted anywhere in src/ (kept honest
|
|
37
38
|
* by a set-equality grep test). Anything outside buckets to `other`. */
|
|
38
39
|
export const KNOWN_ERROR_SLUGS = new Set([
|
|
@@ -222,8 +223,13 @@ export class Metrics {
|
|
|
222
223
|
failures = 0;
|
|
223
224
|
disabled = false;
|
|
224
225
|
sequencing;
|
|
226
|
+
eventLog;
|
|
227
|
+
errorSlugs;
|
|
225
228
|
constructor(opts) {
|
|
226
229
|
this.sequencing = opts.sequencingEnabled !== false;
|
|
230
|
+
this.eventLog = opts.eventLog !== false;
|
|
231
|
+
const extras = (opts.extraErrorSlugs ?? []).filter((s) => EXTRA_ERROR_SLUG_RE.test(s));
|
|
232
|
+
this.errorSlugs = extras.length ? new Set([...KNOWN_ERROR_SLUGS, ...extras]) : KNOWN_ERROR_SLUGS;
|
|
227
233
|
this.dir = opts.dir ?? path.join(stateDir(opts.env), METRICS_DIR_NAME);
|
|
228
234
|
this.aggDir = path.join(this.dir, 'agg');
|
|
229
235
|
this.eventsFile = path.join(this.dir, 'events.jsonl');
|
|
@@ -252,7 +258,8 @@ export class Metrics {
|
|
|
252
258
|
}
|
|
253
259
|
}
|
|
254
260
|
this.pruneAggs();
|
|
255
|
-
|
|
261
|
+
if (this.eventLog)
|
|
262
|
+
this.pruneEventAge(this.eventsFile);
|
|
256
263
|
}
|
|
257
264
|
catch (e) {
|
|
258
265
|
this.log(`local usage metrics: init failed (${e.message}); disabled for this process\n`);
|
|
@@ -320,7 +327,7 @@ export class Metrics {
|
|
|
320
327
|
if (typeof first === 'string' && first.length < 65_536) {
|
|
321
328
|
const env = JSON.parse(first);
|
|
322
329
|
if (typeof env.error === 'string')
|
|
323
|
-
slug =
|
|
330
|
+
slug = this.errorSlugs.has(env.error) ? env.error : 'other';
|
|
324
331
|
hinted = typeof env.hint === 'string' && env.hint.length > 0;
|
|
325
332
|
}
|
|
326
333
|
}
|
|
@@ -379,29 +386,31 @@ export class Metrics {
|
|
|
379
386
|
}
|
|
380
387
|
this.lastTool = { name: entry.name, at: nowM };
|
|
381
388
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
389
|
+
if (this.eventLog) {
|
|
390
|
+
const event = {
|
|
391
|
+
ts: new Date(this.now()).toISOString().slice(0, 16) + ':00Z',
|
|
392
|
+
boot: this.bootId,
|
|
393
|
+
seq: this.seq++,
|
|
394
|
+
tool: entry.name,
|
|
395
|
+
ok,
|
|
396
|
+
ms: Math.round(ms),
|
|
397
|
+
chars: charsBucket(chars),
|
|
398
|
+
src: 'handler',
|
|
399
|
+
};
|
|
400
|
+
if (slug)
|
|
401
|
+
event.err = slug;
|
|
402
|
+
if (hinted !== undefined)
|
|
403
|
+
event.hint = hinted;
|
|
404
|
+
if (fan)
|
|
405
|
+
event.fan = fan;
|
|
406
|
+
// mid: set mid-dispatch by recordEscapeMethod, attached to the escape
|
|
407
|
+
// call's own event line (per-event field, never a synthetic event).
|
|
408
|
+
if (this.sequencing && entry.name === 'google_api_call' && this.pendingMid) {
|
|
409
|
+
event.mid = this.pendingMid;
|
|
410
|
+
this.pendingMid = undefined;
|
|
411
|
+
}
|
|
412
|
+
this.events.push(JSON.stringify(event));
|
|
403
413
|
}
|
|
404
|
-
this.events.push(JSON.stringify(event));
|
|
405
414
|
this.dirty = true;
|
|
406
415
|
}
|
|
407
416
|
/** Escape hatch: only the RESOLVED methodId (double-gated) or a miss. */
|
|
@@ -413,7 +422,8 @@ export class Metrics {
|
|
|
413
422
|
}
|
|
414
423
|
else if (idOrNull.length <= METHOD_ID_MAX && METHOD_ID_RE.test(idOrNull)) {
|
|
415
424
|
addInto(this.deltas.escape.methods, idOrNull);
|
|
416
|
-
this.
|
|
425
|
+
if (this.sequencing)
|
|
426
|
+
this.pendingMid = idOrNull;
|
|
417
427
|
}
|
|
418
428
|
this.dirty = true;
|
|
419
429
|
}
|
|
@@ -481,12 +491,14 @@ export class Metrics {
|
|
|
481
491
|
else {
|
|
482
492
|
addInto(this.deltas.rpc, RPC_CODES.has(kind) ? `rpc_error_${kind}` : 'rpc_error_other');
|
|
483
493
|
}
|
|
484
|
-
this.
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
494
|
+
if (this.eventLog) {
|
|
495
|
+
this.events.push(JSON.stringify({
|
|
496
|
+
ts: new Date(this.now()).toISOString().slice(0, 16) + ':00Z',
|
|
497
|
+
boot: this.bootId, seq: this.seq++,
|
|
498
|
+
tool: tool && TOOL_NAME_RE.test(tool) ? tool : 'other',
|
|
499
|
+
ok: false, ms: 0, chars: 'le64', src: 'rpc',
|
|
500
|
+
}));
|
|
501
|
+
}
|
|
490
502
|
this.dirty = true;
|
|
491
503
|
}
|
|
492
504
|
catch { /* never throws outward */ }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-google-multi",
|
|
3
|
-
"version": "6.0.0-alpha.
|
|
3
|
+
"version": "6.0.0-alpha.68",
|
|
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",
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
"./write-control": "./dist/write-control.js",
|
|
31
31
|
"./scope-catalog": "./dist/scope-catalog.js",
|
|
32
32
|
"./auth": "./dist/auth.js",
|
|
33
|
+
"./usage-metrics": "./dist/usage-metrics.js",
|
|
34
|
+
"./metrics-tap": "./dist/metrics-tap.js",
|
|
33
35
|
"./package.json": "./package.json"
|
|
34
36
|
},
|
|
35
37
|
"files": [
|