mcp-miro

#!/usr/bin/env node import yargs from "yargs/yargs"; import { hideBin } from "yargs/helpers"; import { MiroClient } from "./MiroClient.js"; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import fs from 'fs/promises'; import path from 'path'; // Parse command line arguments const argv = await yargs(hideBin(process.argv)) .option("token", { alias: "t", type: "string", description: "Miro OAuth token", }) .help().argv; // Get token with precedence: command line > environment variable const oauthToken = (argv.token as string) || process.env.MIRO_OAUTH_TOKEN; if (!oauthToken) { console.error( "Error: Miro OAuth token is required. Provide it via MIRO_OAUTH_TOKEN environment variable or --token argument" ); process.exit(1); } const server = new Server( { name: "mcp-miro", version: "0.1.0", }, { capabilities: { resources: {}, tools: {}, prompts: {}, }, } ); const miroClient = new MiroClient(oauthToken); server.setRequestHandler(ListResourcesRequestSchema, async () => { const boards = await miroClient.getBoards(); return { resources: boards.map((board) => ({ uri: `miro://board/${board.id}`, mimeType: "application/json", name: board.name, description: board.description || `Miro board: ${board.name}`, })), }; }); server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const url = new URL(request.params.uri); if (!request.params.uri.startsWith("miro://board/")) { throw new Error( "Invalid Miro resource URI - must start with miro://board/" ); } const boardId = url.pathname.substring(1); // Remove leading slash from pathname const items = await miroClient.getBoardItems(boardId); return { contents: [ { uri: request.params.uri, mimeType: "application/json", text: JSON.stringify(items, null, 2), }, ], }; }); server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "list_boards", description: "List all available Miro boards and their IDs", inputSchema: { type: "object", properties: {}, }, }, { name: "create_sticky_note", description: "Create a sticky note on a Miro board. By default, sticky notes are 199x228 and available in these colors: gray, light_yellow, yellow, orange, light_green, green, dark_green, cyan, light_pink, pink, violet, red, light_blue, blue, dark_blue, black.", inputSchema: { type: "object", properties: { boardId: { type: "string", description: "ID of the board to create the sticky note on", }, content: { type: "string", description: "Text content of the sticky note", }, color: { type: "string", description: "Color of the sticky note (e.g. 'yellow', 'blue', 'pink')", enum: [ "gray", "light_yellow", "yellow", "orange", "light_green", "green", "dark_green", "cyan", "light_pink", "pink", "violet", "red", "light_blue", "blue", "dark_blue", "black", ], default: "yellow", }, x: { type: "number", description: "X coordinate position", default: 0, }, y: { type: "number", description: "Y coordinate position", default: 0, }, }, required: ["boardId", "content"], }, }, { name: "bulk_create_items", description: "Create multiple items on a Miro board in a single transaction (max 20 items)", inputSchema: { type: "object", properties: { boardId: { type: "string", description: "ID of the board to create the items on", }, items: { type: "array", description: "Array of items to create", items: { type: "object", properties: { type: { type: "string", enum: [ "app_card", "text", "shape", "sticky_note", "image", "document", "card", "frame", "embed", ], description: "Type of item to create", }, data: { type: "object", description: "Item-specific data configuration", }, style: { type: "object", description: "Item-specific style configuration", }, position: { type: "object", description: "Item position configuration", }, geometry: { type: "object", description: "Item geometry configuration", }, parent: { type: "object", description: "Parent item configuration", }, }, required: ["type"], }, minItems: 1, maxItems: 20, }, }, required: ["boardId", "items"], }, }, { name: "get_frames", description: "Get all frames from a Miro board", inputSchema: { type: "object", properties: { boardId: { type: "string", description: "ID of the board to get frames from", }, }, required: ["boardId"], }, }, { name: "get_items_in_frame", description: "Get all items contained within a specific frame on a Miro board", inputSchema: { type: "object", properties: { boardId: { type: "string", description: "ID of the board that contains the frame", }, frameId: { type: "string", description: "ID of the frame to get items from", }, }, required: ["boardId", "frameId"], }, }, { name: "create_shape", description: "Create a shape on a Miro board. Available shapes include basic shapes (rectangle, circle, etc.) and flowchart shapes (process, decision, etc.). Standard geometry specs: width and height in pixels (default 200x200)", inputSchema: { type: "object", properties: { boardId: { type: "string", description: "ID of the board to create the shape on", }, content: { type: "string", description: "Text content to display on the shape", }, shape: { type: "string", description: "Type of shape to create", enum: [ // Basic shapes "rectangle", "round_rectangle", "circle", "triangle", "rhombus", "parallelogram", "trapezoid", "pentagon", "hexagon", "octagon", "wedge_round_rectangle_callout", "star", "flow_chart_predefined_process", "cloud", "cross", "can", "right_arrow", "left_arrow", "left_right_arrow", "left_brace", "right_brace", // Flowchart shapes "flow_chart_connector", "flow_chart_magnetic_disk", "flow_chart_input_output", "flow_chart_decision", "flow_chart_delay", "flow_chart_display", "flow_chart_document", "flow_chart_magnetic_drum", "flow_chart_internal_storage", "flow_chart_manual_input", "flow_chart_manual_operation", "flow_chart_merge", "flow_chart_multidocuments", "flow_chart_note_curly_left", "flow_chart_note_curly_right", "flow_chart_note_square", "flow_chart_offpage_connector", "flow_chart_or", "flow_chart_predefined_process_2", "flow_chart_preparation", "flow_chart_process", "flow_chart_online_storage", "flow_chart_summing_junction", "flow_chart_terminator", ], default: "rectangle", }, style: { type: "object", description: "Style configuration for the shape", properties: { borderColor: { type: "string" }, borderOpacity: { type: "number", minimum: 0, maximum: 1 }, borderStyle: { type: "string", enum: ["normal", "dotted", "dashed"], }, borderWidth: { type: "number", minimum: 1, maximum: 24 }, color: { type: "string" }, fillColor: { type: "string" }, fillOpacity: { type: "number", minimum: 0, maximum: 1 }, fontFamily: { type: "string" }, fontSize: { type: "number", minimum: 10, maximum: 288 }, textAlign: { type: "string", enum: ["left", "center", "right"], }, textAlignVertical: { type: "string", enum: ["top", "middle", "bottom"], }, }, }, position: { type: "object", properties: { x: { type: "number", default: 0 }, y: { type: "number", default: 0 }, origin: { type: "string", default: "center" }, }, }, geometry: { type: "object", properties: { width: { type: "number", default: 200 }, height: { type: "number", default: 200 }, rotation: { type: "number", default: 0 }, }, }, }, required: ["boardId", "shape"], }, }, ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { switch (request.params.name) { case "list_boards": { const boards = await miroClient.getBoards(); return { content: [ { type: "text", text: "Here are the available Miro boards:", }, ...boards.map((b) => ({ type: "text", text: `Board ID: ${b.id}, Name: ${b.name}`, })), ], }; } case "create_sticky_note": { const { boardId, content, color = "yellow", x = 0, y = 0, } = request.params.arguments as any; const stickyNote = await miroClient.createStickyNote(boardId, { data: { content: content, }, style: { fillColor: color, }, position: { x: x, y: y, }, }); return { content: [ { type: "text", text: `Created sticky note ${stickyNote.id} on board ${boardId}`, }, ], }; } case "bulk_create_items": { const { boardId, items } = request.params.arguments as any; const createdItems = await miroClient.bulkCreateItems(boardId, items); return { content: [ { type: "text", text: `Created ${createdItems.length} items on board ${boardId}`, }, ], }; } case "get_frames": { const { boardId } = request.params.arguments as any; const frames = await miroClient.getFrames(boardId); return { content: [ { type: "text", text: JSON.stringify(frames, null, 2), }, ], }; } case "get_items_in_frame": { const { boardId, frameId } = request.params.arguments as any; const items = await miroClient.getItemsInFrame(boardId, frameId); return { content: [ { type: "text", text: JSON.stringify(items, null, 2), }, ], }; } case "create_shape": { const { boardId, shape, content, style, position, geometry } = request .params.arguments as any; const shapeItem = await miroClient.createShape(boardId, { data: { shape: shape, content: content, }, style: style || {}, position: position || { x: 0, y: 0 }, geometry: geometry || { width: 200, height: 200, rotation: 0 }, }); return { content: [ { type: "text", text: `Created ${shape} shape with ID ${shapeItem.id} on board ${boardId}`, }, ], }; } default: throw new Error("Unknown tool"); } }); server.setRequestHandler(ListPromptsRequestSchema, async () => { return { prompts: [ { name: "Working with MIRO", description: "Basic prompt for working with MIRO boards", }, ], }; }); server.setRequestHandler(GetPromptRequestSchema, async (request) => { if (request.params.name === "Working with MIRO") { const keyFactsPath = path.join(process.cwd(), 'resources', 'boards-key-facts.md'); const keyFacts = await fs.readFile(keyFactsPath, 'utf-8'); return { messages: [ { role: "user", content: { type: "text", text: keyFacts, }, }, ], }; } throw new Error("Unknown prompt"); }); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); } main().catch((error) => { console.error("Server error:", error); process.exit(1); });