Skip to main content
Glama
search-tools.ts10.2 kB
import { z } from "zod"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { noteApiRequest } from "../utils/api-client.js"; import { formatNote, formatUser, formatMagazine, analyzeNotes } from "../utils/formatters.js"; import { createSuccessResponse, createErrorResponse, handleApiError, safeExtractData, safeExtractTotal, commonExtractors } from "../utils/error-handler.js"; import { env } from "../config/environment.js"; export function registerSearchTools(server: McpServer) { // 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 { const data = await noteApiRequest(`/v3/searches?context=note&q=${encodeURIComponent(query)}&size=${size}&start=${start}&sort=${sort}`); if (env.DEBUG) { console.error(`API Response structure for search-notes: ${JSON.stringify(data, null, 2)}`); } if (!data || !data.data) { return createErrorResponse(`APIレスポンスが空です: ${JSON.stringify(data)}`); } if (data.status === "error" || data.error) { return createErrorResponse(`APIエラー: ${JSON.stringify(data)}`); } const notesArray = safeExtractData(data, commonExtractors.notes); const totalCount = safeExtractTotal(data, notesArray.length); const formattedNotes = notesArray.map(note => formatNote(note)); return createSuccessResponse({ total: totalCount, notes: formattedNotes, rawResponse: env.DEBUG ? data : undefined }); } catch (error) { return handleApiError(error, "記事検索"); } } ); // 2. 記事分析ツール 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); const data = await noteApiRequest(`/v3/searches?context=note&${params.toString()}`); if (env.DEBUG) { console.error(`API Response structure for analyze-notes: ${JSON.stringify(data, null, 2)}`); } if (!data || !data.data) { return createErrorResponse(`APIレスポンスが空です: ${JSON.stringify(data)}`); } if (data.status === "error" || data.error) { return createErrorResponse(`APIエラー: ${JSON.stringify(data)}`); } const notesArray = safeExtractData(data, commonExtractors.notes); const totalCount = safeExtractTotal(data, notesArray.length); const formattedNotes = notesArray.map(note => formatNote(note, undefined, includeUserDetails, analyzeContent) ); const analytics = analyzeNotes(formattedNotes, query, sort); return createSuccessResponse({ analytics, notes: formattedNotes }); } catch (error) { return handleApiError(error, "記事分析"); } } ); // 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 { const data = await noteApiRequest(`/v3/searches?context=user&q=${encodeURIComponent(query)}&size=${size}&start=${start}`); const usersArray = safeExtractData(data, commonExtractors.users); const totalCount = safeExtractTotal(data, usersArray.length); const formattedUsers = usersArray.map((user: any) => formatUser(user)); return createSuccessResponse({ total: totalCount, users: formattedUsers }); } catch (error) { return handleApiError(error, "ユーザー検索"); } } ); // 4. マガジン検索ツール 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 { const data = await noteApiRequest(`/v3/searches?context=magazine&q=${encodeURIComponent(query)}&size=${size}&start=${start}`); const magazinesArray = safeExtractData(data, commonExtractors.magazines); const totalCount = safeExtractTotal(data, magazinesArray.length); const formattedMagazines = magazinesArray.map((magazine: any) => formatMagazine(magazine)); return createSuccessResponse({ total: totalCount, magazines: formattedMagazines }); } catch (error) { return handleApiError(error, "マガジン検索"); } } ); // 5. 全体検索ツール 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 { const data = await noteApiRequest( `/v3/searches?context=${encodeURIComponent(context)}&mode=${encodeURIComponent(mode)}&q=${encodeURIComponent(query)}&size=${size}&sort=${sort}`, "GET", null, false ); const result = { query, context, mode, size, results: {} as any }; 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) { let notesArray: any[] = []; if (Array.isArray(data.data.notes)) { notesArray = data.data.notes; } else if (typeof data.data.notes === 'object' && data.data.notes !== null) { 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 createSuccessResponse(result); } catch (error) { return handleApiError(error, "全体検索"); } } ); }

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