tachibot-mcp 2.9.0 → 2.10.0

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.
@@ -12,6 +12,7 @@ import { getGrokApiKey, hasGrokApiKey } from "../utils/api-keys.js";
12
12
  import { stripFormatting } from "../utils/format-stripper.js";
13
13
  import { FORMAT_INSTRUCTION } from "../utils/format-constants.js";
14
14
  import { tryOpenRouterGateway, isGatewayEnabled } from "../utils/openrouter-gateway.js";
15
+ import { withHeartbeat } from "../utils/streaming-helper.js";
15
16
  // Note: renderOutput is applied centrally in server.ts safeAddTool() - no need to import here
16
17
  const __filename = fileURLToPath(import.meta.url);
17
18
  const __dirname = path.dirname(__filename);
@@ -131,7 +132,7 @@ export const grokReasonTool = {
131
132
  context: z.string().optional().describe("Additional context for the problem"),
132
133
  useHeavy: z.boolean().optional().describe("Use expensive Grok 4 Heavy model ($3/$15) for complex tasks")
133
134
  }),
134
- execute: async (args, { log }) => {
135
+ execute: async (args, { log, reportProgress }) => {
135
136
  const { problem, approach = "first-principles", context, useHeavy } = args;
136
137
  const approachPrompts = {
137
138
  analytical: "Break down the problem systematically and analyze each component",
@@ -156,8 +157,10 @@ ${FORMAT_INSTRUCTION}`
156
157
  const model = useHeavy ? GrokModel.GROK_4_HEAVY : GrokModel.GROK_4_1_FAST_REASONING;
157
158
  const maxTokens = useHeavy ? 100000 : 16384; // 100k for heavy, 16k for normal reasoning
158
159
  log?.info(`Using Grok model: ${model} for deep reasoning (max tokens: ${maxTokens}, cost: ${useHeavy ? 'expensive $3/$15' : 'cheap $0.20/$0.50'})`);
159
- // Use llm-orchestration context - input may contain code patterns
160
- return stripFormatting(await callGrok(messages, model, 0.7, maxTokens, true, 'llm-orchestration'));
160
+ // Use heartbeat to prevent MCP timeout during long reasoning operations
161
+ const reportFn = reportProgress ?? (async () => { });
162
+ const result = await withHeartbeat(() => callGrok(messages, model, 0.7, maxTokens, true, 'llm-orchestration'), reportFn);
163
+ return stripFormatting(result);
161
164
  }
162
165
  };
163
166
  /**
@@ -174,7 +177,7 @@ export const grokCodeTool = {
174
177
  language: z.string().optional().describe("Programming language (e.g., 'typescript', 'python')"),
175
178
  requirements: z.string().optional().describe("Specific requirements or focus areas")
176
179
  }),
177
- execute: async (args, { log }) => {
180
+ execute: async (args, { log, reportProgress }) => {
178
181
  const { task, code, language, requirements } = args;
179
182
  const taskPrompts = {
180
183
  analyze: "Analyze this code for logic, structure, and potential issues",
@@ -198,8 +201,10 @@ ${FORMAT_INSTRUCTION}`
198
201
  }
199
202
  ];
200
203
  log?.info(`Using Grok 4.1 Fast Non-Reasoning (2M context, tool-calling optimized, $0.20/$0.50)`);
201
- // Use code-analysis context - code content naturally contains patterns
202
- return stripFormatting(await callGrok(messages, GrokModel.GROK_4_1_FAST, 0.2, 4000, true, 'code-analysis'));
204
+ // Use heartbeat to prevent MCP timeout
205
+ const reportFn = reportProgress ?? (async () => { });
206
+ const result = await withHeartbeat(() => callGrok(messages, GrokModel.GROK_4_1_FAST, 0.2, 4000, true, 'code-analysis'), reportFn);
207
+ return stripFormatting(result);
203
208
  }
204
209
  };
