mcp-google-multi 6.0.0-alpha.54 → 6.0.0-alpha.55
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/accounts.d.ts +1 -0
- package/dist/accounts.js +5 -2
- package/dist/compose.d.ts +6 -0
- package/dist/compose.js +26 -1
- package/dist/http-transport.d.ts +15 -6
- package/dist/http-transport.js +26 -14
- package/dist/index.js +12 -34
- package/package.json +1 -1
package/dist/accounts.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface AccountSet {
|
|
|
20
20
|
}
|
|
21
21
|
export declare function resolveAccounts(env?: NodeJS.ProcessEnv, filePath?: string, onInvalid?: 'exit' | 'throw', opts?: {
|
|
22
22
|
tokenDir?: string;
|
|
23
|
+
emptyOk?: boolean;
|
|
23
24
|
}): AccountSet;
|
|
24
25
|
export declare function fileStamp(filePath: string, version: number): string;
|
|
25
26
|
/** Live accessor: dispatch-time readers use this, never a captured snapshot. */
|
package/dist/accounts.js
CHANGED
|
@@ -74,7 +74,10 @@ function noAccountsMessage() {
|
|
|
74
74
|
* empty behaves as unset, mirroring GOOGLE_ACCOUNTS semantics.
|
|
75
75
|
*/
|
|
76
76
|
let warnedLegacy = false;
|
|
77
|
-
export function resolveAccounts(env = process.env, filePath = configFilePath(), onInvalid = 'exit',
|
|
77
|
+
export function resolveAccounts(env = process.env, filePath = configFilePath(), onInvalid = 'exit',
|
|
78
|
+
// emptyOk: zero accounts is a valid set, not an error, even with 'throw'
|
|
79
|
+
// (a context that has linked nothing yet). An invalid file still fails.
|
|
80
|
+
opts = {}) {
|
|
78
81
|
const dir = opts.tokenDir ?? tokenDir;
|
|
79
82
|
const adminEnv = parseCsv(env.GOOGLE_ADMIN_ACCOUNTS);
|
|
80
83
|
const rawEnv = env.GOOGLE_ACCOUNTS;
|
|
@@ -130,7 +133,7 @@ export function resolveAccounts(env = process.env, filePath = configFilePath(),
|
|
|
130
133
|
// empty (BR-4), enforced by assertServerAccountsConfigured() in index.ts.
|
|
131
134
|
// The dispatch-path reload ('throw') still throws so a mid-session emptied
|
|
132
135
|
// config.json keeps the last-good registry (BR-7) instead of dropping tools.
|
|
133
|
-
if (onInvalid === 'throw') {
|
|
136
|
+
if (onInvalid === 'throw' && !opts.emptyOk) {
|
|
134
137
|
throw new Error(`E_NO_ACCOUNTS_CONFIGURED: ${noAccountsMessage()}`);
|
|
135
138
|
}
|
|
136
139
|
return {
|
package/dist/compose.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/server";
|
|
2
|
+
import type { ServerTarget } from './http-transport.js';
|
|
2
3
|
import { ToolRegistry, type DiscoveryMode } from './registry.js';
|
|
3
4
|
import { type IdentityContext } from './identity.js';
|
|
4
5
|
import type { Metrics } from './usage-metrics.js';
|
|
5
6
|
export declare function buildRegistry(server: McpServer, ctx: IdentityContext, mode?: DiscoveryMode, metrics?: Metrics | null): ToolRegistry;
|
|
7
|
+
/** The per-request hooks bound to one registry: argument shapes, unknown-
|
|
8
|
+
* argument screening and the account a validation envelope names. Both
|
|
9
|
+
* transports and every resolved server target build them here, so a mistyped
|
|
10
|
+
* argument behaves the same wherever it arrives. */
|
|
11
|
+
export declare function requestHooksFor(registry: ToolRegistry, ctx: Pick<IdentityContext, 'accounts'>, metrics?: Metrics | null, env?: NodeJS.ProcessEnv): Omit<ServerTarget, 'server'>;
|
package/dist/compose.js
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
// parsing, no process.exit, no prints (accounts.js module-load resolution is
|
|
5
5
|
// the one inherited, pre-existing exception on the import chain).
|
|
6
6
|
import { GENERATED_SERVICES } from './tools/generated/index.js';
|
|
7
|
-
import { GENERATED_GATES, SERVICES } from './services.js';
|
|
7
|
+
import { GENERATED_GATES, SERVICES, unknownToolMessage } from './services.js';
|
|
8
|
+
import { argNormalizationEnabled } from './arg-normalize.js';
|
|
9
|
+
import { unknownArgMode } from './arg-strict.js';
|
|
8
10
|
import { ToolRegistry } from './registry.js';
|
|
9
11
|
import { registerDiscoverTools } from './discover.js';
|
|
10
12
|
import { registerEscapeTools } from './tools/google-api.js';
|
|
@@ -82,3 +84,26 @@ export function buildRegistry(server, ctx, mode, metrics = null) {
|
|
|
82
84
|
registerAccountWizardTools(registry, server);
|
|
83
85
|
return registry;
|
|
84
86
|
}
|
|
87
|
+
/** The per-request hooks bound to one registry: argument shapes, unknown-
|
|
88
|
+
* argument screening and the account a validation envelope names. Both
|
|
89
|
+
* transports and every resolved server target build them here, so a mistyped
|
|
90
|
+
* argument behaves the same wherever it arrives. */
|
|
91
|
+
export function requestHooksFor(registry, ctx, metrics = null, env = process.env) {
|
|
92
|
+
const mode = unknownArgMode(env);
|
|
93
|
+
return {
|
|
94
|
+
argShapeFor: argNormalizationEnabled(env) ? (tool) => registry.argShape(tool) : undefined,
|
|
95
|
+
strictArgs: mode === 'off'
|
|
96
|
+
? undefined
|
|
97
|
+
: {
|
|
98
|
+
mode,
|
|
99
|
+
declaredFor: (tool) => registry.declaredKeys(tool),
|
|
100
|
+
siblingsFor: (tool, keys) => registry.siblingSpellings(tool, keys),
|
|
101
|
+
unknownTool: (name) => unknownToolMessage(registry, name),
|
|
102
|
+
onDrop: metrics ? (tool, keys) => metrics.recordArgDrop(tool, keys) : undefined,
|
|
103
|
+
},
|
|
104
|
+
validationEnvelope: {
|
|
105
|
+
isKnownTool: (name) => registry.hasTool(name),
|
|
106
|
+
defaultAccount: () => ctx.accounts.defaultAccount,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
package/dist/http-transport.d.ts
CHANGED
|
@@ -15,6 +15,16 @@ export type AuthOutcome = {
|
|
|
15
15
|
export type Authenticator = (req: IncomingMessage) => AuthOutcome | Promise<AuthOutcome>;
|
|
16
16
|
/** A mounted extra route (the AS endpoints, B13). Return true if it wrote a response. */
|
|
17
17
|
export type RouteHandler = (req: IncomingMessage, res: ServerResponse, url: URL) => boolean | Promise<boolean>;
|
|
18
|
+
/** What a verified subject resolves to: ITS server plus the per-request hooks
|
|
19
|
+
* bound to that server's registry (tool shapes, declared keys, the default
|
|
20
|
+
* account a validation envelope names). The metrics observers (metricsTap,
|
|
21
|
+
* onArgRename) stay host-wide and must not be bound to any one registry. */
|
|
22
|
+
export interface ServerTarget {
|
|
23
|
+
server: McpServer;
|
|
24
|
+
argShapeFor?: (tool: string) => ArgShape | undefined;
|
|
25
|
+
strictArgs?: StrictArgOptions;
|
|
26
|
+
validationEnvelope?: ValidationEnvelopeOptions;
|
|
27
|
+
}
|
|
18
28
|
export interface HttpHostOptions {
|
|
19
29
|
/** The ONE McpServer + registry built at boot (P1 / BV gap #4: never per request). */
|
|
20
30
|
server: McpServer;
|
|
@@ -25,14 +35,13 @@ export interface HttpHostOptions {
|
|
|
25
35
|
/** Tenant-resolution seam: map the verified subject to ITS server. Absent
|
|
26
36
|
* (free core) = every request dispatches to the one boot `server`. Return
|
|
27
37
|
* null for an unknown subject -> 403 tenant_not_found. Each distinct
|
|
28
|
-
* resolved server gets its own
|
|
38
|
+
* resolved server gets its own dispatch lane. With a resolver, the
|
|
39
|
+
* per-request hooks come from the resolved target ONLY: the host-level
|
|
40
|
+
* argShapeFor / strictArgs / validationEnvelope describe the boot server's
|
|
41
|
+
* registry, never another subject's. */
|
|
29
42
|
resolveServer?: (claims: {
|
|
30
43
|
sub: string;
|
|
31
|
-
}) => Promise<
|
|
32
|
-
server: McpServer;
|
|
33
|
-
} | null> | {
|
|
34
|
-
server: McpServer;
|
|
35
|
-
} | null;
|
|
44
|
+
}) => Promise<ServerTarget | null> | ServerTarget | null;
|
|
36
45
|
/** Extra routes keyed by exact pathname (AS endpoints mount here in B13). */
|
|
37
46
|
routes?: Record<string, RouteHandler>;
|
|
38
47
|
log?: (line: string) => void;
|
package/dist/http-transport.js
CHANGED
|
@@ -43,12 +43,12 @@ export function jsonRpcMethod(body) {
|
|
|
43
43
|
export class HttpTransportHost {
|
|
44
44
|
opts;
|
|
45
45
|
httpServer;
|
|
46
|
-
// Serialize the connect→dispatch critical section PER
|
|
46
|
+
// Serialize the connect→dispatch critical section PER SERVER: a McpServer
|
|
47
47
|
// captures its transport per request (protocol.js), but the gap between
|
|
48
48
|
// connect() and the dispatch capturing it would still race under true
|
|
49
49
|
// concurrency, so all traffic to ONE server runs under one mutex. The free
|
|
50
|
-
// core
|
|
51
|
-
// gets its own
|
|
50
|
+
// core has one lane (the boot server); with resolveServer each resolved
|
|
51
|
+
// server gets its own, however subjects map onto servers.
|
|
52
52
|
locks = new Map();
|
|
53
53
|
constructor(opts) {
|
|
54
54
|
this.opts = opts;
|
|
@@ -56,7 +56,15 @@ export class HttpTransportHost {
|
|
|
56
56
|
serializeFor(key, fn) {
|
|
57
57
|
const prev = this.locks.get(key) ?? Promise.resolve();
|
|
58
58
|
const run = prev.then(fn, fn);
|
|
59
|
-
|
|
59
|
+
const tail = run.then(() => undefined, () => undefined);
|
|
60
|
+
this.locks.set(key, tail);
|
|
61
|
+
// An idle lane drops out, so the map holds only in-flight subjects rather
|
|
62
|
+
// than every subject ever seen. A request that chained on this tail has
|
|
63
|
+
// already replaced it, so the identity check never frees a live lane.
|
|
64
|
+
void tail.then(() => {
|
|
65
|
+
if (this.locks.get(key) === tail)
|
|
66
|
+
this.locks.delete(key);
|
|
67
|
+
});
|
|
60
68
|
return run;
|
|
61
69
|
}
|
|
62
70
|
log(line) {
|
|
@@ -154,7 +162,12 @@ export class HttpTransportHost {
|
|
|
154
162
|
const sub = auth.sub ?? 'owner';
|
|
155
163
|
// Tenant resolution (the seam EE fills): map the verified subject to ITS
|
|
156
164
|
// server. Absent resolver = the one boot server for every subject.
|
|
157
|
-
let target = {
|
|
165
|
+
let target = {
|
|
166
|
+
server: this.opts.server,
|
|
167
|
+
argShapeFor: this.opts.argShapeFor,
|
|
168
|
+
strictArgs: this.opts.strictArgs,
|
|
169
|
+
validationEnvelope: this.opts.validationEnvelope,
|
|
170
|
+
};
|
|
158
171
|
if (this.opts.resolveServer) {
|
|
159
172
|
try {
|
|
160
173
|
target = await this.opts.resolveServer({ sub });
|
|
@@ -204,12 +217,11 @@ export class HttpTransportHost {
|
|
|
204
217
|
// releases. (A shared initialized-state persists across stateless requests;
|
|
205
218
|
// benign for the single-owner design.)
|
|
206
219
|
const deadlineMs = this.opts.dispatchTimeoutMs ?? DISPATCH_TIMEOUT_DEFAULT;
|
|
207
|
-
// The lock lane
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
await this.serializeFor(serializeKey, async () => {
|
|
220
|
+
// The lock lane is the SERVER identity, never the subject: two subjects
|
|
221
|
+
// reaching one server (the boot server, or a resolver mapping several
|
|
222
|
+
// subjects to one) would otherwise race it through the connect gap.
|
|
223
|
+
const { server: mcpServer, argShapeFor, strictArgs, validationEnvelope } = target;
|
|
224
|
+
await this.serializeFor(mcpServer, async () => {
|
|
213
225
|
const disconnected = new Promise((resolve) => res.once('close', () => resolve('closed')));
|
|
214
226
|
let timer;
|
|
215
227
|
const timedOut = new Promise((resolve) => {
|
|
@@ -218,10 +230,10 @@ export class HttpTransportHost {
|
|
|
218
230
|
});
|
|
219
231
|
// Same composition as the stdio leg: the envelope rewrite is innermost,
|
|
220
232
|
// so the tap above it still classifies the original validation prose.
|
|
221
|
-
const enveloped = withValidationEnvelope(transport,
|
|
233
|
+
const enveloped = withValidationEnvelope(transport, validationEnvelope ?? {});
|
|
222
234
|
const tapped = this.opts.metricsTap ? this.opts.metricsTap(enveloped) : enveloped;
|
|
223
|
-
await mcpServer.connect(
|
|
224
|
-
? withArgNormalization(tapped,
|
|
235
|
+
await mcpServer.connect(argShapeFor || strictArgs
|
|
236
|
+
? withArgNormalization(tapped, argShapeFor ?? (() => undefined), this.opts.log, this.opts.onArgRename, strictArgs)
|
|
225
237
|
: tapped);
|
|
226
238
|
// Reflect the dispatch into a non-rejecting arm: if the deadline wins the
|
|
227
239
|
// race, an orphaned handler settling later must not surface as an unhandled
|
package/dist/index.js
CHANGED
|
@@ -1,42 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// First import: triggers accounts.js module load (env files + registry) before
|
|
3
3
|
// anything else. Named to also pull the server-only empty-registry guard (BR-4).
|
|
4
|
-
import { assertServerAccountsConfigured
|
|
4
|
+
import { assertServerAccountsConfigured } from './accounts.js';
|
|
5
5
|
import { accountsAssertRequired, assertNoEnvAccountsMode, assertNoEnvOptionalScopesMode, isMultiTenantBoot, mtBootGates } from './boot-gates.js';
|
|
6
6
|
import { readFileSync } from 'node:fs';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
10
10
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
11
|
-
import { unknownToolMessage } from './services.js';
|
|
12
11
|
import { resolveDiscoveryMode } from './registry.js';
|
|
13
12
|
import { isAllowed, describePolicy } from './write-control.js';
|
|
14
13
|
import { buildIdentityContext } from './identity.js';
|
|
15
|
-
import { buildRegistry } from './compose.js';
|
|
14
|
+
import { buildRegistry, requestHooksFor } from './compose.js';
|
|
16
15
|
import { registerSetupPrompt } from './setup-prompt.js';
|
|
17
16
|
import { applyNetTuning } from './net-tuning.js';
|
|
18
|
-
import {
|
|
19
|
-
import { unknownArgMode } from './arg-strict.js';
|
|
17
|
+
import { withArgNormalization, withValidationEnvelope } from './arg-normalize.js';
|
|
20
18
|
import { envValueSource } from './env-load.js';
|
|
21
19
|
import { loadConfigFile } from './config-file.js';
|
|
22
20
|
import { initUsageMetrics, resolveUsageMetrics, sourceLabel } from './usage-metrics.js';
|
|
23
21
|
applyNetTuning();
|
|
24
22
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
23
|
const pkg = JSON.parse(readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf-8'));
|
|
26
|
-
/** Unknown-argument screening options, or undefined when the feature is off.
|
|
27
|
-
* Shared by both transports so a mistyped argument behaves identically. */
|
|
28
|
-
function strictArgOptions(registry, metrics) {
|
|
29
|
-
const mode = unknownArgMode();
|
|
30
|
-
if (mode === 'off')
|
|
31
|
-
return undefined;
|
|
32
|
-
return {
|
|
33
|
-
mode,
|
|
34
|
-
declaredFor: (tool) => registry.declaredKeys(tool),
|
|
35
|
-
siblingsFor: (tool, keys) => registry.siblingSpellings(tool, keys),
|
|
36
|
-
unknownTool: (name) => unknownToolMessage(registry, name),
|
|
37
|
-
onDrop: metrics ? (tool, keys) => metrics.recordArgDrop(tool, keys) : undefined,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
24
|
async function main() {
|
|
41
25
|
if (process.argv.includes('auth')) {
|
|
42
26
|
const { runAuthFlow } = await import('./auth.js');
|
|
@@ -208,30 +192,28 @@ async function main() {
|
|
|
208
192
|
if (wantStdio) {
|
|
209
193
|
const server = new McpServer({ name: 'mcp-google-multi', version: pkg.version });
|
|
210
194
|
const stdioMetrics = initMetricsFor('stdio', resolveDiscoveryMode());
|
|
211
|
-
const
|
|
195
|
+
const stdioCtx = buildIdentityContext(process.env, { transport: 'stdio' });
|
|
196
|
+
const registry = buildRegistry(server, stdioCtx, undefined, stdioMetrics);
|
|
212
197
|
registry.installListHandler();
|
|
213
198
|
registerSetupPrompt(server);
|
|
199
|
+
const stdioHooks = requestHooksFor(registry, stdioCtx, stdioMetrics);
|
|
214
200
|
// Outbound runs outermost-first, so the composition is deliberate: the
|
|
215
201
|
// envelope rewrite is INNERMOST (last to touch the frame), the tap sits
|
|
216
202
|
// above it (classifying the ORIGINAL validation prose), arg normalization
|
|
217
203
|
// outermost. The tap only counts outbound JSON-RPC error frames (no
|
|
218
204
|
// handler ran) plus id->tool names.
|
|
219
|
-
let transport = withValidationEnvelope(new StdioServerTransport(),
|
|
220
|
-
isKnownTool: (n) => registry.hasTool(n),
|
|
221
|
-
defaultAccount: () => getAccountSet().defaultAccount,
|
|
222
|
-
});
|
|
205
|
+
let transport = withValidationEnvelope(new StdioServerTransport(), stdioHooks.validationEnvelope);
|
|
223
206
|
if (stdioMetrics) {
|
|
224
207
|
const { tapUsageMetrics } = await import('./metrics-tap.js');
|
|
225
208
|
transport = tapUsageMetrics(transport, stdioMetrics, (n) => registry.hasTool(n));
|
|
226
209
|
}
|
|
227
|
-
|
|
228
|
-
await server.connect(argNormalizationEnabled() || strictStdio
|
|
210
|
+
await server.connect(stdioHooks.argShapeFor || stdioHooks.strictArgs
|
|
229
211
|
? withArgNormalization(transport,
|
|
230
212
|
// Gated exactly as the HTTP leg gates `argShapeFor`. Passing the
|
|
231
213
|
// shape unconditionally made stdio rename keys while
|
|
232
214
|
// GOOGLE_ARG_NORMALIZE=off, so the same call succeeded on stdio
|
|
233
215
|
// and failed on HTTP once screening rejects.
|
|
234
|
-
|
|
216
|
+
stdioHooks.argShapeFor ?? (() => undefined), undefined, stdioMetrics ? (tool, n) => stdioMetrics.recordArgFix(tool, n) : undefined, stdioHooks.strictArgs)
|
|
235
217
|
: transport);
|
|
236
218
|
}
|
|
237
219
|
if (wantHttp) {
|
|
@@ -259,7 +241,8 @@ async function main() {
|
|
|
259
241
|
}
|
|
260
242
|
const httpServer = new McpServer({ name: 'mcp-google-multi', version: pkg.version });
|
|
261
243
|
const httpMetrics = initMetricsFor('http', 'curated');
|
|
262
|
-
const
|
|
244
|
+
const httpCtx = buildIdentityContext(process.env, { transport: 'http' });
|
|
245
|
+
const registry = buildRegistry(httpServer, httpCtx, 'curated', httpMetrics);
|
|
263
246
|
registry.installListHandler();
|
|
264
247
|
registerSetupPrompt(httpServer);
|
|
265
248
|
// B13: mount the OAuth 2.1 AS (legs A + B) + the Bearer authenticator.
|
|
@@ -345,14 +328,9 @@ async function main() {
|
|
|
345
328
|
authenticate: authServer.authenticate,
|
|
346
329
|
routes: authServer.routes,
|
|
347
330
|
log: (l) => process.stderr.write(`[http] ${l}\n`),
|
|
348
|
-
|
|
331
|
+
...requestHooksFor(registry, httpCtx, httpMetrics),
|
|
349
332
|
metricsTap: httpTap,
|
|
350
|
-
validationEnvelope: {
|
|
351
|
-
isKnownTool: (n) => registry.hasTool(n),
|
|
352
|
-
defaultAccount: () => getAccountSet().defaultAccount,
|
|
353
|
-
},
|
|
354
333
|
onArgRename: httpMetrics ? (tool, n) => httpMetrics.recordArgFix(tool, n) : undefined,
|
|
355
|
-
strictArgs: strictArgOptions(registry, httpMetrics),
|
|
356
334
|
});
|
|
357
335
|
await host.start();
|
|
358
336
|
process.stderr.write(`HTTP transport listening on http://${httpCfg.host}:${httpCfg.port} (public ${httpCfg.publicUrl})\n`);
|
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.55",
|
|
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",
|