Skip to main content
Glama
index.ts14.2 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from '@modelcontextprotocol/sdk/types.js'; import { FocalboardClient } from './focalboard-client.js'; import { FocalboardConfig } from './types.js'; // Get configuration from environment variables const config: FocalboardConfig = { host: process.env.FOCALBOARD_HOST || '', username: process.env.FOCALBOARD_USERNAME || '', password: process.env.FOCALBOARD_PASSWORD || '' }; // Validate configuration if (!config.host || !config.username || !config.password) { console.error('Error: Missing required environment variables'); console.error('Required: FOCALBOARD_HOST, FOCALBOARD_USERNAME, FOCALBOARD_PASSWORD'); process.exit(1); } // Initialize Focalboard client const focalboard = new FocalboardClient(config); // Define MCP tools const tools: Tool[] = [ { name: 'list_boards', description: 'List all boards for a team. Returns an array of boards with their IDs, titles, and properties.', inputSchema: { type: 'object', properties: { teamId: { type: 'string', description: 'The team ID to list boards for (default: "0" for default team)', default: '0' } } } }, { name: 'get_board', description: 'Get detailed information about a specific board, including all its columns and property definitions.', inputSchema: { type: 'object', properties: { boardId: { type: 'string', description: 'The ID of the board to retrieve' } }, required: ['boardId'] } }, { name: 'search_boards', description: 'Search for boards by name or keyword within a team.', inputSchema: { type: 'object', properties: { teamId: { type: 'string', description: 'The team ID to search within (default: "0" for default team)', default: '0' }, searchTerm: { type: 'string', description: 'The search term to find boards' } }, required: ['searchTerm'] } }, { name: 'create_card', description: 'Create a new card (task) in a board. You can set the title, properties, description, and column placement.', inputSchema: { type: 'object', properties: { boardId: { type: 'string', description: 'The ID of the board to create the card in' }, title: { type: 'string', description: 'The title/name of the card' }, properties: { type: 'object', description: 'Property values for the card (e.g., {"Status": "To Do", "Priority": "High"}). Use property names, not IDs.', additionalProperties: { type: 'string' } }, description: { type: 'string', description: 'Optional description/content for the card in markdown format' } }, required: ['boardId', 'title'] } }, { name: 'get_cards', description: 'List all cards (tasks) in a board with pagination support.', inputSchema: { type: 'object', properties: { boardId: { type: 'string', description: 'The ID of the board to list cards from' }, page: { type: 'number', description: 'Page number for pagination (default: 0)', default: 0 }, perPage: { type: 'number', description: 'Number of cards per page (default: 100)', default: 100 } }, required: ['boardId'] } }, { name: 'get_card', description: 'Get detailed information about a specific card by its ID.', inputSchema: { type: 'object', properties: { cardId: { type: 'string', description: 'The ID of the card to retrieve' } }, required: ['cardId'] } }, { name: 'update_card', description: 'Update a card\'s properties, including moving it to different columns. Accepts human-readable property and column names.', inputSchema: { type: 'object', properties: { cardId: { type: 'string', description: 'The ID of the card to update' }, boardId: { type: 'string', description: 'The ID of the board the card belongs to' }, title: { type: 'string', description: 'New title for the card (optional)' }, properties: { type: 'object', description: 'Property values to update (e.g., {"Status": "In Progress", "Priority": "High"}). Use property names, not IDs.', additionalProperties: { type: 'string' } }, description: { type: 'string', description: 'Update or set the description/content for the card in markdown format (optional)' } }, required: ['cardId', 'boardId'] } }, { name: 'delete_card', description: 'Delete a card (task) from a board permanently.', inputSchema: { type: 'object', properties: { cardId: { type: 'string', description: 'The ID of the card to delete' }, boardId: { type: 'string', description: 'The ID of the board the card belongs to' } }, required: ['cardId', 'boardId'] } }, { name: 'add_card_description', description: 'Add or set description/content to a card. Creates a new text block with markdown content.', inputSchema: { type: 'object', properties: { cardId: { type: 'string', description: 'The ID of the card to add description to' }, boardId: { type: 'string', description: 'The ID of the board the card belongs to' }, description: { type: 'string', description: 'The description content in markdown format' } }, required: ['cardId', 'boardId', 'description'] } }, { name: 'get_card_content', description: 'Get all content blocks (descriptions) for a card.', inputSchema: { type: 'object', properties: { cardId: { type: 'string', description: 'The ID of the card to get content for' } }, required: ['cardId'] } } ]; // Create MCP server const server = new Server( { name: 'focalboard-mcp-server', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); // Handle tool listing server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools }; }); // Handle tool execution server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; switch (name) { // ==================== // Board Tools // ==================== case 'list_boards': { const teamId = (args?.teamId as string) || '0'; const boards = await focalboard.listBoards(teamId); return { content: [ { type: 'text', text: JSON.stringify(boards, null, 2) } ] }; } case 'get_board': { const boardId = args?.boardId as string; if (!boardId) { throw new Error('boardId is required'); } const board = await focalboard.getBoard(boardId); return { content: [ { type: 'text', text: JSON.stringify(board, null, 2) } ] }; } case 'search_boards': { const teamId = (args?.teamId as string) || '0'; const searchTerm = args?.searchTerm as string; if (!searchTerm) { throw new Error('searchTerm is required'); } const boards = await focalboard.searchBoards(teamId, searchTerm); return { content: [ { type: 'text', text: JSON.stringify(boards, null, 2) } ] }; } // ==================== // Card Tools // ==================== case 'create_card': { const boardId = args?.boardId as string; const title = args?.title as string; const properties = (args?.properties as Record<string, string>) || {}; const description = args?.description as string | undefined; if (!boardId || !title) { throw new Error('boardId and title are required'); } // Create the card first const cardData: any = { title, fields: { properties: {}, contentOrder: [] } }; let card = await focalboard.createCard(boardId, cardData); // If properties are provided, update the card with them if (Object.keys(properties).length > 0) { card = await focalboard.updateCardProperties(card.id, boardId, properties); } // If description is provided, add it as a text block if (description) { await focalboard.createTextBlock(boardId, card.id, description); // Refresh card to get updated contentOrder card = await focalboard.getCard(card.id); } return { content: [ { type: 'text', text: JSON.stringify(card, null, 2) } ] }; } case 'get_cards': { const boardId = args?.boardId as string; const page = (args?.page as number) || 0; const perPage = (args?.perPage as number) || 100; if (!boardId) { throw new Error('boardId is required'); } const cards = await focalboard.getCards(boardId, page, perPage); return { content: [ { type: 'text', text: JSON.stringify(cards, null, 2) } ] }; } case 'get_card': { const cardId = args?.cardId as string; if (!cardId) { throw new Error('cardId is required'); } const card = await focalboard.getCard(cardId); return { content: [ { type: 'text', text: JSON.stringify(card, null, 2) } ] }; } case 'update_card': { const cardId = args?.cardId as string; const boardId = args?.boardId as string; const title = args?.title as string; const properties = (args?.properties as Record<string, string>) || {}; const description = args?.description as string | undefined; if (!cardId || !boardId) { throw new Error('cardId and boardId are required'); } if (!title && Object.keys(properties).length === 0 && !description) { throw new Error('Either title, properties, or description must be provided'); } let card; // Update title if provided if (title) { card = await focalboard.updateCard(boardId, cardId, { title }); } // Update properties if provided if (Object.keys(properties).length > 0) { card = await focalboard.updateCardProperties(cardId, boardId, properties); } // Update description if provided if (description) { await focalboard.setCardDescription(boardId, cardId, description); } // Fetch the updated card card = await focalboard.getCard(cardId); return { content: [ { type: 'text', text: JSON.stringify(card, null, 2) } ] }; } case 'delete_card': { const cardId = args?.cardId as string; const boardId = args?.boardId as string; if (!cardId || !boardId) { throw new Error('cardId and boardId are required'); } await focalboard.deleteCard(boardId, cardId); return { content: [ { type: 'text', text: JSON.stringify({ success: true, message: 'Card deleted successfully' }) } ] }; } case 'add_card_description': { const cardId = args?.cardId as string; const boardId = args?.boardId as string; const description = args?.description as string; if (!cardId || !boardId || !description) { throw new Error('cardId, boardId, and description are required'); } await focalboard.setCardDescription(boardId, cardId, description); const card = await focalboard.getCard(cardId); return { content: [ { type: 'text', text: JSON.stringify(card, null, 2) } ] }; } case 'get_card_content': { const cardId = args?.cardId as string; if (!cardId) { throw new Error('cardId is required'); } const contentBlocks = await focalboard.getCardContent(cardId); return { content: [ { type: 'text', text: JSON.stringify(contentBlocks, null, 2) } ] }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: JSON.stringify({ error: errorMessage }, null, 2) } ], isError: true }; } }); // Start the server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('Focalboard MCP Server running on stdio'); } main().catch((error) => { console.error('Server error:', error); process.exit(1); });

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/gmjuhasz/focalboard-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server