mcp-google-multi 6.0.0-alpha.40 → 6.0.0-alpha.42
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/boot-gates.d.ts +26 -0
- package/dist/boot-gates.js +38 -0
- package/dist/client.d.ts +23 -2
- package/dist/client.js +0 -0
- package/dist/http-transport.d.ts +13 -2
- package/dist/http-transport.js +35 -10
- package/dist/identity.js +14 -5
- package/dist/index.js +32 -13
- package/dist/token-store.d.ts +17 -0
- package/dist/token-store.js +0 -0
- package/dist/usage-metrics.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface MtBootGates {
|
|
2
|
+
/** True = multi-tenant deployment: the server boots with zero accounts
|
|
3
|
+
* (tenants provision in-band) and the provisioning gate below replaces the
|
|
4
|
+
* flat owner-allowlist requirement. */
|
|
5
|
+
multiTenant: boolean;
|
|
6
|
+
/** Fail-fast replacement for the E_OWNER_EMAILS_REQUIRED check. MUST refuse
|
|
7
|
+
* (exit/throw) when no valid provisioning mechanism is configured — an
|
|
8
|
+
* ungated HTTP endpoint must never boot, whatever the condition checked. */
|
|
9
|
+
assertProvisioningGate: () => void;
|
|
10
|
+
}
|
|
11
|
+
export declare function setMtBootGates(g: MtBootGates | null): void;
|
|
12
|
+
export declare function mtBootGates(): MtBootGates | null;
|
|
13
|
+
export declare function isMultiTenantBoot(): boolean;
|
|
14
|
+
/** BR-4 applies to stdio always, and to HTTP only when single-owner: a
|
|
15
|
+
* multi-tenant HTTP deploy starts empty by design. */
|
|
16
|
+
export declare function accountsAssertRequired(wantStdio: boolean, multiTenant: boolean): boolean;
|
|
17
|
+
/** A set GOOGLE_ACCOUNTS on a multi-tenant boot is refused at DEPLOY time:
|
|
18
|
+
* the env registry is process-wide, so it would graft one operator's accounts
|
|
19
|
+
* onto every tenant and turn every tenant's first wizard call into a dead
|
|
20
|
+
* end (the per-call E_ENV_ACCOUNTS_MODE guard), N confusing failures instead
|
|
21
|
+
* of one clear one. */
|
|
22
|
+
export declare function assertNoEnvAccountsMode(env?: NodeJS.ProcessEnv): void;
|
|
23
|
+
/** Same class, second vector: GOOGLE_OPTIONAL_SCOPES is a process-wide legacy
|
|
24
|
+
* global scope profile, so on a multi-tenant box one operator's scope choice
|
|
25
|
+
* would silently apply to every tenant's consent. */
|
|
26
|
+
export declare function assertNoEnvOptionalScopesMode(env?: NodeJS.ProcessEnv): void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// S1.16: multi-tenant boot gates. The free core never sets these — every
|
|
2
|
+
// helper reduces to today's single-owner behavior when no gates are
|
|
3
|
+
// installed. Deliberately a side-effect-free module (accounts.ts/auth.ts
|
|
4
|
+
// resolve env at import time) so the tenancy module can import it cheaply.
|
|
5
|
+
import { failStartup } from './config-file.js';
|
|
6
|
+
let gates = null;
|
|
7
|
+
export function setMtBootGates(g) {
|
|
8
|
+
gates = g;
|
|
9
|
+
}
|
|
10
|
+
export function mtBootGates() {
|
|
11
|
+
return gates;
|
|
12
|
+
}
|
|
13
|
+
export function isMultiTenantBoot() {
|
|
14
|
+
return gates?.multiTenant === true;
|
|
15
|
+
}
|
|
16
|
+
/** BR-4 applies to stdio always, and to HTTP only when single-owner: a
|
|
17
|
+
* multi-tenant HTTP deploy starts empty by design. */
|
|
18
|
+
export function accountsAssertRequired(wantStdio, multiTenant) {
|
|
19
|
+
return wantStdio || !multiTenant;
|
|
20
|
+
}
|
|
21
|
+
/** A set GOOGLE_ACCOUNTS on a multi-tenant boot is refused at DEPLOY time:
|
|
22
|
+
* the env registry is process-wide, so it would graft one operator's accounts
|
|
23
|
+
* onto every tenant and turn every tenant's first wizard call into a dead
|
|
24
|
+
* end (the per-call E_ENV_ACCOUNTS_MODE guard), N confusing failures instead
|
|
25
|
+
* of one clear one. */
|
|
26
|
+
export function assertNoEnvAccountsMode(env = process.env) {
|
|
27
|
+
if (env.GOOGLE_ACCOUNTS?.trim()) {
|
|
28
|
+
failStartup('E_ENV_ACCOUNTS_MODE_MT', 'GOOGLE_ACCOUNTS must not be set on a multi-tenant deployment; accounts are linked in-band per tenant. Unset it and restart.');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Same class, second vector: GOOGLE_OPTIONAL_SCOPES is a process-wide legacy
|
|
32
|
+
* global scope profile, so on a multi-tenant box one operator's scope choice
|
|
33
|
+
* would silently apply to every tenant's consent. */
|
|
34
|
+
export function assertNoEnvOptionalScopesMode(env = process.env) {
|
|
35
|
+
if (env.GOOGLE_OPTIONAL_SCOPES?.trim()) {
|
|
36
|
+
failStartup('E_ENV_OPTIONAL_SCOPES_MT', 'GOOGLE_OPTIONAL_SCOPES must not be set on a multi-tenant deployment; scopes are chosen per tenant at consent. Unset it and restart.');
|
|
37
|
+
}
|
|
38
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
1
|
import { OAuth2Client } from 'googleapis-common';
|
|
2
|
-
import type { Account } from './accounts.js';
|
|
3
|
-
|
|
2
|
+
import type { Account, AccountSet } from './accounts.js';
|
|
3
|
+
import type { TokenData } from './types.js';
|
|
4
|
+
export interface GetClientDeps {
|
|
5
|
+
/** Live account view — read at call time, never a captured snapshot. */
|
|
6
|
+
accounts: () => AccountSet;
|
|
7
|
+
readToken: (alias: string) => TokenData | null;
|
|
8
|
+
updateToken: (alias: string, updates: object) => void;
|
|
9
|
+
/** Cross-process staleness probe (single-owner global registry only). */
|
|
10
|
+
refreshIfStale?: () => void;
|
|
11
|
+
}
|
|
12
|
+
/** Test-only visibility into the dedupe map. */
|
|
13
|
+
export declare function __inflightSizeForTest(): number;
|
|
14
|
+
/**
|
|
15
|
+
* Per-context client factory: `contextKey` namespaces the single-flight map
|
|
16
|
+
* (one lane per tenant), `deps` carry that context's account view and token
|
|
17
|
+
* custody. The free core builds exactly one, bound to the global registry.
|
|
18
|
+
*/
|
|
19
|
+
export declare function makeGetClient(contextKey: string, deps: GetClientDeps): (account: Account) => Promise<OAuth2Client>;
|
|
20
|
+
/** The single-owner client, bound to the global registry + token store. The
|
|
21
|
+
* operator surfaces that never carry a tenant (api-probe, executor and
|
|
22
|
+
* escape-hatch `deps.getClientFn ?? getClient` defaults, stdio dispatch)
|
|
23
|
+
* resolve through this. */
|
|
24
|
+
export declare const getClient: (account: Account) => Promise<OAuth2Client>;
|
package/dist/client.js
CHANGED
|
Binary file
|
package/dist/http-transport.d.ts
CHANGED
|
@@ -22,6 +22,17 @@ export interface HttpHostOptions {
|
|
|
22
22
|
version: string;
|
|
23
23
|
ownerConfigured: boolean;
|
|
24
24
|
authenticate: Authenticator;
|
|
25
|
+
/** Tenant-resolution seam: map the verified subject to ITS server. Absent
|
|
26
|
+
* (free core) = every request dispatches to the one boot `server`. Return
|
|
27
|
+
* null for an unknown subject -> 403 tenant_not_found. Each distinct
|
|
28
|
+
* resolved server gets its own serialize key. */
|
|
29
|
+
resolveServer?: (claims: {
|
|
30
|
+
sub: string;
|
|
31
|
+
}) => Promise<{
|
|
32
|
+
server: McpServer;
|
|
33
|
+
} | null> | {
|
|
34
|
+
server: McpServer;
|
|
35
|
+
} | null;
|
|
25
36
|
/** Extra routes keyed by exact pathname (AS endpoints mount here in B13). */
|
|
26
37
|
routes?: Record<string, RouteHandler>;
|
|
27
38
|
log?: (line: string) => void;
|
|
@@ -53,9 +64,9 @@ export declare function jsonRpcMethod(body: unknown): string;
|
|
|
53
64
|
export declare class HttpTransportHost {
|
|
54
65
|
private readonly opts;
|
|
55
66
|
private httpServer?;
|
|
56
|
-
private
|
|
67
|
+
private readonly locks;
|
|
57
68
|
constructor(opts: HttpHostOptions);
|
|
58
|
-
private
|
|
69
|
+
private serializeFor;
|
|
59
70
|
private log;
|
|
60
71
|
start(): Promise<void>;
|
|
61
72
|
close(): Promise<void>;
|
package/dist/http-transport.js
CHANGED
|
@@ -43,18 +43,20 @@ export function jsonRpcMethod(body) {
|
|
|
43
43
|
export class HttpTransportHost {
|
|
44
44
|
opts;
|
|
45
45
|
httpServer;
|
|
46
|
-
// Serialize the connect→dispatch critical section:
|
|
46
|
+
// Serialize the connect→dispatch critical section PER KEY: 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
|
-
// concurrency
|
|
50
|
-
//
|
|
51
|
-
lock
|
|
49
|
+
// concurrency, so all traffic to ONE server runs under one mutex. The free
|
|
50
|
+
// core uses a single key (one boot server); with resolveServer each subject
|
|
51
|
+
// gets its own server and therefore its own independent lock lane.
|
|
52
|
+
locks = new Map();
|
|
52
53
|
constructor(opts) {
|
|
53
54
|
this.opts = opts;
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
56
|
+
serializeFor(key, fn) {
|
|
57
|
+
const prev = this.locks.get(key) ?? Promise.resolve();
|
|
58
|
+
const run = prev.then(fn, fn);
|
|
59
|
+
this.locks.set(key, run.then(() => undefined, () => undefined));
|
|
58
60
|
return run;
|
|
59
61
|
}
|
|
60
62
|
log(line) {
|
|
@@ -149,6 +151,24 @@ export class HttpTransportHost {
|
|
|
149
151
|
this.log(`${auth.status} auth_failed path=/mcp`);
|
|
150
152
|
return;
|
|
151
153
|
}
|
|
154
|
+
const sub = auth.sub ?? 'owner';
|
|
155
|
+
// Tenant resolution (the seam EE fills): map the verified subject to ITS
|
|
156
|
+
// server. Absent resolver = the one boot server for every subject.
|
|
157
|
+
let target = { server: this.opts.server };
|
|
158
|
+
if (this.opts.resolveServer) {
|
|
159
|
+
try {
|
|
160
|
+
target = await this.opts.resolveServer({ sub });
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
target = null;
|
|
164
|
+
}
|
|
165
|
+
if (!target) {
|
|
166
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
167
|
+
res.end(JSON.stringify({ error: 'tenant_not_found', message: 'no tenant is provisioned for this subject' }));
|
|
168
|
+
this.log('403 tenant_not_found path=/mcp');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
152
172
|
// Thread the verified subject to tool handlers: the Node transport forwards
|
|
153
173
|
// req.auth verbatim as ctx.http.authInfo. Token/clientId stay empty — the
|
|
154
174
|
// bearer value must not re-enter the dispatch path via handler context.
|
|
@@ -156,7 +176,7 @@ export class HttpTransportHost {
|
|
|
156
176
|
token: '',
|
|
157
177
|
clientId: '',
|
|
158
178
|
scopes: ['mcp:use'],
|
|
159
|
-
extra: { sub
|
|
179
|
+
extra: { sub },
|
|
160
180
|
};
|
|
161
181
|
let body;
|
|
162
182
|
try {
|
|
@@ -184,7 +204,12 @@ export class HttpTransportHost {
|
|
|
184
204
|
// releases. (A shared initialized-state persists across stateless requests;
|
|
185
205
|
// benign for the single-owner design.)
|
|
186
206
|
const deadlineMs = this.opts.dispatchTimeoutMs ?? DISPATCH_TIMEOUT_DEFAULT;
|
|
187
|
-
|
|
207
|
+
// The lock lane must match the SERVER identity, not the subject: without a
|
|
208
|
+
// resolver every subject shares the one boot server, so keying on sub
|
|
209
|
+
// would let two subjects race one server through the connect gap.
|
|
210
|
+
const serializeKey = this.opts.resolveServer ? sub : '__single__';
|
|
211
|
+
const mcpServer = target.server;
|
|
212
|
+
await this.serializeFor(serializeKey, async () => {
|
|
188
213
|
const disconnected = new Promise((resolve) => res.once('close', () => resolve('closed')));
|
|
189
214
|
let timer;
|
|
190
215
|
const timedOut = new Promise((resolve) => {
|
|
@@ -195,7 +220,7 @@ export class HttpTransportHost {
|
|
|
195
220
|
// so the tap above it still classifies the original validation prose.
|
|
196
221
|
const enveloped = withValidationEnvelope(transport, this.opts.validationEnvelope ?? {});
|
|
197
222
|
const tapped = this.opts.metricsTap ? this.opts.metricsTap(enveloped) : enveloped;
|
|
198
|
-
await
|
|
223
|
+
await mcpServer.connect(this.opts.argShapeFor || this.opts.strictArgs
|
|
199
224
|
? withArgNormalization(tapped, this.opts.argShapeFor ?? (() => undefined), this.opts.log, this.opts.onArgRename, this.opts.strictArgs)
|
|
200
225
|
: tapped);
|
|
201
226
|
// Reflect the dispatch into a non-rejecting arm: if the deadline wins the
|
package/dist/identity.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { getAccountSet } from './accounts.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { getAccountSet, getTokenDir, refreshAccountSetIfStale } from './accounts.js';
|
|
2
|
+
import { makeGetClient } from './client.js';
|
|
3
|
+
import { createTokenStore } from './token-store.js';
|
|
4
4
|
import { resolvePolicy } from './write-control.js';
|
|
5
5
|
export function buildIdentityContext(env = process.env, opts = {}) {
|
|
6
|
+
// Real closures over the single-owner state (the SAME factories EE
|
|
7
|
+
// instantiates per tenant), bound here to the global registry + token dir —
|
|
8
|
+
// behavior-identical to the module-level functions for the free core.
|
|
9
|
+
const tokenStore = createTokenStore(getTokenDir());
|
|
6
10
|
return {
|
|
7
11
|
subject: 'owner',
|
|
8
12
|
// Live getter: the seam must always see the current registry, never a
|
|
@@ -13,7 +17,12 @@ export function buildIdentityContext(env = process.env, opts = {}) {
|
|
|
13
17
|
// The dispatch transport rides into the resolved policy as a reserved seam
|
|
14
18
|
// (cc-write-control B14); it does not change any write-control verdict.
|
|
15
19
|
policy: resolvePolicy(env, { transport: opts.transport }),
|
|
16
|
-
getClient,
|
|
17
|
-
|
|
20
|
+
getClient: makeGetClient('owner', {
|
|
21
|
+
accounts: getAccountSet,
|
|
22
|
+
readToken: tokenStore.readToken,
|
|
23
|
+
updateToken: tokenStore.updateToken,
|
|
24
|
+
refreshIfStale: refreshAccountSetIfStale,
|
|
25
|
+
}),
|
|
26
|
+
tokenStore,
|
|
18
27
|
};
|
|
19
28
|
}
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
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
4
|
import { assertServerAccountsConfigured, getAccountSet } from './accounts.js';
|
|
5
|
+
import { accountsAssertRequired, assertNoEnvAccountsMode, assertNoEnvOptionalScopesMode, isMultiTenantBoot, mtBootGates } from './boot-gates.js';
|
|
5
6
|
import { readFileSync } from 'node:fs';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
7
8
|
import path from 'node:path';
|
|
@@ -198,14 +199,8 @@ async function main() {
|
|
|
198
199
|
}
|
|
199
200
|
return;
|
|
200
201
|
}
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
// (env / migrate-config / account import / auth) before the server runs.
|
|
204
|
-
assertServerAccountsConfigured();
|
|
205
|
-
// Resolve (or provision) the master key BEFORE serving: the hard guard is
|
|
206
|
-
// specced "fatal at startup", never mid-dispatch.
|
|
207
|
-
const { resolveMasterKey } = await import('./master-key.js');
|
|
208
|
-
resolveMasterKey();
|
|
202
|
+
// Transport derivation FIRST: the accounts gate below is transport-aware
|
|
203
|
+
// (a multi-tenant HTTP deploy legitimately boots with zero accounts).
|
|
209
204
|
const { resolveHttpConfig, transportIncludesHttp } = await import('./http-config.js');
|
|
210
205
|
let httpCfg;
|
|
211
206
|
try {
|
|
@@ -217,6 +212,21 @@ async function main() {
|
|
|
217
212
|
}
|
|
218
213
|
const wantStdio = httpCfg.transport === 'stdio' || httpCfg.transport === 'both';
|
|
219
214
|
const wantHttp = transportIncludesHttp(httpCfg.transport);
|
|
215
|
+
// BR-4: the SERVER never boots with an empty registry — stdio always, HTTP
|
|
216
|
+
// when single-owner. The bootstrap and diagnostic CLIs above already
|
|
217
|
+
// returned. A multi-tenant boot instead refuses the two process-wide env
|
|
218
|
+
// vectors that would leak one operator's accounts/scopes into every tenant.
|
|
219
|
+
const mt = isMultiTenantBoot();
|
|
220
|
+
if (accountsAssertRequired(wantStdio, mt))
|
|
221
|
+
assertServerAccountsConfigured();
|
|
222
|
+
if (mt) {
|
|
223
|
+
assertNoEnvAccountsMode();
|
|
224
|
+
assertNoEnvOptionalScopesMode();
|
|
225
|
+
}
|
|
226
|
+
// Resolve (or provision) the master key BEFORE serving: the hard guard is
|
|
227
|
+
// specced "fatal at startup", never mid-dispatch.
|
|
228
|
+
const { resolveMasterKey } = await import('./master-key.js');
|
|
229
|
+
resolveMasterKey();
|
|
220
230
|
// Local usage metrics: fail-closed resolve, self-announcing source, null
|
|
221
231
|
// when off (no wrapper, no dir, nothing initializes). One instance per
|
|
222
232
|
// transport so `boots` keys stay honest under `both`.
|
|
@@ -282,12 +292,21 @@ async function main() {
|
|
|
282
292
|
}
|
|
283
293
|
if (wantHttp) {
|
|
284
294
|
const { HttpTransportHost, parseOwnerEmails } = await import('./http-transport.js');
|
|
285
|
-
// BR3 / C13: the owner allowlist is the entire multi-tenant collapse; refuse
|
|
286
|
-
// to open an ungated HTTP endpoint.
|
|
287
295
|
const owners = parseOwnerEmails(process.env);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
296
|
+
const gates = mtBootGates();
|
|
297
|
+
if (gates?.multiTenant) {
|
|
298
|
+
// Same fail-fast SHAPE, different condition: an ungated HTTP endpoint
|
|
299
|
+
// must never boot; under tenancy the gate is "a valid provisioning
|
|
300
|
+
// mechanism exists", not a flat owner allowlist.
|
|
301
|
+
gates.assertProvisioningGate();
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
// BR3 / C13: the owner allowlist is the entire multi-tenant collapse;
|
|
305
|
+
// refuse to open an ungated HTTP endpoint.
|
|
306
|
+
if (owners.length === 0) {
|
|
307
|
+
process.stderr.write('E_OWNER_EMAILS_REQUIRED: MCP_TRANSPORT includes http but MCP_OWNER_EMAILS is empty. Set MCP_OWNER_EMAILS to the Google email(s) allowed to authenticate.\n');
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
291
310
|
}
|
|
292
311
|
// BR7: stateless HTTP cannot push tools/list_changed, so it forces curated.
|
|
293
312
|
const configuredMode = (process.env.GOOGLE_DISCOVERY ?? '').trim().toLowerCase();
|
package/dist/token-store.d.ts
CHANGED
|
@@ -7,3 +7,20 @@ export declare function readToken(alias: string): TokenData | null;
|
|
|
7
7
|
export declare function writeToken(alias: string, data: object): void;
|
|
8
8
|
export declare function updateToken(alias: string, updates: object): void;
|
|
9
9
|
export declare function hasToken(alias: string): boolean;
|
|
10
|
+
export interface TokenStore {
|
|
11
|
+
readToken(alias: string): TokenData | null;
|
|
12
|
+
writeToken(alias: string, data: object): void;
|
|
13
|
+
updateToken(alias: string, updates: object): void;
|
|
14
|
+
hasToken(alias: string): boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Context-scoped token store: `.enc` files live under `dir`, ciphertext keyed
|
|
18
|
+
* by `tenantId` when present (HKDF v2 envelope; omitted = the single-owner v1
|
|
19
|
+
* envelope). Same lock + atomic-write + error contract as the module-level
|
|
20
|
+
* functions: readToken returns null ONLY when the file is missing and throws
|
|
21
|
+
* on a decrypt failure.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createTokenStore(dir: string, opts?: {
|
|
24
|
+
tenantId?: string;
|
|
25
|
+
resolveKey?: () => string;
|
|
26
|
+
}): TokenStore;
|
package/dist/token-store.js
CHANGED
|
Binary file
|
package/dist/usage-metrics.js
CHANGED
|
@@ -44,7 +44,7 @@ export const KNOWN_ERROR_SLUGS = new Set([
|
|
|
44
44
|
'insufficient_scope', 'internal', 'invalid_client', 'invalid_client_metadata',
|
|
45
45
|
'invalid_grant', 'invalid_params', 'invalid_query', 'invalid_request',
|
|
46
46
|
'invalid_scope', 'network_error', 'not_found', 'rate_limited',
|
|
47
|
-
'reauth_required', 'recipient_not_allowed', 'too_large', 'toolset_disabled', 'unknown_api',
|
|
47
|
+
'reauth_required', 'recipient_not_allowed', 'tenant_not_found', 'too_large', 'toolset_disabled', 'unknown_api',
|
|
48
48
|
'unknown_argument', 'unknown_method', 'unsupported_grant_type', 'unsupported_type',
|
|
49
49
|
'untrusted_host', 'upstream_error', 'validation_error', 'write_disabled',
|
|
50
50
|
]);
|
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.42",
|
|
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",
|