Skip to main content
Glama
note-mcp-server.ts100 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import fetch from "node-fetch"; import dotenv from "dotenv"; import path from "path"; import { fileURLToPath } from "url"; // ESMでの__dirnameの代替 const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // 環境変数を読み込む(ビルドディレクトリを考慮) dotenv.config({ path: path.resolve(__dirname, '../.env') }); dotenv.config({ path: path.resolve(__dirname, '.env') }); // デバッグモード const DEBUG = process.env.DEBUG === "true"; // APIのベースURL const API_BASE_URL = "https://note.com/api"; // note API認証情報(環境変数から取得) const NOTE_SESSION_V5 = process.env.NOTE_SESSION_V5 || ""; const NOTE_XSRF_TOKEN = process.env.NOTE_XSRF_TOKEN || ""; const NOTE_EMAIL = process.env.NOTE_EMAIL || ""; const NOTE_PASSWORD = process.env.NOTE_PASSWORD || ""; const NOTE_USER_ID = process.env.NOTE_USER_ID || ""; // 動的セッション情報を保持する変数 let activeSessionCookie: string | null = null; let activeXsrfToken: string | null = null; // 認証状態 const AUTH_STATUS = { hasCookie: NOTE_SESSION_V5 !== "" || NOTE_XSRF_TOKEN !== "", anyAuth: NOTE_SESSION_V5 !== "" || NOTE_XSRF_TOKEN !== "" || (NOTE_EMAIL !== "" && NOTE_PASSWORD !== "") }; // デバッグログ if (DEBUG) { console.error(`Working directory: ${process.cwd()}`); console.error(`Script directory: ${__dirname}`); console.error(`Authentication status: Cookie=${AUTH_STATUS.hasCookie}`); } // MCP サーバーインスタンスを作成 const server = new McpServer({ name: "note-api", version: "1.0.0" }); // 各種データ型の定義 // メンバーシップ(サークル)型定義 interface Membership { id?: string; key?: string; // メンバーシップ記事取得時に必要 name?: string; description?: string; creatorId?: string; creatorName?: string; creatorUrlname?: string; price?: number; memberCount?: number; notesCount?: number; } // 加入済みメンバーシップサマリー型定義 interface MembershipSummary { id?: string; key?: string; name?: string; urlname?: string; price?: number; creator?: { id?: string; nickname?: string; urlname?: string; profileImageUrl?: string; }; } // メンバーシッププラン型定義 interface MembershipPlan { id?: string; key?: string; name?: string; description?: string; price?: number; memberCount?: number; notesCount?: number; status?: string; } // メンバーシップ記事用の型定義 interface FormattedMembershipNote { id: string; title: string; excerpt: string; publishedAt: string; likesCount: number; commentsCount: number; user: string | { id?: string; nickname?: string; urlname?: string; }; url: string; isMembersOnly: boolean; } interface NoteUser { id?: string; nickname?: string; urlname?: string; bio?: string; profile?: { bio?: string; }; followersCount?: number; followingCount?: number; notesCount?: number; magazinesCount?: number; } interface Note { id?: string; name?: string; key?: string; body?: string; user?: NoteUser; publishAt?: string; likeCount?: number; commentsCount?: number; status?: string; } interface Magazine { id?: string; name?: string; key?: string; description?: string; user?: NoteUser; publishAt?: string; notesCount?: number; } interface Comment { id?: string; body?: string; user?: NoteUser; publishAt?: string; } interface Like { id?: string; user?: NoteUser; createdAt?: string; } // APIレスポンスの型定義 interface NoteApiResponse { data?: { notes?: Note[]; notesCount?: number; users?: NoteUser[]; usersCount?: number; contents?: any[]; totalCount?: number; limit?: number; magazines?: Magazine[]; magazinesCount?: number; likes?: Like[]; [key: string]: any; }; comments?: Comment[]; [key: string]: any; } // 整形済みデータの型定義 interface FormattedNote { id: string; key?: string; title: string; excerpt?: string; body?: string; user: string | { id?: string; name?: string; nickname?: string; urlname?: string; bio?: string; }; publishedAt: string; likesCount: number; commentsCount?: number; status?: string; isDraft?: boolean; format?: string; url: string; editUrl?: string; hasDraftContent?: boolean; lastUpdated?: string; } interface FormattedUser { id: string; nickname: string; urlname: string; bio: string; followersCount: number; followingCount: number; notesCount: number; magazinesCount?: number; url: string; profileImageUrl?: string; } interface FormattedMagazine { id: string; name: string; description: string; notesCount: number; publishedAt: string; user: string | { id?: string; nickname?: string; urlname?: string; }; url: string; } interface FormattedComment { id: string; body: string; user: string | { id?: string; nickname?: string; urlname?: string; }; publishedAt: string; } interface FormattedLike { id: string; user: string | { id?: string; nickname?: string; urlname?: string; }; createdAt: string; } // noteへのログイン処理を行う関数 async function loginToNote(): Promise<boolean> { if (!NOTE_EMAIL || !NOTE_PASSWORD) { console.error("メールアドレスまたはパスワードが設定されていません。"); return false; } const loginPath = "/v1/sessions/sign_in"; // ログインAPIのパス const loginUrl = `${API_BASE_URL}${loginPath}`; try { if (DEBUG) { console.error(`Attempting login to ${loginUrl}`); } const response = await fetch(loginUrl, { method: "POST", headers: { "Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36", "Accept": "application/json", }, body: JSON.stringify({ login: NOTE_EMAIL, password: NOTE_PASSWORD }), }); const responseText = await response.text(); if (DEBUG) { console.error(`Login response: ${response.status} ${response.statusText}`); console.error(`Login response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`); console.error(`Login response body: ${responseText}`); } if (!response.ok) { console.error(`Login failed: ${response.status} ${response.statusText} - ${responseText}`); return false; } // レスポンスボディからトークン情報取得を試みる try { const responseData = JSON.parse(responseText); if (responseData && responseData.data && responseData.data.token) { // レスポンスボディからトークンが見つかった場合 activeSessionCookie = `_note_session_v5=${responseData.data.token}`; if (DEBUG) console.error("Session token found in response body:", responseData.data.token); console.error("Login successful. Session token obtained from response body."); } } catch (e) { if (DEBUG) console.error("Failed to parse response body as JSON:", e); } // 従来のSet-Cookieヘッダーからの取得方法も残す const setCookieHeader = response.headers.get("set-cookie"); if (setCookieHeader) { if (DEBUG) console.error("Set-Cookie header:", setCookieHeader); const cookies = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader]; cookies.forEach(cookieStr => { if (cookieStr.includes("_note_session_v5=")) { activeSessionCookie = cookieStr.split(';')[0]; if (DEBUG) console.error("Session cookie set:", activeSessionCookie); } if (cookieStr.includes("XSRF-TOKEN=")) { activeXsrfToken = cookieStr.split(';')[0].split('=')[1]; if (DEBUG) console.error("XSRF token from cookie:", activeXsrfToken); } }); const responseXsrfToken = response.headers.get("x-xsrf-token"); if (responseXsrfToken) { activeXsrfToken = responseXsrfToken; if (DEBUG) console.error("XSRF Token from header:", activeXsrfToken); } else if (DEBUG && !activeXsrfToken) { console.error("XSRF Token not found in initial login headers."); } } if (!activeSessionCookie) { console.error("Login succeeded but session cookie was not found."); return false; } console.error("Login successful. Session cookie obtained."); // セッションクッキーが取得できたら、current_userリクエストでXSRFトークンを取得する if (activeSessionCookie && !activeXsrfToken) { console.error("Trying to obtain XSRF token from current_user API..."); try { const currentUserResponse = await fetch(`${API_BASE_URL}/v2/current_user`, { method: "GET", headers: { "Accept": "application/json", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36", "Cookie": activeSessionCookie }, }); // XSRFトークンをヘッダーから取得 const xsrfToken = currentUserResponse.headers.get("x-xsrf-token"); if (xsrfToken) { activeXsrfToken = xsrfToken; console.error("XSRF token successfully obtained from current_user API."); if (DEBUG) console.error("XSRF Token:", activeXsrfToken); } else { // Set-Cookieヘッダーからも確認 const currentUserSetCookie = currentUserResponse.headers.get("set-cookie"); if (currentUserSetCookie) { const cookies = Array.isArray(currentUserSetCookie) ? currentUserSetCookie : [currentUserSetCookie]; cookies.forEach(cookieStr => { if (cookieStr.includes("XSRF-TOKEN=")) { activeXsrfToken = cookieStr.split(';')[0].split('=')[1]; console.error("XSRF token found in current_user response cookies."); if (DEBUG) console.error("XSRF Token from cookie:", activeXsrfToken); } }); } if (!activeXsrfToken) { console.error("Could not obtain XSRF token from current_user API."); } } } catch (error) { console.error("Error fetching current_user for XSRF token:", error); } } return activeSessionCookie !== null; } catch (error) { console.error("Error during login:", error); return false; } } // APIリクエスト用のヘルパー関数 async function noteApiRequest(path: string, method: string = "GET", body: any = null, requireAuth: boolean = false): Promise<NoteApiResponse> { const headers: { [key: string]: string } = { "Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36" }; // Acceptヘッダーを追加 headers["Accept"] = "application/json"; // 認証設定 - 環境変数のCookieを優先使用(現在多くのAPIがこれで正常動作している) if (AUTH_STATUS.hasCookie) { // 従来のCookieベースの認証を優先使用 const cookies = []; if (NOTE_SESSION_V5) { cookies.push(`_note_session_v5=${NOTE_SESSION_V5}`); if (DEBUG) console.error("Using session cookie from .env file"); } if (cookies.length > 0) { headers["Cookie"] = cookies.join("; "); } } else if (activeSessionCookie) { // 動的に取得したセッションCookieを使用 headers["Cookie"] = activeSessionCookie; if (DEBUG) console.error("Using dynamically obtained session cookie"); } else if (requireAuth && NOTE_EMAIL && NOTE_PASSWORD) { // 認証情報が必要で、メールアドレスとパスワードが設定されている場合はログイン試行 const loggedIn = await loginToNote(); if (loggedIn && activeSessionCookie) { headers["Cookie"] = activeSessionCookie; } else { throw new Error("認証が必要です。ログインに失敗しました。"); } } else if (requireAuth) { // 認証が必要なのに認証情報がない場合 throw new Error("認証情報が必要です。.envファイルに認証情報を設定してください。"); } // XSRFトークンの設定 if (activeXsrfToken) { // 動的に取得したXSRFトークンを優先使用 headers["X-XSRF-TOKEN"] = activeXsrfToken; } else if (NOTE_XSRF_TOKEN) { // 従来のXSRFトークン設定(互換性のために維持) headers["X-XSRF-TOKEN"] = NOTE_XSRF_TOKEN; } const options: any = { method, headers, }; if (body && (method === "POST" || method === "PUT")) { options.body = JSON.stringify(body); } try { if (DEBUG) { console.error(`Requesting ${API_BASE_URL}${path}`); console.error(`Request Headers: ${JSON.stringify(headers)}`); if (body && (method === "POST" || method === "PUT")) { console.error(`Request Body: ${JSON.stringify(body)}`); } } const response = await fetch(`${API_BASE_URL}${path}`, options); if (!response.ok) { let errorText = ""; try { errorText = await response.text(); } catch (e) { errorText = "(レスポンステキストの取得に失敗)"; } if (DEBUG) { console.error(`API error on path ${path}: ${response.status} ${response.statusText}`); console.error(`API error response body: ${errorText}`); // エンドポイントのバージョンをチェック if (path.includes("/v1/") || path.includes("/v3/")) { console.error(`Note: This endpoint uses API version ${path.includes("/v1/") ? "v1" : "v3"}. Consider trying v2 version if available.`); if (path.includes("/v3/notes/")) { // v3で問題が発生している場合の代替案 const altPath = path.replace("/v3/notes/", "/v2/notes/"); console.error(`Alternative endpoint suggestion: ${altPath}`); } else if (path.includes("/v3/searches")) { const altPath = path.replace("/v3/searches", "/v2/searches"); console.error(`Alternative endpoint suggestion: ${altPath}`); } } } // エラー種別ごとの詳細な説明 if (response.status === 401 || response.status === 403) { throw new Error("認証エラー: noteへのアクセス権限がありません。認証情報を確認してください。"); } else if (response.status === 404) { console.error(`404 Not Found: エンドポイント ${path} が存在しないか、変更された可能性があります。APIバージョンを確認してください。`); } else if (response.status === 400) { console.error(`400 Bad Request: リクエストパラメータが不正な可能性があります。`); } throw new Error(`API error: ${response.status} ${response.statusText} - ${errorText}`); } const data = await response.json() as NoteApiResponse; return data; } catch (error) { if (DEBUG) { console.error(`Error calling note API: ${error}`); } throw error; } } function hasAuth() { // 動的に取得したセッションCookieを優先的にチェック return activeSessionCookie !== null || AUTH_STATUS.anyAuth; } // 検索と分析ツールを拡張 // 1. 記事検索ツール server.tool( "search-notes", "記事を検索する", { query: z.string().describe("検索キーワード"), size: z.number().default(10).describe("取得する件数(最大20)"), start: z.number().default(0).describe("検索結果の開始位置"), sort: z.enum(["new", "popular", "hot"]).default("hot").describe("ソート順(new: 新着順, popular: 人気順, hot: 急上昇)"), }, async ({ query, size, start, sort }) => { try { // 記事検索はv3を使用 const data = await noteApiRequest(`/v3/searches?context=note&q=${encodeURIComponent(query)}&size=${size}&start=${start}&sort=${sort}`); // デバッグ用:APIレスポンスの詳細な構造を確認 console.error(`API Response structure for search-notes: ${JSON.stringify(data, null, 2)}`); console.error(`Response type: ${typeof data}, has data: ${Boolean(data.data)}`); if (data.data) { console.error(`data.data keys: ${Object.keys(data.data)}`); console.error(`notes type: ${Array.isArray(data.data.notes) ? 'array' : typeof data.data.notes}`); } // 結果を見やすく整形 if (!data || !data.data) { return { content: [ { type: "text", text: `APIレスポンスが空です: ${JSON.stringify(data)}` } ] }; } // APIがエラーを返した場合 if (data.status === "error" || data.error) { return { content: [ { type: "text", text: `APIエラー: ${JSON.stringify(data)}` } ], isError: true }; } // 検索結果の処理 try { let formattedNotes: FormattedNote[] = []; let notesArray: any[] = []; let totalCount: number = 0; // v3: data.data.notes may contain contents and total_count if (data.data.notes && Array.isArray((data.data.notes as any).contents)) { notesArray = (data.data.notes as any).contents; totalCount = (data.data.notes as any).total_count || 0; } else if (Array.isArray(data.data.notes)) { notesArray = data.data.notes; totalCount = data.data.notesCount || notesArray.length; } else if (Array.isArray(data.data.contents)) { // fallback: direct contents list notesArray = data.data.contents .filter((item: any) => item.type === 'note') .map((item: any) => item.note || item); totalCount = data.data.notesCount || notesArray.length; } else { console.error(`Unexpected search data keys: ${Object.keys(data.data)}`); } formattedNotes = notesArray.map((note: any) => ({ id: note.id || "", title: note.name || "", excerpt: note.body ? (note.body.length > 100 ? note.body.substr(0, 100) + '...' : note.body) : '本文なし', user: note.user?.nickname || 'ユーザー不明', publishedAt: note.publishAt || '日付不明', likesCount: note.likeCount || 0, url: `https://note.com/${note.user?.urlname || 'unknown'}/n/${note.key || note.id || ''}` })); return { content: [ { type: "text", text: JSON.stringify({ total: totalCount, notes: formattedNotes, rawResponse: data }, null, 2) } ] }; } catch (formatError) { console.error(`Error formatting notes: ${formatError}`); return { content: [ { type: "text", text: `データの整形中にエラーが発生しました: ${formatError}\n元データ: ${JSON.stringify(data)}` } ] }; } } catch (error) { return { content: [ { type: "text", text: `検索に失敗しました: ${error}` } ], isError: true }; } } ); // 1.5 記事分析ツール server.tool( "analyze-notes", "記事の詳細分析を行う(競合分析やコンテンツ成果の比較等)", { query: z.string().describe("検索キーワード"), size: z.number().default(20).describe("取得する件数(分析に十分なデータ量を確保するため、初期値は多め)"), start: z.number().default(0).describe("検索結果の開始位置"), sort: z.enum(["new", "popular", "hot"]).default("popular").describe("ソート順(new: 新着順, popular: 人気順, hot: 急上昇)"), includeUserDetails: z.boolean().default(true).describe("著者情報を詳細に含めるかどうか"), analyzeContent: z.boolean().default(true).describe("コンテンツの特徴(画像数、アイキャッチの有無など)を分析するか"), category: z.string().optional().describe("特定のカテゴリに絞り込む(オプション)"), dateRange: z.string().optional().describe("日付範囲で絞り込む(例: 7d=7日以内、2m=2ヶ月以内)"), priceRange: z.enum(["all", "free", "paid"]).default("all").describe("価格帯(all: 全て, free: 無料のみ, paid: 有料のみ)"), }, async ({ query, size, start, sort, includeUserDetails, analyzeContent, category, dateRange, priceRange }) => { try { // 検索クエリーの構築 const params = new URLSearchParams({ q: query, size: size.toString(), start: start.toString(), sort: sort }); // カテゴリが指定されていれば追加 if (category) { params.append("category", category); } // 日付範囲が指定されていれば追加 if (dateRange) { params.append("date_range", dateRange); } // 価格フィルターの追加 if (priceRange !== "all") { params.append("price", priceRange); } // APIリクエストを実行 const data = await noteApiRequest(`/v3/searches?context=note&${params.toString()}`); if (DEBUG) { console.error(`API Response structure for analyze-notes: ${JSON.stringify(data, null, 2)}`); } // 結果を見やすく整形 if (!data || !data.data) { return { content: [ { type: "text", text: `APIレスポンスが空です: ${JSON.stringify(data)}` } ] }; } // APIがエラーを返した場合 if (data.status === "error" || data.error) { return { content: [ { type: "text", text: `APIエラー: ${JSON.stringify(data)}` } ], isError: true }; } // 検索結果の処理 try { let formattedNotes = []; let notesArray = []; let totalCount = 0; // v3: data.data.notes may contain contents and total_count if (data.data.notes && Array.isArray((data.data.notes as any).contents)) { notesArray = (data.data.notes as any).contents; totalCount = (data.data.notes as any).total_count || 0; } else if (Array.isArray(data.data.notes)) { notesArray = data.data.notes; totalCount = data.data.notesCount || notesArray.length; } else if (Array.isArray(data.data.contents)) { // fallback: direct contents list notesArray = data.data.contents .filter((item: any) => item.type === 'note') .map((item: any) => item.note || item); totalCount = data.data.notesCount || notesArray.length; } else { console.error(`Unexpected search data keys: ${Object.keys(data.data)}`); } // 記事を詳細に分析してフォーマット formattedNotes = notesArray.map((note: any) => { // ユーザー情報の抽出と整形 const user = note.user || {}; // コンテンツ分析用データの整形 const hasEyecatch = Boolean(note.eyecatch || note.sp_eyecatch); const imageCount = note.image_count || (note.pictures ? note.pictures.length : 0); const price = note.price || 0; const isPaid = price > 0; const publishDate = note.publish_at ? new Date(note.publish_at) : null; // 基本情報の整形 return { // 記事基本情報 id: note.id || "", key: note.key || "", title: note.name || "", type: note.type || "TextNote", status: note.status || "published", publishedAt: note.publish_at || "", url: `https://note.com/${user.urlname || 'unknown'}/n/${note.key || ''}`, // エンゲージメント情報 likesCount: note.like_count || 0, commentsCount: note.comment_count || 0, // 実際の閲覧数が利用可能であれば追加 viewCount: note.view_count, // コンテンツ分析情報 contentAnalysis: analyzeContent ? { hasEyecatch, eyecatchUrl: note.eyecatch || note.sp_eyecatch || null, imageCount, hasVideo: note.type === "MovieNote" || Boolean(note.external_url), externalUrl: note.external_url || null, excerpt: note.body ? (note.body.length > 150 ? note.body.substr(0, 150) + '...' : note.body) : '', hasAudio: Boolean(note.audio), format: note.format || "unknown", highlightText: note.highlight || null } : null, // 価格情報 price, isPaid, priceInfo: note.price_info || { is_free: price === 0, has_multiple: false, has_subscription: false, oneshot_lowest_price: price }, // 設定情報 settings: { isLimited: note.is_limited || false, isTrial: note.is_trial || false, disableComment: note.disable_comment || false, isRefund: note.is_refund || false, isMembershipConnected: note.is_membership_connected || false, hasAvailableCirclePlans: note.has_available_circle_plans || false }, // 著者情報 author: { id: user.id || "", name: user.name || user.nickname || "", urlname: user.urlname || "", profileImageUrl: user.user_profile_image_path || "", // 詳細情報はオプションで制御 details: includeUserDetails ? { followerCount: user.follower_count || 0, followingCount: user.following_count || 0, noteCount: user.note_count || 0, profile: user.profile || "", twitterConnected: Boolean(user.twitter_nickname), twitterNickname: user.twitter_nickname || null, isOfficial: user.is_official || false, hasCustomDomain: Boolean(user.custom_domain), hasLikeAppeal: Boolean(user.like_appeal_text || user.like_appeal_image), hasFollowAppeal: Boolean(user.follow_appeal_text) } : null } }; }); // 分析結果の集計 const analytics = { totalFound: totalCount, analyzed: formattedNotes.length, query, sort, // エンゲージメント分析 engagementAnalysis: { averageLikes: formattedNotes.reduce((sum: number, note: any) => sum + note.likesCount, 0) / formattedNotes.length || 0, averageComments: formattedNotes.reduce((sum: number, note: any) => sum + note.commentsCount, 0) / formattedNotes.length || 0, maxLikes: Math.max(...formattedNotes.map((note: any) => note.likesCount)), maxComments: Math.max(...formattedNotes.map((note: any) => note.commentsCount)) }, // コンテンツタイプ分析 contentTypeAnalysis: analyzeContent ? { withEyecatch: formattedNotes.filter((note: any) => note.contentAnalysis?.hasEyecatch).length, withVideo: formattedNotes.filter((note: any) => note.contentAnalysis?.hasVideo).length, withAudio: formattedNotes.filter((note: any) => note.contentAnalysis?.hasAudio).length, averageImageCount: formattedNotes.reduce((sum: number, note: any) => sum + (note.contentAnalysis?.imageCount || 0), 0) / formattedNotes.length || 0 } : null, // 価格分析 priceAnalysis: { free: formattedNotes.filter((note: any) => !note.isPaid).length, paid: formattedNotes.filter((note: any) => note.isPaid).length, averagePrice: formattedNotes.filter((note: any) => note.isPaid).reduce((sum: number, note: any) => sum + note.price, 0) / formattedNotes.filter((note: any) => note.isPaid).length || 0, maxPrice: Math.max(...formattedNotes.map((note: any) => note.price)), minPrice: Math.min(...formattedNotes.filter((note: any) => note.isPaid).map((note: any) => note.price)) || 0 }, // 著者分析 authorAnalysis: includeUserDetails ? { uniqueAuthors: [...new Set(formattedNotes.map((note: any) => note.author.id))].length, averageFollowers: formattedNotes.reduce((sum: number, note: any) => sum + (note.author.details?.followerCount || 0), 0) / formattedNotes.length || 0, maxFollowers: Math.max(...formattedNotes.map((note: any) => note.author.details?.followerCount || 0)), officialAccounts: formattedNotes.filter((note: any) => note.author.details?.isOfficial).length, withTwitterConnection: formattedNotes.filter((note: any) => note.author.details?.twitterConnected).length, withCustomEngagement: formattedNotes.filter((note: any) => note.author.details?.hasLikeAppeal || note.author.details?.hasFollowAppeal).length } : null }; return { content: [ { type: "text", text: JSON.stringify({ analytics, notes: formattedNotes }, null, 2) } ] }; } catch (formatError) { console.error(`Error formatting analysis: ${formatError}`); return { content: [ { type: "text", text: `データの分析中にエラーが発生しました: ${formatError}\n元データ: ${JSON.stringify(data)}` } ] }; } } catch (error) { return { content: [ { type: "text", text: `分析に失敗しました: ${error}` } ], isError: true }; } } ); // 2. 記事詳細取得ツール 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() }); // APIのバージョンをv3に戻し、下書きパラメータを追加 const data = await noteApiRequest( `/v3/notes/${noteId}?${params.toString()}`, "GET", null, true // 認証必須 ); // 結果を見やすく整形 const noteData = data.data || {}; const formattedNote: FormattedNote = { id: noteData.id || "", title: noteData.name || "", body: noteData.body || "", user: { id: noteData.user?.id || "", name: noteData.user?.nickname || "", urlname: noteData.user?.urlname || "", bio: noteData.user?.bio || "", }, publishedAt: noteData.publishAt || "", likesCount: noteData.likeCount || 0, commentsCount: noteData.commentsCount || 0, status: noteData.status || "", url: `https://note.com/${noteData.user?.urlname || 'unknown'}/n/${noteData.key || ''}` }; return { content: [ { type: "text", text: JSON.stringify(formattedNote, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `記事の取得に失敗しました: ${error}` } ], isError: true }; } } ); // 3. ユーザー検索ツール server.tool( "search-users", "ユーザーを検索する", { query: z.string().describe("検索キーワード"), size: z.number().default(10).describe("取得する件数(最大20)"), start: z.number().default(0).describe("検索結果の開始位置"), }, async ({ query, size, start }) => { try { // ユーザー検索はv3を使用 const data = await noteApiRequest(`/v3/searches?context=user&q=${encodeURIComponent(query)}&size=${size}&start=${start}`); // 結果を見やすく整形 let formattedUsers: FormattedUser[] = []; if (data.data && data.data.users) { formattedUsers = data.data.users.map((user: NoteUser) => ({ id: user.id || "", nickname: user.nickname || "", urlname: user.urlname || "", bio: user.profile?.bio || '', followersCount: user.followersCount || 0, followingCount: user.followingCount || 0, notesCount: user.notesCount || 0, url: `https://note.com/${user.urlname || ''}` })); } return { content: [ { type: "text", text: JSON.stringify({ total: data.data?.usersCount || 0, users: formattedUsers }, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `検索に失敗しました: ${error}` } ], isError: true }; } } ); // 4. ユーザー詳細取得ツール server.tool( "get-user", "ユーザーの詳細情報を取得する", { username: z.string().describe("ユーザー名(例: princess_11)"), }, async ({ username }) => { try { const data = await noteApiRequest(`/v2/creators/${username}`); // 結果を見やすく整形 const userData = data.data || {}; // デバッグモードの場合はレスポンス全体をログに出力 if (DEBUG) { console.error(`User API Response: ${JSON.stringify(data, null, 2)}`); } // APIレスポンスの中で、フォロワー数のプロパティ名は followerCount (単数形) を使用 const formattedUser: FormattedUser = { id: userData.id || "", nickname: userData.nickname || "", urlname: userData.urlname || "", bio: userData.profile?.bio || '', // 両方のプロパティ名をチェックする followersCount: userData.followerCount || userData.followersCount || 0, followingCount: userData.followingCount || 0, notesCount: userData.noteCount || userData.notesCount || 0, magazinesCount: userData.magazineCount || userData.magazinesCount || 0, url: `https://note.com/${userData.urlname || ''}`, profileImageUrl: userData.profileImageUrl || '' }; return { content: [ { type: "text", text: JSON.stringify(formattedUser, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `ユーザー情報の取得に失敗しました: ${error}` } ], isError: true }; } } ); // 5. ユーザーの記事一覧取得ツール server.tool( "get-user-notes", "ユーザーの記事一覧を取得する", { username: z.string().describe("ユーザー名"), page: z.number().default(1).describe("ページ番号"), }, async ({ username, page }) => { try { const data = await noteApiRequest(`/v2/creators/${username}/contents?kind=note&page=${page}`); // 結果を見やすく整形 let formattedNotes: FormattedNote[] = []; if (data.data && data.data.contents) { formattedNotes = data.data.contents.map((note: Note) => ({ id: note.id || "", title: note.name || "", excerpt: note.body ? (note.body.length > 100 ? note.body.substr(0, 100) + '...' : note.body) : '本文なし', publishedAt: note.publishAt || '日付不明', likesCount: note.likeCount || 0, commentsCount: note.commentsCount || 0, user: username, url: `https://note.com/${username}/n/${note.key || ''}` })); } return { content: [ { type: "text", text: JSON.stringify({ total: data.data?.totalCount || 0, limit: data.data?.limit || 0, notes: formattedNotes }, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `記事一覧の取得に失敗しました: ${error}` } ], isError: true }; } } ); // 6. コメント一覧取得ツール server.tool( "get-comments", "記事へのコメント一覧を取得する", { noteId: z.string().describe("記事ID"), }, async ({ noteId }) => { try { const data = await noteApiRequest(`/v1/note/${noteId}/comments`); // 結果を見やすく整形 let formattedComments: FormattedComment[] = []; if (data.comments) { formattedComments = data.comments.map((comment: Comment) => ({ id: comment.id || "", body: comment.body || "", user: comment.user?.nickname || "匿名ユーザー", publishedAt: comment.publishAt || "" })); } return { content: [ { type: "text", text: JSON.stringify({ comments: formattedComments }, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `コメントの取得に失敗しました: ${error}` } ], isError: true }; } } ); // 7. 記事投稿ツール(下書き保存) 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 }) => { try { // 認証が必要なエンドポイント if (!hasAuth()) { return { content: [ { type: "text", text: "認証情報がないため、投稿できません。.envファイルに認証情報を設定してください。" } ], isError: true }; } // リクエスト内容をログに出力 console.error("下書き保存リクエスト内容:"); // 試行1: 最新のAPI形式で試行 try { console.error("試行1: 最新のAPI形式"); // v3のAPI形式に合わせて修正 const postData1 = { title: title, // タイトル body: body, // 本文 status: "draft", // 下書きステータス tags: tags || [], // タグ配列 publish_at: null, // 公開日時(下書きはヌル) eyecatch_image: null, // アイキャッチ画像 price: 0, // 価格(無料) is_magazine_note: false // マガジン記事かどうか }; console.error(`リクエスト内容: ${JSON.stringify(postData1, null, 2)}`); // 最新のAPIエンドポイントを使用する // v3のAPIを使用して下書きを保存 let endpoint = ""; if (id) { // 既存記事の編集 endpoint = `/v3/notes/${id}/draft`; } else { // 新規下書きの作成 endpoint = `/v3/notes/draft`; } const data = await noteApiRequest(endpoint, "POST", postData1, true); console.error(`成功: ${JSON.stringify(data, null, 2)}`); return { content: [ { type: "text", text: JSON.stringify({ success: true, data: data, message: "記事を下書き保存しました(試行1)" }, null, 2) } ] }; } 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)}`); // v1形式でもユーザーIDを指定 const endpoint = id ? `/v1/text_notes/draft_save?id=${id}&user_id=${NOTE_USER_ID}` : `/v1/text_notes/draft_save?user_id=${NOTE_USER_ID}`; const data = await noteApiRequest(endpoint, "POST", postData2, true); console.error(`成功: ${JSON.stringify(data, null, 2)}`); return { content: [ { type: "text", text: JSON.stringify({ success: true, data: data, message: "記事を下書き保存しました(試行2)" }, null, 2) } ] }; } catch (error2) { // どちらの試行も失敗した場合 console.error(`試行2でエラー: ${error2}`); return { content: [ { type: "text", text: `記事の投稿に失敗しました:\n試行1エラー: ${error1}\n試行2エラー: ${error2}\n\nセッションの有効期限が切れている可能性があります。.envファイルのCookie情報を更新してください。` } ], isError: true }; } } } catch (error) { console.error(`下書き保存処理全体でエラー: ${error}`); return { content: [ { type: "text", text: `記事の投稿に失敗しました: ${error}` } ], isError: true }; } } ); // 8. コメント投稿ツール server.tool( "post-comment", "記事にコメントを投稿する", { noteId: z.string().describe("記事ID"), text: z.string().describe("コメント本文"), }, async ({ noteId, text }) => { try { // 認証が必要なエンドポイント if (!hasAuth()) { return { content: [ { type: "text", text: "認証情報がないため、コメントできません。.envファイルに認証情報を設定してください。" } ], isError: true }; } const data = await noteApiRequest(`/v1/note/${noteId}/comments`, "POST", { text }, true); return { content: [ { type: "text", text: `コメントを投稿しました:\n${JSON.stringify(data, null, 2)}` } ] }; } catch (error) { return { content: [ { type: "text", text: `コメントの投稿に失敗しました: ${error}` } ], isError: true }; } } ); // 9. スキ取得ツール server.tool( "get-likes", "記事のスキ一覧を取得する", { noteId: z.string().describe("記事ID"), }, async ({ noteId }) => { try { // いいね一覧取得はv3を使用 const data = await noteApiRequest(`/v3/notes/${noteId}/likes`); // 結果を見やすく整形 let formattedLikes: FormattedLike[] = []; if (data.data && data.data.likes) { formattedLikes = data.data.likes.map((like: Like) => ({ id: like.id || "", createdAt: like.createdAt || "", user: like.user?.nickname || "匿名ユーザー" })); } return { content: [ { type: "text", text: JSON.stringify({ likes: formattedLikes }, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `スキ一覧の取得に失敗しました: ${error}` } ], isError: true }; } } ); // 10. スキをつけるツール server.tool( "like-note", "記事にスキをする", { noteId: z.string().describe("記事ID"), }, async ({ noteId }) => { try { // 認証が必要なエンドポイント if (!hasAuth()) { return { content: [ { type: "text", text: "認証情報がないため、スキできません。.envファイルに認証情報を設定してください。" } ], isError: true }; } // いいね追加はv3を使用 const data = await noteApiRequest(`/v3/notes/${noteId}/likes`, "POST", {}, true); return { content: [ { type: "text", text: "スキをつけました" } ] }; } catch (error) { return { content: [ { type: "text", text: `スキに失敗しました: ${error}` } ], isError: true }; } } ); // 11. スキを削除するツール server.tool( "unlike-note", "記事のスキを削除する", { noteId: z.string().describe("記事ID"), }, async ({ noteId }) => { try { // 認証が必要なエンドポイント if (!hasAuth()) { return { content: [ { type: "text", text: "認証情報がないため、スキの削除ができません。.envファイルに認証情報を設定してください。" } ], isError: true }; } // いいね削除はv3を使用 const data = await noteApiRequest(`/v3/notes/${noteId}/likes`, "DELETE", {}, true); return { content: [ { type: "text", text: "スキを削除しました" } ] }; } catch (error) { return { content: [ { type: "text", text: `スキの削除に失敗しました: ${error}` } ], isError: true }; } } ); // 12. マガジン検索ツール server.tool( "search-magazines", "マガジンを検索する", { query: z.string().describe("検索キーワード"), size: z.number().default(10).describe("取得する件数(最大20)"), start: z.number().default(0).describe("検索結果の開始位置"), }, async ({ query, size, start }) => { try { // マガジン検索はv3を使用 const data = await noteApiRequest(`/v3/searches?context=magazine&q=${encodeURIComponent(query)}&size=${size}&start=${start}`); // 結果を見やすく整形 let formattedMagazines: FormattedMagazine[] = []; if (data.data && data.data.magazines) { formattedMagazines = data.data.magazines.map((magazine: Magazine) => ({ id: magazine.id || "", name: magazine.name || "", description: magazine.description || "", notesCount: magazine.notesCount || 0, publishedAt: magazine.publishAt || "", user: magazine.user?.nickname || "匿名ユーザー", url: `https://note.com/${magazine.user?.urlname || ''}/m/${magazine.key || ''}` })); } return { content: [ { type: "text", text: JSON.stringify({ total: data.data?.magazinesCount || 0, magazines: formattedMagazines }, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `検索に失敗しました: ${error}` } ], isError: true }; } } ); // 13. マガジン詳細取得ツール server.tool( "get-magazine", "マガジンの詳細情報を取得する", { magazineId: z.string().describe("マガジンID(例: m75081e161aeb)"), }, async ({ magazineId }) => { try { const data = await noteApiRequest(`/v1/magazines/${magazineId}`); // 結果を見やすく整形 const magazineData = data.data || {}; const formattedMagazine: FormattedMagazine = { id: magazineData.id || "", name: magazineData.name || "", description: magazineData.description || "", notesCount: magazineData.notesCount || 0, publishedAt: magazineData.publishAt || "", user: magazineData.user?.nickname || "匿名ユーザー", url: `https://note.com/${magazineData.user?.urlname || ''}/m/${magazineData.key || ''}` }; return { content: [ { type: "text", text: JSON.stringify(formattedMagazine, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `マガジンの取得に失敗しました: ${error}` } ], isError: true }; } } ); // 14. カテゴリー記事一覧取得ツール server.tool( "get-category-notes", "カテゴリーに含まれる記事一覧を取得する", { category: z.string().describe("カテゴリー名(例: tech)"), page: z.number().default(1).describe("ページ番号"), sort: z.enum(["new", "trend"]).default("new").describe("ソート方法(new: 新着順, trend: 人気順)"), }, async ({ category, page, sort }) => { try { const data = await noteApiRequest(`/v1/categories/${category}?note_intro_only=true&sort=${sort}&page=${page}`); // 結果を見やすく整形 let formattedNotes: FormattedNote[] = []; if (data.data && data.data.notes) { formattedNotes = data.data.notes.map((note: Note) => ({ id: note.id || "", title: note.name || "", excerpt: note.body ? (note.body.length > 100 ? note.body.substr(0, 100) + '...' : note.body) : '本文なし', user: { nickname: note.user?.nickname || "", urlname: note.user?.urlname || "" }, publishedAt: note.publishAt || '日付不明', likesCount: note.likeCount || 0, url: `https://note.com/${note.user?.urlname || ''}/n/${note.key || ''}` })); } return { content: [ { type: "text", text: JSON.stringify({ category, page, notes: formattedNotes }, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `カテゴリー記事の取得に失敗しました: ${error}` } ], isError: true }; } } ); // 15. PV統計情報取得ツール server.tool( "get-stats", "ダッシュボードのPV統計情報を取得する", { filter: z.enum(["all", "day", "week", "month"]).default("all").describe("期間フィルター"), page: z.number().default(1).describe("ページ番号"), sort: z.enum(["pv", "date"]).default("pv").describe("ソート方法(pv: PV数順, date: 日付順)"), }, async ({ filter, page, sort }) => { try { // 認証が必要なエンドポイント if (!hasAuth()) { return { content: [ { type: "text", text: "認証情報がないため、統計情報を取得できません。.envファイルに認証情報を設定してください。" } ], isError: true }; } const data = await noteApiRequest(`/v1/stats/pv?filter=${filter}&page=${page}&sort=${sort}`, "GET", null, true); return { content: [ { type: "text", text: JSON.stringify(data, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `統計情報の取得に失敗しました: ${error}` } ], isError: true }; } } ); // 追加のAPIツール server.tool( "add-magazine-note", "マガジンに記事を追加する", { magazineId: z.string().describe("マガジンID(例: mxxxx)"), noteId: z.string().describe("記事ID(例: nxxxx)") }, async ({ magazineId, noteId }) => { try { if (!hasAuth()) throw new Error("認証情報が必要です。"); const data = await noteApiRequest(`/v1/our/magazines/${magazineId}/notes`, "POST", { id: noteId }, true); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } catch (e) { return { content: [{ type: "text", text: `マガジンへの記事追加に失敗: ${e}` }], isError: true }; } } ); server.tool( "remove-magazine-note", "マガジンから記事を削除する", { magazineId: z.string(), noteId: z.string() }, async ({ magazineId, noteId }) => { try { if (!hasAuth()) throw new Error("認証情報が必要です。"); const data = await noteApiRequest(`/v1/our/magazines/${magazineId}/notes/${noteId}`, "DELETE", null, true); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } catch (e) { return { content: [{ type: "text", text: `記事削除に失敗: ${e}` }], isError: true }; } } ); server.tool( "list-categories", "カテゴリー一覧を取得する", {}, async () => { try { const data = await noteApiRequest(`/v2/categories`, "GET"); return { content: [{ type: "text", text: JSON.stringify(data.data || data, null, 2) }] }; } catch (e) { return { content: [{ type: "text", text: `カテゴリー取得失敗: ${e}` }], isError: true }; } } ); server.tool( "list-hashtags", "ハッシュタグ一覧を取得する", {}, async () => { try { const data = await noteApiRequest(`/v2/hashtags`, "GET"); return { content: [{ type: "text", text: JSON.stringify(data.data || data, null, 2) }] }; } catch (e) { return { content: [{ type: "text", text: `一覧取得失敗: ${e}` }], isError: true }; } } ); server.tool( "get-hashtag", "ハッシュタグの詳細を取得する", { tag: z.string().describe("ハッシュタグ名") }, async ({ tag }) => { try { const data = await noteApiRequest(`/v2/hashtags/${encodeURIComponent(tag)}`, "GET"); return { content: [{ type: "text", text: JSON.stringify(data.data || data, null, 2) }] }; } catch (e) { return { content: [{ type: "text", text: `詳細取得失敗: ${e}` }], isError: true }; } } ); server.tool( "get-search-history", "検索履歴を取得する", {}, async () => { try { const data = await noteApiRequest(`/v2/search_histories`, "GET"); return { content: [{ type: "text", text: JSON.stringify(data.data || data, null, 2) }] }; } catch (e) { return { content: [{ type: "text", text: `履歴取得失敗: ${e}` }], isError: true }; } } ); server.tool( "list-contests", "コンテスト一覧を取得する", {}, async () => { try { const data = await noteApiRequest(`/v2/contests`, "GET"); return { content: [{ type: "text", text: JSON.stringify(data.data || data, null, 2) }] }; } catch (e) { return { content: [{ type: "text", text: `コンテスト取得失敗: ${e}` }], isError: true }; } } ); server.tool( "get-notice-counts", "通知件数を取得する", {}, async () => { // 通知件数取得はv3を使用 try { const data = await noteApiRequest(`/v3/notice_counts`, "GET"); return { content: [{ type: "text", text: JSON.stringify(data.data || data, null, 2) }] }; } catch (e) { return { content: [{ type: "text", text: `通知件数取得失敗: ${e}` }], isError: true }; } } ); // プロンプトの追加 // 検索用のプロンプトテンプレート server.prompt( "note-search", { query: z.string().describe("検索したいキーワード"), }, ({ query }) => ({ messages: [{ role: "user", content: { type: "text", text: `note.comで「${query}」に関する記事を検索して、要約してください。特に参考になりそうな記事があれば詳しく教えてください。` } }] }) ); // 競合分析プロンプト server.prompt( "competitor-analysis", { username: z.string().describe("分析したい競合のユーザー名"), }, ({ username }) => ({ messages: [{ role: "user", content: { type: "text", text: `note.comの「${username}」というユーザーの記事を分析して、以下の観点から教えてください:\n\n- 主なコンテンツの傾向\n- 人気記事の特徴\n- 投稿の頻度\n- エンゲージメントの高い記事の特徴\n- 差別化できそうなポイント` } }] }) ); // アイデア生成プロンプト server.prompt( "content-idea-generation", { topic: z.string().describe("記事のトピック"), }, ({ topic }) => ({ messages: [{ role: "user", content: { type: "text", text: `「${topic}」に関するnote.comの記事のアイデアを5つ考えてください。各アイデアには以下を含めてください:\n\n- キャッチーなタイトル案\n- 記事の概要(100文字程度)\n- 含めるべき主なポイント(3-5つ)\n- 差別化できるユニークな切り口` } }] }) ); // 記事分析プロンプト server.prompt( "article-analysis", { noteId: z.string().describe("分析したい記事のID"), }, ({ noteId }) => ({ messages: [{ role: "user", content: { type: "text", text: `note.comの記事ID「${noteId}」の内容を分析して、以下の観点から教えてください:\n\n- 記事の主なテーマと要点\n- 文章の構成と特徴\n- エンゲージメントを得ている要素\n- 改善できそうなポイント\n- 参考にできる文章テクニック` } }] }) ); // サーバーの起動 async function main() { try { console.error("Starting note API MCP Server..."); // メールアドレスとパスワードが設定されていれば自動ログインを試行 if (NOTE_EMAIL && NOTE_PASSWORD) { console.error("メールアドレスとパスワードからログイン試行中..."); const loginSuccess = await loginToNote(); if (loginSuccess) { console.error("ログイン成功: セッションCookieを取得しました。"); } else { console.error("ログイン失敗: メールアドレスまたはパスワードが正しくない可能性があります。"); } } // STDIOトランスポートを作成して接続 const transport = new StdioServerTransport(); await server.connect(transport); console.error("note API MCP Server is running on stdio transport"); // 認証状態を表示 if (activeSessionCookie || NOTE_SESSION_V5 || NOTE_XSRF_TOKEN) { console.error("認証情報が設定されています。認証が必要な機能も利用できます。"); } else { console.error("警告: 認証情報が設定されていません。読み取り機能のみ利用可能です。"); console.error("投稿、コメント、スキなどの機能を使うには.envファイルに認証情報を設定してください。"); } } catch (error) { console.error("Fatal error during server startup:", error); process.exit(1); } } // メンバーシップ(サークル)関連のツール // テスト用:ダミーデータを返すツール server.tool( "get-test-membership-summaries", "テスト用:加入済みメンバーシップ一覧をダミーデータで取得する", {}, async () => { try { // ダミーデータを作成 const dummySummaries = [ { id: "membership-1", key: "dummy-key-1", name: "テストメンバーシップ 1", urlname: "test-membership-1", price: 500, creator: { id: "creator-1", nickname: "テストクリエイター 1", urlname: "test-creator-1", profileImageUrl: "https://example.com/profile1.jpg" } }, { id: "membership-2", key: "dummy-key-2", name: "テストメンバーシップ 2", urlname: "test-membership-2", price: 1000, creator: { id: "creator-2", nickname: "テストクリエイター 2", urlname: "test-creator-2", profileImageUrl: "https://example.com/profile2.jpg" } } ]; return { content: [ { type: "text", text: JSON.stringify({ total: dummySummaries.length, summaries: dummySummaries }, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `テストデータ取得エラー: ${error}` } ], isError: true }; } } ); // テスト用:ダミーのメンバーシップ記事を取得するツール server.tool( "get-test-membership-notes", "テスト用:メンバーシップの記事一覧をダミーデータで取得する", { membershipKey: z.string().describe("メンバーシップキー(例: dummy-key-1)"), page: z.number().default(1).describe("ページ番号"), perPage: z.number().default(20).describe("ページあたりの記事数"), }, async ({ membershipKey, page, perPage }) => { try { // ダミーデータを作成 const membershipData = { id: "membership-id", key: membershipKey, name: `テストメンバーシップ (${membershipKey})`, description: "これはテスト用のメンバーシップ説明です。", creatorName: "テストクリエイター", price: 500, memberCount: 100, notesCount: 30 }; // 記事のダミーデータを生成 const dummyNotes = []; const startIndex = (page - 1) * perPage; const endIndex = startIndex + perPage; const totalNotes = 30; // 全体の記事数 for (let i = startIndex; i < Math.min(endIndex, totalNotes); i++) { dummyNotes.push({ id: `note-${i + 1}`, title: `テスト記事 ${i + 1}`, excerpt: `これはテスト記事 ${i + 1} の要約です。メンバーシップ限定コンテンツとなります。`, publishedAt: new Date(2025, 0, i + 1).toISOString(), likesCount: Math.floor(Math.random() * 100), commentsCount: Math.floor(Math.random() * 20), user: "テストクリエイター", url: `https://note.com/test-creator/n/n${i + 1}`, isMembersOnly: true }); } return { content: [ { type: "text", text: JSON.stringify({ total: totalNotes, page: page, perPage: perPage, membership: membershipData, notes: dummyNotes }, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `メンバーシップ記事取得エラー: ${error}` } ], isError: true }; } } ); // 1. 加入済みメンバーシップ一覧取得ツール server.tool( "get-membership-summaries", "加入済みメンバーシップ一覧を取得する", {}, async () => { try { // v2のメンバーシップサマリー取得APIを使用 const data = await noteApiRequest("/v2/circle/memberships/summaries", "GET", null, true); // DEBUGモードの場合のみ、レスポンスの詳細をログに出力 if (DEBUG) { console.error(`\n===== FULL Membership Summaries API Response =====\n${JSON.stringify(data, null, 2)}`); // 返却されたデータの型と構造を確認 console.error(`\nResponse type: ${typeof data}`); if (data && typeof data === 'object') { console.error(`Has data property: ${data.hasOwnProperty('data')}`); if (data.data) { console.error(`Data type: ${typeof data.data}`); console.error(`Is array: ${Array.isArray(data.data)}`); if (!Array.isArray(data.data) && typeof data.data === 'object') { // オブジェクトの場合、全てのキーを確認 console.error(`Data keys: ${Object.keys(data.data).join(', ')}`); // summariesプロパティがある場合 if (data.data.summaries) { console.error(`Has summaries property: ${data.data.hasOwnProperty('summaries')}`); console.error(`Summaries type: ${typeof data.data.summaries}`); console.error(`Summaries is array: ${Array.isArray(data.data.summaries)}`); console.error(`Summaries length: ${Array.isArray(data.data.summaries) ? data.data.summaries.length : 'N/A'}`); // 配列の場合、最初の要素を確認 if (Array.isArray(data.data.summaries) && data.data.summaries.length > 0) { console.error(`First summary item: ${JSON.stringify(data.data.summaries[0], null, 2)}`); // このオブジェクトのキーを確認 console.error(`First summary keys: ${Object.keys(data.data.summaries[0]).join(', ')}`); } } } } } } // 実際のAPIレスポンスからデータを抽出し、正しくフォーマットする let formattedSummaries: MembershipSummary[] = []; let rawSummaries: any[] = []; // 実際のAPIレスポンスの構造に合わせてデータ抽出ロジックを修正 if (data.data) { // APIが配列を直接返す場合 if (Array.isArray(data.data)) { if (DEBUG) console.error("Processing direct array data"); rawSummaries = data.data; } // summariesプロパティがある場合 else if (data.data.summaries && Array.isArray(data.data.summaries)) { if (DEBUG) console.error("Processing data.data.summaries"); rawSummaries = data.data.summaries; } // membership_summariesプロパティがある場合 else if (data.data.membership_summaries && Array.isArray(data.data.membership_summaries)) { if (DEBUG) console.error("Processing data.data.membership_summaries"); rawSummaries = data.data.membership_summaries; } // 其他の既知のプロパティを確認 else if (data.data.circles && Array.isArray(data.data.circles)) { if (DEBUG) console.error("Processing data.data.circles"); rawSummaries = data.data.circles; } else if (data.data.memberships && Array.isArray(data.data.memberships)) { if (DEBUG) console.error("Processing data.data.memberships"); rawSummaries = data.data.memberships; } // 如何なるプロパティも見つからない場合、全てのキーを確認してみる else { if (DEBUG) console.error(`No known array properties found. All keys in data.data: ${Object.keys(data.data).join(', ')}`); // 最初の配列を探す for (const key in data.data) { if (Array.isArray(data.data[key])) { if (DEBUG) console.error(`Found array property: ${key} with ${data.data[key].length} items`); rawSummaries = data.data[key]; break; } } } } if (DEBUG) console.error(`Raw summaries found: ${rawSummaries.length} items`); // MCPサーバーのフィルタリングを回避するための工夫 // 実際のデータを文字列化して送信 const apiDataRaw = JSON.stringify(data); // 生のデータを使ってマッピング if (rawSummaries.length > 0) { if (DEBUG) console.error(`First raw summary: ${JSON.stringify(rawSummaries[0], null, 2)}`); formattedSummaries = rawSummaries.map((summary: any) => { // 実際のAPIレスポンスではcircleプロパティにデータが入っている const circle = summary.circle || {}; const owner = circle.owner || {}; // 各フィールドの存在確認と取得を先に行う let id = "", key = "", name = "", urlname = "", price = 0; let creator: any = {}; // idの確認 - circleプロパティから取得 id = circle.id || summary.id || ""; // keyの確認 - circleプロパティから取得 key = circle.key || summary.key || ""; // nameの確認 - circleプロパティから取得 name = circle.name || summary.name || ""; // urlnameの確認 urlname = circle.urlname || owner.urlname || ""; // priceの確認 - 実際のAPIレスポンスには価格情報が含まれていない場合もある price = circle.price || summary.price || 0; // creator情報の確認 - ownerプロパティから取得 creator = { id: owner.id || "", nickname: owner.nickname || "", urlname: owner.urlname || "", profileImageUrl: owner.userProfileImagePath || "" }; // circlePlansの情報も抽出 const plans = summary.circlePlans || []; const planNames = plans.map((plan: any) => plan.name || "").filter((name: string) => name); return { id: id, key: key, name: name, urlname: urlname, price: price, description: circle.description || "", headerImagePath: summary.headerImagePath || circle.headerImagePath || "", creator: creator, plans: planNames, joinedAt: circle.joinedAt || "" }; }); if (DEBUG) console.error(`Formatted summaries: ${formattedSummaries.length} items`); } if (DEBUG) { console.error(`Returning real API data with ${formattedSummaries.length} formatted summaries`); if (formattedSummaries.length > 0) { console.error(`First formatted summary: ${JSON.stringify(formattedSummaries[0], null, 2)}`); } } return { content: [ { type: "text", text: JSON.stringify({ total: formattedSummaries.length, summaries: formattedSummaries }, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `メンバーシップ一覧取得エラー: ${error}` } ], isError: true }; } } ); // 2. 自分のメンバーシッププラン一覧取得ツール server.tool( "get-membership-plans", "自分のメンバーシッププラン一覧を取得する", {}, async () => { try { // v2のメンバーシッププラン取得APIを使用 const data = await noteApiRequest("/v2/circle/plans", "GET", null, true); // DEBUGモードの場合のみ、レスポンスの詳細をログに出力 if (DEBUG) { console.error(`\n===== FULL Membership Plans API Response =====\n${JSON.stringify(data, null, 2)}`); // 返却されたデータの型と構造を確認 console.error(`\nResponse type: ${typeof data}`); if (data && typeof data === 'object') { console.error(`Has data property: ${data.hasOwnProperty('data')}`); if (data.data) { console.error(`Data type: ${typeof data.data}`); console.error(`Is array: ${Array.isArray(data.data)}`); if (!Array.isArray(data.data) && typeof data.data === 'object') { // オブジェクトの場合、全てのキーを確認 console.error(`Data keys: ${Object.keys(data.data).join(', ')}`); // plansプロパティがある場合 if (data.data.plans) { console.error(`Has plans property: ${data.data.hasOwnProperty('plans')}`); console.error(`Plans type: ${typeof data.data.plans}`); console.error(`Plans is array: ${Array.isArray(data.data.plans)}`); console.error(`Plans length: ${Array.isArray(data.data.plans) ? data.data.plans.length : 'N/A'}`); // 配列の場合、最初の要素を確認 if (Array.isArray(data.data.plans) && data.data.plans.length > 0) { console.error(`First plan item: ${JSON.stringify(data.data.plans[0], null, 2)}`); // このオブジェクトのキーを確認 console.error(`First plan keys: ${Object.keys(data.data.plans[0]).join(', ')}`); } } } } } } // 実際のAPIレスポンスからデータを抽出し、正しくフォーマットする let formattedPlans: MembershipPlan[] = []; let rawPlans: any[] = []; // 実際のAPIレスポンスの構造に合わせてデータ抽出ロジックを修正 if (data.data) { // APIが配列を直接返す場合 if (Array.isArray(data.data)) { if (DEBUG) console.error("Processing direct array data"); rawPlans = data.data; } // plansプロパティがある場合 else if (data.data.plans && Array.isArray(data.data.plans)) { if (DEBUG) console.error("Processing data.data.plans"); rawPlans = data.data.plans; } // membership_plansプロパティがある場合 else if (data.data.membership_plans && Array.isArray(data.data.membership_plans)) { if (DEBUG) console.error("Processing data.data.membership_plans"); rawPlans = data.data.membership_plans; } // 其他の既知のプロパティを確認 else if (data.data.circle_plans && Array.isArray(data.data.circle_plans)) { if (DEBUG) console.error("Processing data.data.circle_plans"); rawPlans = data.data.circle_plans; } // 如何なるプロパティも見つからない場合、全てのキーを確認してみる else { if (DEBUG) console.error(`No known array properties found. All keys in data.data: ${Object.keys(data.data).join(', ')}`); // 最初の配列を探す for (const key in data.data) { if (Array.isArray(data.data[key])) { if (DEBUG) console.error(`Found array property: ${key} with ${data.data[key].length} items`); rawPlans = data.data[key]; break; } } } } if (DEBUG) console.error(`Raw plans found: ${rawPlans.length} items`); // 生のデータを使ってマッピング if (rawPlans.length > 0) { if (DEBUG) console.error(`First raw plan: ${JSON.stringify(rawPlans[0], null, 2)}`); formattedPlans = rawPlans.map((plan: any) => { // 実際のAPIレスポンスに合わせてプラン情報を抽出 const circle = plan.circle || {}; const circlePlans = plan.circlePlans || []; const owner = circle.owner || {}; // 各フィールドの存在確認と取得 let id = "", key = "", name = "", description = "", status = ""; let price = 0, memberCount = 0, notesCount = 0; // idの確認 - circleプロパティから取得 id = circle.id || plan.id || ""; // keyの確認 - circleプロパティから取得 key = circle.key || plan.key || ""; // nameの確認 - circlePlansから取得するか、circleから取得 if (circlePlans && circlePlans.length > 0) { name = circlePlans[0].name || ""; } else { name = circle.name || plan.name || ""; } // descriptionの確認 description = circle.description || plan.description || ""; // priceの確認 - 実際のAPIレスポンスには直接含まれていない場合もある price = plan.price || circle.price || 0; // memberCountの確認 memberCount = circle.subscriptionCount || circle.membershipNumber || 0; // notesCountの確認 - APIレスポンスに含まれていない場合は0 notesCount = plan.notesCount || 0; // statusの確認 status = circle.isCirclePublished ? "active" : "inactive"; return { id: id, key: key, name: name, description: description, price: price, memberCount: memberCount, notesCount: notesCount, status: status, ownerName: owner.nickname || owner.name || "", headerImagePath: plan.headerImagePath || circle.headerImagePath || "", plans: circlePlans.map((p: any) => p.name || "").filter((n: string) => n), url: owner.customDomain ? `https://${owner.customDomain.host}/membership` : `https://note.com/${owner.urlname || ""}/membership` }; }); } if (DEBUG) { console.error(`Formatted plans: ${formattedPlans.length} items`); if (formattedPlans.length > 0) { console.error(`First formatted plan: ${JSON.stringify(formattedPlans[0], null, 2)}`); } } return { content: [ { type: "text", text: JSON.stringify({ total: formattedPlans.length, plans: formattedPlans }, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `メンバーシッププラン取得エラー: ${error}` } ], isError: true }; } } ); // 3. サークル情報取得ツール server.tool( "get-circle-info", "サークル情報を取得する", {}, async () => { try { // v2のサークル情報取得APIを使用 const data = await noteApiRequest("/v2/circle", "GET", null, true); if (DEBUG) { console.error(`\nCircle Info API Response:\n${JSON.stringify(data, null, 2)}`); } // 実際のレスポンス構造を確認して整形したデータを返す const circleData = data.data || {}; // 必要なプロパティが存在するか確認し、適切なデフォルト値を設定 const formattedCircleInfo = { id: circleData.id || "", name: circleData.name || "", description: circleData.description || "", urlname: circleData.urlname || "", iconUrl: circleData.icon_url || "", createdAt: circleData.created_at || "", updatedAt: circleData.updated_at || "", isPublic: circleData.is_public || false, planCount: circleData.plan_count || 0, memberCount: circleData.member_count || 0, noteCount: circleData.note_count || 0, userId: circleData.user_id || "" }; return { content: [ { type: "text", text: JSON.stringify(formattedCircleInfo, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `サークル情報取得エラー: ${error}` } ], isError: true }; } } ); // 4. メンバーシップ記事一覧取得ツール server.tool( "get-membership-notes", "メンバーシップの記事一覧を取得する", { membershipKey: z.string().describe("メンバーシップキー(例: fed4670a87bc)"), page: z.number().default(1).describe("ページ番号"), perPage: z.number().default(20).describe("ページあたりの記事数"), }, async ({ membershipKey, page, perPage }) => { try { if (DEBUG) { console.error(`Getting membership notes for membershipKey: ${membershipKey}, page: ${page}, perPage: ${perPage}`); } // v3のメンバーシップ記事一覧取得APIを使用 const data = await noteApiRequest(`/v3/memberships/${membershipKey}/notes?page=${page}&per=${perPage}`, "GET", null, true); if (DEBUG) { console.error(`\n===== FULL Membership Notes API Response =====\n${JSON.stringify(data, null, 2)}`); // 得られたレスポンスの構造を確認 console.error(`Response type: ${typeof data}`); if (data && typeof data === 'object') { console.error(`Has data property: ${data.hasOwnProperty('data')}`); if (data.data) { // 構造の分析 console.error(`Data type: ${typeof data.data}`); console.error(`Is array: ${Array.isArray(data.data)}`); if (!Array.isArray(data.data) && typeof data.data === 'object') { console.error(`Data keys: ${Object.keys(data.data).join(', ')}`); // notesプロパティの確認 if (data.data.notes) { console.error(`Notes is array: ${Array.isArray(data.data.notes)}`); console.error(`Notes length: ${Array.isArray(data.data.notes) ? data.data.notes.length : 'N/A'}`); } // itemsプロパティの確認 if (data.data.items) { console.error(`Items is array: ${Array.isArray(data.data.items)}`); console.error(`Items length: ${Array.isArray(data.data.items) ? data.data.items.length : 'N/A'}`); } // membership情報の確認 if (data.data.membership) { console.error(`Has membership info: ${typeof data.data.membership}`); console.error(`Membership keys: ${Object.keys(data.data.membership).join(', ')}`); } } } } } // 結果を見やすく整形 let formattedNotes: FormattedMembershipNote[] = []; let totalCount = 0; let membershipInfo: any = {}; // 実際のAPIレスポンスの構造に合わせてデータ抽出ロジックを修正 if (data.data) { // notesプロパティがある場合 if (data.data.notes && Array.isArray(data.data.notes)) { formattedNotes = data.data.notes.map((note: any) => ({ id: note.id || "", title: note.name || note.title || "", excerpt: note.body ? (note.body.length > 100 ? note.body.substr(0, 100) + '...' : note.body) : '本文なし', publishedAt: note.publishAt || note.published_at || note.createdAt || note.created_at || '日付不明', likesCount: note.likeCount || note.likes_count || 0, commentsCount: note.commentsCount || note.comments_count || 0, user: note.user?.nickname || note.creator?.nickname || "", url: note.url || (note.user ? `https://note.com/${note.user.urlname}/n/${note.key || ''}` : ''), isMembersOnly: note.is_members_only || note.isMembersOnly || true })); totalCount = data.data.totalCount || data.data.total_count || data.data.total || formattedNotes.length; membershipInfo = data.data.membership || data.data.circle || {}; } // itemsプロパティがある場合 else if (data.data.items && Array.isArray(data.data.items)) { formattedNotes = data.data.items.map((note: any) => ({ id: note.id || "", title: note.name || note.title || "", excerpt: note.body ? (note.body.length > 100 ? note.body.substr(0, 100) + '...' : note.body) : '本文なし', publishedAt: note.publishAt || note.published_at || note.createdAt || note.created_at || '日付不明', likesCount: note.likeCount || note.likes_count || 0, commentsCount: note.commentsCount || note.comments_count || 0, user: note.user?.nickname || note.creator?.nickname || "", url: note.url || (note.user ? `https://note.com/${note.user.urlname}/n/${note.key || ''}` : ''), isMembersOnly: note.is_members_only || note.isMembersOnly || true })); totalCount = data.data.totalCount || data.data.total_count || data.data.total || formattedNotes.length; membershipInfo = data.data.membership || data.data.circle || {}; } // 配列が直接返される場合 else if (Array.isArray(data.data)) { formattedNotes = data.data.map((note: any) => ({ id: note.id || "", title: note.name || note.title || "", excerpt: note.body ? (note.body.length > 100 ? note.body.substr(0, 100) + '...' : note.body) : '本文なし', publishedAt: note.publishAt || note.published_at || note.createdAt || note.created_at || '日付不明', likesCount: note.likeCount || note.likes_count || 0, commentsCount: note.commentsCount || note.comments_count || 0, user: note.user?.nickname || note.creator?.nickname || "", url: note.url || (note.user ? `https://note.com/${note.user.urlname}/n/${note.key || ''}` : ''), isMembersOnly: note.is_members_only || note.isMembersOnly || true })); totalCount = formattedNotes.length; } } // メンバーシップ情報を整形 const formattedMembership = { id: membershipInfo?.id || "", key: membershipInfo?.key || membershipKey || "", name: membershipInfo?.name || "", description: membershipInfo?.description || "", creatorName: membershipInfo?.creator?.nickname || membershipInfo?.creatorName || "", price: membershipInfo?.price || 0, memberCount: membershipInfo?.memberCount || membershipInfo?.member_count || 0, notesCount: membershipInfo?.notesCount || membershipInfo?.notes_count || 0 }; return { content: [ { type: "text", text: JSON.stringify({ total: totalCount, page: page, perPage: perPage, membership: formattedMembership, notes: formattedNotes }, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `メンバーシップ記事取得エラー: ${error}` } ], isError: true }; } } ); // 自分の記事一覧(下書きを含む)取得ツール 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 (!NOTE_USER_ID) { return { content: [{ type: "text", text: "環境変数 NOTE_USER_ID が設定されていません。.envファイルを確認してください。" }], isError: true }; } // 記事一覧を取得するパラメータを設定 const params = new URLSearchParams({ page: page.toString(), per_page: perPage.toString(), draft: "true", // 下書きも含める draft_reedit: "false", // 再編集モードは含めない ts: Date.now().toString() }); // status フィルターの適用 if (status === "draft") { params.set("status", "draft"); } else if (status === "public") { params.set("status", "public"); } // 自分の記事一覧を取得 // APIパスから重複する "api/" を除去 // API_BASE_URLはすでに "https://note.com/api" を含んでいる const data = await noteApiRequest( `/v2/note_list/contents?${params.toString()}`, "GET", null, true // 認証必須 ); if (DEBUG) { console.error(`API Response: ${JSON.stringify(data, null, 2)}`); } // 結果を見やすく整形 let formattedNotes: FormattedNote[] = []; let totalCount = 0; let currentPage = 1; // デフォルトは1ページ目 if (data.data) { // notes配列がある場合、そこから記事情報を取得 if (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 || ""; // 下書き記事のタイトルと本文は noteDraft プロパティにある場合がある 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) { // HTMLタグを除去する簡易的な方法(Node.js環境用) // 正規表現を使用してHTMLタグを除去 const textContent = note.noteDraft.body ? note.noteDraft.body.replace(/<[^>]*>/g, '') // HTMLタグを除去 : ""; 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/${NOTE_USER_ID}/n/${noteKey}`, editUrl: `https://note.com/${NOTE_USER_ID}/n/${noteKey}/edit`, hasDraftContent: note.noteDraft ? true : false, // 下書き内容があるかどうか lastUpdated: note.noteDraft?.updatedAt || note.createdAt || "", // 最終更新日時 user: { id: note.user?.id || NOTE_USER_ID, name: note.user?.name || note.user?.nickname || "", urlname: note.user?.urlname || NOTE_USER_ID } }; }); } // 総件数とページ番号 totalCount = data.data.totalCount || 0; // クエリパラメータから現在のページ番号を取得 currentPage = page; } return { content: [ { type: "text", text: JSON.stringify({ total: totalCount, page: currentPage, perPage: perPage, status: status, totalPages: Math.ceil(totalCount / perPage), hasNextPage: currentPage * perPage < totalCount, hasPreviousPage: currentPage > 1, draftCount: formattedNotes.filter(note => note.isDraft).length, publicCount: formattedNotes.filter(note => !note.isDraft).length, notes: formattedNotes }, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `記事一覧の取得に失敗しました: ${error}` } ], isError: true }; } } ); // 記事編集ページを開くツール server.tool( "open-note-editor", "記事の編集ページを開く", { noteId: z.string().describe("記事ID(例: n1a2b3c4d5e6)"), }, async ({ noteId }) => { try { if (!NOTE_USER_ID) { return { content: [{ type: "text", text: "環境変数 NOTE_USER_ID が設定されていません。.envファイルを確認してください。" }], isError: true }; } // noteIdからキーを抽出(必要に応じて) let noteKey = noteId; if (noteId.startsWith('n')) { noteKey = noteId; } // 編集URLを生成 const editUrl = `https://note.com/${NOTE_USER_ID}/n/${noteKey}/edit`; return { content: [ { type: "text", text: JSON.stringify({ status: "success", editUrl: editUrl, message: `編集ページのURLを生成しました。以下のURLを開いてください:\n${editUrl}` }, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `編集ページURLの生成に失敗しました: ${error}` } ], isError: true }; } } ); // 全体検索ツール server.tool( "search-all", "note全体検索(ユーザー、ハッシュタグ、記事など)", { query: z.string().describe("検索キーワード"), context: z.string().default("user,hashtag,note").describe("検索コンテキスト(user,hashtag,noteなどをカンマ区切りで指定)"), mode: z.string().default("typeahead").describe("検索モード(typeaheadなど)"), size: z.number().default(10).describe("取得する件数(最大5件)"), sort: z.enum(["new", "popular", "hot"]).default("hot").describe("ソート順(new: 新着順, popular: 人気順, hot: 急上昇)"), }, async ({ query, context, mode, size, sort }) => { try { // 認証なしで全体検索ができるか試す // API_BASE_URLはすでに "https://note.com/api" を含むため、パスから重複する "api/" を除去 const data = await noteApiRequest( `/v3/searches?context=${encodeURIComponent(context)}&mode=${encodeURIComponent(mode)}&q=${encodeURIComponent(query)}&size=${size}&sort=${sort}`, "GET", null, false // 認証なしで試す ); if (DEBUG) { console.error(`API Response: ${JSON.stringify(data, null, 2)}`); } // 全体検索結果を整形 // 結果型を明示的に定義 const result: { query: string; context: string; mode: string; size: number; results: { users?: any[]; hashtags?: any[]; notes?: any[]; [key: string]: any; }; } = { query, context, mode, size, results: {} }; // レスポンスのデータを整形 if (data.data) { // ユーザー検索結果 if (data.data.users && Array.isArray(data.data.users)) { result.results.users = data.data.users.map((user: any) => ({ id: user.id || "", nickname: user.nickname || "", urlname: user.urlname || "", bio: user.profile?.bio || user.bio || "", profileImageUrl: user.profileImageUrl || "", url: `https://note.com/${user.urlname || ''}` })); } // ハッシュタグ検索結果 if (data.data.hashtags && Array.isArray(data.data.hashtags)) { result.results.hashtags = data.data.hashtags.map((tag: any) => ({ name: tag.name || "", displayName: tag.displayName || tag.name || "", url: `https://note.com/hashtag/${tag.name || ''}` })); } // 記事検索結果 if (data.data.notes) { // notesの型を確認して処理 let notesArray: any[] = []; if (Array.isArray(data.data.notes)) { // notesが配列の場合 notesArray = data.data.notes; } else if (typeof data.data.notes === 'object' && data.data.notes !== null) { // notesがオブジェクトで、contentsプロパティを持つ場合 const notesObj = data.data.notes as { contents?: any[] }; if (notesObj.contents && Array.isArray(notesObj.contents)) { notesArray = notesObj.contents; } } result.results.notes = notesArray.map((note: any) => ({ id: note.id || "", title: note.name || note.title || "", excerpt: note.body ? (note.body.length > 100 ? note.body.substring(0, 100) + '...' : note.body) : '', user: note.user?.nickname || 'unknown', publishedAt: note.publishAt || note.publish_at || '', url: `https://note.com/${note.user?.urlname || 'unknown'}/n/${note.key || ''}` })); } } return { content: [ { type: "text", text: JSON.stringify(result, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `検索に失敗しました: ${error}` } ], isError: true }; } } ); main().catch(error => { console.error("Fatal error:", error); process.exit(1); });

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