mcp-google-multi 5.4.1-alpha.2 → 6.0.0-alpha.2

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.
Files changed (116) hide show
  1. package/README.md +21 -16
  2. package/dist/accounts.d.ts +44 -5
  3. package/dist/accounts.js +267 -25
  4. package/dist/auth.d.ts +11 -4
  5. package/dist/auth.js +85 -78
  6. package/dist/client-config.d.ts +90 -0
  7. package/dist/client-config.js +282 -0
  8. package/dist/client.js +10 -4
  9. package/dist/config-file.d.ts +38 -0
  10. package/dist/config-file.js +117 -0
  11. package/dist/discover.js +57 -3
  12. package/dist/doctor.d.ts +86 -0
  13. package/dist/doctor.js +384 -0
  14. package/dist/env-load.d.ts +11 -0
  15. package/dist/env-load.js +52 -0
  16. package/dist/executor.d.ts +2 -0
  17. package/dist/executor.js +2 -1
  18. package/dist/fanout.js +8 -5
  19. package/dist/fs-atomic.d.ts +3 -0
  20. package/dist/fs-atomic.js +123 -0
  21. package/dist/http-config.d.ts +35 -0
  22. package/dist/http-config.js +113 -0
  23. package/dist/http-transport.d.ts +59 -0
  24. package/dist/http-transport.js +269 -0
  25. package/dist/identity.d.ts +24 -0
  26. package/dist/identity.js +19 -0
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +199 -18
  29. package/dist/master-key.d.ts +53 -0
  30. package/dist/master-key.js +242 -0
  31. package/dist/mcp-token.d.ts +81 -0
  32. package/dist/mcp-token.js +176 -0
  33. package/dist/migrate-config.d.ts +7 -0
  34. package/dist/migrate-config.js +103 -0
  35. package/dist/migrate-tokens.js +4 -3
  36. package/dist/net-tuning.d.ts +8 -0
  37. package/dist/net-tuning.js +25 -0
  38. package/dist/oauth-as.d.ts +52 -0
  39. package/dist/oauth-as.js +457 -0
  40. package/dist/oauth-consent.d.ts +28 -0
  41. package/dist/oauth-consent.js +109 -0
  42. package/dist/open-url.d.ts +1 -0
  43. package/dist/open-url.js +30 -0
  44. package/dist/reauth-hint.d.ts +2 -0
  45. package/dist/reauth-hint.js +15 -0
  46. package/dist/registry-transfer.d.ts +41 -0
  47. package/dist/registry-transfer.js +219 -0
  48. package/dist/registry.d.ts +27 -1
  49. package/dist/registry.js +117 -6
  50. package/dist/scope-catalog.d.ts +19 -0
  51. package/dist/scope-catalog.js +182 -0
  52. package/dist/scope-observability.d.ts +78 -0
  53. package/dist/scope-observability.js +192 -0
  54. package/dist/services.js +1 -1
  55. package/dist/setup-prompt.d.ts +9 -0
  56. package/dist/setup-prompt.js +59 -0
  57. package/dist/ssrf-guard.d.ts +48 -0
  58. package/dist/ssrf-guard.js +242 -0
  59. package/dist/token-store.js +23 -125
  60. package/dist/tools/_errors.d.ts +8 -2
  61. package/dist/tools/_errors.js +97 -6
  62. package/dist/tools/account-wizard.d.ts +33 -0
  63. package/dist/tools/account-wizard.js +284 -0
  64. package/dist/tools/accounts-tool.d.ts +8 -5
  65. package/dist/tools/accounts-tool.js +38 -15
  66. package/dist/tools/admin.js +2 -2
  67. package/dist/tools/calendar.js +2 -2
  68. package/dist/tools/chat.js +2 -2
  69. package/dist/tools/contacts.d.ts +40 -0
  70. package/dist/tools/contacts.js +135 -2
  71. package/dist/tools/docs.d.ts +3 -0
  72. package/dist/tools/docs.js +60 -9
  73. package/dist/tools/drive.d.ts +3 -0
  74. package/dist/tools/drive.js +60 -8
  75. package/dist/tools/forms.js +4 -2
  76. package/dist/tools/generated/_shared.d.ts +1 -3
  77. package/dist/tools/generated/_shared.js +3 -3
  78. package/dist/tools/generated/admin.js +175 -132
  79. package/dist/tools/generated/appsmarket.js +6 -2
  80. package/dist/tools/generated/calendar.js +45 -27
  81. package/dist/tools/generated/chat.js +81 -47
  82. package/dist/tools/generated/classroom.js +131 -104
  83. package/dist/tools/generated/cloudidentity.js +73 -61
  84. package/dist/tools/generated/cloudsearch.js +59 -49
  85. package/dist/tools/generated/contacts.js +22 -14
  86. package/dist/tools/generated/drive.js +51 -35
  87. package/dist/tools/generated/driveactivity.js +5 -1
  88. package/dist/tools/generated/drivelabels.js +30 -25
  89. package/dist/tools/generated/forms.js +7 -3
  90. package/dist/tools/generated/gmail.js +73 -58
  91. package/dist/tools/generated/groupsmigration.js +5 -1
  92. package/dist/tools/generated/groupssettings.js +7 -3
  93. package/dist/tools/generated/keep.js +12 -7
  94. package/dist/tools/generated/licensing.js +11 -7
  95. package/dist/tools/generated/meet.js +20 -13
  96. package/dist/tools/generated/postmaster.js +8 -4
  97. package/dist/tools/generated/reseller.js +22 -17
  98. package/dist/tools/generated/script.js +26 -16
  99. package/dist/tools/generated/sheets.js +11 -7
  100. package/dist/tools/generated/tasks.js +6 -2
  101. package/dist/tools/generated/vault.js +38 -33
  102. package/dist/tools/generated/workspaceevents.js +12 -7
  103. package/dist/tools/gmail-mime.d.ts +38 -8
  104. package/dist/tools/gmail-mime.js +75 -71
  105. package/dist/tools/gmail.d.ts +31 -0
  106. package/dist/tools/gmail.js +483 -99
  107. package/dist/tools/google-api.js +7 -4
  108. package/dist/tools/meet.js +2 -2
  109. package/dist/tools/searchconsole.js +2 -2
  110. package/dist/tools/sheets.js +6 -2
  111. package/dist/tools/slides.js +4 -2
  112. package/dist/tools/tasks.js +2 -2
  113. package/dist/types.d.ts +1 -1
  114. package/dist/write-control.d.ts +24 -1
  115. package/dist/write-control.js +26 -1
  116. package/package.json +26 -7
