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 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.0-111111?style=flat-square" alt="Version 0.1.0">
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.0).** 워크스페이스 연결과 공식 도구 전달은 동작하며, 실제 워크스페이스 두 개를 Claude Code에 연결해 시험했습니다. 계획한 기능 중 아직 없는 것이 여럿 있습니다. [로드맵](#로드맵)과 [알려진 제약](#알려진-제약)을 참고하세요.
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.0-111111?style=flat-square" alt="Version 0.1.0">
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.0).** 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).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multi-workspace-for-notion",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "scripts": {
5
5
  "dev": "tsx src/index.ts",
6
6
  "typecheck": "tsc --noEmit",
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: url2.searchParams.get("state") } : { error: error2 ?? "code\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4" });
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(expectedState, timeoutMs) {
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.0";
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
- /** 새로 연결할 때. redirectUrl은 콜백 서버의 주소다. */
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
- /** 이미 연결된 워크스페이스를 쓸 때. 저장된 토큰이 없으면 undefined. */
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
- /** 콜백 서버가 비교할 state. redirectToAuthorization() 이후에만 값이 있다. */
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
- * 메모리의 옛 리프레시 토큰을 쓰면, 한 번만 쓸 수 있는 토큰이라 invalid_grant가 난다.
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
- 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;
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
- * 업스트림 MCP 세션이 만료되면 서버가 404를 돌려준다. 그 경우 요청이 처리되지 않았으므로
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
- const cached2 = JSON.parse(await readFile2(cachePath(alias), "utf8"));
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
- const readOnly = catalog.tools.find((t) => t.name === name)?.annotations?.readOnlyHint === true;
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,