#!/usr/bin/env node
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 { TrelloClient } from './trello-client.js';
class TrelloMCPServer {
constructor() {
this.server = new Server(
{
name: 'trello-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Initialize board configurations
this.boardConfigs = this.initializeBoardConfigs();
// Create TrelloClient instances for each board
this.trelloClients = new Map();
for (const [boardKey, config] of Object.entries(this.boardConfigs)) {
this.trelloClients.set(boardKey, new TrelloClient({
apiKey: process.env.TRELLO_API_KEY,
token: process.env.TRELLO_TOKEN,
knowledgeBoardId: config.boardId,
}));
}
this.setupToolHandlers();
}
initializeBoardConfigs() {
const configs = {};
// Personal board
if (process.env.TRELLO_PERSONAL_BOARD_ID) {
configs.personal = {
boardId: process.env.TRELLO_PERSONAL_BOARD_ID,
name: process.env.TRELLO_PERSONAL_BOARD_NAME || 'Personal',
key: 'personal'
};
}
// Work board
if (process.env.TRELLO_WORK_BOARD_ID) {
configs.work = {
boardId: process.env.TRELLO_WORK_BOARD_ID,
name: process.env.TRELLO_WORK_BOARD_NAME || 'Work',
key: 'work'
};
}
if (Object.keys(configs).length === 0) {
throw new Error('No Trello boards configured. Please set TRELLO_PERSONAL_BOARD_ID and/or TRELLO_WORK_BOARD_ID environment variables.');
}
return configs;
}
getBoardSelectionSchema() {
return {
type: 'string',
description: 'Which board to use for this operation',
enum: Object.keys(this.boardConfigs),
enumDescriptions: Object.values(this.boardConfigs).map(config => config.name),
title: 'Board Selection'
};
}
setupToolHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'store_knowledge',
description: 'Store a piece of knowledge (code snippet, note, etc.) in Trello',
inputSchema: {
type: 'object',
properties: {
board: this.getBoardSelectionSchema(),
title: {
type: 'string',
description: 'Title for the knowledge item'
},
content: {
type: 'string',
description: 'The actual content (code, notes, etc.)'
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Tags to categorize this knowledge'
},
category: {
type: 'string',
description: 'Category/topic (e.g., symfony, javascript, docker)'
}
},
required: ['board', 'title', 'content']
}
},
{
name: 'search_knowledge',
description: 'Search for knowledge items by keywords, tags, or categories',
inputSchema: {
type: 'object',
properties: {
board: this.getBoardSelectionSchema(),
query: {
type: 'string',
description: 'Search query (searches titles, content, and tags)'
},
category: {
type: 'string',
description: 'Filter by specific category'
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Filter by specific tags'
}
},
required: ['board', 'query']
}
},
{
name: 'get_knowledge',
description: 'Retrieve a specific knowledge item by ID',
inputSchema: {
type: 'object',
properties: {
board: this.getBoardSelectionSchema(),
id: {
type: 'string',
description: 'The Trello card ID'
}
},
required: ['board', 'id']
}
},
{
name: 'list_topics',
description: 'List all available topics/categories in the knowledge base',
inputSchema: {
type: 'object',
properties: {
board: this.getBoardSelectionSchema()
},
required: ['board']
}
},
{
name: 'update_knowledge',
description: 'Update an existing knowledge item',
inputSchema: {
type: 'object',
properties: {
board: this.getBoardSelectionSchema(),
id: {
type: 'string',
description: 'The Trello card ID to update'
},
title: {
type: 'string',
description: 'New title (optional)'
},
content: {
type: 'string',
description: 'New content (optional)'
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'New tags (optional)'
}
},
required: ['board', 'id']
}
},
{
name: 'list_boards',
description: 'List all configured Trello boards',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'read_card_from_url',
description: 'Read any Trello card by pasting its URL or card ID',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'Trello card URL (e.g., https://trello.com/c/WuHQFDWt/...) or card ID'
}
},
required: ['url']
}
}
]
}));
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'store_knowledge':
return await this.handleStoreKnowledge(args);
case 'search_knowledge':
return await this.handleSearchKnowledge(args);
case 'get_knowledge':
return await this.handleGetKnowledge(args);
case 'list_topics':
return await this.handleListTopics(args);
case 'update_knowledge':
return await this.handleUpdateKnowledge(args);
case 'list_boards':
return await this.handleListBoards(args);
case 'read_card_from_url':
return await this.handleReadCardFromUrl(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error.message}`
}
]
};
}
});
}
getTrelloClient(boardKey) {
const client = this.trelloClients.get(boardKey);
if (!client) {
throw new Error(`Invalid board: ${boardKey}. Available boards: ${Object.keys(this.boardConfigs).join(', ')}`);
}
return client;
}
getBoardName(boardKey) {
return this.boardConfigs[boardKey]?.name || boardKey;
}
async handleStoreKnowledge(args) {
const { board, title, content, tags = [], category } = args;
const trello = this.getTrelloClient(board);
const boardName = this.getBoardName(board);
const card = await trello.createKnowledgeCard({
title,
content,
tags,
category
});
return {
content: [
{
type: 'text',
text: `✅ Knowledge stored successfully in **${boardName}** board!\n\n**Title:** ${title}\n**ID:** ${card.id}\n**Tags:** ${tags.join(', ') || 'None'}\n**Category:** ${category || 'General'}\n\nYou can retrieve this later by searching for keywords or using the ID.`
}
]
};
}
async handleSearchKnowledge(args) {
const { board, query, category, tags } = args;
const trello = this.getTrelloClient(board);
const boardName = this.getBoardName(board);
const results = await trello.searchKnowledge({
query,
category,
tags
});
if (results.length === 0) {
return {
content: [
{
type: 'text',
text: `No knowledge found in **${boardName}** board for query: "${query}"`
}
]
};
}
const formattedResults = results.map(item =>
`**${item.title}**\n` +
`ID: ${item.id}\n` +
`Tags: ${item.tags.join(', ') || 'None'}\n` +
`Preview: ${item.content.substring(0, 150)}${item.content.length > 150 ? '...' : ''}\n`
).join('\n---\n');
return {
content: [
{
type: 'text',
text: `Found ${results.length} knowledge item(s) in **${boardName}** board:\n\n${formattedResults}`
}
]
};
}
async handleGetKnowledge(args) {
const { board, id } = args;
const trello = this.getTrelloClient(board);
const boardName = this.getBoardName(board);
const item = await trello.getKnowledgeById(id);
return {
content: [
{
type: 'text',
text: `**${item.title}** (from **${boardName}** board)\n\n${item.content}\n\n**Tags:** ${item.tags.join(', ') || 'None'}\n**Category:** ${item.category || 'General'}`
}
]
};
}
async handleListTopics(args) {
const { board } = args;
const trello = this.getTrelloClient(board);
const boardName = this.getBoardName(board);
const topics = await trello.getAllTopics();
return {
content: [
{
type: 'text',
text: `Available topics/categories in **${boardName}** board:\n\n${topics.map(topic => `• ${topic}`).join('\n')}`
}
]
};
}
async handleUpdateKnowledge(args) {
const { board, id, title, content, tags } = args;
const trello = this.getTrelloClient(board);
const boardName = this.getBoardName(board);
const updatedCard = await trello.updateKnowledgeCard(id, {
title,
content,
tags
});
return {
content: [
{
type: 'text',
text: `✅ Knowledge updated successfully in **${boardName}** board!\n\n**Title:** ${updatedCard.title}\n**ID:** ${updatedCard.id}`
}
]
};
}
async handleListBoards(args) {
const boardsList = Object.entries(this.boardConfigs)
.map(([key, config]) => `• **${config.name}** (${key}) - ID: ${config.boardId}`)
.join('\n');
return {
content: [
{
type: 'text',
text: `Configured Trello boards:\n\n${boardsList}\n\nUse the board key (in parentheses) when calling other tools.`
}
]
};
}
async handleReadCardFromUrl(args) {
const { url } = args;
const cardId = TrelloClient.parseCardIdFromUrl(url);
const trello = this.trelloClients.values().next().value;
const card = await trello.getCardById(cardId);
const labelsText = card.labels.length > 0
? card.labels.map(l => `${l.name}${l.color ? ` (${l.color})` : ''}`).join(', ')
: 'None';
const membersText = card.members.length > 0
? card.members.map(m => m.fullName || m.username).join(', ')
: 'None';
const dueText = card.due ? new Date(card.due).toLocaleString() : 'Not set';
return {
content: [
{
type: 'text',
text: `# ${card.title}
**Board:** ${card.board?.name || 'Unknown'}
**List:** ${card.list?.name || 'Unknown'}
**Labels:** ${labelsText}
**Members:** ${membersText}
**Due Date:** ${dueText}
**URL:** ${card.shortUrl}
## Description
${card.description || '*No description*'}`
}
]
};
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Trello MCP server running on stdio');
}
}
// Start the server
const server = new TrelloMCPServer();
server.run().catch(console.error);