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/README.md +41 -19
- package/dist/index.js +483 -62
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/typings/services/mcpService.d.ts +14 -4
- package/typings/tools/sftp.d.ts +22 -0
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
|
-
|
|
6745
|
-
|
|
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
|
-
|
|
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
|
-
&&
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
25950
|
-
refs.
|
|
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.
|
|
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
|
|
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: '
|
|
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
|
-
//
|
|
33693
|
-
this.
|
|
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
|
-
//
|
|
33698
|
-
this.
|
|
33699
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
33714
|
-
this.
|
|
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
|
-
|
|
33726
|
-
this.
|
|
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:
|
|
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:
|
|
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
|
-
//
|
|
33862
|
-
|
|
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
|
-
|
|
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
|
|
34359
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|