multi-workspace-for-notion 0.1.0 → 0.1.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 +5 -3
- package/README.md +5 -3
- package/package.json +1 -1
- package/plugin/server.mjs +37 -32
package/README.ko.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<p align="center"><em>Claude Code 하나로 모든 Notion 워크스페이스를. 공식 도구는 그대로.</em></p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<img src="https://img.shields.io/badge/version-0.1.
|
|
6
|
+
<img src="https://img.shields.io/badge/version-0.1.1-111111?style=flat-square" alt="Version 0.1.1">
|
|
7
7
|
<img src="https://img.shields.io/badge/Claude%20Code-plugin-111111?style=flat-square" alt="Claude Code plugin">
|
|
8
8
|
<img src="https://img.shields.io/badge/platform-macOS-111111?style=flat-square" alt="macOS">
|
|
9
9
|
<img src="https://img.shields.io/badge/license-MIT-111111?style=flat-square" alt="MIT license">
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
Claude Code에서 여러 Notion 워크스페이스를 동시에 쓸 수 있게 해 줍니다. 공식 Notion MCP 서버의 도구를 하나도 빠짐없이 그대로 씁니다.
|
|
17
17
|
|
|
18
|
-
> **상태: 개발 초기 (v0.1.
|
|
18
|
+
> **상태: 개발 초기 (v0.1.1).** 워크스페이스 연결과 공식 도구 전달은 동작하며, 실제 워크스페이스 두 개를 Claude Code에 연결해 시험했습니다. 계획한 기능 중 아직 없는 것이 여럿 있습니다. [로드맵](#로드맵)과 [알려진 제약](#알려진-제약)을 참고하세요.
|
|
19
19
|
>
|
|
20
20
|
> 비공식 프로젝트입니다. Notion Labs, Inc.와 관계가 없고 Notion의 승인을 받지 않았습니다.
|
|
21
21
|
|
|
@@ -106,6 +106,8 @@ claude.ai Notion 커넥터도 켜져 있으면 Claude에게 Notion 도구가 두
|
|
|
106
106
|
|
|
107
107
|
## Claude Desktop 확장(`.mcpb`, 아직 시험 전)
|
|
108
108
|
|
|
109
|
+
[최신 릴리스](https://github.com/heoeunseo08/multi-workspace-for-notion/releases/latest)에서 `multi-workspace-for-notion.mcpb`를 받거나, 직접 빌드합니다.
|
|
110
|
+
|
|
109
111
|
```bash
|
|
110
112
|
npm install
|
|
111
113
|
npm run pack:mcpb # multi-workspace-for-notion.mcpb 생성
|
|
@@ -160,7 +162,7 @@ claude mcp add --scope user multi-workspace -- node /absolute/path/to/multi-work
|
|
|
160
162
|
| 도구 목록과 안내문 캐시 | `~/.config/multi-workspace-for-notion/cache/` |
|
|
161
163
|
| 토큰 저장용 잠금 파일 | `~/.config/multi-workspace-for-notion/locks/` |
|
|
162
164
|
|
|
163
|
-
다른 폴더를 쓰려면 `MWN_CONFIG_DIR`을 설정하세요. 파일은 본인만 읽고 쓸 수 있는 권한으로 만들고, 토큰은 절대 들어가지 않습니다.
|
|
165
|
+
다른 폴더를 쓰려면 `MWN_CONFIG_DIR`을 설정하세요. 본인만 접근할 수 있는 폴더로 지정하세요(기본값은 `0700`으로 만듭니다). 파일은 본인만 읽고 쓸 수 있는 권한으로 만들고, 토큰은 절대 들어가지 않습니다.
|
|
164
166
|
|
|
165
167
|
## 보안
|
|
166
168
|
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<p align="center"><em>One Claude Code, every Notion workspace. The official tools, untouched.</em></p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<img src="https://img.shields.io/badge/version-0.1.
|
|
6
|
+
<img src="https://img.shields.io/badge/version-0.1.1-111111?style=flat-square" alt="Version 0.1.1">
|
|
7
7
|
<img src="https://img.shields.io/badge/Claude%20Code-plugin-111111?style=flat-square" alt="Claude Code plugin">
|
|
8
8
|
<img src="https://img.shields.io/badge/platform-macOS-111111?style=flat-square" alt="macOS">
|
|
9
9
|
<img src="https://img.shields.io/badge/license-MIT-111111?style=flat-square" alt="MIT license">
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
Use several Notion workspaces at the same time from Claude Code, with every tool the official Notion MCP server offers.
|
|
17
17
|
|
|
18
|
-
> **Status: early development (v0.1.
|
|
18
|
+
> **Status: early development (v0.1.1).** Connecting workspaces and proxying the official tools work and have been tested with two real workspaces in Claude Code. Several planned pieces are still missing; see [Roadmap](#roadmap) and [Known limitations](#known-limitations).
|
|
19
19
|
>
|
|
20
20
|
> This is an unofficial project. It is not affiliated with or endorsed by Notion Labs, Inc.
|
|
21
21
|
|
|
@@ -106,6 +106,8 @@ Each new session also starts with a one-line status, such as `Notion: personal(M
|
|
|
106
106
|
|
|
107
107
|
## Claude Desktop extension (`.mcpb`, not tested yet)
|
|
108
108
|
|
|
109
|
+
Download `multi-workspace-for-notion.mcpb` from the [latest release](https://github.com/heoeunseo08/multi-workspace-for-notion/releases/latest), or build it yourself:
|
|
110
|
+
|
|
109
111
|
```bash
|
|
110
112
|
npm install
|
|
111
113
|
npm run pack:mcpb # writes multi-workspace-for-notion.mcpb
|
|
@@ -162,7 +164,7 @@ CLI messages are currently in Korean.
|
|
|
162
164
|
| Cached tool lists and instructions | `~/.config/multi-workspace-for-notion/cache/` |
|
|
163
165
|
| Lock files for saving tokens | `~/.config/multi-workspace-for-notion/locks/` |
|
|
164
166
|
|
|
165
|
-
Set `MWN_CONFIG_DIR` to use a different directory. Files are written with owner-only permissions and never contain tokens.
|
|
167
|
+
Set `MWN_CONFIG_DIR` to use a different directory; point it at a private, owner-only path (the default is created with `0700`). Files are written with owner-only permissions and never contain tokens.
|
|
166
168
|
|
|
167
169
|
## Security
|
|
168
170
|
|
package/package.json
CHANGED
package/plugin/server.mjs
CHANGED
|
@@ -16635,6 +16635,7 @@ async function registerClient(authorizationServerUrl, { metadata, clientMetadata
|
|
|
16635
16635
|
import { createServer } from "node:http";
|
|
16636
16636
|
async function startCallbackServer() {
|
|
16637
16637
|
let deliver;
|
|
16638
|
+
let expectedState;
|
|
16638
16639
|
const received = new Promise((resolve) => deliver = resolve);
|
|
16639
16640
|
const handler = (req, res) => {
|
|
16640
16641
|
const url2 = new URL(req.url ?? "/", "http://localhost");
|
|
@@ -16645,12 +16646,18 @@ async function startCallbackServer() {
|
|
|
16645
16646
|
}
|
|
16646
16647
|
const error2 = url2.searchParams.get("error");
|
|
16647
16648
|
const code = url2.searchParams.get("code");
|
|
16649
|
+
const state = url2.searchParams.get("state");
|
|
16650
|
+
if (!error2 && !code || expectedState === void 0 || state !== expectedState) {
|
|
16651
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
16652
|
+
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>`);
|
|
16653
|
+
return;
|
|
16654
|
+
}
|
|
16648
16655
|
const ok = !error2 && code;
|
|
16649
16656
|
res.writeHead(ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
16650
16657
|
res.end(
|
|
16651
16658
|
`<!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
16659
|
);
|
|
16653
|
-
deliver?.(ok ? { code, state
|
|
16660
|
+
deliver?.(ok ? { code, state } : { error: error2 ?? "code\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4" });
|
|
16654
16661
|
deliver = void 0;
|
|
16655
16662
|
};
|
|
16656
16663
|
const servers = [createServer(handler)];
|
|
@@ -16660,7 +16667,8 @@ async function startCallbackServer() {
|
|
|
16660
16667
|
if (await listen(ipv62, port, "::1").then(() => true, () => false)) servers.push(ipv62);
|
|
16661
16668
|
return {
|
|
16662
16669
|
redirectUrl: `http://localhost:${port}/callback`,
|
|
16663
|
-
async waitForCode(
|
|
16670
|
+
async waitForCode(expected, timeoutMs) {
|
|
16671
|
+
expectedState = expected;
|
|
16664
16672
|
let timer;
|
|
16665
16673
|
const timeout = new Promise((_, reject) => {
|
|
16666
16674
|
timer = setTimeout(() => reject(new Error("\uC778\uC99D \uB300\uAE30 \uC2DC\uAC04\uC774 \uC9C0\uB0AC\uC2B5\uB2C8\uB2E4.")), timeoutMs);
|
|
@@ -16668,7 +16676,6 @@ async function startCallbackServer() {
|
|
|
16668
16676
|
try {
|
|
16669
16677
|
const result = await Promise.race([received, timeout]);
|
|
16670
16678
|
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
16679
|
return result.code;
|
|
16673
16680
|
} finally {
|
|
16674
16681
|
clearTimeout(timer);
|
|
@@ -19238,7 +19245,7 @@ var StreamableHTTPClientTransport = class {
|
|
|
19238
19245
|
|
|
19239
19246
|
// src/meta.ts
|
|
19240
19247
|
var SERVER_NAME = "multi-workspace-for-notion";
|
|
19241
|
-
var SERVER_VERSION = "0.1.
|
|
19248
|
+
var SERVER_VERSION = "0.1.1";
|
|
19242
19249
|
|
|
19243
19250
|
// src/notion.ts
|
|
19244
19251
|
var NOTION_MCP_URL = new URL("https://mcp.notion.com/mcp");
|
|
@@ -19268,7 +19275,7 @@ var NotionSession = class _NotionSession {
|
|
|
19268
19275
|
async callTool(name, args2) {
|
|
19269
19276
|
return await this.client.callTool({ name, arguments: args2 });
|
|
19270
19277
|
}
|
|
19271
|
-
/** notion-get-self
|
|
19278
|
+
/** Asks notion-get-self which workspace this token actually belongs to. */
|
|
19272
19279
|
async getSelf() {
|
|
19273
19280
|
const result = await this.client.callTool({ name: "notion-get-self", arguments: {} });
|
|
19274
19281
|
const text2 = result.content.filter((part) => part.type === "text").map((part) => part.text ?? "").join("\n");
|
|
@@ -19351,13 +19358,13 @@ var WorkspaceAuthProvider = class _WorkspaceAuthProvider {
|
|
|
19351
19358
|
onRedirect;
|
|
19352
19359
|
verifier;
|
|
19353
19360
|
expectedState;
|
|
19354
|
-
/**
|
|
19361
|
+
/** The keychain value was seen or saved. If it disappears later, the workspace was disconnected or renamed. */
|
|
19355
19362
|
persisted = false;
|
|
19356
|
-
/**
|
|
19363
|
+
/** For a new connection. redirectUrl is the callback server's address. */
|
|
19357
19364
|
static forNewConnection(alias, redirectUrl, onRedirect) {
|
|
19358
19365
|
return new _WorkspaceAuthProvider(credentialAccount(alias), { redirectUrl }, onRedirect);
|
|
19359
19366
|
}
|
|
19360
|
-
/**
|
|
19367
|
+
/** For a connected workspace. Undefined when nothing is stored. */
|
|
19361
19368
|
static async forExisting(alias) {
|
|
19362
19369
|
const raw = await keychain.get(credentialAccount(alias));
|
|
19363
19370
|
if (raw === void 0) return void 0;
|
|
@@ -19381,7 +19388,7 @@ var WorkspaceAuthProvider = class _WorkspaceAuthProvider {
|
|
|
19381
19388
|
this.expectedState = randomBytes2(24).toString("base64url");
|
|
19382
19389
|
return this.expectedState;
|
|
19383
19390
|
}
|
|
19384
|
-
/**
|
|
19391
|
+
/** The state the callback must echo back. Set once auth() has started an authorization. */
|
|
19385
19392
|
get pendingState() {
|
|
19386
19393
|
return this.expectedState;
|
|
19387
19394
|
}
|
|
@@ -19392,8 +19399,8 @@ var WorkspaceAuthProvider = class _WorkspaceAuthProvider {
|
|
|
19392
19399
|
await this.update((s) => s.clientInformation = clientInformation);
|
|
19393
19400
|
}
|
|
19394
19401
|
/**
|
|
19395
|
-
* Claude
|
|
19396
|
-
*
|
|
19402
|
+
* Each Claude session runs its own server process, so re-read the keychain every time to pick up tokens another
|
|
19403
|
+
* process refreshed. Refresh tokens are single-use: a stale one from memory fails with invalid_grant.
|
|
19397
19404
|
*/
|
|
19398
19405
|
async tokens() {
|
|
19399
19406
|
await this.reload();
|
|
@@ -19411,7 +19418,7 @@ var WorkspaceAuthProvider = class _WorkspaceAuthProvider {
|
|
|
19411
19418
|
}
|
|
19412
19419
|
await this.onRedirect(authorizationUrl);
|
|
19413
19420
|
}
|
|
19414
|
-
// PKCE verifier
|
|
19421
|
+
// The PKCE verifier lives only for one authorization, so it stays in memory.
|
|
19415
19422
|
saveCodeVerifier(codeVerifier) {
|
|
19416
19423
|
this.verifier = codeVerifier;
|
|
19417
19424
|
}
|
|
@@ -19446,9 +19453,10 @@ var WorkspaceAuthProvider = class _WorkspaceAuthProvider {
|
|
|
19446
19453
|
if (raw !== void 0) this.stored = JSON.parse(raw);
|
|
19447
19454
|
}
|
|
19448
19455
|
/**
|
|
19449
|
-
*
|
|
19450
|
-
*
|
|
19451
|
-
*
|
|
19456
|
+
* Writing the in-memory copy wholesale would overwrite newer tokens another process saved meanwhile.
|
|
19457
|
+
* So take the lock, re-read the keychain and change only the given fields.
|
|
19458
|
+
* If the value vanished because another process disconnected or renamed the workspace, write nothing,
|
|
19459
|
+
* so deleted tokens stay deleted.
|
|
19452
19460
|
*/
|
|
19453
19461
|
async update(change) {
|
|
19454
19462
|
await withFileLock(this.account, async () => {
|
|
@@ -19500,20 +19508,18 @@ var UpstreamPool = class {
|
|
|
19500
19508
|
open;
|
|
19501
19509
|
sessions = /* @__PURE__ */ new Map();
|
|
19502
19510
|
session(alias) {
|
|
19503
|
-
|
|
19504
|
-
if (
|
|
19505
|
-
|
|
19506
|
-
|
|
19507
|
-
|
|
19508
|
-
|
|
19509
|
-
|
|
19510
|
-
|
|
19511
|
-
}
|
|
19512
|
-
return pending;
|
|
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;
|
|
19513
19519
|
}
|
|
19514
19520
|
/**
|
|
19515
|
-
*
|
|
19516
|
-
*
|
|
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.
|
|
19517
19523
|
*/
|
|
19518
19524
|
async callTool(alias, name, args2) {
|
|
19519
19525
|
try {
|
|
@@ -19559,7 +19565,6 @@ async function fetchLive(alias, pool) {
|
|
|
19559
19565
|
alias,
|
|
19560
19566
|
tools: await session.listTools(),
|
|
19561
19567
|
instructions: session.instructions,
|
|
19562
|
-
source: "live",
|
|
19563
19568
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
19564
19569
|
};
|
|
19565
19570
|
}
|
|
@@ -19572,8 +19577,7 @@ async function writeCache(catalog) {
|
|
|
19572
19577
|
}
|
|
19573
19578
|
async function readCache(alias) {
|
|
19574
19579
|
try {
|
|
19575
|
-
|
|
19576
|
-
return { ...cached2, source: "cache" };
|
|
19580
|
+
return JSON.parse(await readFile2(cachePath(alias), "utf8"));
|
|
19577
19581
|
} catch {
|
|
19578
19582
|
return void 0;
|
|
19579
19583
|
}
|
|
@@ -19641,6 +19645,7 @@ async function openWorkspace(alias) {
|
|
|
19641
19645
|
return NotionSession.open(provider);
|
|
19642
19646
|
}
|
|
19643
19647
|
async function disconnectWorkspace(alias) {
|
|
19648
|
+
assertAlias(alias);
|
|
19644
19649
|
const workspaces = await loadWorkspaces();
|
|
19645
19650
|
const raw = await withFileLock(credentialAccount(alias), async () => {
|
|
19646
19651
|
const found = await keychain.get(credentialAccount(alias));
|
|
@@ -20349,8 +20354,7 @@ async function callProxiedTool(catalog, name, args2, call) {
|
|
|
20349
20354
|
}
|
|
20350
20355
|
const others = available.filter((alias) => alias !== workspace);
|
|
20351
20356
|
if (!isNotFound(result) || others.length === 0) return result;
|
|
20352
|
-
|
|
20353
|
-
if (!readOnly) {
|
|
20357
|
+
if (!FAN_OUT_ON_NOT_FOUND.has(name)) {
|
|
20354
20358
|
return withNote(
|
|
20355
20359
|
result,
|
|
20356
20360
|
`[${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 +20368,7 @@ async function callProxiedTool(catalog, name, args2, call) {
|
|
|
20364
20368
|
`[${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
20369
|
);
|
|
20366
20370
|
}
|
|
20371
|
+
var FAN_OUT_ON_NOT_FOUND = /* @__PURE__ */ new Set(["notion-fetch"]);
|
|
20367
20372
|
var isNotFound = (result) => result.isError === true && result.content.some((c) => c.type === "text" && c.text.includes("object_not_found"));
|
|
20368
20373
|
var withNote = (result, note) => ({
|
|
20369
20374
|
...result,
|