multi-workspace-for-notion 0.1.0 → 0.2.0

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.

Potentially problematic release.


This version of multi-workspace-for-notion might be problematic. Click here for more details.

package/plugin/server.mjs CHANGED
@@ -15609,34 +15609,180 @@ var StdioServerTransport = class {
15609
15609
  };
15610
15610
 
15611
15611
  // src/config.ts
15612
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
15612
+ import { mkdir as mkdir2, readFile, rename, rm as rm2, writeFile } from "node:fs/promises";
15613
15613
  import { homedir } from "node:os";
15614
+ import { join as join2 } from "node:path";
15615
+
15616
+ // src/keychain.ts
15617
+ import { randomBytes } from "node:crypto";
15618
+ import { spawn } from "node:child_process";
15619
+ var SERVICE = "multi-workspace-for-notion";
15620
+ var CHUNK_CHARS = 3e3;
15621
+ var keychain = {
15622
+ async get(account) {
15623
+ for (let attempt = 0; attempt < 2; attempt++) {
15624
+ const manifest = await readItem(account);
15625
+ if (manifest === void 0) return void 0;
15626
+ const { generation, count } = parseManifest(manifest);
15627
+ const chunks = [];
15628
+ for (let i = 0; i < count; i++) {
15629
+ const chunk = await readItem(chunkAccount(account, generation, i));
15630
+ if (chunk === void 0) break;
15631
+ chunks.push(chunk);
15632
+ }
15633
+ if (chunks.length === count) return Buffer.from(chunks.join(""), "base64").toString("utf8");
15634
+ }
15635
+ throw new Error(`\uD0A4\uCCB4\uC778 \uAC12\uC774 \uC77C\uBD80 \uC5C6\uC2B5\uB2C8\uB2E4: ${account}`);
15636
+ },
15637
+ async set(account, value) {
15638
+ assertSafeAccount(account);
15639
+ const previous = await readItem(account);
15640
+ const encoded = Buffer.from(value, "utf8").toString("base64");
15641
+ const generation = randomBytes(4).toString("hex");
15642
+ const count = Math.max(1, Math.ceil(encoded.length / CHUNK_CHARS));
15643
+ for (let i = 0; i < count; i++) {
15644
+ await writeItem(chunkAccount(account, generation, i), encoded.slice(i * CHUNK_CHARS, (i + 1) * CHUNK_CHARS));
15645
+ }
15646
+ await writeItem(account, `${generation}:${count}`);
15647
+ if (previous !== void 0) await deleteChunks(account, previous);
15648
+ },
15649
+ async delete(account) {
15650
+ const manifest = await readItem(account);
15651
+ if (manifest === void 0) return;
15652
+ await deleteItem(account);
15653
+ await deleteChunks(account, manifest);
15654
+ }
15655
+ };
15656
+ function parseManifest(manifest) {
15657
+ const match = /^([0-9a-f]+):(\d+)$/.exec(manifest);
15658
+ if (!match) throw new Error(`\uD0A4\uCCB4\uC778 \uAC12 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: ${manifest.slice(0, 20)}`);
15659
+ return { generation: match[1], count: Number(match[2]) };
15660
+ }
15661
+ async function deleteChunks(account, manifest) {
15662
+ const { generation, count } = parseManifest(manifest);
15663
+ for (let i = 0; i < count; i++) await deleteItem(chunkAccount(account, generation, i));
15664
+ }
15665
+ var chunkAccount = (account, generation, index) => `${account}.${generation}.${index}`;
15666
+ function assertSafeAccount(account) {
15667
+ if (!/^[A-Za-z0-9:_.-]+$/.test(account)) throw new Error(`\uD0A4\uCCB4\uC778 \uACC4\uC815 \uC774\uB984\uC5D0 \uC4F8 \uC218 \uC5C6\uB294 \uBB38\uC790: ${account}`);
15668
+ }
15669
+ async function readItem(account) {
15670
+ const { code, stdout, stderr } = await run(["find-generic-password", "-s", SERVICE, "-a", account, "-w"]);
15671
+ if (code === 44) return void 0;
15672
+ if (code !== 0) throw new Error(`\uD0A4\uCCB4\uC778 \uC77D\uAE30 \uC2E4\uD328 (${code}): ${stderr.trim()}`);
15673
+ return stdout.trim();
15674
+ }
15675
+ async function writeItem(account, value) {
15676
+ const { stdout, stderr } = await run(["-i"], `add-generic-password -U -s ${SERVICE} -a ${account} -w ${value}
15677
+ `);
15678
+ if (await readItem(account) !== value) {
15679
+ throw new Error(`\uD0A4\uCCB4\uC778 \uC4F0\uAE30 \uC2E4\uD328: ${(stderr || stdout).trim()}`);
15680
+ }
15681
+ }
15682
+ async function deleteItem(account) {
15683
+ const { code, stderr } = await run(["delete-generic-password", "-s", SERVICE, "-a", account]);
15684
+ if (code !== 0 && code !== 44) throw new Error(`\uD0A4\uCCB4\uC778 \uC0AD\uC81C \uC2E4\uD328 (${code}): ${stderr.trim()}`);
15685
+ }
15686
+ function run(args2, input) {
15687
+ return new Promise((resolve, reject) => {
15688
+ const child = spawn("/usr/bin/security", args2, { stdio: ["pipe", "pipe", "pipe"] });
15689
+ let stdout = "";
15690
+ let stderr = "";
15691
+ child.stdout.on("data", (chunk) => stdout += chunk);
15692
+ child.stderr.on("data", (chunk) => stderr += chunk);
15693
+ child.on("error", reject);
15694
+ child.on("close", (code) => resolve({ code, stdout, stderr }));
15695
+ child.stdin.end(input ?? "");
15696
+ });
15697
+ }
15698
+
15699
+ // src/lock.ts
15700
+ import { mkdir, rm, stat } from "node:fs/promises";
15614
15701
  import { join } from "node:path";
15702
+ var STALE_MS = 1e4;
15703
+ var RETRY_MS = 20;
15704
+ async function withFileLock(name, fn) {
15705
+ const dir = join(configDir(), "locks");
15706
+ const path = join(dir, `${name.replace(/[^A-Za-z0-9_.-]/g, "_")}.lock`);
15707
+ await mkdir(dir, { recursive: true, mode: 448 });
15708
+ for (; ; ) {
15709
+ try {
15710
+ await mkdir(path);
15711
+ break;
15712
+ } catch (error2) {
15713
+ if (error2.code !== "EEXIST") throw error2;
15714
+ const age = await stat(path).then((s) => Date.now() - s.mtimeMs, () => 0);
15715
+ if (age > STALE_MS) await rm(path, { recursive: true, force: true });
15716
+ else await new Promise((resolve) => setTimeout(resolve, RETRY_MS));
15717
+ }
15718
+ }
15719
+ try {
15720
+ return await fn();
15721
+ } finally {
15722
+ await rm(path, { recursive: true, force: true });
15723
+ }
15724
+ }
15725
+
15726
+ // src/config.ts
15615
15727
  function configDir() {
15616
- return process.env.MWN_CONFIG_DIR ?? join(homedir(), ".config", "multi-workspace-for-notion");
15728
+ return process.env.MWN_CONFIG_DIR ?? join2(homedir(), ".config", "multi-workspace-for-notion");
15729
+ }
15730
+ var credentialAccount = (workspaceId) => `workspace:${workspaceId}`;
15731
+ var cachePath = (workspaceId) => join2(configDir(), "cache", `catalog-${workspaceId}.json`);
15732
+ var configPath = () => join2(configDir(), "config.json");
15733
+ function uniqueName(name, workspaceId, others) {
15734
+ return others.some((w) => w.name === name) ? `${name} (${workspaceId.slice(0, 8)})` : name;
15617
15735
  }
15618
- var configPath = () => join(configDir(), "config.json");
15619
15736
  async function loadWorkspaces() {
15620
- try {
15621
- const parsed = JSON.parse(await readFile(configPath(), "utf8"));
15622
- return parsed.workspaces;
15623
- } catch (error2) {
15624
- if (error2.code === "ENOENT") return [];
15625
- throw error2;
15626
- }
15737
+ const file = await readConfig();
15738
+ if (!file) return [];
15739
+ if (file.version === 2) return file.workspaces;
15740
+ return withFileLock("config", migrateIfNeeded);
15627
15741
  }
15628
15742
  async function saveWorkspaces(workspaces) {
15629
- await mkdir(configDir(), { recursive: true, mode: 448 });
15743
+ await mkdir2(configDir(), { recursive: true, mode: 448 });
15630
15744
  const tmp = `${configPath()}.${process.pid}.tmp`;
15631
- const data = { version: 1, workspaces };
15745
+ const data = { version: 2, workspaces };
15632
15746
  await writeFile(tmp, `${JSON.stringify(data, null, 2)}
15633
15747
  `, { mode: 384 });
15634
15748
  await rename(tmp, configPath());
15635
15749
  }
15750
+ async function updateWorkspaces(change) {
15751
+ await withFileLock("config", async () => saveWorkspaces(change(await migrateIfNeeded())));
15752
+ }
15753
+ async function readConfig() {
15754
+ try {
15755
+ return JSON.parse(await readFile(configPath(), "utf8"));
15756
+ } catch (error2) {
15757
+ if (error2.code === "ENOENT") return void 0;
15758
+ throw error2;
15759
+ }
15760
+ }
15761
+ async function migrateIfNeeded() {
15762
+ const file = await readConfig();
15763
+ if (!file) return [];
15764
+ if (file.version === 2) return file.workspaces;
15765
+ const migrated = [];
15766
+ for (const { alias, notionWorkspaceName, ...rest } of file.workspaces) {
15767
+ const from = `workspace:${alias}`;
15768
+ const to = credentialAccount(rest.notionWorkspaceId);
15769
+ await withFileLock(from, async () => {
15770
+ const credentials = await keychain.get(from);
15771
+ if (credentials !== void 0) {
15772
+ await keychain.set(to, credentials);
15773
+ await keychain.delete(from);
15774
+ }
15775
+ });
15776
+ await rm2(join2(configDir(), "cache", `catalog-${alias}.json`), { force: true });
15777
+ migrated.push({ ...rest, name: uniqueName(notionWorkspaceName, rest.notionWorkspaceId, migrated) });
15778
+ }
15779
+ await saveWorkspaces(migrated);
15780
+ return migrated;
15781
+ }
15636
15782
 
15637
15783
  // src/connect.ts
15638
15784
  import { spawn as spawn2 } from "node:child_process";
15639
- import { rm as rm2 } from "node:fs/promises";
15785
+ import { rm as rm3 } from "node:fs/promises";
15640
15786
 
15641
15787
  // node_modules/pkce-challenge/dist/index.node.js
15642
15788
  var crypto;
