mcp-google-multi 6.0.0-beta.3 → 6.0.0-beta.5
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 +20 -6
- package/dist/accounts.d.ts +3 -3
- package/dist/accounts.js +18 -31
- package/dist/arg-normalize.js +15 -35
- package/dist/arg-strict.js +8 -28
- package/dist/auth.d.ts +1 -1
- package/dist/auth.js +5 -10
- package/dist/boot-gates.d.ts +1 -1
- package/dist/boot-gates.js +3 -5
- package/dist/cimd-cache.js +5 -11
- package/dist/client-config.d.ts +2 -2
- package/dist/client-config.js +8 -14
- package/dist/client.js +0 -0
- package/dist/compose.d.ts +8 -1
- package/dist/compose.js +11 -11
- package/dist/config-file.js +7 -12
- package/dist/discover.js +6 -12
- package/dist/discovery-client.js +14 -33
- package/dist/doctor.d.ts +3 -3
- package/dist/doctor.js +12 -18
- package/dist/env-load.js +2 -5
- package/dist/executor.js +2 -4
- package/dist/fanout.js +8 -17
- package/dist/fs-atomic.d.ts +0 -3
- package/dist/fs-atomic.js +104 -22
- package/dist/hint-provider.d.ts +4 -1
- package/dist/hint-provider.js +4 -10
- package/dist/http-config.d.ts +2 -2
- package/dist/http-config.js +3 -7
- package/dist/http-transport.d.ts +5 -5
- package/dist/http-transport.js +39 -92
- package/dist/identity.d.ts +2 -2
- package/dist/identity.js +6 -9
- package/dist/index.js +19 -31
- package/dist/master-key.d.ts +3 -3
- package/dist/master-key.js +11 -22
- package/dist/mcp-token.d.ts +40 -16
- package/dist/mcp-token.js +121 -44
- 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 +3 -5
- package/dist/net-tuning.js +2 -4
- package/dist/oauth-as.d.ts +7 -2
- package/dist/oauth-as.js +45 -78
- package/dist/oauth-consent.js +4 -10
- package/dist/outbound-allowlist.js +4 -9
- package/dist/reauth-hint.js +2 -5
- package/dist/registry-transfer.d.ts +1 -1
- package/dist/registry-transfer.js +5 -13
- package/dist/registry.d.ts +3 -9
- package/dist/registry.js +97 -76
- package/dist/scope-catalog.d.ts +5 -0
- package/dist/scope-catalog.js +17 -7
- package/dist/scope-observability.d.ts +2 -3
- package/dist/scope-observability.js +8 -14
- package/dist/services.d.ts +3 -0
- package/dist/services.js +5 -10
- package/dist/ssrf-guard.js +11 -22
- package/dist/tenant-purge.js +2 -4
- package/dist/token-store.js +0 -0
- package/dist/tools/_coerce.d.ts +4 -4
- package/dist/tools/_errors.js +22 -39
- package/dist/tools/_local-files.js +4 -8
- package/dist/tools/account-wizard.d.ts +4 -3
- package/dist/tools/account-wizard.js +48 -54
- package/dist/tools/accounts-tool.d.ts +8 -1
- package/dist/tools/accounts-tool.js +18 -7
- package/dist/tools/admin.js +8 -4
- package/dist/tools/analytics.js +32 -16
- package/dist/tools/calendar.d.ts +5 -0
- package/dist/tools/calendar.js +63 -7
- package/dist/tools/chat.js +2 -3
- package/dist/tools/contacts.js +19 -14
- package/dist/tools/docs.js +45 -21
- package/dist/tools/drive.js +72 -39
- package/dist/tools/forms.js +2 -3
- package/dist/tools/generated/gmail.js +0 -14
- package/dist/tools/generated/method-map.js +1 -1
- package/dist/tools/gmail-mime.d.ts +1 -1
- package/dist/tools/gmail-mime.js +7 -12
- package/dist/tools/gmail.d.ts +3 -3
- package/dist/tools/gmail.js +135 -52
- package/dist/tools/google-api.js +5 -7
- package/dist/tools/meet.js +2 -3
- package/dist/tools/searchconsole.js +2 -3
- package/dist/tools/sheets.d.ts +10 -0
- package/dist/tools/sheets.js +111 -5
- package/dist/tools/slides.js +2 -3
- package/dist/tools/tasks.js +2 -3
- package/dist/trim.js +2 -3
- package/dist/usage-metrics.js +5 -12
- package/dist/write-control.d.ts +4 -8
- package/dist/write-control.js +18 -6
- package/package.json +24 -22
- package/skills/google-workspace/SKILL.md +162 -0
- package/skills/google-workspace/reference/admin.md +105 -0
- package/skills/google-workspace/reference/analytics.md +70 -0
- package/skills/google-workspace/reference/appsmarket.md +5 -0
- package/skills/google-workspace/reference/calendar.md +175 -0
- package/skills/google-workspace/reference/chat.md +59 -0
- package/skills/google-workspace/reference/classroom.md +5 -0
- package/skills/google-workspace/reference/cloudidentity.md +5 -0
- package/skills/google-workspace/reference/cloudsearch.md +5 -0
- package/skills/google-workspace/reference/contacts.md +138 -0
- package/skills/google-workspace/reference/docs.md +395 -0
- package/skills/google-workspace/reference/drive.md +485 -0
- package/skills/google-workspace/reference/driveactivity.md +5 -0
- package/skills/google-workspace/reference/drivelabels.md +5 -0
- package/skills/google-workspace/reference/forms.md +92 -0
- package/skills/google-workspace/reference/gmail.md +305 -0
- package/skills/google-workspace/reference/groupsmigration.md +5 -0
- package/skills/google-workspace/reference/groupssettings.md +5 -0
- package/skills/google-workspace/reference/keep.md +5 -0
- package/skills/google-workspace/reference/licensing.md +5 -0
- package/skills/google-workspace/reference/meet.md +68 -0
- package/skills/google-workspace/reference/postmaster.md +5 -0
- package/skills/google-workspace/reference/reseller.md +5 -0
- package/skills/google-workspace/reference/script.md +5 -0
- package/skills/google-workspace/reference/searchconsole.md +128 -0
- package/skills/google-workspace/reference/sheets.md +430 -0
- package/skills/google-workspace/reference/slides.md +67 -0
- package/skills/google-workspace/reference/tasks.md +167 -0
- package/skills/google-workspace/reference/vault.md +5 -0
- package/skills/google-workspace/reference/workspaceevents.md +5 -0
package/dist/hint-provider.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
// A plain array literal, not derived from the union
|
|
2
|
-
//
|
|
3
|
-
// caught by the completeness check below instead of silently compiling.
|
|
4
|
-
// `as const` so that check narrows to the literal tuple rather than widening
|
|
5
|
-
// every element back to the whole HintCase['kind'] union (which would make
|
|
6
|
-
// it pass trivially, whatever was listed).
|
|
1
|
+
// A plain array literal, not derived from the union, so a missing member is
|
|
2
|
+
// caught by the completeness check below rather than silently compiling.
|
|
7
3
|
export const HINT_KINDS = [
|
|
8
4
|
'reauth',
|
|
9
5
|
'legacy_plaintext_token',
|
|
@@ -43,10 +39,8 @@ let installed = null;
|
|
|
43
39
|
export function setHintProvider(p) {
|
|
44
40
|
installed = p;
|
|
45
41
|
}
|
|
46
|
-
// A throwing provider must never take down the call site:
|
|
47
|
-
//
|
|
48
|
-
// produce a Rendered falls back to the same literal a missing provider
|
|
49
|
-
// would, and the provider's own error text never reaches a client.
|
|
42
|
+
// A throwing provider must never take down the call site: it falls back to
|
|
43
|
+
// the same literal a missing provider would, never leaking its own error text.
|
|
50
44
|
export function hints() {
|
|
51
45
|
if (!installed)
|
|
52
46
|
return null;
|
package/dist/http-config.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface HttpConfig {
|
|
|
11
11
|
transport: Transport;
|
|
12
12
|
host: string;
|
|
13
13
|
port: number;
|
|
14
|
-
/** Canonical public base URL
|
|
14
|
+
/** Canonical public base URL: lowercase scheme+host, no default port,
|
|
15
15
|
* no trailing slash, no fragment/query. The issuer and every advertised
|
|
16
16
|
* endpoint derive from this. */
|
|
17
17
|
publicUrl: string;
|
|
@@ -27,7 +27,7 @@ export interface HttpConfig {
|
|
|
27
27
|
export declare function transportIncludesHttp(t: Transport): boolean;
|
|
28
28
|
export declare function resolveTransport(env?: NodeJS.ProcessEnv): Transport;
|
|
29
29
|
/**
|
|
30
|
-
* Canonicalize a public base URL
|
|
30
|
+
* Canonicalize a public base URL. WHATWG `URL` already lowercases the
|
|
31
31
|
* scheme+host and omits a default port from `.host`; this adds the HTTPS-only
|
|
32
32
|
* scheme check, strips fragment/query/userinfo, and removes a trailing slash.
|
|
33
33
|
*/
|
package/dist/http-config.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
// Transport
|
|
2
|
-
//
|
|
3
|
-
// (cc-transport-hosting) and the AS metadata in B13 (oauth-authorization-server).
|
|
4
|
-
// Owns the canonicalization rule (cc-transport-hosting BR4): a single mismatch
|
|
5
|
-
// between the advertised resource URI and the client's `resource`/`aud` = a
|
|
6
|
-
// perpetual 401 loop (the #1 interop bug), so it is normalized exactly once.
|
|
1
|
+
// Transport + public-base-URL / Origin / host config, pure and resolved once
|
|
2
|
+
// at boot. A mismatch between the advertised URI and the client's `aud` is a perpetual 401 loop.
|
|
7
3
|
export const DEFAULT_HTTP_HOST = '127.0.0.1';
|
|
8
4
|
export const DEFAULT_HTTP_PORT = 4243;
|
|
9
5
|
export const CLAUDE_AI_ORIGIN = 'https://claude.ai';
|
|
@@ -30,7 +26,7 @@ export function resolveTransport(env = process.env) {
|
|
|
30
26
|
throw new HttpConfigError('E_INVALID_TRANSPORT', `MCP_TRANSPORT must be one of stdio|http|both (got "${env.MCP_TRANSPORT}")`);
|
|
31
27
|
}
|
|
32
28
|
/**
|
|
33
|
-
* Canonicalize a public base URL
|
|
29
|
+
* Canonicalize a public base URL. WHATWG `URL` already lowercases the
|
|
34
30
|
* scheme+host and omits a default port from `.host`; this adds the HTTPS-only
|
|
35
31
|
* scheme check, strips fragment/query/userinfo, and removes a trailing slash.
|
|
36
32
|
*/
|
package/dist/http-transport.d.ts
CHANGED
|
@@ -12,9 +12,9 @@ export type AuthOutcome = {
|
|
|
12
12
|
body: string;
|
|
13
13
|
headers?: Record<string, string>;
|
|
14
14
|
};
|
|
15
|
-
/** Bearer / owner check for POST /mcp.
|
|
15
|
+
/** Bearer / owner check for POST /mcp. Default = loopback-owner; the AS swaps in JWT verify. */
|
|
16
16
|
export type Authenticator = (req: IncomingMessage) => AuthOutcome | Promise<AuthOutcome>;
|
|
17
|
-
/** A mounted extra route (the AS endpoints
|
|
17
|
+
/** A mounted extra route (the AS endpoints). Return true if it wrote a response. */
|
|
18
18
|
export type RouteHandler = (req: IncomingMessage, res: ServerResponse, url: URL) => boolean | Promise<boolean>;
|
|
19
19
|
/** What a verified subject resolves to: ITS server plus the per-request hooks
|
|
20
20
|
* bound to that server's registry (tool shapes, declared keys, the default
|
|
@@ -27,7 +27,7 @@ export interface ServerTarget {
|
|
|
27
27
|
validationEnvelope?: ValidationEnvelopeOptions;
|
|
28
28
|
}
|
|
29
29
|
export interface HttpHostOptions {
|
|
30
|
-
/** The ONE McpServer + registry built at boot (
|
|
30
|
+
/** The ONE McpServer + registry built at boot (never per request). */
|
|
31
31
|
server: McpServer;
|
|
32
32
|
config: HttpConfig;
|
|
33
33
|
version: string;
|
|
@@ -53,7 +53,7 @@ export interface HttpHostOptions {
|
|
|
53
53
|
message: string;
|
|
54
54
|
};
|
|
55
55
|
};
|
|
56
|
-
/** Extra routes keyed by exact pathname (AS endpoints mount here
|
|
56
|
+
/** Extra routes keyed by exact pathname (AS endpoints mount here). */
|
|
57
57
|
routes?: Record<string, RouteHandler>;
|
|
58
58
|
log?: (line: string) => void;
|
|
59
59
|
/** Max /mcp JSON body bytes (DoS guard). */
|
|
@@ -81,7 +81,7 @@ export interface HttpHostOptions {
|
|
|
81
81
|
export declare function parseOwnerEmails(env?: NodeJS.ProcessEnv): string[];
|
|
82
82
|
/** Front guard: an Origin, if present, must be allowlisted; a Host must be
|
|
83
83
|
* allowlisted. Absent Origin passes (native/CLI/backend clients — the claude.ai
|
|
84
|
-
* connector calls /mcp server-to-server with no Origin
|
|
84
|
+
* connector calls /mcp server-to-server with no Origin). */
|
|
85
85
|
export declare function originAllowed(origin: string | undefined, allowed: string[]): boolean;
|
|
86
86
|
export declare function hostAllowed(host: string | undefined, allowed: string[]): boolean;
|
|
87
87
|
/** JSON-RPC method name(s) for an observability log line: no params, no PII.
|
package/dist/http-transport.js
CHANGED
|
@@ -1,30 +1,21 @@
|
|
|
1
1
|
import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
|
|
2
|
-
// node:http server, the route table, the front guard (Host / Origin / DNS-rebind),
|
|
3
|
-
// and the stateless per-request /mcp dispatch. The OAuth AS endpoints and Bearer
|
|
4
|
-
// verification are a seam filled by B13 (oauth-authorization-server); B12 ships a
|
|
5
|
-
// loopback-owner authenticator so the local-HTTP model works before the AS lands.
|
|
6
2
|
import { createServer } from 'node:http';
|
|
7
3
|
import { withArgNormalization, withValidationEnvelope } from './arg-normalize.js';
|
|
8
4
|
import { logSafe } from './trim.js';
|
|
9
5
|
// A host wrapping the log callbacks it hands core needs this, and trim has no package export.
|
|
10
6
|
export { logSafe } from './trim.js';
|
|
11
7
|
// A hung handler that keeps the connection open would otherwise hold the global
|
|
12
|
-
// serialize() lock forever
|
|
13
|
-
// Drive exports, fan-out) still finish; the point is only to guarantee release.
|
|
8
|
+
// serialize() lock forever; generous by default so slow-but-valid calls still finish.
|
|
14
9
|
const DISPATCH_TIMEOUT_DEFAULT = 120_000;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* per-call work by however many frames fit in it. */
|
|
10
|
+
// Without a cap, one body multiplies per-call work by however many
|
|
11
|
+
// JSON-RPC frames the SDK's synchronous batch screening finds in it.
|
|
18
12
|
const MAX_BATCH_FRAMES = 16;
|
|
19
|
-
// A
|
|
20
|
-
//
|
|
21
|
-
// shaped slug or an overlong message. No new slug literal enters core itself —
|
|
22
|
-
// the slug is always the host's own value.
|
|
13
|
+
// A refusal's body reaches the wire verbatim, so it's validated: no new slug
|
|
14
|
+
// literal enters core, and an overlong message can't leak an operator path/env var.
|
|
23
15
|
const REFUSAL_SLUG_RE = /^[a-z][a-z0-9_]{0,39}$/;
|
|
24
16
|
const MAX_REFUSAL_MESSAGE_CHARS = 300;
|
|
25
|
-
// C0/C1 controls, format chars (
|
|
26
|
-
//
|
|
27
|
-
// letters stay allowed.
|
|
17
|
+
// C0/C1 controls, format chars (bidi overrides, zero-width joiners) and
|
|
18
|
+
// line/paragraph separators; ordinary non-ASCII letters stay allowed.
|
|
28
19
|
const NON_PRINTABLE_RE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
|
29
20
|
function isValidRefusal(r) {
|
|
30
21
|
return (typeof r.error === 'string' &&
|
|
@@ -42,7 +33,7 @@ export function parseOwnerEmails(env = process.env) {
|
|
|
42
33
|
}
|
|
43
34
|
/** Front guard: an Origin, if present, must be allowlisted; a Host must be
|
|
44
35
|
* allowlisted. Absent Origin passes (native/CLI/backend clients — the claude.ai
|
|
45
|
-
* connector calls /mcp server-to-server with no Origin
|
|
36
|
+
* connector calls /mcp server-to-server with no Origin). */
|
|
46
37
|
export function originAllowed(origin, allowed) {
|
|
47
38
|
if (!origin)
|
|
48
39
|
return true;
|
|
@@ -81,25 +72,17 @@ export function jsonRpcMethod(body) {
|
|
|
81
72
|
/** setTimeout fires at once for a delay above this, so a longer grace would
|
|
82
73
|
* cut every connection immediately. */
|
|
83
74
|
const MAX_GRACE_MS = 2 ** 31 - 1;
|
|
84
|
-
// A lingering close
|
|
85
|
-
// body pipelined after the close) is reset by the kernel, which drops whatever
|
|
86
|
-
// of the last response is still unsent. So the socket only ends its side and
|
|
87
|
-
// waits for the client's FIN, or for the grace to end.
|
|
75
|
+
// A lingering close (ends the socket, never destroys it); see docs/internals.md (Graceful close).
|
|
88
76
|
function endAfterFlush(socket) {
|
|
89
77
|
if (socket.destroyed || socket.writableEnded)
|
|
90
78
|
return;
|
|
91
|
-
//
|
|
92
|
-
// socket
|
|
93
|
-
// an early destroy does. Only the client's FIN or the grace ends it now.
|
|
79
|
+
// Disarm the keep-alive timer Node arms on finish, or it destroys the
|
|
80
|
+
// socket ~6s later and cuts a slow reader's unread tail the same way an early destroy does.
|
|
94
81
|
socket.setTimeout(0);
|
|
95
82
|
socket.end();
|
|
96
83
|
}
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
// pile up without limit. Reading stops for good instead, which lets TCP flow
|
|
100
|
-
// control stop the client. Node resumes a socket after every parsed request
|
|
101
|
-
// and on drain, so resume becomes a no-op. Such a socket never sees the
|
|
102
|
-
// client's FIN either: the grace ends it.
|
|
84
|
+
// Stops reading for good so TCP flow control stops the client, instead of
|
|
85
|
+
// piling up a flood pipelined behind a held response; see docs/internals.md (Graceful close).
|
|
103
86
|
function stopReading(socket) {
|
|
104
87
|
socket.pause();
|
|
105
88
|
socket.resume = () => socket;
|
|
@@ -117,18 +100,12 @@ function pathOf(req) {
|
|
|
117
100
|
export class HttpTransportHost {
|
|
118
101
|
opts;
|
|
119
102
|
httpServer;
|
|
120
|
-
//
|
|
121
|
-
// captures its transport per request (protocol.js), but the gap between
|
|
122
|
-
// connect() and the dispatch capturing it would still race under true
|
|
123
|
-
// concurrency, so all traffic to ONE server runs under one mutex. The free
|
|
124
|
-
// core has one lane (the boot server); with resolveServer each resolved
|
|
125
|
-
// server gets its own, however subjects map onto servers.
|
|
103
|
+
// Per-server dispatch mutex; see docs/internals.md.
|
|
126
104
|
locks = new Map();
|
|
127
105
|
// Requests holding a lane slot, per server; used only with maxQueuedPerLane.
|
|
128
106
|
depth = new Map();
|
|
129
|
-
// What a graceful close waits on: open sockets
|
|
130
|
-
//
|
|
131
|
-
// every handle() that has not settled.
|
|
107
|
+
// What a graceful close waits on: open sockets and every handle() not yet
|
|
108
|
+
// settled; see docs/internals.md (Graceful close).
|
|
132
109
|
sockets = new Set();
|
|
133
110
|
pending = new Map();
|
|
134
111
|
// Sockets that have accepted a request: nothing pending means the last
|
|
@@ -145,9 +122,8 @@ export class HttpTransportHost {
|
|
|
145
122
|
const run = prev.then(fn, fn);
|
|
146
123
|
const tail = run.then(() => undefined, () => undefined);
|
|
147
124
|
this.locks.set(key, tail);
|
|
148
|
-
// An idle lane drops out
|
|
149
|
-
//
|
|
150
|
-
// already replaced it, so the identity check never frees a live lane.
|
|
125
|
+
// An idle lane drops out of the map; the identity check below skips a
|
|
126
|
+
// later request that already replaced this tail, so it never frees a live lane.
|
|
151
127
|
void tail.then(() => {
|
|
152
128
|
if (this.locks.get(key) === tail)
|
|
153
129
|
this.locks.delete(key);
|
|
@@ -161,9 +137,8 @@ export class HttpTransportHost {
|
|
|
161
137
|
const { config } = this.opts;
|
|
162
138
|
const server = createServer((req, res) => {
|
|
163
139
|
const socket = req.socket;
|
|
164
|
-
// Once closing, a
|
|
165
|
-
//
|
|
166
|
-
// Its body is left unread: nothing reads the socket again.
|
|
140
|
+
// Once closing, a request arriving now is never handled (RFC 9112 §9.6)
|
|
141
|
+
// and its body is left unread.
|
|
167
142
|
if (this.closing) {
|
|
168
143
|
stopReading(socket);
|
|
169
144
|
return;
|
|
@@ -190,10 +165,8 @@ export class HttpTransportHost {
|
|
|
190
165
|
};
|
|
191
166
|
res.once('finish', settle);
|
|
192
167
|
res.once('close', settle);
|
|
193
|
-
// Never rejects
|
|
194
|
-
//
|
|
195
|
-
// The thrown message can name server paths (a lock timeout does), so it
|
|
196
|
-
// goes only to the host's log.
|
|
168
|
+
// Never rejects (close() waits on it); the thrown message can name
|
|
169
|
+
// server paths, so it goes only to the host's log.
|
|
197
170
|
const done = this.handle(req, res)
|
|
198
171
|
.catch((e) => {
|
|
199
172
|
try {
|
|
@@ -265,20 +238,13 @@ export class HttpTransportHost {
|
|
|
265
238
|
}
|
|
266
239
|
async closeGracefully(s, graceMs) {
|
|
267
240
|
this.closing = true;
|
|
268
|
-
// Node's close() also destroys
|
|
269
|
-
//
|
|
270
|
-
// body would be cut short. Idle sockets are closed below instead.
|
|
241
|
+
// Node's close() also destroys idle connections, which would cut short a
|
|
242
|
+
// large response still flushing; idle sockets are closed below instead.
|
|
271
243
|
const nodeIdleCloser = s.closeIdleConnections;
|
|
272
244
|
s.closeIdleConnections = () => undefined;
|
|
273
245
|
const closed = new Promise((resolve) => s.close(() => resolve()));
|
|
274
246
|
s.closeIdleConnections = nodeIdleCloser;
|
|
275
|
-
//
|
|
276
|
-
// after the last of them: that one says so if it has not started, and the
|
|
277
|
-
// settle hook in start() ends the socket once it finishes. Node itself
|
|
278
|
-
// destroys a socket once a Connection: close response is flushed, which
|
|
279
|
-
// resets it just as endAfterFlush explains, so that end lingers too. A
|
|
280
|
-
// socket with nothing pending may still be delivering the tail of its
|
|
281
|
-
// last response, so only one that never accepted a request is destroyed.
|
|
247
|
+
// Socket bookkeeping below; see docs/internals.md (Graceful close).
|
|
282
248
|
for (const socket of this.sockets) {
|
|
283
249
|
const entry = this.pending.get(socket);
|
|
284
250
|
if (!entry && !this.served.has(socket)) {
|
|
@@ -357,10 +323,7 @@ export class HttpTransportHost {
|
|
|
357
323
|
}
|
|
358
324
|
if (!this.frontGuard(req, res))
|
|
359
325
|
return;
|
|
360
|
-
// Registered before anything awaits
|
|
361
|
-
// waits on the lane must not have it dispatched once its turn comes. A
|
|
362
|
-
// response queued behind another on its connection never emits 'close', so
|
|
363
|
-
// the socket is checked too.
|
|
326
|
+
// Registered before anything awaits (client-gone skip); see docs/internals.md.
|
|
364
327
|
let gone = false;
|
|
365
328
|
res.once('close', () => (gone = true));
|
|
366
329
|
const socket = req.socket;
|
|
@@ -375,8 +338,8 @@ export class HttpTransportHost {
|
|
|
375
338
|
return;
|
|
376
339
|
}
|
|
377
340
|
const sub = auth.sub ?? 'owner';
|
|
378
|
-
// Tenant resolution (
|
|
379
|
-
// server. Absent resolver = the one boot server for every subject.
|
|
341
|
+
// Tenant resolution (a multi-tenant host fills this seam): map the
|
|
342
|
+
// verified subject to ITS server. Absent resolver = the one boot server for every subject.
|
|
380
343
|
let target = {
|
|
381
344
|
server: this.opts.server,
|
|
382
345
|
argShapeFor: this.opts.argShapeFor,
|
|
@@ -400,8 +363,7 @@ export class HttpTransportHost {
|
|
|
400
363
|
return;
|
|
401
364
|
}
|
|
402
365
|
// An invalid shape falls through to today's exact answer rather than
|
|
403
|
-
// forwarding something unvalidated
|
|
404
|
-
// either way.
|
|
366
|
+
// forwarding something unvalidated.
|
|
405
367
|
resolved = null;
|
|
406
368
|
}
|
|
407
369
|
target = resolved;
|
|
@@ -437,9 +399,8 @@ export class HttpTransportHost {
|
|
|
437
399
|
}
|
|
438
400
|
}
|
|
439
401
|
async admitted(req, res, target, sub, clientGone) {
|
|
440
|
-
// Thread the verified subject to tool handlers
|
|
441
|
-
//
|
|
442
|
-
// bearer value must not re-enter the dispatch path via handler context.
|
|
402
|
+
// Thread the verified subject to tool handlers via ctx.http.authInfo;
|
|
403
|
+
// token/clientId stay empty so the bearer value never re-enters the dispatch path.
|
|
443
404
|
req.auth = {
|
|
444
405
|
token: '',
|
|
445
406
|
clientId: '',
|
|
@@ -457,28 +418,16 @@ export class HttpTransportHost {
|
|
|
457
418
|
if (Array.isArray(body) && body.length > maxFrames) {
|
|
458
419
|
return this.fail(res, 400, 'batch_too_large', `a JSON-RPC batch may hold at most ${maxFrames} messages`);
|
|
459
420
|
}
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
// exact-Host match is stricter than the front guard and would 403 valid
|
|
463
|
-
// Hosts (double enforcement, differing rules).
|
|
421
|
+
// The SDK's own DNS-rebind guard is disabled: Host/Origin are already
|
|
422
|
+
// enforced above, and its stricter exact-Host match would 403 valid Hosts.
|
|
464
423
|
const transport = new NodeStreamableHTTPServerTransport({
|
|
465
424
|
sessionIdGenerator: undefined,
|
|
466
425
|
enableJsonResponse: true,
|
|
467
426
|
enableDnsRebindingProtection: false,
|
|
468
427
|
});
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
// can never hit "Already connected". The serialized section is guaranteed to
|
|
472
|
-
// settle three ways — the handler finishes, the client disconnects (res
|
|
473
|
-
// 'close'), or a dispatch deadline fires — so neither a client that holds the
|
|
474
|
-
// connection open nor a hung upstream can wedge the lock. The transport is
|
|
475
|
-
// closed in a finally (which resets server._transport) before the lock
|
|
476
|
-
// releases. (A shared initialized-state persists across stateless requests;
|
|
477
|
-
// benign for the single-owner design.)
|
|
428
|
+
// connect + dispatch + disconnect run inside the per-server mutex, keyed
|
|
429
|
+
// on the server identity, never the subject; see docs/internals.md.
|
|
478
430
|
const deadlineMs = this.opts.dispatchTimeoutMs ?? DISPATCH_TIMEOUT_DEFAULT;
|
|
479
|
-
// The lock lane is the SERVER identity, never the subject: two subjects
|
|
480
|
-
// reaching one server (the boot server, or a resolver mapping several
|
|
481
|
-
// subjects to one) would otherwise race it through the connect gap.
|
|
482
431
|
const { server: mcpServer, argShapeFor, strictArgs, validationEnvelope } = target;
|
|
483
432
|
await this.serializeFor(mcpServer, async () => {
|
|
484
433
|
if (clientGone()) {
|
|
@@ -491,16 +440,14 @@ export class HttpTransportHost {
|
|
|
491
440
|
timer = setTimeout(() => resolve('timeout'), deadlineMs);
|
|
492
441
|
timer.unref?.();
|
|
493
442
|
});
|
|
494
|
-
//
|
|
495
|
-
// so the tap above it still classifies the original validation prose.
|
|
443
|
+
// Envelope rewrite innermost, as on stdio; see docs/internals.md (Argument normalization).
|
|
496
444
|
const enveloped = withValidationEnvelope(transport, validationEnvelope ?? {});
|
|
497
445
|
const tapped = this.opts.metricsTap ? this.opts.metricsTap(enveloped) : enveloped;
|
|
498
446
|
await mcpServer.connect(argShapeFor || strictArgs
|
|
499
447
|
? withArgNormalization(tapped, argShapeFor ?? (() => undefined), this.opts.log, this.opts.onArgRename, strictArgs)
|
|
500
448
|
: tapped);
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
// rejection — but a genuine dispatch error still propagates (rethrown below).
|
|
449
|
+
// Non-rejecting arm: an orphaned handler settling after the deadline wins
|
|
450
|
+
// must not surface as an unhandled rejection; a real error is rethrown below.
|
|
504
451
|
let dispatchErr;
|
|
505
452
|
const dispatchArm = transport
|
|
506
453
|
.handleRequest(req, res, body)
|
|
@@ -538,7 +485,7 @@ export class HttpTransportHost {
|
|
|
538
485
|
health(res) {
|
|
539
486
|
const { config, ownerConfigured, version } = this.opts;
|
|
540
487
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
541
|
-
// No secrets, no owner emails
|
|
488
|
+
// No secrets, no owner emails.
|
|
542
489
|
res.end(JSON.stringify({
|
|
543
490
|
status: 'ok',
|
|
544
491
|
transport: config.transport,
|
package/dist/identity.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { hasToken, readToken, updateToken, writeToken } from './token-store.js';
|
|
|
4
4
|
import { type Policy, type Transport } from './write-control.js';
|
|
5
5
|
/**
|
|
6
6
|
* The one forward-compat seam (frozen public API): the free core builds
|
|
7
|
-
* exactly one context with subject "owner";
|
|
8
|
-
* from N registries. Nothing here may assume a global current-account.
|
|
7
|
+
* exactly one context with subject "owner"; a multi-tenant host instantiates
|
|
8
|
+
* N contexts from N registries. Nothing here may assume a global current-account.
|
|
9
9
|
* `subject` is a string (not the literal 'owner') so a context can carry a
|
|
10
10
|
* tenant id; the free core still always builds 'owner'.
|
|
11
11
|
*/
|
package/dist/identity.js
CHANGED
|
@@ -2,18 +2,15 @@ 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
|
|
6
|
-
// never unlocks
|
|
7
|
-
// operator diagnose report): a hand-built context claiming 'owner' stays a
|
|
8
|
-
// non-owner context.
|
|
5
|
+
// Only buildIdentityContext mints the owner brand: the subject string alone
|
|
6
|
+
// never unlocks owner-only surfaces, so a hand-built context claiming 'owner' stays non-owner.
|
|
9
7
|
const ownerContexts = new WeakSet();
|
|
10
8
|
export function isOwnerContext(ctx) {
|
|
11
9
|
return ctx !== undefined && ownerContexts.has(ctx);
|
|
12
10
|
}
|
|
13
11
|
export function buildIdentityContext(env = process.env, opts = {}) {
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// behavior-identical to the module-level functions for the free core.
|
|
12
|
+
// The SAME factories a multi-tenant host instantiates per tenant, bound here
|
|
13
|
+
// to the global registry + token dir.
|
|
17
14
|
const tokenStore = createTokenStore(getTokenDir());
|
|
18
15
|
const ctx = {
|
|
19
16
|
subject: 'owner',
|
|
@@ -22,8 +19,8 @@ export function buildIdentityContext(env = process.env, opts = {}) {
|
|
|
22
19
|
get accounts() {
|
|
23
20
|
return getAccountSet();
|
|
24
21
|
},
|
|
25
|
-
// The dispatch transport rides into the resolved policy as a reserved seam
|
|
26
|
-
//
|
|
22
|
+
// The dispatch transport rides into the resolved policy as a reserved seam;
|
|
23
|
+
// it does not change any write-control verdict.
|
|
27
24
|
policy: resolvePolicy(env, { transport: opts.transport }),
|
|
28
25
|
getClient: makeGetClient('owner', {
|
|
29
26
|
accounts: getAccountSet,
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// First import: triggers accounts.js module load (env files + registry) before
|
|
3
|
-
// anything else. Named to also pull the server-only empty-registry guard
|
|
3
|
+
// anything else. Named to also pull the server-only empty-registry guard.
|
|
4
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
|
-
import { McpServer } from "@modelcontextprotocol/server";
|
|
11
10
|
import { resolveDiscoveryMode } from './registry.js';
|
|
12
11
|
import { isAllowed, describePolicy } from './write-control.js';
|
|
13
12
|
import { buildIdentityContext } from './identity.js';
|
|
14
|
-
import { buildRegistry, requestHooksFor } from './compose.js';
|
|
13
|
+
import { buildRegistry, createMcpServer, requestHooksFor } from './compose.js';
|
|
15
14
|
import { registerSetupPrompt } from './setup-prompt.js';
|
|
16
15
|
import { applyNetTuning } from './net-tuning.js';
|
|
17
16
|
import { withArgNormalization, withValidationEnvelope } from './arg-normalize.js';
|
|
@@ -81,7 +80,7 @@ async function main() {
|
|
|
81
80
|
if (process.argv.includes('config') && process.argv.includes('check')) {
|
|
82
81
|
const ctx = buildIdentityContext();
|
|
83
82
|
const policy = ctx.policy;
|
|
84
|
-
const registry = buildRegistry(
|
|
83
|
+
const registry = buildRegistry(createMcpServer(pkg.version), ctx);
|
|
85
84
|
const cud = registry.tools.filter((t) => t.cud !== 'read');
|
|
86
85
|
const disabled = cud.filter((t) => !isAllowed(t, policy));
|
|
87
86
|
const counts = registry.visibleCount();
|
|
@@ -140,10 +139,8 @@ async function main() {
|
|
|
140
139
|
}
|
|
141
140
|
const wantStdio = httpCfg.transport === 'stdio' || httpCfg.transport === 'both';
|
|
142
141
|
const wantHttp = transportIncludesHttp(httpCfg.transport);
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
// returned. A multi-tenant boot instead refuses the two process-wide env
|
|
146
|
-
// vectors that would leak one operator's accounts/scopes into every tenant.
|
|
142
|
+
// The SERVER never boots with an empty registry (stdio always, HTTP when
|
|
143
|
+
// single-owner); a multi-tenant boot instead refuses the two process-wide env vectors.
|
|
147
144
|
const mt = isMultiTenantBoot();
|
|
148
145
|
if (accountsAssertRequired(wantStdio, mt))
|
|
149
146
|
assertServerAccountsConfigured();
|
|
@@ -155,9 +152,7 @@ async function main() {
|
|
|
155
152
|
// specced "fatal at startup", never mid-dispatch.
|
|
156
153
|
const { resolveMasterKey } = await import('./master-key.js');
|
|
157
154
|
resolveMasterKey();
|
|
158
|
-
//
|
|
159
|
-
// when off (no wrapper, no dir, nothing initializes). One instance per
|
|
160
|
-
// transport so `boots` keys stay honest under `both`.
|
|
155
|
+
// One Metrics instance per transport so `boots` keys stay honest under `both`.
|
|
161
156
|
const envSrc = envValueSource('GOOGLE_USAGE_METRICS');
|
|
162
157
|
const metricsState = resolveUsageMetrics(process.env, loadConfigFile()?.usageMetrics, envSrc?.kind === 'file' ? envSrc.file : undefined);
|
|
163
158
|
if (metricsState.warning)
|
|
@@ -187,21 +182,17 @@ async function main() {
|
|
|
187
182
|
});
|
|
188
183
|
}
|
|
189
184
|
}
|
|
190
|
-
// Build one McpServer + registry per transport at boot (
|
|
191
|
-
//
|
|
185
|
+
// Build one McpServer + registry per transport at boot (never
|
|
186
|
+
// rebuilt per request); `both` runs the two concurrently.
|
|
192
187
|
if (wantStdio) {
|
|
193
|
-
const server =
|
|
188
|
+
const server = createMcpServer(pkg.version);
|
|
194
189
|
const stdioMetrics = initMetricsFor('stdio', resolveDiscoveryMode());
|
|
195
190
|
const stdioCtx = buildIdentityContext(process.env, { transport: 'stdio' });
|
|
196
191
|
const registry = buildRegistry(server, stdioCtx, undefined, stdioMetrics);
|
|
197
192
|
registry.installListHandler();
|
|
198
193
|
registerSetupPrompt(server);
|
|
199
194
|
const stdioHooks = requestHooksFor(registry, stdioCtx, stdioMetrics);
|
|
200
|
-
//
|
|
201
|
-
// envelope rewrite is INNERMOST (last to touch the frame), the tap sits
|
|
202
|
-
// above it (classifying the ORIGINAL validation prose), arg normalization
|
|
203
|
-
// outermost. The tap only counts outbound JSON-RPC error frames (no
|
|
204
|
-
// handler ran) plus id->tool names.
|
|
195
|
+
// Composition order (outbound runs outermost-first): see docs/internals.md (Argument normalization).
|
|
205
196
|
let transport = withValidationEnvelope(new StdioServerTransport(), stdioHooks.validationEnvelope);
|
|
206
197
|
if (stdioMetrics) {
|
|
207
198
|
const { tapUsageMetrics } = await import('./metrics-tap.js');
|
|
@@ -209,10 +200,8 @@ async function main() {
|
|
|
209
200
|
}
|
|
210
201
|
await server.connect(stdioHooks.argShapeFor || stdioHooks.strictArgs
|
|
211
202
|
? withArgNormalization(transport,
|
|
212
|
-
// Gated exactly as the HTTP leg gates
|
|
213
|
-
//
|
|
214
|
-
// GOOGLE_ARG_NORMALIZE=off, so the same call succeeded on stdio
|
|
215
|
-
// and failed on HTTP once screening rejects.
|
|
203
|
+
// Gated exactly as the HTTP leg gates argShapeFor, or GOOGLE_ARG_NORMALIZE=off
|
|
204
|
+
// would rename keys on stdio while HTTP's screening rejects the same call.
|
|
216
205
|
stdioHooks.argShapeFor ?? (() => undefined), undefined, stdioMetrics ? (tool, n) => stdioMetrics.recordArgFix(tool, n) : undefined, stdioHooks.strictArgs)
|
|
217
206
|
: transport);
|
|
218
207
|
}
|
|
@@ -221,31 +210,30 @@ async function main() {
|
|
|
221
210
|
const owners = parseOwnerEmails(process.env);
|
|
222
211
|
const gates = mtBootGates();
|
|
223
212
|
if (gates?.multiTenant) {
|
|
224
|
-
// Same fail-fast
|
|
225
|
-
// must never boot; under tenancy the gate is "a valid provisioning
|
|
213
|
+
// Same fail-fast shape as below, but the gate is "a valid provisioning
|
|
226
214
|
// mechanism exists", not a flat owner allowlist.
|
|
227
215
|
gates.assertProvisioningGate();
|
|
228
216
|
}
|
|
229
217
|
else {
|
|
230
|
-
//
|
|
218
|
+
// The owner allowlist is the entire multi-tenant collapse;
|
|
231
219
|
// refuse to open an ungated HTTP endpoint.
|
|
232
220
|
if (owners.length === 0) {
|
|
233
221
|
process.stderr.write('E_OWNER_EMAILS_REQUIRED: MCP_TRANSPORT includes http but MCP_OWNER_EMAILS is empty. Set MCP_OWNER_EMAILS to the Google email(s) allowed to authenticate.\n');
|
|
234
222
|
process.exit(1);
|
|
235
223
|
}
|
|
236
224
|
}
|
|
237
|
-
//
|
|
225
|
+
// Stateless HTTP cannot push tools/list_changed, so it forces curated.
|
|
238
226
|
const configuredMode = (process.env.GOOGLE_DISCOVERY ?? '').trim().toLowerCase();
|
|
239
227
|
if (configuredMode && configuredMode !== 'curated') {
|
|
240
228
|
process.stderr.write(`GOOGLE_DISCOVERY="${configuredMode}" is ignored over HTTP; the stateless transport forces "curated".\n`);
|
|
241
229
|
}
|
|
242
|
-
const httpServer =
|
|
230
|
+
const httpServer = createMcpServer(pkg.version);
|
|
243
231
|
const httpMetrics = initMetricsFor('http', 'curated');
|
|
244
232
|
const httpCtx = buildIdentityContext(process.env, { transport: 'http' });
|
|
245
233
|
const registry = buildRegistry(httpServer, httpCtx, 'curated', httpMetrics);
|
|
246
234
|
registry.installListHandler();
|
|
247
235
|
registerSetupPrompt(httpServer);
|
|
248
|
-
//
|
|
236
|
+
// Mount the OAuth 2.1 AS (MCP-client tokens + owner login) + the Bearer authenticator.
|
|
249
237
|
const { buildAuthServer, verifiedEmailFromIdToken } = await import('./oauth-as.js');
|
|
250
238
|
const { jwtSecretFrom } = await import('./mcp-token.js');
|
|
251
239
|
const { resolveJwtKey, resolveMasterKeyForDispatch } = await import('./master-key.js');
|
|
@@ -288,7 +276,7 @@ async function main() {
|
|
|
288
276
|
aliasEmail: (alias) => getAccountSet().configs[alias]?.email,
|
|
289
277
|
log: (l) => process.stderr.write(`[as] ${l}\n`),
|
|
290
278
|
});
|
|
291
|
-
//
|
|
279
|
+
// Over HTTP, a dead/missing per-account token surfaces a clickable,
|
|
292
280
|
// signed re-auth link into the AS's alias_reauth flow instead of a CLI hint.
|
|
293
281
|
const { setHttpReauthLink } = await import('./reauth-hint.js');
|
|
294
282
|
setHttpReauthLink((alias) => authServer.reauthLink(alias));
|
|
@@ -314,7 +302,7 @@ async function main() {
|
|
|
314
302
|
});
|
|
315
303
|
await host.start();
|
|
316
304
|
process.stderr.write(`HTTP transport listening on http://${httpCfg.host}:${httpCfg.port} (public ${httpCfg.publicUrl})\n`);
|
|
317
|
-
// Graceful shutdown so `docker run --init`
|
|
305
|
+
// Graceful shutdown so `docker run --init` forwards SIGTERM cleanly.
|
|
318
306
|
const shutdown = () => {
|
|
319
307
|
host.close().finally(() => process.exit(0));
|
|
320
308
|
};
|
package/dist/master-key.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface KeyDeps {
|
|
|
15
15
|
hasAnyToken?: () => boolean;
|
|
16
16
|
}
|
|
17
17
|
export declare function __setKeychainFactoryForTest(fn: ((account: string) => KeychainEntry | null) | null): void;
|
|
18
|
-
/**
|
|
18
|
+
/** Single choke point: no other module may read the env vars directly.
|
|
19
19
|
* Boot/CLI form: failures exit with a clean stderr slug ("fatal at startup"). */
|
|
20
20
|
export declare function resolveMasterKey(deps?: KeyDeps): ResolvedKey;
|
|
21
21
|
/** Dispatch form: identical resolution, but failures THROW (caught by the tool
|
|
@@ -28,14 +28,14 @@ export declare function peekMasterKeyProvenance(deps?: KeyDeps): KeyProvenance |
|
|
|
28
28
|
* outstanding MCP tokens and clients re-auth silently. */
|
|
29
29
|
export declare function resolveJwtKey(deps?: KeyDeps): ResolvedKey;
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
31
|
+
* After the FIRST successful token decrypt with an env-sourced key,
|
|
32
32
|
* mirror it into the keychain (only when the keychain holds no entry) so the
|
|
33
33
|
* key survives an env loss. Gating on a successful decrypt means a stale or
|
|
34
34
|
* wrong env key is never mirrored. Best-effort: failures are doctor material,
|
|
35
35
|
* never fatal.
|
|
36
36
|
*/
|
|
37
37
|
export declare function noteSuccessfulDecrypt(): void;
|
|
38
|
-
/**
|
|
38
|
+
/** Reset: drop the generated MASTER_KEY material (0600 file + keychain
|
|
39
39
|
* entry) so the next run re-provisions. Never removes an env-provided key (the
|
|
40
40
|
* deployer owns that; `env:true` flags it so reset can warn). The caller MUST
|
|
41
41
|
* have wiped every .enc token first — regenerating with tokens present would
|