/**
* MCP Handlers for LearnMCP
* Defines all MCP tools and handles tool calls
*/
import { createLearnLogger } from './utils/custom-logger.js';
export class MCPHandlers {
constructor() {
this.logger = createLearnLogger('MCPHandlers');
}
/**
* Get all tool definitions for MCP
*/
getToolDefinitions() {
return [
{
name: 'add_learning_sources',
description:
'Add learning sources (URLs) to a project for content extraction and summarization',
inputSchema: {
type: 'object',
properties: {
project_id: {
type: 'string',
description: 'Project ID to add sources to',
},
urls: {
type: 'array',
items: { type: 'string' },
description: 'Array of URLs to add (YouTube, PDF, articles)',
},
},
required: ['project_id', 'urls'],
},
},
{
name: 'process_learning_sources',
description: 'Start background processing of pending learning sources for a project',
inputSchema: {
type: 'object',
properties: {
project_id: {
type: 'string',
description: 'Project ID to process sources for',
},
},
required: ['project_id'],
},
},
{
name: 'list_learning_sources',
description: 'List learning sources for a project, optionally filtered by status',
inputSchema: {
type: 'object',
properties: {
project_id: {
type: 'string',
description: 'Project ID to list sources for',
},
status: {
type: 'string',
enum: ['pending', 'processing', 'completed', 'failed'],
description: 'Optional status filter',
},
},
required: ['project_id'],
},
},
{
name: 'get_learning_summary',
description: 'Get learning content summary for a project or specific source',
inputSchema: {
type: 'object',
properties: {
project_id: {
type: 'string',
description: 'Project ID to get summary for',
},
source_id: {
type: 'string',
description:
'Optional specific source ID. If not provided, returns aggregated summary',
},
token_limit: {
type: 'number',
description: 'Maximum tokens for aggregated summary (default: 2000)',
default: 2000,
},
},
required: ['project_id'],
},
},
{
name: 'delete_learning_sources',
description: 'Delete learning sources and their summaries from a project',
inputSchema: {
type: 'object',
properties: {
project_id: {
type: 'string',
description: 'Project ID to delete sources from',
},
source_ids: {
type: 'array',
items: { type: 'string' },
description: 'Array of source IDs to delete',
},
},
required: ['project_id', 'source_ids'],
},
},
{
name: 'get_processing_status',
description: 'Get current processing status for learning sources in a project',
inputSchema: {
type: 'object',
properties: {
project_id: {
type: 'string',
description: 'Project ID to check status for',
},
},
required: ['project_id'],
},
},
];
}
/**
* Handle tool calls
*/
async handleToolCall(toolName, args, learnService) {
try {
this.logger.debug('Handling tool call', { tool: toolName, args });
switch (toolName) {
case 'add_learning_sources':
return await this.handleAddLearningSources(args, learnService);
case 'process_learning_sources':
return await this.handleProcessLearningSources(args, learnService);
case 'list_learning_sources':
return await this.handleListLearningSources(args, learnService);
case 'get_learning_summary':
return await this.handleGetLearningSummary(args, learnService);
case 'delete_learning_sources':
return await this.handleDeleteLearningSources(args, learnService);
case 'get_processing_status':
return await this.handleGetProcessingStatus(args, learnService);
default:
throw new Error(`Unknown tool: ${toolName}`);
}
} catch (error) {
this.logger.error('Tool call failed', {
tool: toolName,
error: error.message,
});
throw error;
}
}
/**
* Handle add_learning_sources tool
*/
async handleAddLearningSources(args, learnService) {
const { project_id, urls } = args;
if (!project_id || !urls || !Array.isArray(urls)) {
throw new Error('Invalid arguments: project_id and urls array required');
}
const result = await learnService.addLearningSources(project_id, urls);
return {
content: [
{
type: 'text',
text:
`**Learning Sources Added**\n\n${result.message}\n\n` +
`**Added Sources:**\n${result.addedSources
.map(s => `- ${s.type.toUpperCase()}: ${s.url}`)
.join('\n')}\n\n${
result.invalidUrls.length > 0
? `**Invalid URLs:**\n${result.invalidUrls
.map(u => `- ${u.url}: ${u.reason}`)
.join('\n')}\n\n`
: ''
}Use \`process_learning_sources\` to start content extraction.`,
},
],
};
}
/**
* Handle process_learning_sources tool
*/
async handleProcessLearningSources(args, learnService) {
const { project_id } = args;
if (!project_id) {
throw new Error('Invalid arguments: project_id required');
}
const result = await learnService.processLearningSources(project_id);
return {
content: [
{
type: 'text',
text:
`**Learning Source Processing Started**\n\n${result.message}\n\n` +
`Processing will run in the background. Use \`get_processing_status\` to check progress.`,
},
],
};
}
/**
* Handle list_learning_sources tool
*/
async handleListLearningSources(args, learnService) {
const { project_id, status } = args;
if (!project_id) {
throw new Error('Invalid arguments: project_id required');
}
const sources = await learnService.listLearningSources(project_id, status);
if (sources.length === 0) {
return {
content: [
{
type: 'text',
text:
`**No Learning Sources Found**\n\n` +
`Project: ${project_id}\n` +
`Status Filter: ${status || 'all'}\n\n` +
`Use \`add_learning_sources\` to add content sources.`,
},
],
};
}
const sourcesList = sources
.map(source => {
const statusEmoji =
{
pending: '⏳',
processing: '🔄',
completed: '✅',
failed: '❌',
}[source.status] || '❓';
return (
`${statusEmoji} **${source.metadata.title || 'Unknown Title'}**\n` +
` Type: ${source.type.toUpperCase()}\n` +
` Status: ${source.status}\n` +
` URL: ${source.url}\n` +
` Added: ${new Date(source.timestamps.added).toLocaleDateString()}`
);
})
.join('\n\n');
return {
content: [
{
type: 'text',
text: `**Learning Sources (${sources.length})**\n\n${sourcesList}`,
},
],
};
}
/**
* Handle get_learning_summary tool
*/
async handleGetLearningSummary(args, learnService) {
const { project_id, source_id, token_limit = 2000 } = args;
if (!project_id) {
throw new Error('Invalid arguments: project_id required');
}
const summary = await learnService.getLearningSummary(project_id, source_id, token_limit);
if (!summary) {
return {
content: [
{
type: 'text',
text:
`**No Learning Content Found**\n\n` +
`Project: ${project_id}\n${
source_id ? `Source: ${source_id}\n` : ''
}\nNo processed learning content available. Add sources and process them first.`,
},
],
};
}
if (source_id) {
// Single source summary
return {
content: [
{
type: 'text',
text:
`**Learning Summary: ${summary.metadata?.title || source_id}**\n\n` +
`**Source:** ${summary.sourceType.toUpperCase()}\n` +
`**URL:** ${summary.sourceUrl}\n` +
`**Method:** ${summary.summaryMethod}\n` +
`**Relevance Score:** ${summary.metadata?.relevanceScore || 'N/A'}/10\n\n` +
`**Summary:**\n${summary.summary}\n\n${
summary.keyPoints?.length > 0
? `**Key Points:**\n${summary.keyPoints.map(p => `• ${p}`).join('\n')}\n\n`
: ''
}${summary.tags?.length > 0 ? `**Tags:** ${summary.tags.join(', ')}` : ''}`,
},
],
};
} else {
// Aggregated summary
return {
content: [
{
type: 'text',
text:
`**Aggregated Learning Summary**\n\n` +
`**Sources Included:** ${summary.sourcesIncluded.length}/${summary.totalSources}\n` +
`**Token Count:** ${summary.tokenCount}/${token_limit}\n${
summary.metadata?.truncated
? `**Note:** Summary truncated due to token limit\n`
: ''
}\n**Content:**\n${summary.aggregatedSummary}`,
},
],
};
}
}
/**
* Handle delete_learning_sources tool
*/
async handleDeleteLearningSources(args, learnService) {
const { project_id, source_ids } = args;
if (!project_id || !source_ids || !Array.isArray(source_ids)) {
throw new Error('Invalid arguments: project_id and source_ids array required');
}
const result = await learnService.deleteLearningSources(project_id, source_ids);
return {
content: [
{
type: 'text',
text:
`**Learning Sources Deleted**\n\n${result.message}\n\n` +
`**Deleted Sources:**\n${result.deletedSources
.map(s => `- ${s.metadata?.title || s.id}: ${s.url}`)
.join('\n')}`,
},
],
};
}
/**
* Handle get_processing_status tool
*/
async handleGetProcessingStatus(args, learnService) {
const { project_id } = args;
if (!project_id) {
throw new Error('Invalid arguments: project_id required');
}
const status = await learnService.getProcessingStatus(project_id);
const statusText = Object.entries(status.sources.statusCounts)
.map(([statusName, count]) => `${statusName}: ${count}`)
.join(', ');
return {
content: [
{
type: 'text',
text:
`**Processing Status**\n\n` +
`**Project:** ${project_id}\n` +
`**Total Sources:** ${status.sources.totalSources}\n` +
`**Status Breakdown:** ${statusText}\n` +
`**Currently Processing:** ${status.sources.isProcessing ? 'Yes' : 'No'}\n\n` +
`**Background Processor:**\n` +
`- Running: ${status.processor.isRunning ? 'Yes' : 'No'}\n` +
`- Queue Size: ${status.processor.queueSize}\n` +
`- Processing Count: ${status.processor.processingCount}\n\n` +
`**Last Updated:** ${new Date(status.lastUpdated).toLocaleString()}`,
},
],
};
}
}
// Export singleton instance
export const mcpHandlers = new MCPHandlers();