UNPKG

gemini-grounding-mcp

Version:

MCP server for Gemini AI web search with grounding, featuring AI-powered summaries and batch search capabilities

896 lines (886 loc) 33.4 kB
#!/usr/bin/env node //#region rolldown:runtime var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion require("dotenv/config"); const node_fs = __toESM(require("node:fs")); const node_path = __toESM(require("node:path")); const __modelcontextprotocol_sdk_server_index_js = __toESM(require("@modelcontextprotocol/sdk/server/index.js")); const __modelcontextprotocol_sdk_server_stdio_js = __toESM(require("@modelcontextprotocol/sdk/server/stdio.js")); const __modelcontextprotocol_sdk_types_js = __toESM(require("@modelcontextprotocol/sdk/types.js")); const __google_generative_ai = __toESM(require("@google/generative-ai")); const node_os = __toESM(require("node:os")); const dotenv = __toESM(require("dotenv")); //#region src/const.ts const OAUTH_CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"; const OAUTH_CLIENT_SECRET = "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"; //#endregion //#region src/auth/oauth2.ts var OAuth2Client = class { oauthPath = (0, node_path.join)((0, node_os.homedir)(), ".gemini", "oauth_creds.json"); tokenEndpoint = "https://oauth2.googleapis.com/token"; async getValidToken() { const token = this.loadToken(); if (!token) throw new Error("No OAuth token found. Please authenticate using 'gemini' command first."); const now = Date.now(); if (token.expiry_date && token.expiry_date > now) return token.access_token; console.log("OAuth token expired, refreshing..."); try { const refreshedToken = await this.refreshToken(token.refresh_token); this.saveToken(refreshedToken); return refreshedToken.access_token; } catch (error) { console.error("Token refresh failed:", error); throw new Error("OAuth token has expired and refresh failed. Please re-authenticate using 'gemini' command."); } } loadToken() { try { const data = (0, node_fs.readFileSync)(this.oauthPath, "utf8"); return JSON.parse(data); } catch { return null; } } saveToken(token) { try { (0, node_fs.writeFileSync)(this.oauthPath, JSON.stringify(token, null, 2)); } catch (error) { console.error("Failed to save OAuth token:", error); } } async refreshToken(refreshToken) { const params = new URLSearchParams({ client_id: OAUTH_CLIENT_ID, client_secret: OAUTH_CLIENT_SECRET, refresh_token: refreshToken, grant_type: "refresh_token" }); const response = await fetch(this.tokenEndpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params.toString() }); if (!response.ok) { const error = await response.text(); throw new Error(`Failed to refresh token: ${error}`); } const data = await response.json(); const expiryDate = Date.now() + data.expires_in * 1e3; return { access_token: data.access_token, refresh_token: refreshToken, token_type: data.token_type, expiry_date: expiryDate }; } }; //#endregion //#region src/auth/config.ts dotenv.default.config(); var AuthConfig = class { apiKey = null; oauthToken = null; refreshToken = null; tokenExpiry = null; authMethod = null; oauth2Client = null; constructor() { this._initialize(); } _initialize() { if (process.env.GEMINI_API_KEY) { this.apiKey = process.env.GEMINI_API_KEY; this.authMethod = "api-key"; return; } try { const oauthPath = (0, node_path.join)((0, node_os.homedir)(), ".gemini", "oauth_creds.json"); const oauthData = JSON.parse((0, node_fs.readFileSync)(oauthPath, "utf8")); if (oauthData.access_token) { this.oauthToken = oauthData.access_token; this.refreshToken = oauthData.refresh_token; if (oauthData.expiry_date) this.tokenExpiry = new Date(oauthData.expiry_date); this.authMethod = "oauth"; this.oauth2Client = new OAuth2Client(); return; } } catch (_error) {} throw new Error("No authentication method found. Please set GEMINI_API_KEY environment variable or run \"gemini auth login\""); } isApiKey() { return this.authMethod === "api-key"; } isOAuth() { return this.authMethod === "oauth"; } getApiKey() { if (!this.isApiKey() || !this.apiKey) throw new Error("API key not available"); return this.apiKey; } async getOAuthToken() { if (!this.isOAuth() || !this.oauth2Client) throw new Error("OAuth not available"); const token = await this.oauth2Client.getValidToken(); this.oauthToken = token; return token; } async getHeaders() { if (this.isOAuth()) { const token = await this.getOAuthToken(); return { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }; } return {}; } }; //#endregion //#region src/utils/formatter.ts function formatSearchResult$1(response, query) { if (!response || !response.text) return { query, summary: `No results found for query: "${query}"`, citations: [] }; return { query, summary: response.text, citations: response.citations || [] }; } function formatBatchResults(results) { return { totalQueries: results.length, results: results.map((result) => ({ query: result.query, summary: result.summary, citations: result.citations || [], searchResults: result.searchResults || [], scrapedContent: result.scrapedContent || [], error: result.error, searchResultCount: result.searchResultCount, targetResultCount: result.targetResultCount })) }; } function extractSearchResults(groundingMetadata) { if (!groundingMetadata || !groundingMetadata.groundingSupports) return []; const results = []; const seen = /* @__PURE__ */ new Set(); for (const support of groundingMetadata.groundingSupports) if (support.groundingChunkIndices && support.groundingChunkIndices.length > 0) { const chunkIndex = support.groundingChunkIndices[0]; const chunk = groundingMetadata.groundingChunks?.[chunkIndex]; if (chunk?.web) { const url = chunk.web.uri; if (!seen.has(url)) { seen.add(url); results.push({ title: chunk.web.title || "Untitled", url, snippet: support.segment?.text || "" }); } } } const MAX_SEARCH_RESULTS = 5; return results.slice(0, MAX_SEARCH_RESULTS); } function insertCitations(text, groundingSupports) { if (!groundingSupports || groundingSupports.length === 0) return text; const sortedSupports = [...groundingSupports].sort((a, b) => { const aIndex = a.endIndex ?? a.segment?.endIndex ?? 0; const bIndex = b.endIndex ?? b.segment?.endIndex ?? 0; return bIndex - aIndex; }); let result = text; const insertedPositions = /* @__PURE__ */ new Set(); for (const support of sortedSupports) if (support.groundingChunkIndices && support.groundingChunkIndices.length > 0) { const citations = support.groundingChunkIndices.map((idx) => `[${idx + 1}]`).join(""); const position = support.endIndex ?? support.segment?.endIndex; if (!position) continue; if (!insertedPositions.has(position)) { insertedPositions.add(position); result = result.slice(0, position) + citations + result.slice(position); } } return result; } function formatError(error, context) { return { error: true, message: error.message || "An unknown error occurred", context, timestamp: (/* @__PURE__ */ new Date()).toISOString() }; } //#endregion //#region src/utils/scraper.ts let readabilityModule = null; const getReadability = async () => { if (!readabilityModule) readabilityModule = await import("@mizchi/readability"); return readabilityModule; }; var Scraper = class { cache = /* @__PURE__ */ new Map(); cacheTTL; scrapeTimeout; scrapeRetries; excerptLength; summaryLength; geminiClient; constructor(geminiClient$1) { this.geminiClient = geminiClient$1; this.cacheTTL = Number.parseInt(process.env.CACHE_TTL || "3600", 10) * 1e3; this.scrapeTimeout = Number.parseInt(process.env.SCRAPE_TIMEOUT || "10000", 10); this.scrapeRetries = Number.parseInt(process.env.SCRAPE_RETRIES || "3", 10); this.excerptLength = Number.parseInt(process.env.EXCERPT_LENGTH || "1000", 10); this.summaryLength = Number.parseInt(process.env.SUMMARY_LENGTH || "5000", 10); if (process.env.DEBUG === "true") console.log("Scraper configuration:", { excerptLength: this.excerptLength, summaryLength: this.summaryLength }); } async scrapeUrl(url, options) { const maxRetries = options?.retries ?? this.scrapeRetries; const contentMode = options?.contentMode ?? "full"; const maxContentLength = options?.maxContentLength ?? 1e4; const cached = this.cache.get(url); if (cached && Date.now() - cached.timestamp < this.cacheTTL) return cached.content; let lastError = null; for (let attempt = 1; attempt <= maxRetries; attempt++) try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.scrapeTimeout); const response = await fetch(url, { headers: { "User-Agent": "Mozilla/5.0 (compatible; GeminiGroundingMCP/1.0)" }, signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const html = await response.text(); const { extract, toMarkdown } = await getReadability(); const extracted = extract(html, { charThreshold: 100, url }); if (!extracted || !extracted.root) throw new Error("Failed to extract content from URL"); const fullMarkdown = toMarkdown(extracted.root); let processedContent; switch (contentMode) { case "excerpt": if (this.geminiClient && fullMarkdown.length > this.excerptLength * 1.5) try { processedContent = await this.geminiClient.summarize(fullMarkdown, this.excerptLength); } catch (error) { console.error("Failed to generate AI excerpt, falling back to truncation:", error); processedContent = fullMarkdown.slice(0, this.excerptLength); if (fullMarkdown.length > this.excerptLength) processedContent += "..."; } else { processedContent = fullMarkdown.slice(0, this.excerptLength); if (fullMarkdown.length > this.excerptLength) processedContent += "..."; } break; case "summary": if (this.geminiClient && fullMarkdown.length > this.summaryLength * 1.2) try { processedContent = await this.geminiClient.summarize(fullMarkdown, this.summaryLength); } catch (error) { console.error("Failed to generate AI summary, falling back to truncation:", error); processedContent = fullMarkdown.slice(0, this.summaryLength); if (fullMarkdown.length > this.summaryLength) processedContent += "\n\n[Content truncated for summary mode]"; } else { processedContent = fullMarkdown.slice(0, this.summaryLength); if (fullMarkdown.length > this.summaryLength) processedContent += "\n\n[Content truncated for summary mode]"; } break; default: if (fullMarkdown.length > maxContentLength) { processedContent = fullMarkdown.slice(0, maxContentLength); processedContent += `\n\n[Content truncated at ${maxContentLength} characters]`; } else processedContent = fullMarkdown; break; } const result = { url, title: extracted.metadata?.title || "Scraped Content", content: processedContent, scrapedAt: (/* @__PURE__ */ new Date()).toISOString() }; this.cache.set(url, { content: result, timestamp: Date.now() }); return result; } catch (error) { lastError = error instanceof Error ? error : /* @__PURE__ */ new Error("Unknown error"); console.error(`Failed to scrape ${url} (attempt ${attempt}/${maxRetries}):`, lastError.message); if (attempt < maxRetries) await new Promise((resolve) => setTimeout(resolve, 2 ** (attempt - 1) * 1e3)); } const errorMessage = lastError?.message || "Unknown error"; return { url, title: "Error", content: null, error: errorMessage, scrapedAt: (/* @__PURE__ */ new Date()).toISOString() }; } async scrapeUrls(urls, options) { const batchSize = Number.parseInt(process.env.BATCH_SIZE || "5", 10); const results = []; for (let i = 0; i < urls.length; i += batchSize) { const batch = urls.slice(i, i + batchSize); const batchResults = await Promise.all(batch.map((url) => this.scrapeUrl(url, options))); results.push(...batchResults); if (i + batchSize < urls.length) await new Promise((resolve) => setTimeout(resolve, 100)); } return results; } clearCache() { this.cache.clear(); } }; //#endregion //#region src/gemini/code-assist-client.ts var CodeAssistClient = class { baseURL = "https://cloudcode-pa.googleapis.com"; projectId = null; auth; constructor(auth) { this.auth = auth; } async makeAuthenticatedRequest(url, body) { const headers = await this.auth.getHeaders(); const response = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) }); return response; } async ensureProjectId() { if (this.projectId) return this.projectId; const loadResponse = await this.makeAuthenticatedRequest(`${this.baseURL}/v1internal:loadCodeAssist`, {}); if (!loadResponse.ok) { const error = await loadResponse.text(); throw new Error(`Failed to load Code Assist: ${error}`); } const loadData = await loadResponse.json(); if (loadData.cloudaicompanionProject) { this.projectId = loadData.cloudaicompanionProject; return this.projectId; } if (loadData.currentTier) {} const tiers = loadData.allowedTiers || []; const defaultTier = tiers.find((t) => t.isDefault); const selectedTier = defaultTier || tiers[0]; if (!selectedTier) throw new Error("No available tiers for Code Assist"); const onboardBody = { tier: selectedTier.id }; const onboardResponse = await this.makeAuthenticatedRequest(`${this.baseURL}/v1internal:onboardUser`, onboardBody); if (!onboardResponse.ok) { const error = await onboardResponse.text(); throw new Error(`Failed to onboard user: ${error}`); } const onboardData = await onboardResponse.json(); if (!onboardData.operation) throw new Error("No operation returned from onboarding"); let operation = onboardData.operation; const MAX_POLLING_RETRIES = 30; const POLLING_INTERVAL_MS = 1e3; let retries = 0; while (!operation.done && retries < MAX_POLLING_RETRIES) { await new Promise((resolve) => setTimeout(resolve, POLLING_INTERVAL_MS)); const opUrl = `${this.baseURL}/${operation.name}`; const opResponse = await fetch(opUrl, { method: "GET", headers: await this.auth.getHeaders() }); if (!opResponse.ok) { const error = await opResponse.text(); throw new Error(`Failed to get operation status: ${error}`); } operation = await opResponse.json(); retries++; } if (!operation.done) throw new Error("Onboarding operation timed out"); this.projectId = operation.response?.cloudaicompanionProject?.id || null; if (!this.projectId) throw new Error("Failed to obtain project ID from onboarding"); return this.projectId; } async generateContent(model, query) { const projectId = await this.ensureProjectId(); const request = { model, request: { contents: [{ role: "user", parts: [{ text: query }] }], tools: [{ googleSearch: {} }] }, project: projectId }; const MAX_RETRIES = 3; const INITIAL_DELAY_MS = 4e3; const MAX_DELAY_MS = 6e4; for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) try { const response = await this.makeAuthenticatedRequest(`${this.baseURL}/v1internal:generateContent`, request); if (!response.ok) { const errorText = await response.text(); if (response.status === 429 && attempt < MAX_RETRIES) { const delay = Math.min(INITIAL_DELAY_MS * 2 ** attempt, MAX_DELAY_MS); try { JSON.parse(errorText); } catch {} console.error(`Rate limit hit (attempt ${attempt + 1}/${MAX_RETRIES + 1}). Retrying in ${delay / 1e3} seconds...`); await new Promise((resolve) => setTimeout(resolve, delay)); continue; } throw new Error(`Code Assist API error: ${response.status} - ${errorText}`); } const result = await response.json(); return result.response || result; } catch (error) { if (attempt === MAX_RETRIES) throw error; const delay = Math.min(INITIAL_DELAY_MS * 2 ** attempt, MAX_DELAY_MS); console.error(`Request failed (attempt ${attempt + 1}/${MAX_RETRIES + 1}): ${error}. Retrying in ${delay / 1e3} seconds...`); await new Promise((resolve) => setTimeout(resolve, delay)); } throw new Error("Failed to generate content after all retries"); } }; //#endregion //#region src/gemini/client.ts var GeminiClient = class { auth; scraper; model = null; codeAssistClient = null; constructor() { this.auth = new AuthConfig(); this.scraper = new Scraper(this); this._initializeModel(); if (this.auth.isOAuth()) this.codeAssistClient = new CodeAssistClient(this.auth); } _initializeModel() { if (this.auth.isApiKey()) { const genAI = new __google_generative_ai.GoogleGenerativeAI(this.auth.getApiKey()); const searchTool = { googleSearchRetrieval: {} }; this.model = genAI.getGenerativeModel({ model: "gemini-2.5-flash", tools: [searchTool] }); } } async summarize(text, maxLength = 500) { try { const prompt = `Please provide a concise summary of the following text in about ${maxLength} characters. Focus on the main points and key information:\n\n${text}`; if (this.auth.isApiKey() && this.model) { const result = await this.model.generateContent(prompt); return result.response.text(); } else { const response = await this._oauthRequest(prompt); return response.candidates?.[0]?.content?.parts?.[0]?.text || "Summary generation failed"; } } catch (error) { console.error("Summarization error:", error); return `${text.slice(0, maxLength)}...`; } } async searchWithOptions(query, _options) { const result = await this.search(query); return result; } async search(query) { try { let response; if (this.auth.isApiKey() && this.model) { const result = await this.model.generateContent(query); response = result.response; const groundingMetadata = response.candidates?.[0]?.groundingMetadata; if (groundingMetadata?.groundingSupports) { let text = response.text(); text = insertCitations(text, groundingMetadata.groundingSupports); const result$1 = formatSearchResult$1({ text, citations: this._extractCitations(groundingMetadata) }, query); return result$1; } return formatSearchResult$1({ text: response.text(), citations: [] }, query); } else { const oauthResponse = await this._oauthSearch(query); const candidates = oauthResponse.candidates || oauthResponse.response?.candidates; if (!candidates || candidates.length === 0) throw new Error("No valid response from Code Assist API"); response = { candidates, text: () => candidates[0]?.content?.parts?.[0]?.text || "" }; const candidate = candidates[0]; if (candidate?.content?.parts?.[0]?.text) { let text = candidate.content.parts[0].text; const groundingMetadata = candidate.groundingMetadata; const citationPattern = /\[\d+\]/g; const existingCitations = text.match(citationPattern); if (existingCitations && existingCitations.length > 0) text = this._removeDuplicateContent(text); else if (groundingMetadata?.groundingSupports) text = insertCitations(text, groundingMetadata.groundingSupports); return formatSearchResult$1({ text, citations: this._extractCitations(groundingMetadata) }, query); } throw new Error("No valid response from Code Assist API"); } } catch (error) { console.error("Search error:", error); return formatError(error, { query }); } } async batchSearch(queries, options = { scrapeContent: true }) { const results = []; const DEFAULT_BATCH_SIZE = 5; const DEFAULT_RATE_LIMIT_DELAY_MS = 100; const batchSize = Number.parseInt(process.env.BATCH_SIZE || String(DEFAULT_BATCH_SIZE), 10); const delay = Number.parseInt(process.env.RATE_LIMIT_DELAY || String(DEFAULT_RATE_LIMIT_DELAY_MS), 10); let rateLimitErrors = 0; const RATE_LIMIT_THRESHOLD = 2; const RATE_LIMIT_BACKOFF_MULTIPLIER = 3; for (let i = 0; i < queries.length; i += batchSize) { const batch = queries.slice(i, i + batchSize); const batchPromises = batch.map(async (query) => { try { const searchResult = await this._searchWithDetails(query); const urls = searchResult.searchResults.map((r) => r.url); const scrapedContent = options.scrapeContent && urls.length > 0 ? await this.scraper.scrapeUrls(urls, { contentMode: options.contentMode, maxContentLength: options.maxContentLength }) : []; return { query, summary: searchResult.summary, citations: searchResult.citations, searchResults: searchResult.searchResults, scrapedContent, searchResultCount: searchResult.searchResults.length, targetResultCount: 5 }; } catch (error) { console.error(`Error processing query "${query}":`, error); const errorMessage = error.message; if (errorMessage.includes("429") || errorMessage.includes("RESOURCE_EXHAUSTED")) { rateLimitErrors++; return { query, error: `Failed to load Code Assist: ${JSON.stringify({ error: { code: 429, message: "Resource has been exhausted (e.g. check quota).", status: "RESOURCE_EXHAUSTED" } }, null, 2)}` }; } return { query, error: errorMessage }; } }); const batchResults = await Promise.all(batchPromises); results.push(...batchResults); if (i + batchSize < queries.length) if (rateLimitErrors >= RATE_LIMIT_THRESHOLD) { const backoffDelay = delay * RATE_LIMIT_BACKOFF_MULTIPLIER; console.error(`Rate limit errors detected (${rateLimitErrors}). Increasing delay to ${backoffDelay / 1e3} seconds for next batch.`); await new Promise((resolve) => setTimeout(resolve, backoffDelay)); rateLimitErrors = 0; } else await new Promise((resolve) => setTimeout(resolve, delay)); } return formatBatchResults(results); } async _searchWithDetails(query) { try { let response; if (this.auth.isApiKey() && this.model) { const result = await this.model.generateContent(query); response = result.response; } else { const apiResponse = await this._oauthSearch(query); const candidates = apiResponse.candidates || apiResponse.response?.candidates; response = { text: () => candidates?.[0]?.content?.parts?.[0]?.text || "", candidates }; } const groundingMetadata = response.candidates?.[0]?.groundingMetadata; const searchResults = extractSearchResults(groundingMetadata); let summary = response.text(); const citationPattern = /\[\d+\]/g; const existingCitations = summary.match(citationPattern); if (!existingCitations && groundingMetadata?.groundingSupports) summary = insertCitations(summary, groundingMetadata.groundingSupports); return { summary, searchResults, citations: this._extractCitations(groundingMetadata) }; } catch (error) { console.error("Search details error:", error); throw error; } } async _oauthSearch(query) { if (!this.codeAssistClient) throw new Error("Code Assist client not initialized"); const response = await this.codeAssistClient.generateContent("gemini-2.5-flash", query); return response; } async _oauthRequest(prompt) { if (!this.codeAssistClient) throw new Error("Code Assist client not initialized"); const response = await this.codeAssistClient.generateContent("gemini-2.5-flash", prompt); return response; } _extractCitations(groundingMetadata) { if (!groundingMetadata || !groundingMetadata.groundingChunks) return []; return groundingMetadata.groundingChunks.filter((chunk) => chunk.web).map((chunk, index) => ({ number: index + 1, title: chunk.web?.title || "Untitled", url: chunk.web?.uri || "" })); } async scrapeUrl(url) { return this.scraper.scrapeUrl(url); } _removeDuplicateContent(text) { const sentencePattern = /(?<=[.!?])(?:\[\d+\])*\s*/g; const sentences = text.split(sentencePattern).filter((s) => s.trim()); const seenSentences = /* @__PURE__ */ new Map(); const result = []; for (const sentence of sentences) if (sentence.trim()) { const normalizedKey = sentence.replace(/\[\d+\]/g, "").replace(/\s+/g, " ").trim().toLowerCase(); if (!seenSentences.has(normalizedKey)) { seenSentences.set(normalizedKey, sentence); result.push(sentence); } else { const existing = seenSentences.get(normalizedKey) || ""; const existingHasCitations = /\[\d+\]/.test(existing); const currentHasCitations = /\[\d+\]/.test(sentence); if (!existingHasCitations && currentHasCitations) { const index = result.indexOf(existing); if (index !== -1) { result[index] = sentence; seenSentences.set(normalizedKey, sentence); } } } } return result.join(". ").replace(/\.\s*\./g, ".").trim(); } }; //#endregion //#region src/index.ts const packageJson = JSON.parse((0, node_fs.readFileSync)((0, node_path.join)(__dirname, "..", "package.json"), "utf-8")); const server = new __modelcontextprotocol_sdk_server_index_js.Server({ name: "gemini-grounding", vendor: "gemini-grounding-mcp", version: packageJson.version, description: packageJson.description }, { capabilities: { tools: {} } }); let geminiClient; try { geminiClient = new GeminiClient(); } catch (error) { console.error("Failed to initialize Gemini client:", error); process.exit(1); } const TOOLS = [{ name: "google_search", description: "Uses Google Search via Gemini AI grounding to find information and provide synthesized answers with citations. Returns AI-generated summaries rather than raw search results.", inputSchema: { type: "object", properties: { query: { type: "string", description: "The search query to find information on the web" }, includeSearchResults: { type: "boolean", description: "Include raw search results in addition to AI summary", default: false }, maxResults: { type: "number", description: "Maximum number of search results to return", default: 5 } }, required: ["query"] } }, { name: "google_search_batch", description: "Search multiple queries in parallel and optionally scrape content from results. Processes up to 10 queries simultaneously for comprehensive research.", inputSchema: { type: "object", properties: { queries: { type: "array", items: { type: "string" }, description: "Array of search queries (max 10)", minItems: 1, maxItems: 10 }, scrapeContent: { type: "boolean", description: "Whether to scrape full content from search result URLs", default: true }, contentMode: { type: "string", enum: [ "excerpt", "summary", "full" ], description: "Content extraction mode: excerpt (AI summary ~1000 chars), summary (AI summary ~3000 chars), or full", default: "full" }, maxContentLength: { type: "number", description: "Maximum content length for full mode (default: 10000)", default: 1e4 } }, required: ["queries"] } }]; server.setRequestHandler(__modelcontextprotocol_sdk_types_js.ListToolsRequestSchema, async () => { return { tools: TOOLS }; }); server.setRequestHandler(__modelcontextprotocol_sdk_types_js.CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case "google_search": { if (!args?.query || typeof args.query !== "string") throw new __modelcontextprotocol_sdk_types_js.McpError(__modelcontextprotocol_sdk_types_js.ErrorCode.InvalidParams, "Query parameter is required and must be a string"); const result = await geminiClient.searchWithOptions(args.query, { includeSearchResults: args.includeSearchResults, maxResults: args.maxResults }); if ("error" in result && result.error) throw new __modelcontextprotocol_sdk_types_js.McpError(__modelcontextprotocol_sdk_types_js.ErrorCode.InternalError, result.message); return { content: [{ type: "text", text: formatSearchResult(result) }] }; } case "google_search_batch": { if (!args?.queries || !Array.isArray(args.queries)) throw new __modelcontextprotocol_sdk_types_js.McpError(__modelcontextprotocol_sdk_types_js.ErrorCode.InvalidParams, "Queries parameter is required and must be an array"); if (args.queries.length === 0 || args.queries.length > 10) throw new __modelcontextprotocol_sdk_types_js.McpError(__modelcontextprotocol_sdk_types_js.ErrorCode.InvalidParams, "Queries array must contain between 1 and 10 items"); const scrapeContent = args.scrapeContent !== false; const result = await geminiClient.batchSearch(args.queries, { scrapeContent, contentMode: args.contentMode, maxContentLength: args.maxContentLength }); return { content: [{ type: "text", text: formatBatchSearchResult(result) }] }; } default: throw new __modelcontextprotocol_sdk_types_js.McpError(__modelcontextprotocol_sdk_types_js.ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { if (error instanceof __modelcontextprotocol_sdk_types_js.McpError) throw error; console.error(`Error in tool ${name}:`, error); throw new __modelcontextprotocol_sdk_types_js.McpError(__modelcontextprotocol_sdk_types_js.ErrorCode.InternalError, error instanceof Error ? error.message : "An unknown error occurred"); } }); function formatSearchResult(result) { let output = `Query: "${result.query}"\n\n`; output += `${result.summary}\n`; if (result.citations && result.citations.length > 0) { output += "\nCitations:\n"; for (const citation of result.citations) output += `[${citation.number}] ${citation.title}\n ${citation.url}\n`; } return output; } function formatBatchSearchResult(result) { let output = `# Batch Search Results (${result.totalQueries} ${result.totalQueries === 1 ? "query" : "queries"})\n\n`; output += `${"=".repeat(50)}\n\n`; let queryIndex = 0; for (const queryResult of result.results) { queryIndex++; output += `## Query ${queryIndex}: "${queryResult.query}"\n\n`; if (queryResult.error) { output += `❌ **Error**: ${queryResult.error}\n\n`; output += `${"-".repeat(50)}\n\n`; continue; } if (queryResult.summary) output += `### Summary\n\n${queryResult.summary}\n\n`; if (queryResult.citations && queryResult.citations.length > 0) { output += `### Citations\n`; for (const citation of queryResult.citations) output += `[${citation.number}] ${citation.title}\n ${citation.url}\n`; output += "\n"; } if (queryResult.searchResults && queryResult.searchResults.length > 0) { const resultCount = queryResult.searchResultCount || queryResult.searchResults.length; const targetCount = queryResult.targetResultCount || 5; output += `### Search Results (${resultCount}/${targetCount})\n\n`; for (const [idx, result$1] of queryResult.searchResults.entries()) { output += `**${idx + 1}. ${result$1.title}**\n`; output += `- URL: ${result$1.url}\n`; if (result$1.snippet) output += `- Snippet: ${result$1.snippet}\n`; output += "\n"; } } if (queryResult.scrapedContent && queryResult.scrapedContent.length > 0) { output += `### Scraped Content\n\n`; let successCount = 0; let failureCount = 0; for (const content of queryResult.scrapedContent) if (content.error) { failureCount++; output += `#### ❌ Failed: ${content.title}\n`; output += `- URL: ${content.url}\n`; output += `- Error: ${content.error}\n\n`; } else { successCount++; output += `#### ✅ ${content.title}\n`; output += `- URL: ${content.url}\n`; if (content.content) { const contentPreview = content.content.slice(0, 200); output += `- Content Preview: ${contentPreview}${content.content.length > 200 ? "..." : ""}\n`; output += `- Full Length: ${content.content.length} characters\n`; } output += "\n"; } if (successCount > 0 || failureCount > 0) output += `📊 **Scraping Stats**: ${successCount} succeeded, ${failureCount} failed\n\n`; } output += `${"-".repeat(50)}\n\n`; } return output; } async function main() { const transport = new __modelcontextprotocol_sdk_server_stdio_js.StdioServerTransport(); await server.connect(transport); console.error("Gemini Grounding MCP server started"); } main().catch((error) => { console.error("Fatal error:", error); process.exit(1); }); //#endregion