/**
* MCP Tool Implementations
*
* Implements all MCP tools for NotebookLM integration:
* - ask_question: Ask NotebookLM with session support
* - list_sessions: List all active sessions
* - close_session: Close a specific session
* - reset_session: Reset session chat history
* - get_health: Server health check
* - setup_auth: Interactive authentication setup
*
* Based on the Python implementation from tools/*.py
*/
import { SessionManager } from '../session/session-manager.js';
import { AuthManager } from '../auth/auth-manager.js';
import { NotebookLibrary } from '../library/notebook-library.js';
import type {
AddNotebookInput,
UpdateNotebookInput,
NotebookEntry,
LibraryStats,
} from '../library/types.js';
import { CONFIG, applyBrowserOptions, type BrowserOptions } from '../config.js';
import { log } from '../utils/logger.js';
import type { AskQuestionResult, ToolResult, Tool, ProgressCallback } from '../types.js';
import { RateLimitError } from '../errors.js';
import { CleanupManager } from '../utils/cleanup-manager.js';
/**
* Build dynamic tool description for ask_question based on active notebook or library
*/
function buildAskQuestionDescription(library: NotebookLibrary): string {
const active = library.getActiveNotebook();
if (active) {
const topics = active.topics.join(', ');
const useCases = active.use_cases.map((uc) => ` - ${uc}`).join('\n');
return `# Conversational Research Partner (NotebookLM • Gemini 2.5 • Session RAG)
**Active Notebook:** ${active.name}
**Content:** ${active.description}
**Topics:** ${topics}
> Auth tip: If login is required, use the prompt 'notebooklm.auth-setup' and then verify with the 'get_health' tool. If authentication later fails (e.g., expired cookies), use the prompt 'notebooklm.auth-repair'.
## What This Tool Is
- Full conversational research with Gemini (LLM) grounded on your notebook sources
- Session-based: each follow-up uses prior context for deeper, more precise answers
- Source-cited responses designed to minimize hallucinations
## When To Use
${useCases}
## Rules (Important)
- Always prefer continuing an existing session for the same task
- If you start a new thread, create a new session and keep its session_id
- Ask clarifying questions before implementing; do not guess missing details
- If multiple notebooks could apply, propose the top 1–2 and ask which to use
- If task context changes, ask to reset the session or switch notebooks
- If authentication fails, use the prompts 'notebooklm.auth-repair' (or 'notebooklm.auth-setup') and verify with 'get_health'
- After every NotebookLM answer: pause, compare with the user's goal, and only respond if you are 100% sure the information is complete. Otherwise, plan the next NotebookLM question in the same session.
## Session Flow (Recommended)
\`\`\`javascript
// 1) Start broad (no session_id → creates one)
ask_question({ question: "Give me an overview of [topic]" })
// ← Save: result.session_id
// 2) Go specific (same session)
ask_question({ question: "Key APIs/methods?", session_id })
// 3) Cover pitfalls (same session)
ask_question({ question: "Common edge cases + gotchas?", session_id })
// 4) Ask for production example (same session)
ask_question({ question: "Show a production-ready example", session_id })
\`\`\`
## Automatic Multi-Pass Strategy (Host-driven)
- Simple prompts return once-and-done answers.
- For complex prompts, the host should issue follow-up calls:
1. Implementation plan (APIs, dependencies, configuration, authentication).
2. Pitfalls, gaps, validation steps, missing prerequisites.
- Keep the same session_id for all follow-ups, review NotebookLM's answer, and ask more questions until the problem is fully resolved.
- Before replying to the user, double-check: do you truly have everything? If not, queue another ask_question immediately.
## 🔥 REAL EXAMPLE
Task: "Implement error handling in n8n workflow"
Bad (shallow):
\`\`\`
Q: "How do I handle errors in n8n?"
A: [basic answer]
→ Implement → Probably missing edge cases!
\`\`\`
Good (deep):
\`\`\`
Q1: "What are n8n's error handling mechanisms?" (session created)
A1: [Overview of error handling]
Q2: "What's the recommended pattern for API errors?" (same session)
A2: [Specific patterns, uses context from Q1]
Q3: "How do I handle retry logic and timeouts?" (same session)
A3: [Detailed approach, builds on Q1+Q2]
Q4: "Show me a production example with all these patterns" (same session)
A4: [Complete example with full context]
→ NOW implement with confidence!
\`\`\`
## Notebook Selection
- Default: active notebook (${active.id})
- Or set notebook_id to use a library notebook
- Or set notebook_url for ad-hoc notebooks (not in library)
- If ambiguous which notebook fits, ASK the user which to use`;
} else {
return `# Conversational Research Partner (NotebookLM • Gemini 2.5 • Session RAG)
## No Active Notebook
- Visit https://notebooklm.google to create a notebook and get a share link
- Use **add_notebook** to add it to your library (explains how to get the link)
- Use **list_notebooks** to show available sources
- Use **select_notebook** to set one active
> Auth tip: If login is required, use the prompt 'notebooklm.auth-setup' and then verify with the 'get_health' tool. If authentication later fails (e.g., expired cookies), use the prompt 'notebooklm.auth-repair'.
Tip: Tell the user you can manage NotebookLM library and ask which notebook to use for the current task.`;
}
}
/**
* Build Tool Definitions with NotebookLibrary context
*/
export function buildToolDefinitions(library: NotebookLibrary): Tool[] {
return [
{
name: 'ask_question',
description: buildAskQuestionDescription(library),
inputSchema: {
type: 'object',
properties: {
question: {
type: 'string',
description: 'The question to ask NotebookLM',
},
session_id: {
type: 'string',
description:
'Optional session ID for contextual conversations. If omitted, a new session is created.',
},
notebook_id: {
type: 'string',
description:
'Optional notebook ID from your library. If omitted, uses the active notebook. ' +
'Use list_notebooks to see available notebooks.',
},
notebook_url: {
type: 'string',
description:
'Optional notebook URL (overrides notebook_id). Use this for ad-hoc queries to notebooks not in your library.',
},
show_browser: {
type: 'boolean',
description:
'Show browser window for debugging (simple version). ' +
'For advanced control (typing speed, stealth, etc.), use browser_options instead.',
},
browser_options: {
type: 'object',
description:
'Optional browser behavior settings. Claude can control everything: ' +
'visibility, typing speed, stealth mode, timeouts. Useful for debugging or fine-tuning.',
properties: {
show: {
type: 'boolean',
description: 'Show browser window (default: from ENV or false)',
},
headless: {
type: 'boolean',
description: 'Run browser in headless mode (default: true)',
},
timeout_ms: {
type: 'number',
description: 'Browser operation timeout in milliseconds (default: 30000)',
},
stealth: {
type: 'object',
description: 'Human-like behavior settings to avoid detection',
properties: {
enabled: {
type: 'boolean',
description: 'Master switch for all stealth features (default: true)',
},
random_delays: {
type: 'boolean',
description: 'Random delays between actions (default: true)',
},
human_typing: {
type: 'boolean',
description: 'Human-like typing patterns (default: true)',
},
mouse_movements: {
type: 'boolean',
description: 'Realistic mouse movements (default: true)',
},
typing_wpm_min: {
type: 'number',
description: 'Minimum typing speed in WPM (default: 160)',
},
typing_wpm_max: {
type: 'number',
description: 'Maximum typing speed in WPM (default: 240)',
},
delay_min_ms: {
type: 'number',
description: 'Minimum delay between actions in ms (default: 100)',
},
delay_max_ms: {
type: 'number',
description: 'Maximum delay between actions in ms (default: 400)',
},
},
},
viewport: {
type: 'object',
description: 'Browser viewport size',
properties: {
width: {
type: 'number',
description: 'Viewport width in pixels (default: 1920)',
},
height: {
type: 'number',
description: 'Viewport height in pixels (default: 1080)',
},
},
},
},
},
},
required: ['question'],
},
},
{
name: 'auto_discover_notebook',
description: `🚀 AUTO-DISCOVERY — Automatically generate notebook metadata via NotebookLM (RECOMMENDED)
## When to Use
- User provides NotebookLM URL and wants quick/automatic setup
- User prefers not to manually specify metadata
- Default choice for adding notebooks
## Workflow
1) User provides NotebookLM URL
2) Ask confirmation: "Add '[URL]' with auto-generated metadata?"
3) Call this tool → NotebookLM generates name, description, tags
4) Show generated metadata to user for review
## Benefits
- ✅ 30 seconds vs 5 minutes manual entry
- ✅ Zero-friction notebook addition
- ✅ Consistent metadata quality
- ✅ Discovers topics user might not think of
## Example
User: "Add this NotebookLM: https://notebooklm.google.com/notebook/abc123"
You: "Add this notebook with auto-generated metadata?"
User: "Yes"
You: Call auto_discover_notebook(url="https://...")
→ Returns: {name: "n8n-workflow-guide", description: "...", tags: [...]}
## Fallback
If auto-discovery fails (rare), use add_notebook tool for manual entry.
## How to Get a NotebookLM Share Link
Visit https://notebooklm.google/ → Login (free: 100 notebooks, 50 sources each, 500k words, 50 daily queries)
1) Click "+ New" (top right) → Upload sources (docs, knowledge)
2) Click "Share" (top right) → Select "Anyone with the link"
3) Click "Copy link" (bottom left) → Give this link to Claude
(Upgraded: Google AI Pro/Ultra gives 5x higher limits)`,
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'The NotebookLM notebook URL',
},
},
required: ['url'],
},
},
{
name: 'add_notebook',
description: `📝 MANUAL ENTRY — Add notebook with manually specified metadata (use auto_discover_notebook instead)
## When to Use
- Auto-discovery failed or unavailable
- User has specific metadata requirements
- User prefers manual control
## Conversation Workflow (Mandatory)
When the user says: "I have a NotebookLM with X"
**FIRST:** Try auto_discover_notebook for faster setup
**ONLY IF** user refuses auto-discovery or it fails:
1) Ask URL: "What is the NotebookLM URL?"
2) Ask content: "What knowledge is inside?" (1–2 sentences)
3) Ask topics: "Which topics does it cover?" (3–5)
4) Ask use cases: "When should we consult it?"
5) Propose metadata and confirm:
- Name: [suggested]
- Description: [from user]
- Topics: [list]
- Use cases: [list]
"Add it to your library now?"
6) Only after explicit "Yes" → call this tool
## Rules
- Do not add without user permission
- Prefer auto_discover_notebook when possible
- Do not guess metadata — ask concisely
- Confirm summary before calling the tool
## Example
User: "I have a notebook with n8n docs"
You: "Want me to auto-generate the metadata?" (offer auto_discover_notebook first)
User: "No, I'll specify it myself"
You: Ask URL → content → topics → use cases; propose summary
User: "Yes"
You: Call add_notebook
## How to Get a NotebookLM Share Link
Visit https://notebooklm.google/ → Login (free: 100 notebooks, 50 sources each, 500k words, 50 daily queries)
1) Click "+ New" (top right) → Upload sources (docs, knowledge)
2) Click "Share" (top right) → Select "Anyone with the link"
3) Click "Copy link" (bottom left) → Give this link to Claude
(Upgraded: Google AI Pro/Ultra gives 5x higher limits)`,
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'The NotebookLM notebook URL',
},
name: {
type: 'string',
description: "Display name for the notebook (e.g., 'n8n Documentation')",
},
description: {
type: 'string',
description: 'What knowledge/content is in this notebook',
},
topics: {
type: 'array',
items: { type: 'string' },
description: 'Topics covered in this notebook',
},
content_types: {
type: 'array',
items: { type: 'string' },
description: "Types of content (e.g., ['documentation', 'examples', 'best practices'])",
},
use_cases: {
type: 'array',
items: { type: 'string' },
description:
"When should Claude use this notebook (e.g., ['Implementing n8n workflows'])",
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Optional tags for organization',
},
},
required: ['url', 'name', 'description', 'topics'],
},
},
{
name: 'list_notebooks',
description:
'List all library notebooks with metadata (name, topics, use cases, URL). ' +
'Use this to present options, then ask which notebook to use for the task.',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'get_notebook',
description: 'Get detailed information about a specific notebook by ID',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'The notebook ID',
},
},
required: ['id'],
},
},
{
name: 'select_notebook',
description: `Set a notebook as the active default (used when ask_question has no notebook_id).
## When To Use
- User switches context: "Let's work on React now"
- User asks explicitly to activate a notebook
- Obvious task change requires another notebook
## Auto-Switching
- Safe to auto-switch if the context is clear and you announce it:
"Switching to React notebook for this task..."
- If ambiguous, ask: "Switch to [notebook] for this task?"
## Example
User: "Now let's build the React frontend"
You: "Switching to React notebook..." (call select_notebook)`,
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'The notebook ID to activate',
},
},
required: ['id'],
},
},
{
name: 'update_notebook',
description: `Update notebook metadata based on user intent.
## Pattern
1) Identify target notebook and fields (topics, description, use_cases, tags, url)
2) Propose the exact change back to the user
3) After explicit confirmation, call this tool
## Examples
- User: "React notebook also covers Next.js 14"
You: "Add 'Next.js 14' to topics for React?"
User: "Yes" → call update_notebook
- User: "Include error handling in n8n description"
You: "Update the n8n description to mention error handling?"
User: "Yes" → call update_notebook
Tip: You may update multiple fields at once if requested.`,
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'The notebook ID to update',
},
name: {
type: 'string',
description: 'New display name',
},
description: {
type: 'string',
description: 'New description',
},
topics: {
type: 'array',
items: { type: 'string' },
description: 'New topics list',
},
content_types: {
type: 'array',
items: { type: 'string' },
description: 'New content types',
},
use_cases: {
type: 'array',
items: { type: 'string' },
description: 'New use cases',
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'New tags',
},
url: {
type: 'string',
description: 'New notebook URL',
},
},
required: ['id'],
},
},
{
name: 'remove_notebook',
description: `Dangerous — requires explicit user confirmation.
## Confirmation Workflow
1) User requests removal ("Remove the React notebook")
2) Look up full name to confirm
3) Ask: "Remove '[notebook_name]' from your library? (Does not delete the actual NotebookLM notebook)"
4) Only on explicit "Yes" → call remove_notebook
Never remove without permission or based on assumptions.
Example:
User: "Delete the old React notebook"
You: "Remove 'React Best Practices' from your library?"
User: "Yes" → call remove_notebook`,
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'The notebook ID to remove',
},
},
required: ['id'],
},
},
{
name: 'search_notebooks',
description:
'Search library by query (name, description, topics, tags). ' +
'Use to propose relevant notebooks for the task and then ask which to use.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query',
},
},
required: ['query'],
},
},
{
name: 'get_library_stats',
description: 'Get statistics about your notebook library (total notebooks, usage, etc.)',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'list_sessions',
description:
'List all active sessions with stats (age, message count, last activity). ' +
'Use to continue the most relevant session instead of starting from scratch.',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'close_session',
description:
'Close a specific session by session ID. Ask before closing if the user might still need it.',
inputSchema: {
type: 'object',
properties: {
session_id: {
type: 'string',
description: 'The session ID to close',
},
},
required: ['session_id'],
},
},
{
name: 'reset_session',
description:
"Reset a session's chat history (keep same session ID). " +
'Use for a clean slate when the task changes; ask the user before resetting.',
inputSchema: {
type: 'object',
properties: {
session_id: {
type: 'string',
description: 'The session ID to reset',
},
},
required: ['session_id'],
},
},
{
name: 'get_health',
description:
'Get server health status including authentication state, active sessions, and configuration. ' +
'Use this to verify the server is ready before starting research workflows.\n\n' +
'If authenticated=false and having persistent issues:\n' +
'Consider running cleanup_data(preserve_library=true) + setup_auth for fresh start with clean browser session.',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'setup_auth',
description:
'Google authentication for NotebookLM access - opens a browser window for manual login to your Google account. ' +
'Returns immediately after opening the browser. You have up to 10 minutes to complete the login. ' +
"Use 'get_health' tool afterwards to verify authentication was saved successfully. " +
'Use this for first-time authentication or when auto-login credentials are not available. ' +
'IMPORTANT: If already authenticated, this tool will skip re-authentication. ' +
"For switching accounts or rate-limit workarounds, use 're_auth' tool instead.\n\n" +
'TROUBLESHOOTING for persistent auth issues:\n' +
'If setup_auth fails or you encounter browser/session issues:\n' +
'1. Ask user to close ALL Chrome/Chromium instances\n' +
'2. Run cleanup_data(confirm=true, preserve_library=true) to clean old data\n' +
'3. Run setup_auth again for fresh start\n' +
'This helps resolve conflicts from old browser sessions and installation data.',
inputSchema: {
type: 'object',
properties: {
show_browser: {
type: 'boolean',
description:
'Show browser window (simple version). Default: true for setup. ' +
'For advanced control, use browser_options instead.',
},
browser_options: {
type: 'object',
description:
'Optional browser settings. Control visibility, timeouts, and stealth behavior.',
properties: {
show: {
type: 'boolean',
description: 'Show browser window (default: true for setup)',
},
headless: {
type: 'boolean',
description: 'Run browser in headless mode (default: false for setup)',
},
timeout_ms: {
type: 'number',
description: 'Browser operation timeout in milliseconds (default: 30000)',
},
},
},
},
},
},
{
name: 'de_auth',
description:
'De-authenticate (logout) - Clears all authentication data for security. ' +
'Use this when:\n' +
'- User wants to log out for security reasons\n' +
'- Removing credentials before shutting down\n' +
'- Clearing auth without immediately re-authenticating\n\n' +
'This will:\n' +
'1. Close all active browser sessions\n' +
'2. Delete all saved authentication data (cookies, Chrome profile)\n' +
'3. Preserve notebook library and other data\n\n' +
'IMPORTANT: After de_auth, the server will need re-authentication via setup_auth or re_auth before making queries.\n\n' +
"Use 'get_health' to verify de-authentication was successful (authenticated: false).",
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 're_auth',
description:
'Switch to a different Google account or re-authenticate. ' +
'Use this when:\n' +
'- NotebookLM rate limit is reached (50 queries/day for free accounts)\n' +
'- You want to switch to a different Google account\n' +
'- Authentication is broken and needs a fresh start\n\n' +
'This will:\n' +
'1. Close all active browser sessions\n' +
'2. Delete all saved authentication data (cookies, Chrome profile)\n' +
'3. Open browser for fresh Google login\n\n' +
"After completion, use 'get_health' to verify authentication.\n\n" +
'TROUBLESHOOTING for persistent auth issues:\n' +
'If re_auth fails repeatedly:\n' +
'1. Ask user to close ALL Chrome/Chromium instances\n' +
'2. Run cleanup_data(confirm=false, preserve_library=true) to preview old files\n' +
'3. Run cleanup_data(confirm=true, preserve_library=true) to clean everything except library\n' +
'4. Run re_auth again for completely fresh start\n' +
'This removes old installation data and browser sessions that can cause conflicts.',
inputSchema: {
type: 'object',
properties: {
show_browser: {
type: 'boolean',
description:
'Show browser window (simple version). Default: true for re-auth. ' +
'For advanced control, use browser_options instead.',
},
browser_options: {
type: 'object',
description:
'Optional browser settings. Control visibility, timeouts, and stealth behavior.',
properties: {
show: {
type: 'boolean',
description: 'Show browser window (default: true for re-auth)',
},
headless: {
type: 'boolean',
description: 'Run browser in headless mode (default: false for re-auth)',
},
timeout_ms: {
type: 'number',
description: 'Browser operation timeout in milliseconds (default: 30000)',
},
},
},
},
},
},
{
name: 'cleanup_data',
description:
'ULTRATHINK Deep Cleanup - Scans entire system for ALL NotebookLM MCP data files across 8 categories. Always runs in deep mode, shows categorized preview before deletion.\n\n' +
'⚠️ CRITICAL: Close ALL Chrome/Chromium instances BEFORE running this tool! Open browsers can prevent cleanup and cause issues.\n\n' +
'Categories scanned:\n' +
'1. Legacy Installation (notebooklm-mcp-nodejs) - Old paths with -nodejs suffix\n' +
'2. Current Installation (notebooklm-mcp) - Active data, browser profiles, library\n' +
'3. NPM/NPX Cache - Cached installations from npx\n' +
'4. Claude CLI MCP Logs - MCP server logs from Claude CLI\n' +
'5. Temporary Backups - Backup directories in system temp\n' +
'6. Claude Projects Cache - Project-specific cache (optional)\n' +
'7. Editor Logs (Cursor/VSCode) - MCP logs from code editors (optional)\n' +
'8. Trash Files - Deleted notebooklm files in system trash (optional)\n\n' +
'Works cross-platform (Linux, Windows, macOS). Safe by design: shows detailed preview before deletion, requires explicit confirmation.\n\n' +
'LIBRARY PRESERVATION: Set preserve_library=true to keep your notebook library.json file while cleaning everything else.\n\n' +
'RECOMMENDED WORKFLOW for fresh start:\n' +
'1. Ask user to close ALL Chrome/Chromium instances\n' +
'2. Run cleanup_data(confirm=false, preserve_library=true) to preview\n' +
'3. Run cleanup_data(confirm=true, preserve_library=true) to execute\n' +
'4. Run setup_auth or re_auth for fresh browser session\n\n' +
'Use cases: Clean reinstall, troubleshooting auth issues, removing all traces before uninstall, cleaning old browser sessions and installation data.',
inputSchema: {
type: 'object',
properties: {
confirm: {
type: 'boolean',
description:
'Confirmation flag. Tool shows preview first, then user confirms deletion. ' +
'Set to true only after user has reviewed the preview and explicitly confirmed.',
},
preserve_library: {
type: 'boolean',
description:
'Preserve library.json file during cleanup. Default: false. ' +
'Set to true to keep your notebook library while deleting everything else (browser data, caches, logs).',
default: false,
},
},
required: ['confirm'],
},
},
];
}
/**
* MCP Tool Handlers
*/
export class ToolHandlers {
private sessionManager: SessionManager;
private authManager: AuthManager;
private library: NotebookLibrary;
constructor(sessionManager: SessionManager, authManager: AuthManager, library: NotebookLibrary) {
this.sessionManager = sessionManager;
this.authManager = authManager;
this.library = library;
}
/**
* Handle ask_question tool
*/
async handleAskQuestion(
args: {
question: string;
session_id?: string;
notebook_id?: string;
notebook_url?: string;
show_browser?: boolean;
browser_options?: BrowserOptions;
},
sendProgress?: ProgressCallback
): Promise<ToolResult<AskQuestionResult>> {
const { question, session_id, notebook_id, notebook_url, show_browser, browser_options } = args;
log.info(`🔧 [TOOL] ask_question called`);
log.info(` Question: "${question.substring(0, 100)}..."`);
if (session_id) {
log.info(` Session ID: ${session_id}`);
}
if (notebook_id) {
log.info(` Notebook ID: ${notebook_id}`);
}
if (notebook_url) {
log.info(` Notebook URL: ${notebook_url}`);
}
try {
// Resolve notebook URL
let resolvedNotebookUrl = notebook_url;
if (!resolvedNotebookUrl && notebook_id) {
const notebook = this.library.incrementUseCount(notebook_id);
if (!notebook) {
const allNotebooks = this.library.listNotebooks();
if (allNotebooks.length === 0) {
throw new Error(
`Notebook not found: '${notebook_id}'\n\n` +
`❌ No notebooks configured in library.\n\n` +
`To add a notebook:\n` +
` POST /notebooks with { url, name, description, topics }\n\n` +
`Or use notebook_url directly in your request:\n` +
` { "question": "...", "notebook_url": "https://notebooklm.google.com/notebook/..." }`
);
} else {
const availableIds = allNotebooks.map((n) => n.id).join(', ');
throw new Error(
`Notebook not found: '${notebook_id}'\n\n` +
`Available notebooks: ${availableIds}\n\n` +
`To list all notebooks: GET /notebooks\n` +
`To add a new notebook: POST /notebooks`
);
}
}
resolvedNotebookUrl = notebook.url;
log.info(` Resolved notebook: ${notebook.name}`);
} else if (!resolvedNotebookUrl) {
const active = this.library.getActiveNotebook();
if (active) {
const notebook = this.library.incrementUseCount(active.id);
if (!notebook) {
throw new Error(`Active notebook not found: ${active.id}`);
}
resolvedNotebookUrl = notebook.url;
log.info(` Using active notebook: ${notebook.name}`);
} else {
// No notebook_url, no notebook_id, and no active notebook
const allNotebooks = this.library.listNotebooks();
if (allNotebooks.length === 0) {
throw new Error(
`❌ No notebook specified and no notebooks configured in library.\n\n` +
`Please either:\n` +
`1. Add a notebook to the library:\n` +
` POST /notebooks with { url, name, description, topics }\n\n` +
`2. Or specify notebook_url in your request:\n` +
` { "question": "...", "notebook_url": "https://notebooklm.google.com/notebook/..." }\n\n` +
`3. Or specify notebook_id from existing notebooks:\n` +
` GET /notebooks to list available notebooks`
);
} else {
const availableIds = allNotebooks.map((n) => `${n.id} (${n.name})`).join('\n - ');
throw new Error(
`❌ No notebook specified.\n\n` +
`Available notebooks:\n - ${availableIds}\n\n` +
`Please specify one of:\n` +
` - notebook_id: "${allNotebooks[0].id}"\n` +
` - notebook_url: "https://notebooklm.google.com/notebook/..."\n\n` +
`Or set an active notebook: PUT /notebooks/${allNotebooks[0].id}/activate`
);
}
}
}
// Progress: Getting or creating session
await sendProgress?.('Getting or creating browser session...', 1, 5);
// Apply browser options temporarily
const originalConfig = { ...CONFIG };
const effectiveConfig = applyBrowserOptions(browser_options, show_browser);
Object.assign(CONFIG, effectiveConfig);
// Calculate overrideHeadless parameter for session manager
// show_browser takes precedence over browser_options.headless
let overrideHeadless: boolean | undefined = undefined;
if (show_browser !== undefined) {
overrideHeadless = show_browser;
} else if (browser_options?.show !== undefined) {
overrideHeadless = browser_options.show;
} else if (browser_options?.headless !== undefined) {
overrideHeadless = !browser_options.headless;
}
try {
// Get or create session (with headless override to handle mode changes)
const session = await this.sessionManager.getOrCreateSession(
session_id,
resolvedNotebookUrl,
overrideHeadless
);
// Progress: Asking question
await sendProgress?.('Asking question to NotebookLM...', 2, 5);
// Ask the question (pass progress callback)
const rawAnswer = await session.ask(question, sendProgress);
// Note: FOLLOW_UP_REMINDER removed for cleaner responses
const answer = rawAnswer.trimEnd();
// Get session info
const sessionInfo = session.getInfo();
const result: AskQuestionResult = {
status: 'success',
question,
answer,
session_id: session.sessionId,
notebook_url: session.notebookUrl,
session_info: {
age_seconds: sessionInfo.age_seconds,
message_count: sessionInfo.message_count,
last_activity: sessionInfo.last_activity,
},
};
// Progress: Complete
await sendProgress?.('Question answered successfully!', 5, 5);
log.success(`✅ [TOOL] ask_question completed successfully`);
return {
success: true,
data: result,
};
} finally {
// Restore original CONFIG
Object.assign(CONFIG, originalConfig);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
// Special handling for rate limit errors
if (error instanceof RateLimitError || errorMessage.toLowerCase().includes('rate limit')) {
log.error(`🚫 [TOOL] Rate limit detected`);
return {
success: false,
error:
'NotebookLM rate limit reached (50 queries/day for free accounts).\n\n' +
'You can:\n' +
"1. Use the 're_auth' tool to login with a different Google account\n" +
'2. Wait until tomorrow for the quota to reset\n' +
'3. Upgrade to Google AI Pro/Ultra for 5x higher limits\n\n' +
`Original error: ${errorMessage}`,
};
}
log.error(`❌ [TOOL] ask_question failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle list_sessions tool
*/
async handleListSessions(): Promise<
ToolResult<{
active_sessions: number;
max_sessions: number;
session_timeout: number;
oldest_session_seconds: number;
total_messages: number;
sessions: Array<{
id: string;
created_at: number;
last_activity: number;
age_seconds: number;
inactive_seconds: number;
message_count: number;
notebook_url: string;
}>;
}>
> {
log.info(`🔧 [TOOL] list_sessions called`);
try {
const stats = this.sessionManager.getStats();
const sessions = this.sessionManager.getAllSessionsInfo();
const result = {
active_sessions: stats.active_sessions,
max_sessions: stats.max_sessions,
session_timeout: stats.session_timeout,
oldest_session_seconds: stats.oldest_session_seconds,
total_messages: stats.total_messages,
sessions: sessions.map((info) => ({
id: info.id,
created_at: info.created_at,
last_activity: info.last_activity,
age_seconds: info.age_seconds,
inactive_seconds: info.inactive_seconds,
message_count: info.message_count,
notebook_url: info.notebook_url,
})),
};
log.success(`✅ [TOOL] list_sessions completed (${result.active_sessions} sessions)`);
return {
success: true,
data: result,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] list_sessions failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle close_session tool
*/
async handleCloseSession(args: {
session_id: string;
}): Promise<ToolResult<{ status: string; message: string; session_id: string }>> {
const { session_id } = args;
log.info(`🔧 [TOOL] close_session called`);
log.info(` Session ID: ${session_id}`);
try {
const closed = await this.sessionManager.closeSession(session_id);
if (closed) {
log.success(`✅ [TOOL] close_session completed`);
return {
success: true,
data: {
status: 'success',
message: `Session ${session_id} closed successfully`,
session_id,
},
};
} else {
log.warning(`⚠️ [TOOL] Session ${session_id} not found`);
return {
success: false,
error: `Session ${session_id} not found`,
};
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] close_session failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle reset_session tool
*/
async handleResetSession(args: {
session_id: string;
}): Promise<ToolResult<{ status: string; message: string; session_id: string }>> {
const { session_id } = args;
log.info(`🔧 [TOOL] reset_session called`);
log.info(` Session ID: ${session_id}`);
try {
const session = this.sessionManager.getSession(session_id);
if (!session) {
log.warning(`⚠️ [TOOL] Session ${session_id} not found`);
return {
success: false,
error: `Session ${session_id} not found`,
};
}
await session.reset();
log.success(`✅ [TOOL] reset_session completed`);
return {
success: true,
data: {
status: 'success',
message: `Session ${session_id} reset successfully`,
session_id,
},
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] reset_session failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle get_health tool
*/
async handleGetHealth(): Promise<
ToolResult<{
status: string;
authenticated: boolean;
notebook_url: string;
active_sessions: number;
max_sessions: number;
session_timeout: number;
total_messages: number;
headless: boolean;
auto_login_enabled: boolean;
stealth_enabled: boolean;
troubleshooting_tip?: string;
}>
> {
log.info(`🔧 [TOOL] get_health called`);
try {
// Check authentication status
const statePath = await this.authManager.getValidStatePath();
const authenticated = statePath !== null;
// Get session stats
const stats = this.sessionManager.getStats();
const result = {
status: 'ok',
authenticated,
notebook_url: CONFIG.notebookUrl || 'not configured',
active_sessions: stats.active_sessions,
max_sessions: stats.max_sessions,
session_timeout: stats.session_timeout,
total_messages: stats.total_messages,
headless: CONFIG.headless,
auto_login_enabled: CONFIG.autoLoginEnabled,
stealth_enabled: CONFIG.stealthEnabled,
// Add troubleshooting tip if not authenticated
...(!authenticated && {
troubleshooting_tip:
'For fresh start with clean browser session: Close all Chrome instances → ' +
'cleanup_data(confirm=true, preserve_library=true) → setup_auth',
}),
};
log.success(`✅ [TOOL] get_health completed`);
return {
success: true,
data: result,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] get_health failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle setup_auth tool
*
* Opens a browser window for manual login with live progress updates.
* The operation waits synchronously for login completion (up to 10 minutes).
*/
async handleSetupAuth(
args: {
show_browser?: boolean;
browser_options?: BrowserOptions;
},
sendProgress?: ProgressCallback
): Promise<
ToolResult<{
status: string;
message: string;
authenticated: boolean;
duration_seconds?: number;
}>
> {
const { show_browser, browser_options } = args;
// CRITICAL: Send immediate progress to reset timeout from the very start
await sendProgress?.('Initializing authentication setup...', 0, 10);
log.info(`🔧 [TOOL] setup_auth called`);
if (show_browser !== undefined) {
log.info(` Show browser: ${show_browser}`);
}
const startTime = Date.now();
// Apply browser options temporarily
const originalConfig = { ...CONFIG };
const effectiveConfig = applyBrowserOptions(browser_options, show_browser);
Object.assign(CONFIG, effectiveConfig);
try {
// Progress: Starting
await sendProgress?.('Preparing authentication browser...', 1, 10);
log.info(` 🌐 Opening browser for interactive login...`);
// Progress: Opening browser
await sendProgress?.('Opening browser window...', 2, 10);
// Perform setup with progress updates (uses CONFIG internally)
const success = await this.authManager.performSetup(sendProgress);
const durationSeconds = (Date.now() - startTime) / 1000;
if (success) {
// Progress: Complete
await sendProgress?.('Authentication saved successfully!', 10, 10);
log.success(`✅ [TOOL] setup_auth completed (${durationSeconds.toFixed(1)}s)`);
return {
success: true,
data: {
status: 'authenticated',
message: 'Successfully authenticated and saved browser state',
authenticated: true,
duration_seconds: durationSeconds,
},
};
} else {
log.error(`❌ [TOOL] setup_auth failed (${durationSeconds.toFixed(1)}s)`);
return {
success: false,
error: 'Authentication failed or was cancelled',
};
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const durationSeconds = (Date.now() - startTime) / 1000;
log.error(`❌ [TOOL] setup_auth failed: ${errorMessage} (${durationSeconds.toFixed(1)}s)`);
return {
success: false,
error: errorMessage,
};
} finally {
// Restore original CONFIG
Object.assign(CONFIG, originalConfig);
}
}
/**
* Handle de_auth tool
*
* De-authenticates (logout) by clearing all authentication data.
* This preserves the notebook library but removes all credentials.
*
* Steps:
* 1. Closes all active browser sessions
* 2. Deletes all saved authentication data (cookies, Chrome profile)
*
* Use for security logout or clearing credentials without re-authenticating.
*/
async handleDeAuth(): Promise<
ToolResult<{
status: string;
message: string;
authenticated: boolean;
}>
> {
log.info(`🔧 [TOOL] de_auth called`);
try {
// 1. Close all active sessions
log.info(' 🛑 Closing all sessions...');
await this.sessionManager.closeAllSessions();
log.success(' ✅ All sessions closed');
// 2. Clear all auth data
log.info(' 🗑️ Clearing all authentication data...');
await this.authManager.clearAllAuthData();
log.success(' ✅ Authentication data cleared');
log.success(`✅ [TOOL] de_auth completed - Successfully logged out`);
return {
success: true,
data: {
status: 'de-authenticated',
message: 'Successfully logged out. Use setup_auth or re_auth to authenticate again.',
authenticated: false,
},
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] de_auth failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle re_auth tool
*
* Performs a complete re-authentication:
* 1. De-authenticates (calls de_auth internally)
* 2. Opens browser for fresh Google login
*
* Use for switching Google accounts or recovering from rate limits.
*/
async handleReAuth(
args: {
show_browser?: boolean;
browser_options?: BrowserOptions;
},
sendProgress?: ProgressCallback
): Promise<
ToolResult<{
status: string;
message: string;
authenticated: boolean;
duration_seconds?: number;
}>
> {
const { show_browser, browser_options } = args;
await sendProgress?.('Preparing re-authentication...', 0, 12);
log.info(`🔧 [TOOL] re_auth called`);
if (show_browser !== undefined) {
log.info(` Show browser: ${show_browser}`);
}
const startTime = Date.now();
// Apply browser options temporarily
const originalConfig = { ...CONFIG };
const effectiveConfig = applyBrowserOptions(browser_options, show_browser);
Object.assign(CONFIG, effectiveConfig);
try {
// 1. De-authenticate first (logout)
await sendProgress?.('De-authenticating...', 1, 12);
log.info(' 🔓 De-authenticating (logout)...');
const deAuthResult = await this.handleDeAuth();
if (!deAuthResult.success) {
throw new Error(`De-authentication failed: ${deAuthResult.error}`);
}
log.success(' ✅ De-authentication complete');
// 2. Perform fresh setup
await sendProgress?.('Starting fresh authentication...', 3, 12);
log.info(' 🌐 Starting fresh authentication setup...');
const success = await this.authManager.performSetup(sendProgress);
const durationSeconds = (Date.now() - startTime) / 1000;
if (success) {
await sendProgress?.('Re-authentication complete!', 12, 12);
log.success(`✅ [TOOL] re_auth completed (${durationSeconds.toFixed(1)}s)`);
return {
success: true,
data: {
status: 'authenticated',
message:
'Successfully re-authenticated with new account. All previous sessions have been closed.',
authenticated: true,
duration_seconds: durationSeconds,
},
};
} else {
log.error(`❌ [TOOL] re_auth failed (${durationSeconds.toFixed(1)}s)`);
return {
success: false,
error: 'Re-authentication failed or was cancelled',
};
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const durationSeconds = (Date.now() - startTime) / 1000;
log.error(`❌ [TOOL] re_auth failed: ${errorMessage} (${durationSeconds.toFixed(1)}s)`);
return {
success: false,
error: errorMessage,
};
} finally {
// Restore original CONFIG
Object.assign(CONFIG, originalConfig);
}
}
/**
* Handle auto_discover_notebook tool
*/
async handleAutoDiscoverNotebook(args: {
url: string;
}): Promise<ToolResult<{ notebook: NotebookEntry }>> {
log.info(`🔧 [TOOL] auto_discover_notebook called`);
log.info(` URL: ${args.url}`);
try {
// Import auto-discovery module
const { AutoDiscovery } = await import('../auto-discovery/auto-discovery.js');
// Create AutoDiscovery instance and discover metadata
log.info(` 🤖 Querying NotebookLM for auto-generated metadata...`);
const autoDiscovery = new AutoDiscovery(this.sessionManager);
const metadata = await autoDiscovery.discoverMetadata(args.url);
// Prepare notebook input
const notebookInput = {
url: args.url,
name: metadata.name,
description: metadata.description,
topics: metadata.tags, // tags → topics
content_types: ['documentation'],
use_cases: metadata.tags.slice(0, 3), // Use first 3 tags as use cases
auto_generated: true,
};
// Add notebook to library
const notebook = await this.library.addNotebook(notebookInput);
log.success(`✅ [TOOL] auto_discover_notebook completed: ${notebook.id}`);
log.info(` Generated metadata:`);
log.info(` Name: ${metadata.name}`);
log.info(` Description: ${metadata.description.substring(0, 100)}...`);
log.info(` Tags: ${metadata.tags.join(', ')}`);
return {
success: true,
data: { notebook },
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] auto_discover_notebook failed: ${errorMessage}`);
return {
success: false,
error: `Auto-discovery failed: ${errorMessage}. Try using add_notebook for manual entry instead.`,
};
}
}
/**
* Handle add_notebook tool
*/
async handleAddNotebook(
args: AddNotebookInput
): Promise<ToolResult<{ notebook: NotebookEntry }>> {
log.info(`🔧 [TOOL] add_notebook called`);
log.info(` Name: ${args.name}`);
try {
const notebook = await this.library.addNotebook(args);
log.success(`✅ [TOOL] add_notebook completed: ${notebook.id}`);
return {
success: true,
data: { notebook },
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] add_notebook failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle list_notebooks tool
*/
async handleListNotebooks(): Promise<
ToolResult<{ notebooks: NotebookEntry[]; active_notebook_id: string | null }>
> {
log.info(`🔧 [TOOL] list_notebooks called`);
try {
const notebooks = this.library.listNotebooks();
const activeNotebook = this.library.getActiveNotebook();
const active_notebook_id = activeNotebook ? activeNotebook.id : null;
log.success(
`✅ [TOOL] list_notebooks completed (${notebooks.length} notebooks, active: ${active_notebook_id || 'none'})`
);
return {
success: true,
data: {
notebooks,
active_notebook_id,
},
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] list_notebooks failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle get_notebook tool
*/
async handleGetNotebook(args: { id: string }): Promise<ToolResult<{ notebook: NotebookEntry }>> {
log.info(`🔧 [TOOL] get_notebook called`);
log.info(` ID: ${args.id}`);
try {
const notebook = this.library.getNotebook(args.id);
if (!notebook) {
log.warning(`⚠️ [TOOL] Notebook not found: ${args.id}`);
return {
success: false,
error: `Notebook not found: ${args.id}`,
};
}
log.success(`✅ [TOOL] get_notebook completed: ${notebook.name}`);
return {
success: true,
data: { notebook },
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] get_notebook failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle select_notebook tool
*/
async handleSelectNotebook(args: {
id: string;
}): Promise<ToolResult<{ notebook: NotebookEntry }>> {
log.info(`🔧 [TOOL] select_notebook called`);
log.info(` ID: ${args.id}`);
try {
const notebook = this.library.selectNotebook(args.id);
log.success(`✅ [TOOL] select_notebook completed: ${notebook.name}`);
return {
success: true,
data: { notebook },
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] select_notebook failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle update_notebook tool
*/
async handleUpdateNotebook(
args: UpdateNotebookInput
): Promise<ToolResult<{ notebook: NotebookEntry }>> {
log.info(`🔧 [TOOL] update_notebook called`);
log.info(` ID: ${args.id}`);
try {
const notebook = this.library.updateNotebook(args);
log.success(`✅ [TOOL] update_notebook completed: ${notebook.name}`);
return {
success: true,
data: { notebook },
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] update_notebook failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle remove_notebook tool
*/
async handleRemoveNotebook(args: {
id: string;
}): Promise<ToolResult<{ removed: boolean; closed_sessions: number }>> {
log.info(`🔧 [TOOL] remove_notebook called`);
log.info(` ID: ${args.id}`);
try {
const notebook = this.library.getNotebook(args.id);
if (!notebook) {
log.warning(`⚠️ [TOOL] Notebook not found: ${args.id}`);
return {
success: false,
error: `Notebook not found: ${args.id}`,
};
}
const removed = this.library.removeNotebook(args.id);
if (removed) {
const closedSessions = await this.sessionManager.closeSessionsForNotebook(notebook.url);
log.success(`✅ [TOOL] remove_notebook completed`);
return {
success: true,
data: { removed: true, closed_sessions: closedSessions },
};
} else {
log.warning(`⚠️ [TOOL] Notebook not found: ${args.id}`);
return {
success: false,
error: `Notebook not found: ${args.id}`,
};
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] remove_notebook failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle search_notebooks tool
*/
async handleSearchNotebooks(args: {
query: string;
}): Promise<ToolResult<{ notebooks: NotebookEntry[] }>> {
log.info(`🔧 [TOOL] search_notebooks called`);
log.info(` Query: "${args.query}"`);
try {
const notebooks = this.library.searchNotebooks(args.query);
log.success(`✅ [TOOL] search_notebooks completed (${notebooks.length} results)`);
return {
success: true,
data: { notebooks },
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] search_notebooks failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle get_library_stats tool
*/
async handleGetLibraryStats(): Promise<ToolResult<LibraryStats>> {
log.info(`🔧 [TOOL] get_library_stats called`);
try {
const stats = this.library.getStats();
log.success(`✅ [TOOL] get_library_stats completed`);
return {
success: true,
data: stats,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] get_library_stats failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Handle cleanup_data tool
*
* ULTRATHINK Deep Cleanup - scans entire system for ALL NotebookLM MCP files
*/
async handleCleanupData(args: { confirm: boolean; preserve_library?: boolean }): Promise<
ToolResult<{
status: string;
mode: string;
preview?: {
categories: Array<{
name: string;
description: string;
paths: string[];
totalBytes: number;
optional: boolean;
}>;
totalPaths: number;
totalSizeBytes: number;
};
result?: {
deletedPaths: string[];
failedPaths: string[];
totalSizeBytes: number;
categorySummary: Record<string, { count: number; bytes: number }>;
};
}>
> {
const { confirm, preserve_library = false } = args;
log.info(`🔧 [TOOL] cleanup_data called`);
log.info(` Confirm: ${confirm}`);
log.info(` Preserve Library: ${preserve_library}`);
const cleanupManager = new CleanupManager();
try {
// Always run in deep mode
const mode = 'deep';
if (!confirm) {
// Preview mode - show what would be deleted
log.info(` 📋 Generating cleanup preview (mode: ${mode})...`);
const preview = await cleanupManager.getCleanupPaths(mode, preserve_library);
const platformInfo = cleanupManager.getPlatformInfo();
log.info(
` Found ${preview.totalPaths.length} items (${cleanupManager.formatBytes(preview.totalSizeBytes)})`
);
log.info(` Platform: ${platformInfo.platform}`);
return {
success: true,
data: {
status: 'preview',
mode,
preview: {
categories: preview.categories,
totalPaths: preview.totalPaths.length,
totalSizeBytes: preview.totalSizeBytes,
},
},
};
} else {
// Cleanup mode - actually delete files
log.info(` 🗑️ Performing cleanup (mode: ${mode})...`);
const result = await cleanupManager.performCleanup(mode, preserve_library);
if (result.success) {
log.success(
`✅ [TOOL] cleanup_data completed - deleted ${result.deletedPaths.length} items`
);
} else {
log.warning(`⚠️ [TOOL] cleanup_data completed with ${result.failedPaths.length} errors`);
}
return {
success: result.success,
data: {
status: result.success ? 'completed' : 'partial',
mode,
result: {
deletedPaths: result.deletedPaths,
failedPaths: result.failedPaths,
totalSizeBytes: result.totalSizeBytes,
categorySummary: result.categorySummary,
},
},
};
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`❌ [TOOL] cleanup_data failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Cleanup all resources (called on server shutdown)
*/
async cleanup(): Promise<void> {
log.info(`🧹 Cleaning up tool handlers...`);
await this.sessionManager.closeAllSessions();
log.success(`✅ Tool handlers cleanup complete`);
}
}