UNPKG

perspective-mcp

Version:

MCP server for multi-perspective analysis and structured debates

508 lines 21.9 kB
#!/usr/bin/env node "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const types_js_1 = require("@modelcontextprotocol/sdk/types.js"); class PerspectiveAnalysisServer { perspectives = []; currentPerspectiveIndex = 0; analyses = new Map(); debateState = { active: false, topic: '', participants: [], currentRound: 0, currentTurnIndex: 0, history: [], constraints: [] }; constructor() { this.resetState(); } resetState() { this.perspectives = []; this.currentPerspectiveIndex = 0; this.analyses.clear(); this.resetDebateState(); } resetDebateState() { this.debateState = { active: false, topic: '', participants: [], currentRound: 0, currentTurnIndex: 0, history: [], constraints: [] }; } setPerspectives(perspectives) { this.resetState(); this.perspectives = perspectives; } getCurrentPerspective() { if (this.currentPerspectiveIndex >= this.perspectives.length) { return null; } return this.perspectives[this.currentPerspectiveIndex]; } addAnalysis(analysis, moveToNext) { const currentPerspective = this.getCurrentPerspective(); if (currentPerspective) { this.analyses.set(currentPerspective.role, analysis); } if (moveToNext) { this.currentPerspectiveIndex++; } const nextPerspectiveNeeded = this.currentPerspectiveIndex < this.perspectives.length; return { analysis, nextPerspectiveNeeded }; } getContext() { const previousAnalyses = Array.from(this.analyses.entries()).map(([role, analysis]) => ({ role, analysis })); return { currentPerspective: this.getCurrentPerspective(), previousAnalyses, perspectivesRemaining: Math.max(0, this.perspectives.length - this.currentPerspectiveIndex) }; } // Debate mode methods startDebate(topic, participantRoles) { if (participantRoles.length < 2 || participantRoles.length > 4) { throw new Error("Debate requires 2-4 participants"); } const participants = participantRoles .map(role => this.perspectives.find(p => p.role === role)) .filter(p => p !== undefined); if (participants.length !== participantRoles.length) { throw new Error("One or more specified personas not found in perspectives"); } this.resetDebateState(); this.debateState = { active: true, topic, participants, currentRound: 1, currentTurnIndex: 0, history: [], constraints: [] }; } getCurrentDebateParticipant() { if (!this.debateState.active) return null; const index = this.debateState.currentTurnIndex % this.debateState.participants.length; return this.debateState.participants[index]; } addDebateTurn(statement, moveToNext) { if (!this.debateState.active) { throw new Error("No active debate. Use start_debate first."); } const currentParticipant = this.getCurrentDebateParticipant(); if (!currentParticipant) { throw new Error("No current participant found"); } // Determine who this participant is responding to in rounds 2+ let respondingTo; if (this.debateState.currentRound > 1) { const previousTurns = this.debateState.history.filter(turn => turn.round === this.debateState.currentRound - 1); if (previousTurns.length > 0) { // Find conflicting viewpoint from previous round respondingTo = previousTurns.find(turn => turn.persona !== currentParticipant.role)?.persona; } } const turn = { round: this.debateState.currentRound, persona: currentParticipant.role, statement, respondingTo, timestamp: new Date() }; this.debateState.history.push(turn); let isRoundComplete = false; let nextParticipantNeeded = true; if (moveToNext) { this.debateState.currentTurnIndex++; // Check if round is complete const turnsInRound = this.debateState.currentTurnIndex; const participantsCount = this.debateState.participants.length; if (turnsInRound % participantsCount === 0) { isRoundComplete = true; // Move to next round if not at round 3 if (this.debateState.currentRound < 3) { this.debateState.currentRound++; } else { // Debate is complete after round 3 nextParticipantNeeded = false; } } } const nextParticipant = this.getCurrentDebateParticipant(); return { round: this.debateState.currentRound, participant: currentParticipant.role, nextParticipantNeeded: nextParticipantNeeded && this.debateState.currentRound <= 3, isRoundComplete }; } addConstraint(constraint) { if (!this.debateState.active) { throw new Error("No active debate to add constraints to"); } this.debateState.constraints.push(constraint); } getDebateContext() { const previousRoundStatements = this.debateState.currentRound > 1 ? this.debateState.history.filter(turn => turn.round === this.debateState.currentRound - 1) : []; return { active: this.debateState.active, topic: this.debateState.topic, currentRound: this.debateState.currentRound, currentParticipant: this.getCurrentDebateParticipant(), participants: this.debateState.participants, history: this.debateState.history, constraints: this.debateState.constraints, previousRoundStatements }; } generateDebateSummary() { if (!this.debateState.active && this.debateState.history.length === 0) { throw new Error("No debate to summarize"); } const keyPoints = new Map(); // Group key points by participant this.debateState.participants.forEach(p => { const turns = this.debateState.history.filter(turn => turn.persona === p.role); keyPoints.set(p.role, turns.map(turn => turn.statement)); }); return { topic: this.debateState.topic, participants: this.debateState.participants.map(p => p.role), rounds: this.debateState.currentRound, keyPoints, constraints: this.debateState.constraints, totalTurns: this.debateState.history.length }; } } const perspectiveServer = new PerspectiveAnalysisServer(); const server = new index_js_1.Server({ name: "perspective-mcp", version: "1.0.0", }, { capabilities: { tools: {}, }, }); server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => { return { tools: [ { name: "set_perspectives", description: "Define the list of professional roles/personas that will analyze the topic", inputSchema: { type: "object", properties: { perspectives: { type: "array", items: { type: "object", properties: { role: { type: "string", description: "The professional role or persona name" }, focusAreas: { type: "array", items: { type: "string" }, description: "Primary focus areas for this perspective" }, personality: { type: "string", description: "Personality traits and approach style" } }, required: ["role", "focusAreas", "personality"] }, description: "Array of perspective objects" } }, required: ["perspectives"] } }, { name: "perspective", description: "Perform analysis from the current perspective, building on previous insights", inputSchema: { type: "object", properties: { analysis: { type: "string", description: "The analysis from the current perspective" }, nextPerspectiveNeeded: { type: "boolean", description: "Whether to move to the next perspective after this analysis" }, nextThoughtNeeded: { type: "boolean", description: "Alias for nextPerspectiveNeeded for compatibility" } }, required: ["analysis"] } }, { name: "start_debate", description: "Start a debate between selected personas on a specific topic", inputSchema: { type: "object", properties: { topic: { type: "string", description: "The specific question or constraint to debate" }, participants: { type: "array", items: { type: "string" }, minItems: 2, maxItems: 4, description: "Array of 2-4 persona roles to participate in the debate" } }, required: ["topic", "participants"] } }, { name: "debate_turn", description: "Submit a statement in the ongoing debate", inputSchema: { type: "object", properties: { statement: { type: "string", description: "The participant's statement or response" }, nextTurnNeeded: { type: "boolean", description: "Whether to proceed to the next turn" } }, required: ["statement", "nextTurnNeeded"] } }, { name: "inject_constraint", description: "Add a new constraint or consideration to the ongoing debate", inputSchema: { type: "object", properties: { constraint: { type: "string", description: "The new constraint or consideration to add" } }, required: ["constraint"] } }, { name: "debate_summary", description: "Generate a summary of the debate including key points from each participant", inputSchema: { type: "object", properties: {} } } ], }; }); server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => { switch (request.params.name) { case "set_perspectives": { const { perspectives } = request.params.arguments; if (!Array.isArray(perspectives) || perspectives.length === 0) { throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, "Perspectives must be a non-empty array"); } perspectiveServer.setPerspectives(perspectives); return { content: [ { type: "text", text: `Successfully set ${perspectives.length} perspectives. Starting with: ${perspectives[0].role}` } ] }; } case "perspective": { const args = request.params.arguments; const { analysis } = args; // Support both parameter names for compatibility let nextPerspectiveNeeded = args.nextPerspectiveNeeded ?? args.nextThoughtNeeded ?? false; // Handle string "true"/"false" for compatibility if (typeof nextPerspectiveNeeded === 'string') { nextPerspectiveNeeded = nextPerspectiveNeeded === 'true'; } if (typeof analysis !== 'string' || analysis.trim() === '') { throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, "Analysis must be a non-empty string"); } const context = perspectiveServer.getContext(); if (!context.currentPerspective) { throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, "No perspectives defined or all perspectives completed. Use set_perspectives first."); } const result = perspectiveServer.addAnalysis(analysis, nextPerspectiveNeeded); const updatedContext = perspectiveServer.getContext(); let responseText = `Analysis recorded for ${context.currentPerspective.role}.\\n`; responseText += `Progress: ${updatedContext.previousAnalyses.length}/${perspectiveServer.perspectives.length} perspectives completed.\\n`; if (result.nextPerspectiveNeeded && updatedContext.currentPerspective) { responseText += `\\nNext perspective: ${updatedContext.currentPerspective.role}\\n`; responseText += `Focus areas: ${updatedContext.currentPerspective.focusAreas.join(", ")}\\n`; responseText += `Personality: ${updatedContext.currentPerspective.personality}`; } else if (!result.nextPerspectiveNeeded || !updatedContext.currentPerspective) { responseText += `\\nAll perspectives completed. Total analyses: ${updatedContext.previousAnalyses.length}`; } return { content: [ { type: "text", text: responseText } ] }; } case "start_debate": { const { topic, participants } = request.params.arguments; try { perspectiveServer.startDebate(topic, participants); const context = perspectiveServer.getDebateContext(); return { content: [ { type: "text", text: `Debate started on topic: "${topic}"\\n\\nParticipants:\\n${participants.map((p, i) => `${i + 1}. ${p}`).join('\\n')}\\n\\nRound 1: Initial positions\\nCurrent speaker: ${context.currentParticipant?.role}` } ] }; } catch (error) { throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, error instanceof Error ? error.message : "Failed to start debate"); } } case "debate_turn": { const { statement, nextTurnNeeded } = request.params.arguments; try { const result = perspectiveServer.addDebateTurn(statement, nextTurnNeeded); const context = perspectiveServer.getDebateContext(); let responseText = `Statement recorded for ${result.participant}.\\n`; if (result.isRoundComplete && result.nextParticipantNeeded) { responseText += `\\nRound ${result.round - 1} complete. Starting Round ${result.round}.\\n`; if (result.round === 2) { responseText += "Participants will now respond to conflicting viewpoints.\\n"; } else if (result.round === 3) { responseText += "Final round: Synthesis and compromise proposals.\\n"; } } if (result.nextParticipantNeeded && context.currentParticipant) { responseText += `\\nNext speaker: ${context.currentParticipant.role}`; if (context.previousRoundStatements.length > 0) { const toRespond = context.previousRoundStatements.find(s => s.persona !== context.currentParticipant?.role); if (toRespond) { responseText += ` (responding to ${toRespond.persona})`; } } } else if (!result.nextParticipantNeeded) { responseText += `\\nDebate complete after ${context.history.length} turns.`; } return { content: [ { type: "text", text: responseText } ] }; } catch (error) { throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, error instanceof Error ? error.message : "Failed to process debate turn"); } } case "inject_constraint": { const { constraint } = request.params.arguments; try { perspectiveServer.addConstraint(constraint); const context = perspectiveServer.getDebateContext(); return { content: [ { type: "text", text: `New constraint added: "${constraint}"\\n\\nTotal constraints: ${context.constraints.length}\\nCurrent speaker should consider this in their response.` } ] }; } catch (error) { throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, error instanceof Error ? error.message : "Failed to add constraint"); } } case "debate_summary": { try { const summary = perspectiveServer.generateDebateSummary(); let responseText = `## Debate Summary\\n\\n`; responseText += `**Topic:** ${summary.topic}\\n\\n`; responseText += `**Participants:** ${summary.participants.join(', ')}\\n\\n`; responseText += `**Duration:** ${summary.rounds} rounds, ${summary.totalTurns} total statements\\n\\n`; if (summary.constraints.length > 0) { responseText += `**Constraints Added:**\\n`; summary.constraints.forEach((c, i) => { responseText += `${i + 1}. ${c}\\n`; }); responseText += `\\n`; } responseText += `**Key Points by Participant:**\\n`; summary.keyPoints.forEach((points, participant) => { responseText += `\\n### ${participant}\\n`; points.forEach((point, i) => { responseText += `Round ${i + 1}: ${point.substring(0, 150)}${point.length > 150 ? '...' : ''}\\n`; }); }); return { content: [ { type: "text", text: responseText } ] }; } catch (error) { throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, error instanceof Error ? error.message : "Failed to generate summary"); } } default: throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); } }); async function main() { const transport = new stdio_js_1.StdioServerTransport(); await server.connect(transport); console.error("Perspective MCP server running on stdio"); } main().catch((error) => { console.error("Server error:", error); process.exit(1); }); //# sourceMappingURL=index.js.map