@trops/dash-core 0.1.490 → 0.1.492

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.
@@ -17,7 +17,7 @@ var require$$0$5 = require('@modelcontextprotocol/sdk/client/index.js');
17
17
  var require$$1$4 = require('@modelcontextprotocol/sdk/client/stdio.js');
18
18
  var require$$0$4 = require('pkce-challenge');
19
19
  var require$$2$1 = require('os');
20
- var require$$9$1 = require('child_process');
20
+ var require$$11 = require('child_process');
21
21
  var require$$3$2 = require('adm-zip');
22
22
  var require$$4$1 = require('url');
23
23
  var require$$2$2 = require('vm');
@@ -21522,6 +21522,183 @@ var permissionGate = {
21522
21522
  PATH_ARG_KEYS,
21523
21523
  };
21524
21524
 
21525
+ /**
21526
+ * mcpServerKey.js
21527
+ *
21528
+ * Slice 3a: per-workspace MCP server process isolation.
21529
+ *
21530
+ * `mcpController.activeServers` was historically keyed by `serverName`
21531
+ * alone, so two workspaces using the same MCP server type (e.g. both
21532
+ * using `filesystem`) shared one process. Slice 3a keys by the
21533
+ * compound `(workspaceId, serverName)` so each workspace gets its
21534
+ * own process. Slice 3b will configure each process with the union
21535
+ * of grants from widgets on that workspace; for now, processes are
21536
+ * simply isolated.
21537
+ *
21538
+ * Format: `<workspaceId>::<serverName>`. The `::` separator is
21539
+ * unlikely to appear in a UUID-shaped workspace id; serverName is
21540
+ * everything after the first `::` so server names containing `::`
21541
+ * round-trip cleanly.
21542
+ *
21543
+ * Callers without a workspace context (legacy IPC, dash MCP server
21544
+ * tools, AI Builder previews) supply `null`/`undefined` and land on
21545
+ * the `NO_WORKSPACE` sentinel bucket — that's the pre-Slice-3 bucket.
21546
+ *
21547
+ * NOTE: workspaceId is renderer-supplied. Slice 3a uses it only as a
21548
+ * process-isolation key, NOT as a trust boundary. Slice 3b will tie
21549
+ * server scope (e.g. filesystem `--allowed` paths) to it; that's when
21550
+ * the trust boundary appears.
21551
+ */
21552
+
21553
+ const NO_WORKSPACE = "__no_workspace__";
21554
+ const SEP = "::";
21555
+
21556
+ function serverKey$1(workspaceId, serverName) {
21557
+ if (typeof serverName !== "string" || !serverName) {
21558
+ throw new Error("serverKey: serverName is required");
21559
+ }
21560
+ const wid =
21561
+ typeof workspaceId === "string" && workspaceId ? workspaceId : NO_WORKSPACE;
21562
+ return wid + SEP + serverName;
21563
+ }
21564
+
21565
+ function parseServerKey$1(key) {
21566
+ if (typeof key !== "string") {
21567
+ throw new Error("parseServerKey: key must be a string");
21568
+ }
21569
+ const idx = key.indexOf(SEP);
21570
+ if (idx < 0) {
21571
+ throw new Error(
21572
+ "parseServerKey: malformed key (no '::' separator): " + key,
21573
+ );
21574
+ }
21575
+ return {
21576
+ workspaceId: key.slice(0, idx),
21577
+ serverName: key.slice(idx + SEP.length),
21578
+ };
21579
+ }
21580
+
21581
+ var mcpServerKey = {
21582
+ serverKey: serverKey$1,
21583
+ parseServerKey: parseServerKey$1,
21584
+ NO_WORKSPACE,
21585
+ SEP,
21586
+ };
21587
+
21588
+ /**
21589
+ * mcpScopeResolver.js
21590
+ *
21591
+ * Slice 3b: per-workspace path scope reconfiguration.
21592
+ *
21593
+ * The Slice-3a process isolation key (`workspaceId::serverName`) is the
21594
+ * lifecycle handle. This module computes WHAT the spawned process is
21595
+ * configured to see — the union of granted paths from widgets on the
21596
+ * active workspace, applied as credential overrides at spawn time.
21597
+ *
21598
+ * Design:
21599
+ * - The renderer enumerates widgets on the active workspace, looks up
21600
+ * each widget's grant via window.mainApi.widgetMcp.getGrant, and
21601
+ * hands the array to unionPathScope() to compute the workspace
21602
+ * scope for a given server (e.g. "filesystem").
21603
+ * - The renderer passes the resulting scope as `pathScope` to
21604
+ * mcpStartServer.
21605
+ * - mcpController applies the scope to credentials (replacing
21606
+ * allowedPaths etc.) before its existing argsMapping spreads them
21607
+ * into the spawn args. Servers that don't declare argsMapping for
21608
+ * the path keys are unaffected.
21609
+ *
21610
+ * Feature flag: the controller only applies the override when
21611
+ * `security.enforceWidgetMcpPermissions` is on. When off, server starts
21612
+ * with credentials as-configured (pre-3b behavior).
21613
+ *
21614
+ * Out of scope here:
21615
+ * - Hot-respawn on widget add/remove (see Slice 3b plan, deferred).
21616
+ * - Catalog schema for new path-scoped servers (filesystem already
21617
+ * has argsMapping.allowedPaths; others added as discovered).
21618
+ */
21619
+
21620
+ /**
21621
+ * Compute the workspace-scoped path union for a given server.
21622
+ *
21623
+ * @param {Array<{widgetId, granted}>} grants - widgets-on-workspace + their grants
21624
+ * @param {string} serverName - the MCP server name (e.g. "filesystem")
21625
+ * @returns {{ readPaths: string[], writePaths: string[], allowedPaths: string[] }}
21626
+ *
21627
+ * `allowedPaths` is the dedup union of read+write — used by
21628
+ * filesystem-style servers that take a single allowed-list. Servers
21629
+ * that distinguish read-only vs read-write can use the readPaths /
21630
+ * writePaths arrays directly.
21631
+ */
21632
+ function unionPathScope(grants, serverName) {
21633
+ const reads = new Set();
21634
+ const writes = new Set();
21635
+
21636
+ if (!Array.isArray(grants)) {
21637
+ return { readPaths: [], writePaths: [], allowedPaths: [] };
21638
+ }
21639
+
21640
+ for (const entry of grants) {
21641
+ if (!entry || typeof entry !== "object") continue;
21642
+ const granted = entry.granted;
21643
+ if (!granted || typeof granted !== "object") continue;
21644
+ const servers = granted.servers;
21645
+ if (!servers || typeof servers !== "object") continue;
21646
+ const serverPerms = servers[serverName];
21647
+ if (!serverPerms || typeof serverPerms !== "object") continue;
21648
+
21649
+ if (Array.isArray(serverPerms.readPaths)) {
21650
+ for (const p of serverPerms.readPaths) {
21651
+ if (typeof p === "string" && p) reads.add(p);
21652
+ }
21653
+ }
21654
+ if (Array.isArray(serverPerms.writePaths)) {
21655
+ for (const p of serverPerms.writePaths) {
21656
+ if (typeof p === "string" && p) writes.add(p);
21657
+ }
21658
+ }
21659
+ }
21660
+
21661
+ const readPaths = [...reads];
21662
+ const writePaths = [...writes];
21663
+ const allowedPaths = [...new Set([...reads, ...writes])];
21664
+
21665
+ return { readPaths, writePaths, allowedPaths };
21666
+ }
21667
+
21668
+ /**
21669
+ * Override credential keys with values derived from a path scope.
21670
+ *
21671
+ * Filesystem-style servers expect `allowedPaths` as a comma-separated
21672
+ * string (the catalog's `argsMapping.allowedPaths.split` then expands
21673
+ * it back into positional args at spawn time). This helper joins the
21674
+ * scope's allowedPaths to match that convention.
21675
+ *
21676
+ * Returns a NEW credentials object — does not mutate the input.
21677
+ *
21678
+ * If pathScope is empty (no granted paths at all), the existing
21679
+ * credentials are returned unchanged so the user's globally-configured
21680
+ * allowedPaths still works for the LLM tool path / NO_WORKSPACE bucket.
21681
+ */
21682
+ function applyPathScopeToCredentials$1(credentials, pathScope) {
21683
+ const base =
21684
+ credentials && typeof credentials === "object" ? { ...credentials } : {};
21685
+
21686
+ if (!pathScope || typeof pathScope !== "object") return base;
21687
+
21688
+ const allowed = Array.isArray(pathScope.allowedPaths)
21689
+ ? pathScope.allowedPaths
21690
+ : [];
21691
+ if (allowed.length === 0) return base;
21692
+
21693
+ base.allowedPaths = allowed.join(",");
21694
+ return base;
21695
+ }
21696
+
21697
+ var mcpScopeResolver = {
21698
+ unionPathScope,
21699
+ applyPathScopeToCredentials: applyPathScopeToCredentials$1,
21700
+ };
21701
+
21525
21702
  /**
21526
21703
  * mcpController.js
21527
21704
  *
@@ -21547,6 +21724,8 @@ const fs$9 = require$$0$2;
21547
21724
  const os$2 = require$$2$1;
21548
21725
  const responseCache$2 = responseCache_1;
21549
21726
  const { gateToolCall } = permissionGate;
21727
+ const { serverKey, parseServerKey } = mcpServerKey;
21728
+ const { applyPathScopeToCredentials } = mcpScopeResolver;
21550
21729
  const { app: app$7 } = require$$0$1;
21551
21730
 
21552
21731
  // Read the widget-MCP-enforcement feature flag from settings.json.
@@ -21703,7 +21882,7 @@ function getShellPath$1() {
21703
21882
  return _shellPath$1;
21704
21883
  }
21705
21884
 
21706
- const { execSync } = require$$9$1;
21885
+ const { execSync } = require$$11;
21707
21886
  const fallbackDirs = ["/usr/local/bin", "/opt/homebrew/bin"];
21708
21887
 
21709
21888
  // Scan nvm versions, tracking both latest and best compatible version
@@ -21936,17 +22115,50 @@ const mcpController$3 = {
21936
22115
  * startServer
21937
22116
  * Start an MCP server with the given config and credentials
21938
22117
  *
22118
+ * Slice 3a: server instances are keyed by `(workspaceId, serverName)`
22119
+ * so two workspaces using the same server type get separate processes.
22120
+ * Pass `null`/`undefined` workspaceId to land on the legacy
22121
+ * NO_WORKSPACE bucket (e.g. dash MCP server tools, AI Builder previews).
22122
+ *
22123
+ * Slice 3b: when `security.enforceWidgetMcpPermissions` is on AND a
22124
+ * non-empty `pathScope` is supplied, the scope's allowed paths
22125
+ * override the server's existing path-style credentials before the
22126
+ * catalog's argsMapping spreads them into spawn args. This scopes
22127
+ * the server's OS-level capability to the union of widget grants on
22128
+ * the active workspace.
22129
+ *
21939
22130
  * @param {BrowserWindow} win the main window
21940
22131
  * @param {string} serverName unique name for this server instance
21941
22132
  * @param {object} mcpConfig { transport, command, args, envMapping }
21942
22133
  * @param {object} credentials decrypted credentials object
22134
+ * @param {string|null} workspaceId active workspace id (Slice 3a)
22135
+ * @param {object|null} pathScope { readPaths, writePaths, allowedPaths } (Slice 3b)
21943
22136
  * @returns {{ success, serverName, tools, status } | { error, message }}
21944
22137
  */
