@alfe.ai/notion-mcp 0.3.16 → 0.3.18

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.
@@ -0,0 +1,804 @@
1
+ #!/usr/bin/env node
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ let node_fs = require("node:fs");
4
+ let node_module = require("node:module");
5
+ let node_path = require("node:path");
6
+ let node_url = require("node:url");
7
+ let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
8
+ let _alfe_ai_config = require("@alfe.ai/config");
9
+ let _alfe_ai_mcp_bundler = require("@alfe.ai/mcp-bundler");
10
+ let _modelcontextprotocol_sdk_client_index_js = require("@modelcontextprotocol/sdk/client/index.js");
11
+ let _modelcontextprotocol_sdk_client_stdio_js = require("@modelcontextprotocol/sdk/client/stdio.js");
12
+ let _modelcontextprotocol_sdk_server_index_js = require("@modelcontextprotocol/sdk/server/index.js");
13
+ let _modelcontextprotocol_sdk_server_stdio_js = require("@modelcontextprotocol/sdk/server/stdio.js");
14
+ let _modelcontextprotocol_sdk_types_js = require("@modelcontextprotocol/sdk/types.js");
15
+ let node_crypto = require("node:crypto");
16
+ const MAX_FORWARDED_ARGUMENT_BYTES = 2 * 1024 * 1024;
17
+ const MAX_TOOL_RESULT_BYTES = 5 * 1024 * 1024;
18
+ const MAX_SCHEMA_BYTES = 768 * 1024;
19
+ const MAX_CHECK_RESPONSE_BYTES = 64 * 1024;
20
+ const MAX_JSON_DEPTH = 16;
21
+ const MAX_JSON_NODES = 1e5;
22
+ const MAX_JSON_ARRAY_ITEMS = 5e3;
23
+ const MAX_ARGUMENT_STRING_CHARS = 15e5;
24
+ const MAX_TOKEN_CHARS = 32768;
25
+ const MAX_ERROR_CHARS = 2048;
26
+ const NOTION_CHECK_TIMEOUT_MS = 15e3;
27
+ const WORKSPACE_ID_PATTERN = /^[A-Za-z0-9_-]{1,256}$/u;
28
+ const TOOL_NAME_PATTERN = /^[A-Za-z0-9_.:-]{1,128}$/u;
29
+ const UNSAFE_KEYS = new Set([
30
+ "__proto__",
31
+ "constructor",
32
+ "prototype"
33
+ ]);
34
+ const RESERVED_TOOLS = new Set(["notion_list_accounts", "notion_check_connection"]);
35
+ const CHILD_ENV_ALLOWLIST = [
36
+ "HOME",
37
+ "TMPDIR",
38
+ "TMP",
39
+ "TEMP",
40
+ "LANG",
41
+ "LC_ALL",
42
+ "LC_CTYPE",
43
+ "TZ",
44
+ "SSL_CERT_FILE",
45
+ "SSL_CERT_DIR",
46
+ "NODE_EXTRA_CA_CERTS",
47
+ "HTTP_PROXY",
48
+ "HTTPS_PROXY",
49
+ "NO_PROXY",
50
+ "http_proxy",
51
+ "https_proxy",
52
+ "no_proxy"
53
+ ];
54
+ function buildChildEnvironment(accessToken, source = process.env) {
55
+ const token = validateAccessToken(accessToken, false);
56
+ const environment = Object.create(null);
57
+ for (const name of CHILD_ENV_ALLOWLIST) {
58
+ const value = source[name];
59
+ if (value !== void 0 && !containsControlCharacter(value)) environment[name] = value;
60
+ }
61
+ return {
62
+ ...environment,
63
+ NOTION_TOKEN: token
64
+ };
65
+ }
66
+ function normalizeNotionAccount(value) {
67
+ if (!isRecord(value)) throw new Error("Notion account must be an object");
68
+ return {
69
+ connectionId: validateText("connectionId", value.connectionId, 128),
70
+ workspaceId: validateWorkspaceId(value.workspaceId),
71
+ workspaceName: validateDisplayText("workspace name", value.workspaceName, 512),
72
+ connectedAt: validateConnectedAt(value.connectedAt),
73
+ accessToken: validateAccessToken(value.accessToken, true)
74
+ };
75
+ }
76
+ function assertAccountCount(value) {
77
+ if (!Array.isArray(value)) throw new Error("Notion accounts response must be an array");
78
+ if (value.length > 50) throw new Error(`Notion accounts response exceeds ${String(50)} accounts`);
79
+ }
80
+ function readNotionAccountsResponse(value) {
81
+ if (!isRecord(value)) throw new Error("Notion accounts response must be an object");
82
+ assertAccountCount(value.accounts);
83
+ return value.accounts;
84
+ }
85
+ function injectWorkspaceSelector(tool) {
86
+ if (!isRecord(tool)) throw new Error("Child tool descriptor must be an object");
87
+ const name = validateToolName(tool.name);
88
+ if (RESERVED_TOOLS.has(name)) throw new Error(`Child tool collides with reserved proxy tool: ${name}`);
89
+ const original = cloneJsonRecord("tool input schema", tool.inputSchema, MAX_SCHEMA_BYTES, 5e5);
90
+ const originalProperties = Object.hasOwn(original, "properties") ? cloneJsonRecord("tool schema properties", original.properties, MAX_SCHEMA_BYTES, 5e5) : Object.create(null);
91
+ const originalRequired = validateRequired(original.required);
92
+ const properties = {
93
+ ...originalProperties,
94
+ workspaceId: {
95
+ type: "string",
96
+ pattern: WORKSPACE_ID_PATTERN.source,
97
+ maxLength: 256,
98
+ description: "Notion workspaceId from notion_list_accounts. This required selector chooses the exact connected OAuth workspace."
99
+ }
100
+ };
101
+ const required = new Set(["workspaceId", ...originalRequired]);
102
+ if (name === "API-delete-a-block") {
103
+ properties.confirmBlockId = {
104
+ type: "string",
105
+ maxLength: 256,
106
+ description: "Repeat the exact block_id to confirm deletion of this Notion block."
107
+ };
108
+ required.add("confirmBlockId");
109
+ }
110
+ if (name === "API-update-a-block") {
111
+ const archived = properties.archived;
112
+ if (isRecord(archived)) properties.archived = {
113
+ ...archived,
114
+ default: false,
115
+ description: "Set true only to archive/delete this block; confirmBlockId must then exactly match block_id."
116
+ };
117
+ properties.confirmBlockId = {
118
+ type: "string",
119
+ maxLength: 256,
120
+ description: "Required only when archived=true; repeat the exact block_id to confirm deletion."
121
+ };
122
+ }
123
+ if (name === "API-patch-page") properties.confirmPageId = {
124
+ type: "string",
125
+ maxLength: 256,
126
+ description: "Required only when in_trash=true or archived=true; repeat the exact page_id to confirm deletion."
127
+ };
128
+ const result = {
129
+ name,
130
+ inputSchema: {
131
+ ...original,
132
+ type: "object",
133
+ properties,
134
+ required: [...required]
135
+ }
136
+ };
137
+ if (tool.title !== void 0) result.title = validateText("tool title", tool.title, 256);
138
+ if (tool.description !== void 0) result.description = validateDocumentationText("tool description", tool.description, 2e4);
139
+ if (tool.outputSchema !== void 0) result.outputSchema = cloneJsonRecord("tool output schema", tool.outputSchema, MAX_SCHEMA_BYTES, 5e5);
140
+ if (tool.annotations !== void 0) result.annotations = validateAnnotations(tool.annotations);
141
+ if (tool.execution !== void 0) result.execution = validateExecution(tool.execution);
142
+ return result;
143
+ }
144
+ function validateToolCatalog(tools) {
145
+ if (!Array.isArray(tools)) throw new Error("Child tool catalog must be an array");
146
+ if (tools.length > 100) throw new Error(`Child tool catalog exceeds ${String(100)} tools`);
147
+ const names = /* @__PURE__ */ new Set();
148
+ return tools.map((tool) => {
149
+ const proxied = injectWorkspaceSelector(tool);
150
+ if (names.has(proxied.name)) throw new Error(`Child tool catalog contains duplicate name: ${proxied.name}`);
151
+ names.add(proxied.name);
152
+ return proxied;
153
+ });
154
+ }
155
+ function prepareForwardedArguments(toolName, value) {
156
+ const cloned = cloneJsonRecord("tool arguments", value, MAX_FORWARDED_ARGUMENT_BYTES, MAX_ARGUMENT_STRING_CHARS);
157
+ const workspaceId = validateWorkspaceId(cloned.workspaceId);
158
+ if (toolName === "API-delete-a-block") requireConfirmation(cloned, "block_id", "confirmBlockId");
159
+ if (toolName === "API-update-a-block" && cloned.archived === true) requireConfirmation(cloned, "block_id", "confirmBlockId");
160
+ if (toolName === "API-patch-page" && (cloned.in_trash === true || cloned.archived === true)) requireConfirmation(cloned, "page_id", "confirmPageId");
161
+ const proxyFields = new Set([
162
+ "workspaceId",
163
+ "confirmBlockId",
164
+ "confirmPageId"
165
+ ]);
166
+ return {
167
+ workspaceId,
168
+ forwarded: Object.fromEntries(Object.entries(cloned).filter(([key]) => !proxyFields.has(key)))
169
+ };
170
+ }
171
+ function assertBoundedToolArguments(value) {
172
+ return cloneJsonRecord("tool arguments", value, MAX_FORWARDED_ARGUMENT_BYTES, MAX_ARGUMENT_STRING_CHARS);
173
+ }
174
+ function assertBoundedToolResult(value) {
175
+ return cloneJsonRecord("child tool result", value, MAX_TOOL_RESULT_BYTES, MAX_TOOL_RESULT_BYTES);
176
+ }
177
+ function childResultIsError(value) {
178
+ if (value.isError === true) return true;
179
+ if (isRecord(value.structuredContent) && value.structuredContent.status === "error") return true;
180
+ const content = value.content;
181
+ if (!Array.isArray(content)) return false;
182
+ return content.some((entry) => {
183
+ if (!isRecord(entry) || typeof entry.text !== "string") return false;
184
+ try {
185
+ const parsed = JSON.parse(entry.text);
186
+ return isRecord(parsed) && parsed.status === "error";
187
+ } catch {
188
+ return false;
189
+ }
190
+ });
191
+ }
192
+ function extractToolErrorText(value) {
193
+ if (!Array.isArray(value.content)) return "";
194
+ return value.content.slice(0, 100).map((entry) => isRecord(entry) && typeof entry.text === "string" ? entry.text.slice(0, MAX_ERROR_CHARS) : "").join("\n").slice(0, MAX_ERROR_CHARS);
195
+ }
196
+ async function checkNotionConnection(accessToken, fetchFn = fetch) {
197
+ const response = await fetchFn("https://api.notion.com/v1/users/me", {
198
+ headers: {
199
+ Authorization: `Bearer ${validateAccessToken(accessToken, false)}`,
200
+ "Notion-Version": "2025-09-03"
201
+ },
202
+ redirect: "error",
203
+ signal: AbortSignal.timeout(NOTION_CHECK_TIMEOUT_MS)
204
+ });
205
+ if (!response.ok) {
206
+ await response.body?.cancel().catch(() => void 0);
207
+ return {
208
+ ok: false,
209
+ status: response.status
210
+ };
211
+ }
212
+ const text = await readResponseText(response, MAX_CHECK_RESPONSE_BYTES);
213
+ let parsed;
214
+ try {
215
+ parsed = JSON.parse(text);
216
+ } catch {
217
+ throw new Error("Notion /users/me returned invalid JSON");
218
+ }
219
+ if (!isRecord(parsed) || parsed.object !== "user") throw new Error("Notion /users/me returned an invalid user object");
220
+ validateText("Notion user id", parsed.id, 128);
221
+ if (parsed.type !== "bot" && parsed.type !== "person") throw new Error("Notion /users/me returned an invalid user type");
222
+ return {
223
+ ok: true,
224
+ status: response.status
225
+ };
226
+ }
227
+ function snapshotToolCatalog(tools) {
228
+ return tools.map((tool) => structuredClone(tool));
229
+ }
230
+ function safeErrorMessage(error, secrets = []) {
231
+ let message = (error instanceof Error ? error.message : String(error)).slice(0, MAX_ERROR_CHARS);
232
+ for (const secret of secrets) if (secret.length >= 4) message = message.split(secret).join("[REDACTED]");
233
+ return flattenControls(message).replace(/\b(Bearer|Basic)\s+[^\s,;]+/giu, "$1 [REDACTED]").replace(/\b(api[_-]?key|access[_-]?token|refresh[_-]?token|secret|password)\s*[=:]\s*[^\s,;]+/giu, "$1=[REDACTED]").replace(/https?:\/\/[^\s/@:]+:[^\s/@]+@/giu, "https://[REDACTED]@").replace(/\balfe_[A-Za-z0-9_-]{8,}/gu, "[REDACTED]").slice(0, MAX_ERROR_CHARS);
234
+ }
235
+ function connectionLogId(value) {
236
+ return (0, node_crypto.createHash)("sha256").update(value).digest("hex").slice(0, 12);
237
+ }
238
+ async function withDeadline(promise, timeoutMs, label) {
239
+ let timer;
240
+ try {
241
+ return await Promise.race([promise, new Promise((_resolve, reject) => {
242
+ timer = setTimeout(() => {
243
+ reject(/* @__PURE__ */ new Error(`${label} timed out`));
244
+ }, timeoutMs);
245
+ timer.unref();
246
+ })]);
247
+ } finally {
248
+ if (timer !== void 0) clearTimeout(timer);
249
+ }
250
+ }
251
+ function requireConfirmation(value, targetField, confirmationField) {
252
+ const target = value[targetField];
253
+ const confirmation = value[confirmationField];
254
+ if (typeof target !== "string" || target.length < 1) throw new Error(`${targetField} must be a non-empty string`);
255
+ if (confirmation !== target) throw new Error(`${confirmationField} must exactly match ${targetField}`);
256
+ }
257
+ function validateWorkspaceId(value) {
258
+ if (typeof value !== "string" || !WORKSPACE_ID_PATTERN.test(value)) throw new Error("workspaceId must contain 1 to 256 letters, digits, underscores, or hyphens");
259
+ return value;
260
+ }
261
+ function validateAccessToken(value, allowEmpty) {
262
+ if (allowEmpty && value === "") return "";
263
+ if (typeof value !== "string" || value.length < 8 || value.length > MAX_TOKEN_CHARS || containsControlCharacter(value) || value !== value.trim()) throw new Error("Notion access token is invalid");
264
+ return value;
265
+ }
266
+ function validateConnectedAt(value) {
267
+ const checked = validateText("connectedAt", value, 64);
268
+ const parsed = new Date(checked);
269
+ if (Number.isNaN(parsed.valueOf()) || parsed.toISOString() !== checked) throw new Error("connectedAt must be a canonical ISO date-time");
270
+ return checked;
271
+ }
272
+ function validateToolName(value) {
273
+ if (typeof value !== "string" || !TOOL_NAME_PATTERN.test(value)) throw new Error("Child tool name is invalid");
274
+ return value;
275
+ }
276
+ function validateText(label, value, maxChars) {
277
+ if (typeof value !== "string" || value.length < 1 || value.length > maxChars || containsControlCharacter(value)) throw new Error(`${label} must contain 1 to ${String(maxChars)} non-control characters`);
278
+ return value;
279
+ }
280
+ function validateDisplayText(label, value, maxChars) {
281
+ if (typeof value !== "string" || value.length > maxChars || containsControlCharacter(value)) throw new Error(`${label} must contain at most ${String(maxChars)} non-control characters`);
282
+ return value;
283
+ }
284
+ function validateDocumentationText(label, value, maxChars) {
285
+ if (typeof value !== "string" || value.length < 1 || value.length > maxChars || Array.from(value).some((character) => {
286
+ const codePoint = character.codePointAt(0) ?? 0;
287
+ return codePoint < 32 && character !== "\n" && character !== "\r" && character !== " " || codePoint === 127;
288
+ })) throw new Error(`${label} must contain 1 to ${String(maxChars)} safe characters`);
289
+ return value;
290
+ }
291
+ function validateRequired(value) {
292
+ if (value === void 0) return [];
293
+ if (!Array.isArray(value) || value.length > 100) throw new Error("tool schema required must be an array");
294
+ const result = [];
295
+ for (const entry of value) {
296
+ if (typeof entry !== "string" || entry.length < 1 || entry.length > 128) throw new Error("tool schema required contains an invalid property name");
297
+ if (!result.includes(entry)) result.push(entry);
298
+ }
299
+ return result;
300
+ }
301
+ function validateAnnotations(value) {
302
+ if (!isRecord(value)) throw new Error("tool annotations must be an object");
303
+ const result = {};
304
+ if (value.title !== void 0) result.title = validateText("annotation title", value.title, 256);
305
+ for (const key of [
306
+ "readOnlyHint",
307
+ "destructiveHint",
308
+ "idempotentHint",
309
+ "openWorldHint"
310
+ ]) {
311
+ const hint = value[key];
312
+ if (hint !== void 0) {
313
+ if (typeof hint !== "boolean") throw new Error(`tool annotation ${key} must be boolean`);
314
+ result[key] = hint;
315
+ }
316
+ }
317
+ return result;
318
+ }
319
+ function validateExecution(value) {
320
+ if (!isRecord(value)) throw new Error("tool execution metadata must be an object");
321
+ const taskSupport = value.taskSupport;
322
+ if (taskSupport !== void 0 && taskSupport !== "optional" && taskSupport !== "required" && taskSupport !== "forbidden") throw new Error("tool execution taskSupport is invalid");
323
+ return taskSupport === void 0 ? {} : { taskSupport };
324
+ }
325
+ async function readResponseText(response, maxBytes) {
326
+ const declared = response.headers.get("content-length");
327
+ if (declared !== null && (!/^\d+$/u.test(declared) || Number(declared) > maxBytes)) {
328
+ await response.body?.cancel().catch(() => void 0);
329
+ throw new Error("Notion /users/me response exceeds the byte limit");
330
+ }
331
+ if (response.body === null) return "";
332
+ const reader = response.body.getReader();
333
+ const decoder = new TextDecoder();
334
+ let bytes = 0;
335
+ let text = "";
336
+ try {
337
+ for (;;) {
338
+ const result = await reader.read();
339
+ if (result.done) break;
340
+ if (result.value === void 0) throw new Error("Notion response stream returned no bytes");
341
+ bytes += result.value.byteLength;
342
+ if (bytes > maxBytes) {
343
+ await reader.cancel("response too large").catch(() => void 0);
344
+ throw new Error("Notion /users/me response exceeds the byte limit");
345
+ }
346
+ text += decoder.decode(result.value, { stream: true });
347
+ }
348
+ text += decoder.decode();
349
+ return text;
350
+ } finally {
351
+ reader.releaseLock();
352
+ }
353
+ }
354
+ function cloneJsonRecord(label, value, maxBytes, maxStringChars) {
355
+ if (!isRecord(value)) throw new Error(`${label} must be an object`);
356
+ const cloned = cloneRecord(label, value, 0, { nodes: 0 }, maxStringChars);
357
+ if (Buffer.byteLength(JSON.stringify(cloned), "utf8") > maxBytes) throw new Error(`${label} exceeds the ${String(maxBytes)} byte limit`);
358
+ return cloned;
359
+ }
360
+ function cloneJson(label, value, depth, budget, maxStringChars) {
361
+ budget.nodes += 1;
362
+ if (budget.nodes > MAX_JSON_NODES) throw new Error(`${label} contains too many values`);
363
+ if (depth > MAX_JSON_DEPTH) throw new Error(`${label} exceeds the JSON depth limit`);
364
+ if (value === null || typeof value === "boolean") return value;
365
+ if (typeof value === "number") {
366
+ if (!Number.isFinite(value)) throw new Error(`${label} contains a non-finite number`);
367
+ return value;
368
+ }
369
+ if (typeof value === "string") {
370
+ if (value.length > maxStringChars) throw new Error(`${label} contains an oversized string`);
371
+ return value;
372
+ }
373
+ if (Array.isArray(value)) {
374
+ if (value.length > MAX_JSON_ARRAY_ITEMS) throw new Error(`${label} contains an oversized array`);
375
+ return value.map((entry) => cloneJson(label, entry, depth + 1, budget, maxStringChars));
376
+ }
377
+ if (!isRecord(value)) throw new Error(`${label} contains a non-JSON value`);
378
+ return cloneRecord(label, value, depth, budget, maxStringChars);
379
+ }
380
+ function cloneRecord(label, value, depth, budget, maxStringChars) {
381
+ const result = Object.create(null);
382
+ for (const [key, nested] of Object.entries(value)) {
383
+ if (key.length < 1 || key.length > 256 || UNSAFE_KEYS.has(key)) throw new Error(`${label} contains an unsafe property name`);
384
+ result[key] = cloneJson(label, nested, depth + 1, budget, maxStringChars);
385
+ }
386
+ return result;
387
+ }
388
+ function containsControlCharacter(value) {
389
+ return Array.from(value).some((character) => {
390
+ const codePoint = character.codePointAt(0) ?? 0;
391
+ return codePoint < 32 || codePoint === 127;
392
+ });
393
+ }
394
+ function flattenControls(value) {
395
+ let output = "";
396
+ let previousWasControl = false;
397
+ for (const character of value) {
398
+ const codePoint = character.codePointAt(0) ?? 0;
399
+ const isControl = codePoint < 32 || codePoint === 127;
400
+ if (isControl) {
401
+ if (!previousWasControl) output += " ";
402
+ } else output += character;
403
+ previousWasControl = isControl;
404
+ }
405
+ return output;
406
+ }
407
+ function isRecord(value) {
408
+ return value !== null && typeof value === "object" && !Array.isArray(value);
409
+ }
410
+ //#endregion
411
+ //#region src/server.ts
412
+ /**
413
+ * Notion MCP proxy (Pattern A multi-account).
414
+ *
415
+ * OpenClaw communicates with this process over stdio. The proxy owns one
416
+ * exact-version official Notion child per connected workspace and injects a
417
+ * required workspaceId selector into every credential-touching child tool.
418
+ */
419
+ const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
420
+ const CHILD_PACKAGE = "@notionhq/notion-mcp-server";
421
+ const packageMetadata = require$1("../package.json");
422
+ const childPackagePath = require$1.resolve(`${CHILD_PACKAGE}/package.json`);
423
+ const childMetadata = require$1(childPackagePath);
424
+ const SERVER_VERSION = validateSemver("notion-mcp package version", packageMetadata.version);
425
+ const CHILD_VERSION = validateExactChildVersion(packageMetadata.dependencies?.[CHILD_PACKAGE], childMetadata.version);
426
+ const CHILD_BIN_PATH = resolveChildBin(childPackagePath, childMetadata.bin);
427
+ const CHILD_START_TIMEOUT_MS = 3e4;
428
+ const CHILD_CATALOG_TIMEOUT_MS = 15e3;
429
+ const CHILD_CALL_TIMEOUT_MS = 6e4;
430
+ const CHILD_CLOSE_TIMEOUT_MS = 5e3;
431
+ var NotionRuntime = class {
432
+ workspaces = /* @__PURE__ */ new Map();
433
+ claimedWorkspaceIds = /* @__PURE__ */ new Map();
434
+ allAccountsSnapshot = [];
435
+ catalogToolNames = /* @__PURE__ */ new Set();
436
+ cachedTools = [];
437
+ closePromise;
438
+ spawnChild;
439
+ fetchImpl;
440
+ constructor(apiClient, options = {}) {
441
+ this.apiClient = apiClient;
442
+ this.spawnChild = options.spawnChild ?? spawnOfficialChild;
443
+ this.fetchImpl = options.fetchImpl ?? fetch;
444
+ }
445
+ async initialize() {
446
+ await this.loadAccounts();
447
+ this.cachedTools = appendLocalTools(await this.discoverChildCatalog());
448
+ (0, _alfe_ai_mcp_bundler.assertPatternA)(this.cachedTools.map((tool) => ({
449
+ name: tool.name,
450
+ parameters: tool.inputSchema
451
+ })), {
452
+ selector: "workspaceId",
453
+ exempt: ["notion_list_accounts"]
454
+ });
455
+ }
456
+ listTools() {
457
+ return snapshotToolCatalog(this.cachedTools);
458
+ }
459
+ async handleToolCall(request) {
460
+ const { name } = request.params;
461
+ const args = request.params.arguments ?? {};
462
+ if (name === "notion_list_accounts") try {
463
+ const bounded = assertBoundedToolArguments(args);
464
+ if (Object.keys(bounded).length !== 0) throw new Error("notion_list_accounts takes no arguments");
465
+ return jsonResult({ workspaces: this.allAccountsSnapshot });
466
+ } catch (error) {
467
+ return errorResult(error);
468
+ }
469
+ if (name === "notion_check_connection") {
470
+ let workspace;
471
+ try {
472
+ const prepared = prepareForwardedArguments(name, args);
473
+ if (Object.keys(prepared.forwarded).length !== 0) throw new Error("notion_check_connection accepts only workspaceId");
474
+ workspace = this.resolveWorkspace(prepared.workspaceId);
475
+ const result = await checkNotionConnection(workspace.accessToken, this.fetchImpl);
476
+ if (!result.ok) return errorResult("Token may have been revoked. Ask the user to reconnect this Notion workspace from the dashboard.", {
477
+ workspaceId: workspace.workspaceId,
478
+ connected: false,
479
+ status: result.status
480
+ });
481
+ return jsonResult({
482
+ workspaceId: workspace.workspaceId,
483
+ connected: true,
484
+ status: result.status
485
+ });
486
+ } catch (error) {
487
+ return errorResult(error, {}, workspace === void 0 ? [] : [workspace.accessToken]);
488
+ }
489
+ }
490
+ if (!this.catalogToolNames.has(name)) return errorResult(`Unknown Notion tool: ${name}`);
491
+ let workspace;
492
+ let forwarded;
493
+ try {
494
+ const prepared = prepareForwardedArguments(name, args);
495
+ workspace = this.resolveWorkspace(prepared.workspaceId);
496
+ forwarded = prepared.forwarded;
497
+ } catch (error) {
498
+ return errorResult(error);
499
+ }
500
+ try {
501
+ const result = assertBoundedToolResult(await withDeadline(workspace.client.callTool({
502
+ name,
503
+ arguments: forwarded
504
+ }), CHILD_CALL_TIMEOUT_MS, `Notion ${name}`));
505
+ if (childResultIsError(result)) return errorResult(extractToolErrorText(result) || "Notion child tool returned an error", {
506
+ workspaceId: workspace.workspaceId,
507
+ tool: name
508
+ }, [workspace.accessToken]);
509
+ return result;
510
+ } catch (error) {
511
+ return errorResult(error, {
512
+ workspaceId: workspace.workspaceId,
513
+ tool: name
514
+ }, [workspace.accessToken]);
515
+ }
516
+ }
517
+ async close() {
518
+ if (this.closePromise !== void 0) return this.closePromise;
519
+ this.closePromise = (async () => {
520
+ const current = [...this.workspaces.values()];
521
+ this.workspaces.clear();
522
+ await Promise.all(current.map(async ({ client }) => closeChild(client)));
523
+ })();
524
+ return this.closePromise;
525
+ }
526
+ async loadAccounts() {
527
+ const rawAccounts = readNotionAccountsResponse(await this.apiClient.getNotionAccounts());
528
+ if (rawAccounts.length === 0) log("No Notion workspaces connected; starting in discovery-only mode");
529
+ for (const rawAccount of rawAccounts) {
530
+ let account;
531
+ try {
532
+ account = normalizeNotionAccount(rawAccount);
533
+ } catch (error) {
534
+ log(`Rejected invalid Notion connection ${typeof rawAccount.connectionId === "string" ? connectionLogId(rawAccount.connectionId) : "invalid"}: ${safeErrorMessage(error)}`);
535
+ this.allAccountsSnapshot.push({
536
+ workspaceId: null,
537
+ workspaceName: null,
538
+ connectedAt: null,
539
+ connected: false,
540
+ reason: "invalid_account"
541
+ });
542
+ continue;
543
+ }
544
+ if (this.claimedWorkspaceIds.has(account.workspaceId)) {
545
+ log(`Rejected duplicate Notion workspace from connection ${connectionLogId(account.connectionId)}`);
546
+ this.allAccountsSnapshot.push({
547
+ workspaceId: account.workspaceId,
548
+ workspaceName: account.workspaceName,
549
+ connectedAt: account.connectedAt,
550
+ connected: false,
551
+ reason: "duplicate_workspace"
552
+ });
553
+ continue;
554
+ }
555
+ this.claimedWorkspaceIds.set(account.workspaceId, account.connectionId);
556
+ if (account.accessToken.length === 0) {
557
+ log(`Missing token for Notion connection ${connectionLogId(account.connectionId)}`);
558
+ this.allAccountsSnapshot.push({
559
+ workspaceId: account.workspaceId,
560
+ workspaceName: account.workspaceName,
561
+ connectedAt: account.connectedAt,
562
+ connected: false,
563
+ reason: "missing_access_token"
564
+ });
565
+ continue;
566
+ }
567
+ try {
568
+ const controller = new AbortController();
569
+ let client;
570
+ try {
571
+ client = await withDeadline(this.spawnChild(account.accessToken, controller.signal), CHILD_START_TIMEOUT_MS, "Notion child startup");
572
+ } catch (error) {
573
+ controller.abort(error);
574
+ throw error;
575
+ }
576
+ this.workspaces.set(account.workspaceId, {
577
+ ...account,
578
+ client
579
+ });
580
+ this.allAccountsSnapshot.push({
581
+ workspaceId: account.workspaceId,
582
+ workspaceName: account.workspaceName,
583
+ connectedAt: account.connectedAt,
584
+ connected: true
585
+ });
586
+ log(`Spawned child for Notion connection ${connectionLogId(account.connectionId)}`);
587
+ } catch (error) {
588
+ log(`Child startup failed for Notion connection ${connectionLogId(account.connectionId)}: ${safeErrorMessage(error, [account.accessToken])}`);
589
+ this.allAccountsSnapshot.push({
590
+ workspaceId: account.workspaceId,
591
+ workspaceName: account.workspaceName,
592
+ connectedAt: account.connectedAt,
593
+ connected: false,
594
+ reason: "spawn_failed"
595
+ });
596
+ }
597
+ }
598
+ }
599
+ async discoverChildCatalog() {
600
+ for (const workspace of this.workspaces.values()) try {
601
+ const tools = validateToolCatalog((await withDeadline(workspace.client.listTools(), CHILD_CATALOG_TIMEOUT_MS, "Notion child tool discovery")).tools);
602
+ for (const tool of tools) this.catalogToolNames.add(tool.name);
603
+ return tools;
604
+ } catch (error) {
605
+ log(`Tool discovery failed for Notion connection ${connectionLogId(workspace.connectionId)}: ${safeErrorMessage(error, [workspace.accessToken])}`);
606
+ }
607
+ return [];
608
+ }
609
+ resolveWorkspace(workspaceId) {
610
+ const workspace = this.workspaces.get(workspaceId);
611
+ if (workspace !== void 0) return workspace;
612
+ const known = this.allAccountsSnapshot.find((summary) => summary.workspaceId === workspaceId);
613
+ if (known !== void 0) throw new Error(`workspaceId ${workspaceId} is connected on this agent but its Notion credential process is unavailable (${known.reason ?? "unknown"}). Ask the user to reconnect this workspace from the dashboard.`);
614
+ throw new Error(`Unknown workspaceId: ${workspaceId}. Call notion_list_accounts to see the connected Notion workspaces on this agent.`);
615
+ }
616
+ };
617
+ function appendLocalTools(tools) {
618
+ return [
619
+ ...tools,
620
+ {
621
+ name: "notion_list_accounts",
622
+ description: "List every connected Notion workspace and its local proxy readiness. Use workspaceId as the explicit selector on every other Notion tool.",
623
+ inputSchema: {
624
+ type: "object",
625
+ properties: {},
626
+ additionalProperties: false
627
+ },
628
+ annotations: {
629
+ readOnlyHint: true,
630
+ destructiveHint: false,
631
+ idempotentHint: true,
632
+ openWorldHint: false
633
+ }
634
+ },
635
+ {
636
+ name: "notion_check_connection",
637
+ description: "Verify that one selected Notion OAuth token is still valid. The result exposes only status, never the credential or Notion user profile.",
638
+ inputSchema: {
639
+ type: "object",
640
+ properties: { workspaceId: {
641
+ type: "string",
642
+ pattern: "^[A-Za-z0-9_-]{1,256}$",
643
+ maxLength: 256,
644
+ description: "Notion workspaceId from notion_list_accounts."
645
+ } },
646
+ required: ["workspaceId"],
647
+ additionalProperties: false
648
+ },
649
+ annotations: {
650
+ readOnlyHint: true,
651
+ destructiveHint: false,
652
+ idempotentHint: true,
653
+ openWorldHint: true
654
+ }
655
+ }
656
+ ];
657
+ }
658
+ async function spawnOfficialChild(accessToken, signal) {
659
+ const transport = new _modelcontextprotocol_sdk_client_stdio_js.StdioClientTransport({
660
+ command: process.execPath,
661
+ args: [CHILD_BIN_PATH],
662
+ env: buildChildEnvironment(accessToken),
663
+ stderr: "ignore"
664
+ });
665
+ const client = new _modelcontextprotocol_sdk_client_index_js.Client({
666
+ name: "notion-mcp-proxy",
667
+ version: SERVER_VERSION
668
+ });
669
+ const abort = () => {
670
+ closeChild(client);
671
+ };
672
+ signal.addEventListener("abort", abort, { once: true });
673
+ try {
674
+ await client.connect(transport);
675
+ if (signal.aborted) throw new Error("Notion child startup was aborted");
676
+ return client;
677
+ } catch (error) {
678
+ await closeChild(client);
679
+ throw error;
680
+ } finally {
681
+ signal.removeEventListener("abort", abort);
682
+ }
683
+ }
684
+ async function closeChild(client) {
685
+ await withDeadline(client.close(), CHILD_CLOSE_TIMEOUT_MS, "Notion child close").catch(() => void 0);
686
+ }
687
+ async function createProxyServer(apiClient, options = {}) {
688
+ const runtime = new NotionRuntime(apiClient, options);
689
+ try {
690
+ await runtime.initialize();
691
+ } catch (error) {
692
+ await runtime.close();
693
+ throw error;
694
+ }
695
+ const server = new _modelcontextprotocol_sdk_server_index_js.Server({
696
+ name: "notion-mcp-proxy",
697
+ version: SERVER_VERSION
698
+ }, { capabilities: { tools: {} } });
699
+ server.setRequestHandler(_modelcontextprotocol_sdk_types_js.ListToolsRequestSchema, () => ({ tools: runtime.listTools() }));
700
+ server.setRequestHandler(_modelcontextprotocol_sdk_types_js.CallToolRequestSchema, (request) => runtime.handleToolCall(request));
701
+ return {
702
+ server,
703
+ runtime
704
+ };
705
+ }
706
+ async function startServer(apiClient, options = {}) {
707
+ let client = apiClient;
708
+ if (client === void 0) {
709
+ const config = (0, _alfe_ai_config.resolveConfig)();
710
+ client = new _alfe_ai_agent_api_client.AgentApiClient({
711
+ apiKey: config.apiKey,
712
+ apiUrl: config.apiUrl
713
+ });
714
+ }
715
+ const { server, runtime } = await createProxyServer(client, options);
716
+ let closePromise;
717
+ const close = () => {
718
+ closePromise ??= Promise.all([runtime.close(), withDeadline(server.close(), CHILD_CLOSE_TIMEOUT_MS, "Notion proxy close").catch(() => void 0)]).then(() => void 0);
719
+ return closePromise;
720
+ };
721
+ try {
722
+ await server.connect(new _modelcontextprotocol_sdk_server_stdio_js.StdioServerTransport());
723
+ log(`Proxy running with ${String(runtime.listTools().length - 2)} child tool(s)`);
724
+ return {
725
+ server,
726
+ runtime,
727
+ close
728
+ };
729
+ } catch (error) {
730
+ await close();
731
+ throw error;
732
+ }
733
+ }
734
+ function isProcessEntrypoint(argvPath, metaUrl) {
735
+ try {
736
+ if (!argvPath) return false;
737
+ return (0, node_url.pathToFileURL)((0, node_fs.realpathSync)(argvPath)).href === (0, node_url.pathToFileURL)((0, node_fs.realpathSync)((0, node_url.fileURLToPath)(metaUrl))).href;
738
+ } catch {
739
+ return false;
740
+ }
741
+ }
742
+ function resolveChildBin(packagePath, bin) {
743
+ if (bin === null || typeof bin !== "object" || Array.isArray(bin)) throw new Error(`${CHILD_PACKAGE} package bin metadata is invalid`);
744
+ const relativeBin = bin["notion-mcp-server"];
745
+ if (typeof relativeBin !== "string" || relativeBin.length === 0 || (0, node_path.isAbsolute)(relativeBin)) throw new Error(`${CHILD_PACKAGE} package bin path is invalid`);
746
+ const packageRoot = (0, node_fs.realpathSync)((0, node_path.dirname)(packagePath));
747
+ const binPath = (0, node_fs.realpathSync)((0, node_path.resolve)(packageRoot, relativeBin));
748
+ const childRelative = (0, node_path.relative)(packageRoot, binPath);
749
+ if (childRelative === "" || childRelative.startsWith("..") || (0, node_path.isAbsolute)(childRelative)) throw new Error(`${CHILD_PACKAGE} package bin resolves outside its package`);
750
+ return binPath;
751
+ }
752
+ function validateExactChildVersion(declared, installed) {
753
+ const declaredVersion = validateSemver(`${CHILD_PACKAGE} dependency`, declared);
754
+ const installedVersion = validateSemver(`${CHILD_PACKAGE} installed version`, installed);
755
+ if (declaredVersion !== installedVersion) throw new Error(`${CHILD_PACKAGE} installed version ${installedVersion} does not match exact dependency ${declaredVersion}`);
756
+ return declaredVersion;
757
+ }
758
+ function validateSemver(label, value) {
759
+ if (typeof value !== "string" || !/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u.test(value)) throw new Error(`${label} is invalid`);
760
+ return value;
761
+ }
762
+ function jsonResult(data) {
763
+ return { content: [{
764
+ type: "text",
765
+ text: JSON.stringify(data)
766
+ }] };
767
+ }
768
+ function errorResult(error, context = {}, secrets = []) {
769
+ return {
770
+ content: [{
771
+ type: "text",
772
+ text: JSON.stringify({
773
+ ...context,
774
+ error: safeErrorMessage(error, secrets)
775
+ })
776
+ }],
777
+ isError: true
778
+ };
779
+ }
780
+ function log(message) {
781
+ process.stderr.write(`[notion-mcp-proxy] ${message}\n`);
782
+ }
783
+ if (isProcessEntrypoint(process.argv[1], require("url").pathToFileURL(__filename).href)) startServer().then((running) => {
784
+ let shutdownPromise;
785
+ const shutdown = () => {
786
+ if (shutdownPromise !== void 0) return;
787
+ shutdownPromise = running.close().catch((error) => {
788
+ log(`Failed to close cleanly: ${safeErrorMessage(error)}`);
789
+ }).then(() => process.exit(0));
790
+ };
791
+ process.once("SIGTERM", shutdown);
792
+ process.once("SIGINT", shutdown);
793
+ }).catch((error) => {
794
+ log(`Fatal: ${safeErrorMessage(error)}`);
795
+ process.exitCode = 1;
796
+ });
797
+ //#endregion
798
+ exports.CHILD_BIN_PATH = CHILD_BIN_PATH;
799
+ exports.CHILD_VERSION = CHILD_VERSION;
800
+ exports.NotionRuntime = NotionRuntime;
801
+ exports.SERVER_VERSION = SERVER_VERSION;
802
+ exports.createProxyServer = createProxyServer;
803
+ exports.isProcessEntrypoint = isProcessEntrypoint;
804
+ exports.startServer = startServer;