@@ -16635,6 +16781,7 @@ async function registerClient(authorizationServerUrl, { metadata, clientMetadata
16635
16781
  import { createServer } from "node:http";
16636
16782
  async function startCallbackServer() {
16637
16783
  let deliver;
16784
+ let expectedState;
16638
16785
  const received = new Promise((resolve) => deliver = resolve);
16639
16786
  const handler = (req, res) => {
16640
16787
  const url2 = new URL(req.url ?? "/", "http://localhost");
@@ -16645,12 +16792,18 @@ async function startCallbackServer() {
16645
16792
  }
16646
16793
  const error2 = url2.searchParams.get("error");
16647
16794
  const code = url2.searchParams.get("code");
16795
+ const state = url2.searchParams.get("state");
16796
+ if (!error2 && !code || expectedState === void 0 || state !== expectedState) {
16797
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
16798
+ res.end(`<!doctype html><meta charset="utf-8"><p style="font-family:system-ui">\uC798\uBABB\uB41C \uCF5C\uBC31 \uC694\uCCAD\uC785\uB2C8\uB2E4.</p>`);
16799
+ return;
16800
+ }
16648
16801
  const ok = !error2 && code;
16649
16802
  res.writeHead(ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
16650
16803
  res.end(
16651
16804
  `<!doctype html><meta charset="utf-8"><title>Multi-Workspace for Notion</title><p style="font-family:system-ui;margin:20vh auto;max-width:420px">` + (ok ? "\uC778\uC99D\uC774 \uB05D\uB0AC\uC2B5\uB2C8\uB2E4. \uC774 \uCC3D\uC744 \uB2EB\uACE0 \uD130\uBBF8\uB110\uB85C \uB3CC\uC544\uAC00\uC138\uC694." : "\uC778\uC99D\uC774 \uCDE8\uC18C\uB418\uC5C8\uAC70\uB098 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. \uD130\uBBF8\uB110\uC744 \uD655\uC778\uD558\uC138\uC694.") + `</p>`
16652
16805
  );
16653
- deliver?.(ok ? { code, state: url2.searchParams.get("state") } : { error: error2 ?? "code\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4" });
16806
+ deliver?.(ok ? { code, state } : { error: error2 ?? "code\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4" });
16654
16807
  deliver = void 0;
16655
16808
  };
16656
16809
  const servers = [createServer(handler)];
@@ -16660,7 +16813,8 @@ async function startCallbackServer() {
16660
16813
  if (await listen(ipv62, port, "::1").then(() => true, () => false)) servers.push(ipv62);
16661
16814
  return {
16662
16815
  redirectUrl: `http://localhost:${port}/callback`,
16663
- async waitForCode(expectedState, timeoutMs) {
16816
+ async waitForCode(expected, timeoutMs) {
16817
+ expectedState = expected;
16664
16818
  let timer;
16665
16819
  const timeout = new Promise((_, reject) => {
16666
16820
  timer = setTimeout(() => reject(new Error("\uC778\uC99D \uB300\uAE30 \uC2DC\uAC04\uC774 \uC9C0\uB0AC\uC2B5\uB2C8\uB2E4.")), timeoutMs);
@@ -16668,7 +16822,6 @@ async function startCallbackServer() {
16668
16822
  try {
16669
16823
  const result = await Promise.race([received, timeout]);
16670
16824
  if ("error" in result) throw new Error(`Notion \uC778\uC99D \uC2E4\uD328: ${result.error}`);
16671
- if (result.state !== expectedState) throw new Error("state\uAC00 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC544 \uC778\uC99D \uC751\uB2F5\uC744 \uAC70\uBD80\uD588\uC2B5\uB2C8\uB2E4.");
16672
16825
  return result.code;
16673
16826
  } finally {
16674
16827
  clearTimeout(timer);
@@ -16689,89 +16842,6 @@ function listen(server, port, host) {
16689
16842
  });
16690
16843
  }
16691
16844
 
16692
- // src/keychain.ts
16693
- import { randomBytes } from "node:crypto";
16694
- import { spawn } from "node:child_process";
16695
- var SERVICE = "multi-workspace-for-notion";
16696
- var CHUNK_CHARS = 3e3;
16697
- var keychain = {
16698
- async get(account) {
16699
- for (let attempt = 0; attempt < 2; attempt++) {
16700
- const manifest = await readItem(account);
16701
- if (manifest === void 0) return void 0;
16702
- const { generation, count } = parseManifest(manifest);
16703
- const chunks = [];
16704
- for (let i = 0; i < count; i++) {
16705
- const chunk = await readItem(chunkAccount(account, generation, i));
16706
- if (chunk === void 0) break;
16707
- chunks.push(chunk);
16708
- }
16709
- if (chunks.length === count) return Buffer.from(chunks.join(""), "base64").toString("utf8");
16710
- }
16711
- throw new Error(`\uD0A4\uCCB4\uC778 \uAC12\uC774 \uC77C\uBD80 \uC5C6\uC2B5\uB2C8\uB2E4: ${account}`);
16712
- },
16713
- async set(account, value) {
16714
- assertSafeAccount(account);
16715
- const previous = await readItem(account);
16716
- const encoded = Buffer.from(value, "utf8").toString("base64");
16717
- const generation = randomBytes(4).toString("hex");
16718
- const count = Math.max(1, Math.ceil(encoded.length / CHUNK_CHARS));
16719
- for (let i = 0; i < count; i++) {
16720
- await writeItem(chunkAccount(account, generation, i), encoded.slice(i * CHUNK_CHARS, (i + 1) * CHUNK_CHARS));
16721
- }
16722
- await writeItem(account, `${generation}:${count}`);
16723
- if (previous !== void 0) await deleteChunks(account, previous);
16724
- },
16725
- async delete(account) {
16726
- const manifest = await readItem(account);
16727
- if (manifest === void 0) return;
16728
- await deleteItem(account);
16729
- await deleteChunks(account, manifest);
16730
- }
16731
- };
16732
- function parseManifest(manifest) {
16733
- const match = /^([0-9a-f]+):(\d+)$/.exec(manifest);
16734
- if (!match) throw new Error(`\uD0A4\uCCB4\uC778 \uAC12 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: ${manifest.slice(0, 20)}`);
16735
- return { generation: match[1], count: Number(match[2]) };
16736
- }
16737
- async function deleteChunks(account, manifest) {
16738
- const { generation, count } = parseManifest(manifest);
16739
- for (let i = 0; i < count; i++) await deleteItem(chunkAccount(account, generation, i));
16740
- }
16741
- var chunkAccount = (account, generation, index) => `${account}.${generation}.${index}`;
16742
- function assertSafeAccount(account) {
16743
- if (!/^[A-Za-z0-9:_.-]+$/.test(account)) throw new Error(`\uD0A4\uCCB4\uC778 \uACC4\uC815 \uC774\uB984\uC5D0 \uC4F8 \uC218 \uC5C6\uB294 \uBB38\uC790: ${account}`);
16744
- }
16745
- async function readItem(account) {
16746
- const { code, stdout, stderr } = await run(["find-generic-password", "-s", SERVICE, "-a", account, "-w"]);
16747
- if (code === 44) return void 0;
16748
- if (code !== 0) throw new Error(`\uD0A4\uCCB4\uC778 \uC77D\uAE30 \uC2E4\uD328 (${code}): ${stderr.trim()}`);
16749
- return stdout.trim();
16750
- }
16751
- async function writeItem(account, value) {
16752
- const { stdout, stderr } = await run(["-i"], `add-generic-password -U -s ${SERVICE} -a ${account} -w ${value}
16753
- `);
16754
- if (await readItem(account) !== value) {
16755
- throw new Error(`\uD0A4\uCCB4\uC778 \uC4F0\uAE30 \uC2E4\uD328: ${(stderr || stdout).trim()}`);
16756
- }
16757
- }
16758
- async function deleteItem(account) {
16759
- const { code, stderr } = await run(["delete-generic-password", "-s", SERVICE, "-a", account]);
16760
- if (code !== 0 && code !== 44) throw new Error(`\uD0A4\uCCB4\uC778 \uC0AD\uC81C \uC2E4\uD328 (${code}): ${stderr.trim()}`);
16761
- }
16762
- function run(args2, input) {
16763
- return new Promise((resolve, reject) => {
16764
- const child = spawn("/usr/bin/security", args2, { stdio: ["pipe", "pipe", "pipe"] });
16765
- let stdout = "";
16766
- let stderr = "";
16767
- child.stdout.on("data", (chunk) => stdout += chunk);
16768
- child.stderr.on("data", (chunk) => stderr += chunk);
16769
- child.on("error", reject);
16770
- child.on("close", (code) => resolve({ code, stdout, stderr }));
16771
- child.stdin.end(input ?? "");
16772
- });
16773
- }
16774
-
16775
16845
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
16776
16846
  function isZ4Schema(s) {
16777
16847
  const schema = s;
@@ -19238,7 +19308,7 @@ var StreamableHTTPClientTransport = class {
19238
19308
 
19239
19309
  // src/meta.ts
19240
19310
  var SERVER_NAME = "multi-workspace-for-notion";
19241
- var SERVER_VERSION = "0.1.0";
19311
+ var SERVER_VERSION = "0.2.0";
19242
19312
 
19243
19313
  // src/notion.ts
19244
19314
  var NOTION_MCP_URL = new URL("https://mcp.notion.com/mcp");
@@ -19268,7 +19338,7 @@ var NotionSession = class _NotionSession {
19268
19338
  async callTool(name, args2) {
19269
19339
  return await this.client.callTool({ name, arguments: args2 });
19270
19340
  }
19271
- /** notion-get-self로 이 토큰이 실제로 어느 워크스페이스에 붙었는지 확인한다. */
19341
+ /** Asks notion-get-self which workspace this token actually belongs to. */
19272
19342
  async getSelf() {
19273
19343
  const result = await this.client.callTool({ name: "notion-get-self", arguments: {} });
19274
19344
  const text2 = result.content.filter((part) => part.type === "text").map((part) => part.text ?? "").join("\n");
@@ -19280,66 +19350,15 @@ var NotionSession = class _NotionSession {
19280
19350
  }
19281
19351
  };
19282
19352
  function parseSelf(text2) {
19283
- const json2 = JSON.parse(text2);
19284
- const id = findString(json2, ["workspace_id", "workspaceId"]) ?? findNested(json2, "workspace", ["id"]);
19285
- const name = findString(json2, ["workspace_name", "workspaceName"]) ?? findNested(json2, "workspace", ["name"]);
19286
- if (!id || !name) throw new Error(`notion-get-self \uC751\uB2F5\uC5D0\uC11C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4: ${text2.slice(0, 300)}`);
19287
- return { workspaceId: id, workspaceName: name };
19288
- }
19289
- function findString(value, keys) {
19290
- if (!value || typeof value !== "object") return void 0;
19291
- for (const [key, child] of Object.entries(value)) {
19292
- if (keys.includes(key) && typeof child === "string") return child;
19293
- const nested = findString(child, keys);
19294
- if (nested) return nested;
19353
+ const workspace = JSON.parse(text2).workspace;
19354
+ if (typeof workspace?.id !== "string" || typeof workspace.name !== "string") {
19355
+ throw new Error(`notion-get-self \uC751\uB2F5\uC5D0\uC11C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4: ${text2.slice(0, 300)}`);
19295
19356
  }
19296
- return void 0;
19297
- }
19298
- function findNested(value, parentKey, keys) {
19299
- if (!value || typeof value !== "object") return void 0;
19300
- for (const [key, child] of Object.entries(value)) {
19301
- if (key === parentKey) {
19302
- const found = findString(child, keys);
19303
- if (found) return found;
19304
- }
19305
- const nested = findNested(child, parentKey, keys);
19306
- if (nested) return nested;
19307
- }
19308
- return void 0;
19357
+ return { workspaceId: workspace.id, workspaceName: workspace.name };
19309
19358
  }
19310
19359
 
19311
19360
  // src/oauth.ts
19312
19361
  import { randomBytes as randomBytes2 } from "node:crypto";
19313
-
19314
- // src/lock.ts
19315
- import { mkdir as mkdir2, rm, stat } from "node:fs/promises";
19316
- import { join as join2 } from "node:path";
19317
- var STALE_MS = 1e4;
19318
- var RETRY_MS = 20;
19319
- async function withFileLock(name, fn) {
19320
- const dir = join2(configDir(), "locks");
19321
- const path = join2(dir, `${name.replace(/[^A-Za-z0-9_.-]/g, "_")}.lock`);
19322
- await mkdir2(dir, { recursive: true, mode: 448 });
19323
- for (; ; ) {
19324
- try {
19325
- await mkdir2(path);
19326
- break;
19327
- } catch (error2) {
19328
- if (error2.code !== "EEXIST") throw error2;
19329
- const age = await stat(path).then((s) => Date.now() - s.mtimeMs, () => 0);
19330
- if (age > STALE_MS) await rm(path, { recursive: true, force: true });
19331
- else await new Promise((resolve) => setTimeout(resolve, RETRY_MS));
19332
- }
19333
- }
19334
- try {
19335
- return await fn();
19336
- } finally {
19337
- await rm(path, { recursive: true, force: true });
19338
- }
19339
- }
19340
-
19341
- // src/oauth.ts
19342
- var credentialAccount = (alias) => `workspace:${alias}`;
19343
19362
  var WorkspaceAuthProvider = class _WorkspaceAuthProvider {
19344
19363
  constructor(account, stored, onRedirect) {
19345
19364
  this.account = account;
@@ -19351,17 +19370,20 @@ var WorkspaceAuthProvider = class _WorkspaceAuthProvider {
19351
19370
  onRedirect;
19352
19371
  verifier;
19353
19372
  expectedState;
19354
- /** 키체인에 값이 있는 것을 확인했거나 저장한 적이 있다. 그 뒤에 값이 사라졌다면 연결 해제나 별칭 변경 때문이다. */
19373
+ /** The keychain value was seen or saved. If it disappears later, the workspace was disconnected. */
19355
19374
  persisted = false;
19356
- /** 새로 연결할 때. redirectUrl은 콜백 서버의 주소다. */
19357
- static forNewConnection(alias, redirectUrl, onRedirect) {
19358
- return new _WorkspaceAuthProvider(credentialAccount(alias), { redirectUrl }, onRedirect);
19375
+ /**
19376
+ * For a new connection. The workspace (and so its ID) is only known after sign-in, so this saves under a
19377
+ * temporary account that the caller moves to `workspace:<id>` with moveTo(). redirectUrl is the callback server's address.
19378
+ */
19379
+ static forNewConnection(redirectUrl, onRedirect) {
19380
+ return new _WorkspaceAuthProvider(`pending:${randomBytes2(8).toString("hex")}`, { redirectUrl }, onRedirect);
19359
19381
  }
19360
- /** 이미 연결된 워크스페이스를 쓸 때. 저장된 토큰이 없으면 undefined. */
19361
- static async forExisting(alias) {
19362
- const raw = await keychain.get(credentialAccount(alias));
19382
+ /** For a connected workspace. Undefined when nothing is stored. */
19383
+ static async forExisting(workspaceId) {
19384
+ const raw = await keychain.get(credentialAccount(workspaceId));
19363
19385
  if (raw === void 0) return void 0;
19364
- const provider = new _WorkspaceAuthProvider(credentialAccount(alias), JSON.parse(raw), void 0);
19386
+ const provider = new _WorkspaceAuthProvider(credentialAccount(workspaceId), JSON.parse(raw), void 0);
19365
19387
  provider.persisted = true;
19366
19388
  return provider;
19367
19389
  }
@@ -19381,7 +19403,7 @@ var WorkspaceAuthProvider = class _WorkspaceAuthProvider {
19381
19403
  this.expectedState = randomBytes2(24).toString("base64url");
19382
19404
  return this.expectedState;
19383
19405
  }
19384
- /** 콜백 서버가 비교할 state. redirectToAuthorization() 이후에만 값이 있다. */
19406
+ /** The state the callback must echo back. Set once auth() has started an authorization. */
19385
19407
  get pendingState() {
19386
19408
  return this.expectedState;
19387
19409
  }
@@ -19392,18 +19414,15 @@ var WorkspaceAuthProvider = class _WorkspaceAuthProvider {
19392
19414
  await this.update((s) => s.clientInformation = clientInformation);
19393
19415
  }
19394
19416
  /**
19395
- * Claude 세션마다 서버 프로세스가 따로 뜨므로, 다른 프로세스가 갱신한 토큰을 바로 쓰도록 매번 키체인에서 다시 읽는다.
19396
- * 메모리의 옛 리프레시 토큰을 쓰면, 한 번만 쓸 수 있는 토큰이라 invalid_grant가 난다.
19417
+ * Each Claude session runs its own server process, so re-read the keychain every time to pick up tokens another
19418
+ * process refreshed. Refresh tokens are single-use: a stale one from memory fails with invalid_grant.
19397
19419
  */
19398
19420
  async tokens() {
19399
19421
  await this.reload();
19400
19422
  return this.stored.tokens;
19401
19423
  }
19402
19424
  async saveTokens(tokens) {
19403
- await this.update((s) => {
19404
- s.tokens = tokens;
19405
- s.tokensSavedAt = Date.now();
19406
- });
19425
+ await this.update((s) => s.tokens = tokens);
19407
19426
  }
19408
19427
  async redirectToAuthorization(authorizationUrl) {
19409
19428
  if (!this.onRedirect) {
@@ -19411,7 +19430,7 @@ var WorkspaceAuthProvider = class _WorkspaceAuthProvider {
19411
19430
  }
19412
19431
  await this.onRedirect(authorizationUrl);
19413
19432
  }
19414
- // PKCE verifier는 한 번의 인증 동안만 필요하므로 메모리에만 둔다.
19433
+ // The PKCE verifier lives only for one authorization, so it stays in memory.
19415
19434
  saveCodeVerifier(codeVerifier) {
19416
19435
  this.verifier = codeVerifier;
19417
19436
  }
@@ -19431,30 +19450,36 @@ var WorkspaceAuthProvider = class _WorkspaceAuthProvider {
19431
19450
  if (scope === "verifier") return;
19432
19451
  await this.update((s) => {
19433
19452
  if (scope === "all" || scope === "client") delete s.clientInformation;
19434
- if (scope === "all") {
19435
- delete s.tokens;
19436
- delete s.tokensSavedAt;
19437
- }
19453
+ if (scope === "all") delete s.tokens;
19438
19454
  if (scope === "all" || scope === "discovery") delete s.discoveryState;
19439
19455
  });
19440
19456
  }
19441
19457
  async deleteStored() {
19442
19458
  await keychain.delete(this.account);
19443
19459
  }
19460
+ /** Moves the stored credentials to another keychain account (a new connection's final `workspace:<id>`). */
19461
+ async moveTo(account) {
19462
+ const raw = await keychain.get(this.account);
19463
+ if (raw === void 0) throw new Error("\uC800\uC7A5\uB41C \uC778\uC99D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
19464
+ await keychain.set(account, raw);
19465
+ await keychain.delete(this.account);
19466
+ this.account = account;
19467
+ }
19444
19468
  async reload() {
19445
19469
  const raw = await keychain.get(this.account);
19446
19470
  if (raw !== void 0) this.stored = JSON.parse(raw);
19447
19471
  }
19448
19472
  /**
19449
- * 메모리에 든 값을 통째로 쓰면 그사이 다른 프로세스가 저장한 새 토큰을 옛 토큰으로 덮어쓴다.
19450
- * 그래서 잠금을 잡고 키체인을 다시 읽은 뒤, 바꾸려는 항목만 고쳐서 쓴다.
19451
- * 다른 프로세스가 연결을 해제했거나 별칭을 바꿔 값이 사라졌으면, 지운 토큰을 되살리지 않도록 쓰지 않는다.
19473
+ * Writing the in-memory copy wholesale would overwrite newer tokens another process saved meanwhile.
19474
+ * So take the lock, re-read the keychain and change only the given fields.
19475
+ * If the value vanished because another process disconnected the workspace, write nothing,
19476
+ * so deleted tokens stay deleted.
19452
19477
  */
19453
19478
  async update(change) {
19454
19479
  await withFileLock(this.account, async () => {
19455
19480
  const raw = await keychain.get(this.account);
19456
19481
  if (raw !== void 0) this.stored = JSON.parse(raw);
19457
- else if (this.persisted) throw new Error("\uC774 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB294 \uC5F0\uACB0\uC774 \uD574\uC81C\uB418\uC5C8\uAC70\uB098 \uBCC4\uCE6D\uC774 \uBC14\uB00C\uC5C8\uC2B5\uB2C8\uB2E4. \uC0C8 \uB300\uD654\uC5D0\uC11C \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.");
19482
+ else if (this.persisted) throw new Error("\uC774 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB294 \uB2E4\uB978 \uC138\uC158\uC5D0\uC11C \uC5F0\uACB0\uC774 \uD574\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC0C8 \uB300\uD654\uC5D0\uC11C \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.");
19458
19483
  change(this.stored);
19459
19484
  await keychain.set(this.account, JSON.stringify(this.stored));
19460
19485
  this.persisted = true;
@@ -19490,114 +19515,12 @@ async function revokeTokens(stored, fetchFn = fetch) {
19490
19515
  }
19491
19516
  }
19492
19517
 
19493
- // src/upstream.ts
19494
- import { mkdir as mkdir3, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
19495
- import { join as join3 } from "node:path";
19496
- var UpstreamPool = class {
19497
- constructor(open2 = openWorkspace) {
19498
- this.open = open2;
19499
- }
19500
- open;
19501
- sessions = /* @__PURE__ */ new Map();
19502
- session(alias) {
19503
- let pending = this.sessions.get(alias);
19504
- if (!pending) {
19505
- pending = this.open(alias);
19506
- const opened = pending;
19507
- opened.catch(() => {
19508
- if (this.sessions.get(alias) === opened) this.sessions.delete(alias);
19509
- });
19510
- this.sessions.set(alias, pending);
19511
- }
19512
- return pending;
19513
- }
19514
- /**
19515
- * 업스트림 MCP 세션이 만료되면 서버가 404를 돌려준다. 그 경우 요청이 처리되지 않았으므로
19516
- * 세션을 새로 열어 한 번 더 보낸다. 다른 오류는 이미 처리됐을 수 있어 다시 보내지 않는다.
19517
- */
19518
- async callTool(alias, name, args2) {
19519
- try {
19520
- return await (await this.session(alias)).callTool(name, args2);
19521
- } catch (error2) {
19522
- if (!(error2 instanceof StreamableHTTPError && error2.code === 404)) throw error2;
19523
- await this.reset(alias);
19524
- return (await this.session(alias)).callTool(name, args2);
19525
- }
19526
- }
19527
- async reset(alias) {
19528
- const pending = this.sessions.get(alias);
19529
- this.sessions.delete(alias);
19530
- await pending?.then((s) => s.close()).catch(() => {
19531
- });
19532
- }
19533
- async closeAll() {
19534
- await Promise.all([...this.sessions.keys()].map((alias) => this.reset(alias)));
19535
- }
19536
- };
19537
- var LIVE_TIMEOUT_MS = 15e3;
19538
- async function loadCatalogs(workspaces, pool) {
19539
- const results = await Promise.all(
19540
- workspaces.map(async (w) => {
19541
- try {
19542
- const catalog = await withTimeout(fetchLive(w.alias, pool), LIVE_TIMEOUT_MS);
19543
- await writeCache(catalog);
19544
- return catalog;
19545
- } catch (error2) {
19546
- const cached2 = await readCache(w.alias);
19547
- return cached2 ?? { alias: w.alias, reason: error2.message };
19548
- }
19549
- })
19550
- );
19551
- return {
19552
- catalogs: results.filter((r) => "tools" in r),
19553
- failures: results.filter((r) => "reason" in r)
19554
- };
19555
- }
19556
- async function fetchLive(alias, pool) {
19557
- const session = await pool.session(alias);
19558
- return {
19559
- alias,
19560
- tools: await session.listTools(),
19561
- instructions: session.instructions,
19562
- source: "live",
19563
- fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
19564
- };
19565
- }
19566
- var cachePath = (alias) => join3(configDir(), "cache", `catalog-${alias}.json`);
19567
- async function writeCache(catalog) {
19568
- await mkdir3(join3(configDir(), "cache"), { recursive: true, mode: 448 });
19569
- const tmp = `${cachePath(catalog.alias)}.${process.pid}.tmp`;
19570
- await writeFile2(tmp, JSON.stringify(catalog), { mode: 384 });
19571
- await rename2(tmp, cachePath(catalog.alias));
19572
- }
19573
- async function readCache(alias) {
19574
- try {
19575
- const cached2 = JSON.parse(await readFile2(cachePath(alias), "utf8"));
19576
- return { ...cached2, source: "cache" };
19577
- } catch {
19578
- return void 0;
19579
- }
19580
- }
19581
- function withTimeout(promise, ms) {
19582
- let timer;
19583
- return Promise.race([
19584
- promise,
19585
- new Promise((_, reject) => {
19586
- timer = setTimeout(() => reject(new Error(`${ms / 1e3}\uCD08 \uC548\uC5D0 \uC751\uB2F5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.`)), ms);
19587
- })
19588
- ]).finally(() => clearTimeout(timer));
19589
- }
19590
-
19591
19518
  // src/connect.ts
19592
- var ALIAS_PATTERN = /^[a-z0-9][a-z0-9_-]{0,31}$/;
19593
19519
  var AUTH_TIMEOUT_MS = 10 * 60 * 1e3;
19520
+ var WORKSPACE_ID = /^[0-9a-f-]{32,36}$/i;
19594
19521
  async function connectWorkspace(options) {
19595
- const { alias, description } = options;
19596
- assertAlias(alias);
19597
- const existing = await loadWorkspaces();
19598
- if (existing.some((w) => w.alias === alias)) throw new Error(`\uC774\uBBF8 \uC4F0\uACE0 \uC788\uB294 \uBCC4\uCE6D\uC785\uB2C8\uB2E4: ${alias}`);
19599
19522
  const callback = await startCallbackServer();
19600
- const provider = WorkspaceAuthProvider.forNewConnection(alias, callback.redirectUrl, (url2) => {
19523
+ const provider = WorkspaceAuthProvider.forNewConnection(callback.redirectUrl, (url2) => {
19601
19524
  options.onAuthorizationUrl(url2);
19602
19525
  openBrowser(url2);
19603
19526
  });
@@ -19609,23 +19532,26 @@ async function connectWorkspace(options) {
19609
19532
  }
19610
19533
  session = await NotionSession.open(provider);
19611
19534
  const self = await session.getSelf();
19612
- const duplicate = existing.find((w) => w.notionWorkspaceId === self.workspaceId);
19535
+ if (!WORKSPACE_ID.test(self.workspaceId)) throw new Error(`\uC608\uC0C1\uD558\uC9C0 \uBABB\uD55C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 ID\uC785\uB2C8\uB2E4: ${self.workspaceId}`);
19536
+ const duplicate = (await loadWorkspaces()).find((w) => w.notionWorkspaceId === self.workspaceId);
19613
19537
  if (duplicate) {
19614
19538
  throw new Error(
19615
- `"${self.workspaceName}" \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB294 \uC774\uBBF8 "${duplicate.alias}"(\uC73C)\uB85C \uC5F0\uACB0\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. Notion \uC778\uC99D \uD654\uBA74\uC5D0\uC11C \uB2E4\uB978 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB97C \uACE8\uB790\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694.`
19539
+ `"${duplicate.name}" \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB294 \uC774\uBBF8 \uC5F0\uACB0\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. Notion \uC778\uC99D \uD654\uBA74\uC5D0\uC11C \uB2E4\uB978 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB97C \uACE8\uB790\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694.`
19616
19540
  );
19617
19541
  }
19618
19542
  const tools = await session.listTools();
19619
- const entry = {
19620
- alias,
19621
- description,
19622
- notionWorkspaceId: self.workspaceId,
19623
- notionWorkspaceName: self.workspaceName,
19624
- status: "connected",
19625
- lastSuccessAt: (/* @__PURE__ */ new Date()).toISOString()
19626
- };
19627
- const latest = await loadWorkspaces();
19628
- await saveWorkspaces([...latest.filter((w) => w.alias !== alias), entry]);
19543
+ await provider.moveTo(credentialAccount(self.workspaceId));
19544
+ let entry;
19545
+ await updateWorkspaces((latest) => {
19546
+ entry = {
19547
+ name: uniqueName(self.workspaceName, self.workspaceId, latest),
19548
+ notionWorkspaceId: self.workspaceId,
19549
+ description: options.description,
19550
+ status: "connected",
19551
+ lastSuccessAt: (/* @__PURE__ */ new Date()).toISOString()
19552
+ };
19553
+ return [...latest, entry];
19554
+ });
19629
19555
  return { entry, tools };
19630
19556
  } catch (error2) {
19631
19557
  await provider.deleteStored();
@@ -19635,54 +19561,27 @@ async function connectWorkspace(options) {
19635
19561
  await session?.close();
19636
19562
  }
19637
19563
  }
19638
- async function openWorkspace(alias) {
19639
- const provider = await WorkspaceAuthProvider.forExisting(alias);
19640
- if (!provider) throw new Error(`"${alias}"\uC758 \uC800\uC7A5\uB41C \uC778\uC99D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC5F0\uACB0\uD558\uC138\uC694.`);
19564
+ async function findWorkspace(name) {
19565
+ const entry = (await loadWorkspaces()).find((w) => w.name === name);
19566
+ if (!entry) throw new Error(`\uC5F0\uACB0\uB41C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uAC00 \uC544\uB2D9\uB2C8\uB2E4: ${name}`);
19567
+ return entry;
19568
+ }
19569
+ async function openWorkspace(name) {
19570
+ const entry = await findWorkspace(name);
19571
+ const provider = await WorkspaceAuthProvider.forExisting(entry.notionWorkspaceId);
19572
+ if (!provider) throw new Error(`"${name}"\uC758 \uC800\uC7A5\uB41C \uC778\uC99D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC5F0\uACB0\uD558\uC138\uC694.`);
19641
19573
  return NotionSession.open(provider);
19642
19574
  }
19643
- async function disconnectWorkspace(alias) {
19644
- const workspaces = await loadWorkspaces();
19645
- const raw = await withFileLock(credentialAccount(alias), async () => {
19646
- const found = await keychain.get(credentialAccount(alias));
19647
- await keychain.delete(credentialAccount(alias));
19575
+ async function disconnectWorkspace(name) {
19576
+ const { notionWorkspaceId: id } = await findWorkspace(name);
19577
+ const raw = await withFileLock(credentialAccount(id), async () => {
19578
+ const found = await keychain.get(credentialAccount(id));
19579
+ await keychain.delete(credentialAccount(id));
19648
19580
  return found;
19649
19581
  });
19650
- await rm2(cachePath(alias), { force: true });
19651
- const remaining = workspaces.filter((w) => w.alias !== alias);
19652
- await saveWorkspaces(remaining);
19653
- const revoked = raw !== void 0 && await revokeTokens(JSON.parse(raw));
19654
- return { removed: remaining.length !== workspaces.length || raw !== void 0, revoked };
19655
- }
19656
- async function renameWorkspace(alias, changes) {
19657
- const workspaces = await loadWorkspaces();
19658
- const entry = workspaces.find((w) => w.alias === alias);
19659
- if (!entry) throw new Error(`\uC5F0\uACB0\uB41C \uC801 \uC5C6\uB294 \uBCC4\uCE6D\uC785\uB2C8\uB2E4: ${alias}`);
19660
- const newAlias = changes.newAlias ?? alias;
19661
- const moving = newAlias !== alias;
19662
- if (moving) {
19663
- assertAlias(newAlias);
19664
- if (workspaces.some((w) => w.alias === newAlias)) throw new Error(`\uC774\uBBF8 \uC4F0\uACE0 \uC788\uB294 \uBCC4\uCE6D\uC785\uB2C8\uB2E4: ${newAlias}`);
19665
- }
19666
- const updated = { ...entry, alias: newAlias, description: changes.description ?? entry.description };
19667
- const save = () => saveWorkspaces(workspaces.map((w) => w.alias === alias ? updated : w));
19668
- if (!moving) {
19669
- await save();
19670
- return updated;
19671
- }
19672
- await withFileLock(credentialAccount(alias), async () => {
19673
- const credentials = await keychain.get(credentialAccount(alias));
19674
- if (credentials === void 0) throw new Error(`"${alias}"\uC758 \uC800\uC7A5\uB41C \uC778\uC99D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC5F0\uACB0\uD558\uC138\uC694.`);
19675
- await keychain.set(credentialAccount(newAlias), credentials);
19676
- await save();
19677
- await keychain.delete(credentialAccount(alias));
19678
- });
19679
- await rm2(cachePath(alias), { force: true });
19680
- return updated;
19681
- }
19682
- function assertAlias(alias) {
19683
- if (!ALIAS_PATTERN.test(alias)) {
19684
- throw new Error("\uBCC4\uCE6D\uC740 \uC601\uC5B4 \uC18C\uBB38\uC790\xB7\uC22B\uC790\uB85C \uC2DC\uC791\uD558\uACE0 \uC18C\uBB38\uC790, \uC22B\uC790, -, _\uB9CC \uC4F8 \uC218 \uC788\uC2B5\uB2C8\uB2E4(\uCD5C\uB300 32\uC790).");
19685
- }
19582
+ await rm3(cachePath(id), { force: true });
19583
+ await updateWorkspaces((workspaces) => workspaces.filter((w) => w.notionWorkspaceId !== id));
19584
+ return { revoked: raw !== void 0 && await revokeTokens(JSON.parse(raw)) };
19686
19585
  }
19687
19586
  function openBrowser(url2) {
19688
19587
  const child = spawn2("open", [url2.href], { stdio: "ignore", detached: true });
@@ -20317,10 +20216,11 @@ ${tool.description ?? ""}` : tool.description,
20317
20216
  inputSchema: {
20318
20217
  ...tool.inputSchema,
20319
20218
  properties: {
20219
+ // A plain string, not an enum: Claude keeps a conversation's tool schemas even after tools/list_changed,
20220
+ // so an enum would go stale as soon as a workspace is connected. The server checks the name.
20320
20221
  [WORKSPACE_PARAM]: {
20321
20222
  type: "string",
20322
- enum: aliases,
20323
- description: "\uC774 \uD638\uCD9C\uC744 \uBCF4\uB0BC Notion \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC758 \uBCC4\uCE6D"
20223
+ description: `\uC774 \uD638\uCD9C\uC744 \uBCF4\uB0BC Notion \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC758 \uC774\uB984. \uC774 \uC11C\uBC84\uB97C \uC2DC\uC791\uD560 \uB54C \uC5F0\uACB0\uB3FC \uC788\uB358 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4: ${aliases.join(", ")}. \uC774\uD6C4 \uC0C8\uB85C \uC5F0\uACB0\uD55C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB3C4 \uBC14\uB85C \uC4F8 \uC218 \uC788\uB2E4.`
20324
20224
  },
20325
20225
  ...tool.inputSchema.properties
20326
20226
  },
@@ -20349,8 +20249,7 @@ async function callProxiedTool(catalog, name, args2, call) {
20349
20249
  }
20350
20250
  const others = available.filter((alias) => alias !== workspace);
20351
20251
  if (!isNotFound(result) || others.length === 0) return result;
20352
- const readOnly = catalog.tools.find((t) => t.name === name)?.annotations?.readOnlyHint === true;
20353
- if (!readOnly) {
20252
+ if (!FAN_OUT_ON_NOT_FOUND.has(name)) {
20354
20253
  return withNote(
20355
20254
  result,
20356
20255
  `[${workspace} \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC5D0\uC11C \uCC3E\uC9C0 \uBABB\uD588\uB2E4. \uB2E4\uB978 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4(${others.join(", ")})\uC758 ID\uC77C \uC218 \uC788\uB2E4. notion-fetch\uB85C \uADF8 ID\uAC00 \uC788\uB294 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB97C \uD655\uC778\uD55C \uB4A4, \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uD655\uC778\uC744 \uBC1B\uACE0 \uADF8 workspace\uB85C \uB2E4\uC2DC \uD638\uCD9C\uD55C\uB2E4.]`
@@ -20364,6 +20263,7 @@ async function callProxiedTool(catalog, name, args2, call) {
20364
20263
  `[${workspace} \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC5D0\uB294 \uC5C6\uC5B4\uC11C ${others[found]} \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC5D0\uC11C \uCC3E\uC740 \uACB0\uACFC\uB2E4. \uC774 \uB300\uC0C1\uC744 \uB2E4\uB8E8\uB294 \uB2E4\uC74C \uD638\uCD9C\uC740 workspace=${others[found]}\uB85C \uBCF4\uB0B8\uB2E4.]`
20365
20264
  );
20366
20265
  }
20266
+ var FAN_OUT_ON_NOT_FOUND = /* @__PURE__ */ new Set(["notion-fetch"]);
20367
20267
  var isNotFound = (result) => result.isError === true && result.content.some((c) => c.type === "text" && c.text.includes("object_not_found"));
20368
20268
  var withNote = (result, note) => ({
20369
20269
  ...result,
@@ -20412,108 +20312,103 @@ function errorResult(text2) {
20412
20312
  }
20413
20313
 
20414
20314
  // src/management.ts
20415
- var aliasParam = { type: "string", description: "\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uBCC4\uCE6D" };
20315
+ var workspaceParam = { type: "string", description: "\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uC774\uB984(workspace_list\uC758 name)" };
20416
20316
  var managementTools = [
20417
20317
  {
20418
20318
  name: "workspace_list",
20419
- description: "\uC5F0\uACB0\uB41C Notion \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC758 \uBCC4\uCE6D, \uC124\uBA85, \uC2E4\uC81C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uC774\uB984\uACFC ID, \uC0C1\uD0DC, \uC5F0\uACB0\uC744 \uB9C8\uC9C0\uB9C9\uC73C\uB85C \uD655\uC778\uD55C \uC2DC\uAC01\uC744 \uBCF4\uC5EC\uC900\uB2E4.",
20319
+ description: "\uC5F0\uACB0\uB41C Notion \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC758 \uC774\uB984, ID, \uC124\uBA85, \uC0C1\uD0DC, \uC5F0\uACB0\uC744 \uB9C8\uC9C0\uB9C9\uC73C\uB85C \uD655\uC778\uD55C \uC2DC\uAC01\uC744 \uBCF4\uC5EC\uC900\uB2E4.",
20420
20320
  inputSchema: { type: "object", properties: {} },
20421
20321
  annotations: { title: "\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uBAA9\uB85D", readOnlyHint: true }
20422
20322
  },
20423
20323
  {
20424
20324
  name: "workspace_connect",
20425
- description: "\uC0C8 Notion \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB97C \uC5F0\uACB0\uD55C\uB2E4. \uC0AC\uC6A9\uC790\uC758 \uBE0C\uB77C\uC6B0\uC800\uC5D0 Notion \uB85C\uADF8\uC778 \uD654\uBA74\uC774 \uC5F4\uB9AC\uACE0, \uC0AC\uC6A9\uC790\uAC00 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB97C \uACE8\uB77C \uC2B9\uC778\uD560 \uB54C\uAE4C\uC9C0(\uCD5C\uB300 10\uBD84) \uAE30\uB2E4\uB9B0\uB2E4. \uC2B9\uC778 \uD6C4 \uC2E4\uC81C\uB85C \uC5F0\uACB0\uB41C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uC774\uB984\uC744 \uB3CC\uB824\uC8FC\uBBC0\uB85C \uC0AC\uC6A9\uC790\uAC00 \uC758\uB3C4\uD55C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC778\uC9C0 \uD655\uC778\uC2DC\uD0A8\uB2E4. \uC0C8 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC758 Notion \uB3C4\uAD6C\uB294 \uC0C8 \uB300\uD654\uBD80\uD130 \uBCF4\uC778\uB2E4.",
20325
+ description: "\uC0C8 Notion \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB97C \uC5F0\uACB0\uD55C\uB2E4. \uCCAB \uD638\uCD9C\uC740 \uC0AC\uC6A9\uC790\uC758 \uBE0C\uB77C\uC6B0\uC800\uC5D0 Notion \uB85C\uADF8\uC778 \uD654\uBA74\uC744 \uC5F4\uACE0 \uADF8 \uB85C\uADF8\uC778 \uC8FC\uC18C(authorizationUrl)\uB97C \uBC14\uB85C \uB3CC\uB824\uC900\uB2E4. \uBE0C\uB77C\uC6B0\uC800\uAC00 \uC548 \uC5F4\uB838\uC744 \uC218 \uC788\uC73C\uB2C8 \uC8FC\uC18C\uB97C \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBCF4\uC5EC\uC8FC\uACE0, \uC5F0\uACB0\uD560 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB97C \uACE8\uB77C \uC2B9\uC778\uD558\uB77C\uACE0 \uD55C\uB2E4(\uCD5C\uB300 10\uBD84). \uC0AC\uC6A9\uC790\uAC00 \uC2B9\uC778\uD588\uB2E4\uACE0 \uD558\uBA74 \uB2E4\uC2DC \uD638\uCD9C\uD574 \uACB0\uACFC\uB97C \uBC1B\uB294\uB2E4. \uC5F0\uACB0\uB41C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB294 Notion\uC5D0\uC11C\uC758 \uC774\uB984\uC73C\uB85C \uBD88\uB9AC\uACE0, \uC774 \uB300\uD654\uC5D0\uC11C \uBC14\uB85C workspace \uAC12\uC73C\uB85C \uC4F8 \uC218 \uC788\uB2E4.",
20426
20326
  inputSchema: {
20427
20327
  type: "object",
20428
20328
  properties: {
20429
- alias: {
20430
- type: "string",
20431
- description: "\uC0C8 \uBCC4\uCE6D. \uC601\uC5B4 \uC18C\uBB38\uC790\uB098 \uC22B\uC790\uB85C \uC2DC\uC791\uD558\uACE0 \uC601\uC5B4 \uC18C\uBB38\uC790, \uC22B\uC790, -, _\uB9CC \uC4F4\uB2E4(\uCD5C\uB300 32\uC790)."
20432
- },
20433
20329
  description: { type: "string", description: "\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC5D0 \uBB34\uC5C7\uC774 \uC788\uB294\uC9C0 \uC9E7\uC740 \uC124\uBA85. Claude\uAC00 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB97C \uACE0\uB97C \uB54C \uC4F4\uB2E4." }
20434
- },
20435
- required: ["alias"]
20330
+ }
20436
20331
  },
20437
20332
  annotations: { title: "\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uC5F0\uACB0", readOnlyHint: false, destructiveHint: false, openWorldHint: true }
20438
20333
  },
20439
20334
  {
20440
20335
  name: "workspace_disconnect",
20441
20336
  description: "\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uC5F0\uACB0\uC744 \uD574\uC81C\uD55C\uB2E4. \uC774 \uCEF4\uD4E8\uD130\uC5D0 \uC800\uC7A5\uB41C \uD1A0\uD070\uACFC \uC124\uC815\uC744 \uC9C0\uC6B0\uACE0 Notion \uCABD \uD1A0\uD070\uB3C4 \uD574\uC81C\uD55C\uB2E4. \uB418\uB3CC\uB9B4 \uC218 \uC5C6\uC73C\uBBC0\uB85C \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uD655\uC778\uC744 \uBC1B\uC740 \uB4A4 \uD638\uCD9C\uD55C\uB2E4.",
20442
- inputSchema: { type: "object", properties: { alias: aliasParam }, required: ["alias"] },
20337
+ inputSchema: { type: "object", properties: { workspace: workspaceParam }, required: ["workspace"] },
20443
20338
  annotations: { title: "\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uC5F0\uACB0 \uD574\uC81C", readOnlyHint: false, destructiveHint: true, idempotentHint: true }
20444
20339
  },
20445
- {
20446
- name: "workspace_rename",
20447
- description: "\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC758 \uBCC4\uCE6D\uC774\uB098 \uC124\uBA85\uC744 \uBC14\uAFBC\uB2E4. \uB2E4\uC2DC \uB85C\uADF8\uC778\uD558\uC9C0 \uC54A\uB294\uB2E4. \uBC14\uB010 \uBCC4\uCE6D\uC740 \uC0C8 \uB300\uD654\uBD80\uD130 workspace \uAC12\uC73C\uB85C \uC4F8 \uC218 \uC788\uB2E4.",
20448
- inputSchema: {
20449
- type: "object",
20450
- properties: {
20451
- alias: { type: "string", description: "\uC9C0\uAE08 \uBCC4\uCE6D" },
20452
- new_alias: {
20453
- type: "string",
20454
- description: "\uC0C8 \uBCC4\uCE6D. \uC601\uC5B4 \uC18C\uBB38\uC790\uB098 \uC22B\uC790\uB85C \uC2DC\uC791\uD558\uACE0 \uC601\uC5B4 \uC18C\uBB38\uC790, \uC22B\uC790, -, _\uB9CC \uC4F4\uB2E4(\uCD5C\uB300 32\uC790). \uBC14\uAFB8\uC9C0 \uC54A\uC73C\uBA74 \uC0DD\uB7B5\uD55C\uB2E4."
20455
- },
20456
- description: { type: "string", description: "\uC0C8 \uC124\uBA85. \uBC14\uAFB8\uC9C0 \uC54A\uC73C\uBA74 \uC0DD\uB7B5\uD55C\uB2E4." }
20457
- },
20458
- required: ["alias"]
20459
- },
20460
- annotations: { title: "\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uC774\uB984 \uBC14\uAFB8\uAE30", readOnlyHint: false, destructiveHint: false, idempotentHint: true }
20461
- },
20462
20340
  {
20463
20341
  name: "workspace_check",
20464
20342
  description: "\uC800\uC7A5\uB41C \uC778\uC99D\uC73C\uB85C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC5D0 \uB2E4\uC2DC \uBD99\uC5B4\uC11C \uC5F0\uACB0\uC774 \uC0B4\uC544 \uC788\uB294\uC9C0, \uCC98\uC74C \uC5F0\uACB0\uD55C \uADF8 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uAC00 \uB9DE\uB294\uC9C0 \uD655\uC778\uD558\uACE0 \uB3C4\uAD6C \uC218\uB97C \uC54C\uB824\uC900\uB2E4. \uACB0\uACFC\uB85C \uC0C1\uD0DC\uB97C \uAC31\uC2E0\uD55C\uB2E4.",
20465
- inputSchema: { type: "object", properties: { alias: aliasParam }, required: ["alias"] },
20343
+ inputSchema: { type: "object", properties: { workspace: workspaceParam }, required: ["workspace"] },
20466
20344
  annotations: { title: "\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uC5F0\uACB0 \uD655\uC778", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
20467
20345
  }
20468
20346
  ];
20347
+ var pendingConnect;
20348
+ async function connectInBackground(description, onChanged, connect = connectWorkspace) {
20349
+ if (pendingConnect?.outcome) {
20350
+ const { outcome } = pendingConnect;
20351
+ pendingConnect = void 0;
20352
+ return outcome;
20353
+ }
20354
+ if (pendingConnect) {
20355
+ return json({ status: "waiting_for_approval", authorizationUrl: pendingConnect.url, note: "\uC544\uC9C1 \uC2B9\uC778\uC744 \uAE30\uB2E4\uB9AC\uB294 \uC911\uC785\uB2C8\uB2E4." });
20356
+ }
20357
+ const state = {};
20358
+ pendingConnect = state;
20359
+ let urlReady = () => {
20360
+ };
20361
+ const gotUrl = new Promise((resolve) => urlReady = resolve);
20362
+ const done = connect({
20363
+ description,
20364
+ onAuthorizationUrl: (url2) => {
20365
+ state.url = url2.href;
20366
+ urlReady();
20367
+ }
20368
+ }).then(
20369
+ async ({ entry, tools }) => {
20370
+ await onChanged().catch(() => {
20371
+ });
20372
+ state.outcome = json({ status: "connected", connected: entry, toolCount: tools.length });
20373
+ },
20374
+ (error2) => {
20375
+ state.outcome = errorResult(error2.message);
20376
+ }
20377
+ );
20378
+ await Promise.race([gotUrl, done]);
20379
+ if (state.outcome) {
20380
+ pendingConnect = void 0;
20381
+ return state.outcome;
20382
+ }
20383
+ return json({
20384
+ status: "waiting_for_approval",
20385
+ authorizationUrl: state.url,
20386
+ note: "\uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC774 \uC8FC\uC18C\uB85C Notion\uC5D0 \uB85C\uADF8\uC778\uD574 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB97C \uACE8\uB77C \uC2B9\uC778\uD55C \uB4A4, workspace_connect\uB97C \uB2E4\uC2DC \uD638\uCD9C\uD558\uC138\uC694."
20387
+ });
20388
+ }
20469
20389
  var text = (value) => ({ content: [{ type: "text", text: value }] });
20470
20390
  var json = (value) => text(JSON.stringify(value, null, 2));
20471
- async function callManagementTool(name, args2, onDisconnected) {
20472
- const alias = typeof args2.alias === "string" ? args2.alias : "";
20391
+ async function callManagementTool(name, args2, onChanged) {
20392
+ const workspace = typeof args2.workspace === "string" ? args2.workspace : "";
20473
20393
  try {
20474
20394
  switch (name) {
20475
20395
  case "workspace_list":
20476
20396
  return json(await loadWorkspaces());
20477
- case "workspace_connect": {
20478
- if (!alias) return errorResult("alias\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
20479
- const { entry, tools } = await connectWorkspace({
20480
- alias,
20481
- description: typeof args2.description === "string" ? args2.description : "",
20482
- onAuthorizationUrl: () => {
20483
- }
20484
- });
20485
- return json({
20486
- connected: entry,
20487
- toolCount: tools.length,
20488
- note: "\uC0C8 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC758 Notion \uB3C4\uAD6C\uB294 \uC0C8 \uB300\uD654\uBD80\uD130 \uC4F8 \uC218 \uC788\uC2B5\uB2C8\uB2E4."
20489
- });
20490
- }
20397
+ case "workspace_connect":
20398
+ return await connectInBackground(typeof args2.description === "string" ? args2.description : "", onChanged);
20491
20399
  case "workspace_disconnect": {
20492
- if (!alias) return errorResult("alias\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
20493
- const { removed, revoked } = await disconnectWorkspace(alias);
20494
- if (!removed) return errorResult(`\uC5F0\uACB0\uB41C \uC801 \uC5C6\uB294 \uBCC4\uCE6D\uC785\uB2C8\uB2E4: ${alias}`);
20495
- await onDisconnected(alias);
20400
+ if (!workspace) return errorResult("workspace\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
20401
+ const { revoked } = await disconnectWorkspace(workspace);
20402
+ await onChanged(workspace);
20496
20403
  return json({
20497
- disconnected: alias,
20404
+ disconnected: workspace,
20498
20405
  revokedOnNotion: revoked,
20499
20406
  note: revoked ? "\uC774 \uCEF4\uD4E8\uD130\uC758 \uD1A0\uD070\uACFC \uC124\uC815\uC744 \uC9C0\uC6B0\uACE0, Notion \uCABD \uD1A0\uD070\uB3C4 \uD574\uC81C\uD588\uC2B5\uB2C8\uB2E4." : "\uC774 \uCEF4\uD4E8\uD130\uC758 \uD1A0\uD070\uACFC \uC124\uC815\uC744 \uC9C0\uC6E0\uC2B5\uB2C8\uB2E4. Notion \uCABD \uD1A0\uD070 \uD574\uC81C\uB294 \uC2E4\uD328\uD588\uC73C\uB2C8, Notion \uC124\uC815\uC758 \uC5F0\uACB0(Connections) \uBA54\uB274\uC5D0\uC11C \uD574\uC81C\uD558\uC138\uC694."
20500
20407
  });
20501
20408
  }
20502
- case "workspace_rename": {
20503
- if (!alias) return errorResult("alias\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
20504
- const newAlias = typeof args2.new_alias === "string" ? args2.new_alias : void 0;
20505
- const description = typeof args2.description === "string" ? args2.description : void 0;
20506
- if (newAlias === void 0 && description === void 0) return errorResult("new_alias\uB098 description\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
20507
- const entry = await renameWorkspace(alias, { newAlias, description });
20508
- if (entry.alias !== alias) await onDisconnected(alias);
20509
- return json({
20510
- renamed: entry,
20511
- note: entry.alias === alias ? void 0 : "\uC0C8 \uBCC4\uCE6D\uC740 \uC0C8 \uB300\uD654\uBD80\uD130 workspace \uAC12\uC73C\uB85C \uC4F8 \uC218 \uC788\uC2B5\uB2C8\uB2E4."
20512
- });
20513
- }
20514
20409
  case "workspace_check":
20515
- if (!alias) return errorResult("alias\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
20516
- return await checkWorkspace(alias);
20410
+ if (!workspace) return errorResult("workspace\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
20411
+ return await checkWorkspace(workspace);
20517
20412
  default:
20518
20413
  return void 0;
20519
20414
  }
@@ -20521,81 +20416,177 @@ async function callManagementTool(name, args2, onDisconnected) {
20521
20416
  return errorResult(error2.message);
20522
20417
  }
20523
20418
  }
20524
- async function checkWorkspace(alias) {
20525
- const entry = (await loadWorkspaces()).find((w) => w.alias === alias);
20526
- if (!entry) return errorResult(`\uC5F0\uACB0\uB41C \uC801 \uC5C6\uB294 \uBCC4\uCE6D\uC785\uB2C8\uB2E4: ${alias}`);
20419
+ async function checkWorkspace(name) {
20420
+ const entry = await findWorkspace(name);
20527
20421
  let status;
20528
20422
  let detail;
20529
20423
  try {
20530
- const session = await openWorkspace(alias);
20424
+ const session = await openWorkspace(name);
20531
20425
  try {
20532
20426
  const self = await session.getSelf();
20533
20427
  const tools = await session.listTools();
20534
20428
  const sameWorkspace = self.workspaceId === entry.notionWorkspaceId;
20535
20429
  status = sameWorkspace ? "connected" : "error";
20536
- detail = sameWorkspace ? { workspace: self.workspaceName, toolCount: tools.length } : {
20537
- problem: `\uCC98\uC74C \uC5F0\uACB0\uD55C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4("${entry.notionWorkspaceName}")\uAC00 \uC544\uB2C8\uB77C "${self.workspaceName}"\uC5D0 \uBD99\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uC5F0\uACB0\uC744 \uD574\uC81C\uD558\uACE0 \uB2E4\uC2DC \uC5F0\uACB0\uD558\uC138\uC694.`
20538
- };
20430
+ detail = sameWorkspace ? { notionName: self.workspaceName, toolCount: tools.length } : { problem: `\uC800\uC7A5\uB41C \uC778\uC99D\uC774 \uB2E4\uB978 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4("${self.workspaceName}")\uC5D0 \uBD99\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uC5F0\uACB0\uC744 \uD574\uC81C\uD558\uACE0 \uB2E4\uC2DC \uC5F0\uACB0\uD558\uC138\uC694.` };
20539
20431
  } finally {
20540
20432
  await session.close();
20541
20433
  }
20542
20434
  } catch (error2) {
20543
20435
  const message = error2.message;
20544
- status = /인증|auth|401|invalid_grant/i.test(message) ? "needs_reauth" : "error";
20436
+ const signInDead = error2 instanceof InvalidGrantError || error2 instanceof UnauthorizedError || /저장된 인증/.test(message);
20437
+ status = signInDead ? "needs_reauth" : "error";
20545
20438
  detail = { problem: message };
20546
20439
  }
20547
- await updateEntry(alias, (w) => ({
20548
- ...w,
20549
- status,
20550
- lastSuccessAt: status === "connected" ? (/* @__PURE__ */ new Date()).toISOString() : w.lastSuccessAt
20551
- }));
20552
- const result = json({ alias, status, ...detail });
20440
+ await updateWorkspaces(
20441
+ (workspaces) => workspaces.map(
20442
+ (w) => w.notionWorkspaceId === entry.notionWorkspaceId ? { ...w, status, lastSuccessAt: status === "connected" ? (/* @__PURE__ */ new Date()).toISOString() : w.lastSuccessAt } : w
20443
+ )
20444
+ );
20445
+ const result = json({ workspace: name, status, ...detail });
20553
20446
  return status === "connected" ? result : { ...result, isError: true };
20554
20447
  }
20555
- async function updateEntry(alias, update) {
20556
- const workspaces = await loadWorkspaces();
20557
- await saveWorkspaces(workspaces.map((w) => w.alias === alias ? update(w) : w));
20448
+
20449
+ // src/upstream.ts
20450
+ import { mkdir as mkdir3, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
20451
+ import { join as join3 } from "node:path";
20452
+ var UpstreamPool = class {
20453
+ constructor(open2 = openWorkspace) {
20454
+ this.open = open2;
20455
+ }
20456
+ open;
20457
+ sessions = /* @__PURE__ */ new Map();
20458
+ session(alias) {
20459
+ const existing = this.sessions.get(alias);
20460
+ if (existing) return existing;
20461
+ const opened = this.open(alias);
20462
+ opened.catch(() => {
20463
+ if (this.sessions.get(alias) === opened) this.sessions.delete(alias);
20464
+ });
20465
+ this.sessions.set(alias, opened);
20466
+ return opened;
20467
+ }
20468
+ /**
20469
+ * An expired upstream MCP session gets a 404 and the request was not processed, so reopen and send it once more.
20470
+ * Other errors may come after the request was processed, so those are never resent.
20471
+ */
20472
+ async callTool(alias, name, args2) {
20473
+ try {
20474
+ return await (await this.session(alias)).callTool(name, args2);
20475
+ } catch (error2) {
20476
+ if (!(error2 instanceof StreamableHTTPError && error2.code === 404)) throw error2;
20477
+ await this.reset(alias);
20478
+ return (await this.session(alias)).callTool(name, args2);
20479
+ }
20480
+ }
20481
+ async reset(alias) {
20482
+ const pending = this.sessions.get(alias);
20483
+ this.sessions.delete(alias);
20484
+ await pending?.then((s) => s.close()).catch(() => {
20485
+ });
20486
+ }
20487
+ async closeAll() {
20488
+ await Promise.all([...this.sessions.keys()].map((alias) => this.reset(alias)));
20489
+ }
20490
+ };
20491
+ var LIVE_TIMEOUT_MS = 15e3;
20492
+ async function loadCatalogs(workspaces, pool) {
20493
+ const results = await Promise.all(
20494
+ workspaces.map(async (w) => {
20495
+ try {
20496
+ const catalog = await withTimeout(fetchLive(w.name, pool), LIVE_TIMEOUT_MS);
20497
+ await writeCache(catalog, w.notionWorkspaceId);
20498
+ return catalog;
20499
+ } catch (error2) {
20500
+ const cached2 = await readCache(w.notionWorkspaceId);
20501
+ return cached2 ? { ...cached2, alias: w.name } : { alias: w.name, reason: error2.message };
20502
+ }
20503
+ })
20504
+ );
20505
+ return {
20506
+ catalogs: results.filter((r) => "tools" in r),
20507
+ failures: results.filter((r) => "reason" in r)
20508
+ };
20509
+ }
20510
+ async function fetchLive(alias, pool) {
20511
+ const session = await pool.session(alias);
20512
+ return {
20513
+ alias,
20514
+ tools: await session.listTools(),
20515
+ instructions: session.instructions
20516
+ };
20517
+ }
20518
+ async function writeCache(catalog, workspaceId) {
20519
+ await mkdir3(join3(configDir(), "cache"), { recursive: true, mode: 448 });
20520
+ const tmp = `${cachePath(workspaceId)}.${process.pid}.tmp`;
20521
+ await writeFile2(tmp, JSON.stringify(catalog), { mode: 384 });
20522
+ await rename2(tmp, cachePath(workspaceId));
20523
+ }
20524
+ async function readCache(workspaceId) {
20525
+ try {
20526
+ return JSON.parse(await readFile2(cachePath(workspaceId), "utf8"));
20527
+ } catch {
20528
+ return void 0;
20529
+ }
20530
+ }
20531
+ function withTimeout(promise, ms) {
20532
+ let timer;
20533
+ return Promise.race([
20534
+ promise,
20535
+ new Promise((_, reject) => {
20536
+ timer = setTimeout(() => reject(new Error(`${ms / 1e3}\uCD08 \uC548\uC5D0 \uC751\uB2F5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.`)), ms);
20537
+ })
20538
+ ]).finally(() => clearTimeout(timer));
20558
20539
  }
20559
20540
 
20560
20541
  // src/server.ts
20561
20542
  var log = (message) => console.error(`[${SERVER_NAME}] ${message}`);
20562
20543
  async function createServer2() {
20563
- const workspaces = await loadWorkspaces();
20564
20544
  const pool = new UpstreamPool();
20545
+ const workspaces = await loadWorkspaces();
20565
20546
  const { catalogs, failures } = await loadCatalogs(workspaces, pool);
20566
- for (const f of failures) log(`${f.alias}: \uB3C4\uAD6C \uBAA9\uB85D\uC744 \uBC1B\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4 (${f.reason})`);
20567
- const catalog = mergeCatalogs(catalogs, log);
20568
- const searchAll = searchAllTool(catalog);
20547
+ let current = toolsFor(catalogs, failures);
20569
20548
  const server = new Server(
20570
20549
  { name: SERVER_NAME, version: SERVER_VERSION },
20571
- { capabilities: { tools: {} }, instructions: buildInstructions(workspaces, catalogs, failures) }
20550
+ { capabilities: { tools: { listChanged: true } }, instructions: buildInstructions(workspaces, catalogs, failures) }
20572
20551
  );
20552
+ const refresh = async (staleAlias) => {
20553
+ if (staleAlias) await pool.reset(staleAlias);
20554
+ const latest = await loadCatalogs(await loadWorkspaces(), pool);
20555
+ current = toolsFor(latest.catalogs, latest.failures);
20556
+ await server.sendToolListChanged();
20557
+ };
20573
20558
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
20574
- tools: [...managementTools, ...searchAll ? [searchAll] : [], ...catalog.tools]
20559
+ tools: [...managementTools, ...current.searchAll ? [current.searchAll] : [], ...current.catalog.tools]
20575
20560
  }));
20576
20561
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
20577
20562
  const { name, arguments: args2 } = request.params;
20563
+ const { catalog, searchAll } = current;
20578
20564
  const call = (alias, tool, rest) => pool.callTool(alias, tool, rest);
20579
20565
  if (searchAll && name === SEARCH_ALL) return callSearchAll(catalog, args2, call);
20580
- const managed = await callManagementTool(name, args2 ?? {}, (alias) => pool.reset(alias));
20566
+ const managed = await callManagementTool(name, args2 ?? {}, refresh);
20581
20567
  return managed ?? callProxiedTool(catalog, name, args2, call);
20582
20568
  });
20583
20569
  server.onclose = () => void pool.closeAll();
20584
20570
  return server;
20585
20571
  }
20572
+ function toolsFor(catalogs, failures) {
20573
+ for (const f of failures) log(`${f.alias}: \uB3C4\uAD6C \uBAA9\uB85D\uC744 \uBC1B\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4 (${f.reason})`);
20574
+ const catalog = mergeCatalogs(catalogs, log);
20575
+ return { catalog, searchAll: searchAllTool(catalog) };
20576
+ }
20586
20577
  function buildInstructions(workspaces, catalogs, failures) {
20587
20578
  const failed = new Map(failures.map((f) => [f.alias, f.reason]));
20588
20579
  const lines = workspaces.length === 0 ? ["- (\uC5C6\uC74C) workspace_connect \uB3C4\uAD6C\uB85C \uBA3C\uC800 \uC5F0\uACB0\uD574\uC57C \uD55C\uB2E4."] : workspaces.map((w) => {
20589
- const label = `- ${w.alias}: Notion \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 "${w.notionWorkspaceName}"${w.description ? ` (${w.description})` : ""}`;
20590
- return failed.has(w.alias) ? `${label} - \uC9C0\uAE08 \uC5F0\uACB0\uD560 \uC218 \uC5C6\uC74C: ${failed.get(w.alias)}` : label;
20580
+ const label = `- "${w.name}"${w.description ? `: ${w.description}` : ""}`;
20581
+ return failed.has(w.name) ? `${label} - \uC9C0\uAE08 \uC5F0\uACB0\uD560 \uC218 \uC5C6\uC74C: ${failed.get(w.name)}` : label;
20591
20582
  });
20592
20583
  const sections = [
20593
20584
  [
20594
20585
  "\uC774 \uC11C\uBC84(Multi-Workspace for Notion)\uB294 \uC5EC\uB7EC Notion \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB97C \uB3D9\uC2DC\uC5D0 \uC5F0\uACB0\uD574 \uB454\uB2E4.",
20595
- `\uBAA8\uB4E0 Notion \uB3C4\uAD6C\uC5D0\uB294 ${WORKSPACE_PARAM} \uD30C\uB77C\uBBF8\uD130\uAC00 \uC788\uACE0, \uD638\uCD9C\uB9C8\uB2E4 \uB300\uC0C1 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC758 \uBCC4\uCE6D\uC744 \uB123\uC5B4\uC57C \uD55C\uB2E4.`,
20586
+ `\uBAA8\uB4E0 Notion \uB3C4\uAD6C\uC5D0\uB294 ${WORKSPACE_PARAM} \uD30C\uB77C\uBBF8\uD130\uAC00 \uC788\uACE0, \uD638\uCD9C\uB9C8\uB2E4 \uB300\uC0C1 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC758 \uC774\uB984(\uC544\uB798 \uBAA9\uB85D\uC758 \uC774\uB984 \uADF8\uB300\uB85C)\uC744 \uB123\uC5B4\uC57C \uD55C\uB2E4.`,
20596
20587
  "\uC0AC\uC6A9\uC790\uAC00 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB97C \uB9D0\uD558\uC9C0 \uC54A\uC558\uACE0 \uB9E5\uB77D\uC73C\uB85C\uB3C4 \uC54C \uC218 \uC5C6\uC744 \uB54C, \uC4F0\uAE30\xB7\uC218\uC815\xB7\uC0AD\uC81C\uB294 \uC2E4\uD589\uD558\uAE30 \uC804\uC5D0 \uC5B4\uB290 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC778\uC9C0 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBB3B\uB294\uB2E4.",
20597
20588
  `\uC5EC\uB7EC \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB97C \uD55C\uAEBC\uBC88\uC5D0 \uCC3E\uC744 \uB54C\uB294 ${SEARCH_ALL} \uB3C4\uAD6C\uB97C \uC4F4\uB2E4(\uC5F0\uACB0\uB41C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uAC00 \uB458 \uC774\uC0C1\uC77C \uB54C\uB9CC \uC788\uB2E4).`,
20598
- "\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uAD00\uB9AC\uB294 workspace_list, workspace_connect, workspace_disconnect, workspace_rename, workspace_check \uB3C4\uAD6C\uB85C \uD55C\uB2E4.",
20589
+ "\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uAD00\uB9AC\uB294 workspace_list, workspace_connect, workspace_disconnect, workspace_check \uB3C4\uAD6C\uB85C \uD55C\uB2E4.",
20599
20590
  "",
20600
20591
  "\uC5F0\uACB0\uB41C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4:",
20601
20592
  ...lines
@@ -20622,10 +20613,10 @@ var [command, ...args] = process.argv.slice(2);
20622
20613
  var USAGE = `\uC0AC\uC6A9\uBC95:
20623
20614
  multi-workspace-for-notion MCP \uC11C\uBC84(stdio)\uB85C \uC2E4\uD589
20624
20615
  multi-workspace-for-notion list \uC5F0\uACB0\uB41C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uBAA9\uB85D
20625
- multi-workspace-for-notion add <\uBCC4\uCE6D> [\uC124\uBA85...] \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uC5F0\uACB0(\uBE0C\uB77C\uC6B0\uC800 \uC778\uC99D)
20626
- multi-workspace-for-notion tools <\uBCC4\uCE6D> \uC800\uC7A5\uB41C \uC778\uC99D\uC73C\uB85C \uBD99\uC5B4\uC11C \uD655\uC778\uD558\uACE0 \uB3C4\uAD6C \uBAA9\uB85D \uCD9C\uB825
20627
- multi-workspace-for-notion rename <\uBCC4\uCE6D> <\uC0C8 \uBCC4\uCE6D> \uBCC4\uCE6D \uBC14\uAFB8\uAE30(\uB2E4\uC2DC \uB85C\uADF8\uC778\uD558\uC9C0 \uC54A\uC74C)
20628
- multi-workspace-for-notion remove <\uBCC4\uCE6D> \uC5F0\uACB0 \uD574\uC81C\uC640 \uD1A0\uD070 \uC0AD\uC81C`;
20616
+ multi-workspace-for-notion add [\uC124\uBA85...] \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uC5F0\uACB0(\uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uACE0\uB978 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC758 \uC774\uB984\uC73C\uB85C \uC5F0\uACB0)
20617
+ multi-workspace-for-notion tools <\uC774\uB984> \uC800\uC7A5\uB41C \uC778\uC99D\uC73C\uB85C \uBD99\uC5B4\uC11C \uD655\uC778\uD558\uACE0 \uB3C4\uAD6C \uBAA9\uB85D \uCD9C\uB825
20618
+ multi-workspace-for-notion remove <\uC774\uB984> \uC5F0\uACB0 \uD574\uC81C\uC640 \uD1A0\uD070 \uC0AD\uC81C
20619
+ (\uC774\uB984\uC5D0 \uACF5\uBC31\uC774 \uC788\uC73C\uBA74 \uB530\uC634\uD45C\uB85C \uAC10\uC2FC\uB2E4: remove "\uD5C8 \uC740\uC11C\uC758 Notion")`;
20629
20620
  try {
20630
20621
  switch (command) {
20631
20622
  case void 0: {
@@ -20639,7 +20630,7 @@ try {
20639
20630
  console.log("\uC5F0\uACB0\uB41C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
20640
20631
  }
20641
20632
  for (const w of workspaces) {
20642
- console.log(`${w.alias} ${w.notionWorkspaceName} ${w.status} ${w.description}`);
20633
+ console.log(`${w.name} ${w.status} ${w.description}`);
20643
20634
  }
20644
20635
  break;
20645
20636
  }
@@ -20648,11 +20639,8 @@ try {
20648
20639
  break;
20649
20640
  }
20650
20641
  case "add": {
20651
- const [alias, ...descriptionWords] = args;
20652
- if (!alias) throw new UsageError();
20653
20642
  const { entry, tools } = await connectWorkspace({
20654
- alias,
20655
- description: descriptionWords.join(" "),
20643
+ description: args.join(" "),
20656
20644
  onAuthorizationUrl: (url2) => console.log(
20657
20645
  `\uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C Notion \uC778\uC99D \uD654\uBA74\uC744 \uC5FD\uB2C8\uB2E4. \uC5F0\uACB0\uD560 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB97C \uACE8\uB77C \uC2B9\uC778\uD558\uC138\uC694.
20658
20646
  \uBE0C\uB77C\uC6B0\uC800\uAC00 \uC5F4\uB9AC\uC9C0 \uC54A\uC73C\uBA74 \uC774 \uC8FC\uC18C\uB85C \uB4E4\uC5B4\uAC00\uC138\uC694:
@@ -20660,38 +20648,30 @@ ${url2.href}
20660
20648
  `
20661
20649
  )
20662
20650
  });
20663
- console.log(`\uC5F0\uACB0\uD588\uC2B5\uB2C8\uB2E4: ${entry.alias} \u2192 "${entry.notionWorkspaceName}" (${entry.notionWorkspaceId})`);
20651
+ console.log(`\uC5F0\uACB0\uD588\uC2B5\uB2C8\uB2E4: "${entry.name}" (${entry.notionWorkspaceId})`);
20664
20652
  console.log(`\uACF5\uC2DD \uB3C4\uAD6C ${tools.length}\uAC1C\uB97C \uBC1B\uC558\uC2B5\uB2C8\uB2E4.`);
20665
20653
  break;
20666
20654
  }
20667
20655
  case "tools": {
20668
- const [alias] = args;
20669
- if (!alias) throw new UsageError();
20670
- const session = await openWorkspace(alias);
20656
+ const [name] = args;
20657
+ if (!name) throw new UsageError();
20658
+ const session = await openWorkspace(name);
20671
20659
  try {
20672
20660
  const self = await session.getSelf();
20673
20661
  const tools = await session.listTools();
20674
- console.log(`${alias} \u2192 "${self.workspaceName}" (${self.workspaceId}), \uB3C4\uAD6C ${tools.length}\uAC1C`);
20662
+ console.log(`"${name}" \u2192 Notion "${self.workspaceName}" (${self.workspaceId}), \uB3C4\uAD6C ${tools.length}\uAC1C`);
20675
20663
  for (const tool of tools) console.log(` ${tool.name}`);
20676
20664
  } finally {
20677
20665
  await session.close();
20678
20666
  }
20679
20667
  break;
20680
20668
  }
20681
- case "rename": {
20682
- const [alias, newAlias] = args;
20683
- if (!alias || !newAlias) throw new UsageError();
20684
- await renameWorkspace(alias, { newAlias });
20685
- console.log(`\uBCC4\uCE6D\uC744 \uBC14\uAFE8\uC2B5\uB2C8\uB2E4: ${alias} \u2192 ${newAlias}`);
20686
- break;
20687
- }
20688
20669
  case "remove": {
20689
- const [alias] = args;
20690
- if (!alias) throw new UsageError();
20691
- const { removed, revoked } = await disconnectWorkspace(alias);
20692
- if (!removed) console.log(`\uC5F0\uACB0\uB41C \uC801 \uC5C6\uB294 \uBCC4\uCE6D\uC785\uB2C8\uB2E4: ${alias}`);
20693
- else if (revoked) console.log(`\uC5F0\uACB0\uC744 \uD574\uC81C\uD588\uC2B5\uB2C8\uB2E4: ${alias} (Notion \uCABD \uD1A0\uD070\uB3C4 \uD574\uC81C)`);
20694
- else console.log(`\uC5F0\uACB0\uC744 \uD574\uC81C\uD588\uC2B5\uB2C8\uB2E4: ${alias}
20670
+ const [name] = args;
20671
+ if (!name) throw new UsageError();
20672
+ const { revoked } = await disconnectWorkspace(name);
20673
+ if (revoked) console.log(`\uC5F0\uACB0\uC744 \uD574\uC81C\uD588\uC2B5\uB2C8\uB2E4: ${name} (Notion \uCABD \uD1A0\uD070\uB3C4 \uD574\uC81C)`);
20674
+ else console.log(`\uC5F0\uACB0\uC744 \uD574\uC81C\uD588\uC2B5\uB2C8\uB2E4: ${name}
20695
20675
  Notion \uCABD \uD1A0\uD070 \uD574\uC81C\uB294 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. Notion \uC124\uC815\uC758 \uC5F0\uACB0(Connections) \uBA54\uB274\uC5D0\uC11C \uD574\uC81C\uD558\uC138\uC694.`);
20696
20676
  break;
20697
20677
  }
@@ -20710,8 +20690,8 @@ ${USAGE}` : USAGE);
20710
20690
  process.exitCode = 1;
20711
20691
  }
20712
20692
  function sessionStatus(workspaces) {
20713
- if (workspaces.length === 0) return "Notion: \uC5F0\uACB0\uB41C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. /multi-workspace:workspace-add <\uBCC4\uCE6D> \uC73C\uB85C \uC5F0\uACB0\uD558\uC138\uC694.";
20714
- const names = workspaces.map((w) => `${w.alias}(${w.notionWorkspaceName})`).join(", ");
20715
- const broken = workspaces.filter((w) => w.status !== "connected").map((w) => w.alias);
20693
+ if (workspaces.length === 0) return "Notion: \uC5F0\uACB0\uB41C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. /multi-workspace:workspace-add \uB85C \uC5F0\uACB0\uD558\uC138\uC694.";
20694
+ const names = workspaces.map((w) => w.name).join(", ");
20695
+ const broken = workspaces.filter((w) => w.status !== "connected").map((w) => w.name);
20716
20696
  return broken.length === 0 ? `Notion: ${names}` : `Notion: ${names} \xB7 \uD655\uC778 \uD544\uC694: ${broken.join(", ")} \u2192 /multi-workspace:workspace-check`;
20717
20697
  }