multi-workspace-for-notion 0.1.1 → 0.2.1
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/README.ko.md +35 -39
- package/README.md +35 -39
- package/package.json +4 -5
- package/plugin/server.mjs +430 -449
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 ??
|
|
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
|
-
|
|
15621
|
-
|
|
15622
|
-
|
|
15623
|
-
|
|
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
|
|
15743
|
+
await mkdir2(configDir(), { recursive: true, mode: 448 });
|
|
15630
15744
|
const tmp = `${configPath()}.${process.pid}.tmp`;
|
|
15631
|
-
const data = { version:
|
|
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
|
|
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;
|
|
@@ -16696,89 +16842,6 @@ function listen(server, port, host) {
|
|
|
16696
16842
|
});
|
|
16697
16843
|
}
|
|
16698
16844
|
|
|
16699
|
-
// src/keychain.ts
|
|
16700
|
-
import { randomBytes } from "node:crypto";
|
|
16701
|
-
import { spawn } from "node:child_process";
|
|
16702
|
-
var SERVICE = "multi-workspace-for-notion";
|
|
16703
|
-
var CHUNK_CHARS = 3e3;
|
|
16704
|
-
var keychain = {
|
|
16705
|
-
async get(account) {
|
|
16706
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
16707
|
-
const manifest = await readItem(account);
|
|
16708
|
-
if (manifest === void 0) return void 0;
|
|
16709
|
-
const { generation, count } = parseManifest(manifest);
|
|
16710
|
-
const chunks = [];
|
|
16711
|
-
for (let i = 0; i < count; i++) {
|
|
16712
|
-
const chunk = await readItem(chunkAccount(account, generation, i));
|
|
16713
|
-
if (chunk === void 0) break;
|
|
16714
|
-
chunks.push(chunk);
|
|
16715
|
-
}
|
|
16716
|
-
if (chunks.length === count) return Buffer.from(chunks.join(""), "base64").toString("utf8");
|
|
16717
|
-
}
|
|
16718
|
-
throw new Error(`\uD0A4\uCCB4\uC778 \uAC12\uC774 \uC77C\uBD80 \uC5C6\uC2B5\uB2C8\uB2E4: ${account}`);
|
|
16719
|
-
},
|
|
16720
|
-
async set(account, value) {
|
|
16721
|
-
assertSafeAccount(account);
|
|
16722
|
-
const previous = await readItem(account);
|
|
16723
|
-
const encoded = Buffer.from(value, "utf8").toString("base64");
|
|
16724
|
-
const generation = randomBytes(4).toString("hex");
|
|
16725
|
-
const count = Math.max(1, Math.ceil(encoded.length / CHUNK_CHARS));
|
|
16726
|
-
for (let i = 0; i < count; i++) {
|
|
16727
|
-
await writeItem(chunkAccount(account, generation, i), encoded.slice(i * CHUNK_CHARS, (i + 1) * CHUNK_CHARS));
|
|
16728
|
-
}
|
|
16729
|
-
await writeItem(account, `${generation}:${count}`);
|
|
16730
|
-
if (previous !== void 0) await deleteChunks(account, previous);
|
|
16731
|
-
},
|
|
16732
|
-
async delete(account) {
|
|
16733
|
-
const manifest = await readItem(account);
|
|
16734
|
-
if (manifest === void 0) return;
|
|
16735
|
-
await deleteItem(account);
|
|
16736
|
-
await deleteChunks(account, manifest);
|
|
16737
|
-
}
|
|
16738
|
-
};
|
|
16739
|
-
function parseManifest(manifest) {
|
|
16740
|
-
const match = /^([0-9a-f]+):(\d+)$/.exec(manifest);
|
|
16741
|
-
if (!match) throw new Error(`\uD0A4\uCCB4\uC778 \uAC12 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: ${manifest.slice(0, 20)}`);
|
|
16742
|
-
return { generation: match[1], count: Number(match[2]) };
|
|
16743
|
-
}
|
|
16744
|
-
async function deleteChunks(account, manifest) {
|
|
16745
|
-
const { generation, count } = parseManifest(manifest);
|
|
16746
|
-
for (let i = 0; i < count; i++) await deleteItem(chunkAccount(account, generation, i));
|
|
16747
|
-
}
|
|
16748
|
-
var chunkAccount = (account, generation, index) => `${account}.${generation}.${index}`;
|
|
16749
|
-
function assertSafeAccount(account) {
|
|
16750
|
-
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}`);
|
|
16751
|
-
}
|
|
16752
|
-
async function readItem(account) {
|
|
16753
|
-
const { code, stdout, stderr } = await run(["find-generic-password", "-s", SERVICE, "-a", account, "-w"]);
|
|
16754
|
-
if (code === 44) return void 0;
|
|
16755
|
-
if (code !== 0) throw new Error(`\uD0A4\uCCB4\uC778 \uC77D\uAE30 \uC2E4\uD328 (${code}): ${stderr.trim()}`);
|
|
16756
|
-
return stdout.trim();
|
|
16757
|
-
}
|
|
16758
|
-
async function writeItem(account, value) {
|
|
16759
|
-
const { stdout, stderr } = await run(["-i"], `add-generic-password -U -s ${SERVICE} -a ${account} -w ${value}
|
|
16760
|
-
`);
|
|
16761
|
-
if (await readItem(account) !== value) {
|
|
16762
|
-
throw new Error(`\uD0A4\uCCB4\uC778 \uC4F0\uAE30 \uC2E4\uD328: ${(stderr || stdout).trim()}`);
|
|
16763
|
-
}
|
|
16764
|
-
}
|
|
16765
|
-
async function deleteItem(account) {
|
|
16766
|
-
const { code, stderr } = await run(["delete-generic-password", "-s", SERVICE, "-a", account]);
|
|
16767
|
-
if (code !== 0 && code !== 44) throw new Error(`\uD0A4\uCCB4\uC778 \uC0AD\uC81C \uC2E4\uD328 (${code}): ${stderr.trim()}`);
|
|
16768
|
-
}
|
|
16769
|
-
function run(args2, input) {
|
|
16770
|
-
return new Promise((resolve, reject) => {
|
|
16771
|
-
const child = spawn("/usr/bin/security", args2, { stdio: ["pipe", "pipe", "pipe"] });
|
|
16772
|
-
let stdout = "";
|
|
16773
|
-
let stderr = "";
|
|
16774
|
-
child.stdout.on("data", (chunk) => stdout += chunk);
|
|
16775
|
-
child.stderr.on("data", (chunk) => stderr += chunk);
|
|
16776
|
-
child.on("error", reject);
|
|
16777
|
-
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
16778
|
-
child.stdin.end(input ?? "");
|
|
16779
|
-
});
|
|
16780
|
-
}
|
|
16781
|
-
|
|
16782
16845
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
|
|
16783
16846
|
function isZ4Schema(s) {
|
|
16784
16847
|
const schema = s;
|
|
@@ -19245,7 +19308,7 @@ var StreamableHTTPClientTransport = class {
|
|
|
19245
19308
|
|
|
19246
19309
|
// src/meta.ts
|
|
19247
19310
|
var SERVER_NAME = "multi-workspace-for-notion";
|
|
19248
|
-
var SERVER_VERSION = "0.
|
|
19311
|
+
var SERVER_VERSION = "0.2.1";
|
|
19249
19312
|
|
|
19250
19313
|
// src/notion.ts
|
|
19251
19314
|
var NOTION_MCP_URL = new URL("https://mcp.notion.com/mcp");
|
|
@@ -19287,66 +19350,15 @@ var NotionSession = class _NotionSession {
|
|
|
19287
19350
|
}
|
|
19288
19351
|
};
|
|
19289
19352
|
function parseSelf(text2) {
|
|
19290
|
-
const
|
|
19291
|
-
|
|
19292
|
-
|
|
19293
|
-
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)}`);
|
|
19294
|
-
return { workspaceId: id, workspaceName: name };
|
|
19295
|
-
}
|
|
19296
|
-
function findString(value, keys) {
|
|
19297
|
-
if (!value || typeof value !== "object") return void 0;
|
|
19298
|
-
for (const [key, child] of Object.entries(value)) {
|
|
19299
|
-
if (keys.includes(key) && typeof child === "string") return child;
|
|
19300
|
-
const nested = findString(child, keys);
|
|
19301
|
-
if (nested) return nested;
|
|
19302
|
-
}
|
|
19303
|
-
return void 0;
|
|
19304
|
-
}
|
|
19305
|
-
function findNested(value, parentKey, keys) {
|
|
19306
|
-
if (!value || typeof value !== "object") return void 0;
|
|
19307
|
-
for (const [key, child] of Object.entries(value)) {
|
|
19308
|
-
if (key === parentKey) {
|
|
19309
|
-
const found = findString(child, keys);
|
|
19310
|
-
if (found) return found;
|
|
19311
|
-
}
|
|
19312
|
-
const nested = findNested(child, parentKey, keys);
|
|
19313
|
-
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)}`);
|
|
19314
19356
|
}
|
|
19315
|
-
return
|
|
19357
|
+
return { workspaceId: workspace.id, workspaceName: workspace.name };
|
|
19316
19358
|
}
|
|
19317
19359
|
|
|
19318
19360
|
// src/oauth.ts
|
|
19319
19361
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
19320
|
-
|
|
19321
|
-
// src/lock.ts
|
|
19322
|
-
import { mkdir as mkdir2, rm, stat } from "node:fs/promises";
|
|
19323
|
-
import { join as join2 } from "node:path";
|
|
19324
|
-
var STALE_MS = 1e4;
|
|
19325
|
-
var RETRY_MS = 20;
|
|
19326
|
-
async function withFileLock(name, fn) {
|
|
19327
|
-
const dir = join2(configDir(), "locks");
|
|
19328
|
-
const path = join2(dir, `${name.replace(/[^A-Za-z0-9_.-]/g, "_")}.lock`);
|
|
19329
|
-
await mkdir2(dir, { recursive: true, mode: 448 });
|
|
19330
|
-
for (; ; ) {
|
|
19331
|
-
try {
|
|
19332
|
-
await mkdir2(path);
|
|
19333
|
-
break;
|
|
19334
|
-
} catch (error2) {
|
|
19335
|
-
if (error2.code !== "EEXIST") throw error2;
|
|
19336
|
-
const age = await stat(path).then((s) => Date.now() - s.mtimeMs, () => 0);
|
|
19337
|
-
if (age > STALE_MS) await rm(path, { recursive: true, force: true });
|
|
19338
|
-
else await new Promise((resolve) => setTimeout(resolve, RETRY_MS));
|
|
19339
|
-
}
|
|
19340
|
-
}
|
|
19341
|
-
try {
|
|
19342
|
-
return await fn();
|
|
19343
|
-
} finally {
|
|
19344
|
-
await rm(path, { recursive: true, force: true });
|
|
19345
|
-
}
|
|
19346
|
-
}
|
|
19347
|
-
|
|
19348
|
-
// src/oauth.ts
|
|
19349
|
-
var credentialAccount = (alias) => `workspace:${alias}`;
|
|
19350
19362
|
var WorkspaceAuthProvider = class _WorkspaceAuthProvider {
|
|
19351
19363
|
constructor(account, stored, onRedirect) {
|
|
19352
19364
|
this.account = account;
|
|
@@ -19358,17 +19370,20 @@ var WorkspaceAuthProvider = class _WorkspaceAuthProvider {
|
|
|
19358
19370
|
onRedirect;
|
|
19359
19371
|
verifier;
|
|
19360
19372
|
expectedState;
|
|
19361
|
-
/** The keychain value was seen or saved. If it disappears later, the workspace was disconnected
|
|
19373
|
+
/** The keychain value was seen or saved. If it disappears later, the workspace was disconnected. */
|
|
19362
19374
|
persisted = false;
|
|
19363
|
-
/**
|
|
19364
|
-
|
|
19365
|
-
|
|
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);
|
|
19366
19381
|
}
|
|
19367
19382
|
/** For a connected workspace. Undefined when nothing is stored. */
|
|
19368
|
-
static async forExisting(
|
|
19369
|
-
const raw = await keychain.get(credentialAccount(
|
|
19383
|
+
static async forExisting(workspaceId) {
|
|
19384
|
+
const raw = await keychain.get(credentialAccount(workspaceId));
|
|
19370
19385
|
if (raw === void 0) return void 0;
|
|
19371
|
-
const provider = new _WorkspaceAuthProvider(credentialAccount(
|
|
19386
|
+
const provider = new _WorkspaceAuthProvider(credentialAccount(workspaceId), JSON.parse(raw), void 0);
|
|
19372
19387
|
provider.persisted = true;
|
|
19373
19388
|
return provider;
|
|
19374
19389
|
}
|
|
@@ -19407,10 +19422,7 @@ var WorkspaceAuthProvider = class _WorkspaceAuthProvider {
|
|
|
19407
19422
|
return this.stored.tokens;
|
|
19408
19423
|
}
|
|
19409
19424
|
async saveTokens(tokens) {
|
|
19410
|
-
await this.update((s) =>
|
|
19411
|
-
s.tokens = tokens;
|
|
19412
|
-
s.tokensSavedAt = Date.now();
|
|
19413
|
-
});
|
|
19425
|
+
await this.update((s) => s.tokens = tokens);
|
|
19414
19426
|
}
|
|
19415
19427
|
async redirectToAuthorization(authorizationUrl) {
|
|
19416
19428
|
if (!this.onRedirect) {
|
|
@@ -19438,16 +19450,21 @@ var WorkspaceAuthProvider = class _WorkspaceAuthProvider {
|
|
|
19438
19450
|
if (scope === "verifier") return;
|
|
19439
19451
|
await this.update((s) => {
|
|
19440
19452
|
if (scope === "all" || scope === "client") delete s.clientInformation;
|
|
19441
|
-
if (scope === "all")
|
|
19442
|
-
delete s.tokens;
|
|
19443
|
-
delete s.tokensSavedAt;
|
|
19444
|
-
}
|
|
19453
|
+
if (scope === "all") delete s.tokens;
|
|
19445
19454
|
if (scope === "all" || scope === "discovery") delete s.discoveryState;
|
|
19446
19455
|
});
|
|
19447
19456
|
}
|
|
19448
19457
|
async deleteStored() {
|
|
19449
19458
|
await keychain.delete(this.account);
|
|
19450
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
|
+
}
|
|
19451
19468
|
async reload() {
|
|
19452
19469
|
const raw = await keychain.get(this.account);
|
|
19453
19470
|
if (raw !== void 0) this.stored = JSON.parse(raw);
|
|
@@ -19455,14 +19472,14 @@ var WorkspaceAuthProvider = class _WorkspaceAuthProvider {
|
|
|
19455
19472
|
/**
|
|
19456
19473
|
* Writing the in-memory copy wholesale would overwrite newer tokens another process saved meanwhile.
|
|
19457
19474
|
* So take the lock, re-read the keychain and change only the given fields.
|
|
19458
|
-
* If the value vanished because another process disconnected
|
|
19475
|
+
* If the value vanished because another process disconnected the workspace, write nothing,
|
|
19459
19476
|
* so deleted tokens stay deleted.
|
|
19460
19477
|
*/
|
|
19461
19478
|
async update(change) {
|
|
19462
19479
|
await withFileLock(this.account, async () => {
|
|
19463
19480
|
const raw = await keychain.get(this.account);
|
|
19464
19481
|
if (raw !== void 0) this.stored = JSON.parse(raw);
|
|
19465
|
-
else if (this.persisted) throw new Error("\uC774 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB294 \
|
|
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.");
|
|
19466
19483
|
change(this.stored);
|
|
19467
19484
|
await keychain.set(this.account, JSON.stringify(this.stored));
|
|
19468
19485
|
this.persisted = true;
|
|
@@ -19498,110 +19515,12 @@ async function revokeTokens(stored, fetchFn = fetch) {
|
|
|
19498
19515
|
}
|
|
19499
19516
|
}
|
|
19500
19517
|
|
|
19501
|
-
// src/upstream.ts
|
|
19502
|
-
import { mkdir as mkdir3, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
|
|
19503
|
-
import { join as join3 } from "node:path";
|
|
19504
|
-
var UpstreamPool = class {
|
|
19505
|
-
constructor(open2 = openWorkspace) {
|
|
19506
|
-
this.open = open2;
|
|
19507
|
-
}
|
|
19508
|
-
open;
|
|
19509
|
-
sessions = /* @__PURE__ */ new Map();
|
|
19510
|
-
session(alias) {
|
|
19511
|
-
const existing = this.sessions.get(alias);
|
|
19512
|
-
if (existing) return existing;
|
|
19513
|
-
const opened = this.open(alias);
|
|
19514
|
-
opened.catch(() => {
|
|
19515
|
-
if (this.sessions.get(alias) === opened) this.sessions.delete(alias);
|
|
19516
|
-
});
|
|
19517
|
-
this.sessions.set(alias, opened);
|
|
19518
|
-
return opened;
|
|
19519
|
-
}
|
|
19520
|
-
/**
|
|
19521
|
-
* An expired upstream MCP session gets a 404 and the request was not processed, so reopen and send it once more.
|
|
19522
|
-
* Other errors may come after the request was processed, so those are never resent.
|
|
19523
|
-
*/
|
|
19524
|
-
async callTool(alias, name, args2) {
|
|
19525
|
-
try {
|
|
19526
|
-
return await (await this.session(alias)).callTool(name, args2);
|
|
19527
|
-
} catch (error2) {
|
|
19528
|
-
if (!(error2 instanceof StreamableHTTPError && error2.code === 404)) throw error2;
|
|
19529
|
-
await this.reset(alias);
|
|
19530
|
-
return (await this.session(alias)).callTool(name, args2);
|
|
19531
|
-
}
|
|
19532
|
-
}
|
|
19533
|
-
async reset(alias) {
|
|
19534
|
-
const pending = this.sessions.get(alias);
|
|
19535
|
-
this.sessions.delete(alias);
|
|
19536
|
-
await pending?.then((s) => s.close()).catch(() => {
|
|
19537
|
-
});
|
|
19538
|
-
}
|
|
19539
|
-
async closeAll() {
|
|
19540
|
-
await Promise.all([...this.sessions.keys()].map((alias) => this.reset(alias)));
|
|
19541
|
-
}
|
|
19542
|
-
};
|
|
19543
|
-
var LIVE_TIMEOUT_MS = 15e3;
|
|
19544
|
-
async function loadCatalogs(workspaces, pool) {
|
|
19545
|
-
const results = await Promise.all(
|
|
19546
|
-
workspaces.map(async (w) => {
|
|
19547
|
-
try {
|
|
19548
|
-
const catalog = await withTimeout(fetchLive(w.alias, pool), LIVE_TIMEOUT_MS);
|
|
19549
|
-
await writeCache(catalog);
|
|
19550
|
-
return catalog;
|
|
19551
|
-
} catch (error2) {
|
|
19552
|
-
const cached2 = await readCache(w.alias);
|
|
19553
|
-
return cached2 ?? { alias: w.alias, reason: error2.message };
|
|
19554
|
-
}
|
|
19555
|
-
})
|
|
19556
|
-
);
|
|
19557
|
-
return {
|
|
19558
|
-
catalogs: results.filter((r) => "tools" in r),
|
|
19559
|
-
failures: results.filter((r) => "reason" in r)
|
|
19560
|
-
};
|
|
19561
|
-
}
|
|
19562
|
-
async function fetchLive(alias, pool) {
|
|
19563
|
-
const session = await pool.session(alias);
|
|
19564
|
-
return {
|
|
19565
|
-
alias,
|
|
19566
|
-
tools: await session.listTools(),
|
|
19567
|
-
instructions: session.instructions,
|
|
19568
|
-
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
19569
|
-
};
|
|
19570
|
-
}
|
|
19571
|
-
var cachePath = (alias) => join3(configDir(), "cache", `catalog-${alias}.json`);
|
|
19572
|
-
async function writeCache(catalog) {
|
|
19573
|
-
await mkdir3(join3(configDir(), "cache"), { recursive: true, mode: 448 });
|
|
19574
|
-
const tmp = `${cachePath(catalog.alias)}.${process.pid}.tmp`;
|
|
19575
|
-
await writeFile2(tmp, JSON.stringify(catalog), { mode: 384 });
|
|
19576
|
-
await rename2(tmp, cachePath(catalog.alias));
|
|
19577
|
-
}
|
|
19578
|
-
async function readCache(alias) {
|
|
19579
|
-
try {
|
|
19580
|
-
return JSON.parse(await readFile2(cachePath(alias), "utf8"));
|
|
19581
|
-
} catch {
|
|
19582
|
-
return void 0;
|
|
19583
|
-
}
|
|
19584
|
-
}
|
|
19585
|
-
function withTimeout(promise, ms) {
|
|
19586
|
-
let timer;
|
|
19587
|
-
return Promise.race([
|
|
19588
|
-
promise,
|
|
19589
|
-
new Promise((_, reject) => {
|
|
19590
|
-
timer = setTimeout(() => reject(new Error(`${ms / 1e3}\uCD08 \uC548\uC5D0 \uC751\uB2F5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.`)), ms);
|
|
19591
|
-
})
|
|
19592
|
-
]).finally(() => clearTimeout(timer));
|
|
19593
|
-
}
|
|
19594
|
-
|
|
19595
19518
|
// src/connect.ts
|
|
19596
|
-
var ALIAS_PATTERN = /^[a-z0-9][a-z0-9_-]{0,31}$/;
|
|
19597
19519
|
var AUTH_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
19520
|
+
var WORKSPACE_ID = /^[0-9a-f-]{32,36}$/i;
|
|
19598
19521
|
async function connectWorkspace(options) {
|
|
19599
|
-
const { alias, description } = options;
|
|
19600
|
-
assertAlias(alias);
|
|
19601
|
-
const existing = await loadWorkspaces();
|
|
19602
|
-
if (existing.some((w) => w.alias === alias)) throw new Error(`\uC774\uBBF8 \uC4F0\uACE0 \uC788\uB294 \uBCC4\uCE6D\uC785\uB2C8\uB2E4: ${alias}`);
|
|
19603
19522
|
const callback = await startCallbackServer();
|
|
19604
|
-
const provider = WorkspaceAuthProvider.forNewConnection(
|
|
19523
|
+
const provider = WorkspaceAuthProvider.forNewConnection(callback.redirectUrl, (url2) => {
|
|
19605
19524
|
options.onAuthorizationUrl(url2);
|
|
19606
19525
|
openBrowser(url2);
|
|
19607
19526
|
});
|
|
@@ -19613,23 +19532,26 @@ async function connectWorkspace(options) {
|
|
|
19613
19532
|
}
|
|
19614
19533
|
session = await NotionSession.open(provider);
|
|
19615
19534
|
const self = await session.getSelf();
|
|
19616
|
-
|
|
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);
|
|
19617
19537
|
if (duplicate) {
|
|
19618
19538
|
throw new Error(
|
|
19619
|
-
`"${
|
|
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.`
|
|
19620
19540
|
);
|
|
19621
19541
|
}
|
|
19622
19542
|
const tools = await session.listTools();
|
|
19623
|
-
|
|
19624
|
-
|
|
19625
|
-
|
|
19626
|
-
|
|
19627
|
-
|
|
19628
|
-
|
|
19629
|
-
|
|
19630
|
-
|
|
19631
|
-
|
|
19632
|
-
|
|
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
|
+
});
|
|
19633
19555
|
return { entry, tools };
|
|
19634
19556
|
} catch (error2) {
|
|
19635
19557
|
await provider.deleteStored();
|
|
@@ -19639,55 +19561,27 @@ async function connectWorkspace(options) {
|
|
|
19639
19561
|
await session?.close();
|
|
19640
19562
|
}
|
|
19641
19563
|
}
|
|
19642
|
-
async function
|
|
19643
|
-
const
|
|
19644
|
-
if (!
|
|
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.`);
|
|
19645
19573
|
return NotionSession.open(provider);
|
|
19646
19574
|
}
|
|
19647
|
-
async function disconnectWorkspace(
|
|
19648
|
-
|
|
19649
|
-
const
|
|
19650
|
-
|
|
19651
|
-
|
|
19652
|
-
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));
|
|
19653
19580
|
return found;
|
|
19654
19581
|
});
|
|
19655
|
-
await
|
|
19656
|
-
|
|
19657
|
-
await
|
|
19658
|
-
const revoked = raw !== void 0 && await revokeTokens(JSON.parse(raw));
|
|
19659
|
-
return { removed: remaining.length !== workspaces.length || raw !== void 0, revoked };
|
|
19660
|
-
}
|
|
19661
|
-
async function renameWorkspace(alias, changes) {
|
|
19662
|
-
const workspaces = await loadWorkspaces();
|
|
19663
|
-
const entry = workspaces.find((w) => w.alias === alias);
|
|
19664
|
-
if (!entry) throw new Error(`\uC5F0\uACB0\uB41C \uC801 \uC5C6\uB294 \uBCC4\uCE6D\uC785\uB2C8\uB2E4: ${alias}`);
|
|
19665
|
-
const newAlias = changes.newAlias ?? alias;
|
|
19666
|
-
const moving = newAlias !== alias;
|
|
19667
|
-
if (moving) {
|
|
19668
|
-
assertAlias(newAlias);
|
|
19669
|
-
if (workspaces.some((w) => w.alias === newAlias)) throw new Error(`\uC774\uBBF8 \uC4F0\uACE0 \uC788\uB294 \uBCC4\uCE6D\uC785\uB2C8\uB2E4: ${newAlias}`);
|
|
19670
|
-
}
|
|
19671
|
-
const updated = { ...entry, alias: newAlias, description: changes.description ?? entry.description };
|
|
19672
|
-
const save = () => saveWorkspaces(workspaces.map((w) => w.alias === alias ? updated : w));
|
|
19673
|
-
if (!moving) {
|
|
19674
|
-
await save();
|
|
19675
|
-
return updated;
|
|
19676
|
-
}
|
|
19677
|
-
await withFileLock(credentialAccount(alias), async () => {
|
|
19678
|
-
const credentials = await keychain.get(credentialAccount(alias));
|
|
19679
|
-
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.`);
|
|
19680
|
-
await keychain.set(credentialAccount(newAlias), credentials);
|
|
19681
|
-
await save();
|
|
19682
|
-
await keychain.delete(credentialAccount(alias));
|
|
19683
|
-
});
|
|
19684
|
-
await rm2(cachePath(alias), { force: true });
|
|
19685
|
-
return updated;
|
|
19686
|
-
}
|
|
19687
|
-
function assertAlias(alias) {
|
|
19688
|
-
if (!ALIAS_PATTERN.test(alias)) {
|
|
19689
|
-
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).");
|
|
19690
|
-
}
|
|
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)) };
|
|
19691
19585
|
}
|
|
19692
19586
|
function openBrowser(url2) {
|
|
19693
19587
|
const child = spawn2("open", [url2.href], { stdio: "ignore", detached: true });
|
|
@@ -20322,10 +20216,11 @@ ${tool.description ?? ""}` : tool.description,
|
|
|
20322
20216
|
inputSchema: {
|
|
20323
20217
|
...tool.inputSchema,
|
|
20324
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.
|
|
20325
20221
|
[WORKSPACE_PARAM]: {
|
|
20326
20222
|
type: "string",
|
|
20327
|
-
|
|
20328
|
-
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.`
|
|
20329
20224
|
},
|
|
20330
20225
|
...tool.inputSchema.properties
|
|
20331
20226
|
},
|
|
@@ -20417,108 +20312,109 @@ function errorResult(text2) {
|
|
|
20417
20312
|
}
|
|
20418
20313
|
|
|
20419
20314
|
// src/management.ts
|
|
20420
|
-
var
|
|
20315
|
+
var workspaceParam = { type: "string", description: "\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uC774\uB984(workspace_list\uC758 name)" };
|
|
20421
20316
|
var managementTools = [
|
|
20422
20317
|
{
|
|
20423
20318
|
name: "workspace_list",
|
|
20424
|
-
description: "\uC5F0\uACB0\uB41C Notion \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC758 \
|
|
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.",
|
|
20425
20320
|
inputSchema: { type: "object", properties: {} },
|
|
20426
20321
|
annotations: { title: "\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uBAA9\uB85D", readOnlyHint: true }
|
|
20427
20322
|
},
|
|
20428
20323
|
{
|
|
20429
20324
|
name: "workspace_connect",
|
|
20430
|
-
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\
|
|
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.",
|
|
20431
20326
|
inputSchema: {
|
|
20432
20327
|
type: "object",
|
|
20433
20328
|
properties: {
|
|
20434
|
-
alias: {
|
|
20435
|
-
type: "string",
|
|
20436
|
-
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)."
|
|
20437
|
-
},
|
|
20438
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." }
|
|
20439
|
-
}
|
|
20440
|
-
required: ["alias"]
|
|
20330
|
+
}
|
|
20441
20331
|
},
|
|
20442
20332
|
annotations: { title: "\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uC5F0\uACB0", readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
20443
20333
|
},
|
|
20444
20334
|
{
|
|
20445
20335
|
name: "workspace_disconnect",
|
|
20446
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.",
|
|
20447
|
-
inputSchema: { type: "object", properties: {
|
|
20337
|
+
inputSchema: { type: "object", properties: { workspace: workspaceParam }, required: ["workspace"] },
|
|
20448
20338
|
annotations: { title: "\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uC5F0\uACB0 \uD574\uC81C", readOnlyHint: false, destructiveHint: true, idempotentHint: true }
|
|
20449
20339
|
},
|
|
20450
|
-
{
|
|
20451
|
-
name: "workspace_rename",
|
|
20452
|
-
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.",
|
|
20453
|
-
inputSchema: {
|
|
20454
|
-
type: "object",
|
|
20455
|
-
properties: {
|
|
20456
|
-
alias: { type: "string", description: "\uC9C0\uAE08 \uBCC4\uCE6D" },
|
|
20457
|
-
new_alias: {
|
|
20458
|
-
type: "string",
|
|
20459
|
-
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."
|
|
20460
|
-
},
|
|
20461
|
-
description: { type: "string", description: "\uC0C8 \uC124\uBA85. \uBC14\uAFB8\uC9C0 \uC54A\uC73C\uBA74 \uC0DD\uB7B5\uD55C\uB2E4." }
|
|
20462
|
-
},
|
|
20463
|
-
required: ["alias"]
|
|
20464
|
-
},
|
|
20465
|
-
annotations: { title: "\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uC774\uB984 \uBC14\uAFB8\uAE30", readOnlyHint: false, destructiveHint: false, idempotentHint: true }
|
|
20466
|
-
},
|
|
20467
20340
|
{
|
|
20468
20341
|
name: "workspace_check",
|
|
20469
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.",
|
|
20470
|
-
inputSchema: { type: "object", properties: {
|
|
20343
|
+
inputSchema: { type: "object", properties: { workspace: workspaceParam }, required: ["workspace"] },
|
|
20471
20344
|
annotations: { title: "\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uC5F0\uACB0 \uD655\uC778", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
20472
20345
|
}
|
|
20473
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 {
|
|
20353
|
+
...outcome,
|
|
20354
|
+
content: [
|
|
20355
|
+
...outcome.content,
|
|
20356
|
+
{ type: "text", text: "\uC774 \uACB0\uACFC\uB294 \uC55E\uC11C \uC2DC\uC791\uD55C \uC5F0\uACB0\uC758 \uACB0\uACFC\uB2E4. \uB2E4\uB978 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB97C \uC0C8\uB85C \uC5F0\uACB0\uD558\uB824\uB358 \uAC83\uC774\uBA74 workspace_connect\uB97C \uD55C \uBC88 \uB354 \uD638\uCD9C\uD55C\uB2E4." }
|
|
20357
|
+
]
|
|
20358
|
+
};
|
|
20359
|
+
}
|
|
20360
|
+
if (pendingConnect) {
|
|
20361
|
+
return json({ status: "waiting_for_approval", authorizationUrl: pendingConnect.url, note: "\uC544\uC9C1 \uC2B9\uC778\uC744 \uAE30\uB2E4\uB9AC\uB294 \uC911\uC785\uB2C8\uB2E4." });
|
|
20362
|
+
}
|
|
20363
|
+
const state = {};
|
|
20364
|
+
pendingConnect = state;
|
|
20365
|
+
let urlReady = () => {
|
|
20366
|
+
};
|
|
20367
|
+
const gotUrl = new Promise((resolve) => urlReady = resolve);
|
|
20368
|
+
const done = connect({
|
|
20369
|
+
description,
|
|
20370
|
+
onAuthorizationUrl: (url2) => {
|
|
20371
|
+
state.url = url2.href;
|
|
20372
|
+
urlReady();
|
|
20373
|
+
}
|
|
20374
|
+
}).then(
|
|
20375
|
+
async ({ entry, tools }) => {
|
|
20376
|
+
await onChanged().catch(() => {
|
|
20377
|
+
});
|
|
20378
|
+
state.outcome = json({ status: "connected", connected: entry, toolCount: tools.length });
|
|
20379
|
+
},
|
|
20380
|
+
(error2) => {
|
|
20381
|
+
state.outcome = errorResult(error2.message);
|
|
20382
|
+
}
|
|
20383
|
+
);
|
|
20384
|
+
await Promise.race([gotUrl, done]);
|
|
20385
|
+
if (state.outcome) {
|
|
20386
|
+
pendingConnect = void 0;
|
|
20387
|
+
return state.outcome;
|
|
20388
|
+
}
|
|
20389
|
+
return json({
|
|
20390
|
+
status: "waiting_for_approval",
|
|
20391
|
+
authorizationUrl: state.url,
|
|
20392
|
+
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."
|
|
20393
|
+
});
|
|
20394
|
+
}
|
|
20474
20395
|
var text = (value) => ({ content: [{ type: "text", text: value }] });
|
|
20475
20396
|
var json = (value) => text(JSON.stringify(value, null, 2));
|
|
20476
|
-
async function callManagementTool(name, args2,
|
|
20477
|
-
const
|
|
20397
|
+
async function callManagementTool(name, args2, onChanged) {
|
|
20398
|
+
const workspace = typeof args2.workspace === "string" ? args2.workspace : "";
|
|
20478
20399
|
try {
|
|
20479
20400
|
switch (name) {
|
|
20480
20401
|
case "workspace_list":
|
|
20481
20402
|
return json(await loadWorkspaces());
|
|
20482
|
-
case "workspace_connect":
|
|
20483
|
-
|
|
20484
|
-
const { entry, tools } = await connectWorkspace({
|
|
20485
|
-
alias,
|
|
20486
|
-
description: typeof args2.description === "string" ? args2.description : "",
|
|
20487
|
-
onAuthorizationUrl: () => {
|
|
20488
|
-
}
|
|
20489
|
-
});
|
|
20490
|
-
return json({
|
|
20491
|
-
connected: entry,
|
|
20492
|
-
toolCount: tools.length,
|
|
20493
|
-
note: "\uC0C8 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC758 Notion \uB3C4\uAD6C\uB294 \uC0C8 \uB300\uD654\uBD80\uD130 \uC4F8 \uC218 \uC788\uC2B5\uB2C8\uB2E4."
|
|
20494
|
-
});
|
|
20495
|
-
}
|
|
20403
|
+
case "workspace_connect":
|
|
20404
|
+
return await connectInBackground(typeof args2.description === "string" ? args2.description : "", onChanged);
|
|
20496
20405
|
case "workspace_disconnect": {
|
|
20497
|
-
if (!
|
|
20498
|
-
const {
|
|
20499
|
-
|
|
20500
|
-
await onDisconnected(alias);
|
|
20406
|
+
if (!workspace) return errorResult("workspace\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
|
|
20407
|
+
const { revoked } = await disconnectWorkspace(workspace);
|
|
20408
|
+
await onChanged(workspace);
|
|
20501
20409
|
return json({
|
|
20502
|
-
disconnected:
|
|
20410
|
+
disconnected: workspace,
|
|
20503
20411
|
revokedOnNotion: revoked,
|
|
20504
20412
|
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."
|
|
20505
20413
|
});
|
|
20506
20414
|
}
|
|
20507
|
-
case "workspace_rename": {
|
|
20508
|
-
if (!alias) return errorResult("alias\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
|
|
20509
|
-
const newAlias = typeof args2.new_alias === "string" ? args2.new_alias : void 0;
|
|
20510
|
-
const description = typeof args2.description === "string" ? args2.description : void 0;
|
|
20511
|
-
if (newAlias === void 0 && description === void 0) return errorResult("new_alias\uB098 description\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
|
|
20512
|
-
const entry = await renameWorkspace(alias, { newAlias, description });
|
|
20513
|
-
if (entry.alias !== alias) await onDisconnected(alias);
|
|
20514
|
-
return json({
|
|
20515
|
-
renamed: entry,
|
|
20516
|
-
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."
|
|
20517
|
-
});
|
|
20518
|
-
}
|
|
20519
20415
|
case "workspace_check":
|
|
20520
|
-
if (!
|
|
20521
|
-
return await checkWorkspace(
|
|
20416
|
+
if (!workspace) return errorResult("workspace\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
|
|
20417
|
+
return await checkWorkspace(workspace);
|
|
20522
20418
|
default:
|
|
20523
20419
|
return void 0;
|
|
20524
20420
|
}
|
|
@@ -20526,81 +20422,177 @@ async function callManagementTool(name, args2, onDisconnected) {
|
|
|
20526
20422
|
return errorResult(error2.message);
|
|
20527
20423
|
}
|
|
20528
20424
|
}
|
|
20529
|
-
async function checkWorkspace(
|
|
20530
|
-
const entry =
|
|
20531
|
-
if (!entry) return errorResult(`\uC5F0\uACB0\uB41C \uC801 \uC5C6\uB294 \uBCC4\uCE6D\uC785\uB2C8\uB2E4: ${alias}`);
|
|
20425
|
+
async function checkWorkspace(name, open2 = openWorkspace) {
|
|
20426
|
+
const entry = await findWorkspace(name);
|
|
20532
20427
|
let status;
|
|
20533
20428
|
let detail;
|
|
20534
20429
|
try {
|
|
20535
|
-
const session = await
|
|
20430
|
+
const session = await open2(name);
|
|
20536
20431
|
try {
|
|
20537
20432
|
const self = await session.getSelf();
|
|
20538
20433
|
const tools = await session.listTools();
|
|
20539
20434
|
const sameWorkspace = self.workspaceId === entry.notionWorkspaceId;
|
|
20540
20435
|
status = sameWorkspace ? "connected" : "error";
|
|
20541
|
-
detail = sameWorkspace ? {
|
|
20542
|
-
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.`
|
|
20543
|
-
};
|
|
20436
|
+
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.` };
|
|
20544
20437
|
} finally {
|
|
20545
20438
|
await session.close();
|
|
20546
20439
|
}
|
|
20547
20440
|
} catch (error2) {
|
|
20548
20441
|
const message = error2.message;
|
|
20549
|
-
|
|
20442
|
+
const signInDead = error2 instanceof InvalidGrantError || error2 instanceof UnauthorizedError || /저장된 인증/.test(message);
|
|
20443
|
+
status = signInDead ? "needs_reauth" : "error";
|
|
20550
20444
|
detail = { problem: message };
|
|
20551
20445
|
}
|
|
20552
|
-
await
|
|
20553
|
-
|
|
20554
|
-
|
|
20555
|
-
|
|
20556
|
-
|
|
20557
|
-
const result = json({
|
|
20446
|
+
await updateWorkspaces(
|
|
20447
|
+
(workspaces) => workspaces.map(
|
|
20448
|
+
(w) => w.notionWorkspaceId === entry.notionWorkspaceId ? { ...w, status, lastSuccessAt: status === "connected" ? (/* @__PURE__ */ new Date()).toISOString() : w.lastSuccessAt } : w
|
|
20449
|
+
)
|
|
20450
|
+
);
|
|
20451
|
+
const result = json({ workspace: name, status, ...detail });
|
|
20558
20452
|
return status === "connected" ? result : { ...result, isError: true };
|
|
20559
20453
|
}
|
|
20560
|
-
|
|
20561
|
-
|
|
20562
|
-
|
|
20454
|
+
|
|
20455
|
+
// src/upstream.ts
|
|
20456
|
+
import { mkdir as mkdir3, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
|
|
20457
|
+
import { join as join3 } from "node:path";
|
|
20458
|
+
var UpstreamPool = class {
|
|
20459
|
+
constructor(open2 = openWorkspace) {
|
|
20460
|
+
this.open = open2;
|
|
20461
|
+
}
|
|
20462
|
+
open;
|
|
20463
|
+
sessions = /* @__PURE__ */ new Map();
|
|
20464
|
+
session(alias) {
|
|
20465
|
+
const existing = this.sessions.get(alias);
|
|
20466
|
+
if (existing) return existing;
|
|
20467
|
+
const opened = this.open(alias);
|
|
20468
|
+
opened.catch(() => {
|
|
20469
|
+
if (this.sessions.get(alias) === opened) this.sessions.delete(alias);
|
|
20470
|
+
});
|
|
20471
|
+
this.sessions.set(alias, opened);
|
|
20472
|
+
return opened;
|
|
20473
|
+
}
|
|
20474
|
+
/**
|
|
20475
|
+
* An expired upstream MCP session gets a 404 and the request was not processed, so reopen and send it once more.
|
|
20476
|
+
* Other errors may come after the request was processed, so those are never resent.
|
|
20477
|
+
*/
|
|
20478
|
+
async callTool(alias, name, args2) {
|
|
20479
|
+
try {
|
|
20480
|
+
return await (await this.session(alias)).callTool(name, args2);
|
|
20481
|
+
} catch (error2) {
|
|
20482
|
+
if (!(error2 instanceof StreamableHTTPError && error2.code === 404)) throw error2;
|
|
20483
|
+
await this.reset(alias);
|
|
20484
|
+
return (await this.session(alias)).callTool(name, args2);
|
|
20485
|
+
}
|
|
20486
|
+
}
|
|
20487
|
+
async reset(alias) {
|
|
20488
|
+
const pending = this.sessions.get(alias);
|
|
20489
|
+
this.sessions.delete(alias);
|
|
20490
|
+
await pending?.then((s) => s.close()).catch(() => {
|
|
20491
|
+
});
|
|
20492
|
+
}
|
|
20493
|
+
async closeAll() {
|
|
20494
|
+
await Promise.all([...this.sessions.keys()].map((alias) => this.reset(alias)));
|
|
20495
|
+
}
|
|
20496
|
+
};
|
|
20497
|
+
var LIVE_TIMEOUT_MS = 15e3;
|
|
20498
|
+
async function loadCatalogs(workspaces, pool) {
|
|
20499
|
+
const results = await Promise.all(
|
|
20500
|
+
workspaces.map(async (w) => {
|
|
20501
|
+
try {
|
|
20502
|
+
const catalog = await withTimeout(fetchLive(w.name, pool), LIVE_TIMEOUT_MS);
|
|
20503
|
+
await writeCache(catalog, w.notionWorkspaceId);
|
|
20504
|
+
return catalog;
|
|
20505
|
+
} catch (error2) {
|
|
20506
|
+
const cached2 = await readCache(w.notionWorkspaceId);
|
|
20507
|
+
return cached2 ? { ...cached2, alias: w.name } : { alias: w.name, reason: error2.message };
|
|
20508
|
+
}
|
|
20509
|
+
})
|
|
20510
|
+
);
|
|
20511
|
+
return {
|
|
20512
|
+
catalogs: results.filter((r) => "tools" in r),
|
|
20513
|
+
failures: results.filter((r) => "reason" in r)
|
|
20514
|
+
};
|
|
20515
|
+
}
|
|
20516
|
+
async function fetchLive(alias, pool) {
|
|
20517
|
+
const session = await pool.session(alias);
|
|
20518
|
+
return {
|
|
20519
|
+
alias,
|
|
20520
|
+
tools: await session.listTools(),
|
|
20521
|
+
instructions: session.instructions
|
|
20522
|
+
};
|
|
20523
|
+
}
|
|
20524
|
+
async function writeCache(catalog, workspaceId) {
|
|
20525
|
+
await mkdir3(join3(configDir(), "cache"), { recursive: true, mode: 448 });
|
|
20526
|
+
const tmp = `${cachePath(workspaceId)}.${process.pid}.tmp`;
|
|
20527
|
+
await writeFile2(tmp, JSON.stringify(catalog), { mode: 384 });
|
|
20528
|
+
await rename2(tmp, cachePath(workspaceId));
|
|
20529
|
+
}
|
|
20530
|
+
async function readCache(workspaceId) {
|
|
20531
|
+
try {
|
|
20532
|
+
return JSON.parse(await readFile2(cachePath(workspaceId), "utf8"));
|
|
20533
|
+
} catch {
|
|
20534
|
+
return void 0;
|
|
20535
|
+
}
|
|
20536
|
+
}
|
|
20537
|
+
function withTimeout(promise, ms) {
|
|
20538
|
+
let timer;
|
|
20539
|
+
return Promise.race([
|
|
20540
|
+
promise,
|
|
20541
|
+
new Promise((_, reject) => {
|
|
20542
|
+
timer = setTimeout(() => reject(new Error(`${ms / 1e3}\uCD08 \uC548\uC5D0 \uC751\uB2F5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.`)), ms);
|
|
20543
|
+
})
|
|
20544
|
+
]).finally(() => clearTimeout(timer));
|
|
20563
20545
|
}
|
|
20564
20546
|
|
|
20565
20547
|
// src/server.ts
|
|
20566
20548
|
var log = (message) => console.error(`[${SERVER_NAME}] ${message}`);
|
|
20567
20549
|
async function createServer2() {
|
|
20568
|
-
const workspaces = await loadWorkspaces();
|
|
20569
20550
|
const pool = new UpstreamPool();
|
|
20551
|
+
const workspaces = await loadWorkspaces();
|
|
20570
20552
|
const { catalogs, failures } = await loadCatalogs(workspaces, pool);
|
|
20571
|
-
|
|
20572
|
-
const catalog = mergeCatalogs(catalogs, log);
|
|
20573
|
-
const searchAll = searchAllTool(catalog);
|
|
20553
|
+
let current = toolsFor(catalogs, failures);
|
|
20574
20554
|
const server = new Server(
|
|
20575
20555
|
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
20576
|
-
{ capabilities: { tools: {} }, instructions: buildInstructions(workspaces, catalogs, failures) }
|
|
20556
|
+
{ capabilities: { tools: { listChanged: true } }, instructions: buildInstructions(workspaces, catalogs, failures) }
|
|
20577
20557
|
);
|
|
20558
|
+
const refresh = async (staleAlias) => {
|
|
20559
|
+
if (staleAlias) await pool.reset(staleAlias);
|
|
20560
|
+
const latest = await loadCatalogs(await loadWorkspaces(), pool);
|
|
20561
|
+
current = toolsFor(latest.catalogs, latest.failures);
|
|
20562
|
+
await server.sendToolListChanged();
|
|
20563
|
+
};
|
|
20578
20564
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
20579
|
-
tools: [...managementTools, ...searchAll ? [searchAll] : [], ...catalog.tools]
|
|
20565
|
+
tools: [...managementTools, ...current.searchAll ? [current.searchAll] : [], ...current.catalog.tools]
|
|
20580
20566
|
}));
|
|
20581
20567
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
20582
20568
|
const { name, arguments: args2 } = request.params;
|
|
20569
|
+
const { catalog, searchAll } = current;
|
|
20583
20570
|
const call = (alias, tool, rest) => pool.callTool(alias, tool, rest);
|
|
20584
20571
|
if (searchAll && name === SEARCH_ALL) return callSearchAll(catalog, args2, call);
|
|
20585
|
-
const managed = await callManagementTool(name, args2 ?? {},
|
|
20572
|
+
const managed = await callManagementTool(name, args2 ?? {}, refresh);
|
|
20586
20573
|
return managed ?? callProxiedTool(catalog, name, args2, call);
|
|
20587
20574
|
});
|
|
20588
20575
|
server.onclose = () => void pool.closeAll();
|
|
20589
20576
|
return server;
|
|
20590
20577
|
}
|
|
20578
|
+
function toolsFor(catalogs, failures) {
|
|
20579
|
+
for (const f of failures) log(`${f.alias}: \uB3C4\uAD6C \uBAA9\uB85D\uC744 \uBC1B\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4 (${f.reason})`);
|
|
20580
|
+
const catalog = mergeCatalogs(catalogs, log);
|
|
20581
|
+
return { catalog, searchAll: searchAllTool(catalog) };
|
|
20582
|
+
}
|
|
20591
20583
|
function buildInstructions(workspaces, catalogs, failures) {
|
|
20592
20584
|
const failed = new Map(failures.map((f) => [f.alias, f.reason]));
|
|
20593
20585
|
const lines = workspaces.length === 0 ? ["- (\uC5C6\uC74C) workspace_connect \uB3C4\uAD6C\uB85C \uBA3C\uC800 \uC5F0\uACB0\uD574\uC57C \uD55C\uB2E4."] : workspaces.map((w) => {
|
|
20594
|
-
const label = `-
|
|
20595
|
-
return failed.has(w.
|
|
20586
|
+
const label = `- "${w.name}"${w.description ? `: ${w.description}` : ""}`;
|
|
20587
|
+
return failed.has(w.name) ? `${label} - \uC9C0\uAE08 \uC5F0\uACB0\uD560 \uC218 \uC5C6\uC74C: ${failed.get(w.name)}` : label;
|
|
20596
20588
|
});
|
|
20597
20589
|
const sections = [
|
|
20598
20590
|
[
|
|
20599
20591
|
"\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.",
|
|
20600
|
-
`\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 \
|
|
20592
|
+
`\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.`,
|
|
20601
20593
|
"\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.",
|
|
20602
20594
|
`\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).`,
|
|
20603
|
-
"\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uAD00\uB9AC\uB294 workspace_list, workspace_connect, workspace_disconnect,
|
|
20595
|
+
"\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uAD00\uB9AC\uB294 workspace_list, workspace_connect, workspace_disconnect, workspace_check \uB3C4\uAD6C\uB85C \uD55C\uB2E4.",
|
|
20604
20596
|
"",
|
|
20605
20597
|
"\uC5F0\uACB0\uB41C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4:",
|
|
20606
20598
|
...lines
|
|
@@ -20627,10 +20619,10 @@ var [command, ...args] = process.argv.slice(2);
|
|
|
20627
20619
|
var USAGE = `\uC0AC\uC6A9\uBC95:
|
|
20628
20620
|
multi-workspace-for-notion MCP \uC11C\uBC84(stdio)\uB85C \uC2E4\uD589
|
|
20629
20621
|
multi-workspace-for-notion list \uC5F0\uACB0\uB41C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uBAA9\uB85D
|
|
20630
|
-
multi-workspace-for-notion add
|
|
20631
|
-
multi-workspace-for-notion tools <\
|
|
20632
|
-
multi-workspace-for-notion
|
|
20633
|
-
|
|
20622
|
+
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)
|
|
20623
|
+
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
|
|
20624
|
+
multi-workspace-for-notion remove <\uC774\uB984> \uC5F0\uACB0 \uD574\uC81C\uC640 \uD1A0\uD070 \uC0AD\uC81C
|
|
20625
|
+
(\uC774\uB984\uC5D0 \uACF5\uBC31\uC774 \uC788\uC73C\uBA74 \uB530\uC634\uD45C\uB85C \uAC10\uC2FC\uB2E4: remove "\uD5C8 \uC740\uC11C\uC758 Notion")`;
|
|
20634
20626
|
try {
|
|
20635
20627
|
switch (command) {
|
|
20636
20628
|
case void 0: {
|
|
@@ -20644,7 +20636,7 @@ try {
|
|
|
20644
20636
|
console.log("\uC5F0\uACB0\uB41C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
20645
20637
|
}
|
|
20646
20638
|
for (const w of workspaces) {
|
|
20647
|
-
console.log(`${w.
|
|
20639
|
+
console.log(`${w.name} ${w.status} ${w.description}`);
|
|
20648
20640
|
}
|
|
20649
20641
|
break;
|
|
20650
20642
|
}
|
|
@@ -20653,11 +20645,8 @@ try {
|
|
|
20653
20645
|
break;
|
|
20654
20646
|
}
|
|
20655
20647
|
case "add": {
|
|
20656
|
-
const [alias, ...descriptionWords] = args;
|
|
20657
|
-
if (!alias) throw new UsageError();
|
|
20658
20648
|
const { entry, tools } = await connectWorkspace({
|
|
20659
|
-
|
|
20660
|
-
description: descriptionWords.join(" "),
|
|
20649
|
+
description: args.join(" "),
|
|
20661
20650
|
onAuthorizationUrl: (url2) => console.log(
|
|
20662
20651
|
`\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.
|
|
20663
20652
|
\uBE0C\uB77C\uC6B0\uC800\uAC00 \uC5F4\uB9AC\uC9C0 \uC54A\uC73C\uBA74 \uC774 \uC8FC\uC18C\uB85C \uB4E4\uC5B4\uAC00\uC138\uC694:
|
|
@@ -20665,38 +20654,30 @@ ${url2.href}
|
|
|
20665
20654
|
`
|
|
20666
20655
|
)
|
|
20667
20656
|
});
|
|
20668
|
-
console.log(`\uC5F0\uACB0\uD588\uC2B5\uB2C8\uB2E4:
|
|
20657
|
+
console.log(`\uC5F0\uACB0\uD588\uC2B5\uB2C8\uB2E4: "${entry.name}" (${entry.notionWorkspaceId})`);
|
|
20669
20658
|
console.log(`\uACF5\uC2DD \uB3C4\uAD6C ${tools.length}\uAC1C\uB97C \uBC1B\uC558\uC2B5\uB2C8\uB2E4.`);
|
|
20670
20659
|
break;
|
|
20671
20660
|
}
|
|
20672
20661
|
case "tools": {
|
|
20673
|
-
const [
|
|
20674
|
-
if (!
|
|
20675
|
-
const session = await openWorkspace(
|
|
20662
|
+
const [name] = args;
|
|
20663
|
+
if (!name) throw new UsageError();
|
|
20664
|
+
const session = await openWorkspace(name);
|
|
20676
20665
|
try {
|
|
20677
20666
|
const self = await session.getSelf();
|
|
20678
20667
|
const tools = await session.listTools();
|
|
20679
|
-
console.log(
|
|
20668
|
+
console.log(`"${name}" \u2192 Notion "${self.workspaceName}" (${self.workspaceId}), \uB3C4\uAD6C ${tools.length}\uAC1C`);
|
|
20680
20669
|
for (const tool of tools) console.log(` ${tool.name}`);
|
|
20681
20670
|
} finally {
|
|
20682
20671
|
await session.close();
|
|
20683
20672
|
}
|
|
20684
20673
|
break;
|
|
20685
20674
|
}
|
|
20686
|
-
case "rename": {
|
|
20687
|
-
const [alias, newAlias] = args;
|
|
20688
|
-
if (!alias || !newAlias) throw new UsageError();
|
|
20689
|
-
await renameWorkspace(alias, { newAlias });
|
|
20690
|
-
console.log(`\uBCC4\uCE6D\uC744 \uBC14\uAFE8\uC2B5\uB2C8\uB2E4: ${alias} \u2192 ${newAlias}`);
|
|
20691
|
-
break;
|
|
20692
|
-
}
|
|
20693
20675
|
case "remove": {
|
|
20694
|
-
const [
|
|
20695
|
-
if (!
|
|
20696
|
-
const {
|
|
20697
|
-
if (
|
|
20698
|
-
else
|
|
20699
|
-
else console.log(`\uC5F0\uACB0\uC744 \uD574\uC81C\uD588\uC2B5\uB2C8\uB2E4: ${alias}
|
|
20676
|
+
const [name] = args;
|
|
20677
|
+
if (!name) throw new UsageError();
|
|
20678
|
+
const { revoked } = await disconnectWorkspace(name);
|
|
20679
|
+
if (revoked) console.log(`\uC5F0\uACB0\uC744 \uD574\uC81C\uD588\uC2B5\uB2C8\uB2E4: ${name} (Notion \uCABD \uD1A0\uD070\uB3C4 \uD574\uC81C)`);
|
|
20680
|
+
else console.log(`\uC5F0\uACB0\uC744 \uD574\uC81C\uD588\uC2B5\uB2C8\uB2E4: ${name}
|
|
20700
20681
|
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.`);
|
|
20701
20682
|
break;
|
|
20702
20683
|
}
|
|
@@ -20715,8 +20696,8 @@ ${USAGE}` : USAGE);
|
|
|
20715
20696
|
process.exitCode = 1;
|
|
20716
20697
|
}
|
|
20717
20698
|
function sessionStatus(workspaces) {
|
|
20718
|
-
if (workspaces.length === 0) return "Notion: \uC5F0\uACB0\uB41C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. /multi-workspace:workspace-add
|
|
20719
|
-
const names = workspaces.map((w) =>
|
|
20720
|
-
const broken = workspaces.filter((w) => w.status !== "connected").map((w) => w.
|
|
20699
|
+
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.";
|
|
20700
|
+
const names = workspaces.map((w) => w.name).join(", ");
|
|
20701
|
+
const broken = workspaces.filter((w) => w.status !== "connected").map((w) => w.name);
|
|
20721
20702
|
return broken.length === 0 ? `Notion: ${names}` : `Notion: ${names} \xB7 \uD655\uC778 \uD544\uC694: ${broken.join(", ")} \u2192 /multi-workspace:workspace-check`;
|
|
20722
20703
|
}
|