@modootoday/datalab-extension-mcp 1.8.7 → 1.8.9

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.
package/README.en.md CHANGED
@@ -55,10 +55,10 @@ Avoid sharing a screenshot or the copied setup. If it may have leaked, press **[
55
55
  One command finds **every** AI app on your computer and connects them automatically. Nothing to do per app.
56
56
 
57
57
  1. **Open a terminal** — on Windows type `cmd` in the search box → "Command Prompt"; on Mac press `⌘+Space` and type `Terminal`; on Linux open your terminal app (often `Ctrl+Alt+T`).
58
- 2. **Paste this command and press Enter.** (`@1.8.7` is the pinned version, so the connector never changes behind your back.)
58
+ 2. **Paste this command and press Enter.** (`@1.8.9` is the pinned version, so the connector never changes behind your back.)
59
59
 
60
60
  ```
61
- npx -y @modootoday/datalab-extension-mcp@1.8.7 install
61
+ npx -y @modootoday/datalab-extension-mcp@1.8.9 install
62
62
  ```
63
63
 
64
64
  Lines of English scrolling by is normal. This only downloads and runs the connector — it touches nothing else.
@@ -252,7 +252,7 @@ Start it again after a full quit and it picks the tools up. If they still aren't
252
252
  - **When the connection disappears** — run the very command you used at the start once more; it safely re-registers. (Some AI apps clean up their settings file and drop the connection.)
253
253
 
254
254
  ```
255
- npx -y @modootoday/datalab-extension-mcp@1.8.7 install
255
+ npx -y @modootoday/datalab-extension-mcp@1.8.9 install
256
256
  ```
257
257
 
258
258
  - **`'npx' is not recognized...`** — Node.js isn't installed. The "No Node.js?" one-liner in Step 2 is the simplest fix.
@@ -282,7 +282,7 @@ Start it again after a full quit and it picks the tools up. If they still aren't
282
282
  1. Run this in a terminal (it removes the entries registered in your AI apps).
283
283
 
284
284
  ```
285
- npx -y @modootoday/datalab-extension-mcp@1.8.7 uninstall
285
+ npx -y @modootoday/datalab-extension-mcp@1.8.9 uninstall
286
286
  ```
287
287
 
288
288
  2. Turn **MCP 연결** off in the extension settings. The panel is the bridge, so with it off a leftover config line can look nothing up.
package/README.md CHANGED
@@ -49,10 +49,10 @@ Claude 같은 AI 앱을 데이터랩툴즈에 연결해 주는 작은 무료 프
49
49
  명령 한 줄이면 컴퓨터에 있는 AI 앱을 **모두 자동으로** 찾아 연결해 줘요. 앱마다 따로 할 일은 없어요.
50
50
 
51
51
  1. **터미널을 열어요** — Windows는 검색창에 `cmd` → "명령 프롬프트", Mac은 `⌘+스페이스`로 `터미널`, Linux는 터미널 앱(보통 `Ctrl+Alt+T`)이에요.
52
- 2. **아래 명령을 붙여넣고 Enter를 눌러요.** (`@1.8.7`은 커넥터가 나도 모르게 바뀌지 않도록 고정한 버전이에요.)
52
+ 2. **아래 명령을 붙여넣고 Enter를 눌러요.** (`@1.8.9`은 커넥터가 나도 모르게 바뀌지 않도록 고정한 버전이에요.)
53
53
 
54
54
  ```
55
- npx -y @modootoday/datalab-extension-mcp@1.8.7 install
55
+ npx -y @modootoday/datalab-extension-mcp@1.8.9 install
56
56
  ```
57
57
 
58
58
  영어 글자가 여러 줄 올라오는 건 정상이에요. 이 명령은 커넥터를 내려받아 실행할 뿐, 다른 건 건드리지 않아요.
@@ -220,7 +220,7 @@ AI가 "그런 기능은 없다" 고 답하면, **"도구 목록을 확인해 보
220
220
  - **연결이 사라졌을 때** — 처음 실행했던 그 명령을 한 번 더 실행하면 안전하게 다시 등록돼요. (일부 AI 앱이 설정 파일을 정리하며 연결을 지우기도 해요.)
221
221
 
222
222
  ```
223
- npx -y @modootoday/datalab-extension-mcp@1.8.7 install
223
+ npx -y @modootoday/datalab-extension-mcp@1.8.9 install
224
224
  ```
225
225
 
226
226
  - **`'npx'을(는) 내부 또는 외부 명령...`** — Node.js가 없는 거예요. 위 2단계의 "Node.js가 없다면" 한 줄 설치가 가장 간단해요.
@@ -250,7 +250,7 @@ AI가 "그런 기능은 없다" 고 답하면, **"도구 목록을 확인해 보
250
250
  1. 터미널에서 아래 명령을 실행해요(AI 앱에 등록된 설정을 지워 줘요).
251
251
 
252
252
  ```
253
- npx -y @modootoday/datalab-extension-mcp@1.8.7 uninstall
253
+ npx -y @modootoday/datalab-extension-mcp@1.8.9 uninstall
254
254
  ```
255
255
 
256
256
  2. 확장 설정에서 **MCP 연결을 꺼요.** 패널이 브릿지라, 꺼 두면 어딘가 설정 줄이 남아 있어도 AI 앱이 아무것도 조회할 수 없어요.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DEFAULT_HOST,
3
3
  DEFAULT_PORT
4
- } from "./chunk-YTMY2DAN.js";
4
+ } from "./chunk-GCRJQLTV.js";
5
5
  import {
6
6
  TOKEN_PROMPT_GUIDE,
7
7
  TOKEN_PROMPT_QUESTION,
@@ -1,4 +1,4 @@
1
- // ../mcp-server/dist/chunk-CWKYOEBJ.js
1
+ // ../mcp-server/dist/chunk-TUYW5HMB.js
2
2
  import { createHash } from "crypto";
3
3
  import { createHash as createHash2, randomUUID } from "crypto";
4
4
  import { mkdir, readFile, readdir, rm, writeFile } from "fs/promises";
@@ -14460,7 +14460,9 @@ var HelloNackReasonSchema = external_exports.enum([
14460
14460
  /** No version in common. `supported` tells the extension what to do. */
14461
14461
  "version_mismatch",
14462
14462
  /** Origin header absent or not our extension. */
14463
- "forbidden_origin"
14463
+ "forbidden_origin",
14464
+ /** This browser already has an active event stream. */
14465
+ "session_active"
14464
14466
  ]);
14465
14467
  var BrowserRoutingSchema = external_exports.object({
14466
14468
  targetBrowserId: external_exports.string(),
@@ -15618,7 +15620,6 @@ function sessionKeyFor(credential) {
15618
15620
  return credential.kind === "claimed" ? credential.browserId : LEGACY_SESSION_KEY;
15619
15621
  }
15620
15622
  var DEFAULT_RATE_LIMIT = BRIDGE_LIMITS;
15621
- var STALE_TOKEN_LIMIT = 5;
15622
15623
  function daemonIdTag() {
15623
15624
  return Math.floor(Math.random() * 16777215).toString(36).padStart(4, "0");
15624
15625
  }
@@ -15628,8 +15629,7 @@ var Bridge = class {
15628
15629
  #idTag = daemonIdTag();
15629
15630
  #sessionSeq = 0;
15630
15631
  #defaultKey = null;
15631
- /** Consecutive handshakes refused for our own token. See `#noteStaleToken`. */
15632
- #badTokenStreak = 0;
15632
+ #retireSession = null;
15633
15633
  #bucketTokens = 0;
15634
15634
  #bucketRefilledAt = 0;
15635
15635
  // Last catalog the panel reported, kept across disconnects: many hosts cache
@@ -15639,6 +15639,12 @@ var Bridge = class {
15639
15639
  constructor(deps) {
15640
15640
  this.#deps = deps;
15641
15641
  }
15642
+ bindRetireSession(retire) {
15643
+ if (this.#retireSession !== null) {
15644
+ throw new Error("bridge transport retirement is already bound");
15645
+ }
15646
+ this.#retireSession = retire;
15647
+ }
15642
15648
  /**
15643
15649
  * The session a request with no browser named goes to. Map preserves
15644
15650
  * insertion order, so this is the oldest one — while the key space holds a
@@ -15756,6 +15762,7 @@ var Bridge = class {
15756
15762
  const entry = this.#sessions.get(key);
15757
15763
  if (!entry || entry.session.sessionId !== sessionId) return false;
15758
15764
  entry.streamActive = true;
15765
+ if (this.#defaultKey === null) this.#defaultKey = key;
15759
15766
  return true;
15760
15767
  }
15761
15768
  isSessionStreamActive(key, sessionId) {
@@ -15944,36 +15951,6 @@ var Bridge = class {
15944
15951
  }
15945
15952
  return null;
15946
15953
  }
15947
- /**
15948
- * The extension is right about the token, and we can be wrong about it.
15949
- *
15950
- * This server is detached and outlives the app that started it, and a
15951
- * replacement only takes over when its version is newer. Once the user
15952
- * issues a fresh token, stepping aside is gated on the very token that no
15953
- * longer matches, so the server has to notice on its own. A run of refusals
15954
- * with nobody connected says exactly that, and exiting costs nothing.
15955
- */
15956
- #noteStaleToken(browserId, tokenHash) {
15957
- if (this.#deps.wasRevoked(tokenHash)) {
15958
- this.#deps.log?.("refused a revoked registration; not ours to leave for");
15959
- return;
15960
- }
15961
- if (this.#isClaimed(browserId)) {
15962
- this.#deps.log?.("refused a claimed browser; not ours to step aside for");
15963
- return;
15964
- }
15965
- if (this.#sessions.has(LEGACY_SESSION_KEY)) return;
15966
- this.#badTokenStreak += 1;
15967
- if (this.#badTokenStreak < STALE_TOKEN_LIMIT) return;
15968
- this.#deps.log?.(
15969
- `refused ${this.#badTokenStreak} handshakes for a stale token; stepping aside`
15970
- );
15971
- this.#badTokenStreak = 0;
15972
- this.#deps.onStaleToken?.();
15973
- }
15974
- #isClaimed(browserId) {
15975
- return browserId !== void 0 && this.#deps.claimedBrowsers.has(browserId);
15976
- }
15977
15954
  /**
15978
15955
  * Run the handshake.
15979
15956
  *
@@ -16014,7 +15991,9 @@ var Bridge = class {
16014
15991
  { rootToken: this.#deps.token, claimed: this.#deps.claimedBrowsers }
16015
15992
  );
16016
15993
  if (credential.kind === "refused") {
16017
- this.#noteStaleToken(parsed.data.browserId, tokenHash);
15994
+ this.#deps.log?.(
15995
+ this.#deps.wasRevoked(tokenHash) ? "refused a revoked browser credential" : "refused an unauthorized browser credential"
15996
+ );
16018
15997
  return {
16019
15998
  t: "hello_nack",
16020
15999
  reason: credential.reason,
@@ -16022,7 +16001,6 @@ var Bridge = class {
16022
16001
  message: credential.message
16023
16002
  };
16024
16003
  }
16025
- if (credential.kind === "root") this.#badTokenStreak = 0;
16026
16004
  const negotiated = negotiateProtocol(parsed.data.protocolVersions);
16027
16005
  if (!negotiated.ok) {
16028
16006
  return {
@@ -16033,6 +16011,10 @@ var Bridge = class {
16033
16011
  };
16034
16012
  }
16035
16013
  const key = sessionKeyFor(credential);
16014
+ const held = this.#sessions.get(key);
16015
+ if (held?.streamActive) {
16016
+ this.#retireSession?.(key, held.session.sessionId);
16017
+ }
16036
16018
  const session = {
16037
16019
  // A counter, not a timestamp: a panel reload produces two handshakes
16038
16020
  // inside one millisecond, and the new session must never be
@@ -17182,14 +17164,13 @@ function createHttpBridge(opts) {
17182
17164
  opts.bridge.disconnect(key);
17183
17165
  if (wasPrimary) scheduleToolsDrop();
17184
17166
  };
17185
- const retireStaleSocket = (key) => {
17186
- const stale = removeStream(key);
17187
- if (!stale) return;
17188
- try {
17189
- stale.end();
17190
- } catch {
17191
- }
17192
- };
17167
+ opts.bridge.bindRetireSession((key, sessionId) => {
17168
+ const stream = streams.get(key);
17169
+ if (!stream || stream.sessionId !== sessionId) return;
17170
+ const wasPrimary = key === opts.bridge.sessionKey;
17171
+ removeStream(key)?.end();
17172
+ if (wasPrimary) scheduleToolsDrop();
17173
+ });
17193
17174
  const server = createServer((req, res) => {
17194
17175
  const url2 = new URL(req.url ?? "/", `http://${host}`);
17195
17176
  const origin = req.headers.origin;
@@ -17231,13 +17212,21 @@ function createHttpBridge(opts) {
17231
17212
  });
17232
17213
  return;
17233
17214
  }
17215
+ if (req.method === "POST" && url2.pathname === "/mcp/authority") {
17216
+ void readJson(req, MAX_BODY_BYTES).then((body) => {
17217
+ const token = body?.token;
17218
+ if (!opts.bridge.verifyRootToken(typeof token === "string" ? token : "")) {
17219
+ json2(res, 403, { error: "authority_mismatch" });
17220
+ return;
17221
+ }
17222
+ res.writeHead(204, { "cache-control": "no-store" });
17223
+ res.end();
17224
+ }).catch(() => json2(res, 400, { error: "bad request" }));
17225
+ return;
17226
+ }
17234
17227
  if (req.method === "POST" && url2.pathname === "/bridge/hello") {
17235
17228
  void readJson(req, MAX_BODY_BYTES).then((body) => {
17236
17229
  const out = opts.bridge.handshake(body, origin);
17237
- if (out.t === "hello_ack") {
17238
- const key = opts.bridge.keyForSession(out.sessionId);
17239
- if (key !== null) retireStaleSocket(key);
17240
- }
17241
17230
  json2(res, out.t === "hello_ack" ? 200 : 403, out);
17242
17231
  }).catch(
17243
17232
  () => json2(res, 400, {
@@ -18002,13 +17991,6 @@ function runDaemon(env, deps = {}) {
18002
17991
  }
18003
17992
  const bridge = new Bridge({
18004
17993
  send: (frame, key) => http.send(frame, key),
18005
- // Step aside once our own token looks stale. This process is detached
18006
- // and survives an app restart, and a replacement only takes over on a newer
18007
- // version — so nothing outside can unblock the install if we do not.
18008
- onStaleToken: () => {
18009
- log("pairing token is stale; exiting so the next call can start fresh");
18010
- void http.close().finally(() => exit(0));
18011
- },
18012
17994
  token,
18013
17995
  extensionIds,
18014
17996
  serverVersion: version2,
@@ -4,7 +4,7 @@ import {
4
4
  mediaToolDescriptors,
5
5
  spawnDaemon,
6
6
  tryConnect
7
- } from "./chunk-YTMY2DAN.js";
7
+ } from "./chunk-GCRJQLTV.js";
8
8
  import {
9
9
  __export
10
10
  } from "./chunk-MLKGABMK.js";
@@ -14599,7 +14599,9 @@ var HelloNackReasonSchema = external_exports.enum([
14599
14599
  /** No version in common. `supported` tells the extension what to do. */
14600
14600
  "version_mismatch",
14601
14601
  /** Origin header absent or not our extension. */
14602
- "forbidden_origin"
14602
+ "forbidden_origin",
14603
+ /** This browser already has an active event stream. */
14604
+ "session_active"
14603
14605
  ]);
14604
14606
  var BrowserRoutingSchema = external_exports.object({
14605
14607
  targetBrowserId: external_exports.string(),
@@ -15584,6 +15586,21 @@ async function defaultShutdown(base, token) {
15584
15586
  return false;
15585
15587
  }
15586
15588
  }
15589
+ async function defaultCheckAuthority(base, token) {
15590
+ try {
15591
+ const res = await globalThis.fetch(
15592
+ `${base}/mcp/authority`,
15593
+ {
15594
+ method: "POST",
15595
+ headers: { "content-type": "application/json" },
15596
+ body: JSON.stringify({ token })
15597
+ }
15598
+ );
15599
+ return res.status === 204;
15600
+ } catch {
15601
+ return false;
15602
+ }
15603
+ }
15587
15604
  async function ensureDaemonRunning(deps = {}) {
15588
15605
  const host = deps.host ?? DEFAULT_HOST;
15589
15606
  const port = deps.port ?? DEFAULT_PORT;
@@ -15599,6 +15616,7 @@ async function ensureDaemonRunning(deps = {}) {
15599
15616
  const token = deps.token ?? process.env["DATALAB_MCP_TOKEN"];
15600
15617
  const readVersion = deps.readVersion ?? defaultReadVersion;
15601
15618
  const shutdown = deps.shutdown ?? defaultShutdown;
15619
+ const checkAuthority = deps.checkAuthority ?? defaultCheckAuthority;
15602
15620
  const spawnAndWait = async () => {
15603
15621
  spawn(daemonEntry, ["serve"]);
15604
15622
  for (let i = 0; i < attempts; i += 1) {
@@ -15621,14 +15639,26 @@ async function ensureDaemonRunning(deps = {}) {
15621
15639
  fast.destroy();
15622
15640
  if (!selfVersion || !token) return;
15623
15641
  const running = await readVersion(base);
15624
- if (!running || !isOlderVersion(running, selfVersion)) return;
15642
+ if (!running) {
15643
+ throw new Error(
15644
+ `port ${String(port)} is occupied by something other than the DataLab connector`
15645
+ );
15646
+ }
15647
+ if (!isOlderVersion(running, selfVersion)) {
15648
+ if (await checkAuthority(base, token)) return;
15649
+ throw new Error(
15650
+ `connector authority conflict on port ${String(port)}; every MCP host on this machine must use the same DataLab workbench token`
15651
+ );
15652
+ }
15625
15653
  log(`updating the connector: replacing ${running} with ${selfVersion}`);
15626
15654
  const accepted = await shutdown(base, token);
15627
15655
  if (!accepted) {
15628
15656
  log(
15629
15657
  "the running connector is too old to update automatically; restart it (or wait for it to idle out) to pick up the new version"
15630
15658
  );
15631
- return;
15659
+ throw new Error(
15660
+ `connector authority conflict on port ${String(port)}; the running connector did not accept this workbench token`
15661
+ );
15632
15662
  }
15633
15663
  for (let i = 0; i < attempts; i += 1) {
15634
15664
  const socket = await connect(host, port, PROBE_TIMEOUT_MS);
package/dist/cli.js CHANGED
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  dispatchCli,
4
4
  runAdapter
5
- } from "./chunk-JOEFFC3W.js";
5
+ } from "./chunk-WBMZLPXC.js";
6
6
  import {
7
7
  runDaemon
8
- } from "./chunk-YTMY2DAN.js";
8
+ } from "./chunk-GCRJQLTV.js";
9
9
  import "./chunk-MLKGABMK.js";
10
10
 
11
11
  // src/cli.ts
@@ -62,7 +62,7 @@ async function main() {
62
62
  // Loaded lazily, like the installer: the adapter path is what an MCP host
63
63
  // spawns and must not pay for a command only an operator types.
64
64
  browsers: async (argv) => {
65
- const { runBrowsers } = await import("./browsers-RT4ZPGXZ.js");
65
+ const { runBrowsers } = await import("./browsers-DRGNGPSI.js");
66
66
  const { createNodeIo } = await import("./dist-JICXADYR.js");
67
67
  const io = createNodeIo();
68
68
  const code = await runBrowsers(argv, {
package/dist/index.d.ts CHANGED
@@ -62,6 +62,8 @@ interface EnsureRunningDeps {
62
62
  readVersion?: (base: string) => Promise<string | null>;
63
63
  /** Injected in tests; defaults to `POST /mcp/shutdown`. */
64
64
  shutdown?: (base: string, token: string) => Promise<boolean>;
65
+ /** Injected in tests; defaults to the authenticated authority probe. */
66
+ checkAuthority?: (base: string, token: string) => Promise<boolean>;
65
67
  }
66
68
  /**
67
69
  * Ensure a daemon is up before the adapter starts proxying. A daemon already
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@ import {
3
3
  dispatchCli,
4
4
  ensureDaemonRunning,
5
5
  runAdapter
6
- } from "./chunk-JOEFFC3W.js";
7
- import "./chunk-YTMY2DAN.js";
6
+ } from "./chunk-WBMZLPXC.js";
7
+ import "./chunk-GCRJQLTV.js";
8
8
  import "./chunk-MLKGABMK.js";
9
9
  export {
10
10
  createAdapterServer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modootoday/datalab-extension-mcp",
3
- "version": "1.8.7",
3
+ "version": "1.8.9",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",