@@ -0,0 +1,52 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { fileURLToPath } from 'node:url';
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ // Precedence: real env > CWD .env > package-root .env > ~/.config .env.
7
+ // process.loadEnvFile never overwrites keys already in process.env (pinned by
8
+ // tests/env-load.test.ts T4), so highest-priority file loads FIRST and real
9
+ // env wins by already being present; the boot snapshot re-assert is a second
10
+ // line of defense should those semantics ever change.
11
+ export function loadEnvFiles(paths = {}) {
12
+ // loadEnvFile exists since 20.12, so a feature check alone would let 20.12-21.x through.
13
+ const major = Number(process.versions.node.split('.', 1)[0]);
14
+ if (major < 22 || typeof process.loadEnvFile !== 'function') {
15
+ process.stderr.write(`E_NODE_TOO_OLD: mcp-google-multi requires Node.js >= 22 (running ${process.versions.node}). Upgrade to Node 22 LTS or newer.\n`);
16
+ process.exit(1);
17
+ }
18
+ const configDir = paths.configDir ??
19
+ path.join(process.env.XDG_CONFIG_HOME || path.join(homedir(), '.config'), 'mcp-google-multi');
20
+ const explicit = process.env.MCP_GOOGLE_MULTI_ENV;
21
+ const candidates = explicit
22
+ ? [path.resolve(explicit)]
23
+ : [
24
+ path.join(paths.cwd ?? process.cwd(), '.env'),
25
+ path.join(paths.packageRoot ?? path.resolve(__dirname, '..'), '.env'),
26
+ path.join(configDir, '.env'),
27
+ ];
28
+ const boot = new Map(Object.entries(process.env).filter((e) => e[1] !== undefined));
29
+ const loaded = [];
30
+ for (const p of candidates) {
31
+ try {
32
+ process.loadEnvFile(p);
33
+ loaded.push(p);
34
+ }
35
+ catch (e) {
36
+ if (e.code === 'ENOENT') {
37
+ // Node collapses every open()-stage failure (EACCES, ENOTDIR, ...) into
38
+ // ENOENT, so stat to tell "absent" from "present but unreadable".
39
+ if (explicit) {
40
+ const reason = existsSync(p) ? 'exists but is not readable' : 'does not exist';
41
+ process.stderr.write(`E_ENV_NOT_FOUND: MCP_GOOGLE_MULTI_ENV points to "${p}" but the file ${reason}.\n`);
42
+ process.exit(1);
43
+ }
44
+ continue;
45
+ }
46
+ throw e;
47
+ }
48
+ }
49
+ for (const [k, v] of boot)
50
+ process.env[k] = v;
51
+ return { loaded, searched: candidates };
52
+ }
@@ -8,6 +8,8 @@ export interface ApiMethodRef {
8
8
  path: string;
9
9
  baseUrl: string;
10
10
  requiredParams: string[];
11
+ /** Escape hatch: from runtime Discovery. Generated tools: baked at gen time. */
12
+ scopes?: readonly string[];
11
13
  }