21945
- startServer: async (win, serverName, mcpConfig, credentials) => {
22138
+ startServer: async (
22139
+ win,
22140
+ serverName,
22141
+ mcpConfig,
22142
+ credentials,
22143
+ workspaceId,
22144
+ pathScope = null,
22145
+ ) => {
22146
+ const key = serverKey(workspaceId, serverName);
22147
+ // Slice 3b: when the gate is enforced, override credentials with
22148
+ // the workspace's union of granted paths so the spawned process
22149
+ // can't see anything broader than the user has consented to.
22150
+ if (
22151
+ isWidgetPermissionEnforcementEnabled() &&
22152
+ pathScope &&
22153
+ Array.isArray(pathScope.allowedPaths) &&
22154
+ pathScope.allowedPaths.length > 0
22155
+ ) {
22156
+ credentials = applyPathScopeToCredentials(credentials, pathScope);
22157
+ }
21946
22158
  // 1. Already connected? Return existing connection
21947
- const existing = activeServers.get(serverName);
22159
+ const existing = activeServers.get(key);
21948
22160
  if (existing && existing.status === STATUS$1.CONNECTED && existing.client) {
21949
- console.log(`[mcpController] Server already connected: ${serverName}`);
22161
+ console.log(`[mcpController] Server already connected: ${key}`);
21950
22162
  return {
21951
22163
  success: true,
21952
22164
  serverName,
@@ -21957,19 +22169,19 @@ const mcpController$3 = {
21957
22169
  }
21958
22170
 
21959
22171
  // 2. Already starting? Piggyback on the pending promise
21960
- if (pendingStarts.has(serverName)) {
22172
+ if (pendingStarts.has(key)) {
21961
22173
  console.log(
21962
- `[mcpController] Server already starting, deduplicating: ${serverName}`,
22174
+ `[mcpController] Server already starting, deduplicating: ${key}`,
21963
22175
  );
21964
- return pendingStarts.get(serverName);
22176
+ return pendingStarts.get(key);
21965
22177
  }
21966
22178
 
21967
22179
  // 3. Fresh start — wrap in a promise and track it
21968
22180
  const startPromise = (async () => {
21969
22181
  try {
21970
22182
  // Stop if in stale/error state
21971
- if (activeServers.has(serverName)) {
21972
- await mcpController$3.stopServer(win, serverName);
22183
+ if (activeServers.has(key)) {
22184
+ await mcpController$3.stopServer(win, serverName, workspaceId);
21973
22185
  }
21974
22186
 
21975
22187
  // Merge with catalog entry to pick up updated command/args
@@ -22100,12 +22312,14 @@ const mcpController$3 = {
22100
22312
  }
22101
22313
 
22102
22314
  // Update status to connecting
22103
- activeServers.set(serverName, {
22315
+ activeServers.set(key, {
22104
22316
  client: null,
22105
22317
  transport,
22106
22318
  tools: [],
22107
22319
  resources: [],
22108
22320
  status: STATUS$1.CONNECTING,
22321
+ workspaceId: workspaceId || null,
22322
+ serverName,
22109
22323
  });
22110
22324
 
22111
22325
  // Create MCP client
@@ -22139,16 +22353,18 @@ const mcpController$3 = {
22139
22353
  }
22140
22354
 
22141
22355
  // Store the active connection
22142
- activeServers.set(serverName, {
22356
+ activeServers.set(key, {
22143
22357
  client,
22144
22358
  transport,
22145
22359
  tools,
22146
22360
  resources,
22147
22361
  status: STATUS$1.CONNECTED,
22362
+ workspaceId: workspaceId || null,
22363
+ serverName,
22148
22364
  });
22149
22365
 
22150
22366
  console.log(
22151
- `[mcpController] Server connected: ${serverName} (${tools.length} tools, ${resources.length} resources)`,
22367
+ `[mcpController] Server connected: ${key} (${tools.length} tools, ${resources.length} resources)`,
22152
22368
  );
22153
22369
 
22154
22370
  return {
@@ -22173,13 +22389,15 @@ const mcpController$3 = {
22173
22389
  }
22174
22390
 
22175
22391
  // Mark as error state
22176
- activeServers.set(serverName, {
22392
+ activeServers.set(key, {
22177
22393
  client: null,
22178
22394
  transport: null,
22179
22395
  tools: [],
22180
22396
  resources: [],
22181
22397
  status: STATUS$1.ERROR,
22182
22398
  error: errorMessage,
22399
+ workspaceId: workspaceId || null,
22400
+ serverName,
22183
22401
  });
22184
22402
 
22185
22403
  return {
@@ -22189,11 +22407,11 @@ const mcpController$3 = {
22189
22407
  status: STATUS$1.ERROR,
22190
22408
  };
22191
22409
  } finally {
22192
- pendingStarts.delete(serverName);
22410
+ pendingStarts.delete(key);
22193
22411
  }
22194
22412
  })();
22195
22413
 
22196
- pendingStarts.set(serverName, startPromise);
22414
+ pendingStarts.set(key, startPromise);
22197
22415
  return startPromise;
22198
22416
  },
22199
22417
 
@@ -22203,20 +22421,22 @@ const mcpController$3 = {
22203
22421
  *
22204
22422
  * @param {BrowserWindow} win the main window
22205
22423
  * @param {string} serverName the server to stop
22424
+ * @param {string|null} workspaceId active workspace id (Slice 3a)
22206
22425
  * @returns {{ success, serverName } | { error, message }}
22207
22426
  */
22208
- stopServer: async (win, serverName) => {
22427
+ stopServer: async (win, serverName, workspaceId) => {
22428
+ const key = serverKey(workspaceId, serverName);
22209
22429
  try {
22210
22430
  // Wait for any in-flight start to finish before stopping
22211
- if (pendingStarts.has(serverName)) {
22431
+ if (pendingStarts.has(key)) {
22212
22432
  try {
22213
- await pendingStarts.get(serverName);
22433
+ await pendingStarts.get(key);
22214
22434
  } catch (e) {
22215
22435
  /* stopping anyway */
22216
22436
  }
22217
22437
  }
22218
22438
 
22219
- const server = activeServers.get(serverName);
22439
+ const server = activeServers.get(key);
22220
22440
  if (!server) {
22221
22441
  return {
22222
22442
  success: true,
@@ -22225,7 +22445,7 @@ const mcpController$3 = {
22225
22445
  };
22226
22446
  }
22227
22447
 
22228
- console.log(`[mcpController] Stopping server: ${serverName}`);
22448
+ console.log(`[mcpController] Stopping server: ${key}`);
22229
22449
 
22230
22450
  // Close the client connection
22231
22451
  if (server.client) {
@@ -22233,27 +22453,24 @@ const mcpController$3 = {
22233
22453
  await server.client.close();
22234
22454
  } catch (closeError) {
22235
22455
  console.warn(
22236
- `[mcpController] Error closing client for ${serverName}:`,
22456
+ `[mcpController] Error closing client for ${key}:`,
22237
22457
  closeError.message,
22238
22458
  );
22239
22459
  }
22240
22460
  }
22241
22461
 
22242
- activeServers.delete(serverName);
22462
+ activeServers.delete(key);
22243
22463
 
22244
- console.log(`[mcpController] Server stopped: ${serverName}`);
22464
+ console.log(`[mcpController] Server stopped: ${key}`);
22245
22465
 
22246
22466
  return {
22247
22467
  success: true,
22248
22468
  serverName,
22249
22469
  };
22250
22470
  } catch (error) {
22251
- console.error(
22252
- `[mcpController] Error stopping server ${serverName}:`,
22253
- error,
22254
- );
22471
+ console.error(`[mcpController] Error stopping server ${key}:`, error);
22255
22472
  // Clean up anyway
22256
- activeServers.delete(serverName);
22473
+ activeServers.delete(key);
22257
22474
  return {
22258
22475
  error: true,
22259
22476
  message: error.message,
@@ -22270,6 +22487,10 @@ const mcpController$3 = {
22270
22487
  * @param {string} toolName the tool to call
22271
22488
  * @param {object} args arguments for the tool
22272
22489
  * @param {Array<string>} allowedTools optional whitelist of allowed tool names
22490
+ * @param {string|null} widgetId the widget originating the call (Slice 1+2)
22491
+ * @param {string|null} workspaceId the active workspace (Slice 3a) — used
22492
+ * to scope the server process per workspace.
22493
+ * Slice 3b will tie path scope to this id.
22273
22494
  * @returns {{ result } | { error, message }}
22274
22495
  */
22275
22496
  callTool: async (
@@ -22279,11 +22500,13 @@ const mcpController$3 = {
22279
22500
  args,
22280
22501
  allowedTools = null,
22281
22502
  widgetId = null,
22503
+ workspaceId = null,
22282
22504
  ) => {
22505
+ const key = serverKey(workspaceId, serverName);
22283
22506
  try {
22284
- const server = activeServers.get(serverName);
22507
+ const server = activeServers.get(key);
22285
22508
  if (!server || !server.client) {
22286
- throw new Error(`Server not connected: ${serverName}`);
22509
+ throw new Error(`Server not connected: ${key}`);
22287
22510
  }
22288
22511
 
22289
22512
  // Per-widget manifest gate. Activated by the
@@ -22316,7 +22539,7 @@ const mcpController$3 = {
22316
22539
  }
22317
22540
 
22318
22541
  const doCall = async () => {
22319
- console.log(`[mcpController] Calling tool: ${serverName}/${toolName}`);
22542
+ console.log(`[mcpController] Calling tool: ${key}/${toolName}`);
22320
22543
  const result = await server.client.callTool({
22321
22544
  name: toolName,
22322
22545
  arguments: args || {},
@@ -22327,18 +22550,21 @@ const mcpController$3 = {
22327
22550
  };
22328
22551
  };
22329
22552
 
22330
- // Cache read-only tool calls with in-flight dedup.
22331
- // Writes always hit the source (and we invalidate the server's cache).
22553
+ // Cache read-only tool calls with in-flight dedup. Cache key is
22554
+ // scoped per (workspace, server, tool, args) so two workspaces
22555
+ // calling the same read on the same server type don't share a
22556
+ // cached response — they're separate processes with potentially
22557
+ // different scopes (Slice 3b will make that explicit).
22332
22558
  if (isReadOnlyTool(toolName)) {
22333
- const key = `mcp:${serverName}:${toolName}:${JSON.stringify(args || {})}`;
22334
- return responseCache$2.get(key, doCall, {
22559
+ const cacheKey = `mcp:${key}:${toolName}:${JSON.stringify(args || {})}`;
22560
+ return responseCache$2.get(cacheKey, doCall, {
22335
22561
  ttl: DEFAULT_TOOL_CACHE_TTL,
22336
22562
  });
22337
22563
  }
22338
22564
 
22339
22565
  // Write/mutation: invalidate any cached reads for this server
22340
22566
  // (safest default — broad invalidation when state changes)
22341
- responseCache$2.invalidatePrefix(`mcp:${serverName}:`);
22567
+ responseCache$2.invalidatePrefix(`mcp:${key}:`);
22342
22568
  return doCall();
22343
22569
  } catch (error) {
22344
22570
  console.error(
@@ -22358,13 +22584,15 @@ const mcpController$3 = {
22358
22584
  *
22359
22585
  * @param {BrowserWindow} win the main window
22360
22586
  * @param {string} serverName the server name
22587
+ * @param {string|null} workspaceId active workspace id (Slice 3a)
22361
22588
  * @returns {{ tools } | { error, message }}
22362
22589
  */
22363
- listTools: async (win, serverName) => {
22590
+ listTools: async (win, serverName, workspaceId) => {
22591
+ const key = serverKey(workspaceId, serverName);
22364
22592
  try {
22365
- const server = activeServers.get(serverName);
22593
+ const server = activeServers.get(key);
22366
22594
  if (!server || !server.client) {
22367
- throw new Error(`Server not connected: ${serverName}`);
22595
+ throw new Error(`Server not connected: ${key}`);
22368
22596
  }
22369
22597
 
22370
22598
  // Refresh tool list from server
@@ -22396,13 +22624,15 @@ const mcpController$3 = {
22396
22624
  *
22397
22625
  * @param {BrowserWindow} win the main window
22398
22626
  * @param {string} serverName the server name
22627
+ * @param {string|null} workspaceId active workspace id (Slice 3a)
22399
22628
  * @returns {{ resources } | { error, message }}
22400
22629
  */
22401
- listResources: async (win, serverName) => {
22630
+ listResources: async (win, serverName, workspaceId) => {
22631
+ const key = serverKey(workspaceId, serverName);
22402
22632
  try {
22403
- const server = activeServers.get(serverName);
22633
+ const server = activeServers.get(key);
22404
22634
  if (!server || !server.client) {
22405
- throw new Error(`Server not connected: ${serverName}`);
22635
+ throw new Error(`Server not connected: ${key}`);
22406
22636
  }
22407
22637
 
22408
22638
  const resourcesResult = await server.client.listResources();
@@ -22434,13 +22664,15 @@ const mcpController$3 = {
22434
22664
  * @param {BrowserWindow} win the main window
22435
22665
  * @param {string} serverName the server name
22436
22666
  * @param {string} uri the resource URI
22667
+ * @param {string|null} workspaceId active workspace id (Slice 3a)
22437
22668
  * @returns {{ resource } | { error, message }}
22438
22669
  */
22439
- readResource: async (win, serverName, uri) => {
22670
+ readResource: async (win, serverName, uri, workspaceId) => {
22671
+ const key = serverKey(workspaceId, serverName);
22440
22672
  try {
22441
- const server = activeServers.get(serverName);
22673
+ const server = activeServers.get(key);
22442
22674
  if (!server || !server.client) {
22443
- throw new Error(`Server not connected: ${serverName}`);
22675
+ throw new Error(`Server not connected: ${key}`);
22444
22676
  }
22445
22677
 
22446
22678
  const result = await server.client.readResource({ uri });
@@ -22469,8 +22701,9 @@ const mcpController$3 = {
22469
22701
  * @param {string} serverName the server name
22470
22702
  * @returns {{ status, tools, error }}
22471
22703
  */
22472
- getServerStatus: (win, serverName) => {
22473
- const server = activeServers.get(serverName);
22704
+ getServerStatus: (win, serverName, workspaceId) => {
22705
+ const key = serverKey(workspaceId, serverName);
22706
+ const server = activeServers.get(key);
22474
22707
  if (!server) {
22475
22708
  return {
22476
22709
  serverName,
@@ -22603,7 +22836,7 @@ const mcpController$3 = {
22603
22836
  * @returns {{ success } | { error, message }}
22604
22837
  */
22605
22838
  runAuth: async (win, mcpConfig, credentials, authCommand) => {
22606
- const { spawn } = require$$9$1;
22839
+ const { spawn } = require$$11;
22607
22840
 
22608
22841
  const env = cleanEnvForChildProcess();
22609
22842
 
@@ -22732,12 +22965,39 @@ const mcpController$3 = {
22732
22965
  `[mcpController] Stopping all servers (${activeServers.size} active)`,
22733
22966
  );
22734
22967
  const promises = [];
22735
- for (const [serverName] of activeServers) {
22736
- promises.push(mcpController$3.stopServer(null, serverName));
22968
+ // Slice 3a: keys are compound `(workspaceId, serverName)`. Parse
22969
+ // to call stopServer with the original args.
22970
+ for (const [key] of activeServers) {
22971
+ const { workspaceId, serverName } = parseServerKey(key);
22972
+ promises.push(mcpController$3.stopServer(null, serverName, workspaceId));
22737
22973
  }
22738
22974
  await Promise.allSettled(promises);
22739
22975
  console.log("[mcpController] All servers stopped");
22740
22976
  },
22977
+
22978
+ /**
22979
+ * stopServersForWorkspace
22980
+ * Stop every server keyed under the given workspaceId. Called when
22981
+ * a workspace unmounts so its scoped MCP processes don't leak.
22982
+ *
22983
+ * @param {string} workspaceId the workspace whose servers to stop
22984
+ */
22985
+ stopServersForWorkspace: async (workspaceId) => {
22986
+ if (!workspaceId) return;
22987
+ const promises = [];
22988
+ for (const [key] of activeServers) {
22989
+ const parsed = parseServerKey(key);
22990
+ if (parsed.workspaceId !== workspaceId) continue;
22991
+ promises.push(
22992
+ mcpController$3.stopServer(null, parsed.serverName, workspaceId),
22993
+ );
22994
+ }
22995
+ if (promises.length === 0) return;
22996
+ console.log(
22997
+ `[mcpController] Stopping ${promises.length} server(s) for workspace ${workspaceId}`,
22998
+ );
22999
+ await Promise.allSettled(promises);
23000
+ },
22741
23001
  };
22742
23002
 
22743
23003
  mcpController$4.exports = mcpController$3;
@@ -47743,7 +48003,7 @@ var mcpDashServerController_1 = mcpDashServerController$4;
47743
48003
  * can use the Chat widget without a separate API key.
47744
48004
  */
47745
48005
 
47746
- const { spawn, execSync } = require$$9$1;
48006
+ const { spawn, execSync } = require$$11;
47747
48007
  const {
47748
48008
  LLM_STREAM_DELTA: LLM_STREAM_DELTA$2,
47749
48009
  LLM_STREAM_TOOL_CALL: LLM_STREAM_TOOL_CALL$2,
@@ -62132,13 +62392,27 @@ const mcpApi$2 = {
62132
62392
  * @param {string} serverName unique name for this server instance
62133
62393
  * @param {object} mcpConfig { transport, command, args, envMapping }
62134
62394
  * @param {object} credentials decrypted credentials object
62395
+ * @param {string|null} workspaceId active workspace id (Slice 3a) —
62396
+ * server processes are keyed per workspace.
62397
+ * @param {object|null} pathScope (Slice 3b) — when provided, the
62398
+ * workspace's union of granted paths overrides the server's
62399
+ * path-style credentials at spawn time. Shape:
62400
+ * `{ readPaths, writePaths, allowedPaths }`.
62135
62401
  * @returns {Promise<{ success, serverName, tools, status } | { error, message }>}
62136
62402
  */
62137
- startServer: (serverName, mcpConfig, credentials) =>
62403
+ startServer: (
62404
+ serverName,
62405
+ mcpConfig,
62406
+ credentials,
62407
+ workspaceId = null,
62408
+ pathScope = null,
62409
+ ) =>
62138
62410
  ipcRenderer$i.invoke(MCP_START_SERVER, {
62139
62411
  serverName,
62140
62412
  mcpConfig,
62141
62413
  credentials,
62414
+ workspaceId,
62415
+ pathScope,
62142
62416
  }),
62143
62417
 
62144
62418
  /**
@@ -62146,19 +62420,22 @@ const mcpApi$2 = {
62146
62420
  * Stop a running MCP server
62147
62421
  *
62148
62422
  * @param {string} serverName the server to stop
62423
+ * @param {string|null} workspaceId active workspace id (Slice 3a)
62149
62424
  * @returns {Promise<{ success, serverName } | { error, message }>}
62150
62425
  */
62151
- stopServer: (serverName) =>
62152
- ipcRenderer$i.invoke(MCP_STOP_SERVER, { serverName }),
62426
+ stopServer: (serverName, workspaceId = null) =>
62427
+ ipcRenderer$i.invoke(MCP_STOP_SERVER, { serverName, workspaceId }),
62153
62428
 
62154
62429
  /**
62155
62430
  * listTools
62156
62431
  * List available tools for a running MCP server
62157
62432
  *
62158
62433
  * @param {string} serverName the server name
62434
+ * @param {string|null} workspaceId active workspace id (Slice 3a)
62159
62435
  * @returns {Promise<{ tools } | { error, message }>}
62160
62436
  */
62161
- listTools: (serverName) => ipcRenderer$i.invoke(MCP_LIST_TOOLS, { serverName }),
62437
+ listTools: (serverName, workspaceId = null) =>
62438
+ ipcRenderer$i.invoke(MCP_LIST_TOOLS, { serverName, workspaceId }),
62162
62439
 
62163
62440
  /**
62164
62441
  * callTool
@@ -62173,6 +62450,9 @@ const mcpApi$2 = {
62173
62450
  * used to look up the widget's MCP permission manifest and gate
62174
62451
  * the call accordingly. Should be the npm package name of the
62175
62452
  * calling widget (e.g. "@trops/notes-summarizer").
62453
+ * @param {string|null} workspaceId active workspace id (Slice 3a) —
62454
+ * the server process is scoped per (workspace, server). Slice 3b
62455
+ * will tie path scope to this id.
62176
62456
  * @returns {Promise<{ result } | { error, message }>}
62177
62457
  */
62178
62458
  callTool: (
@@ -62181,6 +62461,7 @@ const mcpApi$2 = {
62181
62461
  args,
62182
62462
  allowedTools = null,
62183
62463
  widgetId = null,
62464
+ workspaceId = null,
62184
62465
  ) =>
62185
62466
  ipcRenderer$i.invoke(MCP_CALL_TOOL, {
62186
62467
  serverName,
@@ -62188,6 +62469,7 @@ const mcpApi$2 = {
62188
62469
  args,
62189
62470
  allowedTools,
62190
62471
  widgetId,
62472
+ workspaceId,
62191
62473
  }),
62192
62474
 
62193
62475
  /**
@@ -62195,10 +62477,11 @@ const mcpApi$2 = {
62195
62477
  * List available resources for a running MCP server
62196
62478
  *
62197
62479
  * @param {string} serverName the server name
62480
+ * @param {string|null} workspaceId active workspace id (Slice 3a)
62198
62481
  * @returns {Promise<{ resources } | { error, message }>}
62199
62482
  */
62200
- listResources: (serverName) =>
62201
- ipcRenderer$i.invoke(MCP_LIST_RESOURCES, { serverName }),
62483
+ listResources: (serverName, workspaceId = null) =>
62484
+ ipcRenderer$i.invoke(MCP_LIST_RESOURCES, { serverName, workspaceId }),
62202
62485
 
62203
62486
  /**
62204
62487
  * readResource
@@ -62206,20 +62489,22 @@ const mcpApi$2 = {
62206
62489
  *
62207
62490
  * @param {string} serverName the server name
62208
62491
  * @param {string} uri the resource URI
62492
+ * @param {string|null} workspaceId active workspace id (Slice 3a)
62209
62493
  * @returns {Promise<{ resource } | { error, message }>}
62210
62494
  */
62211
- readResource: (serverName, uri) =>
62212
- ipcRenderer$i.invoke(MCP_READ_RESOURCE, { serverName, uri }),
62495
+ readResource: (serverName, uri, workspaceId = null) =>
62496
+ ipcRenderer$i.invoke(MCP_READ_RESOURCE, { serverName, uri, workspaceId }),
62213
62497
 
62214
62498
  /**
62215
62499
  * getServerStatus
62216
62500
  * Get the connection status of a server
62217
62501
  *
62218
62502
  * @param {string} serverName the server name
62503
+ * @param {string|null} workspaceId active workspace id (Slice 3a)
62219
62504
  * @returns {Promise<{ status, tools, error }>}
62220
62505
  */
62221
- getServerStatus: (serverName) =>
62222
- ipcRenderer$i.invoke(MCP_SERVER_STATUS, { serverName }),
62506
+ getServerStatus: (serverName, workspaceId = null) =>
62507
+ ipcRenderer$i.invoke(MCP_SERVER_STATUS, { serverName, workspaceId }),
62223
62508
 
62224
62509
  /**
62225
62510
  * getCatalog