mcp-google-multi 6.0.0-alpha.25 → 6.0.0-alpha.26
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/arg-normalize.d.ts +2 -2
- package/dist/arg-normalize.js +7 -3
- package/dist/doctor.d.ts +3 -0
- package/dist/doctor.js +19 -1
- package/dist/http-transport.d.ts +5 -0
- package/dist/http-transport.js +2 -1
- package/dist/index.js +21 -4
- package/dist/metrics-tap.d.ts +3 -0
- package/dist/metrics-tap.js +62 -0
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +4 -0
- package/dist/tools/google-api.js +6 -0
- package/dist/usage-metrics.d.ts +8 -1
- package/dist/usage-metrics.js +33 -4
- package/package.json +1 -1
package/dist/arg-normalize.d.ts
CHANGED
|
@@ -11,9 +11,9 @@ export declare function normalizeCallArguments(shape: ArgShape, args: Record<str
|
|
|
11
11
|
args: Record<string, unknown>;
|
|
12
12
|
renamed: [string, string][];
|
|
13
13
|
};
|
|
14
|
-
export declare function normalizeMessage(msg: JSONRPCMessage, shapeFor: (tool: string) => ArgShape | undefined, log?: (line: string) => void): JSONRPCMessage;
|
|
14
|
+
export declare function normalizeMessage(msg: JSONRPCMessage, shapeFor: (tool: string) => ArgShape | undefined, log?: (line: string) => void, onRename?: (tool: string, renames: number) => void): JSONRPCMessage;
|
|
15
15
|
/** Wrap a server-side transport so tools/call argument keys are normalized
|
|
16
16
|
* before the SDK validates them. The Protocol assigns `onmessage` during
|
|
17
17
|
* connect(); the interceptor lives in that setter, so the wrapper works
|
|
18
18
|
* identically for stdio and (per-request, stateless) HTTP transports. */
|
|
19
|
-
export declare function withArgNormalization(transport: Transport, shapeFor: (tool: string) => ArgShape | undefined, log?: (line: string) => void): Transport;
|
|
19
|
+
export declare function withArgNormalization(transport: Transport, shapeFor: (tool: string) => ArgShape | undefined, log?: (line: string) => void, onRename?: (tool: string, renames: number) => void): Transport;
|
package/dist/arg-normalize.js
CHANGED
|
@@ -36,7 +36,7 @@ export function normalizeCallArguments(shape, args) {
|
|
|
36
36
|
}
|
|
37
37
|
return { args: out ?? args, renamed };
|
|
38
38
|
}
|
|
39
|
-
export function normalizeMessage(msg, shapeFor, log = (l) => process.stderr.write(`${l}\n`)) {
|
|
39
|
+
export function normalizeMessage(msg, shapeFor, log = (l) => process.stderr.write(`${l}\n`), onRename) {
|
|
40
40
|
const m = msg;
|
|
41
41
|
if (m.method !== 'tools/call' || typeof m.params?.name !== 'string')
|
|
42
42
|
return msg;
|
|
@@ -51,6 +51,10 @@ export function normalizeMessage(msg, shapeFor, log = (l) => process.stderr.writ
|
|
|
51
51
|
return msg;
|
|
52
52
|
// Key names only — argument VALUES never reach the log.
|
|
53
53
|
log(`[args] ${m.params.name}: ${renamed.map(([f, t]) => `${f} -> ${t}`).join(', ')}`);
|
|
54
|
+
try {
|
|
55
|
+
onRename?.(m.params.name, renamed.length);
|
|
56
|
+
}
|
|
57
|
+
catch { /* observers never break dispatch */ }
|
|
54
58
|
return {
|
|
55
59
|
...msg,
|
|
56
60
|
params: { ...m.params, arguments: normalized },
|
|
@@ -60,7 +64,7 @@ export function normalizeMessage(msg, shapeFor, log = (l) => process.stderr.writ
|
|
|
60
64
|
* before the SDK validates them. The Protocol assigns `onmessage` during
|
|
61
65
|
* connect(); the interceptor lives in that setter, so the wrapper works
|
|
62
66
|
* identically for stdio and (per-request, stateless) HTTP transports. */
|
|
63
|
-
export function withArgNormalization(transport, shapeFor, log) {
|
|
67
|
+
export function withArgNormalization(transport, shapeFor, log, onRename) {
|
|
64
68
|
const wrapper = {
|
|
65
69
|
start: () => transport.start(),
|
|
66
70
|
send: (message, options) => transport.send(message, options),
|
|
@@ -70,7 +74,7 @@ export function withArgNormalization(transport, shapeFor, log) {
|
|
|
70
74
|
get: () => transport.onmessage,
|
|
71
75
|
set: (handler) => {
|
|
72
76
|
transport.onmessage = handler
|
|
73
|
-
? (message, extra) => handler(normalizeMessage(message, shapeFor, log), extra)
|
|
77
|
+
? (message, extra) => handler(normalizeMessage(message, shapeFor, log, onRename), extra)
|
|
74
78
|
: undefined;
|
|
75
79
|
},
|
|
76
80
|
});
|
package/dist/doctor.d.ts
CHANGED
|
@@ -53,6 +53,9 @@ export interface HttpProbeResult {
|
|
|
53
53
|
}
|
|
54
54
|
/** Console deep-link to enable one API (section-6 hint, error taxonomy B10). */
|
|
55
55
|
export declare function apiEnableLink(api: string): string;
|
|
56
|
+
/** One line, state AND source, so the people being measured can see both
|
|
57
|
+
* here and in `diagnose` (metrics spec section 2). Read-only. */
|
|
58
|
+
export declare function usageMetricsStatusLine(env: Record<string, string | undefined>): string;
|
|
56
59
|
/** Roll section verdicts to an overall verdict. `unknown` never worsens it. */
|
|
57
60
|
export declare function overallVerdict(sections: DiagnosticSection[]): Verdict;
|
|
58
61
|
export declare function runDiagnostics(deps?: DiagnosticsDeps): Promise<DiagnosticsReport>;
|
package/dist/doctor.js
CHANGED
|
@@ -5,7 +5,9 @@ import { getAccountSet } from './accounts.js';
|
|
|
5
5
|
import { deriveAccountHealth } from './tools/accounts-tool.js';
|
|
6
6
|
import { peekMasterKeyProvenance, deleteMasterKeyMaterial } from './master-key.js';
|
|
7
7
|
import { hasToken } from './token-store.js';
|
|
8
|
-
import { configDir } from './config-file.js';
|
|
8
|
+
import { configDir, loadConfigFile } from './config-file.js';
|
|
9
|
+
import { envValueSource } from './env-load.js';
|
|
10
|
+
import { describeMetricsDir, resolveUsageMetrics, sourceLabel } from './usage-metrics.js';
|
|
9
11
|
import { probeApiEnablement } from './api-probe.js';
|
|
10
12
|
import { resolveHttpConfig, HttpConfigError } from './http-config.js';
|
|
11
13
|
import { parseOwnerEmails } from './http-transport.js';
|
|
@@ -114,8 +116,24 @@ function sectionConfig(deps, set) {
|
|
|
114
116
|
hint = (hint ? `${hint} ` : '') + `Move it: \`mv ${envFile} ${target}\`.`;
|
|
115
117
|
}
|
|
116
118
|
}
|
|
119
|
+
lines.push(usageMetricsStatusLine(deps.env));
|
|
117
120
|
return { id: 2, title: 'Config', verdict, lines, ...(hint ? { hint } : {}), ...(slug ? { slug } : {}) };
|
|
118
121
|
}
|
|
122
|
+
/** One line, state AND source, so the people being measured can see both
|
|
123
|
+
* here and in `diagnose` (metrics spec section 2). Read-only. */
|
|
124
|
+
export function usageMetricsStatusLine(env) {
|
|
125
|
+
const envSrc = envValueSource('GOOGLE_USAGE_METRICS');
|
|
126
|
+
let configValue;
|
|
127
|
+
try {
|
|
128
|
+
configValue = loadConfigFile(undefined, 'throw')?.usageMetrics;
|
|
129
|
+
}
|
|
130
|
+
catch { /* invalid config is section 2's business, not this line's */ }
|
|
131
|
+
const state = resolveUsageMetrics(env, configValue, envSrc?.kind === 'file' ? envSrc.file : undefined);
|
|
132
|
+
if (!state.enabled)
|
|
133
|
+
return `local usage metrics: off ${sourceLabel(state.source)}`;
|
|
134
|
+
const d = describeMetricsDir(env);
|
|
135
|
+
return `local usage metrics: on ${sourceLabel(state.source)} -> ${d.dir} (${d.files} files, ${d.kb} KB)`;
|
|
136
|
+
}
|
|
119
137
|
function sectionKeys(deps, aliases) {
|
|
120
138
|
const provenance = deps.masterKeyProvenance();
|
|
121
139
|
const tokensExist = deps.anyTokensExist(aliases);
|
package/dist/http-transport.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
|
2
2
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import type { HttpConfig } from './http-config.js';
|
|
4
4
|
import { type ArgShape } from './arg-normalize.js';
|
|
5
|
+
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
5
6
|
export type AuthOutcome = {
|
|
6
7
|
ok: true;
|
|
7
8
|
} | {
|
|
@@ -31,6 +32,10 @@ export interface HttpHostOptions {
|
|
|
31
32
|
dispatchTimeoutMs?: number;
|
|
32
33
|
/** tools/call argument-key normalization lookup (arg-normalize.ts); absent = off. */
|
|
33
34
|
argShapeFor?: (tool: string) => ArgShape | undefined;
|
|
35
|
+
/** Usage-metrics transport tap (metrics-tap.ts); absent = off. */
|
|
36
|
+
metricsTap?: (t: Transport) => Transport;
|
|
37
|
+
/** Usage-metrics argfix observer, forwarded into arg normalization. */
|
|
38
|
+
onArgRename?: (tool: string, renames: number) => void;
|
|
34
39
|
}
|
|
35
40
|
export declare function parseOwnerEmails(env?: NodeJS.ProcessEnv): string[];
|
|
36
41
|
/** Front guard: an Origin, if present, must be allowlisted; a Host must be
|
package/dist/http-transport.js
CHANGED
|
@@ -183,7 +183,8 @@ export class HttpTransportHost {
|
|
|
183
183
|
timer = setTimeout(() => resolve('timeout'), deadlineMs);
|
|
184
184
|
timer.unref?.();
|
|
185
185
|
});
|
|
186
|
-
|
|
186
|
+
const tapped = this.opts.metricsTap ? this.opts.metricsTap(transport) : transport;
|
|
187
|
+
await this.opts.server.connect(this.opts.argShapeFor ? withArgNormalization(tapped, this.opts.argShapeFor, this.opts.log, this.opts.onArgRename) : tapped);
|
|
187
188
|
// Reflect the dispatch into a non-rejecting arm: if the deadline wins the
|
|
188
189
|
// race, an orphaned handler settling later must not surface as an unhandled
|
|
189
190
|
// rejection — but a genuine dispatch error still propagates (rethrown below).
|
package/dist/index.js
CHANGED
|
@@ -222,11 +222,20 @@ async function main() {
|
|
|
222
222
|
// never rebuilt per request); `both` runs the two concurrently.
|
|
223
223
|
if (wantStdio) {
|
|
224
224
|
const server = new McpServer({ name: 'mcp-google-multi', version: pkg.version });
|
|
225
|
-
const
|
|
225
|
+
const stdioMetrics = initMetricsFor('stdio', resolveDiscoveryMode());
|
|
226
|
+
const registry = buildRegistry(server, buildIdentityContext(process.env, { transport: 'stdio' }), undefined, stdioMetrics);
|
|
226
227
|
registry.installListHandler();
|
|
227
228
|
registerSetupPrompt(server);
|
|
228
|
-
|
|
229
|
-
|
|
229
|
+
// Tap sits under arg normalization at the same wrap site; it only counts
|
|
230
|
+
// outbound JSON-RPC error frames (no handler ran) plus id->tool names.
|
|
231
|
+
let transport = new StdioServerTransport();
|
|
232
|
+
if (stdioMetrics) {
|
|
233
|
+
const { tapUsageMetrics } = await import('./metrics-tap.js');
|
|
234
|
+
transport = tapUsageMetrics(transport, stdioMetrics);
|
|
235
|
+
}
|
|
236
|
+
await server.connect(argNormalizationEnabled()
|
|
237
|
+
? withArgNormalization(transport, (n) => registry.argShape(n), undefined, stdioMetrics ? (tool, n) => stdioMetrics.recordArgFix(tool, n) : undefined)
|
|
238
|
+
: transport);
|
|
230
239
|
}
|
|
231
240
|
if (wantHttp) {
|
|
232
241
|
const { HttpTransportHost, parseOwnerEmails } = await import('./http-transport.js');
|
|
@@ -243,7 +252,8 @@ async function main() {
|
|
|
243
252
|
process.stderr.write(`GOOGLE_DISCOVERY="${configuredMode}" is ignored over HTTP; the stateless transport forces "curated".\n`);
|
|
244
253
|
}
|
|
245
254
|
const httpServer = new McpServer({ name: 'mcp-google-multi', version: pkg.version });
|
|
246
|
-
const
|
|
255
|
+
const httpMetrics = initMetricsFor('http', 'curated');
|
|
256
|
+
const registry = buildRegistry(httpServer, buildIdentityContext(process.env, { transport: 'http' }), 'curated', httpMetrics);
|
|
247
257
|
registry.installListHandler();
|
|
248
258
|
registerSetupPrompt(httpServer);
|
|
249
259
|
// B13: mount the OAuth 2.1 AS (legs A + B) + the Bearer authenticator.
|
|
@@ -309,6 +319,11 @@ async function main() {
|
|
|
309
319
|
// re-auth link into the AS's alias_reauth flow instead of a stdio CLI hint.
|
|
310
320
|
const { setHttpReauthBase } = await import('./reauth-hint.js');
|
|
311
321
|
setHttpReauthBase(httpCfg.publicUrl);
|
|
322
|
+
let httpTap;
|
|
323
|
+
if (httpMetrics) {
|
|
324
|
+
const { tapUsageMetrics } = await import('./metrics-tap.js');
|
|
325
|
+
httpTap = (t) => tapUsageMetrics(t, httpMetrics);
|
|
326
|
+
}
|
|
312
327
|
const host = new HttpTransportHost({
|
|
313
328
|
server: httpServer,
|
|
314
329
|
config: httpCfg,
|
|
@@ -318,6 +333,8 @@ async function main() {
|
|
|
318
333
|
routes: authServer.routes,
|
|
319
334
|
log: (l) => process.stderr.write(`[http] ${l}\n`),
|
|
320
335
|
argShapeFor: argNormalizationEnabled() ? (n) => registry.argShape(n) : undefined,
|
|
336
|
+
metricsTap: httpTap,
|
|
337
|
+
onArgRename: httpMetrics ? (tool, n) => httpMetrics.recordArgFix(tool, n) : undefined,
|
|
321
338
|
});
|
|
322
339
|
await host.start();
|
|
323
340
|
process.stderr.write(`HTTP transport listening on http://${httpCfg.host}:${httpCfg.port} (public ${httpCfg.publicUrl})\n`);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const PENDING_CAP = 1_000;
|
|
2
|
+
export function tapUsageMetrics(transport, metrics) {
|
|
3
|
+
const pending = new Map();
|
|
4
|
+
const wrapper = {
|
|
5
|
+
start: () => transport.start(),
|
|
6
|
+
send: (message, options) => {
|
|
7
|
+
try {
|
|
8
|
+
const m = message;
|
|
9
|
+
if (m.id !== undefined) {
|
|
10
|
+
const tool = pending.get(m.id);
|
|
11
|
+
pending.delete(m.id);
|
|
12
|
+
if (m.error && typeof m.error.code === 'number') {
|
|
13
|
+
if (m.error.code === -32602) {
|
|
14
|
+
if (/unknown tool|not found/i.test(m.error.message ?? ''))
|
|
15
|
+
metrics.recordRpc('tool_not_found');
|
|
16
|
+
else
|
|
17
|
+
metrics.recordRpc('schema_validation', tool);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
metrics.recordRpc(m.error.code);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch { /* the tap may never break the wire */ }
|
|
26
|
+
return transport.send(message, options);
|
|
27
|
+
},
|
|
28
|
+
close: () => transport.close(),
|
|
29
|
+
};
|
|
30
|
+
Object.defineProperty(wrapper, 'onmessage', {
|
|
31
|
+
get: () => transport.onmessage,
|
|
32
|
+
set: (handler) => {
|
|
33
|
+
transport.onmessage = handler
|
|
34
|
+
? (message, extra) => {
|
|
35
|
+
try {
|
|
36
|
+
const m = message;
|
|
37
|
+
if (m.id !== undefined && m.method === 'tools/call' && typeof m.params?.name === 'string') {
|
|
38
|
+
if (pending.size >= PENDING_CAP)
|
|
39
|
+
pending.clear();
|
|
40
|
+
pending.set(m.id, m.params.name);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch { /* never break the wire */ }
|
|
44
|
+
handler(message, extra);
|
|
45
|
+
}
|
|
46
|
+
: undefined;
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
for (const prop of ['onclose', 'onerror']) {
|
|
50
|
+
Object.defineProperty(wrapper, prop, {
|
|
51
|
+
get: () => transport[prop],
|
|
52
|
+
set: (v) => {
|
|
53
|
+
transport[prop] = v;
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
Object.defineProperty(wrapper, 'sessionId', { get: () => transport.sessionId });
|
|
58
|
+
if (transport.setProtocolVersion) {
|
|
59
|
+
wrapper.setProtocolVersion = (v) => transport.setProtocolVersion(v);
|
|
60
|
+
}
|
|
61
|
+
return wrapper;
|
|
62
|
+
}
|
package/dist/registry.d.ts
CHANGED
|
@@ -56,6 +56,8 @@ export declare class ToolRegistry {
|
|
|
56
56
|
* normalization; the kind drives value coercion on renamed keys). */
|
|
57
57
|
argShape(name: string): ArgShape | undefined;
|
|
58
58
|
catalog(service: string, query?: string): CatalogOperation[];
|
|
59
|
+
/** The metrics recorder, for hook sites (escape hatch); null when off. */
|
|
60
|
+
get usageMetrics(): Metrics | null;
|
|
59
61
|
/** Op-name vocabulary for a service, split by provenance so the capped
|
|
60
62
|
* discover descriptions can list curated ops and only summarize the
|
|
61
63
|
* generated long tail. */
|
package/dist/registry.js
CHANGED
|
@@ -252,6 +252,10 @@ export class ToolRegistry {
|
|
|
252
252
|
cud: t.cud,
|
|
253
253
|
}));
|
|
254
254
|
}
|
|
255
|
+
/** The metrics recorder, for hook sites (escape hatch); null when off. */
|
|
256
|
+
get usageMetrics() {
|
|
257
|
+
return this.metrics;
|
|
258
|
+
}
|
|
255
259
|
/** Op-name vocabulary for a service, split by provenance so the capped
|
|
256
260
|
* discover descriptions can list curated ops and only summarize the
|
|
257
261
|
* generated long tail. */
|
package/dist/tools/google-api.js
CHANGED
|
@@ -86,12 +86,15 @@ export function registerEscapeTools(registry, policy, deps = {}) {
|
|
|
86
86
|
if (api) {
|
|
87
87
|
requested = resolveApiAliases(api);
|
|
88
88
|
if (!requested) {
|
|
89
|
+
registry.usageMetrics?.recordSearchApi(null);
|
|
89
90
|
return jsonResult({ error: 'unknown_api', message: `Unknown api "${api}".`, hint: `Known APIs: ${apiList}`, retriable: false }, true);
|
|
90
91
|
}
|
|
91
92
|
const enabled = requested.filter(apiEnabled);
|
|
92
93
|
if (enabled.length === 0)
|
|
93
94
|
return toolsetDisabled(requested[0]);
|
|
94
95
|
requested = enabled;
|
|
96
|
+
// Post-resolution SUPPORTED_APIS keys only (closed vocabulary).
|
|
97
|
+
registry.usageMetrics?.recordSearchApi(requested);
|
|
95
98
|
}
|
|
96
99
|
const apis = requested ?? enabledApis;
|
|
97
100
|
const unavailable = [];
|
|
@@ -172,6 +175,7 @@ export function registerEscapeTools(registry, policy, deps = {}) {
|
|
|
172
175
|
}
|
|
173
176
|
}
|
|
174
177
|
if (!method) {
|
|
178
|
+
registry.usageMetrics?.recordEscapeMethod(null);
|
|
175
179
|
const near = nearestMethodIds(String(methodId), index);
|
|
176
180
|
return jsonResult({
|
|
177
181
|
error: 'unknown_method',
|
|
@@ -182,6 +186,8 @@ export function registerEscapeTools(registry, policy, deps = {}) {
|
|
|
182
186
|
account,
|
|
183
187
|
}, true);
|
|
184
188
|
}
|
|
189
|
+
// The RESOLVED index id only, never the caller's methodId argument.
|
|
190
|
+
registry.usageMetrics?.recordEscapeMethod(method.id);
|
|
185
191
|
const cud = cudFromMethod(method);
|
|
186
192
|
const policyService = serviceForAlias(apiKey);
|
|
187
193
|
const lastSegment = method.id.split('.').pop() ?? method.id;
|
package/dist/usage-metrics.d.ts
CHANGED
|
@@ -79,7 +79,7 @@ export declare class Metrics {
|
|
|
79
79
|
recordEscapeMethod(idOrNull: string | null): void;
|
|
80
80
|
/** Search instrument: post-resolution SUPPORTED_APIS keys, or null on failure. */
|
|
81
81
|
recordSearchApi(resolvedApis: string[] | null): void;
|
|
82
|
-
recordArgFix(tool: string): void;
|
|
82
|
+
recordArgFix(tool: string, by?: number): void;
|
|
83
83
|
/** Protocol-level failures (no handler ran); wired by the transport tap. */
|
|
84
84
|
recordRpc(kind: 'schema_validation' | 'tool_not_found' | number, tool?: string): void;
|
|
85
85
|
private rolloverIfNeeded;
|
|
@@ -91,6 +91,13 @@ export declare class Metrics {
|
|
|
91
91
|
/** For the doctor/diagnose status line. */
|
|
92
92
|
statusLine(): string;
|
|
93
93
|
}
|
|
94
|
+
/** Read-only description of the metrics dir for doctor/diagnose status lines
|
|
95
|
+
* (never creates anything; safe to call with the feature off). */
|
|
96
|
+
export declare function describeMetricsDir(env?: NodeJS.ProcessEnv): {
|
|
97
|
+
dir: string;
|
|
98
|
+
files: number;
|
|
99
|
+
kb: number;
|
|
100
|
+
};
|
|
94
101
|
/**
|
|
95
102
|
* null when off: no directory, no file, no timer, nothing initializes; setting
|
|
96
103
|
* USAGE_METRICS_PATH alone never enables anything and never creates anything.
|
package/dist/usage-metrics.js
CHANGED
|
@@ -28,6 +28,7 @@ const CHARS_BOUNDS = [64, 256, 1024, 4096, 16384, 65536];
|
|
|
28
28
|
// arbitrary strings into recorded values (spec section 1).
|
|
29
29
|
const METHOD_ID_RE = /^[a-z][a-zA-Z0-9]*(\.[a-zA-Z0-9]+)+$/;
|
|
30
30
|
const METHOD_ID_MAX = 128;
|
|
31
|
+
const TOOL_NAME_RE = /^[a-z][a-z0-9_]{0,63}$/;
|
|
31
32
|
/** Union of every `error:` slug literal emitted anywhere in src/ (kept honest
|
|
32
33
|
* by a set-equality grep test). Anything outside buckets to `other`. */
|
|
33
34
|
export const KNOWN_ERROR_SLUGS = new Set([
|
|
@@ -411,11 +412,13 @@ export class Metrics {
|
|
|
411
412
|
}
|
|
412
413
|
catch { /* never throws outward */ }
|
|
413
414
|
}
|
|
414
|
-
recordArgFix(tool) {
|
|
415
|
+
recordArgFix(tool, by = 1) {
|
|
415
416
|
try {
|
|
416
417
|
this.rolloverIfNeeded();
|
|
418
|
+
if (!TOOL_NAME_RE.test(tool))
|
|
419
|
+
return;
|
|
417
420
|
const t = (this.deltas.tools[tool] ??= { n: 0, err: {}, hint: 0, lat: {} });
|
|
418
|
-
t.argfix = (t.argfix ?? 0) +
|
|
421
|
+
t.argfix = (t.argfix ?? 0) + by;
|
|
419
422
|
this.dirty = true;
|
|
420
423
|
}
|
|
421
424
|
catch { /* never throws outward */ }
|
|
@@ -425,7 +428,9 @@ export class Metrics {
|
|
|
425
428
|
try {
|
|
426
429
|
this.rolloverIfNeeded();
|
|
427
430
|
if (kind === 'schema_validation') {
|
|
428
|
-
|
|
431
|
+
// The tap's name came off the wire; the shape gate keeps a weird
|
|
432
|
+
// -32602 from writing free text (registered names always pass).
|
|
433
|
+
if (tool && TOOL_NAME_RE.test(tool))
|
|
429
434
|
addInto(this.deltas.validation, tool);
|
|
430
435
|
}
|
|
431
436
|
else if (kind === 'tool_not_found') {
|
|
@@ -437,7 +442,8 @@ export class Metrics {
|
|
|
437
442
|
this.events.push(JSON.stringify({
|
|
438
443
|
ts: new Date(this.now()).toISOString().slice(0, 16) + ':00Z',
|
|
439
444
|
boot: this.bootId, seq: this.seq++,
|
|
440
|
-
tool: tool
|
|
445
|
+
tool: tool && TOOL_NAME_RE.test(tool) ? tool : 'other',
|
|
446
|
+
ok: false, ms: 0, chars: 'le64', src: 'rpc',
|
|
441
447
|
}));
|
|
442
448
|
this.dirty = true;
|
|
443
449
|
}
|
|
@@ -524,6 +530,29 @@ export class Metrics {
|
|
|
524
530
|
}
|
|
525
531
|
}
|
|
526
532
|
}
|
|
533
|
+
/** Read-only description of the metrics dir for doctor/diagnose status lines
|
|
534
|
+
* (never creates anything; safe to call with the feature off). */
|
|
535
|
+
export function describeMetricsDir(env = process.env) {
|
|
536
|
+
const dir = env.USAGE_METRICS_PATH ?? path.join(stateDir(env), METRICS_DIR_NAME);
|
|
537
|
+
let files = 0;
|
|
538
|
+
let bytes = 0;
|
|
539
|
+
try {
|
|
540
|
+
const candidates = [
|
|
541
|
+
...fs.readdirSync(path.join(dir, 'agg')).map((f) => path.join(dir, 'agg', f)),
|
|
542
|
+
path.join(dir, 'events.jsonl'),
|
|
543
|
+
path.join(dir, 'events.1.jsonl'),
|
|
544
|
+
];
|
|
545
|
+
for (const f of candidates) {
|
|
546
|
+
try {
|
|
547
|
+
bytes += fs.statSync(f).size;
|
|
548
|
+
files += 1;
|
|
549
|
+
}
|
|
550
|
+
catch { /* absent */ }
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
catch { /* dir absent: zeros */ }
|
|
554
|
+
return { dir, files, kb: Math.round(bytes / 1024) };
|
|
555
|
+
}
|
|
527
556
|
/**
|
|
528
557
|
* null when off: no directory, no file, no timer, nothing initializes; setting
|
|
529
558
|
* USAGE_METRICS_PATH alone never enables anything and never creates anything.
|
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.26",
|
|
4
4
|
"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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|