UNPKG

@sousaalex1605/bluma-nootebook

Version:
299 lines (298 loc) 11.6 kB
#!/usr/bin/env node // adapted from https://github.com/modelcontextprotocol/servers/blob/main/src/sequentialthinking/index.ts // for use with mcp tools import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { readFileSync } from 'fs'; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; import { SEQUENTIAL_THINKING_TOOL } from './schema.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')); const { name, version } = pkg; // Create MCP server instance with tools capability const server = new Server({ name, version, }, { capabilities: { tools: {}, }, }); class ToolAwareSequentialThinkingServer { getAvailableTools() { return Array.from(this.available_tools.values()); } constructor(options = {}) { this.thought_history = []; this.branches = {}; this.available_tools = new Map(); // Always include the sequential thinking tool const tools = [ SEQUENTIAL_THINKING_TOOL, ...(options.available_tools || []), ]; // Initialize with provided tools tools.forEach((tool) => { if (this.available_tools.has(tool.name)) { console.error(`Warning: Duplicate tool name '${tool.name}' - using first occurrence`); return; } this.available_tools.set(tool.name, tool); }); // console.error( // 'Available tools:', // Array.from(this.available_tools.keys()), // ); } validateThoughtData(input) { const data = input; if (!data.thought || typeof data.thought !== 'string') { throw new Error('Invalid thought: must be a string'); } if (!data.thought_number || typeof data.thought_number !== 'number') { throw new Error('Invalid thought_number: must be a number'); } if (!data.total_thoughts || typeof data.total_thoughts !== 'number') { throw new Error('Invalid total_thoughts: must be a number'); } if (typeof data.next_thought_needed !== 'boolean') { throw new Error('Invalid next_thought_needed: must be a boolean'); } const validated = { thought: data.thought, thought_number: data.thought_number, total_thoughts: data.total_thoughts, next_thought_needed: data.next_thought_needed, is_revision: data.is_revision, revises_thought: data.revises_thought, branch_from_thought: data.branch_from_thought, branch_id: data.branch_id, needs_more_thoughts: data.needs_more_thoughts, }; // Validate recommendation-related fields if present if (data.current_step) { validated.current_step = data.current_step; } if (data.previous_steps) { if (!Array.isArray(data.previous_steps)) { throw new Error('previous_steps must be an array'); } validated.previous_steps = data.previous_steps; } if (data.remaining_tasks) { if (!Array.isArray(data.remaining_tasks)) { throw new Error('remaining_tasks must be an array'); } validated.remaining_tasks = data.remaining_tasks; } return validated; } // private formatRecommendation(step: StepRecommendation): string { // const tools = step.recommended_tools // .map((tool) => { // const alternatives = tool.alternatives?.length // ? ` (alternatives: ${tool.alternatives.join(', ')})` // : ''; // const inputs = tool.suggested_inputs // ? `\n Suggested inputs: ${JSON.stringify(tool.suggested_inputs)}` // : ''; // return ` - ${tool.tool_name} (priority: ${tool.priority})${alternatives} // Rationale: ${tool.rationale}${inputs}`; // }) // .join('\n'); // return `Step: ${step.step_description} // Recommended Tools: // ${tools} // Expected Outcome: ${step.expected_outcome}${ // step.next_step_conditions // ? `\nConditions for next step:\n - ${step.next_step_conditions.join('\n - ')}` // : '' // }`; // } // private formatThought(thoughtData: ThoughtData): string { // const { // thought_number, // total_thoughts, // thought, // is_revision, // revises_thought, // branch_from_thought, // branch_id, // current_step, // } = thoughtData; // let prefix = ''; // let context = ''; // if (is_revision) { // prefix = chalk.yellow('🔄 Revision'); // context = ` (revising thought ${revises_thought})`; // } else if (branch_from_thought) { // prefix = chalk.green('🌿 Branch'); // context = ` (from thought ${branch_from_thought}, ID: ${branch_id})`; // } else { // prefix = chalk.blue('💭 Thought'); // context = ''; // } // const header = `${prefix} ${thought_number}/${total_thoughts}${context}`; // let content = thought; // // Add recommendation information if present // if (current_step) { // content = `${thought}\n\nRecommendation:\n${this.formatRecommendation(current_step)}`; // } // // Word wrap content to max 100 characters per line // const wrappedContent = content.split('\n').map(line => { // const words = line.split(' '); // let currentLine = ''; // let wrappedLines = []; // words.forEach(word => { // if ((currentLine + ' ' + word).length <= 100) { // currentLine += (currentLine ? ' ' : '') + word; // } else { // wrappedLines.push(currentLine); // currentLine = word; // } // }); // if (currentLine) wrappedLines.push(currentLine); // return wrappedLines; // }).flat(); // const maxWidth = Math.min( // Math.max(header.length, ...wrappedContent.map(l => l.length)), // 100 // ) + 4; // const border = '─'.repeat(maxWidth); // const formattedContent = wrappedContent // .map(line => `│ ${line.padEnd(maxWidth - 2)} │`) // .join('\n'); // return ` // ┌${border}┐ // │ ${header.padEnd(maxWidth - 2)} │ // ├${border}┤ // ${formattedContent} // └${border}┘`; // } async processThought(input) { try { const validatedInput = this.validateThoughtData(input); if (validatedInput.thought_number > validatedInput.total_thoughts) { validatedInput.total_thoughts = validatedInput.thought_number; } // Store the current step in thought history if (validatedInput.current_step) { if (!validatedInput.previous_steps) { validatedInput.previous_steps = []; } validatedInput.previous_steps.push(validatedInput.current_step); } this.thought_history.push(validatedInput); if (validatedInput.branch_from_thought && validatedInput.branch_id) { if (!this.branches[validatedInput.branch_id]) { this.branches[validatedInput.branch_id] = []; } this.branches[validatedInput.branch_id].push(validatedInput); } // const formattedThought = this.formatThought(validatedInput); // // console.error(formattedThought); return { content: [ { type: 'text', text: JSON.stringify({ thought: validatedInput.thought, thought_number: validatedInput.thought_number, total_thoughts: validatedInput.total_thoughts, next_thought_needed: validatedInput.next_thought_needed, branches: Object.keys(this.branches), thought_history_length: this.thought_history.length, current_step: validatedInput.current_step, previous_steps: validatedInput.previous_steps, remaining_tasks: validatedInput.remaining_tasks, }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: JSON.stringify({ error: error instanceof Error ? error.message : String(error), status: 'failed', }, null, 2), }, ], isError: true, }; } } async execute_tool(tool, inputs) { try { // Call the tool through the server's request method const response = await server.request({ method: 'callTool', params: { name: tool.name, arguments: inputs, }, }, CallToolRequestSchema); // Extract the result from the response if ('content' in response && Array.isArray(response.content) && response.content.length > 0) { const content = response.content[0]; if ('text' in content && typeof content.text === 'string') { try { // Attempt to parse JSON result return JSON.parse(content.text); } catch { // If not JSON, return as-is return content.text; } } } throw new Error('Tool execution returned no content'); } catch (error) { throw new Error(`Failed to execute tool ${tool.name}: ${error instanceof Error ? error.message : String(error)}`); } } } const thinkingServer = new ToolAwareSequentialThinkingServer({ available_tools: [], }); // Expose all available tools server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: thinkingServer.getAvailableTools(), })); server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === 'nootebook') { return thinkingServer.processThought(request.params.arguments); } return { content: [ { type: 'text', text: `Unknown tool: ${request.params.name}`, }, ], isError: true, }; }); async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); // console.error('Sequential Thinking MCP Server running on stdio'); } runServer().catch((error) => { console.error('Fatal error running server:', error); process.exit(1); });