tabby-mcp-server 1.3.0 → 1.4.2

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/dist/index.js CHANGED
@@ -6726,6 +6726,7 @@ exports["default"] = def;
6726
6726
 
6727
6727
  Object.defineProperty(exports, "__esModule", ({ value: true }));
6728
6728
  const code_1 = __webpack_require__(5765);
6729
+ const util_1 = __webpack_require__(4227);
6729
6730
  const codegen_1 = __webpack_require__(1410);
6730
6731
  const error = {
6731
6732
  message: ({ schemaCode }) => (0, codegen_1.str) `must match pattern "${schemaCode}"`,
@@ -6738,11 +6739,19 @@ const def = {
6738
6739
  $data: true,
6739
6740
  error,
6740
6741
  code(cxt) {
6741
- const { data, $data, schema, schemaCode, it } = cxt;
6742
- // TODO regexp should be wrapped in try/catchs
6742
+ const { gen, data, $data, schema, schemaCode, it } = cxt;
6743
6743
  const u = it.opts.unicodeRegExp ? "u" : "";
6744
- const regExp = $data ? (0, codegen_1._) `(new RegExp(${schemaCode}, ${u}))` : (0, code_1.usePattern)(cxt, schema);
6745
- cxt.fail$data((0, codegen_1._) `!${regExp}.test(${data})`);
6744
+ if ($data) {
6745
+ const { regExp } = it.opts.code;
6746
+ const regExpCode = regExp.code === "new RegExp" ? (0, codegen_1._) `new RegExp` : (0, util_1.useFunc)(gen, regExp);
6747
+ const valid = gen.let("valid");
6748
+ gen.try(() => gen.assign(valid, (0, codegen_1._) `${regExpCode}(${schemaCode}, ${u}).test(${data})`), () => gen.assign(valid, false));
6749
+ cxt.fail$data((0, codegen_1._) `!${valid}`);
6750
+ }
6751
+ else {
6752
+ const regExp = (0, code_1.usePattern)(cxt, schema);
6753
+ cxt.fail$data((0, codegen_1._) `!${regExp}.test(${data})`);
6754
+ }
6746
6755
  },
6747
6756
  };
6748
6757
  exports["default"] = def;
@@ -25022,7 +25031,7 @@ var parseValues = function parseQueryStringValues(str, options) {
25022
25031
  var cleanStr = options.ignoreQueryPrefix ? str.replace(/^\?/, '') : str;
25023
25032
  cleanStr = cleanStr.replace(/%5B/gi, '[').replace(/%5D/gi, ']');
25024
25033
 
25025
- var limit = options.parameterLimit === Infinity ? undefined : options.parameterLimit;
25034
+ var limit = options.parameterLimit === Infinity ? void undefined : options.parameterLimit;
25026
25035
  var parts = cleanStr.split(
25027
25036
  options.delimiter,
25028
25037
  options.throwOnLimitExceeded ? limit + 1 : limit
@@ -25089,6 +25098,13 @@ var parseValues = function parseQueryStringValues(str, options) {
25089
25098
  val = isArray(val) ? [val] : val;
25090
25099
  }
25091
25100
 
25101
+ if (options.comma && isArray(val) && val.length > options.arrayLimit) {
25102
+ if (options.throwOnLimitExceeded) {
25103
+ throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.');
25104
+ }
25105
+ val = utils.combine([], val, options.arrayLimit, options.plainObjects);
25106
+ }
25107
+
25092
25108
  if (key !== null) {
25093
25109
  var existing = has.call(obj, key);
25094
25110
  if (existing && options.duplicates === 'combine') {
@@ -25139,17 +25155,21 @@ var parseObject = function (chain, val, options, valuesParsed) {
25139
25155
  var cleanRoot = root.charAt(0) === '[' && root.charAt(root.length - 1) === ']' ? root.slice(1, -1) : root;
25140
25156
  var decodedRoot = options.decodeDotInKeys ? cleanRoot.replace(/%2E/g, '.') : cleanRoot;
25141
25157
  var index = parseInt(decodedRoot, 10);
25142
- if (!options.parseArrays && decodedRoot === '') {
25143
- obj = { 0: leaf };
25144
- } else if (
25145
- !isNaN(index)
25158
+ var isValidArrayIndex = !isNaN(index)
25146
25159
  && root !== decodedRoot
25147
25160
  && String(index) === decodedRoot
25148
25161
  && index >= 0
25149
- && (options.parseArrays && index <= options.arrayLimit)
25150
- ) {
25162
+ && options.parseArrays;
25163
+ if (!options.parseArrays && decodedRoot === '') {
25164
+ obj = { 0: leaf };
25165
+ } else if (isValidArrayIndex && index < options.arrayLimit) {
25151
25166
  obj = [];
25152
25167
  obj[index] = leaf;
25168
+ } else if (isValidArrayIndex && options.throwOnLimitExceeded) {
25169
+ throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.');
25170
+ } else if (isValidArrayIndex) {
25171
+ obj[index] = leaf;
25172
+ utils.markOverflow(obj, index);
25153
25173
  } else if (decodedRoot !== '__proto__') {
25154
25174
  obj[decodedRoot] = leaf;
25155
25175
  }
@@ -25189,7 +25209,7 @@ var splitKeyIntoSegments = function splitKeyIntoSegments(givenKey, options) {
25189
25209
  }
25190
25210
  }
25191
25211
 
25192
- keys.push(parent);
25212
+ keys[keys.length] = parent;
25193
25213
  }
25194
25214
 
25195
25215
  var i = 0;
@@ -25203,7 +25223,7 @@ var splitKeyIntoSegments = function splitKeyIntoSegments(givenKey, options) {
25203
25223
  }
25204
25224
  }
25205
25225
 
25206
- keys.push(segment[1]);
25226
+ keys[keys.length] = segment[1];
25207
25227
  }
25208
25228
 
25209
25229
  if (segment) {
@@ -25211,7 +25231,7 @@ var splitKeyIntoSegments = function splitKeyIntoSegments(givenKey, options) {
25211
25231
  throw new RangeError('Input depth exceeded depth option of ' + options.depth + ' and strictDepth is true');
25212
25232
  }
25213
25233
 
25214
- keys.push('[' + key.slice(segment.index) + ']');
25234
+ keys[keys.length] = '[' + key.slice(segment.index) + ']';
25215
25235
  }
25216
25236
 
25217
25237
  return keys;
@@ -25721,7 +25741,7 @@ var setMaxIndex = function setMaxIndex(obj, maxIndex) {
25721
25741
  var hexTable = (function () {
25722
25742
  var array = [];
25723
25743
  for (var i = 0; i < 256; ++i) {
25724
- array.push('%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase());
25744
+ array[array.length] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase();
25725
25745
  }
25726
25746
 
25727
25747
  return array;
@@ -25737,7 +25757,7 @@ var compactQueue = function compactQueue(queue) {
25737
25757
 
25738
25758
  for (var j = 0; j < obj.length; ++j) {
25739
25759
  if (typeof obj[j] !== 'undefined') {
25740
- compacted.push(obj[j]);
25760
+ compacted[compacted.length] = obj[j];
25741
25761
  }
25742
25762
  }
25743
25763
 
@@ -25765,7 +25785,11 @@ var merge = function merge(target, source, options) {
25765
25785
 
25766
25786
  if (typeof source !== 'object' && typeof source !== 'function') {
25767
25787
  if (isArray(target)) {
25768
- target.push(source);
25788
+ var nextIndex = target.length;
25789
+ if (options && typeof options.arrayLimit === 'number' && nextIndex > options.arrayLimit) {
25790
+ return markOverflow(arrayToObject(target.concat(source), options), nextIndex);
25791
+ }
25792
+ target[nextIndex] = source;
25769
25793
  } else if (target && typeof target === 'object') {
25770
25794
  if (isOverflow(target)) {
25771
25795
  // Add at next numeric index for overflow objects
@@ -25798,7 +25822,11 @@ var merge = function merge(target, source, options) {
25798
25822
  }
25799
25823
  return markOverflow(result, getMaxIndex(source) + 1);
25800
25824
  }
25801
- return [target].concat(source);
25825
+ var combined = [target].concat(source);
25826
+ if (options && typeof options.arrayLimit === 'number' && combined.length > options.arrayLimit) {
25827
+ return markOverflow(arrayToObject(combined, options), combined.length - 1);
25828
+ }
25829
+ return combined;
25802
25830
  }
25803
25831
 
25804
25832
  var mergeTarget = target;
@@ -25813,7 +25841,7 @@ var merge = function merge(target, source, options) {
25813
25841
  if (targetItem && typeof targetItem === 'object' && item && typeof item === 'object') {
25814
25842
  target[i] = merge(targetItem, item, options);
25815
25843
  } else {
25816
- target.push(item);
25844
+ target[target.length] = item;
25817
25845
  }
25818
25846
  } else {
25819
25847
  target[i] = item;
@@ -25830,6 +25858,17 @@ var merge = function merge(target, source, options) {
25830
25858
  } else {
25831
25859
  acc[key] = value;
25832
25860
  }
25861
+
25862
+ if (isOverflow(source) && !isOverflow(acc)) {
25863
+ markOverflow(acc, getMaxIndex(source));
25864
+ }
25865
+ if (isOverflow(acc)) {
25866
+ var keyNum = parseInt(key, 10);
25867
+ if (String(keyNum) === key && keyNum >= 0 && keyNum > getMaxIndex(acc)) {
25868
+ setMaxIndex(acc, keyNum);
25869
+ }
25870
+ }
25871
+
25833
25872
  return acc;
25834
25873
  }, mergeTarget);
25835
25874
  };
@@ -25946,8 +25985,8 @@ var compact = function compact(value) {
25946
25985
  var key = keys[j];
25947
25986
  var val = obj[key];
25948
25987
  if (typeof val === 'object' && val !== null && refs.indexOf(val) === -1) {
25949
- queue.push({ obj: obj, prop: key });
25950
- refs.push(val);
25988
+ queue[queue.length] = { obj: obj, prop: key };
25989
+ refs[refs.length] = val;
25951
25990
  }
25952
25991
  }
25953
25992
  }
@@ -25989,7 +26028,7 @@ var maybeMap = function maybeMap(val, fn) {
25989
26028
  if (isArray(val)) {
25990
26029
  var mapped = [];
25991
26030
  for (var i = 0; i < val.length; i += 1) {
25992
- mapped.push(fn(val[i]));
26031
+ mapped[mapped.length] = fn(val[i]);
25993
26032
  }
25994
26033
  return mapped;
25995
26034
  }
@@ -26006,6 +26045,7 @@ module.exports = {
26006
26045
  isBuffer: isBuffer,
26007
26046
  isOverflow: isOverflow,
26008
26047
  isRegExp: isRegExp,
26048
+ markOverflow: markOverflow,
26009
26049
  maybeMap: maybeMap,
26010
26050
  merge: merge
26011
26051
  };
@@ -32101,8 +32141,7 @@ let McpSettingsTabComponent = class McpSettingsTabComponent {
32101
32141
  }
32102
32142
  exportLogsToFile() {
32103
32143
  const logs = this.logger.exportLogs();
32104
- const json = JSON.stringify(logs, null, 2);
32105
- const blob = new Blob([json], { type: 'application/json' });
32144
+ const blob = new Blob([logs], { type: 'application/json' });
32106
32145
  const url = URL.createObjectURL(blob);
32107
32146
  const a = document.createElement('a');
32108
32147
  a.href = url;
@@ -32175,7 +32214,7 @@ let McpSettingsTabComponent = class McpSettingsTabComponent {
32175
32214
  return JSON.stringify({
32176
32215
  mcpServers: {
32177
32216
  'Tabby MCP': {
32178
- type: 'sse',
32217
+ type: 'streamable_http',
32179
32218
  url: `http://localhost:${port}/mcp`
32180
32219
  }
32181
32220
  }
@@ -33674,11 +33713,16 @@ let McpService = class McpService {
33674
33713
  constructor(config, logger) {
33675
33714
  this.config = config;
33676
33715
  this.logger = logger;
33716
+ // Per-session McpServer instances - each client gets its own server to avoid SDK Bug #1459
33717
+ // This prevents one client's disconnect from affecting other clients' pending requests
33718
+ this.sessionServers = {};
33677
33719
  this.legacyTransports = {};
33678
33720
  this.streamableTransports = {};
33679
33721
  this.sockets = new Set();
33680
33722
  this.isRunning = false;
33681
33723
  this.toolCategories = [];
33724
+ // Store tool definitions for registering with each new server
33725
+ this.registeredTools = [];
33682
33726
  // ============================================================
33683
33727
  // CONNECTION MONITORING & MANAGEMENT
33684
33728
  // ============================================================
@@ -33686,20 +33730,41 @@ let McpService = class McpService {
33686
33730
  this.initializeServer();
33687
33731
  }
33688
33732
  /**
33689
- * Initialize the MCP server
33733
+ * Initialize the MCP service (no longer creates a single server)
33690
33734
  */
33691
33735
  initializeServer() {
33692
- // Initialize MCP Server
33693
- this.server = new mcp_js_1.McpServer({
33736
+ // Configure Express - servers are created per-session now
33737
+ this.configureExpress();
33738
+ this.logger.info('MCP Service initialized (Streamable HTTP + Legacy SSE) - per-session servers');
33739
+ }
33740
+ /**
33741
+ * Create a new McpServer instance for a specific session
33742
+ * Each client gets its own server to avoid SDK Bug #1459
33743
+ */
33744
+ createServerForSession(sessionId) {
33745
+ const server = new mcp_js_1.McpServer({
33694
33746
  name: 'Tabby MCP',
33695
33747
  version: version_1.PLUGIN_VERSION
33696
33748
  });
33697
- // Configure Express
33698
- this.configureExpress();
33699
- this.logger.info('MCP Server initialized (Streamable HTTP + Legacy SSE)');
33749
+ // Register all tools with this server instance
33750
+ for (const toolDef of this.registeredTools) {
33751
+ server.tool(toolDef.name, toolDef.description, toolDef.schema, toolDef.handler);
33752
+ }
33753
+ this.sessionServers[sessionId] = server;
33754
+ this.logger.debug(`[Session ${sessionId}] Created new McpServer with ${this.registeredTools.length} tools`);
33755
+ return server;
33700
33756
  }
33701
33757
  /**
33702
- * Register a tool category with the MCP server
33758
+ * Clean up server for a specific session
33759
+ */
33760
+ cleanupServerForSession(sessionId) {
33761
+ if (this.sessionServers[sessionId]) {
33762
+ delete this.sessionServers[sessionId];
33763
+ this.logger.debug(`[Session ${sessionId}] Cleaned up McpServer`);
33764
+ }
33765
+ }
33766
+ /**
33767
+ * Register a tool category - stores definitions for per-session server creation
33703
33768
  */
33704
33769
  registerToolCategory(category) {
33705
33770
  this.toolCategories.push(category);
@@ -33710,20 +33775,32 @@ let McpService = class McpService {
33710
33775
  ? tool.schema.shape
33711
33776
  : tool.schema;
33712
33777
  this.logger.debug(`Registering tool: ${tool.name} with schema keys: ${Object.keys(rawShape || {}).join(', ')}`);
33713
- this.server.tool(tool.name, tool.description, rawShape, tool.handler);
33714
- this.logger.info(`Registered tool: ${tool.name}`);
33778
+ // Store tool definition for later registration with per-session servers
33779
+ this.registeredTools.push({
33780
+ name: tool.name,
33781
+ description: tool.description,
33782
+ schema: rawShape,
33783
+ handler: tool.handler
33784
+ });
33785
+ this.logger.info(`Registered tool definition: ${tool.name}`);
33715
33786
  });
33716
33787
  }
33717
33788
  /**
33718
- * Register a single tool
33789
+ * Register a single tool - stores definition for per-session server creation
33719
33790
  */
33720
33791
  registerTool(tool) {
33721
33792
  // Extract the raw shape from z.object() for MCP SDK compatibility
33722
33793
  const rawShape = tool.schema && typeof tool.schema === 'object' && 'shape' in tool.schema
33723
33794
  ? tool.schema.shape
33724
33795
  : tool.schema;
33725
- this.server.tool(tool.name, tool.description, rawShape, tool.handler);
33726
- this.logger.info(`Registered tool: ${tool.name}`);
33796
+ // Store tool definition for later registration with per-session servers
33797
+ this.registeredTools.push({
33798
+ name: tool.name,
33799
+ description: tool.description,
33800
+ schema: rawShape,
33801
+ handler: tool.handler
33802
+ });
33803
+ this.logger.info(`Registered tool definition: ${tool.name}`);
33727
33804
  }
33728
33805
  /**
33729
33806
  * Configure Express server with Streamable HTTP and SSE endpoints
@@ -33737,7 +33814,7 @@ let McpService = class McpService {
33737
33814
  res.status(200).json({
33738
33815
  status: 'ok',
33739
33816
  server: 'Tabby MCP',
33740
- version: '1.1.3',
33817
+ version: version_1.PLUGIN_VERSION,
33741
33818
  transport: 'StreamableHTTP + SSE',
33742
33819
  uptime: process.uptime()
33743
33820
  });
@@ -33746,7 +33823,7 @@ let McpService = class McpService {
33746
33823
  this.app.get('/info', (_, res) => {
33747
33824
  res.status(200).json({
33748
33825
  name: 'Tabby MCP',
33749
- version: '1.1.3',
33826
+ version: version_1.PLUGIN_VERSION,
33750
33827
  protocolVersion: '2025-03-26',
33751
33828
  transports: ['streamable-http', 'sse'],
33752
33829
  endpoints: {
@@ -33855,11 +33932,13 @@ let McpService = class McpService {
33855
33932
  this.logger.info(`Streamable HTTP: Transport closed (onclose): ${sessionId}`);
33856
33933
  delete this.streamableTransports[sessionId];
33857
33934
  this.sessionMetadata.delete(sessionId);
33935
+ this.cleanupServerForSession(sessionId); // Clean up the per-session server
33858
33936
  };
33859
33937
  this.streamableTransports[sessionId] = transport;
33860
33938
  this.initSessionMetadata(sessionId, 'streamable', req);
33861
- // Connect to MCP server
33862
- await this.server.connect(transport);
33939
+ // Create per-session McpServer and connect to transport
33940
+ const server = this.createServerForSession(sessionId);
33941
+ await server.connect(transport);
33863
33942
  this.logger.info(`Streamable HTTP: New session created: ${sessionId}`);
33864
33943
  }
33865
33944
  // Set session ID header in response
@@ -33927,14 +34006,18 @@ let McpService = class McpService {
33927
34006
  clearInterval(heartbeatInterval);
33928
34007
  delete this.legacyTransports[sessionId];
33929
34008
  this.sessionMetadata.delete(sessionId);
34009
+ this.cleanupServerForSession(sessionId); // Clean up the per-session server
33930
34010
  });
33931
34011
  res.on('error', (err) => {
33932
34012
  this.logger.error(`Legacy SSE: Connection error sessionId=${sessionId}`, err);
33933
34013
  clearInterval(heartbeatInterval);
33934
34014
  delete this.legacyTransports[sessionId];
33935
34015
  this.sessionMetadata.delete(sessionId);
34016
+ this.cleanupServerForSession(sessionId); // Clean up the per-session server
33936
34017
  });
33937
- await this.server.connect(transport);
34018
+ // Create per-session McpServer and connect to transport
34019
+ const server = this.createServerForSession(sessionId);
34020
+ await server.connect(transport);
33938
34021
  }
33939
34022
  catch (error) {
33940
34023
  this.logger.error('Legacy SSE: Failed to establish connection:', error);
@@ -34098,6 +34181,9 @@ let McpService = class McpService {
34098
34181
  }
34099
34182
  }
34100
34183
  this.streamableTransports = {};
34184
+ // Clear all per-session servers
34185
+ this.sessionServers = {};
34186
+ this.logger.debug('Cleared all per-session McpServer instances');
34101
34187
  // Force close all active connections
34102
34188
  if (this.sockets.size > 0) {
34103
34189
  this.logger.info(`Closing ${this.sockets.size} active connections`);
@@ -34355,8 +34441,11 @@ let SFTPToolCategory = class SFTPToolCategory extends base_tool_category_1.BaseT
34355
34441
  this.config = config;
34356
34442
  this.terminalTools = terminalTools;
34357
34443
  this.name = 'sftp';
34358
- // Cache SFTP sessions to avoid reopening
34359
- this.sftpSessionCache = new WeakMap();
34444
+ // Cache SFTP sessions with TTL and SSH session reference
34445
+ // Key: sshSession object (using Map because we need to iterate for cleanup)
34446
+ // Value: { sftpSession, createdAt } - createdAt for TTL expiration
34447
+ this.sftpSessionCache = new Map();
34448
+ this.SFTP_SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes TTL
34360
34449
  // Note: Session IDs are now managed by TerminalToolCategory for consistency
34361
34450
  // Transfer task queue
34362
34451
  this.transferTasks = new Map();
@@ -34412,6 +34501,57 @@ let SFTPToolCategory = class SFTPToolCategory extends base_tool_category_1.BaseT
34412
34501
  this.emitTransferUpdate();
34413
34502
  }
34414
34503
  }
34504
+ /**
34505
+ * Wait for transfer to complete with a polling fallback.
34506
+ *
34507
+ * Tabby's SFTP API (based on russh) may not always resolve the Promise
34508
+ * even when the transfer is complete. This method uses Promise.race to
34509
+ * detect completion via polling the isComplete() method.
34510
+ *
34511
+ * Also checks for SSH session disconnection to prevent infinite waits.
34512
+ *
34513
+ * @param sftpPromise - The original sftpSession.upload/download promise
34514
+ * @param transferObj - StreamFileUpload or StreamFileDownload with isComplete()
34515
+ * @param sshSession - Optional SSH session to check for disconnection
34516
+ * @param pollIntervalMs - How often to check isComplete()
34517
+ * @param maxWaitMs - Maximum time to wait before timing out (0 = no timeout)
34518
+ */
34519
+ async waitForTransferComplete(sftpPromise, transferObj, sshSession, pollIntervalMs = 500, maxWaitMs = 0) {
34520
+ const startTime = Date.now();
34521
+ // Create a polling promise that resolves when transfer is complete
34522
+ const pollingPromise = new Promise((resolve, reject) => {
34523
+ const checkComplete = () => {
34524
+ // Check if SSH session disconnected
34525
+ if (sshSession && sshSession.open === false) {
34526
+ this.logger.warn('[SFTP] SSH session disconnected during transfer');
34527
+ reject(new Error('SSH connection lost during transfer'));
34528
+ return;
34529
+ }
34530
+ // Check if cancelled
34531
+ if (transferObj.isCancelled()) {
34532
+ reject(new Error('Transfer cancelled'));
34533
+ return;
34534
+ }
34535
+ // Check if complete
34536
+ if (transferObj.isComplete()) {
34537
+ this.logger.debug('[SFTP] Transfer detected as complete via polling');
34538
+ resolve('completed_via_polling');
34539
+ return;
34540
+ }
34541
+ // Check timeout (if maxWaitMs > 0)
34542
+ if (maxWaitMs > 0 && (Date.now() - startTime) > maxWaitMs) {
34543
+ reject(new Error(`Transfer timed out after ${maxWaitMs}ms`));
34544
+ return;
34545
+ }
34546
+ // Continue polling
34547
+ setTimeout(checkComplete, pollIntervalMs);
34548
+ };
34549
+ // Start polling with a slight delay to let the native promise resolve first
34550
+ setTimeout(checkComplete, pollIntervalMs);
34551
+ });
34552
+ // Race between the original promise and our polling promise
34553
+ return Promise.race([sftpPromise, pollingPromise]);
34554
+ }
34415
34555
  // ============== Public methods for UI ==============
34416
34556
  /** Get current snapshot of all transfers */
34417
34557
  getTransfers() {
@@ -34559,31 +34699,87 @@ let SFTPToolCategory = class SFTPToolCategory extends base_tool_category_1.BaseT
34559
34699
  try {
34560
34700
  const sshSession = sshTab.sshSession;
34561
34701
  if (!sshSession) {
34702
+ this.logger.debug('[getSFTPSession] No sshSession on tab');
34562
34703
  return null;
34563
34704
  }
34564
- // Key the cache by the actual SSH Session object, not the Tab.
34565
- // This ensures that if the Tab reconnects (getting a new sshSession object),
34566
- // we automatically get a cache miss and create a new SFTP session.
34567
- // Old cached sessions will be garbage collected when the old sshSession is destroyed.
34568
- const cached = this.sftpSessionCache.get(sshSession);
34569
- if (cached) {
34570
- return cached;
34571
- }
34705
+ // Check if SSH session itself is still open
34572
34706
  if (!sshSession.open) {
34707
+ this.logger.debug('[getSFTPSession] sshSession.open is false');
34708
+ // Clear any stale cache entry
34709
+ this.sftpSessionCache.delete(sshSession);
34573
34710
  return null;
34574
34711
  }
34712
+ // Check cache first
34713
+ const cached = this.sftpSessionCache.get(sshSession);
34714
+ if (cached) {
34715
+ const age = Date.now() - cached.createdAt;
34716
+ // Check TTL expiration (proactive cleanup before issues occur)
34717
+ if (age > this.SFTP_SESSION_TTL_MS) {
34718
+ this.logger.info(`[getSFTPSession] SFTP session expired (age: ${Math.round(age / 1000)}s > TTL: ${this.SFTP_SESSION_TTL_MS / 1000}s). Recreating...`);
34719
+ this.sftpSessionCache.delete(sshSession);
34720
+ // Fall through to create new session
34721
+ }
34722
+ else {
34723
+ // CRITICAL: Validate cached session is still alive
34724
+ // Even within TTL, network issues can cause session to become stale
34725
+ try {
34726
+ // Quick health check with a lightweight operation
34727
+ await Promise.race([
34728
+ cached.sftpSession.stat('/'),
34729
+ new Promise((_, reject) => setTimeout(() => reject(new Error('SFTP health check timeout')), 3000))
34730
+ ]);
34731
+ this.logger.debug(`[getSFTPSession] Cached SFTP session validated (age: ${Math.round(age / 1000)}s)`);
34732
+ return cached.sftpSession;
34733
+ }
34734
+ catch (healthError) {
34735
+ // Cached session is stale/broken, remove and recreate
34736
+ this.logger.warn(`[getSFTPSession] Cached SFTP session failed health check: ${healthError.message}. Recreating...`);
34737
+ this.sftpSessionCache.delete(sshSession);
34738
+ // Fall through to create new session
34739
+ }
34740
+ }
34741
+ }
34742
+ // Check if openSFTP is available
34575
34743
  if (typeof sshSession.openSFTP !== 'function') {
34744
+ this.logger.debug('[getSFTPSession] sshSession.openSFTP is not a function');
34576
34745
  return null;
34577
34746
  }
34747
+ // Create new SFTP session
34748
+ this.logger.debug('[getSFTPSession] Opening new SFTP session...');
34578
34749
  const sftpSession = await sshSession.openSFTP();
34579
- this.sftpSessionCache.set(sshSession, sftpSession);
34750
+ // Store with creation timestamp for TTL management
34751
+ this.sftpSessionCache.set(sshSession, {
34752
+ sftpSession,
34753
+ createdAt: Date.now()
34754
+ });
34755
+ this.logger.info('[getSFTPSession] New SFTP session opened and cached');
34756
+ // Cleanup old entries from disconnected sessions
34757
+ this.cleanupStaleSFTPSessions();
34580
34758
  return sftpSession;
34581
34759
  }
34582
34760
  catch (error) {
34583
- this.logger.error('Failed to open SFTP session:', error.message || error);
34761
+ this.logger.error('[getSFTPSession] Failed to open SFTP session:', error.message || error);
34584
34762
  return null;
34585
34763
  }
34586
34764
  }
34765
+ /**
34766
+ * Cleanup stale SFTP sessions from closed SSH connections
34767
+ * Called periodically when creating new sessions
34768
+ */
34769
+ cleanupStaleSFTPSessions() {
34770
+ const now = Date.now();
34771
+ let cleaned = 0;
34772
+ for (const [sshSession, cached] of this.sftpSessionCache.entries()) {
34773
+ // Remove if SSH session is closed OR TTL expired
34774
+ if (sshSession.open === false || (now - cached.createdAt) > this.SFTP_SESSION_TTL_MS) {
34775
+ this.sftpSessionCache.delete(sshSession);
34776
+ cleaned++;
34777
+ }
34778
+ }
34779
+ if (cleaned > 0) {
34780
+ this.logger.debug(`[cleanupStaleSFTPSessions] Removed ${cleaned} stale entries`);
34781
+ }
34782
+ }
34587
34783
  // ============== BASIC SFTP OPERATIONS ==============
34588
34784
  createListFilesTool() {
34589
34785
  return {
@@ -35000,7 +35196,14 @@ Max upload size is configurable in Tabby Settings → MCP → SFTP.`,
35000
35196
  task.progress = Math.round((bytes / task.totalBytes) * 100);
35001
35197
  task.speed = bytes / ((Date.now() - task.startTime) / 1000);
35002
35198
  });
35003
- await sftpSession.upload(params.remotePath, fileUpload);
35199
+ // Wrap with polling fallback to handle Tabby SFTP API not resolving
35200
+ // Also pass sshSession to detect connection loss during transfer
35201
+ const sshSession = session.tab.sshSession;
35202
+ const uploadPromise = sftpSession.upload(params.remotePath, fileUpload);
35203
+ const result = await this.waitForTransferComplete(uploadPromise, fileUpload, sshSession);
35204
+ if (result === 'completed_via_polling') {
35205
+ this.logger.info(`[sftp_upload] Transfer completed via polling fallback`);
35206
+ }
35004
35207
  task.status = 'completed';
35005
35208
  task.progress = 100;
35006
35209
  task.bytesTransferred = task.totalBytes;
@@ -35180,7 +35383,14 @@ Max download size is configurable in Tabby Settings → MCP → SFTP.`,
35180
35383
  task.progress = Math.round((bytes / task.totalBytes) * 100);
35181
35384
  task.speed = bytes / ((Date.now() - task.startTime) / 1000);
35182
35385
  });
35183
- await sftpSession.download(params.remotePath, fileDownload);
35386
+ // Wrap with polling fallback to handle Tabby SFTP API not resolving
35387
+ // Also pass sshSession to detect connection loss during transfer
35388
+ const sshSession = session.tab.sshSession;
35389
+ const downloadPromise = sftpSession.download(params.remotePath, fileDownload);
35390
+ const result = await this.waitForTransferComplete(downloadPromise, fileDownload, sshSession);
35391
+ if (result === 'completed_via_polling') {
35392
+ this.logger.info(`[sftp_download] Transfer completed via polling fallback`);
35393
+ }
35184
35394
  task.status = 'completed';
35185
35395
  task.progress = 100;
35186
35396
  task.bytesTransferred = task.totalBytes;
@@ -38202,6 +38412,14 @@ module.exports = require("node:crypto");
38202
38412
 
38203
38413
  /***/ },
38204
38414
 
38415
+ /***/ 1692
38416
+ (module) {
38417
+
38418
+ "use strict";
38419
+ module.exports = require("node:tls");
38420
+
38421
+ /***/ },
38422
+
38205
38423
  /***/ 3136
38206
38424
  (module) {
38207
38425
 
@@ -38419,7 +38637,7 @@ exports.ExperimentalMcpServerTasks = ExperimentalMcpServerTasks;
38419
38637
  /***/ },
38420
38638
 
38421
38639
  /***/ 3200
38422
- (__unused_webpack_module, exports) {
38640
+ (__unused_webpack_module, exports, __webpack_require__) {
38423
38641
 
38424
38642
  "use strict";
38425
38643
 
@@ -38431,6 +38649,7 @@ exports.ExperimentalMcpServerTasks = ExperimentalMcpServerTasks;
38431
38649
  */
38432
38650
  Object.defineProperty(exports, "__esModule", ({ value: true }));
38433
38651
  exports.ExperimentalServerTasks = void 0;
38652
+ const types_js_1 = __webpack_require__(1294);
38434
38653
  /**
38435
38654
  * Experimental task features for low-level MCP servers.
38436
38655
  *
@@ -38464,6 +38683,162 @@ class ExperimentalServerTasks {
38464
38683
  requestStream(request, resultSchema, options) {
38465
38684
  return this._server.requestStream(request, resultSchema, options);
38466
38685
  }
38686
+ /**
38687
+ * Sends a sampling request and returns an AsyncGenerator that yields response messages.
38688
+ * The generator is guaranteed to end with either a 'result' or 'error' message.
38689
+ *
38690
+ * For task-augmented requests, yields 'taskCreated' and 'taskStatus' messages
38691
+ * before the final result.
38692
+ *
38693
+ * @example
38694
+ * ```typescript
38695
+ * const stream = server.experimental.tasks.createMessageStream({
38696
+ * messages: [{ role: 'user', content: { type: 'text', text: 'Hello' } }],
38697
+ * maxTokens: 100
38698
+ * }, {
38699
+ * onprogress: (progress) => {
38700
+ * // Handle streaming tokens via progress notifications
38701
+ * console.log('Progress:', progress.message);
38702
+ * }
38703
+ * });
38704
+ *
38705
+ * for await (const message of stream) {
38706
+ * switch (message.type) {
38707
+ * case 'taskCreated':
38708
+ * console.log('Task created:', message.task.taskId);
38709
+ * break;
38710
+ * case 'taskStatus':
38711
+ * console.log('Task status:', message.task.status);
38712
+ * break;
38713
+ * case 'result':
38714
+ * console.log('Final result:', message.result);
38715
+ * break;
38716
+ * case 'error':
38717
+ * console.error('Error:', message.error);
38718
+ * break;
38719
+ * }
38720
+ * }
38721
+ * ```
38722
+ *
38723
+ * @param params - The sampling request parameters
38724
+ * @param options - Optional request options (timeout, signal, task creation params, onprogress, etc.)
38725
+ * @returns AsyncGenerator that yields ResponseMessage objects
38726
+ *
38727
+ * @experimental
38728
+ */
38729
+ createMessageStream(params, options) {
38730
+ // Access client capabilities via the server
38731
+ const clientCapabilities = this._server.getClientCapabilities();
38732
+ // Capability check - only required when tools/toolChoice are provided
38733
+ if ((params.tools || params.toolChoice) && !clientCapabilities?.sampling?.tools) {
38734
+ throw new Error('Client does not support sampling tools capability.');
38735
+ }
38736
+ // Message structure validation - always validate tool_use/tool_result pairs.
38737
+ // These may appear even without tools/toolChoice in the current request when
38738
+ // a previous sampling request returned tool_use and this is a follow-up with results.
38739
+ if (params.messages.length > 0) {
38740
+ const lastMessage = params.messages[params.messages.length - 1];
38741
+ const lastContent = Array.isArray(lastMessage.content) ? lastMessage.content : [lastMessage.content];
38742
+ const hasToolResults = lastContent.some(c => c.type === 'tool_result');
38743
+ const previousMessage = params.messages.length > 1 ? params.messages[params.messages.length - 2] : undefined;
38744
+ const previousContent = previousMessage
38745
+ ? Array.isArray(previousMessage.content)
38746
+ ? previousMessage.content
38747
+ : [previousMessage.content]
38748
+ : [];
38749
+ const hasPreviousToolUse = previousContent.some(c => c.type === 'tool_use');
38750
+ if (hasToolResults) {
38751
+ if (lastContent.some(c => c.type !== 'tool_result')) {
38752
+ throw new Error('The last message must contain only tool_result content if any is present');
38753
+ }
38754
+ if (!hasPreviousToolUse) {
38755
+ throw new Error('tool_result blocks are not matching any tool_use from the previous message');
38756
+ }
38757
+ }
38758
+ if (hasPreviousToolUse) {
38759
+ // Extract tool_use IDs from previous message and tool_result IDs from current message
38760
+ const toolUseIds = new Set(previousContent.filter(c => c.type === 'tool_use').map(c => c.id));
38761
+ const toolResultIds = new Set(lastContent.filter(c => c.type === 'tool_result').map(c => c.toolUseId));
38762
+ if (toolUseIds.size !== toolResultIds.size || ![...toolUseIds].every(id => toolResultIds.has(id))) {
38763
+ throw new Error('ids of tool_result blocks and tool_use blocks from previous message do not match');
38764
+ }
38765
+ }
38766
+ }
38767
+ return this.requestStream({
38768
+ method: 'sampling/createMessage',
38769
+ params
38770
+ }, types_js_1.CreateMessageResultSchema, options);
38771
+ }
38772
+ /**
38773
+ * Sends an elicitation request and returns an AsyncGenerator that yields response messages.
38774
+ * The generator is guaranteed to end with either a 'result' or 'error' message.
38775
+ *
38776
+ * For task-augmented requests (especially URL-based elicitation), yields 'taskCreated'
38777
+ * and 'taskStatus' messages before the final result.
38778
+ *
38779
+ * @example
38780
+ * ```typescript
38781
+ * const stream = server.experimental.tasks.elicitInputStream({
38782
+ * mode: 'url',
38783
+ * message: 'Please authenticate',
38784
+ * elicitationId: 'auth-123',
38785
+ * url: 'https://example.com/auth'
38786
+ * }, {
38787
+ * task: { ttl: 300000 } // Task-augmented for long-running auth flow
38788
+ * });
38789
+ *
38790
+ * for await (const message of stream) {
38791
+ * switch (message.type) {
38792
+ * case 'taskCreated':
38793
+ * console.log('Task created:', message.task.taskId);
38794
+ * break;
38795
+ * case 'taskStatus':
38796
+ * console.log('Task status:', message.task.status);
38797
+ * break;
38798
+ * case 'result':
38799
+ * console.log('User action:', message.result.action);
38800
+ * break;
38801
+ * case 'error':
38802
+ * console.error('Error:', message.error);
38803
+ * break;
38804
+ * }
38805
+ * }
38806
+ * ```
38807
+ *
38808
+ * @param params - The elicitation request parameters
38809
+ * @param options - Optional request options (timeout, signal, task creation params, etc.)
38810
+ * @returns AsyncGenerator that yields ResponseMessage objects
38811
+ *
38812
+ * @experimental
38813
+ */
38814
+ elicitInputStream(params, options) {
38815
+ // Access client capabilities via the server
38816
+ const clientCapabilities = this._server.getClientCapabilities();
38817
+ const mode = params.mode ?? 'form';
38818
+ // Capability check based on mode
38819
+ switch (mode) {
38820
+ case 'url': {
38821
+ if (!clientCapabilities?.elicitation?.url) {
38822
+ throw new Error('Client does not support url elicitation.');
38823
+ }
38824
+ break;
38825
+ }
38826
+ case 'form': {
38827
+ if (!clientCapabilities?.elicitation?.form) {
38828
+ throw new Error('Client does not support form elicitation.');
38829
+ }
38830
+ break;
38831
+ }
38832
+ }
38833
+ // Normalize params to ensure mode is set for form mode (defaults to 'form' per spec)
38834
+ const normalizedParams = mode === 'form' && params.mode === undefined ? { ...params, mode: 'form' } : params;
38835
+ // Cast to ServerRequest needed because TypeScript can't narrow the union type
38836
+ // based on the discriminated 'method' field when constructing the object literal
38837
+ return this.requestStream({
38838
+ method: 'elicitation/create',
38839
+ params: normalizedParams
38840
+ }, types_js_1.ElicitResultSchema, options);
38841
+ }
38467
38842
  /**
38468
38843
  * Gets the current status of a task.
38469
38844
  *
@@ -39960,6 +40335,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
39960
40335
  Object.defineProperty(exports, "__esModule", ({ value: true }));
39961
40336
  exports.SSEServerTransport = void 0;
39962
40337
  const node_crypto_1 = __webpack_require__(5217);
40338
+ const node_tls_1 = __webpack_require__(1692);
39963
40339
  const types_js_1 = __webpack_require__(1294);
39964
40340
  const raw_body_1 = __importDefault(__webpack_require__(7680));
39965
40341
  const content_type_1 = __importDefault(__webpack_require__(8597));
@@ -40054,7 +40430,13 @@ class SSEServerTransport {
40054
40430
  return;
40055
40431
  }
40056
40432
  const authInfo = req.auth;
40057
- const requestInfo = { headers: req.headers };
40433
+ const host = req.headers.host;
40434
+ const protocol = req.socket instanceof node_tls_1.TLSSocket ? 'https' : 'http';
40435
+ const fullUrl = host && req.url ? new node_url_1.URL(req.url, `${protocol}://${host}`) : undefined;
40436
+ const requestInfo = {
40437
+ headers: req.headers,
40438
+ url: fullUrl
40439
+ };
40058
40440
  let body;
40059
40441
  try {
40060
40442
  const ct = content_type_1.default.parse(req.headers['content-type'] ?? '');
@@ -40355,6 +40737,7 @@ const types_js_1 = __webpack_require__(1294);
40355
40737
  class WebStandardStreamableHTTPServerTransport {
40356
40738
  constructor(options = {}) {
40357
40739
  this._started = false;
40740
+ this._hasHandledRequest = false;
40358
40741
  this._streamMapping = new Map();
40359
40742
  this._requestToStreamMapping = new Map();
40360
40743
  this._requestResponseMap = new Map();
@@ -40435,6 +40818,12 @@ class WebStandardStreamableHTTPServerTransport {
40435
40818
  * Returns a Response object (Web Standard)
40436
40819
  */
40437
40820
  async handleRequest(req, options) {
40821
+ // In stateless mode (no sessionIdGenerator), each request must use a fresh transport.
40822
+ // Reusing a stateless transport causes message ID collisions between clients.
40823
+ if (!this.sessionIdGenerator && this._hasHandledRequest) {
40824
+ throw new Error('Stateless transport cannot be reused across requests. Create a new transport per request.');
40825
+ }
40826
+ this._hasHandledRequest = true;
40438
40827
  // Validate request headers for DNS rebinding protection
40439
40828
  const validationError = this.validateRequestHeaders(req);
40440
40829
  if (validationError) {
@@ -40480,6 +40869,7 @@ class WebStandardStreamableHTTPServerTransport {
40480
40869
  // The client MUST include an Accept header, listing text/event-stream as a supported content type.
40481
40870
  const acceptHeader = req.headers.get('accept');
40482
40871
  if (!acceptHeader?.includes('text/event-stream')) {
40872
+ this.onerror?.(new Error('Not Acceptable: Client must accept text/event-stream'));
40483
40873
  return this.createJsonErrorResponse(406, -32000, 'Not Acceptable: Client must accept text/event-stream');
40484
40874
  }
40485
40875
  // If an Mcp-Session-Id is returned by the server during initialization,
@@ -40503,6 +40893,7 @@ class WebStandardStreamableHTTPServerTransport {
40503
40893
  // Check if there's already an active standalone SSE stream for this session
40504
40894
  if (this._streamMapping.get(this._standaloneSseStreamId) !== undefined) {
40505
40895
  // Only one GET SSE stream is allowed per session
40896
+ this.onerror?.(new Error('Conflict: Only one SSE stream is allowed per session'));
40506
40897
  return this.createJsonErrorResponse(409, -32000, 'Conflict: Only one SSE stream is allowed per session');
40507
40898
  }
40508
40899
  const encoder = new TextEncoder();
@@ -40548,6 +40939,7 @@ class WebStandardStreamableHTTPServerTransport {
40548
40939
  */
40549
40940
  async replayEvents(lastEventId) {
40550
40941
  if (!this._eventStore) {
40942
+ this.onerror?.(new Error('Event store not configured'));
40551
40943
  return this.createJsonErrorResponse(400, -32000, 'Event store not configured');
40552
40944
  }
40553
40945
  try {
@@ -40556,10 +40948,12 @@ class WebStandardStreamableHTTPServerTransport {
40556
40948
  if (this._eventStore.getStreamIdForEventId) {
40557
40949
  streamId = await this._eventStore.getStreamIdForEventId(lastEventId);
40558
40950
  if (!streamId) {
40951
+ this.onerror?.(new Error('Invalid event ID format'));
40559
40952
  return this.createJsonErrorResponse(400, -32000, 'Invalid event ID format');
40560
40953
  }
40561
40954
  // Check conflict with the SAME streamId we'll use for mapping
40562
40955
  if (this._streamMapping.get(streamId) !== undefined) {
40956
+ this.onerror?.(new Error('Conflict: Stream already has an active connection'));
40563
40957
  return this.createJsonErrorResponse(409, -32000, 'Conflict: Stream already has an active connection');
40564
40958
  }
40565
40959
  }
@@ -40632,7 +41026,8 @@ class WebStandardStreamableHTTPServerTransport {
40632
41026
  controller.enqueue(encoder.encode(eventData));
40633
41027
  return true;
40634
41028
  }
40635
- catch {
41029
+ catch (error) {
41030
+ this.onerror?.(error);
40636
41031
  return false;
40637
41032
  }
40638
41033
  }
@@ -40640,6 +41035,7 @@ class WebStandardStreamableHTTPServerTransport {
40640
41035
  * Handles unsupported requests (PUT, PATCH, etc.)
40641
41036
  */
40642
41037
  handleUnsupportedRequest() {
41038
+ this.onerror?.(new Error('Method not allowed.'));
40643
41039
  return new Response(JSON.stringify({
40644
41040
  jsonrpc: '2.0',
40645
41041
  error: {
@@ -40664,15 +41060,18 @@ class WebStandardStreamableHTTPServerTransport {
40664
41060
  const acceptHeader = req.headers.get('accept');
40665
41061
  // The client MUST include an Accept header, listing both application/json and text/event-stream as supported content types.
40666
41062
  if (!acceptHeader?.includes('application/json') || !acceptHeader.includes('text/event-stream')) {
41063
+ this.onerror?.(new Error('Not Acceptable: Client must accept both application/json and text/event-stream'));
40667
41064
  return this.createJsonErrorResponse(406, -32000, 'Not Acceptable: Client must accept both application/json and text/event-stream');
40668
41065
  }
40669
41066
  const ct = req.headers.get('content-type');
40670
41067
  if (!ct || !ct.includes('application/json')) {
41068
+ this.onerror?.(new Error('Unsupported Media Type: Content-Type must be application/json'));
40671
41069
  return this.createJsonErrorResponse(415, -32000, 'Unsupported Media Type: Content-Type must be application/json');
40672
41070
  }
40673
- // Build request info from headers
41071
+ // Build request info from headers and URL
40674
41072
  const requestInfo = {
40675
- headers: Object.fromEntries(req.headers.entries())
41073
+ headers: Object.fromEntries(req.headers.entries()),
41074
+ url: new URL(req.url)
40676
41075
  };
40677
41076
  let rawMessage;
40678
41077
  if (options?.parsedBody !== undefined) {
@@ -40683,6 +41082,7 @@ class WebStandardStreamableHTTPServerTransport {
40683
41082
  rawMessage = await req.json();
40684
41083
  }
40685
41084
  catch {
41085
+ this.onerror?.(new Error('Parse error: Invalid JSON'));
40686
41086
  return this.createJsonErrorResponse(400, -32700, 'Parse error: Invalid JSON');
40687
41087
  }
40688
41088
  }
@@ -40697,6 +41097,7 @@ class WebStandardStreamableHTTPServerTransport {
40697
41097
  }
40698
41098
  }
40699
41099
  catch {
41100
+ this.onerror?.(new Error('Parse error: Invalid JSON-RPC message'));
40700
41101
  return this.createJsonErrorResponse(400, -32700, 'Parse error: Invalid JSON-RPC message');
40701
41102
  }
40702
41103
  // Check if this is an initialization request
@@ -40706,9 +41107,11 @@ class WebStandardStreamableHTTPServerTransport {
40706
41107
  // If it's a server with session management and the session ID is already set we should reject the request
40707
41108
  // to avoid re-initialization.
40708
41109
  if (this._initialized && this.sessionId !== undefined) {
41110
+ this.onerror?.(new Error('Invalid Request: Server already initialized'));
40709
41111
  return this.createJsonErrorResponse(400, -32600, 'Invalid Request: Server already initialized');
40710
41112
  }
40711
41113
  if (messages.length > 1) {
41114
+ this.onerror?.(new Error('Invalid Request: Only one initialization request is allowed'));
40712
41115
  return this.createJsonErrorResponse(400, -32600, 'Invalid Request: Only one initialization request is allowed');
40713
41116
  }
40714
41117
  this.sessionId = this.sessionIdGenerator?.();
@@ -40870,15 +41273,18 @@ class WebStandardStreamableHTTPServerTransport {
40870
41273
  }
40871
41274
  if (!this._initialized) {
40872
41275
  // If the server has not been initialized yet, reject all requests
41276
+ this.onerror?.(new Error('Bad Request: Server not initialized'));
40873
41277
  return this.createJsonErrorResponse(400, -32000, 'Bad Request: Server not initialized');
40874
41278
  }
40875
41279
  const sessionId = req.headers.get('mcp-session-id');
40876
41280
  if (!sessionId) {
40877
41281
  // Non-initialization requests without a session ID should return 400 Bad Request
41282
+ this.onerror?.(new Error('Bad Request: Mcp-Session-Id header is required'));
40878
41283
  return this.createJsonErrorResponse(400, -32000, 'Bad Request: Mcp-Session-Id header is required');
40879
41284
  }
40880
41285
  if (sessionId !== this.sessionId) {
40881
41286
  // Reject requests with invalid session ID with 404 Not Found
41287
+ this.onerror?.(new Error('Session not found'));
40882
41288
  return this.createJsonErrorResponse(404, -32001, 'Session not found');
40883
41289
  }
40884
41290
  return undefined;
@@ -40899,6 +41305,8 @@ class WebStandardStreamableHTTPServerTransport {
40899
41305
  validateProtocolVersion(req) {
40900
41306
  const protocolVersion = req.headers.get('mcp-protocol-version');
40901
41307
  if (protocolVersion !== null && !types_js_1.SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) {
41308
+ this.onerror?.(new Error(`Bad Request: Unsupported protocol version: ${protocolVersion}` +
41309
+ ` (supported versions: ${types_js_1.SUPPORTED_PROTOCOL_VERSIONS.join(', ')})`));
40902
41310
  return this.createJsonErrorResponse(400, -32000, `Bad Request: Unsupported protocol version: ${protocolVersion} (supported versions: ${types_js_1.SUPPORTED_PROTOCOL_VERSIONS.join(', ')})`);
40903
41311
  }
40904
41312
  return undefined;
@@ -41589,6 +41997,9 @@ class Protocol {
41589
41997
  * The Protocol object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
41590
41998
  */
41591
41999
  async connect(transport) {
42000
+ if (this._transport) {
42001
+ throw new Error('Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.');
42002
+ }
41592
42003
  this._transport = transport;
41593
42004
  const _onclose = this.transport?.onclose;
41594
42005
  this._transport.onclose = () => {
@@ -41624,6 +42035,11 @@ class Protocol {
41624
42035
  this._progressHandlers.clear();
41625
42036
  this._taskProgressTokens.clear();
41626
42037
  this._pendingDebouncedNotifications.clear();
42038
+ // Abort all in-flight request handlers so they stop sending messages
42039
+ for (const controller of this._requestHandlerAbortControllers.values()) {
42040
+ controller.abort();
42041
+ }
42042
+ this._requestHandlerAbortControllers.clear();
41627
42043
  const error = types_js_1.McpError.fromError(types_js_1.ErrorCode.ConnectionClosed, 'Connection closed');
41628
42044
  this._transport = undefined;
41629
42045
  this.onclose?.();
@@ -41684,6 +42100,8 @@ class Protocol {
41684
42100
  sessionId: capturedTransport?.sessionId,
41685
42101
  _meta: request.params?._meta,
41686
42102
  sendNotification: async (notification) => {
42103
+ if (abortController.signal.aborted)
42104
+ return;
41687
42105
  // Include related-task metadata if this request is part of a task
41688
42106
  const notificationOptions = { relatedRequestId: request.id };
41689
42107
  if (relatedTaskId) {
@@ -41692,6 +42110,9 @@ class Protocol {
41692
42110
  await this.notification(notification, notificationOptions);
41693
42111
  },
41694
42112
  sendRequest: async (r, resultSchema, options) => {
42113
+ if (abortController.signal.aborted) {
42114
+ throw new types_js_1.McpError(types_js_1.ErrorCode.ConnectionClosed, 'Request was cancelled');
42115
+ }
41695
42116
  // Include related-task metadata if this request is part of a task
41696
42117
  const requestOptions = { ...options, relatedRequestId: request.id };
41697
42118
  if (relatedTaskId && !requestOptions.relatedTask) {
@@ -67079,7 +67500,7 @@ module.exports = /*#__PURE__*/JSON.parse('{"100":"Continue","101":"Switching Pro
67079
67500
  (module) {
67080
67501
 
67081
67502
  "use strict";
67082
- module.exports = /*#__PURE__*/JSON.parse('{"name":"tabby-mcp-server","version":"1.3.0","description":"MCP (Model Context Protocol) server plugin for Tabby terminal - Complete terminal control with 34 MCP tools including SFTP file transfer","homepage":"https://github.com/GentlemanHu/Tabby-MCP","repository":{"type":"git","url":"https://github.com/GentlemanHu/Tabby-MCP.git"},"bugs":{"url":"https://github.com/GentlemanHu/Tabby-MCP/issues"},"keywords":["tabby-plugin","mcp","model-context-protocol","terminal","ai","cursor","windsurf","ssh","sftp","sse"],"main":"dist/index.js","typings":"typings/index.d.ts","scripts":{"build":"webpack --progress --color","watch":"webpack --progress --color --watch","install-plugin":"bash scripts/install.sh","uninstall-plugin":"bash scripts/uninstall.sh"},"files":["dist","typings"],"author":"GentlemanHu <justfeelingme@gmail.com>","contributors":["AI Assistant (Claude/Gemini)"],"license":"MIT","peerDependencies":{"@angular/animations":"*","@angular/common":"*","@angular/core":"*","@angular/forms":"*","@ng-bootstrap/ng-bootstrap":"*","rxjs":"*","tabby-core":"*","tabby-settings":"*","tabby-terminal":"*","tabby-ssh":"*"},"peerDependenciesMeta":{"tabby-ssh":{"optional":true},"@angular/animations":{"optional":true},"@angular/common":{"optional":true},"@angular/core":{"optional":true},"@angular/forms":{"optional":true},"@ng-bootstrap/ng-bootstrap":{"optional":true},"rxjs":{"optional":true},"tabby-core":{"optional":true},"tabby-settings":{"optional":true},"tabby-terminal":{"optional":true}},"devDependencies":{"@modelcontextprotocol/sdk":"^1.8.0","@xterm/addon-serialize":"^0.12.0","cors":"^2.8.5","express":"^4.18.2","zod":"^3.22.4","@angular/common":"^17.3.0","@angular/core":"^17.3.0","@angular/forms":"^17.3.0","@angular/animations":"^17.3.0","@angular/platform-browser":"^17.3.0","@ng-bootstrap/ng-bootstrap":"^16.0.0","rxjs":"^7.8.0","tabby-core":"^1.0.163","tabby-settings":"^1.0.163","tabby-ssh":"^1.0.163","tabby-terminal":"^1.0.163","@types/cors":"^2.8.17","@types/express":"^4.17.21","@types/node":"^20.10.0","apply-loader":"^2.0.0","css-loader":"^6.8.1","sass":"^1.69.0","sass-loader":"^13.3.2","strip-ansi":"^7.1.0","style-loader":"^3.3.3","ts-loader":"^9.5.0","typescript":"^5.3.0","webpack":"^5.89.0","webpack-cli":"^5.1.4"}}');
67503
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"tabby-mcp-server","version":"1.4.2","description":"MCP (Model Context Protocol) server plugin for Tabby terminal - Complete terminal control with 34 MCP tools including SFTP file transfer","homepage":"https://github.com/GentlemanHu/Tabby-MCP","repository":{"type":"git","url":"https://github.com/GentlemanHu/Tabby-MCP.git"},"bugs":{"url":"https://github.com/GentlemanHu/Tabby-MCP/issues"},"keywords":["tabby-plugin","mcp","model-context-protocol","terminal","ai","cursor","windsurf","ssh","sftp","sse"],"main":"dist/index.js","typings":"typings/index.d.ts","scripts":{"build":"webpack --progress --color","watch":"webpack --progress --color --watch","install-plugin":"bash scripts/install.sh","uninstall-plugin":"bash scripts/uninstall.sh"},"files":["dist","typings"],"author":"GentlemanHu <justfeelingme@gmail.com>","contributors":["AI Assistant (Claude/Gemini)"],"license":"MIT","peerDependencies":{"@angular/animations":"*","@angular/common":"*","@angular/core":"*","@angular/forms":"*","@ng-bootstrap/ng-bootstrap":"*","rxjs":"*","tabby-core":"*","tabby-settings":"*","tabby-terminal":"*","tabby-ssh":"*"},"peerDependenciesMeta":{"tabby-ssh":{"optional":true},"@angular/animations":{"optional":true},"@angular/common":{"optional":true},"@angular/core":{"optional":true},"@angular/forms":{"optional":true},"@ng-bootstrap/ng-bootstrap":{"optional":true},"rxjs":{"optional":true},"tabby-core":{"optional":true},"tabby-settings":{"optional":true},"tabby-terminal":{"optional":true}},"devDependencies":{"@modelcontextprotocol/sdk":"^1.8.0","@xterm/addon-serialize":"^0.12.0","cors":"^2.8.5","express":"^4.18.2","zod":"^3.22.4","@angular/common":"^17.3.0","@angular/core":"^17.3.0","@angular/forms":"^17.3.0","@angular/animations":"^17.3.0","@angular/platform-browser":"^17.3.0","@ng-bootstrap/ng-bootstrap":"^16.0.0","rxjs":"^7.8.0","tabby-core":"^1.0.163","tabby-settings":"^1.0.163","tabby-ssh":"^1.0.163","tabby-terminal":"^1.0.163","@types/cors":"^2.8.17","@types/express":"^4.17.21","@types/node":"^20.10.0","apply-loader":"^2.0.0","css-loader":"^6.8.1","sass":"^1.69.0","sass-loader":"^13.3.2","strip-ansi":"^7.1.0","style-loader":"^3.3.3","ts-loader":"^9.5.0","typescript":"^5.3.0","webpack":"^5.89.0","webpack-cli":"^5.1.4"}}');
67083
67504
 
67084
67505
  /***/ },
67085
67506