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);
});