// installToolsFeature.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { activeSessions } from "./hub.js";
import { installYouTubeTools } from "./custom_tools/youtube.js";
import { consolidateWorkflowEntry, maintainWorkflowHistorySize, type MemoryConsolidationContext } from "./memoryUtils.js";
/**
* Register local demo tools on the given server.
* Call installToolsFeature(hub) once during bootstrap.
*/
export function installToolsFeature(server: McpServer) {
// Install YouTube tools
installYouTubeTools(server);
/* ---- final_response ----------------------------------------- */
server.tool(
"final_response",
{ // ← ZodRawShape, NOT JSON-schema
text: z.string()
},
{ // annotations (optional)
description:
`The context of the tool results returned as a readable response.
Insightful, conversational tone. Format ar markdown.
Work in sections for example:
## Heading
### Subheading
{context in proper markdown for images, links, code blocks, etc.}
Conclude with a summary or call to action.
End with a friendly sign-off or next steps suggestion.
Markdown Examples:
- use \n\n for new paragraphs.
- use \n for line breaks.
- Use **bold** for emphasis.
- Use _italics_ for subtle emphasis.
- Use \`inline code\` for code snippets.
- Use triple backticks for code blocks:
\`\`\`javascript
console.log("Hello, world!");
\`\`\`
- Use bullet points for lists:
- Item 1
- Item 2
- Use numbered lists for sequences:
1. First step
2. Second step
- Use [links](https://example.com) to reference resources.
- Use > for blockquotes.
- Use headings (#, ##, ###) to structure content.
- Include images with .
- Ensure proper spacing and line breaks for readability.
`
},
async ({ text }) => ({
content: [{ type: "text", text }], // ToolResult
isError: false
})
);
/* ---- update_session_planner ---------------------------------- */
server.tool(
"update_session_planner",
{
cycles: z.array(z.object({
name: z.string().describe("Name of the cycle phase"),
steps: z.array(z.object({
type: z.enum(["tool", "resource"]).describe("Type of step"),
target: z.string().describe("Tool name or resource URI to execute."),
arguments: z.record(z.any()).optional().describe("Arguments for the tool (if type is tool)"),
description: z.string().describe("Description of what this step does. NOTE: Always reference conversation context, ie: If user asks to search for images that match my name, and say their name was John, your description would be \"images of John\".")
}))
})).describe("Array of execution cycles with steps"),
userQuery: z.string().optional().describe("The original user query that triggered this plan"),
currentCycle: z.number().optional().describe("Current cycle index (0-based)"),
currentStep: z.number().optional().describe("Current step index within the cycle (0-based)"),
status: z.enum(["ready", "executing", "completed", "idle"]).optional().describe("Current planner status")
},
{
description: "Update the session planner with new cycles and execution plan"
},
async ({ cycles, userQuery, currentCycle, currentStep, status }, extra) => {
try {
// Get session ID from request context - try multiple ways
const sessionId = (extra as any)?.sessionId ||
(extra as any)?.extra?.sessionId;
if (!sessionId) {
// Fallback: try to find any active session (for demo purposes)
const activeSessions_array = Array.from(activeSessions.keys());
const fallbackSessionId = activeSessions_array[activeSessions_array.length - 1];
if (!fallbackSessionId) {
return {
content: [
{
type: "text",
text: "Error: No session ID available for planner update",
},
],
isError: true,
};
}
console.error(`⚠️ Using fallback session ID: ${fallbackSessionId}`);
const session = activeSessions.get(fallbackSessionId);
if (session) {
// Update session planner state
const updatedPlannerState = {
cycles: cycles || [],
currentCycle: currentCycle ?? 0,
currentStep: currentStep ?? 0,
status: status ?? "ready",
lastUpdated: new Date().toISOString(),
sessionId: fallbackSessionId,
userQuery: userQuery || "Unknown query",
previousPlan: session.plannerState ? {
cycles: session.plannerState.cycles,
completedAt: new Date().toISOString()
} : null
};
session.plannerState = updatedPlannerState;
console.error(`🎯 Updated planner for session ${fallbackSessionId} with ${cycles?.length || 0} cycles`);
return {
content: [
{
type: "text",
text: `Successfully updated session planner with ${cycles?.length || 0} cycles. Plan is ready for execution.\n\nCycles:\n${cycles?.map((cycle, i) => `${i + 1}. ${cycle.name} (${cycle.steps?.length || 0} steps)`).join('\n') || 'No cycles'}`,
},
],
isError: false,
};
}
}
// Get current session data
const session = activeSessions.get(sessionId);
if (!session) {
return {
content: [
{
type: "text",
text: `Error: Session ${sessionId} not found`,
},
],
isError: true,
};
}
// Update session planner state
const updatedPlannerState = {
cycles: cycles || [],
currentCycle: currentCycle ?? 0,
currentStep: currentStep ?? 0,
status: status ?? "ready",
lastUpdated: new Date().toISOString(),
sessionId: sessionId,
userQuery: userQuery || "Unknown query",
previousPlan: session.plannerState ? {
cycles: session.plannerState.cycles,
completedAt: new Date().toISOString()
} : null
};
// Update the session's planner state
session.plannerState = updatedPlannerState;
console.error(`🎯 Updated planner for session ${sessionId} with ${cycles?.length || 0} cycles`);
return {
content: [
{
type: "text",
text: `Successfully updated session planner with ${cycles?.length || 0} cycles. Plan is ready for execution.\n\nCycles:\n${cycles?.map((cycle, i) => `${i + 1}. ${cycle.name} (${cycle.steps?.length || 0} steps)`).join('\n') || 'No cycles'}`,
},
],
isError: false,
};
} catch (error) {
console.error("Error updating session planner:", error);
return {
content: [
{
type: "text",
text: `Error updating session planner: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
isError: true,
};
}
}
);
/* ---- update_session_memory ----------------------------------- */
server.tool(
"update_session_memory",
{
type: z.enum(["user_message", "assistant_message", "tool_execution", "insight", "preference"]).describe("Type of memory entry"),
content: z.string().describe("Content to add to memory"),
timestamp: z.number().optional().describe("Timestamp for the entry (defaults to current time)"),
metadata: z.record(z.any()).optional().describe("Additional metadata for the memory entry")
},
{
description: "Update session memory with conversation context, insights, or user preferences"
},
async ({ type, content, timestamp, metadata }, extra) => {
try {
const sessionId = extra?.sessionId;
console.error(`🧠 Updating memory for session ${sessionId} with ${type}: ${content.substring(0, 100)}...`);
if (!sessionId) {
return {
content: [{ type: "text", text: "No session ID" }],
isError: true,
};
}
const session = activeSessions.get(sessionId);
if (!session) {
return {
content: [{ type: "text", text: `Session not found` }],
isError: true,
};
}
// Initialize memory if it doesn't exist
if (!session.memory) {
session.memory = {
conversationThread: [],
conversationContext: { buffer: [], maxSize: 100, lastUpdated: Date.now() },
insights: { userPreferences: {}, patterns: [], keyTopics: [], workflowHistory: [] },
currentSession: {
startTime: session.startTime,
chronologicalUseOfTools: [], // Clean chronological sequence of unique tool usage
currentFocus: null,
sessionGoals: [],
completedTasks: session.successfulTasks || 0
},
metadata: { version: "1.0.0", lastWrite: Date.now(), totalReads: 0, totalWrites: 0, compressionRatio: 1.0, sessionId }
};
}
// Ensure sessionGoals exists even in existing memory
if (!session.memory.currentSession.sessionGoals) {
session.memory.currentSession.sessionGoals = [];
}
// Ensure completedTasks is properly initialized
if (session.memory.currentSession.completedTasks === undefined) {
session.memory.currentSession.completedTasks = session.successfulTasks || 0;
}
// Ensure conversationThread exists even in existing memory
if (!session.memory.conversationThread) {
session.memory.conversationThread = [];
}
const now = timestamp || Date.now();
// Add entry to conversation buffer
const memoryEntry = {
timestamp: now,
type,
content,
metadata: metadata || {}
};
session.memory.conversationContext.buffer.push(memoryEntry);
// Maintain ring buffer size
if (session.memory.conversationContext.buffer.length > session.memory.conversationContext.maxSize) {
session.memory.conversationContext.buffer = session.memory.conversationContext.buffer.slice(-session.memory.conversationContext.maxSize);
}
// Update clean conversation thread for LLM payloads
if (type === "user_message" || type === "assistant_message") {
const role = type === "user_message" ? "user" : "assistant";
// Check for duplicates within the last second
const existingMessage = session.memory.conversationThread.find(
(msg: any) => msg.content === content && Math.abs(msg.timestamp - now) < 1000
);
if (!existingMessage) {
session.memory.conversationThread.push({
role: role as "user" | "assistant",
content,
timestamp: now
});
// Keep conversation thread clean (last 40 messages = 20 exchanges)
if (session.memory.conversationThread.length > 40) {
session.memory.conversationThread = session.memory.conversationThread.slice(-40);
}
}
}
// Extract insights based on type
if (type === "user_message" || type === "assistant_message") {
// Extract potential session goals from user messages
if (type === "user_message" && metadata?.sessionGoal) {
// Add to session goals if not already present
const goalExists = session.memory.currentSession.sessionGoals.find(
(goal: string) => goal.toLowerCase().includes(content.substring(0, 50).toLowerCase())
);
if (!goalExists && session.memory.currentSession.sessionGoals.length < 10) {
session.memory.currentSession.sessionGoals.push(content.substring(0, 100));
console.error(`🎯 Added session goal: ${content.substring(0, 50)}...`);
}
}
// Increment completed tasks for successful workflow completion
if (type === "assistant_message" && metadata?.workflowCompleted === true) {
session.memory.currentSession.completedTasks = (session.memory.currentSession.completedTasks || 0) + 1;
session.successfulTasks = session.memory.currentSession.completedTasks; // Keep session in sync
console.error(`✅ Incremented completed tasks to: ${session.memory.currentSession.completedTasks}`);
}
// Extract key topics from conversation
const words = content.toLowerCase().split(/\s+/).filter(w => w.length > 3);
words.forEach(word => {
let existingTopic = session.memory.insights.keyTopics.find((t: any) => t.topic === word);
if (existingTopic) {
existingTopic.mentions++;
existingTopic.relevance = Math.min(existingTopic.relevance + 0.1, 1.0);
} else if (words.length < 20) { // Only add if not too chatty
session.memory.insights.keyTopics.push({
topic: word,
relevance: 0.1,
mentions: 1
});
}
});
// Keep top 50 topics
session.memory.insights.keyTopics = session.memory.insights.keyTopics
.sort((a: any, b: any) => b.relevance - a.relevance)
.slice(0, 50);
}
// Track tool execution results for accurate success tracking
if (type === "insight" && metadata?.stepSuccess !== undefined) {
// Update conversation buffer with accurate success tracking
const bufferEntry = session.memory.conversationContext.buffer.find(
(entry: any) => entry.metadata?.step === metadata.step &&
Math.abs(entry.timestamp - now) < 5000 // Within 5 seconds
);
if (bufferEntry) {
bufferEntry.metadata.actualSuccess = metadata.stepSuccess;
console.error(`🔄 Updated buffer entry success tracking: ${metadata.step} = ${metadata.stepSuccess}`);
}
}
// Update metadata
session.memory.metadata.lastWrite = now;
session.memory.metadata.totalWrites++;
session.memory.conversationContext.lastUpdated = now;
// Track memory updates using enhanced consolidation logic with duplicate prevention
const consolidationContext: MemoryConsolidationContext = {
toolName: "update_session_memory",
success: true,
timestamp: now,
arguments: { type, content }
};
consolidateWorkflowEntry(session.memory.insights, consolidationContext);
// Keep workflowHistory lean by limiting size (but preserve the consolidated entry)
maintainWorkflowHistorySize(session.memory.insights, 50);
// Calculate confidence based on success rate
const calculateToolConfidence = (toolName: string, workflowHistory: any[]) => {
const toolExecutions = workflowHistory.filter(w => w.workflow === toolName);
if (toolExecutions.length === 0) return 0;
const successCount = toolExecutions.filter(w => w.success).length;
return successCount / toolExecutions.length; // This gives actual success rate
};
// Update pattern confidence to reflect success rate
const toolSuccessRate = calculateToolConfidence("update_session_memory", session.memory.insights.workflowHistory);
let existingPattern = session.memory.insights.patterns.find((p: any) => p.pattern === `frequent_tool:update_session_memory`);
if (existingPattern) {
existingPattern.confidence = toolSuccessRate; // Now confidence = success rate
existingPattern.lastSeen = now;
} else {
// Create new pattern if it doesn't exist
session.memory.insights.patterns.push({
pattern: "frequent_tool:update_session_memory",
confidence: toolSuccessRate,
firstSeen: now,
lastSeen: now
});
}
return {
content: [
{
type: "text",
text: `Memory updated (${type})`,
},
],
isError: false,
};
} catch (error) {
console.error("Error updating session memory:", error);
return {
content: [
{
type: "text",
text: `Memory update error: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
isError: true,
};
}
}
);
}