Scrapbox MCP Server

  • src
#!/usr/bin/env node /** * Scrapbox MCP Server * * This MCP server provides tools to fetch content from Scrapbox pages. * It allows retrieving page content by URL and extracting useful information. */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js"; import axios from "axios"; /** * Type definitions for Scrapbox API responses */ interface ScrapboxPage { id: string; title: string; created: number; updated: number; accessed: number; image: string; descriptions: string[]; user: { id: string; name: string; displayName: string; }; pin: number; views: number; linked: number; commitId: string; persistent: boolean; lines: ScrapboxLine[]; } interface ScrapboxLine { id: string; text: string; userId: string; created: number; updated: number; } /** * Type definition for the get_page_content tool input */ interface GetPageContentArgs { url: string; } /** * Validates if the input is a valid GetPageContentArgs object */ const isValidGetPageContentArgs = (args: any): args is GetPageContentArgs => { return typeof args === "object" && args !== null && typeof args.url === "string"; }; /** * Extracts project name and page title from a Scrapbox URL * * @param url Scrapbox URL (e.g., https://scrapbox.io/project-name/page-title) * @returns Object containing projectName and pageTitle */ function extractScrapboxInfo(url: string): { projectName: string; pageTitle: string } { console.error("[URL] Processing URL:", url); try { const urlObj = new URL(url); // Validate that this is a Scrapbox URL if (urlObj.hostname !== "scrapbox.io") { throw new McpError( ErrorCode.InvalidParams, "Invalid URL: Not a Scrapbox URL" ); } // Extract project name and page title from path const pathParts = urlObj.pathname.split("/").filter(part => part); if (pathParts.length < 2) { throw new McpError( ErrorCode.InvalidParams, "Invalid URL format: Missing project name or page title" ); } const projectName = pathParts[0]; const pageTitle = decodeURIComponent(pathParts[1]); console.error("[URL] Extracted project:", projectName, "page:", pageTitle); return { projectName, pageTitle }; } catch (error) { if (error instanceof McpError) { throw error; } console.error("[Error] URL parsing failed:", error); throw new McpError( ErrorCode.InvalidParams, `Invalid URL format: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Fetches page content from Scrapbox API * * @param projectName Scrapbox project name * @param pageTitle Page title * @returns Formatted page content */ async function fetchScrapboxPage(projectName: string, pageTitle: string): Promise<string> { const apiUrl = `https://scrapbox.io/api/pages/${encodeURIComponent(projectName)}/${encodeURIComponent(pageTitle)}`; console.error("[API] Request to endpoint:", apiUrl); try { const response = await axios.get<ScrapboxPage>(apiUrl); const page = response.data; // Format the page content let formattedContent = `# ${page.title}\n\n`; // Add descriptions if available if (page.descriptions && page.descriptions.length > 0) { formattedContent += "## 概要\n"; formattedContent += page.descriptions.join("\n") + "\n\n"; } // Add content from lines formattedContent += "## 内容\n"; formattedContent += page.lines .slice(1) // Skip the first line (title) .map(line => line.text) .join("\n"); // Add metadata formattedContent += "\n\n## メタデータ\n"; formattedContent += `- 作成日時: ${new Date(page.created).toISOString()}\n`; formattedContent += `- 更新日時: ${new Date(page.updated).toISOString()}\n`; formattedContent += `- 閲覧数: ${page.views}\n`; formattedContent += `- リンク数: ${page.linked}\n`; return formattedContent; } catch (error) { console.error("[Error] API request failed:", error); if (axios.isAxiosError(error)) { if (error.response?.status === 404) { throw new McpError( ErrorCode.InvalidParams, "Page not found: The requested Scrapbox page does not exist" ); } throw new McpError( ErrorCode.InternalError, `Scrapbox API error: ${error.response?.data?.message || error.message}` ); } throw new McpError( ErrorCode.InternalError, `Failed to fetch Scrapbox page: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Create an MCP server with tools capability for fetching Scrapbox content */ const server = new Server( { name: "scrapbox-mcp", version: "0.1.0", }, { capabilities: { tools: {}, }, } ); /** * Handler for listing available tools * Exposes a single "get_page_content" tool that fetches Scrapbox page content */ server.setRequestHandler(ListToolsRequestSchema, async () => { console.error("[Setup] Registering tools"); return { tools: [ { name: "get_page_content", description: "Fetch content from a Scrapbox page by URL", inputSchema: { type: "object", properties: { url: { type: "string", description: "Scrapbox page URL (e.g., https://scrapbox.io/project-name/page-title)" } }, required: ["url"] } } ] }; }); /** * Handler for the get_page_content tool * Fetches content from a Scrapbox page and returns it formatted */ server.setRequestHandler(CallToolRequestSchema, async (request) => { console.error("[Tool] Called:", request.params.name); if (request.params.name !== "get_page_content") { throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } if (!isValidGetPageContentArgs(request.params.arguments)) { throw new McpError( ErrorCode.InvalidParams, "Invalid arguments: 'url' parameter is required and must be a string" ); } try { const { url } = request.params.arguments; const { projectName, pageTitle } = extractScrapboxInfo(url); const content = await fetchScrapboxPage(projectName, pageTitle); return { content: [ { type: "text", text: content } ] }; } catch (error) { console.error("[Error] Tool execution failed:", error); if (error instanceof McpError) { return { content: [ { type: "text", text: error.message } ], isError: true }; } return { content: [ { type: "text", text: `Error fetching Scrapbox content: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } }); /** * Start the server using stdio transport */ async function main() { console.error("[Setup] Initializing Scrapbox MCP server"); const transport = new StdioServerTransport(); await server.connect(transport); console.error("[Setup] Scrapbox MCP server running"); // Handle process termination process.on("SIGINT", async () => { console.error("[Setup] Shutting down server"); await server.close(); process.exit(0); }); } main().catch((error) => { console.error("[Fatal] Server error:", error); process.exit(1); });