12
14
  export interface ExecuteDeps {
13
15
  getClientFn?: typeof getClient;
package/dist/executor.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { getClient } from './client.js';
2
2
  import { expandPath, isGoogleApiUrl } from './discovery-client.js';
3
3
  import { handleGoogleApiError } from './tools/_errors.js';
4
+ import { scopeHintForMethod } from './scope-observability.js';
4
5
  export const MAX_RESPONSE_CHARS = 100_000;
5
6
  export function jsonResult(payload, isError = false) {
6
7
  const base = { content: [{ type: 'text', text: JSON.stringify(payload) }] };
@@ -91,6 +92,6 @@ export async function executeApiMethod(method, args, deps = {}) {
91
92
  return { content: [{ type: 'text', text }] };
92
93
  }
93
94
  catch (error) {
94
- return handleGoogleApiError(error, args.account);
95
+ return handleGoogleApiError(error, args.account, undefined, () => method.scopes?.length ? scopeHintForMethod(method.scopes, args.account) : null);
95
96
  }
96
97
  }
package/dist/fanout.js CHANGED
@@ -1,14 +1,17 @@
1
1
  import { z } from 'zod';
2
- import { ACCOUNTS } from './accounts.js';
2
+ import { ACCOUNTS, getAccountSet } from './accounts.js';
3
3
  export const CSV_RE = /^[a-zA-Z0-9_-]+(\s*,\s*[a-zA-Z0-9_-]+)+$/;
4
4
  const FANOUT_CONCURRENCY = 5;
5
5
  export function fanoutAccountField(description) {
6
6
  const csvExample = ACCOUNTS.length > 1 ? `; or a CSV subset like "${ACCOUNTS.slice(0, 2).join(',')}"` : '';
7
7
  return z
8
- .union([z.enum([...ACCOUNTS, '*']), z.string().regex(CSV_RE)])
9
- .describe(`${description}; "*" = all accounts${csvExample}`);
8
+ // '*' first so the tuple is statically non-empty even when ACCOUNTS is empty
9
+ // (a fresh install): z.enum requires [string, ...string[]].
10
+ .union([z.enum(['*', ...ACCOUNTS]), z.string().regex(CSV_RE)])
11
+ .optional()
12
+ .describe(`${description}; "*" = all accounts${csvExample}; omit for the default account`);
10
13
  }
11
- export function parseAccountSelector(value, accounts = ACCOUNTS) {
14
+ export function parseAccountSelector(value, accounts = getAccountSet().aliases) {
12
15
  if (value === '*')
13
16
  return { ok: true, fanout: true, aliases: [...accounts] };
14
17
  if (!value.includes(','))
@@ -29,7 +32,7 @@ export function parseAccountSelector(value, accounts = ACCOUNTS) {
29
32
  return { ok: false, invalid };
30
33
  return { ok: true, fanout: aliases.length > 1, aliases };
31
34
  }
32
- export function invalidAccountsResult(invalid, accounts = ACCOUNTS) {
35
+ export function invalidAccountsResult(invalid, accounts = getAccountSet().aliases) {
33
36
  return {
34
37
  content: [
35
38
  {
@@ -0,0 +1,3 @@
1
+ export declare function withFileLock<T>(absPath: string, fn: () => T, label?: string): T;
2
+ export declare function atomicWriteFileSync(absPath: string, contents: string, mode?: number): void;
3
+ export declare function atomicWriteWithLock(absPath: string, contents: string, mode?: number): void;
@@ -0,0 +1,123 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomBytes } from 'node:crypto';
4
+ const LOCK_TIMEOUT_MS = 5_000;
5
+ const LOCK_RETRY_MS = 10;
6
+ const RENAME_ATTEMPTS = 5;
7
+ const RENAME_RETRY_MS = 20;
8
+ function sleep(ms) {
9
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
10
+ }
11
+ // Hardlink-based advisory lock, generalized path-keyed from the token store's
12
+ // alias-keyed original (same mechanics: dead-owner recovery via signal-0 probe,
13
+ // EPERM = alive, corrupt lock content recovered as dead, 5s timeout).
14
+ export function withFileLock(absPath, fn, label = `lock: ${absPath}`) {
15
+ const dir = path.dirname(absPath);
16
+ const lock = path.join(dir, `.${path.basename(absPath)}.lock`);
17
+ const ownerFile = `${lock}.${process.pid}.${randomBytes(6).toString('hex')}.owner`;
18
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
19
+ fs.writeFileSync(ownerFile, String(process.pid), { mode: 0o600, flag: 'wx' });
20
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
21
+ try {
22
+ while (true) {
23
+ try {
24
+ fs.linkSync(ownerFile, lock);
25
+ break;
26
+ }
27
+ catch (error) {
28
+ const err = error;
29
+ if (err.code !== 'EEXIST')
30
+ throw error;
31
+ try {
32
+ const observedOwner = fs.readFileSync(lock, 'utf8');
33
+ const owner = Number(observedOwner);
34
+ let ownerDead = !(Number.isSafeInteger(owner) && owner > 0);
35
+ if (!ownerDead) {
36
+ try {
37
+ process.kill(owner, 0);
38
+ }
39
+ catch (ownerError) {
40
+ const code = ownerError.code;
41
+ // EPERM: PID exists but is not signalable (recycled by another
42
+ // user); treat as alive, never break a lock we cannot verify.
43
+ if (code === 'ESRCH')
44
+ ownerDead = true;
45
+ else if (code !== 'EPERM')
46
+ throw ownerError;
47
+ }
48
+ }
49
+ if (ownerDead) {
50
+ if (fs.readFileSync(lock, 'utf8') === observedOwner)
51
+ fs.rmSync(lock, { force: true });
52
+ continue;
53
+ }
54
+ }
55
+ catch (readError) {
56
+ if (readError.code === 'ENOENT') {
57
+ continue;
58
+ }
59
+ throw readError;
60
+ }
61
+ if (Date.now() >= deadline) {
62
+ throw new Error(`Timed out waiting for ${label}`, { cause: error });
63
+ }
64
+ sleep(LOCK_RETRY_MS);
65
+ }
66
+ }
67
+ }
68
+ finally {
69
+ fs.rmSync(ownerFile, { force: true });
70
+ }
71
+ try {
72
+ return fn();
73
+ }
74
+ finally {
75
+ fs.rmSync(lock, { force: true });
76
+ }
77
+ }
78
+ export function atomicWriteFileSync(absPath, contents, mode = 0o600) {
79
+ const dir = path.dirname(absPath);
80
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
81
+ const tmp = path.join(dir, `.${path.basename(absPath)}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`);
82
+ try {
83
+ fs.writeFileSync(tmp, contents, { mode, flag: 'wx' });
84
+ // Open read-write, not read-only: on Windows fsync maps to
85
+ // FlushFileBuffers, which returns EPERM on a read-only handle.
86
+ const fd = fs.openSync(tmp, 'r+');
87
+ try {
88
+ fs.fsyncSync(fd);
89
+ }
90
+ finally {
91
+ fs.closeSync(fd);
92
+ }
93
+ renameWithRetry(tmp, absPath);
94
+ }
95
+ finally {
96
+ try {
97
+ fs.rmSync(tmp, { force: true });
98
+ }
99
+ catch {
100
+ // force only suppresses ENOENT; a Windows handle-holder can make this
101
+ // throw and mask the real write error. The orphan tmp is harmless.
102
+ }
103
+ }
104
+ }
105
+ // Windows only: renaming over a momentarily-open file throws transient EPERM/EACCES/EBUSY (reads take no lock); see docs/internals.md.
106
+ function renameWithRetry(from, to) {
107
+ for (let attempt = 1;; attempt++) {
108
+ try {
109
+ fs.renameSync(from, to);
110
+ return;
111
+ }
112
+ catch (error) {
113
+ const code = error.code;
114
+ const transient = code === 'EPERM' || code === 'EACCES' || code === 'EBUSY';
115
+ if (!transient || attempt >= RENAME_ATTEMPTS)
116
+ throw error;
117
+ sleep(RENAME_RETRY_MS * attempt);
118
+ }
119
+ }
120
+ }
121
+ export function atomicWriteWithLock(absPath, contents, mode = 0o600) {
122
+ withFileLock(absPath, () => atomicWriteFileSync(absPath, contents, mode));
123
+ }
@@ -0,0 +1,35 @@
1
+ export type Transport = 'stdio' | 'http' | 'both';
2
+ export declare const DEFAULT_HTTP_HOST = "127.0.0.1";
3
+ export declare const DEFAULT_HTTP_PORT = 4243;
4
+ export declare const CLAUDE_AI_ORIGIN = "https://claude.ai";
5
+ /** Slug-carrying boot-time config failure; caller surfaces the message. */
6
+ export declare class HttpConfigError extends Error {
7
+ readonly slug: string;
8
+ constructor(slug: string, message: string);
9
+ }
10
+ export interface HttpConfig {
11
+ transport: Transport;
12
+ host: string;
13
+ port: number;
14
+ /** Canonical public base URL (BR4): lowercase scheme+host, no default port,
15
+ * no trailing slash, no fragment/query. The issuer and every advertised
16
+ * endpoint derive from this. */
17
+ publicUrl: string;
18
+ /** `${publicUrl}/mcp` — the canonical resource URI (JWT `aud` / PRM `resource`). */
19
+ resourceUri: string;
20
+ /** DNS-rebind Host allowlist. Both host[:port] and bare-host forms are
21
+ * included: the Host header may or may not carry the port depending on the
22
+ * client/proxy, and a too-narrow allowlist is a silent 403. */
23
+ allowedHosts: string[];
24
+ /** Origin allowlist for the front guard (publicUrl origin + claude.ai + extras). */
25
+ allowedOrigins: string[];
26
+ }
27
+ export declare function transportIncludesHttp(t: Transport): boolean;
28
+ export declare function resolveTransport(env?: NodeJS.ProcessEnv): Transport;
29
+ /**
30
+ * Canonicalize a public base URL per BR4. WHATWG `URL` already lowercases the
31
+ * scheme+host and omits a default port from `.host`; this adds the HTTPS-only
32
+ * scheme check, strips fragment/query/userinfo, and removes a trailing slash.
33
+ */
34
+ export declare function canonicalizePublicUrl(raw: string): string;
35
+ export declare function resolveHttpConfig(env?: NodeJS.ProcessEnv): HttpConfig;
@@ -0,0 +1,113 @@
1
+ // Transport selection + public-base-URL / Origin / host config plumbing (B11).
2
+ // Pure, resolved once at boot; the HTTP host that consumes this lands in B12
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.
7
+ export const DEFAULT_HTTP_HOST = '127.0.0.1';
8
+ export const DEFAULT_HTTP_PORT = 4243;
9
+ export const CLAUDE_AI_ORIGIN = 'https://claude.ai';
10
+ /** Slug-carrying boot-time config failure; caller surfaces the message. */
11
+ export class HttpConfigError extends Error {
12
+ slug;
13
+ constructor(slug, message) {
14
+ super(message);
15
+ this.slug = slug;
16
+ this.name = 'HttpConfigError';
17
+ }
18
+ }
19
+ export function transportIncludesHttp(t) {
20
+ return t === 'http' || t === 'both';
21
+ }
22
+ export function resolveTransport(env = process.env) {
23
+ const raw = (env.MCP_TRANSPORT ?? '').trim().toLowerCase();
24
+ if (raw === '')
25
+ return 'stdio';
26
+ if (raw === 'stdio' || raw === 'http' || raw === 'both')
27
+ return raw;
28
+ // Load-bearing (decides whether a port opens): fail fast rather than silently
29
+ // downgrade — a user who set `http` and got stdio would be badly confused.
30
+ throw new HttpConfigError('E_INVALID_TRANSPORT', `MCP_TRANSPORT must be one of stdio|http|both (got "${env.MCP_TRANSPORT}")`);
31
+ }
32
+ /**
33
+ * Canonicalize a public base URL per BR4. WHATWG `URL` already lowercases the
34
+ * scheme+host and omits a default port from `.host`; this adds the HTTPS-only
35
+ * scheme check, strips fragment/query/userinfo, and removes a trailing slash.
36
+ */
37
+ export function canonicalizePublicUrl(raw) {
38
+ let u;
39
+ try {
40
+ u = new URL(raw);
41
+ }
42
+ catch {
43
+ throw new HttpConfigError('E_PUBLIC_URL_INVALID', `MCP_PUBLIC_URL is not a valid absolute URL: "${raw}"`);
44
+ }
45
+ if (u.protocol !== 'http:' && u.protocol !== 'https:') {
46
+ throw new HttpConfigError('E_PUBLIC_URL_INVALID', `MCP_PUBLIC_URL must be http(s) (got "${raw}")`);
47
+ }
48
+ u.hash = '';
49
+ u.search = '';
50
+ u.username = '';
51
+ u.password = '';
52
+ const path = u.pathname.replace(/\/+$/, '');
53
+ return `${u.protocol}//${u.host}${path}`;
54
+ }
55
+ function resolvePort(raw) {
56
+ const s = (raw ?? '').trim();
57
+ if (s === '')
58
+ return DEFAULT_HTTP_PORT;
59
+ const n = Number(s);
60
+ if (!Number.isInteger(n) || n < 1 || n > 65535) {
61
+ throw new HttpConfigError('E_INVALID_HTTP_PORT', `MCP_HTTP_PORT must be an integer 1-65535 (got "${raw}")`);
62
+ }
63
+ return n;
64
+ }
65
+ /** `host:port`, bracketing a bare IPv6 literal so it is a valid URL authority. */
66
+ function hostPort(host, port) {
67
+ const h = host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
68
+ return `${h}:${port}`;
69
+ }
70
+ function deriveAllowedHosts(publicUrl, host, port) {
71
+ const pu = new URL(publicUrl);
72
+ const set = new Set();
73
+ set.add(pu.host); // hostname[:port]
74
+ set.add(pu.hostname); // bare hostname
75
+ set.add(host);
76
+ set.add(hostPort(host, port));
77
+ return [...set];
78
+ }
79
+ function deriveAllowedOrigins(publicUrl, extra) {
80
+ const set = new Set();
81
+ set.add(new URL(publicUrl).origin);
82
+ set.add(CLAUDE_AI_ORIGIN);
83
+ for (const raw of (extra ?? '').split(',').map((s) => s.trim()).filter(Boolean)) {
84
+ let o;
85
+ try {
86
+ o = new URL(raw).origin;
87
+ }
88
+ catch {
89
+ throw new HttpConfigError('E_INVALID_ORIGIN', `MCP_ALLOWED_ORIGINS entry is not a valid URL: "${raw}"`);
90
+ }
91
+ if (o === 'null') {
92
+ throw new HttpConfigError('E_INVALID_ORIGIN', `MCP_ALLOWED_ORIGINS entry has no usable origin: "${raw}"`);
93
+ }
94
+ set.add(o);
95
+ }
96
+ return [...set];
97
+ }
98
+ export function resolveHttpConfig(env = process.env) {
99
+ const transport = resolveTransport(env);
100
+ const host = (env.MCP_HTTP_HOST ?? '').trim() || DEFAULT_HTTP_HOST;
101
+ const port = resolvePort(env.MCP_HTTP_PORT);
102
+ const rawPublic = (env.MCP_PUBLIC_URL ?? '').trim() || `http://${hostPort(host, port)}`;
103
+ const publicUrl = canonicalizePublicUrl(rawPublic);
104
+ return {
105
+ transport,
106
+ host,
107
+ port,
108
+ publicUrl,
109
+ resourceUri: `${publicUrl}/mcp`,
110
+ allowedHosts: deriveAllowedHosts(publicUrl, host, port),
111
+ allowedOrigins: deriveAllowedOrigins(publicUrl, env.MCP_ALLOWED_ORIGINS),
112
+ };
113
+ }
@@ -0,0 +1,59 @@
1
+ import { type IncomingMessage, type ServerResponse } from 'node:http';
2
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import type { HttpConfig } from './http-config.js';
4
+ export type AuthOutcome = {
5
+ ok: true;
6
+ } | {
7
+ ok: false;
8
+ status: number;
9
+ body: string;
10
+ headers?: Record<string, string>;
11
+ };
12
+ /** Bearer / owner check for POST /mcp. B12 default = loopback-owner; B13 swaps in JWT verify. */
13
+ export type Authenticator = (req: IncomingMessage) => AuthOutcome | Promise<AuthOutcome>;
14
+ /** A mounted extra route (the AS endpoints, B13). Return true if it wrote a response. */
15
+ export type RouteHandler = (req: IncomingMessage, res: ServerResponse, url: URL) => boolean | Promise<boolean>;
16
+ export interface HttpHostOptions {
17
+ /** The ONE McpServer + registry built at boot (P1 / BV gap #4: never per request). */
18
+ server: McpServer;
19
+ config: HttpConfig;
20
+ version: string;
21
+ ownerConfigured: boolean;
22
+ authenticate: Authenticator;
23
+ /** Extra routes keyed by exact pathname (AS endpoints mount here in B13). */
24
+ routes?: Record<string, RouteHandler>;
25
+ log?: (line: string) => void;
26
+ /** Max /mcp JSON body bytes (DoS guard). */
27
+ maxBodyBytes?: number;
28
+ /** Deadline for a single /mcp dispatch; a hung handler past this releases the
29
+ * shared lock instead of wedging the transport (default 120s). */
30
+ dispatchTimeoutMs?: number;
31
+ }
32
+ export declare function parseOwnerEmails(env?: NodeJS.ProcessEnv): string[];
33
+ /** Front guard: an Origin, if present, must be allowlisted; a Host must be
34
+ * allowlisted. Absent Origin passes (native/CLI/backend clients — the claude.ai
35
+ * connector calls /mcp server-to-server with no Origin, BV-4). */
36
+ export declare function originAllowed(origin: string | undefined, allowed: string[]): boolean;
37
+ export declare function hostAllowed(host: string | undefined, allowed: string[]): boolean;
38
+ /** JSON-RPC method name(s) for an observability log line — no params, no PII. */
39
+ export declare function jsonRpcMethod(body: unknown): string;
40
+ export declare class HttpTransportHost {
41
+ private readonly opts;
42
+ private httpServer?;
43
+ private lock;
44
+ constructor(opts: HttpHostOptions);
45
+ private serialize;
46
+ private log;
47
+ start(): Promise<void>;
48
+ close(): Promise<void>;
49
+ /** The bound port (useful when listening on port 0 in tests). */
50
+ address(): {
51
+ port: number;
52
+ } | undefined;
53
+ private handle;
54
+ private frontGuard;
55
+ private mcp;
56
+ private health;
57
+ private readJson;
58
+ private fail;
59
+ }