Skip to main content
Glama
note-tools.ts22.1 kB
import { z } from "zod"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { noteApiRequest } from "../utils/api-client.js"; import { formatNote, formatComment, formatLike } from "../utils/formatters.js"; import { createSuccessResponse, createErrorResponse, createAuthErrorResponse, handleApiError } from "../utils/error-handler.js"; import { hasAuth, buildAuthHeaders, getPreviewAccessToken, } from "../utils/auth.js"; import { env } from "../config/environment.js"; export function registerNoteTools(server: McpServer) { // 1. 記事詳細取得ツール server.tool( "get-note", "記事の詳細情報を取得する", { noteId: z.string().describe("記事ID(例: n4f0c7b884789)"), }, async ({ noteId }) => { try { const params = new URLSearchParams({ draft: "true", draft_reedit: "false", ts: Date.now().toString() }); const data = await noteApiRequest( `/v3/notes/${noteId}?${params.toString()}`, "GET", null, true ); const noteData = data.data || {}; const formattedNote = formatNote(noteData); return createSuccessResponse(formattedNote); } catch (error) { return handleApiError(error, "記事取得"); } } ); // 2. コメント一覧取得ツール server.tool( "get-comments", "記事へのコメント一覧を取得する", { noteId: z.string().describe("記事ID"), }, async ({ noteId }) => { try { const data = await noteApiRequest(`/v1/note/${noteId}/comments`); let formattedComments: any[] = []; if (data.comments) { formattedComments = data.comments.map(formatComment); } return createSuccessResponse({ comments: formattedComments }); } catch (error) { return handleApiError(error, "コメント取得"); } } ); // 3. スキ取得ツール server.tool( "get-likes", "記事のスキ一覧を取得する", { noteId: z.string().describe("記事ID"), }, async ({ noteId }) => { try { const data = await noteApiRequest(`/v3/notes/${noteId}/likes`); let formattedLikes: any[] = []; if (data.data && data.data.likes) { formattedLikes = data.data.likes.map(formatLike); } return createSuccessResponse({ likes: formattedLikes }); } catch (error) { return handleApiError(error, "スキ一覧取得"); } } ); // 4. 記事下書き保存ツール server.tool( "post-draft-note", "下書き状態の記事を新規作成または更新する", { title: z.string().describe("記事のタイトル"), body: z.string().describe("記事の本文"), tags: z.array(z.string()).optional().describe("タグ(最大10個)"), id: z.string().optional().describe("既存の下書きID(既存の下書きを更新する場合)"), }, async ({ title, body, tags, id }) => { let previewAccessToken: string | null = null; try { if (!hasAuth()) { return createAuthErrorResponse(); } if (id) { // 既存の下書きIDがある場合のみpreview_access_tokenを取得 previewAccessToken = await getPreviewAccessToken(id); if (!previewAccessToken) { console.error(`Failed to get preview_access_token for noteId: ${id}. Proceeding without it for draft save.`); // トークン取得失敗時は、以前の挙動(トークンなし)で試行する } } console.error("下書き保存リクエスト内容:"); // 試行1: 公式API形式で試行(参考: https://note.com/taku_sid/n/n1b1b7894e28f) try { console.error("試行1: 公式API形式 /api/v1/text_notes"); // 参照記事に基づく正しいパラメータ形式 const postData1: any = { name: title, // 'title'ではなく'name' body: body, template_key: null // 新規作成時に必要 }; // 更新時はstatusを追加し、template_keyを削除 if (id) { postData1.status = "draft"; delete postData1.template_key; } console.error(`リクエスト内容: ${JSON.stringify(postData1, null, 2)}`); let endpoint = ""; let method: "POST" | "PUT"; if (id) { // 既存記事の更新 endpoint = `/v1/text_notes/${id}`; method = "PUT"; } else { // 新規作成 endpoint = `/v1/text_notes`; method = "POST"; } const headers1 = buildAuthHeaders(); if (previewAccessToken) { headers1['Authorization'] = `Bearer ${previewAccessToken}`; } const data = await noteApiRequest(endpoint, method, postData1, true, headers1); console.error(`成功: ${JSON.stringify(data, null, 2)}`); return createSuccessResponse({ success: true, data: data, message: id ? "既存の記事を下書き保存しました" : "新しい記事を下書き保存しました", noteId: data.data?.key || id || data.id || null }); } catch (error1) { console.error(`試行1でエラー: ${error1}`); // 試行2: 旧APIエンドポイント try { console.error("試行2: 旧APIエンドポイント"); const postData2 = { title, body, tags: tags || [], }; console.error(`リクエスト内容: ${JSON.stringify(postData2, null, 2)}`); const endpoint = id ? `/v1/text_notes/draft_save?id=${id}&user_id=${env.NOTE_USER_ID}` : `/v1/text_notes/draft_save?user_id=${env.NOTE_USER_ID}`; const headers2 = buildAuthHeaders(); // 試行2ではpreviewAccessTokenを必須としない(旧APIのため互換性維持) // もし試行1で取得できていれば利用する形も考えられるが、一旦シンプルに const data = await noteApiRequest(endpoint, "POST", postData2, true, headers2); console.error(`成功: ${JSON.stringify(data, null, 2)}`); return createSuccessResponse({ success: true, data: data, message: id ? "既存の記事を下書き保存しました" : "新しい記事を下書き保存しました", noteId: data.id || data.note_id || id || null }); } catch (error2) { console.error(`試行2でエラー: ${error2}`); return createErrorResponse( `記事の投稿に失敗しました:\n試行1エラー: ${error1}\n試行2エラー: ${error2}\n\nセッションの有効期限が切れている可能性があります。.envファイルのCookie情報を更新してください。` ); } } } catch (error) { console.error(`下書き保存処理全体でエラー: ${error}`); return handleApiError(error, "記事投稿"); } } ); // 5. 記事編集ツール(既存記事の編集) server.tool( "edit-note", "既存の記事を編集する", { noteId: z.string().describe("編集する記事ID(例: n4f0c7b884789)"), title: z.string().describe("記事のタイトル"), body: z.string().describe("記事の本文"), tags: z.array(z.string()).optional().describe("タグ(最大10個)"), isDraft: z.boolean().optional().default(true).describe("下書き状態で保存するか(trueの場合下書き、falseの場合は公開)"), }, async ({ noteId, title, body, tags, isDraft }) => { try { if (!hasAuth()) { return createAuthErrorResponse(); } console.error(`記事編集リクエスト: ${noteId}`); // 先に記事情報を取得してみる try { const params = new URLSearchParams({ draft: "true", draft_reedit: "false", ts: Date.now().toString() }); await noteApiRequest( `/v3/notes/${noteId}?${params.toString()}`, "GET", null, true ); } catch (getError) { console.error(`記事情報取得エラー: ${getError}`); return createErrorResponse(`指定された記事が存在しないか、アクセスできません: ${noteId}`); } // 記事編集API - v3形式を試行 try { let previewAccessToken: string | null = null; if (noteId) { // 既存記事の場合のみトークン取得を試みる try { previewAccessToken = await getPreviewAccessToken(noteId); if (previewAccessToken && env.DEBUG) { console.error(`edit-note: Preview access token obtained for note ${noteId}`); } } catch (tokenError) { console.error(`edit-note: Failed to get preview access token for note ${noteId}: ${tokenError}`); // トークン取得に失敗しても、処理を続行する(セッション認証で試みる) } } const postDataV3 = { title: title, body: body, status: isDraft ? "draft" : "published", tags: tags || [], publish_at: null, eyecatch_image: null, price: 0, is_magazine_note: false }; const headersV3 = buildAuthHeaders(); if (previewAccessToken) { // 取得できていればヘッダーに追加 headersV3['Authorization'] = `Bearer ${previewAccessToken}`; if (env.DEBUG) console.error('edit-note: Authorization header set with previewAccessToken for V3 API'); } // V3編集エンドポイント const endpointV3 = `/v3/notes/${noteId}/${isDraft ? 'draft' : 'publish'}`; const dataV3 = await noteApiRequest(endpointV3, "POST", postDataV3, true, headersV3); console.error(`V3 API 編集成功: ${JSON.stringify(dataV3, null, 2)}`); return createSuccessResponse({ success: true, data: dataV3, message: isDraft ? "記事をV3 APIで下書き保存しました" : "記事をV3 APIで公開しました", noteId: noteId }); } catch (errorV3) { console.error(`V3 API編集エラー: ${errorV3}`); // V3で失敗した場合、V1形式でフォールバック try { const postDataV1 = { // V1 uses a simpler payload title, body, tags: tags || [], }; const headersV1 = buildAuthHeaders(); // V1は通常セッション認証のみ const endpointV1 = `/v1/text_notes/draft_save?id=${noteId}&user_id=${env.NOTE_USER_ID}`; const dataV1 = await noteApiRequest(endpointV1, "POST", postDataV1, true, headersV1); console.error(`V1 API 旧形式での編集成功: ${JSON.stringify(dataV1, null, 2)}`); return createSuccessResponse({ success: true, data: dataV1, message: "記事を下書き状態で更新しました(V1 API旧形式使用)", noteId: noteId }); } catch (errorV1) { console.error(`V1 API編集エラー (フォールバック試行後): ${errorV1}`); return createErrorResponse( `記事の編集に失敗しました:\nAttempted V3 API Error: ${errorV3}\nFallback V1 API Error: ${errorV1}\n\nセッションの有効期限が切れている可能性があります。.envファイルのCookie情報を更新してください。` ); } } } catch (error) { console.error(`記事編集処理全体でエラー: ${error}`); return handleApiError(error, "記事編集"); } } ); // 6. 記事公開ツール server.tool( "publish-note", "下書き状態の記事を公開する", { noteId: z.string().describe("公開する記事ID"), title: z.string().optional().describe("公開時に変更する記事タイトル(省略可)"), body: z.string().optional().describe("公開時に変更する記事本文(省略可)"), tags: z.array(z.string()).optional().describe("公開時に設定するタグ(省略可)"), }, async ({ noteId, title, body, tags }) => { try { if (!hasAuth()) { return createAuthErrorResponse(); } console.error(`記事公開リクエスト: ${noteId}`); // 記事情報を先に取得 let currentNote; try { const params = new URLSearchParams({ draft: "true", draft_reedit: "false", ts: Date.now().toString() }); const noteData = await noteApiRequest( `/v3/notes/${noteId}?${params.toString()}`, "GET", null, true ); currentNote = noteData.data || {}; } catch (getError) { console.error(`記事情報取得エラー: ${getError}`); return createErrorResponse(`指定された記事が存在しないか、アクセスできません: ${noteId}`); } // 公開APIリクエスト try { const postData = { title: title || currentNote.title, body: body || currentNote.body, status: "published", tags: tags || currentNote.tags || [], publish_at: null, eyecatch_image: currentNote.eyecatch_image || null, price: currentNote.price || 0, is_magazine_note: currentNote.is_magazine_note || false }; const endpoint = `/v3/notes/${noteId}/publish`; const data = await noteApiRequest(endpoint, "POST", postData, true); console.error(`公開成功: ${JSON.stringify(data, null, 2)}`); return createSuccessResponse({ success: true, data: data, message: "記事を公開しました", noteId: noteId, noteUrl: data.data?.url || `https://note.com/${env.NOTE_USER_ID}/n/${noteId}` }); } catch (error) { console.error(`公開エラー: ${error}`); return createErrorResponse( `記事の公開に失敗しました: ${error}\n\nセッションの有効期限が切れている可能性があります。.envファイルのCookie情報を更新してください。` ); } } catch (error) { console.error(`記事公開処理全体でエラー: ${error}`); return handleApiError(error, "記事公開"); } } ); // 7. コメント投稿ツール server.tool( "post-comment", "記事にコメントを投稿する", { noteId: z.string().describe("記事ID"), text: z.string().describe("コメント本文"), }, async ({ noteId, text }) => { try { if (!hasAuth()) { return createAuthErrorResponse(); } const data = await noteApiRequest(`/v1/note/${noteId}/comments`, "POST", { text }, true); return createSuccessResponse({ message: "コメントを投稿しました", data: data }); } catch (error) { return handleApiError(error, "コメント投稿"); } } ); // 6. スキをつけるツール server.tool( "like-note", "記事にスキをする", { noteId: z.string().describe("記事ID"), }, async ({ noteId }) => { try { if (!hasAuth()) { return createAuthErrorResponse(); } await noteApiRequest(`/v3/notes/${noteId}/likes`, "POST", {}, true); return createSuccessResponse({ message: "スキをつけました" }); } catch (error) { return handleApiError(error, "スキ"); } } ); // 7. スキを削除するツール server.tool( "unlike-note", "記事のスキを削除する", { noteId: z.string().describe("記事ID"), }, async ({ noteId }) => { try { if (!hasAuth()) { return createAuthErrorResponse(); } await noteApiRequest(`/v3/notes/${noteId}/likes`, "DELETE", {}, true); return createSuccessResponse({ message: "スキを削除しました" }); } catch (error) { return handleApiError(error, "スキ削除"); } } ); // 8. 自分の記事一覧(下書きを含む)取得ツール server.tool( "get-my-notes", "自分の記事一覧(下書きを含む)を取得する", { page: z.number().default(1).describe("ページ番号(デフォルト: 1)"), perPage: z.number().default(20).describe("1ページあたりの表示件数(デフォルト: 20)"), status: z.enum(["all", "draft", "public"]).default("all").describe("記事の状態フィルター(all:すべて, draft:下書きのみ, public:公開済みのみ)"), }, async ({ page, perPage, status }) => { try { if (!env.NOTE_USER_ID) { return createErrorResponse("環境変数 NOTE_USER_ID が設定されていません。.envファイルを確認してください。"); } const params = new URLSearchParams({ page: page.toString(), per_page: perPage.toString(), draft: "true", draft_reedit: "false", ts: Date.now().toString() }); if (status === "draft") { params.set("status", "draft"); } else if (status === "public") { params.set("status", "public"); } const data = await noteApiRequest( `/v2/note_list/contents?${params.toString()}`, "GET", null, true ); if (env.DEBUG) { console.error(`API Response: ${JSON.stringify(data, null, 2)}`); } let formattedNotes: any[] = []; let totalCount = 0; if (data.data && data.data.notes && Array.isArray(data.data.notes)) { formattedNotes = data.data.notes.map((note: any) => { const isDraft = note.status === "draft"; const noteKey = note.key || ""; const noteId = note.id || ""; const draftTitle = note.noteDraft?.name || ""; const title = note.name || draftTitle || "(無題)"; let excerpt = ""; if (note.body) { excerpt = note.body.length > 100 ? note.body.substring(0, 100) + '...' : note.body; } else if (note.peekBody) { excerpt = note.peekBody; } else if (note.noteDraft?.body) { const textContent = note.noteDraft.body.replace(/<[^>]*>/g, ''); excerpt = textContent.length > 100 ? textContent.substring(0, 100) + '...' : textContent; } const publishedAt = note.publishAt || note.publish_at || note.displayDate || note.createdAt || '日付不明'; return { id: noteId, key: noteKey, title: title, excerpt: excerpt, publishedAt: publishedAt, likesCount: note.likeCount || 0, commentsCount: note.commentsCount || 0, status: note.status || "unknown", isDraft: isDraft, format: note.format || "", url: `https://note.com/${env.NOTE_USER_ID}/n/${noteKey}`, editUrl: `https://editor.note.com/notes/${noteId}/edit/`, hasDraftContent: note.noteDraft ? true : false, lastUpdated: note.noteDraft?.updatedAt || note.createdAt || "", user: { id: note.user?.id || env.NOTE_USER_ID, name: note.user?.name || note.user?.nickname || "", urlname: note.user?.urlname || env.NOTE_USER_ID } }; }); } totalCount = data.data?.totalCount || 0; return createSuccessResponse({ total: totalCount, page: page, perPage: perPage, status: status, totalPages: Math.ceil(totalCount / perPage), hasNextPage: page * perPage < totalCount, hasPreviousPage: page > 1, draftCount: formattedNotes.filter((note: any) => note.isDraft).length, publicCount: formattedNotes.filter((note: any) => !note.isDraft).length, notes: formattedNotes }); } catch (error) { return handleApiError(error, "記事一覧取得"); } } ); // 9. 記事編集ページを開くツール server.tool( "open-note-editor", "記事の編集ページを開く", { noteId: z.string().describe("記事ID(例: n1a2b3c4d5e6)"), }, async ({ noteId }) => { try { if (!env.NOTE_USER_ID) { return createErrorResponse("環境変数 NOTE_USER_ID が設定されていません。.envファイルを確認してください。"); } const editUrl = `https://editor.note.com/notes/${noteId}/edit/`; return createSuccessResponse({ status: "success", editUrl: editUrl, message: `編集ページのURLを生成しました。以下のURLを開いてください:\n${editUrl}` }); } catch (error) { return handleApiError(error, "編集ページURL生成"); } } ); }

Latest Blog Posts

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/shimayuz/note-com-mcp'

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