Skip to main content
Glama

Scrapbox Cosense MCP Server

by worldnine
cosense.ts11.4 kB
import { fetch } from "@whatwg-node/fetch"; import { sortPages } from './utils/sort.js'; const API_DOMAIN = process.env.API_DOMAIN || "scrapbox.io"; // /api/pages/:projectname/search/query の型定義 type SearchQueryResponse = { projectName: string; // data取得先のproject名 searchQuery: string; // 検索語句 query: { words: string[]; // AND検索に使った語句 excludes: string[]; // NOT検索に使った語句 }; limit: number; // 検索件数の上限 count: number; // 検索件数 existsExactTitleMatch: boolean; backend: 'elasticsearch'; pages: { id: string; title: string; image: string; words: string[]; lines: string[]; created?: number; updated?: number; user?: { id: string; name: string; displayName: string; photo: string; }; lastUpdateUser?: { id: string; name: string; displayName: string; photo: string; }; collaborators?: { id: string; name: string; displayName: string; photo: string; }[]; }[]; debug?: { // デバッグ情報を追加 request_url?: string; query?: string; total_results?: number; error?: string; }; }; // /api/pages/:projectname/:pagetitle type GetPageResponse = { id: string; title: string; lines: { id: string; text: string; userId: string; created: number; updated: number; }[]; created: number; updated: number; links: string[]; relatedPages: { links1hop: { title: string; descriptions: string[]; }[]; }; user: { // 追加: 最新の編集者情報 id: string; name: string; displayName: string; photo: string; }; lastUpdateUser?: { id: string; name: string; displayName: string; photo: string; } | undefined; collaborators: { id: string; name: string; displayName: string; photo: string; }[]; debug?: { error?: string; warning?: string; } | undefined; }; async function getPage( projectName: string, pageName: string, sid?: string, ): Promise<GetPageResponse | null> { try { const url = `https://${API_DOMAIN}/api/pages/${projectName}/${encodeURIComponent(pageName)}`; const response = sid ? await fetch(url, { headers: { Cookie: `connect.sid=${sid}` }, }) : await fetch(url); if (!response.ok) { return null; } const page = await response.json(); // レスポンスの型チェック if (!page || typeof page !== 'object') { return null; } const typedPage = page as GetPageResponse; if (!Array.isArray(typedPage.lines)) { return { ...typedPage, debug: { error: 'Invalid page response format: lines is not an array' } }; } // userとlastUpdateUserの整合性チェック if (!typedPage.user && typedPage.lastUpdateUser) { // lastUpdateUserが存在するがuserが存在しない場合 return { ...typedPage, user: typedPage.lastUpdateUser, debug: { warning: `Using lastUpdateUser as fallback for user information on page: ${typedPage.title}` } }; } else if (!typedPage.user) { // どちらの情報も存在しない場合 return { ...typedPage, debug: { warning: `Missing both user and lastUpdateUser information for page: ${typedPage.title}` } }; } return typedPage; } catch (error) { return null; } } function toReadablePage(page: GetPageResponse): { title: string; lines: { id: string; text: string; userId: string; created: number; updated: number; }[]; created: number; updated: number; user: { id: string; name: string; displayName: string; photo: string; }; lastUpdateUser?: { id: string; name: string; displayName: string; photo: string; } | undefined; collaborators: { id: string; name: string; displayName: string; photo: string; }[]; links: string[]; } { return { title: page.title, lines: page.lines, created: page.created, updated: page.updated, user: page.user, lastUpdateUser: page.lastUpdateUser ?? undefined, collaborators: page.collaborators, links: page.links, }; } // /api/pages/:projectname type ListPagesResponse = { limit: number; count: number; skip: number; projectName: string; pages: { title: string; lastAccessed?: number | undefined; created?: number | undefined; updated?: number | undefined; accessed?: number | undefined; views?: number | undefined; linked?: number | undefined; pin?: number | undefined; user?: { id: string; name: string; displayName: string; photo: string; } | undefined; lastUpdateUser?: { id: string; name: string; displayName: string; photo: string; } | undefined; }[]; }; // デバッグ情報の型を拡張 type DebugInfo = { request_url?: string; params?: Record<string, string>; error?: string; originalCount?: number; filteredCount?: number; appliedSort?: string; excludedPinned?: boolean; total_results?: number; }; async function listPages( projectName: string, sid?: string, options: { limit?: number | undefined; skip?: number | undefined; sort?: string | undefined; excludePinned?: boolean | undefined } = {} ): Promise<ListPagesResponse & { debug?: DebugInfo }> { try { const { sort, excludePinned } = options; // クエリパラメータの構築 const sortValue = options.sort || 'created'; const params = new URLSearchParams({ limit: (options.limit || 1000).toString(), skip: (options.skip || 0).toString(), sort: sortValue }); const url = `https://${API_DOMAIN}/api/pages/${projectName}?${params}`; // デバッグ情報を含めるための変数 const debugInfo: DebugInfo = { request_url: url, params: Object.fromEntries(params.entries()) }; const response = sid ? await fetch(url, { headers: { Cookie: `connect.sid=${sid}` }, }) : await fetch(url); if (!response.ok) { return { limit: 0, count: 0, skip: 0, projectName: projectName, pages: [], debug: { ...debugInfo, error: `API error: ${response.status} ${response.statusText}` } }; } const pages = await response.json(); const pagesWithDetails = await Promise.all( (pages as ListPagesResponse).pages.map(async (page) => { const pageDetails = await getPage(projectName, page.title, sid); if (pageDetails) { return { ...page, user: pageDetails.user, lastUpdateUser: pageDetails.lastUpdateUser, created: pageDetails.created, updated: pageDetails.updated, collaborators: pageDetails.collaborators, descriptions: pageDetails.lines?.slice(0, 5).map(line => line.text) || [] }; } return page; }) ); // ソートとフィルタリングを適用 const sortedPages = sortPages(pagesWithDetails, { sort: sort ?? undefined, excludePinned: excludePinned ?? undefined }); return { ...(pages as ListPagesResponse), pages: sortedPages, debug: { ...debugInfo, originalCount: pagesWithDetails.length, filteredCount: sortedPages.length, appliedSort: sort || 'created', excludedPinned: excludePinned || false } }; } catch (error) { return { limit: 0, count: 0, skip: 0, projectName: projectName, pages: [], debug: { error: error instanceof Error ? error.message : '不明なエラー' } }; } } function encodeScrapboxBody(body: string): string { // Scrapboxの本文用にエンコード return encodeURIComponent(body); } function createPageUrl(projectName: string, title: string, body?: string): string { const baseUrl = `https://${API_DOMAIN}/${projectName}/${encodeURIComponent(title)}`; return body ? `${baseUrl}?body=${encodeScrapboxBody(body)}` : baseUrl; } /** * プロジェクト内のページを全文検索します * @param projectName プロジェクト名 * @param query 検索クエリ * @param sid セッションID(オプション) * @returns 検索結果 * * 使用例: * - 基本的な検索: searchPages("projectname", "検索語句") * - 複数語句での検索: searchPages("projectname", "word1 word2") * - 除外検索: searchPages("projectname", "word1 -word2") * - フレーズ検索: searchPages("projectname", '"exact phrase"') */ async function searchPages( projectName: string, query: string, sid?: string ): Promise<SearchQueryResponse | null> { const encodedQuery = encodeURIComponent(query); const url = `https://${API_DOMAIN}/api/pages/${projectName}/search/query?q=${encodedQuery}`; const debugInfo = { request_url: url, searchQuery: query, }; const response = sid ? await fetch(url, { headers: { Cookie: `connect.sid=${sid}` } }) : await fetch(url); if (!response.ok) { return { projectName, searchQuery: query, query: { words: [], excludes: [] }, limit: 0, count: 0, existsExactTitleMatch: false, backend: 'elasticsearch', pages: [], debug: { ...debugInfo, error: `Search API error: ${response.status} ${response.statusText}` } }; } const result = await response.json(); return { projectName, searchQuery: query, query: result.query, limit: result.limit, count: result.count, existsExactTitleMatch: result.existsExactTitleMatch, backend: result.backend, pages: result.pages, debug: { ...debugInfo, total_results: result.pages.length } }; } /** * ピン留めページを考慮してソートされたページリストを取得する */ async function listPagesWithSort( projectName: string, options: { limit: number; skip: number; sort?: string | undefined; excludePinned?: boolean | undefined; }, sid?: string ): Promise<ListPagesResponse> { const skip = options.skip || 0; const limit = options.limit; const fetchSize = limit + skip + 100; // skip + limit + 余裕を持って取得 // 1. より多くのページを一度に取得 const response = await listPages(projectName, sid, { limit: fetchSize, skip: 0, // 最初から取得して後でskipを適用 excludePinned: options.excludePinned ?? false }); // 2. 取得したページをメモリ上でソート const sortedPages = sortPages(response.pages, { sort: options.sort ?? undefined, excludePinned: options.excludePinned ?? undefined }); // 3. skip位置から必要な件数を切り出し const resultPages = sortedPages.slice(skip, skip + limit); // 4. 結果を返す return { ...response, pages: resultPages, limit: resultPages.length, skip: skip }; } // 型のエクスポート export type { ListPagesResponse }; // 関数のエクスポート export { getPage, listPages, listPagesWithSort, toReadablePage, createPageUrl, searchPages };

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/worldnine/scrapbox-cosense-mcp'

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