mcp-google-multi 6.0.0-alpha.30 → 6.0.0-alpha.31

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.
@@ -1,5 +1,4 @@
1
- import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
2
- import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
1
+ import type { Transport, JSONRPCMessage } from "@modelcontextprotocol/server";
3
2
  export declare function argNormalizationEnabled(env?: NodeJS.ProcessEnv): boolean;
4
3
  /** Declared scalar kind per schema key; drives value coercion on RENAMED keys
5
4
  * only. Clients string-encode values for keys absent from the advertised
@@ -1,6 +1,6 @@
1
1
  // Wire-level tools/call argument normalization. Clients (LLMs) recurringly
2
2
  // snake_case a camelCase parameter (thread_id for threadId) and burn a retry
3
- // on the -32602. A schema-level fix is off the table: SDK 1.x advertises an
3
+ // on the -32602. A schema-level fix is off the table: the SDK advertises an
4
4
  // EMPTY input schema for any non-object wrapper (pipe/preprocess), so the
5
5
  // only seam that keeps tools/list intact is the JSON-RPC message itself —
6
6
  // which is versioned MCP spec, stabler than any SDK internal. The rename is
@@ -90,5 +90,12 @@ export function withArgNormalization(transport, shapeFor, log, onRename) {
90
90
  if (transport.setProtocolVersion) {
91
91
  wrapper.setProtocolVersion = (v) => transport.setProtocolVersion(v);
92
92
  }
93
+ // v2-only, called by Protocol.connect() before start(). A no-op today (both
94
+ // sides default to the same exported constant), but a proxy that silently
95
+ // eats a member the SDK calls is a bug waiting for the first caller that
96
+ // passes supportedProtocolVersions explicitly.
97
+ if (transport.setSupportedProtocolVersions) {
98
+ wrapper.setSupportedProtocolVersions = (v) => transport.setSupportedProtocolVersions(v);
99
+ }
93
100
  return wrapper;
94
101
  }
@@ -1,8 +1,7 @@
1
+ import type { McpServer, Transport } from "@modelcontextprotocol/server";
1
2
  import { type IncomingMessage, type ServerResponse } from 'node:http';
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';
6
5
  export type AuthOutcome = {
7
6
  ok: true;
8
7
  } | {
@@ -1,10 +1,9 @@
1
- // B12: the Streamable HTTP transport host (cc-transport-hosting T2/T3). Owns the
1
+ import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
2
2
  // node:http server, the route table, the front guard (Host / Origin / DNS-rebind),
3
3
  // and the stateless per-request /mcp dispatch. The OAuth AS endpoints and Bearer
4
4
  // verification are a seam filled by B13 (oauth-authorization-server); B12 ships a
5
5
  // loopback-owner authenticator so the local-HTTP model works before the AS lands.
6
6
  import { createServer } from 'node:http';
7
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
8
7
  import { withArgNormalization } from './arg-normalize.js';
9
8
  // A hung handler that keeps the connection open would otherwise hold the global
10
9
  // serialize() lock forever. Generous by default so slow-but-valid calls (large
@@ -161,7 +160,7 @@ export class HttpTransportHost {
161
160
  // the mounted AS routes), so the SDK's own DNS-rebind guard is disabled: its
162
161
  // exact-Host match is stricter than the front guard and would 403 valid
163
162
  // Hosts (double enforcement, differing rules).
164
- const transport = new StreamableHTTPServerTransport({
163
+ const transport = new NodeStreamableHTTPServerTransport({
165
164
  sessionIdGenerator: undefined,
166
165
  enableJsonResponse: true,
167
166
  enableDnsRebindingProtection: false,
package/dist/index.js CHANGED
@@ -5,8 +5,8 @@ import { assertServerAccountsConfigured } from './accounts.js';
5
5
  import { readFileSync } from 'node:fs';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import path from 'node:path';
8
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
+ import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
9
+ import { McpServer } from "@modelcontextprotocol/server";
10
10
  import { GENERATED_SERVICES } from './tools/generated/index.js';
11
11
  import { GENERATED_GATES, SERVICES } from './services.js';
12
12
  import { ToolRegistry, resolveDiscoveryMode } from './registry.js';
@@ -247,7 +247,7 @@ async function main() {
247
247
  let transport = new StdioServerTransport();
248
248
  if (stdioMetrics) {
249
249
  const { tapUsageMetrics } = await import('./metrics-tap.js');
250
- transport = tapUsageMetrics(transport, stdioMetrics);
250
+ transport = tapUsageMetrics(transport, stdioMetrics, (n) => registry.hasTool(n));
251
251
  }
252
252
  await server.connect(argNormalizationEnabled()
253
253
  ? withArgNormalization(transport, (n) => registry.argShape(n), undefined, stdioMetrics ? (tool, n) => stdioMetrics.recordArgFix(tool, n) : undefined)
@@ -338,7 +338,7 @@ async function main() {
338
338
  let httpTap;
339
339
  if (httpMetrics) {
340
340
  const { tapUsageMetrics } = await import('./metrics-tap.js');
341
- httpTap = (t) => tapUsageMetrics(t, httpMetrics);
341
+ httpTap = (t) => tapUsageMetrics(t, httpMetrics, (n) => registry.hasTool(n));
342
342
  }
343
343
  const host = new HttpTransportHost({
344
344
  server: httpServer,
@@ -1,3 +1,11 @@
1
- import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
1
+ import type { Transport } from "@modelcontextprotocol/server";
2
2
  import type { Metrics } from './usage-metrics.js';
3
- export declare function tapUsageMetrics(transport: Transport, metrics: Metrics): Transport;
3
+ export declare function isSchemaValidationText(text: string): boolean;
4
+ /**
5
+ * @param isKnownTool membership test against the REGISTERED tool set. The
6
+ * name on a `tools/call` frame is client-supplied, so a shape check alone
7
+ * would let a shape-valid but invented name reach disk and break the
8
+ * closed-vocabulary guarantee. A validation failure against a name the server
9
+ * never registered is a not-found, and is counted as one.
10
+ */
11
+ export declare function tapUsageMetrics(transport: Transport, metrics: Metrics, isKnownTool?: (name: string) => boolean): Transport;
@@ -1,6 +1,28 @@
1
1
  const PENDING_CAP = 1_000;
