@vibeplanner/mcp-server
Version:
MCP server for VibePlanner AI - Integrate project management and collaborative memory into Claude Desktop
673 lines • 25.7 kB
JavaScript
/**
* Purpose: Main MCP client entry point implementing Model Context Protocol
* Dependencies: @modelcontextprotocol/sdk, axios, shared types
*
* Example Input:
* ```
* Tools called from Claude via MCP protocol
* ```
*
* Expected Output:
* ```
* Tool responses with project, task, and document data
* ```
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, } from '@modelcontextprotocol/sdk/types.js';
import axios from 'axios';
import { config } from 'dotenv';
// Load environment variables from multiple locations
config({ path: '.env' });
config({ path: '.env.local' });
config({ path: `${process.env.HOME}/.claude-collab/config` });
// API Configuration
const API_BASE_URL = process.env.CLAUDE_COLLAB_API_URL ?? 'http://localhost:8100';
const API_KEY = process.env.API_KEY;
const API_TIMEOUT = parseInt(process.env.CLAUDE_COLLAB_TIMEOUT ?? '30000');
// Create axios instance with defaults
const api = axios.create({
baseURL: `${API_BASE_URL}/api`,
timeout: API_TIMEOUT,
headers: {
'Content-Type': 'application/json',
...(API_KEY !== null && API_KEY !== undefined && API_KEY !== '' ? { 'Authorization': `Bearer ${API_KEY}` } : {})
}
});
// Response interceptor to parse date strings from API
api.interceptors.response.use((response) => {
// Parse date strings to Date objects for consistent handling
const parseDate = (value) => {
if (typeof value === 'string') {
// Check if it's an ISO date string
const dateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/;
if (dateRegex.test(value)) {
return new Date(value);
}
}
else if (value !== null && typeof value === 'object') {
// Recursively parse objects
const parsed = Array.isArray(value) ? [] : {};
for (const key in value) {
parsed[key] = parseDate(value[key]);
}
return parsed;
}
return value;
};
// Apply date parsing to response data
if (response.data) {
response.data = parseDate(response.data);
}
return response;
}, (error) => {
return Promise.reject(error);
});
// Add request/response interceptors for better error handling
api.interceptors.response.use(response => response, error => {
const errorObj = error;
// Extract detailed validation errors if available
let message;
const apiError = errorObj.response?.data?.error;
if (apiError?.validationErrors && apiError.validationErrors.length > 0) {
// Format validation errors with field names and specific messages
const validationMessages = apiError.validationErrors
.map(err => `${err.field}: ${err.message}`)
.join('; ');
message = validationMessages;
}
else {
// Fall back to generic message
message = apiError?.message ?? errorObj.message ?? 'Unknown API error';
}
const status = errorObj.response?.status ?? 500;
throw new McpError(status >= 500 ? ErrorCode.InternalError : ErrorCode.InvalidRequest, `API Error (${status}): ${message}`);
});
// Initialize MCP server
const server = new Server({
name: 'claude-collab-memory',
version: '1.0.0',
}, {
capabilities: {
tools: {},
},
});
// Tool definitions
const TOOLS = [
{
name: 'list_projects',
description: 'List all projects in the collaborative memory system',
inputSchema: {
type: 'object',
properties: {
active_only: {
type: 'boolean',
description: 'Only return active projects',
default: true
},
limit: {
type: 'number',
description: 'Maximum number of projects to return',
default: 20
}
}
}
},
{
name: 'get_project',
description: 'Get detailed information about a specific project',
inputSchema: {
type: 'object',
properties: {
project_id: {
type: 'string',
description: 'UUID of the project to retrieve'
}
},
required: ['project_id']
}
},
{
name: 'create_project',
description: 'Create a new project in the collaborative memory system',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Name of the project'
},
description: {
type: 'string',
description: 'Description of the project'
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Tags for the project'
}
},
required: ['name']
}
},
{
name: 'list_tasks',
description: 'List tasks with optional filtering',
inputSchema: {
type: 'object',
properties: {
project_id: {
type: 'string',
description: 'Filter tasks by project ID'
},
status: {
type: 'array',
items: {
type: 'string',
enum: ['todo', 'in_progress', 'review', 'done', 'blocked']
},
description: 'Filter by task status'
},
assigned_to: {
type: 'string',
description: 'Filter tasks assigned to specific person'
},
limit: {
type: 'number',
description: 'Maximum number of tasks to return',
default: 50
}
}
}
},
{
name: 'get_task',
description: 'Get detailed information about a specific task',
inputSchema: {
type: 'object',
properties: {
task_id: {
type: 'string',
description: 'UUID of the task to retrieve'
}
},
required: ['task_id']
}
},
{
name: 'create_task',
description: 'Create a new task',
inputSchema: {
type: 'object',
properties: {
project_id: {
type: 'string',
description: 'Project ID for the task'
},
title: {
type: 'string',
description: 'Title of the task'
},
description: {
type: 'string',
description: 'Detailed description of the task'
},
task_type: {
type: 'string',
enum: ['epic', 'story', 'task', 'bug', 'component'],
description: 'Type of task'
},
priority: {
type: 'string',
enum: ['low', 'medium', 'high', 'critical'],
description: 'Priority level',
default: 'medium'
},
status: {
type: 'string',
enum: ['todo', 'in_progress', 'review', 'done', 'blocked'],
description: 'Initial status',
default: 'todo'
},
assigned_to: {
type: 'string',
description: 'Person assigned to the task'
},
epic_id: {
type: 'string',
description: 'Parent epic ID if this is a story or task'
},
story_id: {
type: 'string',
description: 'Parent story ID if this is a task'
},
component_type: {
type: 'string',
enum: ['page', 'component', 'service', 'api_endpoint', 'other'],
description: 'Type of component for development tasks'
},
component_path: {
type: 'string',
description: 'File path for component-related tasks'
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Tags for the task'
}
},
required: ['project_id', 'title', 'task_type']
}
},
{
name: 'update_task',
description: 'Update an existing task',
inputSchema: {
type: 'object',
properties: {
task_id: {
type: 'string',
description: 'UUID of the task to update'
},
title: {
type: 'string',
description: 'New title'
},
description: {
type: 'string',
description: 'New description'
},
status: {
type: 'string',
enum: ['todo', 'in_progress', 'review', 'done', 'blocked'],
description: 'New status'
},
priority: {
type: 'string',
enum: ['low', 'medium', 'high', 'critical'],
description: 'New priority'
},
assigned_to: {
type: 'string',
description: 'New assignee'
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'New tags'
}
},
required: ['task_id']
}
},
{
name: 'search_documents',
description: 'Search for documents using text or vector search',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query'
},
search_type: {
type: 'string',
enum: ['text', 'vector', 'hybrid'],
description: 'Type of search to perform',
default: 'hybrid'
},
project_id: {
type: 'string',
description: 'Filter by project ID'
},
document_type: {
type: 'array',
items: {
type: 'string',
enum: ['plan', 'specification', 'notes', 'code', 'other']
},
description: 'Filter by document types'
},
limit: {
type: 'number',
description: 'Maximum number of results',
default: 20
}
},
required: ['query']
}
},
{
name: 'get_document',
description: 'Get a specific document by ID',
inputSchema: {
type: 'object',
properties: {
document_id: {
type: 'string',
description: 'UUID of the document to retrieve'
}
},
required: ['document_id']
}
},
{
name: 'create_document',
description: 'Create a new document',
inputSchema: {
type: 'object',
properties: {
project_id: {
type: 'string',
description: 'Project ID (required if no task_id)'
},
task_id: {
type: 'string',
description: 'Task ID (required if no project_id)'
},
title: {
type: 'string',
description: 'Document title'
},
content: {
type: 'string',
description: 'Document content'
},
type: {
type: 'string',
enum: ['plan', 'specification', 'notes', 'code', 'other'],
description: 'Type of document'
}
},
required: ['title', 'type']
}
},
{
name: 'update_document',
description: 'Update an existing document',
inputSchema: {
type: 'object',
properties: {
document_id: {
type: 'string',
description: 'UUID of the document to update'
},
title: {
type: 'string',
description: 'New title'
},
content: {
type: 'string',
description: 'New content'
},
type: {
type: 'string',
enum: ['plan', 'specification', 'notes', 'code', 'other'],
description: 'New document type'
}
},
required: ['document_id']
}
}
];
// Register tool handlers
server.setRequestHandler(ListToolsRequestSchema, () => {
return { tools: TOOLS };
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const toolArgs = args ?? {};
try {
switch (name) {
case 'list_projects':
return await handleListProjects(toolArgs);
case 'get_project':
return await handleGetProject(toolArgs);
case 'create_project':
return await handleCreateProject(toolArgs);
case 'list_tasks':
return await handleListTasks(toolArgs);
case 'get_task':
return await handleGetTask(toolArgs);
case 'create_task':
return await handleCreateTask(toolArgs);
case 'update_task':
return await handleUpdateTask(toolArgs);
case 'search_documents':
return await handleSearchDocuments(toolArgs);
case 'get_document':
return await handleGetDocument(toolArgs);
case 'create_document':
return await handleCreateDocument(toolArgs);
case 'update_document':
return await handleUpdateDocument(toolArgs);
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
}
catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
}
});
// Tool handlers
async function handleListProjects(args) {
const { active_only = true, limit = 20 } = args;
const response = await api.get('/projects', {
params: {
is_active: active_only,
limit,
page: 1
}
});
const projects = response.data.data.data;
return {
content: [{
type: 'text',
text: `Found ${projects.length} projects:\n\n${projects.map((p) => `• **${p.name}** (${p.id})\n ${p.description ?? 'No description'}\n Tags: ${p.tags.join(', ') !== '' ? p.tags.join(', ') : 'None'}`).join('\n\n')}`
}]
};
}
async function handleGetProject(args) {
const { project_id } = args;
if (typeof project_id !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'project_id must be a string');
}
const response = await api.get(`/projects/${project_id}`);
const project = response.data.data;
return {
content: [{
type: 'text',
text: `**Project: ${project.name}**\n\nID: ${project.id}\nDescription: ${project.description ?? 'No description'}\nTags: ${project.tags.join(', ') !== '' ? project.tags.join(', ') : 'None'}\nActive: ${project.is_active}\nCreated: ${project.created_at instanceof Date ? project.created_at.toISOString() : new Date(project.created_at).toISOString()}\nCreated by: ${project.created_by}`
}]
};
}
async function handleCreateProject(args) {
const { name, description, tags } = args;
if (typeof name !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'name must be a string');
}
const projectData = {
name,
description: typeof description === 'string' ? description : undefined,
tags: Array.isArray(tags) ? tags : [],
created_by: 'claude-ai'
};
const response = await api.post('/projects', projectData);
const project = response.data.data;
return {
content: [{
type: 'text',
text: `✅ Created project: **${project.name}** (${project.id})`
}]
};
}
async function handleListTasks(args) {
const { project_id, status, assigned_to, limit = 50 } = args;
const params = { limit, page: 1 };
if (project_id !== null && project_id !== undefined && project_id !== '')
params.project_id = project_id;
if (Array.isArray(status))
params.status = status;
if (assigned_to !== null && assigned_to !== undefined && assigned_to !== '')
params.assigned_to = assigned_to;
const response = await api.get('/tasks', { params });
const tasks = response.data.data.data;
return {
content: [{
type: 'text',
text: `Found ${tasks.length} tasks:\n\n${tasks.map((t) => `• **${t.title}** [${t.status.toUpperCase()}] (${t.id})\n Type: ${t.task_type} | Priority: ${t.priority}\n ${t.description !== null && t.description !== undefined && t.description !== '' ? t.description.substring(0, 100) + '...' : 'No description'}\n Assigned: ${t.assigned_to ?? 'Unassigned'}`).join('\n\n')}`
}]
};
}
async function handleGetTask(args) {
const { task_id } = args;
if (typeof task_id !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'task_id must be a string');
}
const response = await api.get(`/tasks/${task_id}`);
const task = response.data.data;
return {
content: [{
type: 'text',
text: `**Task: ${task.title}**\n\nID: ${task.id}\nType: ${task.task_type}\nStatus: ${task.status}\nPriority: ${task.priority}\nAssigned: ${task.assigned_to ?? 'Unassigned'}\nProject: ${task.project_id}\n\nDescription:\n${task.description !== null && task.description !== undefined && task.description !== '' ? task.description : 'No description'}\n\nTags: ${task.tags.join(', ') !== null && task.tags.join(', ') !== undefined && task.tags.join(', ') !== '' ? task.tags.join(', ') : 'None'}`
}]
};
}
async function handleCreateTask(args) {
const { project_id, title, description, task_type, priority = 'medium', status = 'todo', assigned_to, epic_id, story_id, component_type, component_path, tags } = args;
if (typeof project_id !== 'string' || typeof title !== 'string' || typeof task_type !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'project_id, title, and task_type are required');
}
const taskData = {
project_id,
title,
description: typeof description === 'string' ? description : undefined,
task_type: task_type,
priority: priority,
status: status,
assigned_to: typeof assigned_to === 'string' ? assigned_to : undefined,
epic_id: typeof epic_id === 'string' ? epic_id : undefined,
story_id: typeof story_id === 'string' ? story_id : undefined,
component_type: typeof component_type === 'string' ? component_type : undefined,
component_path: typeof component_path === 'string' ? component_path : undefined,
tags: Array.isArray(tags) ? tags : [],
created_by: 'claude-ai'
};
const response = await api.post('/tasks', taskData);
const task = response.data.data;
return {
content: [{
type: 'text',
text: `✅ Created task: **${task.title}** [${task.status.toUpperCase()}] (${task.id})`
}]
};
}
async function handleUpdateTask(args) {
const { task_id, ...updates } = args;
if (typeof task_id !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'task_id must be a string');
}
const response = await api.put(`/tasks/${task_id}`, updates);
const task = response.data.data;
return {
content: [{
type: 'text',
text: `✅ Updated task: **${task.title}** [${task.status.toUpperCase()}] (${task.id})`
}]
};
}
async function handleSearchDocuments(args) {
const { query, search_type = 'hybrid', project_id, document_type, limit = 20 } = args;
if (typeof query !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'query must be a string');
}
const params = {
query,
type: search_type,
limit,
page: 1
};
if (project_id !== null && project_id !== undefined && project_id !== '')
params.project_id = project_id;
if (Array.isArray(document_type))
params.document_type = document_type;
const response = await api.get('/documents/search', { params });
const searchResults = response.data.data;
return {
content: [{
type: 'text',
text: `Found ${searchResults.data.length} documents for "${query}":\n\n${searchResults.data.map((result) => {
const doc = result.item;
return `• **${doc.title}** (Score: ${result.score.toFixed(2)}) [${doc.type}]\n ID: ${doc.id}\n ${(doc.content !== null && doc.content !== undefined && doc.content !== '') ? doc.content.substring(0, 150) + '...' : 'No content preview'}`;
}).join('\n\n')}`
}]
};
}
async function handleGetDocument(args) {
const { document_id } = args;
if (typeof document_id !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'document_id must be a string');
}
const response = await api.get(`/documents/${document_id}`);
const document = response.data.data;
return {
content: [{
type: 'text',
text: `**Document: ${document.title}**\n\nID: ${document.id}\nType: ${document.type}\nVersion: ${document.version}\nProject: ${document.project_id ?? 'N/A'}\nTask: ${document.task_id ?? 'N/A'}\nCreated: ${document.created_at instanceof Date ? document.created_at.toISOString() : new Date(document.created_at).toISOString()}\n\nContent:\n${document.content !== null && document.content !== undefined && document.content !== '' ? document.content : 'No content'}`
}]
};
}
async function handleCreateDocument(args) {
const { project_id, task_id, title, content, type } = args;
if (typeof title !== 'string' || typeof type !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'title and type are required');
}
if ((project_id === null || project_id === undefined || project_id === '') && (task_id === null || task_id === undefined || task_id === '')) {
throw new McpError(ErrorCode.InvalidParams, 'Either project_id or task_id must be provided');
}
const documentData = {
project_id: typeof project_id === 'string' ? project_id : undefined,
task_id: typeof task_id === 'string' ? task_id : undefined,
title,
content: typeof content === 'string' ? content : undefined,
type: type,
created_by: 'claude-ai'
};
const response = await api.post('/documents', documentData);
const document = response.data.data;
return {
content: [{
type: 'text',
text: `✅ Created document: **${document.title}** [${document.type}] (${document.id})`
}]
};
}
async function handleUpdateDocument(args) {
const { document_id, ...updates } = args;
if (typeof document_id !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'document_id must be a string');
}
const response = await api.put(`/documents/${document_id}`, updates);
const document = response.data.data;
return {
content: [{
type: 'text',
text: `✅ Updated document: **${document.title}** [${document.type}] (${document.id})`
}]
};
}
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Claude Collaborative Memory MCP Client started');
}
main().catch((error) => {
console.error('Server failed to start:', error);
process.exit(1);
});
//# sourceMappingURL=index.js.map