Skip to main content
Glama

Claude TypeScript MCP Servers

by ukkz
brave-search.ts12.6 kB
/** * Brave検索APIを利用したModel Context Protocol(MCP)サーバーの実装 * このサーバーは、ウェブ検索とローカル検索の2つの機能を提供します */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; /** * MCPサーバーの初期化 * サーバー名、バージョン、機能を定義します */ const server = new McpServer({ name: "brave-search", version: "0.1.0", }); /** * APIキーの確認 * 環境変数からBrave APIキーを取得し、存在しない場合はエラーを表示して終了します */ const BRAVE_API_KEY = process.env.BRAVE_API_KEY!; if (!BRAVE_API_KEY) { console.error("Error: BRAVE_API_KEY environment variable is required"); process.exit(1); } /** * レート制限の設定 * APIの利用制限を定義:毎秒1リクエスト、毎月15000リクエストまで */ const RATE_LIMIT = { perSecond: 1, perMonth: 15000, }; /** * リクエストカウンターの初期化 * APIリクエストの回数を追跡するためのオブジェクト */ let requestCount = { second: 0, month: 0, lastReset: Date.now(), }; /** * レート制限をチェックする関数 * 現在のリクエスト頻度が制限を超えていないか確認し、超えている場合はエラーをスローします */ function checkRateLimit() { const now = Date.now(); if (now - requestCount.lastReset > 1000) { requestCount.second = 0; requestCount.lastReset = now; } if (requestCount.second >= RATE_LIMIT.perSecond || requestCount.month >= RATE_LIMIT.perMonth) { throw new Error("Rate limit exceeded"); } requestCount.second++; requestCount.month++; } /** * ウェブ検索結果のインターフェース定義 * BraveのWeb検索APIからのレスポンス形式を定義します */ interface BraveWeb { web?: { results?: Array<{ title: string; description: string; url: string; language?: string; published?: string; rank?: number; }>; }; locations?: { results?: Array<{ id: string; // Required by API title?: string; }>; }; } /** * 場所情報のインターフェース定義 * Braveのローカル検索APIで返される場所の詳細情報の形式を定義します */ interface BraveLocation { id: string; name: string; address: { streetAddress?: string; addressLocality?: string; addressRegion?: string; postalCode?: string; }; coordinates?: { latitude: number; longitude: number; }; phone?: string; rating?: { ratingValue?: number; ratingCount?: number; }; openingHours?: string[]; priceRange?: string; } /** * POI(Point of Interest)レスポンスのインターフェース定義 * ローカル検索で返される場所のリスト形式を定義します */ interface BravePoiResponse { results: BraveLocation[]; } /** * 場所の説明情報のインターフェース定義 * 場所IDと説明文の対応を定義します */ interface BraveDescription { descriptions: { [id: string]: string }; } /** * ウェブ検索引数の型チェック関数 * 引数が正しくウェブ検索に必要なフォーマットであるかを確認します */ function isBraveWebSearchArgs(args: unknown): args is { query: string; count?: number } { return ( typeof args === "object" && args !== null && "query" in args && typeof (args as { query: string }).query === "string" ); } /** * ローカル検索引数の型チェック関数 * 引数が正しくローカル検索に必要なフォーマットであるかを確認します */ function isBraveLocalSearchArgs(args: unknown): args is { query: string; count?: number } { return ( typeof args === "object" && args !== null && "query" in args && typeof (args as { query: string }).query === "string" ); } /** * ウェブ検索を実行する非同期関数 * BraveのWeb検索APIを呼び出し、結果を整形して返します * * @param query 検索クエリ * @param count 取得する結果の数(デフォルト: 10) * @param offset ページネーションのオフセット(デフォルト: 0) * @returns 整形された検索結果の文字列 */ async function performWebSearch(query: string, count: number = 10, offset: number = 0) { checkRateLimit(); const url = new URL("https://api.search.brave.com/res/v1/web/search"); url.searchParams.set("q", query); url.searchParams.set("count", Math.min(count, 20).toString()); // API limit url.searchParams.set("offset", offset.toString()); const response = await fetch(url, { headers: { Accept: "application/json", "Accept-Encoding": "gzip", "X-Subscription-Token": BRAVE_API_KEY, }, }); if (!response.ok) { throw new Error( `Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`, ); } const data = (await response.json()) as BraveWeb; // ウェブ検索結果のみを抽出 const results = (data.web?.results || []).map((result) => ({ title: result.title || "", description: result.description || "", url: result.url || "", })); return results .map((r) => `Title: ${r.title}\nDescription: ${r.description}\nURL: ${r.url}`) .join("\n\n"); } /** * ローカル検索を実行する非同期関数 * BraveのLocal検索APIを呼び出し、結果を整形して返します * ローカル結果が見つからない場合はウェブ検索にフォールバックします * * @param query 検索クエリ * @param count 取得する結果の数(デフォルト: 5) * @returns 整形された検索結果の文字列 */ async function performLocalSearch(query: string, count: number = 5) { checkRateLimit(); // 場所IDを取得するための初期検索 const webUrl = new URL("https://api.search.brave.com/res/v1/web/search"); webUrl.searchParams.set("q", query); webUrl.searchParams.set("search_lang", "en"); webUrl.searchParams.set("result_filter", "locations"); webUrl.searchParams.set("count", Math.min(count, 20).toString()); const webResponse = await fetch(webUrl, { headers: { Accept: "application/json", "Accept-Encoding": "gzip", "X-Subscription-Token": BRAVE_API_KEY, }, }); if (!webResponse.ok) { throw new Error( `Brave API error: ${webResponse.status} ${webResponse.statusText}\n${await webResponse.text()}`, ); } const webData = (await webResponse.json()) as BraveWeb; const locationIds = webData.locations?.results ?.filter((r): r is { id: string; title?: string } => r.id != null) .map((r) => r.id) || []; if (locationIds.length === 0) { return performWebSearch(query, count); // ウェブ検索へのフォールバック } // POIの詳細と説明を並列で取得 const [poisData, descriptionsData] = await Promise.all([ getPoisData(locationIds), getDescriptionsData(locationIds), ]); return formatLocalResults(poisData, descriptionsData); } /** * 場所の詳細情報を取得する非同期関数 * * @param ids 場所IDの配列 * @returns 場所の詳細情報 */ async function getPoisData(ids: string[]): Promise<BravePoiResponse> { checkRateLimit(); const url = new URL("https://api.search.brave.com/res/v1/local/pois"); ids.filter(Boolean).forEach((id) => url.searchParams.append("ids", id)); const response = await fetch(url, { headers: { Accept: "application/json", "Accept-Encoding": "gzip", "X-Subscription-Token": BRAVE_API_KEY, }, }); if (!response.ok) { throw new Error( `Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`, ); } const poisResponse = (await response.json()) as BravePoiResponse; return poisResponse; } /** * 場所の説明情報を取得する非同期関数 * * @param ids 場所IDの配列 * @returns 場所の説明情報 */ async function getDescriptionsData(ids: string[]): Promise<BraveDescription> { checkRateLimit(); const url = new URL("https://api.search.brave.com/res/v1/local/descriptions"); ids.filter(Boolean).forEach((id) => url.searchParams.append("ids", id)); const response = await fetch(url, { headers: { Accept: "application/json", "Accept-Encoding": "gzip", "X-Subscription-Token": BRAVE_API_KEY, }, }); if (!response.ok) { throw new Error( `Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`, ); } const descriptionsData = (await response.json()) as BraveDescription; return descriptionsData; } /** * ローカル検索結果を整形する関数 * 場所の詳細情報と説明を組み合わせて、読みやすい形式に整形します * * @param poisData 場所の詳細情報 * @param descData 場所の説明情報 * @returns 整形された検索結果の文字列 */ function formatLocalResults(poisData: BravePoiResponse, descData: BraveDescription): string { return ( (poisData.results || []) .map((poi) => { const address = [ poi.address?.streetAddress ?? "", poi.address?.addressLocality ?? "", poi.address?.addressRegion ?? "", poi.address?.postalCode ?? "", ] .filter((part) => part !== "") .join(", ") || "N/A"; return `Name: ${poi.name} Address: ${address} Phone: ${poi.phone || "N/A"} Rating: ${poi.rating?.ratingValue ?? "N/A"} (${poi.rating?.ratingCount ?? 0} reviews) Price Range: ${poi.priceRange || "N/A"} Hours: ${(poi.openingHours || []).join(", ") || "N/A"} Description: ${descData.descriptions[poi.id] || "No description available"} `; }) .join("\n---\n") || "No local results found" ); } /** * ウェブ検索ツールの実装 * BraveのWebサーチAPIを利用して検索を実行します */ server.tool( "brave_web_search", "Keyword-based web search returning a list of search results. Each result includes title, description, and URL. Best for quickly scanning multiple web pages or when you need to see diverse sources. Returns up to 20 search results as a list, not synthesized answers.", { query: z.string().describe("Search query (max 400 chars, 50 words)"), count: z.number().optional().describe("Number of results (1-20, default 10)"), offset: z.number().optional().describe("Pagination offset (max 9, default 0)"), }, async (args) => { try { if (!isBraveWebSearchArgs(args)) { throw new Error("Invalid arguments for brave_web_search"); } const { query, count = 10 } = args; const results = await performWebSearch(query, count); return { content: [{ type: "text", text: results }], isError: false, }; } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }, ); /** * ローカル検索ツールの実装 * BraveのLocalサーチAPIを利用して場所の検索を実行します */ server.tool( "brave_local_search", "Search local businesses, services, and places. Returns real-time data including address, phone, ratings, hours. Use for location-based queries.", { query: z.string().describe("Local search query (e.g. 'pizza near Central Park')"), count: z.number().optional().describe("Number of results (1-20, default 5)"), }, async (args) => { try { if (!isBraveLocalSearchArgs(args)) { throw new Error("Invalid arguments for brave_local_search"); } const { query, count = 5 } = args; const results = await performLocalSearch(query, count); return { content: [{ type: "text", text: results }], isError: false, }; } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }, ); // Start server async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Secure MCP Brave Web Search Server running on stdio"); } runServer().catch((error) => { console.error("Fatal error running server:", error); process.exit(1); });

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/ukkz/claude-ts-mcps'

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