UNPKG

@edicarlos.lds/businessmap-mcp

Version:

Model Context Protocol server for BusinessMap (Kanbanize) integration

282 lines 13.5 kB
import { createBoardSchema, createColumnSchema, createColumnInputSchema, createLaneSchema, deleteColumnSchema, getBoardSchema, getCurrentBoardStructureSchema, getLaneSchema, listBoardsSchema, searchBoardSchema, updateColumnSchema, } from '../../schemas/index.js'; import { logger } from '../../utils/logger.js'; import { createErrorResponse, createSuccessResponse } from './base-tool.js'; export class BoardToolHandler { registerTools(server, client, readOnlyMode) { this.registerListBoards(server, client); this.registerSearchBoard(server, client); this.registerGetColumns(server, client); this.registerGetLanes(server, client); this.registerGetLane(server, client); this.registerGetCurrentBoardStructure(server, client); if (!readOnlyMode) { this.registerCreateBoard(server, client); this.registerCreateLane(server, client); this.registerCreateColumn(server, client); this.registerUpdateColumn(server, client); this.registerDeleteColumn(server, client); } } registerListBoards(server, client) { server.registerTool('list_boards', { title: 'List Boards', description: 'Get a list of boards with optional filters', inputSchema: listBoardsSchema.shape, annotations: { readOnlyHint: true, idempotentHint: true }, }, async (params) => { try { const boards = await client.getBoards(params); return createSuccessResponse(boards); } catch (error) { return createErrorResponse(error, 'fetching boards'); } }); } registerSearchBoard(server, client) { server.registerTool('search_board', { title: 'Search Board', description: 'Search for a board by ID or name, with intelligent fallback to list all boards if direct search fails', inputSchema: searchBoardSchema.shape, annotations: { readOnlyHint: true, idempotentHint: true }, }, async ({ board_id, board_name, workspace_id }) => { try { if (board_id) { return await this.searchBoardById(client, board_id, workspace_id); } if (board_name) { return await this.searchBoardByName(client, board_name, workspace_id); } // If neither ID nor name provided, list all boards return await this.getAllBoards(client, workspace_id); } catch (error) { return createErrorResponse(error, 'searching for board'); } }); } async searchBoardById(client, boardId, workspaceId) { try { const [board, structure] = await Promise.all([ client.getBoard(boardId), client.getBoardStructure(boardId), ]); return createSuccessResponse({ ...board, structure }, 'Board found directly:'); } catch (directError) { logger.warn(`Direct board lookup failed for ID ${boardId}: ${directError instanceof Error ? directError.message : 'Unknown error'}`); return await this.searchBoardByIdFallback(client, boardId, workspaceId); } } async searchBoardByIdFallback(client, boardId, workspaceId) { const boards = await client.getBoards(workspaceId ? { workspace_id: workspaceId } : undefined); const foundBoard = boards.find((b) => b.board_id === boardId); if (!foundBoard) { return createErrorResponse(new Error(`Board with ID ${boardId} not found. Available boards:\n${JSON.stringify(this.formatBoardsList(boards), null, 2)}`), 'searching for board'); } return await this.getBoardWithStructure(client, foundBoard, 'Board found via list search:'); } async searchBoardByName(client, boardName, workspaceId) { const boards = await client.getBoards(workspaceId ? { workspace_id: workspaceId } : undefined); const foundBoards = boards.filter((b) => b.name.toLowerCase().includes(boardName.toLowerCase())); if (foundBoards.length === 0) { return createErrorResponse(new Error(`No boards found matching name "${boardName}". Available boards:\n${JSON.stringify(this.formatBoardsList(boards), null, 2)}`), 'searching for board by name'); } if (foundBoards.length === 1) { const foundBoard = foundBoards[0]; if (!foundBoard.board_id) { return createErrorResponse(new Error('Board missing board_id'), 'board validation'); } return await this.getBoardWithStructure(client, foundBoard, 'Board found by name:'); } return createSuccessResponse(this.formatBoardsList(foundBoards), `Multiple boards found matching "${boardName}":`); } async getAllBoards(client, workspaceId) { const boards = await client.getBoards(workspaceId ? { workspace_id: workspaceId } : undefined); return createSuccessResponse(this.formatBoardsList(boards), 'All available boards:'); } async getBoardWithStructure(client, board, successMessage) { try { const structure = await client.getBoardStructure(board.board_id); return createSuccessResponse({ ...board, structure }, successMessage); } catch (structureError) { logger.warn(`Structure lookup failed for board ID ${board.board_id}: ${structureError instanceof Error ? structureError.message : 'Unknown error'}`); return createSuccessResponse(board, `Board found but structure unavailable. Structure error: ${structureError instanceof Error ? structureError.message : 'Unknown error'}`); } } formatBoardsList(boards) { return boards.map((b) => ({ board_id: b.board_id, name: b.name, workspace_id: b.workspace_id, })); } registerGetColumns(server, client) { server.registerTool('get_columns', { title: 'Get Board Columns', description: 'Get all columns for a board', inputSchema: getBoardSchema.shape, annotations: { readOnlyHint: true, idempotentHint: true }, }, async ({ board_id }) => { try { const columns = await client.getColumns(board_id); return createSuccessResponse(columns); } catch (error) { return createErrorResponse(error, 'fetching board columns'); } }); } registerGetLanes(server, client) { server.registerTool('get_lanes', { title: 'Get Board Lanes', description: 'Get all lanes/swimlanes for a board', inputSchema: getBoardSchema.shape, annotations: { readOnlyHint: true, idempotentHint: true }, }, async ({ board_id }) => { try { const lanes = await client.getLanes(board_id); return createSuccessResponse(lanes); } catch (error) { return createErrorResponse(error, 'fetching board lanes'); } }); } registerGetLane(server, client) { server.registerTool('get_lane', { title: 'Get Lane Details', description: 'Get details of a specific lane/swimlane', inputSchema: getLaneSchema.shape, annotations: { readOnlyHint: true, idempotentHint: true }, }, async ({ lane_id }) => { try { const lane = await client.getLane(lane_id); return createSuccessResponse(lane); } catch (error) { return createErrorResponse(error, 'fetching lane details'); } }); } registerCreateBoard(server, client) { server.registerTool('create_board', { title: 'Create Board', description: 'Create a new board in a workspace', inputSchema: createBoardSchema.shape, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }, }, async ({ name, workspace_id, description }) => { try { const board = await client.createBoard({ name, workspace_id, description, }); return createSuccessResponse(board, 'Board created successfully:'); } catch (error) { return createErrorResponse(error, 'creating board'); } }); } registerCreateLane(server, client) { server.registerTool('create_lane', { title: 'Create Lane', description: 'Create a new lane/swimlane in a board', inputSchema: createLaneSchema.shape, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }, }, async ({ workflow_id, name, description, color, position }) => { try { const lane = await client.createLane({ workflow_id, name, description: description || null, color, position, }); return createSuccessResponse(lane, 'Lane created successfully:'); } catch (error) { return createErrorResponse(error, 'creating lane'); } }); } registerGetCurrentBoardStructure(server, client) { server.registerTool('get_current_board_structure', { title: 'Get Current Board Structure', description: 'Get the complete current structure of a board including workflows, columns, lanes, and configurations', inputSchema: getCurrentBoardStructureSchema.shape, annotations: { readOnlyHint: true, idempotentHint: true }, }, async ({ board_id }) => { try { const structure = await client.getCurrentBoardStructure(board_id); return createSuccessResponse(structure, 'Board structure retrieved successfully:'); } catch (error) { return createErrorResponse(error, 'getting current board structure'); } }); } registerCreateColumn(server, client) { server.registerTool('create_column', { title: 'Create Column', description: 'Create a new column on a board. Supports both main columns (requires workflow_id and section) and sub-columns (requires parent_column_id). Section values: 1=Backlog, 2=Requested, 3=Progress, 4=Done.', inputSchema: createColumnInputSchema.shape, }, async (params) => { try { const { board_id, workflow_id, section, parent_column_id, position, name, limit, description, } = createColumnSchema.parse(params); const columnParams = parent_column_id ? { parent_column_id, position, name, ...(limit !== undefined && { limit }), ...(description && { description }) } : { workflow_id, section, position, name, ...(limit !== undefined && { limit }), ...(description && { description }) }; const column = await client.createColumn(board_id, columnParams); return createSuccessResponse(column, 'Column created successfully:'); } catch (error) { return createErrorResponse(error, 'creating column'); } }); } registerUpdateColumn(server, client) { server.registerTool('update_column', { title: 'Update Column', description: 'Update the details of a specific column on a board', inputSchema: updateColumnSchema.shape, }, async ({ board_id, column_id, name, limit, section, position, description }) => { try { const params = {}; if (name !== undefined) params['name'] = name; if (limit !== undefined) params['limit'] = limit; if (section !== undefined) params['section'] = section; if (position !== undefined) params['position'] = position; if (description !== undefined) params['description'] = description; const column = await client.updateColumn(board_id, column_id, params); return createSuccessResponse(column, 'Column updated successfully:'); } catch (error) { return createErrorResponse(error, 'updating column'); } }); } registerDeleteColumn(server, client) { server.registerTool('delete_column', { title: 'Delete Column', description: 'Delete a column from a board', inputSchema: deleteColumnSchema.shape, }, async ({ board_id, column_id }) => { try { await client.deleteColumn(board_id, column_id); return createSuccessResponse({ board_id, column_id }, 'Column deleted successfully:'); } catch (error) { return createErrorResponse(error, 'deleting column'); } }); } } //# sourceMappingURL=board-tools.js.map