mcp-google-multi 6.0.0-alpha.53 → 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/api-probe.d.ts +5 -0
- package/dist/api-probe.js +20 -15
- package/dist/auth.d.ts +1 -1
- package/dist/auth.js +3 -3
- package/dist/client.d.ts +6 -2
- package/dist/client.js +0 -0
- package/dist/compose.d.ts +7 -1
- package/dist/compose.js +49 -7
- package/dist/config-file.js +3 -2
- package/dist/doctor.d.ts +3 -3
- package/dist/doctor.js +38 -10
- package/dist/executor.d.ts +3 -0
- package/dist/executor.js +1 -1
- package/dist/http-transport.d.ts +15 -6
- package/dist/http-transport.js +26 -14
- package/dist/identity.d.ts +1 -0
- package/dist/identity.js +11 -1
- package/dist/index.js +12 -34
- package/dist/registry.d.ts +3 -0
- package/dist/registry.js +6 -1
- package/dist/scope-observability.d.ts +8 -6
- package/dist/tools/_local-files.d.ts +9 -0
- package/dist/tools/_local-files.js +18 -0
- package/dist/tools/accounts-tool.d.ts +7 -0
- package/dist/tools/accounts-tool.js +21 -12
- package/dist/tools/drive.js +21 -10
- package/dist/tools/generated/index.d.ts +2 -1
- package/dist/tools/gmail.js +16 -8
- package/dist/tools/google-api.d.ts +2 -3
- package/dist/tools/google-api.js +1 -1
- package/package.json +2 -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/api-probe.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getClient } from './client.js';
|
|
2
|
+
import { readToken } from './token-store.js';
|
|
1
3
|
import type { ApiProbeResult } from './doctor.js';
|
|
2
4
|
export interface ApiProbeSpec {
|
|
3
5
|
service: string;
|
|
@@ -15,4 +17,7 @@ export interface ApiProbeDeps {
|
|
|
15
17
|
grantedScopes: (alias: string) => string[];
|
|
16
18
|
request: (alias: string, url: string) => Promise<void>;
|
|
17
19
|
}
|
|
20
|
+
/** Probe deps over one context's token reads and client; the module-level
|
|
21
|
+
* pair is the single owner's. */
|
|
22
|
+
export declare function apiProbeDepsFor(readTokenFn: typeof readToken, getClientFn: typeof getClient): ApiProbeDeps;
|
|
18
23
|
export declare function probeApiEnablement(alias: string, deps?: ApiProbeDeps): Promise<ApiProbeResult[]>;
|
package/dist/api-probe.js
CHANGED
|
@@ -24,21 +24,26 @@ export const API_PROBES = [
|
|
|
24
24
|
export function planProbes(granted, probes = API_PROBES) {
|
|
25
25
|
return probes.filter((p) => granted.some((s) => p.scopePrefixes.some((prefix) => s.startsWith(prefix))));
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
27
|
+
/** Probe deps over one context's token reads and client; the module-level
|
|
28
|
+
* pair is the single owner's. */
|
|
29
|
+
export function apiProbeDepsFor(readTokenFn, getClientFn) {
|
|
30
|
+
return {
|
|
31
|
+
grantedScopes: (alias) => {
|
|
32
|
+
try {
|
|
33
|
+
const scope = readTokenFn(alias)?.scope;
|
|
34
|
+
return typeof scope === 'string' ? scope.split(' ').filter(Boolean) : [];
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
request: async (alias, url) => {
|
|
41
|
+
const auth = await getClientFn(alias);
|
|
42
|
+
await auth.request({ url, timeout: 10_000 });
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const DEFAULT_DEPS = apiProbeDepsFor(readToken, getClient);
|
|
42
47
|
export async function probeApiEnablement(alias, deps = DEFAULT_DEPS) {
|
|
43
48
|
const results = [];
|
|
44
49
|
for (const spec of planProbes(deps.grantedScopes(alias))) {
|
package/dist/auth.d.ts
CHANGED
|
@@ -14,5 +14,5 @@ export declare function getAdminAccounts(set?: AccountSet): string[];
|
|
|
14
14
|
* legacy env) changes its consent set and requires re-running auth. Evaluated
|
|
15
15
|
* per account: `work` can carry admin + gmail_settings while `personal` is
|
|
16
16
|
* never asked for them. */
|
|
17
|
-
export declare function resolveScopesForAccount(alias: string): string[];
|
|
17
|
+
export declare function resolveScopesForAccount(alias: string, set?: AccountSet): string[];
|
|
18
18
|
export declare function runAuthFlow(args: string[]): Promise<void>;
|
package/dist/auth.js
CHANGED
|
@@ -95,15 +95,15 @@ export function getAdminAccounts(set = getAccountSet()) {
|
|
|
95
95
|
* legacy env) changes its consent set and requires re-running auth. Evaluated
|
|
96
96
|
* per account: `work` can carry admin + gmail_settings while `personal` is
|
|
97
97
|
* never asked for them. */
|
|
98
|
-
export function resolveScopesForAccount(alias) {
|
|
99
|
-
const profile = profileForAccount(alias);
|
|
98
|
+
export function resolveScopesForAccount(alias, set = getAccountSet()) {
|
|
99
|
+
const profile = profileForAccount(alias, set);
|
|
100
100
|
const scopes = profile.includesBase === false ? [] : [...BASE_SCOPES];
|
|
101
101
|
for (const bundle of profile.bundles) {
|
|
102
102
|
if (bundle === 'admin')
|
|
103
103
|
continue;
|
|
104
104
|
scopes.push(...BUNDLE_CATALOG[bundle].scopes);
|
|
105
105
|
}
|
|
106
|
-
if (getAdminAccounts().includes(alias)) {
|
|
106
|
+
if (getAdminAccounts(set).includes(alias)) {
|
|
107
107
|
scopes.push(...ADMIN_SCOPES);
|
|
108
108
|
}
|
|
109
109
|
return Array.from(new Set(scopes));
|
package/dist/client.d.ts
CHANGED
|
@@ -21,9 +21,13 @@ export declare function makeGetClient(contextKey: string, deps: GetClientDeps):
|
|
|
21
21
|
* getClient below (today's behavior); a tenant context passes its own. */
|
|
22
22
|
export interface CuratedToolDeps {
|
|
23
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;
|
|
24
28
|
}
|
|
25
29
|
/** The single-owner client, bound to the global registry + token store. The
|
|
26
30
|
* operator surfaces that never carry a tenant (api-probe, executor and
|
|
27
|
-
* escape-hatch `deps.getClientFn ?? getClient` defaults
|
|
28
|
-
*
|
|
31
|
+
* escape-hatch `deps.getClientFn ?? getClient` defaults) resolve through
|
|
32
|
+
* this. */
|
|
29
33
|
export declare const getClient: (account: Account) => Promise<OAuth2Client>;
|
package/dist/client.js
CHANGED
|
Binary file
|
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
|
-
import type
|
|
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,20 +4,36 @@
|
|
|
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';
|
|
11
|
-
import { registerAccountTools } from './tools/accounts-tool.js';
|
|
13
|
+
import { accountHealthDepsFor, registerAccountTools } from './tools/accounts-tool.js';
|
|
12
14
|
import { registerDiagnoseTool } from './doctor.js';
|
|
13
15
|
import { registerAccountWizardTools } from './tools/account-wizard.js';
|
|
14
16
|
import { getToolsets, toolsetEnabled } from './toolsets.js';
|
|
17
|
+
import { isOwnerContext } from './identity.js';
|
|
18
|
+
import { resolveScopesForAccount } from './auth.js';
|
|
15
19
|
export function buildRegistry(server, ctx, mode, metrics = null) {
|
|
16
20
|
const policy = ctx.policy;
|
|
17
21
|
// The registry's account view IS the context's: fan-out expansion,
|
|
18
22
|
// selector validation and default-account injection all follow this
|
|
19
23
|
// context, not the process global (identical for the free core's one ctx).
|
|
20
24
|
const registry = new ToolRegistry(server, policy, mode, metrics, () => ctx.accounts);
|
|
25
|
+
// Every handler resolves its client, token reads and scope hints through
|
|
26
|
+
// this context; ctx.accounts is read per call, never snapshotted here, so
|
|
27
|
+
// the owner's live getter stays live.
|
|
28
|
+
const owner = isOwnerContext(ctx);
|
|
29
|
+
const clientDeps = { getClientFn: ctx.getClient, localFiles: owner };
|
|
30
|
+
const execDeps = {
|
|
31
|
+
getClientFn: ctx.getClient,
|
|
32
|
+
scopeDeps: {
|
|
33
|
+
readTokenFn: ctx.tokenStore.readToken,
|
|
34
|
+
profileFn: (alias) => resolveScopesForAccount(alias, ctx.accounts),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
21
37
|
const toolsets = getToolsets();
|
|
22
38
|
if (toolsets !== 'all') {
|
|
23
39
|
const known = new Set([...SERVICES.map((s) => s.name), ...GENERATED_SERVICES.map((s) => s.name)]);
|
|
@@ -37,7 +53,7 @@ export function buildRegistry(server, ctx, mode, metrics = null) {
|
|
|
37
53
|
}
|
|
38
54
|
continue;
|
|
39
55
|
}
|
|
40
|
-
svc.register(registry);
|
|
56
|
+
svc.register(registry, clientDeps);
|
|
41
57
|
}
|
|
42
58
|
for (const gen of GENERATED_SERVICES) {
|
|
43
59
|
if (!toolsetEnabled(toolsets, gen.name))
|
|
@@ -50,7 +66,7 @@ export function buildRegistry(server, ctx, mode, metrics = null) {
|
|
|
50
66
|
}
|
|
51
67
|
continue;
|
|
52
68
|
}
|
|
53
|
-
gen.register(registry);
|
|
69
|
+
gen.register(registry, execDeps);
|
|
54
70
|
}
|
|
55
71
|
if (registry.services().length === 0) {
|
|
56
72
|
const known = [...new Set([...SERVICES.map((s) => s.name), ...GENERATED_SERVICES.map((s) => s.name)])].sort();
|
|
@@ -59,9 +75,35 @@ export function buildRegistry(server, ctx, mode, metrics = null) {
|
|
|
59
75
|
`Note: optional services need their bundle in an account's scope profile (or legacy GOOGLE_OPTIONAL_SCOPES); admin needs an admin account/profile.`);
|
|
60
76
|
}
|
|
61
77
|
registerDiscoverTools(registry, policy);
|
|
62
|
-
registerEscapeTools(registry, policy);
|
|
63
|
-
registerAccountTools(registry);
|
|
78
|
+
registerEscapeTools(registry, policy, execDeps);
|
|
79
|
+
registerAccountTools(registry, accountHealthDepsFor(ctx.tokenStore));
|
|
64
80
|
registerDiagnoseTool(registry, ctx);
|
|
65
|
-
|
|
81
|
+
// The wizard edits the owner's config.json and token store; any other
|
|
82
|
+
// context has neither.
|
|
83
|
+
if (owner)
|
|
84
|
+
registerAccountWizardTools(registry, server);
|
|
66
85
|
return registry;
|
|
67
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/config-file.js
CHANGED
|
@@ -14,9 +14,10 @@ export function tenantsDir(env = process.env) {
|
|
|
14
14
|
return path.join(configDir(env), 'tenants');
|
|
15
15
|
}
|
|
16
16
|
// A tenant id becomes a filesystem path segment exactly like an account
|
|
17
|
-
// alias, so it rides the same traversal guard.
|
|
17
|
+
// alias, so it rides the same traversal guard. 'owner' is the single-owner
|
|
18
|
+
// context's subject and client lane, so no tenant may take it.
|
|
18
19
|
function assertTenantId(tenantId) {
|
|
19
|
-
if (!ALIAS_RE.test(tenantId) || isReservedAlias(tenantId)) {
|
|
20
|
+
if (!ALIAS_RE.test(tenantId) || isReservedAlias(tenantId) || tenantId === 'owner') {
|
|
20
21
|
throw new Error(`E_TENANT_ID_INVALID: invalid tenant id "${tenantId}".`);
|
|
21
22
|
}
|
|
22
23
|
}
|
package/dist/doctor.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ToolRegistry } from './registry.js';
|
|
2
2
|
import { getAccountSet } from './accounts.js';
|
|
3
3
|
import { type AccountHealth } from './tools/accounts-tool.js';
|
|
4
|
+
import { type IdentityContext } from './identity.js';
|
|
4
5
|
import { peekMasterKeyProvenance } from './master-key.js';
|
|
5
6
|
import { type HttpConfig } from './http-config.js';
|
|
6
7
|
export type Verdict = 'ok' | 'warn' | 'fail' | 'unknown';
|
|
@@ -68,6 +69,7 @@ export declare function renderDoctorText(report: DiagnosticsReport): string;
|
|
|
68
69
|
export declare function renderReport(report: DiagnosticsReport, version: string): string;
|
|
69
70
|
/** Exit non-zero when any section FAILs; with strict, WARN counts too. */
|
|
70
71
|
export declare function exitCodeFor(report: DiagnosticsReport, strict: boolean): number;
|
|
72
|
+
export type DiagnoseContext = Pick<IdentityContext, 'subject' | 'accounts' | 'getClient' | 'tokenStore'>;
|
|
71
73
|
/** Agent-callable structured health report (read-only). Mirrors `doctor`'s
|
|
72
74
|
* engine. Registered as a META tool, like `account_list`: it introspects this
|
|
73
75
|
* server rather than Google data, and as a normal tool it became a service of
|
|
@@ -75,9 +77,7 @@ export declare function exitCodeFor(report: DiagnosticsReport, strict: boolean):
|
|
|
75
77
|
* from already-registered services, and this runs after that), so it was
|
|
76
78
|
* advertised only once something else expanded the surface. The README sends
|
|
77
79
|
* people here when they are stuck, so it has to be findable. */
|
|
78
|
-
export declare function registerDiagnoseTool(registry: ToolRegistry, ctx?:
|
|
79
|
-
subject: string;
|
|
80
|
-
}): void;
|
|
80
|
+
export declare function registerDiagnoseTool(registry: ToolRegistry, ctx?: DiagnoseContext): void;
|
|
81
81
|
export interface ResetOptions {
|
|
82
82
|
account?: string;
|
|
83
83
|
regenerateKey: boolean;
|
package/dist/doctor.js
CHANGED
|
@@ -2,14 +2,15 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import * as readline from 'node:readline';
|
|
4
4
|
import { getAccountSet } from './accounts.js';
|
|
5
|
-
import { deriveAccountHealth } from './tools/accounts-tool.js';
|
|
5
|
+
import { accountHealthDepsFor, deriveAccountHealth } from './tools/accounts-tool.js';
|
|
6
|
+
import { isOwnerContext } from './identity.js';
|
|
6
7
|
import { peekMasterKeyProvenance, deleteMasterKeyMaterial } from './master-key.js';
|
|
7
8
|
import { hasToken } from './token-store.js';
|
|
8
9
|
import { configDir, loadConfigFile } from './config-file.js';
|
|
9
10
|
import { envValueSource } from './env-load.js';
|
|
10
11
|
import { reauthHint } from './reauth-hint.js';
|
|
11
12
|
import { describeMetricsDir, resolveUsageMetrics, sourceLabel } from './usage-metrics.js';
|
|
12
|
-
import { probeApiEnablement } from './api-probe.js';
|
|
13
|
+
import { apiProbeDepsFor, probeApiEnablement } from './api-probe.js';
|
|
13
14
|
import { safeMessage, stringifyEnvelope } from './tools/_errors.js';
|
|
14
15
|
import { resolveHttpConfig, HttpConfigError } from './http-config.js';
|
|
15
16
|
import { parseOwnerEmails } from './http-transport.js';
|
|
@@ -83,7 +84,7 @@ function sectionRuntime(deps) {
|
|
|
83
84
|
}
|
|
84
85
|
return { id: 1, title: 'Runtime', verdict: 'ok', lines: [`Node.js ${deps.nodeVersion} (>= ${MIN_NODE_MAJOR})`] };
|
|
85
86
|
}
|
|
86
|
-
function sectionConfig(deps, set) {
|
|
87
|
+
function sectionConfig(deps, set, tenantScope = false) {
|
|
87
88
|
const lines = [];
|
|
88
89
|
let verdict = 'ok';
|
|
89
90
|
let hint;
|
|
@@ -95,9 +96,14 @@ function sectionConfig(deps, set) {
|
|
|
95
96
|
verdict: 'fail',
|
|
96
97
|
slug: 'E_NO_ACCOUNTS_CONFIGURED',
|
|
97
98
|
lines: ['No accounts configured.'],
|
|
98
|
-
hint: 'Add an account: run `npx mcp-google-multi migrate-config` or set GOOGLE_ACCOUNTS.',
|
|
99
|
+
...(tenantScope ? {} : { hint: 'Add an account: run `npx mcp-google-multi migrate-config` or set GOOGLE_ACCOUNTS.' }),
|
|
99
100
|
};
|
|
100
101
|
}
|
|
102
|
+
// The config file, process env, working directory and metrics store below
|
|
103
|
+
// are the operator's host, not the context's: a tenant sees its own count.
|
|
104
|
+
if (tenantScope) {
|
|
105
|
+
return { id: 2, title: 'Config', verdict: 'ok', lines: [`${set.aliases.length} account(s) configured`] };
|
|
106
|
+
}
|
|
101
107
|
const cfgPath = path.join(configDir(), 'config.json');
|
|
102
108
|
lines.push(deps.fileExists(cfgPath) ? `config.json present (${set.aliases.length} account(s))` : `config.json absent — accounts sourced from env (${set.aliases.length})`);
|
|
103
109
|
const legacyEnv = LEGACY_ENV_KEYS.filter((k) => deps.env[k]);
|
|
@@ -303,12 +309,13 @@ export function overallVerdict(sections) {
|
|
|
303
309
|
export async function runDiagnostics(deps = DEFAULT_DEPS, opts = {}) {
|
|
304
310
|
// Tenant scope omits the OPERATOR sections: §3 (key provenance = box
|
|
305
311
|
// infrastructure) and §7 (its owner-gate lines print MCP_OWNER_EMAILS
|
|
306
|
-
// values — other people's addresses from a tenant's seat)
|
|
312
|
+
// values — other people's addresses from a tenant's seat), and trims §2 to
|
|
313
|
+
// the context's own account count.
|
|
307
314
|
const tenantScope = opts.scope === 'tenant';
|
|
308
315
|
const sections = [];
|
|
309
316
|
sections.push(sectionRuntime(deps));
|
|
310
317
|
const set = deps.accountSet();
|
|
311
|
-
sections.push(sectionConfig(deps, set));
|
|
318
|
+
sections.push(sectionConfig(deps, set, tenantScope));
|
|
312
319
|
const aliases = set?.aliases ?? [];
|
|
313
320
|
if (!tenantScope)
|
|
314
321
|
sections.push(sectionKeys(deps, aliases));
|
|
@@ -390,6 +397,26 @@ export function exitCodeFor(report, strict) {
|
|
|
390
397
|
return 1;
|
|
391
398
|
return 0;
|
|
392
399
|
}
|
|
400
|
+
/** The engine's account, token and probe reads bound to one context, so a
|
|
401
|
+
* report never shows (or probes with) another context's accounts. */
|
|
402
|
+
function diagnosticsDepsFor(ctx) {
|
|
403
|
+
const health = accountHealthDepsFor(ctx.tokenStore, () => ctx.accounts);
|
|
404
|
+
const probe = apiProbeDepsFor(ctx.tokenStore.readToken, ctx.getClient);
|
|
405
|
+
return {
|
|
406
|
+
...DEFAULT_DEPS,
|
|
407
|
+
accountSet: () => {
|
|
408
|
+
try {
|
|
409
|
+
return ctx.accounts;
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
accountHealth: (alias) => deriveAccountHealth(alias, health),
|
|
416
|
+
anyTokensExist: (aliases) => aliases.some((a) => ctx.tokenStore.hasToken(a)),
|
|
417
|
+
probeApi: (alias) => probeApiEnablement(alias, probe),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
393
420
|
/** Agent-callable structured health report (read-only). Mirrors `doctor`'s
|
|
394
421
|
* engine. Registered as a META tool, like `account_list`: it introspects this
|
|
395
422
|
* server rather than Google data, and as a normal tool it became a service of
|
|
@@ -398,16 +425,17 @@ export function exitCodeFor(report, strict) {
|
|
|
398
425
|
* advertised only once something else expanded the surface. The README sends
|
|
399
426
|
* people here when they are stuck, so it has to be findable. */
|
|
400
427
|
export function registerDiagnoseTool(registry, ctx) {
|
|
401
|
-
// A non-owner context gets the tenant-scoped report; the
|
|
402
|
-
//
|
|
403
|
-
const scope = ctx && ctx
|
|
428
|
+
// A non-owner context gets the tenant-scoped report; only the owner context
|
|
429
|
+
// (or the context-free CLI surface) keeps the full operator report.
|
|
430
|
+
const scope = ctx && !isOwnerContext(ctx) ? 'tenant' : 'operator';
|
|
431
|
+
const deps = ctx ? diagnosticsDepsFor(ctx) : DEFAULT_DEPS;
|
|
404
432
|
registry.registerMeta('diagnose', {
|
|
405
433
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
406
434
|
description: 'Health report for this server: runtime, config, keys, per-account token status, scope grants, and API enablement. Read-only; returns copy-pasteable fixes for anything wrong. Call this to diagnose auth/config failures.',
|
|
407
435
|
inputSchema: {},
|
|
408
436
|
}, async () => {
|
|
409
437
|
try {
|
|
410
|
-
const result = await runDiagnostics(
|
|
438
|
+
const result = await runDiagnostics(deps, { scope });
|
|
411
439
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
412
440
|
}
|
|
413
441
|
catch (e) {
|
package/dist/executor.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getClient } from './client.js';
|
|
2
|
+
import { type ScopeSetDeps } from './scope-observability.js';
|
|
2
3
|
export declare const MAX_RESPONSE_CHARS = 100000;
|
|
3
4
|
export type QueryValue = string | number | boolean;
|
|
4
5
|
export type QueryParams = Record<string, QueryValue | QueryValue[]>;
|
|
@@ -13,6 +14,8 @@ export interface ApiMethodRef {
|
|
|
13
14
|
}
|
|
14
15
|
export interface ExecuteDeps {
|
|
15
16
|
getClientFn?: typeof getClient;
|
|
17
|
+
/** Token + profile view behind a 403 scope hint; absent = the single owner. */
|
|
18
|
+
scopeDeps?: ScopeSetDeps;
|
|
16
19
|
}
|
|
17
20
|
export interface ExecuteArgs {
|
|
18
21
|
account: string;
|
package/dist/executor.js
CHANGED
|
@@ -103,6 +103,6 @@ export async function executeApiMethod(method, args, deps = {}) {
|
|
|
103
103
|
return { content: [{ type: 'text', text }] };
|
|
104
104
|
}
|
|
105
105
|
catch (error) {
|
|
106
|
-
return handleGoogleApiError(error, args.account, undefined, () => method.scopes?.length ? scopeHintForMethod(method.scopes, args.account) : null);
|
|
106
|
+
return handleGoogleApiError(error, args.account, undefined, () => method.scopes?.length ? scopeHintForMethod(method.scopes, args.account, deps.scopeDeps) : null);
|
|
107
107
|
}
|
|
108
108
|
}
|
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/identity.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface IdentityContext {
|
|
|
21
21
|
hasToken: typeof hasToken;
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
+
export declare function isOwnerContext(ctx: object | undefined): boolean;
|
|
24
25
|
export declare function buildIdentityContext(env?: NodeJS.ProcessEnv, opts?: {
|
|
25
26
|
transport?: Transport;
|
|
26
27
|
}): IdentityContext;
|
package/dist/identity.js
CHANGED
|
@@ -2,12 +2,20 @@ import { getAccountSet, getTokenDir, refreshAccountSetIfStale } from './accounts
|
|
|
2
2
|
import { makeGetClient } from './client.js';
|
|
3
3
|
import { createTokenStore } from './token-store.js';
|
|
4
4
|
import { resolvePolicy } from './write-control.js';
|
|
5
|
+
// Only buildIdentityContext mints the owner brand. The subject string alone
|
|
6
|
+
// never unlocks the owner-only surfaces (account wizard, host-file tools, the
|
|
7
|
+
// operator diagnose report): a hand-built context claiming 'owner' stays a
|
|
8
|
+
// non-owner context.
|
|
9
|
+
const ownerContexts = new WeakSet();
|
|
10
|
+
export function isOwnerContext(ctx) {
|
|
11
|
+
return ctx !== undefined && ownerContexts.has(ctx);
|
|
12
|
+
}
|
|
5
13
|
export function buildIdentityContext(env = process.env, opts = {}) {
|
|
6
14
|
// Real closures over the single-owner state (the SAME factories EE
|
|
7
15
|
// instantiates per tenant), bound here to the global registry + token dir —
|
|
8
16
|
// behavior-identical to the module-level functions for the free core.
|
|
9
17
|
const tokenStore = createTokenStore(getTokenDir());
|
|
10
|
-
|
|
18
|
+
const ctx = {
|
|
11
19
|
subject: 'owner',
|
|
12
20
|
// Live getter: the seam must always see the current registry, never a
|
|
13
21
|
// snapshot pinned from before a wizard mutation or cross-process reload.
|
|
@@ -25,4 +33,6 @@ export function buildIdentityContext(env = process.env, opts = {}) {
|
|
|
25
33
|
}),
|
|
26
34
|
tokenStore,
|
|
27
35
|
};
|
|
36
|
+
ownerContexts.add(ctx);
|
|
37
|
+
return ctx;
|
|
28
38
|
}
|
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/dist/registry.d.ts
CHANGED
|
@@ -65,6 +65,9 @@ export declare class ToolRegistry {
|
|
|
65
65
|
/** The registry's OWN alias list: registration-time schema builders (the
|
|
66
66
|
* generated account enums) read this, never the process global. */
|
|
67
67
|
accountAliases(): readonly string[];
|
|
68
|
+
/** The registry's OWN account set: handlers needing an account's email or
|
|
69
|
+
* profile read this, never the process global. */
|
|
70
|
+
accountSet(): AccountSet;
|
|
68
71
|
/** Declared input-schema keys + scalar kinds for one tool (tools/call arg
|
|
69
72
|
* normalization; the kind drives value coercion on renamed keys). */
|
|
70
73
|
argShape(name: string): ArgShape | undefined;
|
package/dist/registry.js
CHANGED
|
@@ -225,7 +225,7 @@ export class ToolRegistry {
|
|
|
225
225
|
const v = a?.account;
|
|
226
226
|
if (typeof v !== 'string' || (v !== '*' && !v.includes(',')))
|
|
227
227
|
return 1;
|
|
228
|
-
const sel = parseAccountSelector(v);
|
|
228
|
+
const sel = parseAccountSelector(v, this.accounts().aliases);
|
|
229
229
|
return sel.ok ? sel.aliases.length : 1;
|
|
230
230
|
})
|
|
231
231
|
: finalHandler;
|
|
@@ -250,6 +250,11 @@ export class ToolRegistry {
|
|
|
250
250
|
accountAliases() {
|
|
251
251
|
return this.accounts().aliases;
|
|
252
252
|
}
|
|
253
|
+
/** The registry's OWN account set: handlers needing an account's email or
|
|
254
|
+
* profile read this, never the process global. */
|
|
255
|
+
accountSet() {
|
|
256
|
+
return this.accounts();
|
|
257
|
+
}
|
|
253
258
|
/** Declared input-schema keys + scalar kinds for one tool (tools/call arg
|
|
254
259
|
* normalization; the kind drives value coercion on renamed keys). */
|
|
255
260
|
argShape(name) {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { resolveScopesForAccount } from './auth.js';
|
|
2
1
|
import { readToken } from './token-store.js';
|
|
3
2
|
export type ScopeState = 'callable' | 'requestable_not_granted' | 'not_requestable';
|
|
4
3
|
export type NotRequestableReason = 'add_bundle' | 'account_type' | 'unknown_scope';
|
|
@@ -35,12 +34,15 @@ export interface AccountScopeSets {
|
|
|
35
34
|
granted: Set<string>;
|
|
36
35
|
profile: Set<string>;
|
|
37
36
|
}
|
|
37
|
+
/** Where a scope hint reads the granted token and the configured profile;
|
|
38
|
+
* absent = the single-owner token store and global registry. */
|
|
39
|
+
export interface ScopeSetDeps {
|
|
40
|
+
readTokenFn?: typeof readToken;
|
|
41
|
+
profileFn?: (alias: string) => string[];
|
|
42
|
+
}
|
|
38
43
|
/** Best-effort per-account sets; null when the token/profile is unreadable
|
|
39
44
|
* (decrypt error, missing token) — callers fall back to generic hints. */
|
|
40
|
-
export declare function accountScopeSets(alias: string, deps?:
|
|
41
|
-
readTokenFn?: typeof readToken;
|
|
42
|
-
profileFn?: typeof resolveScopesForAccount;
|
|
43
|
-
}): AccountScopeSets | null;
|
|
45
|
+
export declare function accountScopeSets(alias: string, deps?: ScopeSetDeps): AccountScopeSets | null;
|
|
44
46
|
/**
|
|
45
47
|
* Runtime enrichment for a scope 403 on a method whose required scopes are
|
|
46
48
|
* known (escape hatch: runtime Discovery; generated tools: baked). The
|
|
@@ -48,7 +50,7 @@ export declare function accountScopeSets(alias: string, deps?: {
|
|
|
48
50
|
* profile means Google refuses it for this account type (personal Gmail),
|
|
49
51
|
* so re-auth would loop — report the dead end instead.
|
|
50
52
|
*/
|
|
51
|
-
export declare function scopeHintForMethod(required: readonly string[], alias: string, deps?:
|
|
53
|
+
export declare function scopeHintForMethod(required: readonly string[], alias: string, deps?: ScopeSetDeps): {
|
|
52
54
|
hint: string;
|
|
53
55
|
retriable: boolean;
|
|
54
56
|
} | null;
|
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
|
+
/** Refusal for a host-file argument on a context that may not touch the
|
|
3
|
+
* server's disk: the host belongs to its operator, not to every caller. */
|
|
4
|
+
export declare function hostFilesRefused(account: string | undefined, what: string): {
|
|
5
|
+
content: {
|
|
6
|
+
type: "text";
|
|
7
|
+
text: string;
|
|
8
|
+
}[];
|
|
9
|
+
isError: true;
|
|
10
|
+
};
|
|
2
11
|
export declare function prepareLocalDest(savePath: string, filename: string): string;
|
|
3
12
|
export declare function openLocalReadStream(localPath: string): Promise<fs.ReadStream>;
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
+
import { stringifyEnvelope } from './_errors.js';
|
|
4
|
+
/** Refusal for a host-file argument on a context that may not touch the
|
|
5
|
+
* server's disk: the host belongs to its operator, not to every caller. */
|
|
6
|
+
export function hostFilesRefused(account, what) {
|
|
7
|
+
return {
|
|
8
|
+
content: [{
|
|
9
|
+
type: 'text',
|
|
10
|
+
text: stringifyEnvelope({
|
|
11
|
+
error: 'forbidden',
|
|
12
|
+
message: `${what}: this caller may not read files on the machine running the server.`,
|
|
13
|
+
hint: 'Put the file in Drive first and work from its Drive id.',
|
|
14
|
+
retriable: false,
|
|
15
|
+
account,
|
|
16
|
+
}),
|
|
17
|
+
}],
|
|
18
|
+
isError: true,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
3
21
|
// path.basename() is a traversal guard — a caller-supplied filename must never escape savePath.
|
|
4
22
|
export function prepareLocalDest(savePath, filename) {
|
|
5
23
|
const name = path.basename(filename);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ToolRegistry } from '../registry.js';
|
|
2
|
+
import { type AccountSet } from '../accounts.js';
|
|
2
3
|
import { type ScopesReport } from '../scope-observability.js';
|
|
4
|
+
import { type TokenStore } from '../token-store.js';
|
|
3
5
|
export interface AccountHealthDeps {
|
|
4
6
|
hasToken: (alias: string) => boolean;
|
|
5
7
|
readToken: (alias: string) => {
|
|
@@ -13,7 +15,12 @@ export interface AccountHealthDeps {
|
|
|
13
15
|
* "registered but not requestable" view (B9). account_list stays compact:
|
|
14
16
|
* its report universe is profile ∪ granted only. */
|
|
15
17
|
registeredScopes?: () => string[];
|
|
18
|
+
/** Account view health is derived against; absent = the global registry. */
|
|
19
|
+
accounts?: () => AccountSet;
|
|
16
20
|
}
|
|
21
|
+
/** Health deps over one context's token store (and, optionally, its account
|
|
22
|
+
* view); the module-level functions are the single owner's store. */
|
|
23
|
+
export declare function accountHealthDepsFor(store: Pick<TokenStore, 'hasToken' | 'readToken'>, accounts?: () => AccountSet): AccountHealthDeps;
|
|
17
24
|
export type TokenStatus = 'ok' | 'expired_refreshable' | 'needs_reauth' | 'missing' | 'decrypt_error';
|
|
18
25
|
export interface AccountHealth {
|
|
19
26
|
alias: string;
|
|
@@ -4,16 +4,23 @@ import { getAccountSet, refreshAccountSetIfStale } from '../accounts.js';
|
|
|
4
4
|
import { buildScopesReport } from '../scope-observability.js';
|
|
5
5
|
import { getAdminAccounts, resolveScopesForAccount } from '../auth.js';
|
|
6
6
|
import { hasToken, readToken } from '../token-store.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
/** Health deps over one context's token store (and, optionally, its account
|
|
8
|
+
* view); the module-level functions are the single owner's store. */
|
|
9
|
+
export function accountHealthDepsFor(store, accounts) {
|
|
10
|
+
return {
|
|
11
|
+
hasToken: store.hasToken,
|
|
12
|
+
readToken: store.readToken,
|
|
13
|
+
fileExists: fs.existsSync,
|
|
14
|
+
now: Date.now,
|
|
15
|
+
...(accounts ? { accounts } : {}),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const DEFAULT_DEPS = accountHealthDepsFor({ hasToken, readToken });
|
|
13
19
|
export function deriveAccountHealth(alias, deps = DEFAULT_DEPS) {
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
20
|
+
const set = (deps.accounts ?? getAccountSet)();
|
|
21
|
+
const config = set.configs[alias];
|
|
22
|
+
const admin = getAdminAccounts(set).includes(alias);
|
|
23
|
+
const configured = resolveScopesForAccount(alias, set);
|
|
17
24
|
// BR-10: env-sourced accounts are not persisted in config.json, so the
|
|
18
25
|
// account wizard cannot edit them — the deployer edits env instead.
|
|
19
26
|
const base = {
|
|
@@ -88,14 +95,16 @@ export function registerAccountTools(registry, deps = DEFAULT_DEPS) {
|
|
|
88
95
|
_meta: { 'anthropic/alwaysLoad': true },
|
|
89
96
|
}, async () => {
|
|
90
97
|
refreshAccountSetIfStale();
|
|
98
|
+
const set = registry.accountSet();
|
|
99
|
+
const health = { ...deps, accounts: () => set };
|
|
91
100
|
return {
|
|
92
101
|
content: [
|
|
93
102
|
{
|
|
94
103
|
type: 'text',
|
|
95
104
|
text: JSON.stringify({
|
|
96
|
-
defaultAccount:
|
|
97
|
-
defaultAccountSource:
|
|
98
|
-
accounts:
|
|
105
|
+
defaultAccount: set.defaultAccount ?? null,
|
|
106
|
+
defaultAccountSource: set.defaultAccountSource ?? null,
|
|
107
|
+
accounts: set.aliases.map((alias) => deriveAccountHealth(alias, health)),
|
|
99
108
|
}),
|
|
100
109
|
},
|
|
101
110
|
],
|
package/dist/tools/drive.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { coerceArray, coerceBoolean, coerceNumber } from './_coerce.js';
|
|
3
3
|
import { drive as driveClient } from '@googleapis/drive';
|
|
4
|
-
import { accountArgLive
|
|
4
|
+
import { accountArgLive } from '../accounts.js';
|
|
5
5
|
import { getClient } from '../client.js';
|
|
6
6
|
import { handleGoogleApiError, invalidParams, safeMessage, stringifyEnvelope } from './_errors.js';
|
|
7
|
-
import { openLocalReadStream, prepareLocalDest } from './_local-files.js';
|
|
7
|
+
import { hostFilesRefused, openLocalReadStream, prepareLocalDest } from './_local-files.js';
|
|
8
8
|
import { checkOutbound, outboundDeniedEnvelope, resolveOutboundAllowlist } from '../outbound-allowlist.js';
|
|
9
9
|
import { isAllowed, writeDisabledResult } from '../write-control.js';
|
|
10
10
|
import { capText, listResult } from '../trim.js';
|
|
@@ -128,6 +128,8 @@ export function registerDriveTools(server, deps = {}) {
|
|
|
128
128
|
const accountEnum = accountArgLive(() => server.accountAliases()).optional();
|
|
129
129
|
const requiredAccountEnum = accountArgLive(() => server.accountAliases());
|
|
130
130
|
const getClientFn = deps.getClientFn ?? getClient;
|
|
131
|
+
const localFiles = deps.localFiles ?? true;
|
|
132
|
+
const registerHostFileTool = localFiles ? server.registerTool : (() => undefined);
|
|
131
133
|
// ─── Read / search / list ──────────────────────────────────────────────
|
|
132
134
|
server.registerTool('drive_search', {
|
|
133
135
|
description: 'Search files in a Google Drive account. A plain keyword is treated as a full-text search; pass Drive query syntax for a structured search.',
|
|
@@ -343,7 +345,7 @@ export function registerDriveTools(server, deps = {}) {
|
|
|
343
345
|
}
|
|
344
346
|
});
|
|
345
347
|
// ─── Write / upload / download ─────────────────────────────────────────
|
|
346
|
-
|
|
348
|
+
registerHostFileTool('drive_upload', {
|
|
347
349
|
description: 'Upload a local file to Google Drive. Pass `convertTo` to import it as a native, editable Google Doc/Sheet/Slides/Drawing instead of storing the raw bytes.',
|
|
348
350
|
inputSchema: {
|
|
349
351
|
account: accountEnum.describe('Google account alias'),
|
|
@@ -381,7 +383,7 @@ export function registerDriveTools(server, deps = {}) {
|
|
|
381
383
|
return handleDriveError(error, account);
|
|
382
384
|
}
|
|
383
385
|
});
|
|
384
|
-
|
|
386
|
+
registerHostFileTool('drive_download', {
|
|
385
387
|
description: 'Download a binary file from Drive to local disk. For Google Workspace formats (Docs, Sheets, Slides), use drive_export instead.',
|
|
386
388
|
inputSchema: {
|
|
387
389
|
account: accountEnum.describe('Google account alias'),
|
|
@@ -409,7 +411,7 @@ export function registerDriveTools(server, deps = {}) {
|
|
|
409
411
|
return handleDriveError(error, account);
|
|
410
412
|
}
|
|
411
413
|
});
|
|
412
|
-
|
|
414
|
+
registerHostFileTool('drive_export', {
|
|
413
415
|
description: 'Export a Google Workspace document (Doc, Sheet, Slide) to a standard format and save to disk. Supported: PDF, DOCX, XLSX, PPTX, TXT, CSV, Markdown (text/markdown for Docs).',
|
|
414
416
|
inputSchema: {
|
|
415
417
|
account: accountEnum.describe('Google account alias'),
|
|
@@ -476,11 +478,20 @@ export function registerDriveTools(server, deps = {}) {
|
|
|
476
478
|
fileId: z.string().min(1).describe('Google Drive file ID'),
|
|
477
479
|
newName: z.string().optional().describe('New filename'),
|
|
478
480
|
newParentFolderId: z.string().optional().describe('Move to this folder, named newParentFolderId here, not parentFolderId'),
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
481
|
+
...(localFiles
|
|
482
|
+
? {
|
|
483
|
+
localPath: z.string().optional().describe('Replace file content with this local file (path on the machine running the server)'),
|
|
484
|
+
mimeType: z.string().optional().describe('MIME type of the replacement file (required if localPath is provided)'),
|
|
485
|
+
convertTo: z.enum(CONVERT_TO_VALUES).optional().describe('When replacing content via localPath, convert the new content into this native Google Workspace type on import: "document" | "spreadsheet" | "presentation" | "drawing" (full application/vnd.google-apps.* ids also accepted).'),
|
|
486
|
+
}
|
|
487
|
+
: {}),
|
|
482
488
|
},
|
|
483
|
-
}, async (
|
|
489
|
+
}, async (args) => {
|
|
490
|
+
const { account, fileId, newName, newParentFolderId } = args;
|
|
491
|
+
// Declared only when the context may read host files (see the shape).
|
|
492
|
+
const { localPath: localPathArg, mimeType: mimeTypeArg, convertTo } = args;
|
|
493
|
+
if (localPathArg && !localFiles)
|
|
494
|
+
return hostFilesRefused(account, 'localPath');
|
|
484
495
|
try {
|
|
485
496
|
const auth = await getClientFn(account);
|
|
486
497
|
const drive = driveClient({ version: 'v3', auth });
|
|
@@ -1309,7 +1320,7 @@ export function registerDriveTools(server, deps = {}) {
|
|
|
1309
1320
|
};
|
|
1310
1321
|
}
|
|
1311
1322
|
const intendedName = newName ?? meta.data.name ?? 'transferred-file';
|
|
1312
|
-
const targetEmail =
|
|
1323
|
+
const targetEmail = server.accountSet().configs[toAccount].email;
|
|
1313
1324
|
const finish = async (data, strategy, flags) => {
|
|
1314
1325
|
let moveFailed = false;
|
|
1315
1326
|
let moveSkipped = false;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ToolRegistry } from '../../registry.js';
|
|
2
|
+
import type { ExecuteDeps } from './_shared.js';
|
|
2
3
|
export declare const GENERATED_SERVICES: Array<{
|
|
3
4
|
name: string;
|
|
4
|
-
register: (registry: ToolRegistry) => void;
|
|
5
|
+
register: (registry: ToolRegistry, deps?: ExecuteDeps) => void;
|
|
5
6
|
}>;
|
package/dist/tools/gmail.js
CHANGED
|
@@ -5,7 +5,7 @@ import { accountArgLive } from '../accounts.js';
|
|
|
5
5
|
import { getClient } from '../client.js';
|
|
6
6
|
import { handleGoogleApiError, invalidParams, mapGoogleError } from './_errors.js';
|
|
7
7
|
import { buildReplyHeaders, composeRaw, renderMarkdown, htmlToMarkdown, HeaderInjectionError } from './gmail-mime.js';
|
|
8
|
-
import { prepareLocalDest } from './_local-files.js';
|
|
8
|
+
import { hostFilesRefused, prepareLocalDest } from './_local-files.js';
|
|
9
9
|
import { checkOutbound } from '../outbound-allowlist.js';
|
|
10
10
|
import addressparser from 'nodemailer/lib/addressparser/index.js';
|
|
11
11
|
import { lookup as lookupMime } from 'mime-types';
|
|
@@ -106,9 +106,11 @@ export function deriveReplySubject(sourceSubject) {
|
|
|
106
106
|
// The account's own-address set (primary + Gmail send-as aliases) used to
|
|
107
107
|
// exclude the caller from reply-all. Send-as rarely changes, so memoize per
|
|
108
108
|
// process; a stale miss only costs one extra self-copy, never a wrong send.
|
|
109
|
+
// Keyed by alias AND mailbox: two contexts can share an alias name.
|
|
109
110
|
const ownAddressCache = new Map();
|
|
110
111
|
function getOwnAddresses(gmail, account, primaryEmail) {
|
|
111
|
-
|
|
112
|
+
const key = `${account} ${normAddr(primaryEmail)}`;
|
|
113
|
+
let cached = ownAddressCache.get(key);
|
|
112
114
|
if (!cached) {
|
|
113
115
|
cached = (async () => {
|
|
114
116
|
const set = new Set([normAddr(primaryEmail)]);
|
|
@@ -124,7 +126,7 @@ function getOwnAddresses(gmail, account, primaryEmail) {
|
|
|
124
126
|
}
|
|
125
127
|
return set;
|
|
126
128
|
})();
|
|
127
|
-
ownAddressCache.set(
|
|
129
|
+
ownAddressCache.set(key, cached);
|
|
128
130
|
}
|
|
129
131
|
return cached;
|
|
130
132
|
}
|
|
@@ -441,6 +443,8 @@ export function registerGmailTools(server, deps = {}) {
|
|
|
441
443
|
// custody path is the context's, not the process global.
|
|
442
444
|
const accountEnum = accountArgLive(() => server.accountAliases()).optional();
|
|
443
445
|
const getClientFn = deps.getClientFn ?? getClient;
|
|
446
|
+
const localFiles = deps.localFiles ?? true;
|
|
447
|
+
const registerHostFileTool = localFiles ? server.registerTool : (() => undefined);
|
|
444
448
|
server.registerTool('gmail_search', {
|
|
445
449
|
description: 'Search messages in a Gmail account. Returns compact rows (id, from, subject, date, snippet); ' +
|
|
446
450
|
'pass full=true for threadId, to, labelIds and the untruncated snippet.',
|
|
@@ -626,13 +630,15 @@ export function registerGmailTools(server, deps = {}) {
|
|
|
626
630
|
.describe('With replyToMessageId: include the source To+Cc (minus your own addresses) in cc. Default false (reply to sender only).'),
|
|
627
631
|
replyToThreadId: z.string().optional()
|
|
628
632
|
.describe('Thread ID to send the message in'),
|
|
629
|
-
attachments: coerceJson(attachmentSchema),
|
|
633
|
+
...(localFiles ? { attachments: coerceJson(attachmentSchema) } : {}),
|
|
630
634
|
},
|
|
631
635
|
}, async ({ account, to, subject, body, htmlBody, allowRawHtml, cc, replyToMessageId, replyAll, replyToThreadId, attachments }) => {
|
|
636
|
+
if (!localFiles && attachments?.length)
|
|
637
|
+
return hostFilesRefused(account, 'attachments');
|
|
632
638
|
try {
|
|
633
639
|
const auth = await getClientFn(account);
|
|
634
640
|
const gmail = gmailClient({ version: 'v1', auth });
|
|
635
|
-
const config =
|
|
641
|
+
const config = server.accountSet().configs[account];
|
|
636
642
|
if (htmlBody !== undefined) {
|
|
637
643
|
throw new GmailComposeError('E_HTMLBODY_REMOVED', 'htmlBody was removed in v6: author Markdown in `body`; for literal HTML pass `allowRawHtml: true`.');
|
|
638
644
|
}
|
|
@@ -690,7 +696,7 @@ export function registerGmailTools(server, deps = {}) {
|
|
|
690
696
|
return mapped ?? handleGmailError(error, account);
|
|
691
697
|
}
|
|
692
698
|
});
|
|
693
|
-
|
|
699
|
+
registerHostFileTool('gmail_download_attachment', {
|
|
694
700
|
description: 'Download an email attachment to local disk. Use gmail_read first to get the attachmentId.',
|
|
695
701
|
inputSchema: {
|
|
696
702
|
account: accountEnum.describe('Google account alias'),
|
|
@@ -741,13 +747,15 @@ export function registerGmailTools(server, deps = {}) {
|
|
|
741
747
|
.describe('With replyToMessageId: include the source To+Cc (minus your own addresses) in cc. Default false (reply to sender only).'),
|
|
742
748
|
replyToThreadId: z.string().optional()
|
|
743
749
|
.describe('Thread ID to associate the draft with'),
|
|
744
|
-
attachments: coerceJson(attachmentSchema),
|
|
750
|
+
...(localFiles ? { attachments: coerceJson(attachmentSchema) } : {}),
|
|
745
751
|
},
|
|
746
752
|
}, async ({ account, to, subject, body, htmlBody, allowRawHtml, cc, replyToMessageId, replyAll, replyToThreadId, attachments }) => {
|
|
753
|
+
if (!localFiles && attachments?.length)
|
|
754
|
+
return hostFilesRefused(account, 'attachments');
|
|
747
755
|
try {
|
|
748
756
|
const auth = await getClientFn(account);
|
|
749
757
|
const gmail = gmailClient({ version: 'v1', auth });
|
|
750
|
-
const config =
|
|
758
|
+
const config = server.accountSet().configs[account];
|
|
751
759
|
if (htmlBody !== undefined) {
|
|
752
760
|
throw new GmailComposeError('E_HTMLBODY_REMOVED', 'htmlBody was removed in v6: author Markdown in `body`; for literal HTML pass `allowRawHtml: true`.');
|
|
753
761
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { ToolRegistry } from '../registry.js';
|
|
2
2
|
import { type Policy } from '../write-control.js';
|
|
3
|
-
import { getClient } from '../client.js';
|
|
4
3
|
import { type Toolsets } from '../toolsets.js';
|
|
4
|
+
import { type ExecuteDeps } from '../executor.js';
|
|
5
5
|
import { type DiscoveryDeps, type DiscoveryMethod } from '../discovery-client.js';
|
|
6
|
-
export interface EscapeDeps extends DiscoveryDeps {
|
|
7
|
-
getClientFn?: typeof getClient;
|
|
6
|
+
export interface EscapeDeps extends DiscoveryDeps, ExecuteDeps {
|
|
8
7
|
toolsets?: Toolsets;
|
|
9
8
|
}
|
|
10
9
|
/** Up to three closest known ids for the unknown_method did-you-mean hint;
|
package/dist/tools/google-api.js
CHANGED
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",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"./boot-gates": "./dist/boot-gates.js",
|
|
28
28
|
"./tenant-purge": "./dist/tenant-purge.js",
|
|
29
29
|
"./doctor": "./dist/doctor.js",
|
|
30
|
+
"./write-control": "./dist/write-control.js",
|
|
30
31
|
"./package.json": "./package.json"
|
|
31
32
|
},
|
|
32
33
|
"files": [
|