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.
- package/dist/src/prompt-engineer-lite.js +17 -3
- package/dist/src/server.js +12 -4
- package/dist/src/tools/gemini-tools.js +32 -21
- package/dist/src/tools/grok-tools.js +32 -21
- package/dist/src/tools/openai-tools.js +13 -9
- package/dist/src/tools/openrouter-tools.js +22 -16
- package/dist/src/tools/perplexity-tools.js +43 -33
- package/dist/src/tools/prompt-technique-tools.js +5 -2
- package/dist/src/utils/streaming-helper.js +108 -0
- package/package.json +1 -1
|
@@ -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;
|
package/dist/src/server.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
156
|
+
renderedText = renderOutput(result); // No model badge
|
|
155
157
|
}
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
${
|
|
231
|
+
${focusText}.
|
|
227
232
|
${FORMAT_INSTRUCTION}`;
|
|
228
233
|
// Skip validation - code analysis naturally contains patterns that trigger false positives
|
|
229
|
-
|
|
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.
|
|
248
|
+
type: z.string()
|
|
243
249
|
.optional()
|
|
244
250
|
.default("general")
|
|
245
|
-
.describe("Analysis type
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
129
|
+
approach: z.string()
|
|
129
130
|
.optional()
|
|
130
|
-
.describe("Reasoning approach
|
|
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
|
|
160
|
-
|
|
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.
|
|
172
|
-
.describe("Code task
|
|
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
|
|
202
|
-
|
|
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
|
|
248
|
-
|
|
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.
|
|
268
|
+
scale: z.string()
|
|
262
269
|
.optional()
|
|
263
|
-
.describe("Expected scale
|
|
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
|
|
283
|
-
|
|
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
|
|
317
|
-
|
|
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.
|
|
475
|
+
mode: z.string()
|
|
475
476
|
.optional()
|
|
476
477
|
.default("analytical")
|
|
477
|
-
.describe("Reasoning mode
|
|
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
|
|
501
|
-
|
|
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.
|
|
517
|
+
style: z.string()
|
|
516
518
|
.optional()
|
|
517
|
-
.describe("Brainstorming style
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
157
|
+
approach: z.string()
|
|
155
158
|
.optional()
|
|
156
159
|
.default("step-by-step")
|
|
157
|
-
.describe("Reasoning approach
|
|
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.
|
|
197
|
+
mode: z.string()
|
|
195
198
|
.optional()
|
|
196
199
|
.default("chat")
|
|
197
|
-
.describe("Interaction mode
|
|
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.
|
|
275
|
+
focus: z.string()
|
|
273
276
|
.optional()
|
|
274
277
|
.default("general")
|
|
275
|
-
.describe("Analysis focus
|
|
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
|
-
|
|
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.
|
|
357
|
+
approach: z.string()
|
|
353
358
|
.optional()
|
|
354
359
|
.default("step-by-step")
|
|
355
|
-
.describe("Reasoning approach
|
|
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
|
-
//
|
|
379
|
-
|
|
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
|
-
|
|
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.
|
|
128
|
+
depth: z.string()
|
|
126
129
|
.optional()
|
|
127
|
-
.describe("Research depth
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
180
|
+
content: `Research synthesizer. Concise summary of key findings.${FORMAT_INSTRUCTION}`
|
|
161
181
|
},
|
|
162
182
|
{
|
|
163
183
|
role: "user",
|
|
164
|
-
content:
|
|
184
|
+
content: `Synthesize these research findings into key insights:\n\n${research}`
|
|
165
185
|
}
|
|
166
186
|
];
|
|
167
|
-
const
|
|
168
|
-
research += `##
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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.
|
|
204
|
+
approach: z.string()
|
|
197
205
|
.optional()
|
|
198
|
-
.describe("Reasoning approach
|
|
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
|
-
|
|
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
|
|
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