tachibot-mcp 2.9.0 → 2.10.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.
@@ -29,7 +29,15 @@ export class PromptEngineerLite {
29
29
  ['meta_prompting', (q) => `First, write a better prompt for "${q}" that would get a more useful response. Then, answer using that improved prompt.`],
30
30
  // Debate
31
31
  ['adversarial', (q) => `For "${q}": First argue strongly FOR this position with best evidence. Then argue strongly AGAINST with counterarguments. Finally, synthesize a balanced view.`],
32
- ['persona_simulation', (q) => `Simulate expert debate on "${q}": Have a skeptic raise concerns, an optimist highlight benefits, a pragmatist focus on implementation, and a visionary explore possibilities. Synthesize insights.`]
32
+ ['persona_simulation', (q) => `Simulate expert debate on "${q}": Have a skeptic raise concerns, an optimist highlight benefits, a pragmatist focus on implementation, and a visionary explore possibilities. Synthesize insights.`],
33
+ // Judgment (Council of Experts)
34
+ ['council_of_experts', (q) => `Multi-model council analysis for "${q}":
35
+ 1. GATHER PERSPECTIVES: Consider this from multiple expert angles (researcher, engineer, skeptic, innovator)
36
+ 2. EXTRACT BEST ELEMENTS: What's the most valuable insight from each perspective?
37
+ 3. IDENTIFY CONSENSUS: Where do all perspectives agree?
38
+ 4. RESOLVE CONFLICTS: Where perspectives differ, weigh the tradeoffs
39
+ 5. SYNTHESIZE VERDICT: Combine the best elements into a unified, actionable answer
40
+ Output format: Perspectives → Best Elements → Consensus → Conflicts → Final Synthesis`]
33
41
  ]);
34
42
  // Compact technique mapping (aliases to canonical names)
35
43
  this.techniqueMap = {
@@ -65,7 +73,11 @@ export class PromptEngineerLite {
65
73
  'principles': 'constitutional',
66
74
  'improve_prompt': 'meta_prompting',
67
75
  'critic': 'adversarial',
68
- 'debate': 'persona_simulation'
76
+ 'debate': 'persona_simulation',
77
+ // Judgment aliases
78
+ 'judge': 'council_of_experts',
79
+ 'council': 'council_of_experts',
80
+ 'expert_council': 'council_of_experts'
69
81
  };
70
82
  }
71
83
  applyTechnique(tool, technique, query, prev) {
@@ -118,7 +130,9 @@ export class PromptEngineerLite {
118
130
  'meta_prompting': 'Improve prompt',
119
131
  // Debate
120
132
  'adversarial': 'Pro/Con',
121
- 'persona_simulation': 'Expert debate'
133
+ 'persona_simulation': 'Expert debate',
134
+ // Judgment
135
+ 'council_of_experts': 'Council judge'
122
136
  };
123
137
  const key = this.techniqueMap[technique] || technique;
124
138
  return desc[key] || technique;
@@ -144,19 +144,27 @@ function safeAddTool(tool) {
144
144
  }
145
145
  }
146
146
  // Apply ANSI rendering to string results (centralized - no need to edit each tool!)
147
- // Just render everything - the ~50-100ms for large outputs is fine for CLI
147
+ // Return as TextContent for Claude Code Desktop compatibility
148
+ // Plain strings work in CLI but Desktop needs { type: "text", text: "..." }
148
149
  if (typeof result === 'string') {
149
150
  try {
150
151
  const model = inferModelFromTool(tool.name);
151
152
  // Tools returning null handle their own rendering - skip extra badge
152
153
  // (e.g., nextThought renders its own BigText header)
154
+ let renderedText;
153
155
  if (model === null) {
154
- return renderOutput(result); // No model badge
156
+ renderedText = renderOutput(result); // No model badge
155
157
  }
156
- return renderOutput(result, model);
158
+ else {
159
+ renderedText = renderOutput(result, model);
160
+ }
161
+ // Return as TextContent object for Claude Code Desktop compatibility
162
+ // Adds ~12 tokens fixed overhead, negligible for typical responses
163
+ return { type: "text", text: renderedText };
157
164
  }
158
165
  catch {
159
- return result; // Fallback to raw only on parse errors
166
+ // Fallback to TextContent with raw result on parse errors
167
+ return { type: "text", text: result };
160
168
  }
161
169
  }
