mcp-google-multi 6.0.0-beta.2 → 6.0.0-beta.4
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/README.md +5 -5
- package/dist/accounts.d.ts +20 -4
- package/dist/accounts.js +68 -44
- package/dist/api-probe.d.ts +5 -0
- package/dist/api-probe.js +20 -15
- package/dist/arg-normalize.js +19 -38
- package/dist/arg-strict.d.ts +7 -0
- package/dist/arg-strict.js +38 -28
- package/dist/auth.d.ts +5 -4
- package/dist/auth.js +16 -23
- package/dist/boot-gates.d.ts +26 -0
- package/dist/boot-gates.js +36 -0
- package/dist/cimd-cache.d.ts +38 -0
- package/dist/cimd-cache.js +111 -0
- package/dist/client-config.d.ts +2 -2
- package/dist/client-config.js +8 -14
- package/dist/client.d.ts +32 -2
- package/dist/client.js +0 -0
- package/dist/compose.d.ts +18 -0
- package/dist/compose.js +109 -0
- package/dist/config-file.d.ts +13 -0
- package/dist/config-file.js +41 -10
- package/dist/discover.js +7 -10
- package/dist/discovery-client.d.ts +3 -0
- package/dist/discovery-client.js +36 -32
- package/dist/doctor.d.ts +9 -5
- package/dist/doctor.js +119 -29
- package/dist/env-load.js +2 -5
- package/dist/executor.d.ts +3 -0
- package/dist/executor.js +6 -6
- package/dist/fanout.d.ts +9 -1
- package/dist/fanout.js +47 -22
- package/dist/fs-atomic.d.ts +8 -0
- package/dist/fs-atomic.js +216 -29
- package/dist/hint-provider.d.ts +129 -0
- package/dist/hint-provider.js +58 -0
- package/dist/http-config.d.ts +2 -2
- package/dist/http-config.js +3 -7
- package/dist/http-transport.d.ts +67 -9
- package/dist/http-transport.js +318 -52
- package/dist/identity.d.ts +6 -3
- package/dist/identity.js +24 -8
- package/dist/index.js +76 -160
- package/dist/master-key.d.ts +3 -3
- package/dist/master-key.js +16 -19
- package/dist/mcp-token.d.ts +126 -19
- package/dist/mcp-token.js +557 -76
- package/dist/metrics-cli.d.ts +1 -1
- package/dist/metrics-cli.js +1 -1
- package/dist/metrics-tap.js +2 -5
- package/dist/migrate-config.d.ts +1 -2
- package/dist/migrate-config.js +5 -7
- package/dist/net-tuning.js +2 -4
- package/dist/oauth-as.d.ts +104 -4
- package/dist/oauth-as.js +368 -114
- package/dist/oauth-consent.d.ts +10 -0
- package/dist/oauth-consent.js +20 -10
- package/dist/outbound-allowlist.js +9 -11
- package/dist/reauth-hint.d.ts +1 -1
- package/dist/reauth-hint.js +11 -10
- package/dist/registry-transfer.d.ts +5 -0
- package/dist/registry-transfer.js +63 -11
- package/dist/registry.d.ts +13 -4
- package/dist/registry.js +100 -72
- package/dist/scope-catalog.d.ts +16 -0
- package/dist/scope-catalog.js +40 -13
- package/dist/scope-observability.d.ts +10 -9
- package/dist/scope-observability.js +34 -24
- package/dist/services.d.ts +18 -10
- package/dist/services.js +61 -22
- package/dist/ssrf-guard.d.ts +23 -9
- package/dist/ssrf-guard.js +106 -78
- package/dist/tenant-purge.d.ts +26 -0
- package/dist/tenant-purge.js +65 -0
- package/dist/token-store.d.ts +20 -2
- package/dist/token-store.js +0 -0
- package/dist/tools/_coerce.d.ts +4 -4
- package/dist/tools/_errors.d.ts +3 -0
- package/dist/tools/_errors.js +72 -60
- package/dist/tools/_local-files.d.ts +9 -0
- package/dist/tools/_local-files.js +22 -8
- package/dist/tools/account-wizard.d.ts +13 -3
- package/dist/tools/account-wizard.js +107 -56
- package/dist/tools/accounts-tool.d.ts +8 -1
- package/dist/tools/accounts-tool.js +35 -19
- package/dist/tools/admin.d.ts +10 -1
- package/dist/tools/admin.js +14 -11
- package/dist/tools/analytics.d.ts +10 -1
- package/dist/tools/analytics.js +14 -10
- package/dist/tools/calendar.d.ts +2 -1
- package/dist/tools/calendar.js +19 -18
- package/dist/tools/chat.d.ts +10 -1
- package/dist/tools/chat.js +12 -8
- package/dist/tools/contacts.d.ts +2 -1
- package/dist/tools/contacts.js +22 -19
- package/dist/tools/docs.d.ts +2 -1
- package/dist/tools/docs.js +35 -36
- package/dist/tools/drive.d.ts +2 -1
- package/dist/tools/drive.js +80 -74
- package/dist/tools/forms.d.ts +10 -1
- package/dist/tools/forms.js +18 -12
- package/dist/tools/generated/_shared.d.ts +2 -1
- package/dist/tools/generated/_shared.js +7 -3
- package/dist/tools/generated/admin.d.ts +2 -1
- package/dist/tools/generated/admin.js +265 -265
- package/dist/tools/generated/analytics.d.ts +2 -1
- package/dist/tools/generated/analytics.js +125 -125
- package/dist/tools/generated/appsmarket.d.ts +2 -1
- package/dist/tools/generated/appsmarket.js +5 -5
- package/dist/tools/generated/calendar.d.ts +2 -1
- package/dist/tools/generated/calendar.js +55 -55
- package/dist/tools/generated/chat.d.ts +2 -1
- package/dist/tools/generated/chat.js +95 -95
- package/dist/tools/generated/classroom.d.ts +2 -1
- package/dist/tools/generated/classroom.js +209 -209
- package/dist/tools/generated/cloudidentity.d.ts +2 -1
- package/dist/tools/generated/cloudidentity.js +133 -133
- package/dist/tools/generated/cloudsearch.d.ts +2 -1
- package/dist/tools/generated/cloudsearch.js +99 -99
- package/dist/tools/generated/contacts.d.ts +2 -1
- package/dist/tools/generated/contacts.js +29 -29
- package/dist/tools/generated/drive.d.ts +2 -1
- package/dist/tools/generated/drive.js +71 -71
- package/dist/tools/generated/driveactivity.d.ts +2 -1
- package/dist/tools/generated/driveactivity.js +3 -3
- package/dist/tools/generated/drivelabels.d.ts +2 -1
- package/dist/tools/generated/drivelabels.js +53 -53
- package/dist/tools/generated/forms.d.ts +2 -1
- package/dist/tools/generated/forms.js +7 -7
- package/dist/tools/generated/gmail.d.ts +2 -1
- package/dist/tools/generated/gmail.js +117 -117
- package/dist/tools/generated/groupsmigration.d.ts +2 -1
- package/dist/tools/generated/groupsmigration.js +3 -3
- package/dist/tools/generated/groupssettings.d.ts +2 -1
- package/dist/tools/generated/groupssettings.js +7 -7
- package/dist/tools/generated/index.d.ts +2 -1
- package/dist/tools/generated/keep.d.ts +2 -1
- package/dist/tools/generated/keep.js +15 -15
- package/dist/tools/generated/licensing.d.ts +2 -1
- package/dist/tools/generated/licensing.js +15 -15
- package/dist/tools/generated/meet.d.ts +2 -1
- package/dist/tools/generated/meet.js +27 -27
- package/dist/tools/generated/postmaster.d.ts +2 -1
- package/dist/tools/generated/postmaster.js +9 -9
- package/dist/tools/generated/reseller.d.ts +2 -1
- package/dist/tools/generated/reseller.js +35 -35
- package/dist/tools/generated/script.d.ts +2 -1
- package/dist/tools/generated/script.js +33 -33
- package/dist/tools/generated/searchconsole.d.ts +2 -1
- package/dist/tools/generated/searchconsole.js +3 -3
- package/dist/tools/generated/sheets.d.ts +2 -1
- package/dist/tools/generated/sheets.js +15 -15
- package/dist/tools/generated/tasks.d.ts +2 -1
- package/dist/tools/generated/tasks.js +5 -5
- package/dist/tools/generated/vault.d.ts +2 -1
- package/dist/tools/generated/vault.js +67 -67
- package/dist/tools/generated/workspaceevents.d.ts +2 -1
- package/dist/tools/generated/workspaceevents.js +31 -31
- package/dist/tools/gmail-mime.d.ts +1 -1
- package/dist/tools/gmail-mime.js +7 -12
- package/dist/tools/gmail.d.ts +4 -3
- package/dist/tools/gmail.js +67 -69
- package/dist/tools/google-api.d.ts +2 -3
- package/dist/tools/google-api.js +34 -22
- package/dist/tools/meet.d.ts +10 -1
- package/dist/tools/meet.js +12 -9
- package/dist/tools/searchconsole.d.ts +2 -1
- package/dist/tools/searchconsole.js +16 -13
- package/dist/tools/sheets.d.ts +2 -1
- package/dist/tools/sheets.js +35 -32
- package/dist/tools/slides.d.ts +10 -1
- package/dist/tools/slides.js +16 -10
- package/dist/tools/tasks.d.ts +2 -1
- package/dist/tools/tasks.js +18 -15
- package/dist/trim.d.ts +5 -0
- package/dist/trim.js +19 -0
- package/dist/usage-metrics.d.ts +23 -0
- package/dist/usage-metrics.js +67 -56
- package/dist/write-control.d.ts +4 -8
- package/dist/write-control.js +13 -6
- package/package.json +47 -21
package/dist/arg-strict.js
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
// Unknown-argument screening
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// reproduced: the typo, the correct name and a wholly invented key all
|
|
6
|
-
// returned byte-identical responses.
|
|
7
|
-
//
|
|
8
|
-
// This module is pure and holds no registry reference. It never rewrites a key
|
|
9
|
-
// and never touches a VALUE: it only decides that a key is undeclared, and
|
|
10
|
-
// suggests what the caller probably meant. Rewriting would risk sending a real
|
|
11
|
-
// value to Google on a guess, which is strictly worse than refusing.
|
|
12
|
-
import { editDistance } from './scope-catalog.js';
|
|
1
|
+
// Unknown-argument screening: zod strips undeclared keys before a handler
|
|
2
|
+
// runs, so a misremembered name used to produce a SUCCESS with wrong behavior. See docs/internals.md.
|
|
3
|
+
import { editDistance, SUGGEST_MAX_INPUT } from './scope-catalog.js';
|
|
4
|
+
import { sliceClean } from './trim.js';
|
|
13
5
|
/**
|
|
14
6
|
* `GOOGLE_ARG_UNKNOWN`: reject | warn | off. A typo in the VALUE still falls
|
|
15
7
|
* back to `warn`, not to the default: an operator who misspells the setting
|
|
@@ -25,24 +17,15 @@ export function unknownArgMode(env = process.env) {
|
|
|
25
17
|
process.stderr.write(`GOOGLE_ARG_UNKNOWN="${raw}" is not valid (reject | warn | off); using warn\n`);
|
|
26
18
|
return 'warn';
|
|
27
19
|
}
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
// zod before the handler runs, so `warn` means the CLIENT sees a confident
|
|
31
|
-
// success for a call the server did not perform: drive_list with a misspelled
|
|
32
|
-
// folder key returned the My Drive root, byte-identical to no argument at all.
|
|
33
|
-
// A wrong answer that reads as right is the failure mode agents recover from
|
|
34
|
-
// worst, so the default refuses the call and says what it probably meant.
|
|
20
|
+
// `warn` means the client sees a confident success for a call the server
|
|
21
|
+
// didn't perform; a wrong answer that reads as right is the worst failure mode to recover from.
|
|
35
22
|
const DEFAULT_MODE = 'reject';
|
|
36
23
|
/** Tools that legitimately accept open-ended top-level keys. Empty at 6.0.0:
|
|
37
24
|
* the escape hatch is NOT one, because its open-endedness lives in the VALUES
|
|
38
25
|
* of queryParams/body, never in its six fixed top-level keys. */
|
|
39
26
|
export const STRICT_EXEMPT_TOOLS = new Set();
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
* no arguments; the others are call-plumbing that bridges and proxies have
|
|
43
|
-
* been observed to fold into `arguments` instead of `params._meta`, where the
|
|
44
|
-
* spec puts them. Rejecting these would fail a call the model got right.
|
|
45
|
-
* Measured against the full 978-tool surface: none is a declared key. */
|
|
27
|
+
// Keys a CLIENT adds on its own, not keys the model chose (a probe some
|
|
28
|
+
// clients send to argument-less tools, plus call-plumbing bridges fold into `arguments`).
|
|
46
29
|
const CLIENT_ARTIFACT_KEYS = new Set([
|
|
47
30
|
'random_string',
|
|
48
31
|
'toolCallId',
|
|
@@ -59,6 +42,20 @@ export function isExemptKey(key) {
|
|
|
59
42
|
* alone; they match far too much to be useful. */
|
|
60
43
|
const GENERIC = new Set(['id', 'name', 'title', 'body', 'parent', 'text', 'content', 'type', 'value', 'key', 'data']);
|
|
61
44
|
const flatten = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
45
|
+
/** A call can carry thousands of unknown keys, or one of megabytes: only the
|
|
46
|
+
* first few earn a did-you-mean, and the log line and the envelope echo a
|
|
47
|
+
* bounded prefix of each name. The reject decision itself is never capped. */
|
|
48
|
+
export const MAX_SUGGESTED_KEYS = 8;
|
|
49
|
+
const MAX_ECHO_CHARS = 64;
|
|
50
|
+
export function echoName(name) {
|
|
51
|
+
return name.length > MAX_ECHO_CHARS ? `${sliceClean(name, MAX_ECHO_CHARS)}...` : name;
|
|
52
|
+
}
|
|
53
|
+
/** The first MAX_SUGGESTED_KEYS names, each capped, then "and N more". */
|
|
54
|
+
export function echoNames(names, quote = false) {
|
|
55
|
+
const shown = names.slice(0, MAX_SUGGESTED_KEYS).map((n) => (quote ? `"${echoName(n)}"` : echoName(n)));
|
|
56
|
+
const rest = names.length - shown.length;
|
|
57
|
+
return rest > 0 ? `${shown.join(', ')} and ${rest} more` : shown.join(', ');
|
|
58
|
+
}
|
|
62
59
|
function tokens(s) {
|
|
63
60
|
return s
|
|
64
61
|
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
@@ -76,6 +73,8 @@ function tokens(s) {
|
|
|
76
73
|
* distance 6, well past any sane threshold, which is why tier 1 exists.
|
|
77
74
|
*/
|
|
78
75
|
export function suggestKeys(unknown, declared, limit = 2) {
|
|
76
|
+
if (unknown.length > SUGGEST_MAX_INPUT)
|
|
77
|
+
return [];
|
|
79
78
|
const uFlat = flatten(unknown);
|
|
80
79
|
const uTok = tokens(unknown);
|
|
81
80
|
const tiers = [[], [], [], []];
|
|
@@ -94,8 +93,12 @@ export function suggestKeys(unknown, declared, limit = 2) {
|
|
|
94
93
|
tiers[2].push({ key, rank: Math.abs(kFlat.length - uFlat.length) });
|
|
95
94
|
continue;
|
|
96
95
|
}
|
|
96
|
+
const maxDist = Math.max(1, Math.floor(uFlat.length / 4));
|
|
97
|
+
// The distance is at least the length gap: skip the DP when that alone fails.
|
|
98
|
+
if (Math.abs(uFlat.length - kFlat.length) > maxDist)
|
|
99
|
+
continue;
|
|
97
100
|
const d = editDistance(uFlat, kFlat);
|
|
98
|
-
if (d <=
|
|
101
|
+
if (d <= maxDist)
|
|
99
102
|
tiers[3].push({ key, rank: d });
|
|
100
103
|
}
|
|
101
104
|
const tier = tiers.find((t) => t.length > 0);
|
|
@@ -117,10 +120,17 @@ export function screenArguments(tool, args, declared) {
|
|
|
117
120
|
if (STRICT_EXEMPT_TOOLS.has(tool) || declared.length === 0)
|
|
118
121
|
return out;
|
|
119
122
|
const declaredSet = new Set(declared);
|
|
123
|
+
// Tier 0 is a map lookup, so a redundant twin is recognised past the
|
|
124
|
+
// ranking budget too; a near-miss duplicate still needs that budget.
|
|
125
|
+
const byFlat = new Map();
|
|
126
|
+
for (const k of declared)
|
|
127
|
+
byFlat.set(flatten(k), [...(byFlat.get(flatten(k)) ?? []), k]);
|
|
128
|
+
let ranked = 0;
|
|
120
129
|
for (const key of Object.keys(args)) {
|
|
121
130
|
if (declaredSet.has(key) || isExemptKey(key))
|
|
122
131
|
continue;
|
|
123
|
-
const
|
|
132
|
+
const twins = key.length <= SUGGEST_MAX_INPUT ? byFlat.get(flatten(key)) : undefined;
|
|
133
|
+
const suggestions = twins ? twins.slice(0, 2) : ranked++ < MAX_SUGGESTED_KEYS ? suggestKeys(key, declared) : [];
|
|
124
134
|
// The caller sent both spellings, so the declared one already won and the
|
|
125
135
|
// outcome is what it has always been. Never fail a call that works today.
|
|
126
136
|
if (suggestions.some((s) => s in args)) {
|
|
@@ -133,7 +143,7 @@ export function screenArguments(tool, args, declared) {
|
|
|
133
143
|
}
|
|
134
144
|
/** The agent-facing envelope. Values are never included, only key names. */
|
|
135
145
|
export function unknownArgEnvelope(tool, unknown, declared, account, siblings = []) {
|
|
136
|
-
const names = unknown.map((u) =>
|
|
146
|
+
const names = echoNames(unknown.map((u) => u.sent), true);
|
|
137
147
|
const accepts = `This tool accepts: ${declared.join(', ')}.`;
|
|
138
148
|
const suggested = unknown.flatMap((u) => u.suggestions);
|
|
139
149
|
let hint;
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
+
import type { AccountSet } from './accounts.js';
|
|
1
2
|
import { ADMIN_SCOPES } from './scope-catalog.js';
|
|
2
3
|
export declare const BASE_SCOPES: string[];
|
|
3
4
|
export declare const OPTIONAL_SCOPE_BUNDLES: Record<string, string[]>;
|
|
4
5
|
export { ADMIN_SCOPES };
|
|
5
6
|
/** Union of every account's resolved bundles: a service registers if ANY
|
|
6
|
-
* account can authorize it; per-account authz happens at call time
|
|
7
|
-
export declare function getOptionalBundles(): string[];
|
|
7
|
+
* account can authorize it; per-account authz happens at call time. */
|
|
8
|
+
export declare function getOptionalBundles(set?: AccountSet): string[];
|
|
8
9
|
/** Aliases granted ADMIN_SCOPES: per-account admin flag (env
|
|
9
10
|
* GOOGLE_ADMIN_ACCOUNTS overrides config.json at resolve) OR the account's
|
|
10
11
|
* scope profile carrying admin (boolean or "admin" bundle) — equivalent forms. */
|
|
11
|
-
export declare function getAdminAccounts(): string[];
|
|
12
|
+
export declare function getAdminAccounts(set?: AccountSet): string[];
|
|
12
13
|
/** Scopes are fixed at consent time: changing an account's profile (or the
|
|
13
14
|
* legacy env) changes its consent set and requires re-running auth. Evaluated
|
|
14
15
|
* per account: `work` can carry admin + gmail_settings while `personal` is
|
|
15
16
|
* never asked for them. */
|
|
16
|
-
export declare function resolveScopesForAccount(alias: string): string[];
|
|
17
|
+
export declare function resolveScopesForAccount(alias: string, set?: AccountSet): string[];
|
|
17
18
|
export declare function runAuthFlow(args: string[]): Promise<void>;
|
package/dist/auth.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { openUrl } from './open-url.js';
|
|
3
3
|
import { ACCOUNTS, getAccountSet } from './accounts.js';
|
|
4
|
-
import { ADMIN_SCOPES, BUNDLE_CATALOG, closestBundle, resolveBundleAliases } from './scope-catalog.js';
|
|
4
|
+
import { ADMIN_SCOPES, BUNDLE_CATALOG, closestBundle, isKnownBundle, resolveBundleAliases } from './scope-catalog.js';
|
|
5
5
|
import { resolveMasterKey } from './master-key.js';
|
|
6
6
|
import { writeToken } from './token-store.js';
|
|
7
7
|
import { buildConsentClient, openLoopbackConsent, TESTING_MODE_WARNING } from './oauth-consent.js';
|
|
@@ -31,12 +31,8 @@ function parseCsvEnv(name) {
|
|
|
31
31
|
.map(s => s.trim())
|
|
32
32
|
.filter(Boolean);
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* implicit "legacy-global" profile applied to every account (env wins over
|
|
37
|
-
* file profiles, cc-config R2). Unknown names now fail loudly (BR3) where v5
|
|
38
|
-
* silently dropped them — the intended migration signal.
|
|
39
|
-
*/
|
|
34
|
+
// A set GOOGLE_OPTIONAL_SCOPES acts as an implicit profile applied to every
|
|
35
|
+
// account (env wins over file profiles); unknown names fail loudly.
|
|
40
36
|
function legacyGlobalProfile() {
|
|
41
37
|
const names = parseCsvEnv('GOOGLE_OPTIONAL_SCOPES');
|
|
42
38
|
if (names.length === 0)
|
|
@@ -48,33 +44,31 @@ function legacyGlobalProfile() {
|
|
|
48
44
|
if (bundle === 'admin') {
|
|
49
45
|
throw new Error('E_UNKNOWN_BUNDLE: "admin" is not a global bundle: grant it per account via GOOGLE_ADMIN_ACCOUNTS or an "admin: true" scope profile.');
|
|
50
46
|
}
|
|
51
|
-
if (!(bundle
|
|
47
|
+
if (!isKnownBundle(bundle)) {
|
|
52
48
|
const hint = closestBundle(bundle);
|
|
53
49
|
throw new Error(`E_UNKNOWN_BUNDLE: unknown bundle "${bundle}" in GOOGLE_OPTIONAL_SCOPES${hint ? ` — did you mean "${hint}"?` : ''}`);
|
|
54
50
|
}
|
|
55
51
|
}
|
|
56
52
|
return { bundles };
|
|
57
53
|
}
|
|
58
|
-
function profileForAccount(alias) {
|
|
54
|
+
function profileForAccount(alias, set = getAccountSet()) {
|
|
59
55
|
const legacy = legacyGlobalProfile();
|
|
60
56
|
if (legacy)
|
|
61
57
|
return legacy;
|
|
62
|
-
const set = getAccountSet();
|
|
63
58
|
const name = set.configs[alias]?.scopeProfile ?? 'base';
|
|
64
59
|
// hasOwn: a profile named like an Object.prototype member must never
|
|
65
60
|
// resolve to the inherited function.
|
|
66
61
|
return Object.hasOwn(set.scopeProfiles, name) ? set.scopeProfiles[name] : { bundles: [] };
|
|
67
62
|
}
|
|
68
63
|
/** Union of every account's resolved bundles: a service registers if ANY
|
|
69
|
-
* account can authorize it; per-account authz happens at call time
|
|
70
|
-
export function getOptionalBundles() {
|
|
64
|
+
* account can authorize it; per-account authz happens at call time. */
|
|
65
|
+
export function getOptionalBundles(set = getAccountSet()) {
|
|
71
66
|
const legacy = legacyGlobalProfile();
|
|
72
67
|
if (legacy)
|
|
73
68
|
return legacy.bundles.filter(b => b !== 'admin');
|
|
74
69
|
const union = new Set();
|
|
75
|
-
const set = getAccountSet();
|
|
76
70
|
for (const alias of set.aliases) {
|
|
77
|
-
for (const b of profileForAccount(alias).bundles) {
|
|
71
|
+
for (const b of profileForAccount(alias, set).bundles) {
|
|
78
72
|
if (b !== 'admin')
|
|
79
73
|
union.add(b);
|
|
80
74
|
}
|
|
@@ -84,12 +78,12 @@ export function getOptionalBundles() {
|
|
|
84
78
|
/** Aliases granted ADMIN_SCOPES: per-account admin flag (env
|
|
85
79
|
* GOOGLE_ADMIN_ACCOUNTS overrides config.json at resolve) OR the account's
|
|
86
80
|
* scope profile carrying admin (boolean or "admin" bundle) — equivalent forms. */
|
|
87
|
-
export function getAdminAccounts() {
|
|
88
|
-
const { aliases, configs } =
|
|
81
|
+
export function getAdminAccounts(set = getAccountSet()) {
|
|
82
|
+
const { aliases, configs } = set;
|
|
89
83
|
return aliases.filter((a) => {
|
|
90
84
|
if (configs[a].admin === true)
|
|
91
85
|
return true;
|
|
92
|
-
const p = profileForAccount(a);
|
|
86
|
+
const p = profileForAccount(a, set);
|
|
93
87
|
return p.admin === true || p.bundles.includes('admin');
|
|
94
88
|
});
|
|
95
89
|
}
|
|
@@ -97,15 +91,15 @@ export function getAdminAccounts() {
|
|
|
97
91
|
* legacy env) changes its consent set and requires re-running auth. Evaluated
|
|
98
92
|
* per account: `work` can carry admin + gmail_settings while `personal` is
|
|
99
93
|
* never asked for them. */
|
|
100
|
-
export function resolveScopesForAccount(alias) {
|
|
101
|
-
const profile = profileForAccount(alias);
|
|
94
|
+
export function resolveScopesForAccount(alias, set = getAccountSet()) {
|
|
95
|
+
const profile = profileForAccount(alias, set);
|
|
102
96
|
const scopes = profile.includesBase === false ? [] : [...BASE_SCOPES];
|
|
103
97
|
for (const bundle of profile.bundles) {
|
|
104
98
|
if (bundle === 'admin')
|
|
105
99
|
continue;
|
|
106
100
|
scopes.push(...BUNDLE_CATALOG[bundle].scopes);
|
|
107
101
|
}
|
|
108
|
-
if (getAdminAccounts().includes(alias)) {
|
|
102
|
+
if (getAdminAccounts(set).includes(alias)) {
|
|
109
103
|
scopes.push(...ADMIN_SCOPES);
|
|
110
104
|
}
|
|
111
105
|
return Array.from(new Set(scopes));
|
|
@@ -132,9 +126,8 @@ export async function runAuthFlow(args) {
|
|
|
132
126
|
// Auto-provisions on a fresh install (env > keychain > file > generate);
|
|
133
127
|
// resolves eagerly so a provisioning failure surfaces before the browser opens.
|
|
134
128
|
resolveMasterKey();
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
// errors propagate to main()'s fatal handler like every other CLI failure.
|
|
129
|
+
// Listen first, then build the auth URL from the assigned redirect; errors
|
|
130
|
+
// propagate to main()'s fatal handler like every other CLI failure.
|
|
138
131
|
const loop = await openLoopbackConsent({ timeoutMs: 10 * 60_000 });
|
|
139
132
|
const oauth2Client = buildConsentClient(loop.redirect);
|
|
140
133
|
// CSRF protection for the OAuth callback (RFC 6749 §10.12).
|
|
@@ -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
|
+
/** The empty-registry guard 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,36 @@
|
|
|
1
|
+
// Multi-tenant boot gates: every helper reduces to single-owner behavior when
|
|
2
|
+
// no gates are installed. Deliberately side-effect-free, unlike accounts.ts/auth.ts.
|
|
3
|
+
import { failStartup } from './config-file.js';
|
|
4
|
+
let gates = null;
|
|
5
|
+
export function setMtBootGates(g) {
|
|
6
|
+
gates = g;
|
|
7
|
+
}
|
|
8
|
+
export function mtBootGates() {
|
|
9
|
+
return gates;
|
|
10
|
+
}
|
|
11
|
+
export function isMultiTenantBoot() {
|
|
12
|
+
return gates?.multiTenant === true;
|
|
13
|
+
}
|
|
14
|
+
/** The empty-registry guard applies to stdio always, and to HTTP only when single-owner: a
|
|
15
|
+
* multi-tenant HTTP deploy starts empty by design. */
|
|
16
|
+
export function accountsAssertRequired(wantStdio, multiTenant) {
|
|
17
|
+
return wantStdio || !multiTenant;
|
|
18
|
+
}
|
|
19
|
+
/** A set GOOGLE_ACCOUNTS on a multi-tenant boot is refused at DEPLOY time:
|
|
20
|
+
* the env registry is process-wide, so it would graft one operator's accounts
|
|
21
|
+
* onto every tenant and turn every tenant's first wizard call into a dead
|
|
22
|
+
* end (the per-call E_ENV_ACCOUNTS_MODE guard), N confusing failures instead
|
|
23
|
+
* of one clear one. */
|
|
24
|
+
export function assertNoEnvAccountsMode(env = process.env) {
|
|
25
|
+
if (env.GOOGLE_ACCOUNTS?.trim()) {
|
|
26
|
+
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.');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Same class, second vector: GOOGLE_OPTIONAL_SCOPES is a process-wide legacy
|
|
30
|
+
* global scope profile, so on a multi-tenant box one operator's scope choice
|
|
31
|
+
* would silently apply to every tenant's consent. */
|
|
32
|
+
export function assertNoEnvOptionalScopesMode(env = process.env) {
|
|
33
|
+
if (env.GOOGLE_OPTIONAL_SCOPES?.trim()) {
|
|
34
|
+
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.');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const CIMD_TTL_MS: number;
|
|
2
|
+
/** The oldest validated document still served while the cap is full or the issuer cannot answer. */
|
|
3
|
+
export declare const CIMD_MAX_STALE_MS: number;
|
|
4
|
+
/** Fetches at once. A lookup can outlive the fetch that gave up on it, so ssrf-guard caps its own lookups apart. */
|
|
5
|
+
export declare const CIMD_MAX_IN_FLIGHT = 4;
|
|
6
|
+
export declare const CIMD_CACHE_MAX = 256;
|
|
7
|
+
export type CimdDocument = Record<string, unknown>;
|
|
8
|
+
type CimdError = {
|
|
9
|
+
error: 'E_CIMD_SSRF_BLOCKED' | 'E_CIMD_INVALID';
|
|
10
|
+
};
|
|
11
|
+
/** `fresh` is true only for the request whose fetch produced the document. */
|
|
12
|
+
export type CimdResolution = {
|
|
13
|
+
doc: CimdDocument;
|
|
14
|
+
fresh: boolean;
|
|
15
|
+
} | {
|
|
16
|
+
busy: true;
|
|
17
|
+
} | CimdError;
|
|
18
|
+
export interface CimdCacheDeps {
|
|
19
|
+
fetch: (clientId: string) => Promise<CimdDocument>;
|
|
20
|
+
now: () => number;
|
|
21
|
+
log: (line: string) => void;
|
|
22
|
+
}
|
|
23
|
+
export declare class CimdClientCache {
|
|
24
|
+
private readonly deps;
|
|
25
|
+
private readonly entries;
|
|
26
|
+
private readonly inFlight;
|
|
27
|
+
private busyLoggedAt;
|
|
28
|
+
private busySuppressed;
|
|
29
|
+
constructor(deps: CimdCacheDeps);
|
|
30
|
+
get size(): number;
|
|
31
|
+
/** `validate` runs once per fetch, in the request that started it, and every
|
|
32
|
+
* request waiting on that fetch gets the same answer. */
|
|
33
|
+
resolve(clientId: string, validate: (doc: CimdDocument) => boolean): Promise<CimdResolution>;
|
|
34
|
+
private fetchFor;
|
|
35
|
+
private store;
|
|
36
|
+
private noteBusy;
|
|
37
|
+
}
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Bounds the unauthenticated-fetch surface of GET /authorize: capped
|
|
2
|
+
// in-flight fetches, one fetch per client_id, no error caching. See docs/internals.md.
|
|
3
|
+
import { CimdHttpError, isTransientFetchError, SsrfBlockedError } from './ssrf-guard.js';
|
|
4
|
+
import { logSafe } from './trim.js';
|
|
5
|
+
export const CIMD_TTL_MS = 5 * 60_000;
|
|
6
|
+
/** The oldest validated document still served while the cap is full or the issuer cannot answer. */
|
|
7
|
+
export const CIMD_MAX_STALE_MS = 24 * 60 * 60_000;
|
|
8
|
+
/** Fetches at once. A lookup can outlive the fetch that gave up on it, so ssrf-guard caps its own lookups apart. */
|
|
9
|
+
export const CIMD_MAX_IN_FLIGHT = 4;
|
|
10
|
+
export const CIMD_CACHE_MAX = 256;
|
|
11
|
+
const BUSY_LOG_EVERY_MS = 60_000;
|
|
12
|
+
export class CimdClientCache {
|
|
13
|
+
deps;
|
|
14
|
+
entries = new Map();
|
|
15
|
+
// One entry per running fetch, so its size is the number of permits taken.
|
|
16
|
+
inFlight = new Map();
|
|
17
|
+
busyLoggedAt = -Infinity;
|
|
18
|
+
busySuppressed = 0;
|
|
19
|
+
constructor(deps) {
|
|
20
|
+
this.deps = deps;
|
|
21
|
+
}
|
|
22
|
+
get size() {
|
|
23
|
+
return this.entries.size;
|
|
24
|
+
}
|
|
25
|
+
/** `validate` runs once per fetch, in the request that started it, and every
|
|
26
|
+
* request waiting on that fetch gets the same answer. */
|
|
27
|
+
async resolve(clientId, validate) {
|
|
28
|
+
const entry = this.entries.get(clientId);
|
|
29
|
+
const age = entry ? this.deps.now() - entry.at : 0;
|
|
30
|
+
if (entry && age < CIMD_TTL_MS)
|
|
31
|
+
return { doc: entry.doc, fresh: false };
|
|
32
|
+
if (entry && age >= CIMD_MAX_STALE_MS)
|
|
33
|
+
this.entries.delete(clientId);
|
|
34
|
+
const running = this.inFlight.get(clientId);
|
|
35
|
+
if (running) {
|
|
36
|
+
const out = await running;
|
|
37
|
+
return 'doc' in out ? { doc: out.doc, fresh: false } : out;
|
|
38
|
+
}
|
|
39
|
+
if (this.inFlight.size >= CIMD_MAX_IN_FLIGHT) {
|
|
40
|
+
// A flood of fresh paths is exactly when the cap is full, and a client
|
|
41
|
+
// that signed in before must still be able to.
|
|
42
|
+
const stale = this.entries.get(clientId);
|
|
43
|
+
if (stale)
|
|
44
|
+
return { doc: stale.doc, fresh: false };
|
|
45
|
+
this.noteBusy();
|
|
46
|
+
return { busy: true };
|
|
47
|
+
}
|
|
48
|
+
return this.fetchFor(clientId, validate);
|
|
49
|
+
}
|
|
50
|
+
async fetchFor(clientId, validate) {
|
|
51
|
+
// Never rejects: neither core nor a host installs an unhandledRejection
|
|
52
|
+
// handler, and a waiter that stopped listening must not take the process down.
|
|
53
|
+
let settle;
|
|
54
|
+
this.inFlight.set(clientId, new Promise((r) => (settle = r)));
|
|
55
|
+
let out = { error: 'E_CIMD_INVALID' };
|
|
56
|
+
try {
|
|
57
|
+
const doc = await this.deps.fetch(clientId);
|
|
58
|
+
if (validate(doc)) {
|
|
59
|
+
this.store(clientId, doc);
|
|
60
|
+
out = { doc, fresh: true };
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
this.entries.delete(clientId);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
out = { error: e instanceof SsrfBlockedError ? 'E_CIMD_SSRF_BLOCKED' : 'E_CIMD_INVALID' };
|
|
68
|
+
// stale-if-error: an issuer that's down/overloaded says nothing about
|
|
69
|
+
// the document, so the last good one stays and serves this fetch too.
|
|
70
|
+
const stale = this.entries.get(clientId);
|
|
71
|
+
if (!saysNothingAboutDocument(e))
|
|
72
|
+
this.entries.delete(clientId);
|
|
73
|
+
else if (stale && this.deps.now() - stale.at < CIMD_MAX_STALE_MS)
|
|
74
|
+
out = { doc: stale.doc, fresh: false };
|
|
75
|
+
// The detail goes to the log only; the caller gets a generic answer.
|
|
76
|
+
this.deps.log(`CIMD fetch failed for ${logSafe(clientId, 128)}: ${logSafe(e instanceof Error ? e.message : 'non-Error rejection', 200)}`);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
this.inFlight.delete(clientId);
|
|
80
|
+
settle(out);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
store(clientId, doc) {
|
|
85
|
+
this.entries.delete(clientId);
|
|
86
|
+
while (this.entries.size >= CIMD_CACHE_MAX) {
|
|
87
|
+
const oldest = this.entries.keys().next().value;
|
|
88
|
+
if (oldest === undefined)
|
|
89
|
+
break;
|
|
90
|
+
this.entries.delete(oldest);
|
|
91
|
+
}
|
|
92
|
+
this.entries.set(clientId, { doc, at: this.deps.now() });
|
|
93
|
+
}
|
|
94
|
+
// Over-cap requests arrive at whatever rate the caller likes.
|
|
95
|
+
noteBusy() {
|
|
96
|
+
const now = this.deps.now();
|
|
97
|
+
if (now >= this.busyLoggedAt && now - this.busyLoggedAt < BUSY_LOG_EVERY_MS) {
|
|
98
|
+
this.busySuppressed++;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const suppressed = this.busySuppressed;
|
|
102
|
+
this.busyLoggedAt = now;
|
|
103
|
+
this.busySuppressed = 0;
|
|
104
|
+
this.deps.log(`CIMD busy: in-flight cap reached${suppressed > 0 ? ` (n=${suppressed} suppressed)` : ''}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function saysNothingAboutDocument(e) {
|
|
108
|
+
if (isTransientFetchError(e))
|
|
109
|
+
return true;
|
|
110
|
+
return e instanceof CimdHttpError && (e.status === 408 || e.status === 429 || e.status >= 500);
|
|
111
|
+
}
|
package/dist/client-config.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export interface EntryOptions {
|
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
40
|
* The client `mcpServers` entry. stdio needs no secret env: the server loads
|
|
41
|
-
* its own .env from ~/.config/mcp-google-multi
|
|
41
|
+
* its own .env from ~/.config/mcp-google-multi, so we never
|
|
42
42
|
* inline MASTER_KEY / GOOGLE_CLIENT_SECRET into a world-readable client config.
|
|
43
43
|
*/
|
|
44
44
|
export declare function buildServerEntry(opts: EntryOptions): ServerEntry;
|
|
@@ -73,7 +73,7 @@ export type ApplyResult = {
|
|
|
73
73
|
* the prior file, update-in-place if the key exists.
|
|
74
74
|
*/
|
|
75
75
|
export declare function applyFileEntry(client: ClientInfo, name: string, entry: ServerEntry, deps?: ClientDeps): ApplyResult;
|
|
76
|
-
/** Resolve stdio-vs-http mode from the current transport config
|
|
76
|
+
/** Resolve stdio-vs-http mode from the current transport config.
|
|
77
77
|
* http/both → a `url` entry; stdio → a `command` entry. */
|
|
78
78
|
export declare function resolveMode(httpConfig?: {
|
|
79
79
|
transport: 'stdio' | 'http' | 'both';
|
package/dist/client-config.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// (detection + entry building + idempotent apply), deps-injectable for tests.
|
|
4
|
-
// Consent + non-TTY behavior live in the CLI/tool surfaces; secrets are NEVER
|
|
5
|
-
// inlined — the server self-loads its .env from ~/.config/mcp-google-multi, so
|
|
6
|
-
// a stdio entry needs no secret env at all (distribution.md B15).
|
|
1
|
+
// Registers the server with the user's MCP client so they never hand-edit
|
|
2
|
+
// JSON. Pure core (detection + entry building + idempotent apply), deps-injectable for tests.
|
|
7
3
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
8
4
|
import * as readline from 'node:readline';
|
|
9
5
|
import path from 'node:path';
|
|
@@ -20,9 +16,8 @@ function resolve(deps) {
|
|
|
20
16
|
mkdirp: deps.mkdirp ?? ((dir) => mkdirSync(dir, { recursive: true })),
|
|
21
17
|
};
|
|
22
18
|
}
|
|
23
|
-
// Build paths for the TARGET platform's separators, not the running host's,
|
|
24
|
-
// the injected `platform` is honored deterministically
|
|
25
|
-
// must still produce posix paths for a linux target, and vice versa).
|
|
19
|
+
// Build paths for the TARGET platform's separators, not the running host's,
|
|
20
|
+
// so the injected `platform` is honored deterministically.
|
|
26
21
|
function pp(r) {
|
|
27
22
|
return r.platform === 'win32' ? path.win32 : path.posix;
|
|
28
23
|
}
|
|
@@ -78,7 +73,7 @@ export function detectClients(deps = {}) {
|
|
|
78
73
|
}
|
|
79
74
|
/**
|
|
80
75
|
* The client `mcpServers` entry. stdio needs no secret env: the server loads
|
|
81
|
-
* its own .env from ~/.config/mcp-google-multi
|
|
76
|
+
* its own .env from ~/.config/mcp-google-multi, so we never
|
|
82
77
|
* inline MASTER_KEY / GOOGLE_CLIENT_SECRET into a world-readable client config.
|
|
83
78
|
*/
|
|
84
79
|
export function buildServerEntry(opts) {
|
|
@@ -155,9 +150,8 @@ export function applyFileEntry(client, name, entry, deps = {}) {
|
|
|
155
150
|
const had = Object.prototype.hasOwnProperty.call(prevServers, name);
|
|
156
151
|
const next = { ...existing, mcpServers: { ...prevServers, [name]: entry } };
|
|
157
152
|
r.mkdirp(pp(r).dirname(p));
|
|
158
|
-
// Back up
|
|
159
|
-
//
|
|
160
|
-
// backup + direct write is sufficient — our own config.json uses fs-atomic).
|
|
153
|
+
// Back up before rewriting so a bad write is recoverable; our own
|
|
154
|
+
// config.json uses fs-atomic, but these small foreign configs don't need it.
|
|
161
155
|
let backup;
|
|
162
156
|
if (priorRaw !== null) {
|
|
163
157
|
backup = `${p}.bak`;
|
|
@@ -166,7 +160,7 @@ export function applyFileEntry(client, name, entry, deps = {}) {
|
|
|
166
160
|
r.writeFile(p, `${JSON.stringify(next, null, 2)}\n`);
|
|
167
161
|
return { ok: true, client: client.id, action: had ? 'updated' : 'added', path: p, backup };
|
|
168
162
|
}
|
|
169
|
-
/** Resolve stdio-vs-http mode from the current transport config
|
|
163
|
+
/** Resolve stdio-vs-http mode from the current transport config.
|
|
170
164
|
* http/both → a `url` entry; stdio → a `command` entry. */
|
|
171
165
|
export function resolveMode(httpConfig) {
|
|
172
166
|
if (httpConfig && (httpConfig.transport === 'http' || httpConfig.transport === 'both')) {
|
package/dist/client.d.ts
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
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
|
+
/** Injection surface for curated tool files: absent = the single-owner
|
|
21
|
+
* getClient below (today's behavior); a tenant context passes its own. */
|
|
22
|
+
export interface CuratedToolDeps {
|
|
23
|
+
getClientFn?: typeof getClient;
|
|
24
|
+
/** Whether tools may read or write the server host's disk (local uploads,
|
|
25
|
+
* downloads, attachments). Absent = true; the host belongs to its operator,
|
|
26
|
+
* so a non-owner context passes false. */
|
|
27
|
+
localFiles?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** The single-owner client, bound to the global registry + token store. The
|
|
30
|
+
* operator surfaces that never carry a tenant (api-probe, executor and
|
|
31
|
+
* escape-hatch `deps.getClientFn ?? getClient` defaults) resolve through
|
|
32
|
+
* this. */
|
|
33
|
+
export declare const getClient: (account: Account) => Promise<OAuth2Client>;
|
package/dist/client.js
CHANGED
|
Binary file
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
2
|
+
import type { ServerTarget } from './http-transport.js';
|
|
3
|
+
import { ToolRegistry, type DiscoveryMode } from './registry.js';
|
|
4
|
+
import { type IdentityContext } from './identity.js';
|
|
5
|
+
import type { Metrics } from './usage-metrics.js';
|
|
6
|
+
export interface McpServerOptions {
|
|
7
|
+
/** Sent in the initialize result; omitted entirely when absent or blank. */
|
|
8
|
+
instructions?: string;
|
|
9
|
+
}
|
|
10
|
+
/** The one place the MCP server is constructed, so an embedding host can pass
|
|
11
|
+
* per-session `instructions` without re-deriving the server identity. */
|
|
12
|
+
export declare function createMcpServer(version: string, opts?: McpServerOptions): McpServer;
|
|
13
|
+
export declare function buildRegistry(server: McpServer, ctx: IdentityContext, mode?: DiscoveryMode, metrics?: Metrics | null): ToolRegistry;
|
|
14
|
+
/** The per-request hooks bound to one registry: argument shapes, unknown-
|
|
15
|
+
* argument screening and the account a validation envelope names. Both
|
|
16
|
+
* transports and every resolved server target build them here, so a mistyped
|
|
17
|
+
* argument behaves the same wherever it arrives. */
|
|
18
|
+
export declare function requestHooksFor(registry: ToolRegistry, ctx: Pick<IdentityContext, 'accounts'>, metrics?: Metrics | null, env?: NodeJS.ProcessEnv): Omit<ServerTarget, 'server'>;
|