205
210
  /**
@@ -215,7 +220,7 @@ export const grokDebugTool = {
215
220
  error: z.string().optional().describe("Error message or stack trace"),
216
221
  context: z.string().optional().describe("Additional context about the environment or conditions")
217
222
  }),
218
- execute: async (args, { log }) => {
223
+ execute: async (args, { log, reportProgress }) => {
219
224
  const { issue, code, error, context } = args;
220
225
  let prompt = `Debug this issue: ${issue}\n`;
221
226
  if (error) {
@@ -244,8 +249,10 @@ ${FORMAT_INSTRUCTION}`
244
249
  }
245
250
  ];
246
251
  log?.info(`Using Grok 4.1 Fast Non-Reasoning for debugging (tool-calling optimized, $0.20/$0.50)`);
247
- // Use code-analysis context - debug content may contain error patterns
248
- return stripFormatting(await callGrok(messages, GrokModel.GROK_4_1_FAST, 0.3, 3000, true, 'code-analysis'));
252
+ // Use heartbeat to prevent MCP timeout
253
+ const reportFn = reportProgress ?? (async () => { });
254
+ const result = await withHeartbeat(() => callGrok(messages, GrokModel.GROK_4_1_FAST, 0.3, 3000, true, 'code-analysis'), reportFn);
255
+ return stripFormatting(result);
249
256
  }
250
257
  };
251
258
  /**
@@ -262,7 +269,7 @@ export const grokArchitectTool = {
262
269
  .optional()
263
270
  .describe("Expected scale - must be one of: small, medium, large, enterprise")
264
271
  }),
265
- execute: async (args, { log }) => {
272
+ execute: async (args, { log, reportProgress }) => {
266
273
  const { requirements, constraints, scale } = args;
267
274
  const messages = [
268
275
  {
@@ -279,8 +286,10 @@ ${FORMAT_INSTRUCTION}`
279
286
  }
280
287
  ];
281
288
  log?.info(`Using Grok 4.1 Fast Reasoning for architecture (latest model, $0.20/$0.50)`);
282
- // Use llm-orchestration context - architecture content may contain technical patterns
283
- return stripFormatting(await callGrok(messages, GrokModel.GROK_4_1_FAST_REASONING, 0.6, 4000, true, 'llm-orchestration'));
289
+ // Use heartbeat to prevent MCP timeout
290
+ const reportFn = reportProgress ?? (async () => { });
291
+ const result = await withHeartbeat(() => callGrok(messages, GrokModel.GROK_4_1_FAST_REASONING, 0.6, 4000, true, 'llm-orchestration'), reportFn);
292
+ return stripFormatting(result);
284
293
  }
285
294
  };
286
295
  /**
@@ -296,7 +305,7 @@ export const grokBrainstormTool = {
296
305
  numIdeas: z.number().optional().describe("Number of ideas to generate (default: 5)"),
297
306
  forceHeavy: z.boolean().optional().describe("Use expensive Grok 4 Heavy model ($3/$15) for deeper creativity")
298
307
  }),
299
- execute: async (args, { log }) => {
308
+ execute: async (args, { log, reportProgress }) => {
300
309
  const { topic, constraints, numIdeas = 5, forceHeavy = false } = args; // Changed: Default to cheap model
301
310
  const messages = [
302
311
  {
@@ -313,8 +322,10 @@ ${FORMAT_INSTRUCTION}`
313
322
  // Use GROK_4_1_FAST_REASONING for creative brainstorming (needs reasoning for creativity), GROK_4_HEAVY only if explicitly requested
314
323
  const model = forceHeavy ? GrokModel.GROK_4_HEAVY : GrokModel.GROK_4_1_FAST_REASONING;
315
324
  log?.info(`Brainstorming with Grok model: ${model} (Heavy: ${forceHeavy}, cost: ${forceHeavy ? 'expensive $3/$15' : 'cheap $0.20/$0.50 - latest 4.1'})`);
316
- // Use llm-orchestration context - brainstorm content may contain LLM-generated patterns
317
- return stripFormatting(await callGrok(messages, model, 0.95, 4000, true, 'llm-orchestration')); // High temperature for creativity
325
+ // Use heartbeat to prevent MCP timeout
326
+ const reportFn = reportProgress ?? (async () => { });
327
+ const result = await withHeartbeat(() => callGrok(messages, model, 0.95, 4000, true, 'llm-orchestration'), reportFn);
328
+ return stripFormatting(result);
318
329
  }
319
330
  };
320
331
  /**
@@ -12,6 +12,7 @@ import { tryOpenRouterGateway, isGatewayEnabled } from "../utils/openrouter-gate
12
12
  import { OPENAI_MODELS } from "../config/model-constants.js";
13
13
  import { FORMAT_INSTRUCTION } from "../utils/format-constants.js";
14
14
  import { stripFormatting } from "../utils/format-stripper.js";
15
+ import { withHeartbeat } from "../utils/streaming-helper.js";
15
16
  const __filename = fileURLToPath(import.meta.url);
16
17
  const __dirname = path.dirname(__filename);
17
18
  config({ path: path.resolve(__dirname, '../../../.env') });
@@ -476,7 +477,7 @@ export const openaiGpt5ReasonTool = {
476
477
  .default("analytical")
477
478
  .describe("Reasoning mode - must be one of: mathematical, scientific, logical, analytical")
478
479
  }),
479
- execute: async (args, { log }) => {
480
+ execute: async (args, { log, reportProgress }) => {
480
481
  const modePrompts = {
481
482
  mathematical: "Focus on mathematical proofs, calculations, and formal logic",
482
483
  scientific: "Apply scientific method and empirical reasoning",
@@ -497,8 +498,9 @@ ${FORMAT_INSTRUCTION}`
497
498
  content: args.query
498
499
  }
499
500
  ];
500
- // Use GPT-5.2-thinking with high reasoning effort for complex reasoning
501
- return await callOpenAI(messages, OPENAI_MODELS.DEFAULT, 0.7, 4000, "high");
501
+ // Use heartbeat to prevent MCP timeout during reasoning
502
+ const reportFn = reportProgress ?? (async () => { });
503
+ return await withHeartbeat(() => callOpenAI(messages, OPENAI_MODELS.DEFAULT, 0.7, 4000, "high"), reportFn);
502
504
  }
503
505
  };
504
506
  /**
@@ -565,7 +567,7 @@ export const openaiCodeReviewTool = {
565
567
  .optional()
566
568
  .describe("Focus areas - array of: security, performance, readability, bugs, best-practices")
567
569
  }),
568
- execute: async (args, { log }) => {
570
+ execute: async (args, { log, reportProgress }) => {
569
571
  const focusText = args.focusAreas
570
572
  ? `Focus especially on: ${args.focusAreas.join(', ')}`
571
573
  : "Review all aspects: security, performance, readability, bugs, and best practices";
@@ -584,7 +586,9 @@ ${FORMAT_INSTRUCTION}`
584
586
  content: `Review this code:\n\`\`\`${args.language || ''}\n${args.code}\n\`\`\``
585
587
  }
586
588
  ];
587
- return await callOpenAI(messages, OPENAI_MODELS.DEFAULT, 0.3, 4000, "medium");
589
+ // Use heartbeat to prevent MCP timeout
590
+ const reportFn = reportProgress ?? (async () => { });
591
+ return await withHeartbeat(() => callOpenAI(messages, OPENAI_MODELS.DEFAULT, 0.3, 4000, "medium"), reportFn);
588
592
  }
589
593
  };
590
594
  /**
@@ -5,6 +5,7 @@
5
5
  import { z } from "zod";
6
6
  import { FORMAT_INSTRUCTION } from "../utils/format-constants.js";
7
7
  import { stripFormatting } from "../utils/format-stripper.js";
8
+ import { withHeartbeat } from "../utils/streaming-helper.js";
8
9
  // NOTE: dotenv is loaded in server.ts before any imports
9
10
  // No need to reload here - just read from process.env
10
11
  // OpenRouter API configuration
@@ -114,7 +115,7 @@ export const qwenCoderTool = {
114
115
  language: z.string().optional().describe("Programming language (e.g., 'typescript', 'python')"),
115
116
  useFree: z.boolean().optional().default(false).describe("Use free tier model instead of premium")
116
117
  }),
117
- execute: async (args, { log }) => {
118
+ execute: async (args, { log, reportProgress }) => {
118
119
  const taskPrompts = {
119
120
  generate: "Generate new code according to requirements",
120
121
  review: "Review code for quality, bugs, and improvements",
@@ -138,7 +139,9 @@ ${FORMAT_INSTRUCTION}`;
138
139
  { role: "user", content: userPrompt }
139
140
  ];
140
141
  const model = args.useFree === true ? OpenRouterModel.QWEN3_30B : OpenRouterModel.QWEN3_CODER;
141
- return await callOpenRouter(messages, model, 0.2, 8000);
142
+ // Use heartbeat to prevent MCP timeout
143
+ const reportFn = reportProgress ?? (async () => { });
144
+ return await withHeartbeat(() => callOpenRouter(messages, model, 0.2, 8000), reportFn);
142
145
  }
143
146
  };
144
147
  /**
@@ -274,7 +277,7 @@ export const qwenAlgoTool = {
274
277
  .default("general")
275
278
  .describe("Analysis focus - must be one of: optimize, complexity, data-structure, memory, general")
276
279
  }),
277
- execute: async (args, { log }) => {
280
+ execute: async (args, { log, reportProgress }) => {
278
281
  const focusPrompts = {
279
282
  optimize: "Focus on performance optimization, bottlenecks, and algorithmic improvements.",
280
283
  complexity: "Focus on time/space complexity analysis (best, average, worst case).",
@@ -296,7 +299,9 @@ ${FORMAT_INSTRUCTION}`
296
299
  : args.problem
297
300
  }
298
301
  ];
299
- return await callOpenRouter(messages, OpenRouterModel.QWQ_32B, 0.3, 8000);
302
+ // Use heartbeat to prevent MCP timeout
303
+ const reportFn = reportProgress ?? (async () => { });
304
+ return await withHeartbeat(() => callOpenRouter(messages, OpenRouterModel.QWQ_32B, 0.3, 8000), reportFn);
300
305
  }
301
306
  };
302
307
  /**
@@ -355,7 +360,7 @@ export const kimiThinkingTool = {
355
360
  .describe("Reasoning approach - must be one of: step-by-step, analytical, creative, systematic"),
356
361
  maxSteps: z.number().optional().default(3).describe("Maximum reasoning steps (default: 3)")
357
362
  }),
358
- execute: async (args, { log }) => {
363
+ execute: async (args, { log, reportProgress }) => {
359
364
  const approachPrompts = {
360
365
  "step-by-step": "Break down the problem into clear steps and solve systematically",
361
366
  analytical: "Analyze the problem deeply, considering multiple perspectives and implications",
@@ -375,12 +380,13 @@ ${FORMAT_INSTRUCTION}`
375
380
  content: args.problem
376
381
  }
377
382
  ];
378
- // Optimized for speed: lower tokens (3k), temp (0.4), top_p (0.9), penalties to reduce verbosity
379
- return await callOpenRouter(messages, OpenRouterModel.KIMI_K2_THINKING, 0.4, 3000, {
383
+ // Use heartbeat to prevent MCP timeout during reasoning
384
+ const reportFn = reportProgress ?? (async () => { });
385
+ return await withHeartbeat(() => callOpenRouter(messages, OpenRouterModel.KIMI_K2_THINKING, 0.4, 3000, {
380
386
  top_p: 0.9,
381
387
  presence_penalty: 0.1,
382
388
  frequency_penalty: 0.2
383
- });
389
+ }), reportFn);
384
390
  }
385
391
  };
386
392
  /**
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Streaming Helper - Progress Heartbeats for Long-Running MCP Tools
3
+ *
4
+ * Prevents MCP timeout (~60s) by sending periodic progress notifications.
5
+ * Uses standard MCP notifications/progress which should work with all clients.
6
+ */
7
+ /**
8
+ * Create a heartbeat that sends progress notifications at regular intervals.
9
+ * Useful for keeping MCP connections alive during long-running operations.
10
+ */
11
+ export function createHeartbeat(options) {
12
+ const { intervalMs = 5000, reportProgress, onError } = options;
13
+ let progress = 0;
14
+ let stopped = false;
15
+ const interval = setInterval(async () => {
16
+ if (stopped)
17
+ return;
18
+ // Increment progress, cap at 90% (leave room for completion)
19
+ progress = Math.min(progress + 10, 90);
20
+ try {
21
+ await reportProgress({ progress, total: 100 });
22
+ }
23
+ catch (error) {
24
+ // Client may have disconnected - stop heartbeat
25
+ if (onError && error instanceof Error) {
26
+ onError(error);
27
+ }
28
+ stopped = true;
29
+ clearInterval(interval);
30
+ }
31
+ }, intervalMs);
32
+ return {
33
+ stop: () => {
34
+ stopped = true;
35
+ clearInterval(interval);
36
+ },
37
+ complete: async () => {
38
+ stopped = true;
39
+ clearInterval(interval);
40
+ try {
41
+ await reportProgress({ progress: 100, total: 100 });
42
+ }
43
+ catch {
44
+ // Ignore completion errors - client may have disconnected
45
+ }
46
+ }
47
+ };
48
+ }
49
+ /**
50
+ * Execute a function with automatic progress heartbeats.
51
+ * Heartbeat starts immediately, stops when function completes (success or error).
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * const result = await withHeartbeat(
56
+ * () => callExternalAPI(prompt),
57
+ * context.reportProgress
58
+ * );
59
+ * ```
60
+ */
61
+ export async function withHeartbeat(fn, reportProgress, intervalMs = 5000) {
62
+ const heartbeat = createHeartbeat({ intervalMs, reportProgress });
63
+ try {
64
+ const result = await fn();
65
+ await heartbeat.complete();
66
+ return result;
67
+ }
68
+ catch (error) {
69
+ heartbeat.stop();
70
+ throw error;
71
+ }
72
+ }
73
+ /**
74
+ * Wrapper for streaming API responses with heartbeat support.
75
+ * Sends progress while waiting for streaming response to complete.
76
+ *
77
+ * @param stream - AsyncIterable of chunks (e.g., from OpenAI streaming API)
78
+ * @param reportProgress - Progress reporter from MCP context
79
+ * @param onChunk - Optional callback for each chunk (for streamContent)
80
+ */
81
+ export async function withStreamingHeartbeat(stream, reportProgress, onChunk) {
82
+ const heartbeat = createHeartbeat({ reportProgress });
83
+ const chunks = [];
84
+ try {
85
+ for await (const chunk of stream) {
86
+ chunks.push(chunk);
87
+ if (onChunk) {
88
+ await onChunk(chunk);
89
+ }
90
+ }
91
+ await heartbeat.complete();
92
+ return chunks;
93
+ }
94
+ catch (error) {
95
+ heartbeat.stop();
96
+ throw error;
97
+ }
98
+ }
99
+ /**
100
+ * Check if reportProgress is available in the context.
101
+ * FastMCP always provides it, but this is a safety check.
102
+ */
103
+ export function hasProgressSupport(context) {
104
+ return (typeof context === 'object' &&
105
+ context !== null &&
106
+ 'reportProgress' in context &&
107
+ typeof context.reportProgress === 'function');
108
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "tachibot-mcp",
3
3
  "mcpName": "io.github.byPawel/tachibot-mcp",
4
4
  "displayName": "TachiBot MCP - Universal AI Orchestrator",
5
- "version": "2.9.0",
5
+ "version": "2.10.0",
6
6
  "type": "module",
7
7
  "main": "dist/src/server.js",
8
8
  "bin": {