@waldzellai/collaborative-reasoning
Version:
MCP server for diagrammatic thinking and spatial representation
790 lines (788 loc) • 34.8 kB
JavaScript
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 chalk from 'chalk';
class CollaborativeReasoningServer {
personaRegistry = {};
contributionHistory = {};
disagreementTracker = {};
sessionHistory = {};
nextContributionId = 1;
validateCollaborativeReasoningData(input) {
const data = input;
// Validate required fields
if (!data.topic || typeof data.topic !== 'string') {
throw new Error('Invalid topic: must be a string');
}
if (!Array.isArray(data.personas)) {
throw new Error('Invalid personas: must be an array');
}
if (!Array.isArray(data.contributions)) {
throw new Error('Invalid contributions: must be an array');
}
if (!data.stage || typeof data.stage !== 'string') {
throw new Error('Invalid stage: must be a string');
}
if (!data.activePersonaId || typeof data.activePersonaId !== 'string') {
throw new Error('Invalid activePersonaId: must be a string');
}
if (!data.sessionId || typeof data.sessionId !== 'string') {
throw new Error('Invalid sessionId: must be a string');
}
if (typeof data.iteration !== 'number' || data.iteration < 0) {
throw new Error('Invalid iteration: must be a non-negative number');
}
if (typeof data.nextContributionNeeded !== 'boolean') {
throw new Error('Invalid nextContributionNeeded: must be a boolean');
}
// Validate personas
const personas = [];
for (const persona of data.personas) {
if (!persona.id || typeof persona.id !== 'string') {
throw new Error('Invalid persona id: must be a string');
}
if (!persona.name || typeof persona.name !== 'string') {
throw new Error('Invalid persona name: must be a string');
}
if (!Array.isArray(persona.expertise)) {
throw new Error('Invalid persona expertise: must be an array');
}
if (!persona.background || typeof persona.background !== 'string') {
throw new Error('Invalid persona background: must be a string');
}
if (!persona.perspective || typeof persona.perspective !== 'string') {
throw new Error('Invalid persona perspective: must be a string');
}
if (!Array.isArray(persona.biases)) {
throw new Error('Invalid persona biases: must be an array');
}
if (!persona.communication || typeof persona.communication !== 'object') {
throw new Error('Invalid persona communication: must be an object');
}
const communication = persona.communication;
if (!communication.style || typeof communication.style !== 'string') {
throw new Error('Invalid persona communication style: must be a string');
}
if (!communication.tone || typeof communication.tone !== 'string') {
throw new Error('Invalid persona communication tone: must be a string');
}
const expertise = [];
for (const exp of persona.expertise) {
if (typeof exp === 'string') {
expertise.push(exp);
}
}
const biases = [];
for (const bias of persona.biases) {
if (typeof bias === 'string') {
biases.push(bias);
}
}
personas.push({
id: persona.id,
name: persona.name,
expertise,
background: persona.background,
perspective: persona.perspective,
biases,
communication: {
style: communication.style,
tone: communication.tone
}
});
}
// Validate contributions
const contributions = [];
for (const contribution of data.contributions) {
if (!contribution.personaId || typeof contribution.personaId !== 'string') {
throw new Error('Invalid contribution personaId: must be a string');
}
if (!contribution.content || typeof contribution.content !== 'string') {
throw new Error('Invalid contribution content: must be a string');
}
if (!contribution.type || typeof contribution.type !== 'string') {
throw new Error('Invalid contribution type: must be a string');
}
if (typeof contribution.confidence !== 'number' || contribution.confidence < 0 || contribution.confidence > 1) {
throw new Error('Invalid contribution confidence: must be a number between 0 and 1');
}
const referenceIds = [];
if (Array.isArray(contribution.referenceIds)) {
for (const refId of contribution.referenceIds) {
if (typeof refId === 'string') {
referenceIds.push(refId);
}
}
}
const contributionData = {
personaId: contribution.personaId,
content: contribution.content,
type: contribution.type,
confidence: contribution.confidence,
};
if (referenceIds.length > 0) {
contributionData.referenceIds = referenceIds;
}
contributions.push(contributionData);
}
// Validate disagreements
const disagreements = [];
if (Array.isArray(data.disagreements)) {
for (const disagreement of data.disagreements) {
if (!disagreement.topic || typeof disagreement.topic !== 'string') {
throw new Error('Invalid disagreement topic: must be a string');
}
if (!Array.isArray(disagreement.positions)) {
throw new Error('Invalid disagreement positions: must be an array');
}
const positions = [];
for (const position of disagreement.positions) {
if (!position.personaId || typeof position.personaId !== 'string') {
throw new Error('Invalid position personaId: must be a string');
}
if (!position.position || typeof position.position !== 'string') {
throw new Error('Invalid position statement: must be a string');
}
if (!Array.isArray(position.arguments)) {
throw new Error('Invalid position arguments: must be an array');
}
const args = [];
for (const arg of position.arguments) {
if (typeof arg === 'string') {
args.push(arg);
}
}
positions.push({
personaId: position.personaId,
position: position.position,
arguments: args
});
}
let resolution = undefined;
// Explicit, detailed type guard for disagreement.resolution
if (disagreement.resolution &&
typeof disagreement.resolution === 'object' &&
'type' in disagreement.resolution &&
typeof disagreement.resolution.type === 'string' &&
['consensus', 'compromise', 'integration', 'tabled'].includes(disagreement.resolution.type) &&
'description' in disagreement.resolution &&
typeof disagreement.resolution.description === 'string') {
// Now TS knows the exact shape, inner checks are redundant
resolution = {
type: disagreement.resolution.type,
description: disagreement.resolution.description
};
}
const disagreementData = {
topic: disagreement.topic,
positions,
};
if (resolution) {
disagreementData.resolution = resolution;
}
disagreements.push(disagreementData);
}
}
// Validate optional array fields
const keyInsights = [];
if (Array.isArray(data.keyInsights)) {
for (const insight of data.keyInsights) {
if (typeof insight === 'string') {
keyInsights.push(insight);
}
}
}
const consensusPoints = [];
if (Array.isArray(data.consensusPoints)) {
for (const point of data.consensusPoints) {
if (typeof point === 'string') {
consensusPoints.push(point);
}
}
}
const openQuestions = [];
if (Array.isArray(data.openQuestions)) {
for (const question of data.openQuestions) {
if (typeof question === 'string') {
openQuestions.push(question);
}
}
}
const suggestedContributionTypes = [];
if (Array.isArray(data.suggestedContributionTypes)) {
for (const type of data.suggestedContributionTypes) {
if (typeof type === 'string') {
suggestedContributionTypes.push(type);
}
}
}
// Create validated data object
const validatedData = {
topic: data.topic,
personas,
contributions,
stage: data.stage,
activePersonaId: data.activePersonaId,
sessionId: data.sessionId,
iteration: data.iteration,
nextContributionNeeded: data.nextContributionNeeded,
};
// Conditionally add optional properties
if (disagreements.length > 0) {
validatedData.disagreements = disagreements;
}
if (typeof data.nextPersonaId === 'string') {
validatedData.nextPersonaId = data.nextPersonaId;
}
if (keyInsights.length > 0) {
validatedData.keyInsights = keyInsights;
}
if (consensusPoints.length > 0) {
validatedData.consensusPoints = consensusPoints;
}
if (openQuestions.length > 0) {
validatedData.openQuestions = openQuestions;
}
if (data.finalRecommendation && typeof data.finalRecommendation === 'string') {
validatedData.finalRecommendation = data.finalRecommendation;
}
if (suggestedContributionTypes.length > 0) {
validatedData.suggestedContributionTypes = suggestedContributionTypes;
}
return validatedData;
}
updateRegistries(data) {
const sessionId = data.sessionId;
// Update persona registry
if (!this.personaRegistry[sessionId]) {
this.personaRegistry[sessionId] = {};
}
for (const persona of data.personas) {
this.personaRegistry[sessionId][persona.id] = persona;
}
// Update contribution history
if (!this.contributionHistory[sessionId]) {
this.contributionHistory[sessionId] = [];
}
for (const contribution of data.contributions) {
// Only add new contributions
const exists = this.contributionHistory[sessionId].some(c => c.personaId === contribution.personaId && c.content === contribution.content);
if (!exists) {
this.contributionHistory[sessionId].push(contribution);
}
}
// Update disagreement tracker
if (data.disagreements && data.disagreements.length > 0) {
if (!this.disagreementTracker[sessionId]) {
this.disagreementTracker[sessionId] = [];
}
for (const disagreement of data.disagreements) {
// Check if this disagreement already exists
const existingIndex = this.disagreementTracker[sessionId].findIndex(d => d.topic === disagreement.topic);
if (existingIndex >= 0) {
// Update existing disagreement
this.disagreementTracker[sessionId][existingIndex] = disagreement;
}
else {
// Add new disagreement
this.disagreementTracker[sessionId].push(disagreement);
}
}
}
}
updateSessionHistory(data) {
let historyEntry = this.sessionHistory[data.sessionId]; // Get potential entry
if (!historyEntry) { // Check if it exists
historyEntry = []; // Create new array if not
this.sessionHistory[data.sessionId] = historyEntry; // Assign it back to the object
}
// Now, historyEntry is guaranteed to be CollaborativeReasoningData[]
historyEntry.push(data);
this.updateRegistries(data);
}
selectNextPersona(data) {
// If nextPersonaId is already set, use it
if (data.nextPersonaId) {
return data.nextPersonaId;
}
// Otherwise, select the next persona in rotation
if (!data.personas || data.personas.length === 0) {
throw new Error("Cannot determine next persona: No personas defined in session.");
}
const personaIds = data.personas.map(p => p.id);
const currentIndex = personaIds.indexOf(data.activePersonaId);
// If active persona not found (shouldn't happen ideally), default to first
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % personaIds.length;
// personaIds[nextIndex] is guaranteed to be string due to empty check above
return personaIds[nextIndex];
}
getPersonaColor(index) {
const colors = [
chalk.blue,
chalk.green,
chalk.yellow,
chalk.magenta,
chalk.cyan,
chalk.red
];
// Use non-null assertion as logic guarantees a valid index
return colors[index % colors.length];
}
getContributionTypeColor(type) {
switch (type) {
case 'observation': return chalk.blue;
case 'question': return chalk.cyan;
case 'insight': return chalk.green;
case 'concern': return chalk.yellow;
case 'suggestion': return chalk.magenta;
case 'challenge': return chalk.red;
case 'synthesis': return chalk.white;
default: return chalk.white;
}
}
getConfidenceBar(confidence) {
const barLength = 20;
const filledLength = Math.round(confidence * barLength);
const emptyLength = barLength - filledLength;
let bar = '[';
// Choose color based on confidence level
let color;
if (confidence >= 0.8) {
color = chalk.green;
}
else if (confidence >= 0.5) {
color = chalk.yellow;
}
else {
color = chalk.red;
}
bar += color('='.repeat(filledLength));
bar += ' '.repeat(emptyLength);
bar += `] ${(confidence * 100).toFixed(0)}%`;
return bar;
}
visualizeCollaborativeReasoning(data) {
let output = `\n${chalk.bold(`COLLABORATIVE REASONING: ${data.topic}`)} (ID: ${data.sessionId})\n\n`;
// Stage and iteration
output += `${chalk.cyan('Stage:')} ${data.stage}\n`;
output += `${chalk.cyan('Iteration:')} ${data.iteration}\n\n`;
// Personas
output += `${chalk.bold('PERSONAS:')}\n`;
for (let i = 0; i < data.personas.length; i++) {
const persona = data.personas[i];
const color = this.getPersonaColor(i);
output += `${color(`${persona?.name} (${persona?.id})`)}\n`;
output += ` Expertise: ${persona?.expertise.join(', ')}\n`;
output += ` Perspective: ${persona?.perspective}\n`;
output += ` Communication: ${persona?.communication.style} (${persona?.communication.tone})\n`;
// Highlight active persona
if (persona?.id === data.activePersonaId) {
output += ` ${chalk.bgGreen(chalk.black(' ACTIVE '))}\n`;
}
output += '\n';
}
// Contributions
if (data.contributions.length > 0) {
output += `${chalk.bold('CONTRIBUTIONS:')}\n\n`;
for (const contribution of data.contributions) {
// Find persona
const persona = data.personas.find(p => p.id === contribution.personaId);
if (!persona)
continue;
// Get persona color
const personaIndex = data.personas.findIndex(p => p.id === contribution.personaId);
const personaColor = this.getPersonaColor(personaIndex);
// Get contribution type color
const typeColor = this.getContributionTypeColor(contribution.type);
output += `${personaColor(`[${persona.name}]`)} ${typeColor(`[${contribution.type}]`)}\n`;
output += `${contribution.content}\n`;
output += `Confidence: ${this.getConfidenceBar(contribution.confidence)}\n`;
if (contribution.referenceIds && contribution.referenceIds.length > 0) {
output += `References: ${contribution.referenceIds.join(', ')}\n`;
}
output += '\n';
}
}
// Disagreements
if (data.disagreements && data.disagreements.length > 0) {
output += `${chalk.bold('DISAGREEMENTS:')}\n\n`;
for (const disagreement of data.disagreements) {
output += `${chalk.red('Topic:')} ${disagreement.topic}\n\n`;
for (const position of disagreement.positions) {
// Find persona
const persona = data.personas.find(p => p.id === position.personaId);
if (!persona)
continue;
// Get persona color
const personaIndex = data.personas.findIndex(p => p.id === position.personaId);
const personaColor = this.getPersonaColor(personaIndex);
output += `${personaColor(`[${persona.name}]`)} Position: ${position.position}\n`;
output += ` Arguments:\n`;
for (const argument of position.arguments) {
output += ` - ${argument}\n`;
}
output += '\n';
}
if (disagreement.resolution) {
output += `${chalk.green('Resolution:')} ${disagreement.resolution.type}\n`;
output += `${disagreement.resolution.description}\n\n`;
}
else {
output += `${chalk.yellow('Status:')} Unresolved\n\n`;
}
}
}
// Collaboration output
if (data.keyInsights && data.keyInsights.length > 0) {
output += `${chalk.bold('KEY INSIGHTS:')}\n`;
for (const insight of data.keyInsights) {
output += ` - ${insight}\n`;
}
output += '\n';
}
if (data.consensusPoints && data.consensusPoints.length > 0) {
output += `${chalk.bold('CONSENSUS POINTS:')}\n`;
for (const point of data.consensusPoints) {
output += ` - ${point}\n`;
}
output += '\n';
}
if (data.openQuestions && data.openQuestions.length > 0) {
output += `${chalk.bold('OPEN QUESTIONS:')}\n`;
for (const question of data.openQuestions) {
output += ` - ${question}\n`;
}
output += '\n';
}
if (data.finalRecommendation) {
output += `${chalk.bold('FINAL RECOMMENDATION:')}\n`;
output += `${data.finalRecommendation}\n\n`;
}
// Next steps
if (data.nextContributionNeeded) {
const nextPersonaId = this.selectNextPersona(data);
const nextPersona = data.personas.find(p => p.id === nextPersonaId);
if (nextPersona) {
output += `${chalk.blue('NEXT CONTRIBUTION:')}\n`;
output += ` Next persona: ${nextPersona.name}\n`;
if (data.suggestedContributionTypes && data.suggestedContributionTypes.length > 0) {
output += ` Suggested contribution types: ${data.suggestedContributionTypes.join(', ')}\n`;
}
}
}
return output;
}
processCollaborativeReasoning(input) {
try {
const validatedInput = this.validateCollaborativeReasoningData(input);
// Update the next persona if not specified
if (!validatedInput.nextPersonaId && validatedInput.nextContributionNeeded) {
validatedInput.nextPersonaId = this.selectNextPersona(validatedInput);
}
// Update session state
this.updateSessionHistory(validatedInput);
// Generate visualization
const visualization = this.visualizeCollaborativeReasoning(validatedInput);
console.error(visualization);
// Return the collaboration result
return {
content: [{
type: "text",
text: JSON.stringify({
sessionId: validatedInput.sessionId,
topic: validatedInput.topic,
stage: validatedInput.stage,
iteration: validatedInput.iteration,
personaCount: validatedInput.personas.length,
contributionCount: validatedInput.contributions.length,
disagreementCount: validatedInput.disagreements?.length || 0,
activePersonaId: validatedInput.activePersonaId,
nextPersonaId: validatedInput.nextPersonaId,
nextContributionNeeded: validatedInput.nextContributionNeeded,
suggestedContributionTypes: validatedInput.suggestedContributionTypes
}, 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
};
}
}
}
const COLLABORATIVE_REASONING_TOOL = {
name: "collaborativeReasoning",
description: `A detailed tool for simulating expert collaboration with diverse perspectives.
This tool helps models tackle complex problems by coordinating multiple viewpoints.
It provides a framework for structured collaborative reasoning and perspective integration.
When to use this tool:
- Complex problems requiring diverse expertise
- Issues needing multiple stakeholder perspectives
- Scenarios with potential value trade-offs
- Creative tasks benefiting from diverse viewpoints
- Problems where biases might limit single-perspective thinking
Key features:
- Multi-persona simulation
- Structured collaboration process
- Productive disagreement management
- Cross-pollination of ideas
- Perspective synthesis and integration`,
inputSchema: {
type: "object",
properties: {
topic: {
type: "string",
description: "The topic or problem being addressed"
},
personas: {
type: "array",
description: "The expert personas participating in the collaboration",
items: {
type: "object",
properties: {
id: {
type: "string",
description: "Unique identifier for the persona"
},
name: {
type: "string",
description: "Name of the persona"
},
expertise: {
type: "array",
description: "Areas of expertise",
items: {
type: "string"
}
},
background: {
type: "string",
description: "Background and experience of the persona"
},
perspective: {
type: "string",
description: "Unique perspective or viewpoint"
},
biases: {
type: "array",
description: "Potential biases of this persona",
items: {
type: "string"
}
},
communication: {
type: "object",
description: "Communication style of the persona",
properties: {
style: {
type: "string",
description: "Communication style (e.g., direct, analytical, narrative)"
},
tone: {
type: "string",
description: "Tone of communication (e.g., formal, casual, enthusiastic)"
}
},
required: ["style", "tone"]
}
},
required: ["id", "name", "expertise", "background", "perspective", "biases", "communication"]
}
},
contributions: {
type: "array",
description: "Contributions from the personas",
items: {
type: "object",
properties: {
personaId: {
type: "string",
description: "ID of the contributing persona"
},
content: {
type: "string",
description: "Content of the contribution"
},
type: {
type: "string",
enum: ["observation", "question", "insight", "concern", "suggestion", "challenge", "synthesis"],
description: "Type of contribution"
},
referenceIds: {
type: "array",
description: "IDs of previous contributions this builds upon",
items: {
type: "string"
}
},
confidence: {
type: "number",
minimum: 0,
maximum: 1,
description: "Confidence level in this contribution (0.0-1.0)"
}
},
required: ["personaId", "content", "type", "confidence"]
}
},
disagreements: {
type: "array",
description: "Points of disagreement between personas",
items: {
type: "object",
properties: {
topic: {
type: "string",
description: "Topic of disagreement"
},
positions: {
type: "array",
description: "Different positions on the topic",
items: {
type: "object",
properties: {
personaId: {
type: "string",
description: "ID of the persona holding this position"
},
position: {
type: "string",
description: "Description of the position"
},
arguments: {
type: "array",
description: "Arguments supporting this position",
items: {
type: "string"
}
}
},
required: ["personaId", "position", "arguments"]
}
},
resolution: {
type: "object",
description: "Resolution of the disagreement, if any",
properties: {
type: {
type: "string",
enum: ["consensus", "compromise", "integration", "tabled"],
description: "Type of resolution"
},
description: {
type: "string",
description: "Description of how the disagreement was resolved"
}
},
required: ["type", "description"]
}
},
required: ["topic", "positions"]
}
},
stage: {
type: "string",
enum: ["problem-definition", "ideation", "critique", "integration", "decision", "reflection"],
description: "Current stage of the collaborative process"
},
activePersonaId: {
type: "string",
description: "ID of the currently active persona"
},
nextPersonaId: {
type: "string",
description: "ID of the persona that should contribute next"
},
keyInsights: {
type: "array",
description: "Key insights from the collaboration",
items: {
type: "string"
}
},
consensusPoints: {
type: "array",
description: "Points of consensus among participants",
items: {
type: "string"
}
},
openQuestions: {
type: "array",
description: "Open questions requiring further exploration",
items: {
type: "string"
}
},
finalRecommendation: {
type: "string",
description: "Final recommendation based on the collaboration"
},
sessionId: {
type: "string",
description: "Unique identifier for this collaboration session"
},
iteration: {
type: "number",
minimum: 0,
description: "Current iteration of the collaboration"
},
nextContributionNeeded: {
type: "boolean",
description: "Whether another contribution is needed"
},
suggestedContributionTypes: {
type: "array",
description: "Suggested types for the next contribution",
items: {
type: "string",
enum: ["observation", "question", "insight", "concern", "suggestion", "challenge", "synthesis"]
}
}
},
required: ["topic", "personas", "contributions", "stage", "activePersonaId", "sessionId", "iteration", "nextContributionNeeded"]
}
};
const server = new Server({
name: "collaborative-reasoning-server",
version: "0.1.2",
}, {
capabilities: {
tools: {},
},
});
const collaborativeReasoningServer = new CollaborativeReasoningServer();
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [COLLABORATIVE_REASONING_TOOL],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "collaborativeReasoning") {
return collaborativeReasoningServer.processCollaborativeReasoning(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("Collaborative Reasoning MCP Server running on stdio");
}
runServer().catch((error) => {
console.error("Fatal error running server:", error);
process.exit(1);
});
//# sourceMappingURL=index.js.map