2
- export function tapUsageMetrics(transport, metrics) {
2
+ // The SDK synthesizes input-validation failures as isError RESULTS, skipping
3
+ // the handler entirely, so neither the registry wrapper nor the error-frame
4
+ // path sees them. The text is prose in both SDK eras (v1 prefixed it with
5
+ // "MCP error -32602: ", v2 dropped that prefix), while handler envelopes are
6
+ // always JSON starting with "{" — so this can never double count one.
7
+ const SCHEMA_VALIDATION_TEXT = /^(MCP error -32602: )?Input validation error:/;
8
+ export function isSchemaValidationText(text) {
9
+ return SCHEMA_VALIDATION_TEXT.test(text);
10
+ }
11
+ /**
12
+ * @param isKnownTool membership test against the REGISTERED tool set. The
13
+ * name on a `tools/call` frame is client-supplied, so a shape check alone
14
+ * would let a shape-valid but invented name reach disk and break the
15
+ * closed-vocabulary guarantee. A validation failure against a name the server
16
+ * never registered is a not-found, and is counted as one.
17
+ */
18
+ export function tapUsageMetrics(transport, metrics, isKnownTool = () => false) {
3
19
  const pending = new Map();
20
+ const recordValidation = (tool) => {
21
+ if (tool !== undefined && isKnownTool(tool))
22
+ metrics.recordRpc('schema_validation', tool);
23
+ else
24
+ metrics.recordRpc('tool_not_found');
25
+ };
4
26
  const wrapper = {
5
27
  start: () => transport.start(),
6
28
  send: (message, options) => {
@@ -14,23 +36,16 @@ export function tapUsageMetrics(transport, metrics) {
14
36
  if (/unknown tool|not found/i.test(m.error.message ?? ''))
15
37
  metrics.recordRpc('tool_not_found');
16
38
  else
17
- metrics.recordRpc('schema_validation', tool);
39
+ recordValidation(tool);
18
40
  }
19
41
  else {
20
42
  metrics.recordRpc(m.error.code);
21
43
  }
22
44
  }
23
45
  else if (m.result?.isError === true) {
24
- // SDK 1.x synthesizes input-validation failures as isError RESULTS
25
- // ("MCP error -32602: ..."), skipping the handler entirely, so the
26
- // registry wrapper never sees them either. Handler envelopes are
27
- // JSON text and never carry this prefix, so nothing double counts.
28
46
  const first = m.result.content?.[0]?.text;
29
- if (typeof first === 'string' && first.startsWith('MCP error -32602:')) {
30
- if (/tool \S+ not found|unknown tool/i.test(first))
31
- metrics.recordRpc('tool_not_found');
32
- else
33
- metrics.recordRpc('schema_validation', tool);
47
+ if (typeof first === 'string' && isSchemaValidationText(first)) {
48
+ recordValidation(tool);
34
49
  }
35
50
  }
36
51
  }
@@ -71,5 +86,10 @@ export function tapUsageMetrics(transport, metrics) {
71
86
  if (transport.setProtocolVersion) {
72
87
  wrapper.setProtocolVersion = (v) => transport.setProtocolVersion(v);
73
88
  }
89
+ // v2-only; see the same forward in arg-normalize.ts. Both proxies compose,
90
+ // so a member dropped by either layer never reaches the real transport.
91
+ if (transport.setSupportedProtocolVersions) {
92
+ wrapper.setSupportedProtocolVersions = (v) => transport.setSupportedProtocolVersions(v);
93
+ }
74
94
  return wrapper;
75
95
  }
@@ -1,4 +1,4 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import type { McpServer } from "@modelcontextprotocol/server";
2
2
  import { z } from 'zod';
3
3
  import { type Policy } from './write-control.js';
4
4
  import type { ArgShape } from './arg-normalize.js';
@@ -58,6 +58,10 @@ export declare class ToolRegistry {
58
58
  catalog(service: string, query?: string): CatalogOperation[];
59
59
  /** The metrics recorder, for hook sites (escape hatch); null when off. */
60
60
  get usageMetrics(): Metrics | null;
61
+ /** Membership test against the REGISTERED tool set (hidden tools included:
62
+ * they stay callable). The metrics tap uses this so a client-supplied name
63
+ * can never enter the closed vocabulary. */
64
+ hasTool(name: string): boolean;
61
65
  /** Op-name vocabulary for a service, split by provenance so the capped
62
66
  * discover descriptions can list curated ops and only summarize the
63
67
  * generated long tail. */
package/dist/registry.js CHANGED
@@ -1,4 +1,3 @@
1
- import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
2
1
  import { z } from 'zod';
3
2
  import { isAllowed, writeDisabledResult, IRREVERSIBLE_TOOLS } from './write-control.js';
4
3
  import { getAccountSet, refreshAccountSetIfStale } from './accounts.js';
@@ -256,6 +255,12 @@ export class ToolRegistry {
256
255
  get usageMetrics() {
257
256
  return this.metrics;
258
257
  }
258
+ /** Membership test against the REGISTERED tool set (hidden tools included:
259
+ * they stay callable). The metrics tap uses this so a client-supplied name
260
+ * can never enter the closed vocabulary. */
261
+ hasTool(name) {
262
+ return this.tools.some((t) => t.name === name);
263
+ }
259
264
  /** Op-name vocabulary for a service, split by provenance so the capped
260
265
  * discover descriptions can list curated ops and only summarize the
261
266
  * generated long tail. */
@@ -320,7 +325,11 @@ export class ToolRegistry {
320
325
  if (this.tools.length === 0) {
321
326
  throw new Error('installListHandler() requires at least one registered tool');
322
327
  }
323
- this.server.server.setRequestHandler(ListToolsRequestSchema, async () => ({
328
+ this.server.server.setRequestHandler('tools/list', async () => ({
329
+ // The wire Tool is hand-built because the SDK's own types drop the
330
+ // anthropic/* _meta keys honoring clients read. z.toJSONSchema emits a
331
+ // valid draft-7 object schema by construction, which the SDK's recursive
332
+ // JSON-Schema type cannot infer from our cached `unknown`.
324
333
  tools: this.tools.filter((t) => this.isVisible(t)).map((t) => this.toToolJson(t)),
325
334
  }));
326
335
  }
@@ -1,4 +1,4 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import type { McpServer } from "@modelcontextprotocol/server";
2
2
  export interface SetupOptions {
3
3
  /** Public base URL when serving over HTTP; used for the Web client redirect. */
4
4
  publicUrl?: string;
@@ -47,7 +47,7 @@ export function registerSetupPrompt(server) {
47
47
  server.registerPrompt('setup', {
48
48
  title: 'Set up mcp-google-multi',
49
49
  description: 'Guided Google Cloud Console prelude: project, APIs, consent screen, OAuth client, and credentials. The one-time browser setup that has no API.',
50
- argsSchema: { publicUrl: z.string().optional().describe('Public base URL when serving over HTTP (for the Web OAuth client redirect). Omit for stdio.') },
50
+ argsSchema: z.object({ publicUrl: z.string().optional().describe('Public base URL when serving over HTTP (for the Web OAuth client redirect). Omit for stdio.') }),
51
51
  }, (args) => ({
52
52
  messages: [
53
53
  {
@@ -1,4 +1,4 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import type { McpServer } from "@modelcontextprotocol/server";
2
2
  import type { ToolRegistry } from '../registry.js';
3
3
  export interface AddForm {
4
4
  alias: string;
@@ -18,6 +18,7 @@ const RETRY_WINDOW_MS = 10 * 60_000;
18
18
  const BIGRAM_GAP_MS = 5 * 60_000;
19
19
  const BIGRAM_CAP = 1_500;
20
20
  const ERR_SLUG_CAP = 32;
21
+ const VALIDATION_CAP = 200;
21
22
  const ESCAPE_CAP = 500;
22
23
  const EVENTS_ROTATE_BYTES = 4 * 1024 * 1024;
23
24
  const RETENTION_DAYS = 180;
@@ -184,6 +185,9 @@ function applyCaps(day) {
184
185
  };
185
186
  for (const t of Object.values(day.tools))
186
187
  t.err = cap(t.err, ERR_SLUG_CAP);
188
+ // Bounded by the registered tool set via the tap's membership test, but
189
+ // capped anyway: this map is the only one fed by a wire-supplied key.
190
+ day.validation = cap(day.validation, VALIDATION_CAP);
187
191
  day.escape.methods = cap(day.escape.methods, ESCAPE_CAP, (n) => { day.escape._overflow += n; });
188
192
  day.bigrams = cap(day.bigrams, BIGRAM_CAP, (n) => { day.bigrams._overflow = (day.bigrams._overflow ?? 0) + n; });
189
193
  return day;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-google-multi",
3
- "version": "6.0.0-alpha.30",
3
+ "version": "6.0.0-alpha.31",
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",
@@ -81,7 +81,8 @@
81
81
  "@googleapis/slides": "^10.0.0",
82
82
  "@googleapis/tasks": "^17.0.0",
83
83
  "@googleapis/webmasters": "^9.0.0",
84
- "@modelcontextprotocol/sdk": "^1.30.0",
84
+ "@modelcontextprotocol/node": "2.0.0",
85
+ "@modelcontextprotocol/server": "2.0.0",
85
86
  "googleapis-common": "^9.0.0",
86
87
  "jose": "^6.2.8",
87
88
  "markdown-it": "15.0.0",
@@ -115,7 +116,6 @@
115
116
  },
116
117
  "overrides": {
117
118
  "tmp": "^0.2.4",
118
- "fast-uri": "^3.1.6",
119
119
  "hono": "^4.13.5",
120
120
  "qs": "^6.16.0"
121
121
  }