162
170
  return result;
@@ -9,6 +9,7 @@ import { GEMINI_MODELS } from "../config/model-constants.js";
9
9
  import { tryOpenRouterGateway, isGatewayEnabled } from "../utils/openrouter-gateway.js";
10
10
  import { stripFormatting } from "../utils/format-stripper.js";
11
11
  import { FORMAT_INSTRUCTION } from "../utils/format-constants.js";
12
+ import { withHeartbeat } from "../utils/streaming-helper.js";
12
13
  // Note: renderOutput is applied centrally in server.ts safeAddTool() - no need to import here
13
14
  // NOTE: dotenv is loaded in server.ts before any imports
14
15
  // No need to reload here - just read from process.env
@@ -163,7 +164,7 @@ export const geminiQueryTool = {
163
164
  .default("gemini-3")
164
165
  .describe("Model variant - must be one of: gemini-3 (default), pro, flash")
165
166
  }),
166
- execute: async (args, { log }) => {
167
+ execute: async (args, { log, reportProgress }) => {
167
168
  let model = GEMINI_MODELS.GEMINI_3_PRO; // Default to Gemini 3
168
169
  if (args.model === "flash") {
169
170
  model = GEMINI_MODELS.FLASH;
@@ -172,7 +173,9 @@ export const geminiQueryTool = {
172
173
  model = GEMINI_MODELS.PRO;
173
174
  }
174
175
  // Skip validation - queries may contain code or LLM-generated content
175
- return stripFormatting(await callGemini(args.prompt, model, undefined, 0.7, 'llm-orchestration'));
176
+ const reportFn = reportProgress ?? (async () => { });
177
+ const result = await withHeartbeat(() => callGemini(args.prompt, model, undefined, 0.7, 'llm-orchestration'), reportFn);
178
+ return stripFormatting(result);
176
179
  }
177
180
  };
178
181
  /**
@@ -190,13 +193,14 @@ export const geminiBrainstormTool = {
190
193
  claudeThoughts: z.string().optional().describe("Claude's initial thoughts to build upon"),
191
194
  maxRounds: z.number().optional().default(1).describe("Number of brainstorming rounds (default: 1)")
192
195
  }),
193
- execute: async (args, { log }) => {
196
+ execute: async (args, { log, reportProgress }) => {
194
197
  const systemPrompt = `Creative brainstorming partner.
195
198
  ${args.claudeThoughts ? `Building on: ${args.claudeThoughts}` : ''}
196
199
  Generate: multiple approaches, unconventional ideas, challenges, feasibility.
197
200
  ${FORMAT_INSTRUCTION}`;
198
201
  // Skip validation for internal calls - input validated at MCP layer
199
- const response = await callGemini(args.prompt, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.9, 'llm-orchestration');
202
+ const reportFn = reportProgress ?? (async () => { });
203
+ const response = await withHeartbeat(() => callGemini(args.prompt, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.9, 'llm-orchestration'), reportFn);
200
204
  // If multiple rounds requested, we could iterate here
201
205
  // For now, return the single response
202
206
  return stripFormatting(response);
@@ -212,9 +216,9 @@ export const geminiAnalyzeCodeTool = {
212
216
  parameters: z.object({
213
217
  code: z.string().describe("The actual source code to analyze (REQUIRED - put your code here)"),
214
218
  language: z.string().optional().describe("Programming language (e.g., 'typescript', 'python')"),
215
- focus: z.enum(["quality", "security", "performance", "bugs", "general"]).optional().default("general").describe("Analysis focus area - must be one of: quality, security, performance, bugs, general")
219
+ focus: z.string().optional().default("general").describe("Analysis focus (e.g., quality, security, performance, bugs, general)")
216
220
  }),
217
- execute: async (args, { log }) => {
221
+ execute: async (args, { log, reportProgress }) => {
218
222
  const focusPrompts = {
219
223
  quality: "Focus on code quality, readability, and best practices",
220
224
  security: "Focus on security vulnerabilities and potential exploits",
@@ -222,12 +226,14 @@ export const geminiAnalyzeCodeTool = {
222
226
  bugs: "Focus on finding bugs, logic errors, and edge cases",
223
227
  general: "Provide a comprehensive analysis covering all aspects"
224
228
  };
229
+ const focusText = focusPrompts[args.focus || 'general'] || `Focus on: ${args.focus}`;
225
230
  const systemPrompt = `Expert code reviewer. ${args.language || ''} code.
226
- ${focusPrompts[args.focus || 'general']}.
231
+ ${focusText}.
227
232
  ${FORMAT_INSTRUCTION}`;
228
233
  // Skip validation - code analysis naturally contains patterns that trigger false positives
229
- return stripFormatting(await callGemini(`Analyze this code:\n\n\`\`\`${args.language || ''}\n${args.code}\n\`\`\``, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.3, 'llm-orchestration' // skipValidation for code content
230
- ));
234
+ const reportFn = reportProgress ?? (async () => { });
235
+ const result = await withHeartbeat(() => callGemini(`Analyze this code:\n\n\`\`\`${args.language || ''}\n${args.code}\n\`\`\``, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.3, 'llm-orchestration'), reportFn);
236
+ return stripFormatting(result);
231
237
  }
232
238
  };
233
239
  /**
@@ -239,12 +245,12 @@ export const geminiAnalyzeTextTool = {
239
245
  description: "Text analysis. Put the TEXT in the 'text' parameter, NOT in 'type'.",
240
246
  parameters: z.object({
241
247
  text: z.string().describe("The text to analyze (REQUIRED - put your text here)"),
242
- type: z.enum(["sentiment", "summary", "entities", "key-points", "general"])
248
+ type: z.string()
243
249
  .optional()
244
250
  .default("general")
245
- .describe("Analysis type - must be one of: sentiment, summary, entities, key-points, general")
251
+ .describe("Analysis type (e.g., sentiment, summary, entities, key-points, general)")
246
252
  }),
247
- execute: async (args, { log }) => {
253
+ execute: async (args, { log, reportProgress }) => {
248
254
  const analysisPrompts = {
249
255
  sentiment: "Analyze the sentiment (positive, negative, neutral) with confidence scores",
250
256
  summary: "Provide a concise summary of the main points",
@@ -252,11 +258,13 @@ export const geminiAnalyzeTextTool = {
252
258
  "key-points": "Identify and list the key points and main arguments",
253
259
  general: "Provide comprehensive text analysis including sentiment, key points, and entities"
254
260
  };
255
- const systemPrompt = `Text analysis expert. ${analysisPrompts[args.type || 'general']}.
261
+ const analysisText = analysisPrompts[args.type || 'general'] || `Perform ${args.type} analysis`;
262
+ const systemPrompt = `Text analysis expert. ${analysisText}.
256
263
  ${FORMAT_INSTRUCTION}`;
257
264
  // Skip validation - text analysis may contain patterns from LLM discussions
258
- return stripFormatting(await callGemini(`Analyze this text:\n\n${args.text}`, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.3, 'llm-orchestration' // skipValidation for internal calls
259
- ));
265
+ const reportFn = reportProgress ?? (async () => { });
266
+ const result = await withHeartbeat(() => callGemini(`Analyze this text:\n\n${args.text}`, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.3, 'llm-orchestration'), reportFn);
267
+ return stripFormatting(result);
260
268
  }
261
269
  };
262
270
  /**
@@ -277,7 +285,7 @@ export const geminiSummarizeTool = {
277
285
  .default("paragraph")
278
286
  .describe("Output format - must be one of: paragraph, bullet-points, outline")
279
287
  }),
280
- execute: async (args, { log }) => {
288
+ execute: async (args, { log, reportProgress }) => {
281
289
  const lengthGuides = {
282
290
  brief: "1-2 sentences capturing the essence",
283
291
  moderate: "1-2 paragraphs with main points",
@@ -297,8 +305,9 @@ Focus on:
297
305
  - Conclusions and implications
298
306
  ${FORMAT_INSTRUCTION}`;
299
307
  // Skip validation for internal summarization calls
300
- return stripFormatting(await callGemini(`Summarize this content:\n\n${args.content}`, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.3, 'llm-orchestration' // skipValidation
301
- ));
308
+ const reportFn = reportProgress ?? (async () => { });
309
+ const result = await withHeartbeat(() => callGemini(`Summarize this content:\n\n${args.content}`, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.3, 'llm-orchestration'), reportFn);
310
+ return stripFormatting(result);
302
311
  }
303
312
  };
304
313
  /**
@@ -314,7 +323,7 @@ export const geminiImagePromptTool = {
314
323
  mood: z.string().optional().describe("Mood or atmosphere (e.g., 'serene', 'dramatic')"),
315
324
  details: z.string().optional().describe("Additional details to include")
316
325
  }),
317
- execute: async (args, { log }) => {
326
+ execute: async (args, { log, reportProgress }) => {
318
327
  const systemPrompt = `You are an expert at creating detailed image generation prompts.
319
328
  Transform the user's description into a detailed, effective prompt for image generation.
320
329
 
@@ -333,7 +342,9 @@ ${args.style ? `Style: ${args.style}` : ''}
333
342
  ${args.mood ? `Mood: ${args.mood}` : ''}
334
343
  ${args.details ? `Additional details: ${args.details}` : ''}`;
335
344
  // Skip validation for creative content generation
336
- return stripFormatting(await callGemini(userPrompt, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.7, 'llm-orchestration'));
345
+ const reportFn = reportProgress ?? (async () => { });
346
+ const result = await withHeartbeat(() => callGemini(userPrompt, GEMINI_MODELS.GEMINI_3_PRO, systemPrompt, 0.7, 'llm-orchestration'), reportFn);
347
+ return stripFormatting(result);
337
348
  }
338
349
  };
339
350
  /**
@@ -353,7 +364,7 @@ export const geminiSearchTool = {
353
364
  dynamicThreshold: z.number().min(0).max(1).optional().default(0.7)
354
365
  .describe("Confidence threshold for dynamic mode (0-1)")
355
366
  }),
356
- execute: async (args, { log }) => {
367
+ execute: async (args, { log, reportProgress }) => {
357
368
  if (!GEMINI_API_KEY) {
358
369
  return `[Gemini API key not configured. Add GOOGLE_API_KEY to .env file]`;
359
370
  }
@@ -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);
@@ -125,13 +126,13 @@ export const grokReasonTool = {
125
126
  description: "Deep reasoning. Put your PROBLEM or QUESTION in the 'problem' parameter.",
126
127
  parameters: z.object({
127
128
  problem: z.string().describe("The problem or question to reason about (REQUIRED - put your question here)"),
128
- approach: z.enum(["analytical", "creative", "systematic", "first-principles"])
129
+ approach: z.string()
129
130
  .optional()
130
- .describe("Reasoning approach - must be one of: analytical, creative, systematic, first-principles"),
131
+ .describe("Reasoning approach (e.g., analytical, creative, systematic, first-principles)"),
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
  /**
@@ -168,13 +171,13 @@ export const grokCodeTool = {
168
171
  name: "grok_code",
169
172
  description: "Code analysis. Put the CODE in the 'code' parameter, NOT in 'task'.",
170
173
  parameters: z.object({
171
- task: z.enum(["analyze", "optimize", "debug", "review", "refactor"])
172
- .describe("Code task - must be one of: analyze, optimize, debug, review, refactor"),
174
+ task: z.string()
175
+ .describe("Code task (e.g., analyze, optimize, debug, review, refactor)"),
173
176
  code: z.string().describe("The actual source code to analyze (REQUIRED - put your code here)"),
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
  /**
@@ -258,11 +265,11 @@ export const grokArchitectTool = {
258
265
  parameters: z.object({
259
266
  requirements: z.string().describe("The architecture requirements or design question (REQUIRED - put your question here)"),
260
267
  constraints: z.string().optional().describe("Technical or business constraints to consider"),
261
- scale: z.enum(["small", "medium", "large", "enterprise"])
268
+ scale: z.string()
262
269
  .optional()
263
- .describe("Expected scale - must be one of: small, medium, large, enterprise")
270
+ .describe("Expected scale (e.g., 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') });
@@ -471,12 +472,12 @@ export const openaiGpt5ReasonTool = {
471
472
  parameters: z.object({
472
473
  query: z.string().describe("The question or problem to reason about (REQUIRED - put your question here)"),
473
474
  context: z.string().optional().describe("Additional context for the reasoning task"),
474
- mode: z.enum(["mathematical", "scientific", "logical", "analytical"])
475
+ mode: z.string()
475
476
  .optional()
476
477
  .default("analytical")
477
- .describe("Reasoning mode - must be one of: mathematical, scientific, logical, analytical")
478
+ .describe("Reasoning mode (e.g., 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
  /**
@@ -512,9 +514,9 @@ export const openAIBrainstormTool = {
512
514
  problem: z.string().describe("The problem or topic to brainstorm about (REQUIRED - put your question here)"),
513
515
  constraints: z.string().optional().describe("Any constraints to consider in brainstorming"),
514
516
  quantity: z.number().optional().describe("Number of ideas to generate (default: 5)"),
515
- style: z.enum(["innovative", "practical", "wild", "systematic"])
517
+ style: z.string()
516
518
  .optional()
517
- .describe("Brainstorming style - must be one of: innovative, practical, wild, systematic"),
519
+ .describe("Brainstorming style (e.g., innovative, practical, wild, systematic)"),
518
520
  model: z.enum(["gpt-5.2", "gpt-5.2-pro"])
519
521
  .optional()
520
522
  .describe("Model to use - gpt-5.2 (default) or gpt-5.2-pro (more expensive)"),
@@ -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
  /**
@@ -151,10 +154,10 @@ export const qwqReasoningTool = {
151
154
  parameters: z.object({
152
155
  problem: z.string().describe("The problem to reason about (REQUIRED - put your question here)"),
153
156
  context: z.string().optional().describe("Additional context for the reasoning task"),
154
- approach: z.enum(["step-by-step", "mathematical", "logical", "creative"])
157
+ approach: z.string()
155
158
  .optional()
156
159
  .default("step-by-step")
157
- .describe("Reasoning approach - must be one of: step-by-step, mathematical, logical, creative"),
160
+ .describe("Reasoning approach (e.g., step-by-step, mathematical, logical, creative)"),
158
161
  useFree: z.boolean().optional().default(true).describe("Use free tier model (default: true)")
159
162
  }),
160
163
  execute: async (args, { log }) => {
@@ -191,10 +194,10 @@ export const qwenGeneralTool = {
191
194
  description: "General-purpose assistance with Qwen3. Put your QUERY in the 'query' parameter.",
192
195
  parameters: z.object({
193
196
  query: z.string().describe("Your question or request (REQUIRED - put your question here)"),
194
- mode: z.enum(["chat", "analysis", "creative", "technical"])
197
+ mode: z.string()
195
198
  .optional()
196
199
  .default("chat")
197
- .describe("Interaction mode - must be one of: chat, analysis, creative, technical"),
200
+ .describe("Interaction mode (e.g., chat, analysis, creative, technical)"),
198
201
  useFree: z.boolean().optional().default(true).describe("Use free tier model (default: true)")
199
202
  }),
200
203
  execute: async (args, { log }) => {
@@ -269,12 +272,12 @@ export const qwenAlgoTool = {
269
272
  parameters: z.object({
270
273
  problem: z.string().describe("The algorithm problem or code to analyze (REQUIRED - put your question/code here)"),
271
274
  context: z.string().optional().describe("Additional context: current performance, constraints"),
272
- focus: z.enum(["optimize", "complexity", "data-structure", "memory", "general"])
275
+ focus: z.string()
273
276
  .optional()
274
277
  .default("general")
275
- .describe("Analysis focus - must be one of: optimize, complexity, data-structure, memory, general")
278
+ .describe("Analysis focus (e.g., optimize, complexity, data-structure, memory, correctness, 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
  /**
@@ -349,13 +354,13 @@ export const kimiThinkingTool = {
349
354
  parameters: z.object({
350
355
  problem: z.string().describe("The problem to reason about (REQUIRED - put your question here)"),
351
356
  context: z.string().optional().describe("Additional context for the reasoning task"),
352
- approach: z.enum(["step-by-step", "analytical", "creative", "systematic"])
357
+ approach: z.string()
353
358
  .optional()
354
359
  .default("step-by-step")
355
- .describe("Reasoning approach - must be one of: step-by-step, analytical, creative, systematic"),
360
+ .describe("Reasoning approach (e.g., 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
  /**
@@ -7,6 +7,7 @@ import { z } from "zod";
7
7
  import { getPerplexityApiKey } from "../utils/api-keys.js";
8
8
  import { stripFormatting } from "../utils/format-stripper.js";
9
9
  import { FORMAT_INSTRUCTION } from "../utils/format-constants.js";
10
+ import { withHeartbeat } from "../utils/streaming-helper.js";
10
11
  // Perplexity API configuration
11
12
  const PERPLEXITY_API_URL = "https://api.perplexity.ai";
12
13
  // Available Perplexity models (2025 latest)
@@ -14,7 +15,7 @@ export var PerplexityModel;
14
15
  (function (PerplexityModel) {
15
16
  // Models from Perplexity API docs
16
17
  PerplexityModel["SONAR_PRO"] = "sonar";
17
- PerplexityModel["SONAR_REASONING_PRO"] = "sonar-reasoning";
18
+ PerplexityModel["SONAR_REASONING_PRO"] = "sonar-reasoning-pro";
18
19
  PerplexityModel["SONAR_DEEP_RESEARCH"] = "sonar-deep-research";
19
20
  PerplexityModel["SONAR"] = "sonar";
20
21
  PerplexityModel["SONAR_SMALL"] = "sonar-small";
@@ -90,7 +91,7 @@ export const perplexityAskTool = {
90
91
  .optional()
91
92
  .describe("How recent the results should be - must be one of: hour, day, week, month, year")
92
93
  }),
93
- execute: async (args, { log }) => {
94
+ execute: async (args, { log, reportProgress }) => {
94
95
  // Get current date for accurate recency context
95
96
  const now = new Date();
96
97
  const currentDate = now.toLocaleDateString('en-US', {
@@ -109,7 +110,9 @@ export const perplexityAskTool = {
109
110
  content: args.query
110
111
  }
111
112
  ];
112
- return stripFormatting(await callPerplexity(messages, PerplexityModel.SONAR_PRO, args.searchDomain, args.searchRecency));
113
+ const reportFn = reportProgress ?? (async () => { });
114
+ const result = await withHeartbeat(() => callPerplexity(messages, PerplexityModel.SONAR_PRO, args.searchDomain, args.searchRecency), reportFn);
115
+ return stripFormatting(result);
113
116
  }
114
117
  };
115
118
  /**
@@ -122,12 +125,13 @@ export const perplexityResearchTool = {
122
125
  parameters: z.object({
123
126
  topic: z.string().describe("The research topic (REQUIRED - put your topic here)"),
124
127
  questions: z.array(z.string()).optional().describe("Specific questions to research"),
125
- depth: z.enum(["quick", "standard", "deep"])
128
+ depth: z.string()
126
129
  .optional()
127
- .describe("Research depth - must be one of: quick, standard, deep")
130
+ .describe("Research depth (e.g., quick, standard, deep)")
128
131
  }),
129
- execute: async (args, { log }) => {
132
+ execute: async (args, { log, reportProgress }) => {
130
133
  const { topic, questions, depth = "standard" } = args;
134
+ const reportFn = reportProgress ?? (async () => { });
131
135
  // Get current date for accurate research context
132
136
  const now = new Date();
133
137
  const currentDate = now.toLocaleDateString('en-US', {
@@ -151,36 +155,40 @@ export const perplexityResearchTool = {
151
155
  `What are the future predictions for ${topic}?`,
152
156
  `What are the best practices and recommendations for ${topic}?`] :
153
157
  researchQuestions;
154
- let research = `# Research Report: ${topic}\n\n`;
155
- // Conduct research for each question
156
- for (const question of questionsToAsk) {
157
- const messages = [
158
+ // Wrap entire research process in heartbeat since it makes multiple calls
159
+ const result = await withHeartbeat(async () => {
160
+ let research = `# Research Report: ${topic}\n\n`;
161
+ // Conduct research for each question
162
+ for (const question of questionsToAsk) {
163
+ const messages = [
164
+ {
165
+ role: "system",
166
+ content: `Research expert. Today: ${currentDate}. Factual info with sources.${FORMAT_INSTRUCTION}`
167
+ },
168
+ {
169
+ role: "user",
170
+ content: question
171
+ }
172
+ ];
173
+ const answer = await callPerplexity(messages, PerplexityModel.SONAR_PRO);
174
+ research += `## ${question}\n\n${answer}\n\n`;
175
+ }
176
+ // Add synthesis
177
+ const synthesisMessages = [
158
178
  {
159
179
  role: "system",
160
- content: `Research expert. Today: ${currentDate}. Factual info with sources.${FORMAT_INSTRUCTION}`
180
+ content: `Research synthesizer. Concise summary of key findings.${FORMAT_INSTRUCTION}`
161
181
  },
162
182
  {
163
183
  role: "user",
164
- content: question
184
+ content: `Synthesize these research findings into key insights:\n\n${research}`
165
185
  }
166
186
  ];
167
- const answer = await callPerplexity(messages, PerplexityModel.SONAR_PRO);
168
- research += `## ${question}\n\n${answer}\n\n`;
169
- }
170
- // Add synthesis
171
- const synthesisMessages = [
172
- {
173
- role: "system",
174
- content: `Research synthesizer. Concise summary of key findings.${FORMAT_INSTRUCTION}`
175
- },
176
- {
177
- role: "user",
178
- content: `Synthesize these research findings into key insights:\n\n${research}`
179
- }
180
- ];
181
- const synthesis = await callPerplexity(synthesisMessages, PerplexityModel.SONAR_PRO);
182
- research += `## Synthesis\n\n${synthesis}`;
183
- return stripFormatting(research);
187
+ const synthesis = await callPerplexity(synthesisMessages, PerplexityModel.SONAR_PRO);
188
+ research += `## Synthesis\n\n${synthesis}`;
189
+ return research;
190
+ }, reportFn);
191
+ return stripFormatting(result);
184
192
  }
185
193
  };
186
194
  /**
@@ -193,12 +201,13 @@ export const perplexityReasonTool = {
193
201
  parameters: z.object({
194
202
  problem: z.string().describe("The problem to reason about (REQUIRED - put your question here)"),
195
203
  context: z.string().optional().describe("Additional context for the reasoning task"),
196
- approach: z.enum(["analytical", "creative", "systematic", "comparative"])
204
+ approach: z.string()
197
205
  .optional()
198
- .describe("Reasoning approach - must be one of: analytical, creative, systematic, comparative")
206
+ .describe("Reasoning approach (e.g., analytical, creative, systematic, comparative)")
199
207
  }),
200
- execute: async (args, { log }) => {
208
+ execute: async (args, { log, reportProgress }) => {
201
209
  const { problem, context, approach = "analytical" } = args;
210
+ const reportFn = reportProgress ?? (async () => { });
202
211
  // Get current date for accurate reasoning context
203
212
  const now = new Date();
204
213
  const currentDate = now.toLocaleDateString('en-US', {
@@ -224,7 +233,8 @@ ${context ? `Context: ${context}` : ''}${FORMAT_INSTRUCTION}`
224
233
  content: problem
225
234
  }
226
235
  ];
227
- return stripFormatting(await callPerplexity(messages, PerplexityModel.SONAR_REASONING_PRO));
236
+ const result = await withHeartbeat(() => callPerplexity(messages, PerplexityModel.SONAR_REASONING_PRO), reportFn);
237
+ return stripFormatting(result);
228
238
  }
229
239
  };
230
240
  /**
@@ -109,6 +109,9 @@ const TECHNIQUES = {
109
109
  { name: "adversarial", alias: "critic", description: "Argue FOR then AGAINST, find counterarguments, synthesize" },
110
110
  { name: "persona_simulation", alias: "debate", description: "Simulate experts debating (single prompt, not real multi-agent)" },
111
111
  ],
112
+ judgment: [
113
+ { name: "council_of_experts", alias: "judge", description: "Multi-model council: gather diverse perspectives, extract best elements, synthesize final verdict" },
114
+ ],
112
115
  };
113
116
  // All technique names for validation
114
117
  const ALL_TECHNIQUE_NAMES = Object.values(TECHNIQUES)
@@ -128,9 +131,9 @@ function generateToken() {
128
131
  */
129
132
  export const listPromptTechniquesTool = {
130
133
  name: "list_prompt_techniques",
131
- description: "Discover available prompt engineering techniques. Shows all 21 techniques organized by category.",
134
+ description: "Discover available prompt engineering techniques. Shows all 22 techniques organized by category.",
132
135
  parameters: z.object({
133
- filter: z.enum(["all", "creative", "research", "analytical", "reflective", "reasoning", "verification", "meta", "debate"])
136
+ filter: z.enum(["all", "creative", "research", "analytical", "reflective", "reasoning", "verification", "meta", "debate", "judgment"])
134
137
  .optional()
135
138
  .default("all")
136
139
  .describe("Filter by category (default: all)")
@@ -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.1",
6
6
  "type": "module",
7
7
  "main": "dist/src/server.js",
8
8
  "bin": {