tabby-mcp-server 1.4.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/index.js +130 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/typings/services/mcpService.d.ts +1 -0
package/README.md
CHANGED
|
@@ -376,6 +376,13 @@ This project builds upon the work of [tabby-mcp-server](https://github.com/thuan
|
|
|
376
376
|
|
|
377
377
|
## 📝 Changelog
|
|
378
378
|
|
|
379
|
+
### v1.5.1 (2026-04-03)
|
|
380
|
+
|
|
381
|
+
**🐛 Bug Fixes:**
|
|
382
|
+
- 🔧 **Fixed `/api/tool/{name}` returning 404** (Issue #4) - Tool API endpoints were registered during `configureExpress()` when `toolCategories` was still empty due to Angular DI initialization order
|
|
383
|
+
- Moved `configureToolEndpoints()` to `startServer()` where all tools are guaranteed to be registered
|
|
384
|
+
- Added duplicate registration guard to prevent route duplication on server restart
|
|
385
|
+
|
|
379
386
|
### v1.4.0 (2026-03-02)
|
|
380
387
|
|
|
381
388
|
**🐛 Bug Fixes:**
|
package/dist/index.js
CHANGED
|
@@ -265,15 +265,17 @@ var Response2 = class _Response {
|
|
|
265
265
|
this.#init = init;
|
|
266
266
|
}
|
|
267
267
|
if (typeof body === "string" || typeof body?.getReader !== "undefined" || body instanceof Blob || body instanceof Uint8Array) {
|
|
268
|
-
|
|
269
|
-
this[cacheKey] = [init?.status || 200, body, headers];
|
|
268
|
+
;
|
|
269
|
+
this[cacheKey] = [init?.status || 200, body, headers || init?.headers];
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
272
|
get headers() {
|
|
273
273
|
const cache = this[cacheKey];
|
|
274
274
|
if (cache) {
|
|
275
275
|
if (!(cache[2] instanceof Headers)) {
|
|
276
|
-
cache[2] = new Headers(
|
|
276
|
+
cache[2] = new Headers(
|
|
277
|
+
cache[2] || { "content-type": "text/plain; charset=UTF-8" }
|
|
278
|
+
);
|
|
277
279
|
}
|
|
278
280
|
return cache[2];
|
|
279
281
|
}
|
|
@@ -381,6 +383,50 @@ if (typeof global.crypto === "undefined") {
|
|
|
381
383
|
|
|
382
384
|
// src/listener.ts
|
|
383
385
|
var outgoingEnded = Symbol("outgoingEnded");
|
|
386
|
+
var incomingDraining = Symbol("incomingDraining");
|
|
387
|
+
var DRAIN_TIMEOUT_MS = 500;
|
|
388
|
+
var MAX_DRAIN_BYTES = 64 * 1024 * 1024;
|
|
389
|
+
var drainIncoming = (incoming) => {
|
|
390
|
+
const incomingWithDrainState = incoming;
|
|
391
|
+
if (incoming.destroyed || incomingWithDrainState[incomingDraining]) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
incomingWithDrainState[incomingDraining] = true;
|
|
395
|
+
if (incoming instanceof import_node_http22.Http2ServerRequest) {
|
|
396
|
+
try {
|
|
397
|
+
;
|
|
398
|
+
incoming.stream?.close?.(import_node_http22.constants.NGHTTP2_NO_ERROR);
|
|
399
|
+
} catch {
|
|
400
|
+
}
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
let bytesRead = 0;
|
|
404
|
+
const cleanup = () => {
|
|
405
|
+
clearTimeout(timer);
|
|
406
|
+
incoming.off("data", onData);
|
|
407
|
+
incoming.off("end", cleanup);
|
|
408
|
+
incoming.off("error", cleanup);
|
|
409
|
+
};
|
|
410
|
+
const forceClose = () => {
|
|
411
|
+
cleanup();
|
|
412
|
+
const socket = incoming.socket;
|
|
413
|
+
if (socket && !socket.destroyed) {
|
|
414
|
+
socket.destroySoon();
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
const timer = setTimeout(forceClose, DRAIN_TIMEOUT_MS);
|
|
418
|
+
timer.unref?.();
|
|
419
|
+
const onData = (chunk) => {
|
|
420
|
+
bytesRead += chunk.length;
|
|
421
|
+
if (bytesRead > MAX_DRAIN_BYTES) {
|
|
422
|
+
forceClose();
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
incoming.on("data", onData);
|
|
426
|
+
incoming.on("end", cleanup);
|
|
427
|
+
incoming.on("error", cleanup);
|
|
428
|
+
incoming.resume();
|
|
429
|
+
};
|
|
384
430
|
var handleRequestError = () => new Response(null, {
|
|
385
431
|
status: 400
|
|
386
432
|
});
|
|
@@ -407,15 +453,32 @@ var flushHeaders = (outgoing) => {
|
|
|
407
453
|
};
|
|
408
454
|
var responseViaCache = async (res, outgoing) => {
|
|
409
455
|
let [status, body, header] = res[cacheKey];
|
|
410
|
-
|
|
456
|
+
let hasContentLength = false;
|
|
457
|
+
if (!header) {
|
|
458
|
+
header = { "content-type": "text/plain; charset=UTF-8" };
|
|
459
|
+
} else if (header instanceof Headers) {
|
|
460
|
+
hasContentLength = header.has("content-length");
|
|
411
461
|
header = buildOutgoingHttpHeaders(header);
|
|
462
|
+
} else if (Array.isArray(header)) {
|
|
463
|
+
const headerObj = new Headers(header);
|
|
464
|
+
hasContentLength = headerObj.has("content-length");
|
|
465
|
+
header = buildOutgoingHttpHeaders(headerObj);
|
|
466
|
+
} else {
|
|
467
|
+
for (const key in header) {
|
|
468
|
+
if (key.length === 14 && key.toLowerCase() === "content-length") {
|
|
469
|
+
hasContentLength = true;
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
412
473
|
}
|
|
413
|
-
if (
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
474
|
+
if (!hasContentLength) {
|
|
475
|
+
if (typeof body === "string") {
|
|
476
|
+
header["Content-Length"] = Buffer.byteLength(body);
|
|
477
|
+
} else if (body instanceof Uint8Array) {
|
|
478
|
+
header["Content-Length"] = body.byteLength;
|
|
479
|
+
} else if (body instanceof Blob) {
|
|
480
|
+
header["Content-Length"] = body.size;
|
|
481
|
+
}
|
|
419
482
|
}
|
|
420
483
|
outgoing.writeHead(status, header);
|
|
421
484
|
if (typeof body === "string" || body instanceof Uint8Array) {
|
|
@@ -535,14 +598,18 @@ var getRequestListener = (fetchCallback, options = {}) => {
|
|
|
535
598
|
setTimeout(() => {
|
|
536
599
|
if (!incomingEnded) {
|
|
537
600
|
setTimeout(() => {
|
|
538
|
-
incoming
|
|
539
|
-
outgoing.destroy();
|
|
601
|
+
drainIncoming(incoming);
|
|
540
602
|
});
|
|
541
603
|
}
|
|
542
604
|
});
|
|
543
605
|
}
|
|
544
606
|
};
|
|
545
607
|
}
|
|
608
|
+
outgoing.on("finish", () => {
|
|
609
|
+
if (!incomingEnded) {
|
|
610
|
+
drainIncoming(incoming);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
546
613
|
}
|
|
547
614
|
outgoing.on("close", () => {
|
|
548
615
|
const abortController = req[abortControllerKey];
|
|
@@ -557,7 +624,7 @@ var getRequestListener = (fetchCallback, options = {}) => {
|
|
|
557
624
|
setTimeout(() => {
|
|
558
625
|
if (!incomingEnded) {
|
|
559
626
|
setTimeout(() => {
|
|
560
|
-
incoming
|
|
627
|
+
drainIncoming(incoming);
|
|
561
628
|
});
|
|
562
629
|
}
|
|
563
630
|
});
|
|
@@ -24513,6 +24580,7 @@ function pathToRegexp(path, keys, options) {
|
|
|
24513
24580
|
pos = offset + match.length;
|
|
24514
24581
|
|
|
24515
24582
|
if (match === '*') {
|
|
24583
|
+
backtrack = '';
|
|
24516
24584
|
extraOffset += 3;
|
|
24517
24585
|
return '(.*)';
|
|
24518
24586
|
}
|
|
@@ -24542,6 +24610,7 @@ function pathToRegexp(path, keys, options) {
|
|
|
24542
24610
|
+ ')'
|
|
24543
24611
|
+ optional;
|
|
24544
24612
|
|
|
24613
|
+
backtrack = '';
|
|
24545
24614
|
extraOffset += result.length - match.length;
|
|
24546
24615
|
|
|
24547
24616
|
return result;
|
|
@@ -33723,6 +33792,8 @@ let McpService = class McpService {
|
|
|
33723
33792
|
this.toolCategories = [];
|
|
33724
33793
|
// Store tool definitions for registering with each new server
|
|
33725
33794
|
this.registeredTools = [];
|
|
33795
|
+
// Track whether tool endpoints have been registered to prevent duplicates on restart
|
|
33796
|
+
this.toolEndpointsConfigured = false;
|
|
33726
33797
|
// ============================================================
|
|
33727
33798
|
// CONNECTION MONITORING & MANAGEMENT
|
|
33728
33799
|
// ============================================================
|
|
@@ -34055,8 +34126,6 @@ let McpService = class McpService {
|
|
|
34055
34126
|
this.logger.debug(`Legacy SSE: Message received for sessionId=${sessionId}`);
|
|
34056
34127
|
await transport.handlePostMessage(req, res);
|
|
34057
34128
|
});
|
|
34058
|
-
// Configure API endpoints for direct tool access
|
|
34059
|
-
this.configureToolEndpoints();
|
|
34060
34129
|
}
|
|
34061
34130
|
/**
|
|
34062
34131
|
* Validate origin header for security
|
|
@@ -34092,6 +34161,10 @@ let McpService = class McpService {
|
|
|
34092
34161
|
* Configure HTTP API endpoints for direct tool access
|
|
34093
34162
|
*/
|
|
34094
34163
|
configureToolEndpoints() {
|
|
34164
|
+
if (this.toolEndpointsConfigured) {
|
|
34165
|
+
this.logger.debug('Tool endpoints already configured, skipping');
|
|
34166
|
+
return;
|
|
34167
|
+
}
|
|
34095
34168
|
this.toolCategories.forEach(category => {
|
|
34096
34169
|
category.mcpTools.forEach(tool => {
|
|
34097
34170
|
this.app.post(`/api/tool/${tool.name}`, async (req, res) => {
|
|
@@ -34107,6 +34180,8 @@ let McpService = class McpService {
|
|
|
34107
34180
|
});
|
|
34108
34181
|
});
|
|
34109
34182
|
});
|
|
34183
|
+
this.toolEndpointsConfigured = true;
|
|
34184
|
+
this.logger.info(`Configured ${this.toolCategories.reduce((s, c) => s + c.mcpTools.length, 0)} tool API endpoints`);
|
|
34110
34185
|
}
|
|
34111
34186
|
/**
|
|
34112
34187
|
* Start the MCP server
|
|
@@ -34117,6 +34192,10 @@ let McpService = class McpService {
|
|
|
34117
34192
|
return;
|
|
34118
34193
|
}
|
|
34119
34194
|
const serverPort = port || this.config.store.mcp?.port || 3001;
|
|
34195
|
+
// Configure API endpoints for direct tool access
|
|
34196
|
+
// Must be called here (not in configureExpress) because tools are registered
|
|
34197
|
+
// by McpModule constructor AFTER McpService constructor runs (Issue #4)
|
|
34198
|
+
this.configureToolEndpoints();
|
|
34120
34199
|
return new Promise((resolve, reject) => {
|
|
34121
34200
|
try {
|
|
34122
34201
|
this.httpServer = http.createServer(this.app);
|
|
@@ -40091,9 +40170,10 @@ class McpServer {
|
|
|
40091
40170
|
}
|
|
40092
40171
|
}
|
|
40093
40172
|
else if (typeof firstArg === 'object' && firstArg !== null) {
|
|
40094
|
-
//
|
|
40095
|
-
|
|
40096
|
-
|
|
40173
|
+
// ToolAnnotations values are primitives. Nested objects indicate a misplaced schema
|
|
40174
|
+
if (Object.values(firstArg).some(v => typeof v === 'object' && v !== null)) {
|
|
40175
|
+
throw new Error(`Tool ${name} expected a Zod schema or ToolAnnotations, but received an unrecognized object`);
|
|
40176
|
+
}
|
|
40097
40177
|
annotations = rest.shift();
|
|
40098
40178
|
}
|
|
40099
40179
|
}
|
|
@@ -40265,7 +40345,7 @@ function isZodRawShapeCompat(obj) {
|
|
|
40265
40345
|
}
|
|
40266
40346
|
/**
|
|
40267
40347
|
* Converts a provided Zod schema to a Zod object if it is a ZodRawShapeCompat,
|
|
40268
|
-
* otherwise returns the schema as is.
|
|
40348
|
+
* otherwise returns the schema as is. Throws if the value is not a valid Zod schema.
|
|
40269
40349
|
*/
|
|
40270
40350
|
function getZodSchemaObject(schema) {
|
|
40271
40351
|
if (!schema) {
|
|
@@ -40274,6 +40354,9 @@ function getZodSchemaObject(schema) {
|
|
|
40274
40354
|
if (isZodRawShapeCompat(schema)) {
|
|
40275
40355
|
return (0, zod_compat_js_1.objectFromShape)(schema);
|
|
40276
40356
|
}
|
|
40357
|
+
if (!isZodSchemaInstance(schema)) {
|
|
40358
|
+
throw new Error('inputSchema must be a Zod schema or raw shape, received an unrecognized object');
|
|
40359
|
+
}
|
|
40277
40360
|
return schema;
|
|
40278
40361
|
}
|
|
40279
40362
|
function promptArgumentsFromSchema(schema) {
|
|
@@ -42035,6 +42118,10 @@ class Protocol {
|
|
|
42035
42118
|
this._progressHandlers.clear();
|
|
42036
42119
|
this._taskProgressTokens.clear();
|
|
42037
42120
|
this._pendingDebouncedNotifications.clear();
|
|
42121
|
+
for (const info of this._timeoutInfo.values()) {
|
|
42122
|
+
clearTimeout(info.timeoutId);
|
|
42123
|
+
}
|
|
42124
|
+
this._timeoutInfo.clear();
|
|
42038
42125
|
// Abort all in-flight request handlers so they stop sending messages
|
|
42039
42126
|
for (const controller of this._requestHandlerAbortControllers.values()) {
|
|
42040
42127
|
controller.abort();
|
|
@@ -42194,7 +42281,11 @@ class Protocol {
|
|
|
42194
42281
|
})
|
|
42195
42282
|
.catch(error => this._onerror(new Error(`Failed to send response: ${error}`)))
|
|
42196
42283
|
.finally(() => {
|
|
42197
|
-
|
|
42284
|
+
// Only delete if the stored controller is still ours; after close()+connect(),
|
|
42285
|
+
// a new connection may have reused the same request ID with a different controller.
|
|
42286
|
+
if (this._requestHandlerAbortControllers.get(request.id) === abortController) {
|
|
42287
|
+
this._requestHandlerAbortControllers.delete(request.id);
|
|
42288
|
+
}
|
|
42198
42289
|
});
|
|
42199
42290
|
}
|
|
42200
42291
|
_onprogress(notification) {
|
|
@@ -43300,10 +43391,9 @@ exports.CursorSchema = z.string();
|
|
|
43300
43391
|
*/
|
|
43301
43392
|
exports.TaskCreationParamsSchema = z.looseObject({
|
|
43302
43393
|
/**
|
|
43303
|
-
*
|
|
43304
|
-
* If null, the task has unlimited lifetime until manually cleaned up.
|
|
43394
|
+
* Requested duration in milliseconds to retain task from creation.
|
|
43305
43395
|
*/
|
|
43306
|
-
ttl: z.
|
|
43396
|
+
ttl: z.number().optional(),
|
|
43307
43397
|
/**
|
|
43308
43398
|
* Time in milliseconds to wait between task status requests.
|
|
43309
43399
|
*/
|
|
@@ -43731,7 +43821,11 @@ exports.ClientCapabilitiesSchema = z.object({
|
|
|
43731
43821
|
/**
|
|
43732
43822
|
* Present if the client supports task creation.
|
|
43733
43823
|
*/
|
|
43734
|
-
tasks: exports.ClientTasksCapabilitySchema.optional()
|
|
43824
|
+
tasks: exports.ClientTasksCapabilitySchema.optional(),
|
|
43825
|
+
/**
|
|
43826
|
+
* Extensions that the client supports. Keys are extension identifiers (vendor-prefix/extension-name).
|
|
43827
|
+
*/
|
|
43828
|
+
extensions: z.record(z.string(), AssertObjectSchema).optional()
|
|
43735
43829
|
});
|
|
43736
43830
|
exports.InitializeRequestParamsSchema = BaseRequestParamsSchema.extend({
|
|
43737
43831
|
/**
|
|
@@ -43806,7 +43900,11 @@ exports.ServerCapabilitiesSchema = z.object({
|
|
|
43806
43900
|
/**
|
|
43807
43901
|
* Present if the server supports task creation.
|
|
43808
43902
|
*/
|
|
43809
|
-
tasks: exports.ServerTasksCapabilitySchema.optional()
|
|
43903
|
+
tasks: exports.ServerTasksCapabilitySchema.optional(),
|
|
43904
|
+
/**
|
|
43905
|
+
* Extensions that the server supports. Keys are extension identifiers (vendor-prefix/extension-name).
|
|
43906
|
+
*/
|
|
43907
|
+
extensions: z.record(z.string(), AssertObjectSchema).optional()
|
|
43810
43908
|
});
|
|
43811
43909
|
/**
|
|
43812
43910
|
* After receiving an initialize request from the client, the server sends this response.
|
|
@@ -44081,6 +44179,12 @@ exports.ResourceSchema = z.object({
|
|
|
44081
44179
|
* The MIME type of this resource, if known.
|
|
44082
44180
|
*/
|
|
44083
44181
|
mimeType: z.optional(z.string()),
|
|
44182
|
+
/**
|
|
44183
|
+
* The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.
|
|
44184
|
+
*
|
|
44185
|
+
* This can be used by Hosts to display file sizes and estimate context window usage.
|
|
44186
|
+
*/
|
|
44187
|
+
size: z.optional(z.number()),
|
|
44084
44188
|
/**
|
|
44085
44189
|
* Optional annotations for the client.
|
|
44086
44190
|
*/
|
|
@@ -67500,7 +67604,7 @@ module.exports = /*#__PURE__*/JSON.parse('{"100":"Continue","101":"Switching Pro
|
|
|
67500
67604
|
(module) {
|
|
67501
67605
|
|
|
67502
67606
|
"use strict";
|
|
67503
|
-
module.exports = /*#__PURE__*/JSON.parse('{"name":"tabby-mcp-server","version":"1.
|
|
67607
|
+
module.exports = /*#__PURE__*/JSON.parse('{"name":"tabby-mcp-server","version":"1.5.1","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"}}');
|
|
67504
67608
|
|
|
67505
67609
|
/***/ },
|
|
67506
67610
|
|