#!/usr/bin/env node
/**
* MCP Server for Gemini API Documentation.
*
* This server provides tools to search and fetch Gemini API documentation,
* enabling LLMs to access up-to-date information about the Gemini API.
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { DOCS_BASE_URL, CHARACTER_LIMIT } from "./constants.js";
import { fetchHtml, parseDocPage, formatAsMarkdown, formatAsJson } from "./utils/parser.js";
import { searchDocs, formatSearchResultsAsMarkdown, formatSearchResultsAsJson } from "./utils/search.js";
// Response format enum
enum ResponseFormat {
MARKDOWN = "markdown",
JSON = "json",
}
// Create MCP server instance
const server = new McpServer({
name: "gemini-docs-mcp-server",
version: "1.0.0",
});
// ============================================================================
// Tool: search_gemini_docs
// ============================================================================
const SearchDocsInputSchema = z.object({
query: z.string()
.min(1, "Query must not be empty")
.max(200, "Query must not exceed 200 characters")
.describe("Search query to find relevant documentation pages (e.g., 'function calling', 'embeddings', 'authentication')"),
max_results: z.number()
.int()
.min(1)
.max(20)
.default(10)
.describe("Maximum number of results to return (default: 10)"),
response_format: z.nativeEnum(ResponseFormat)
.default(ResponseFormat.MARKDOWN)
.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable"),
}).strict();
type SearchDocsInput = z.infer<typeof SearchDocsInputSchema>;
server.registerTool(
"search_gemini_docs",
{
title: "Search Gemini API Documentation",
description: `Search the Gemini API documentation for relevant pages.
This tool searches through the Gemini API documentation index to find pages matching your query. It searches across page titles, categories, and keywords to find the most relevant documentation.
Args:
- query (string): Search query to find relevant documentation (e.g., "function calling", "embeddings", "rate limits")
- max_results (number): Maximum results to return, 1-20 (default: 10)
- response_format ('markdown' | 'json'): Output format (default: 'markdown')
Returns:
For JSON format: Structured data with schema:
{
"query": string, // The search query
"total": number, // Number of results found
"results": [
{
"title": string, // Page title
"path": string, // Documentation path (e.g., "function-calling")
"url": string, // Full URL to the documentation page
"category": string, // Category (e.g., "Core Capabilities")
"matchedKeywords": [] // Keywords that matched the query
}
]
}
Examples:
- "function calling" -> finds Function Calling documentation
- "embeddings" -> finds Embeddings documentation
- "authentication" or "api key" -> finds API Keys documentation
- "rate limits" -> finds Rate Limits documentation
- "vision" or "image" -> finds Image Understanding documentation
Use this tool first to find relevant documentation pages, then use fetch_gemini_doc to get the full content.`,
inputSchema: SearchDocsInputSchema,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
},
},
async (params: SearchDocsInput) => {
try {
const results = searchDocs(params.query, params.max_results);
let textContent: string;
if (params.response_format === ResponseFormat.MARKDOWN) {
textContent = formatSearchResultsAsMarkdown(results, params.query);
} else {
textContent = formatSearchResultsAsJson(results, params.query);
}
return {
content: [{ type: "text", text: textContent }],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: "text",
text: `Error searching documentation: ${errorMessage}`,
}],
isError: true,
};
}
}
);
// ============================================================================
// Tool: fetch_gemini_doc
// ============================================================================
const FetchDocInputSchema = z.object({
path: z.string()
.max(200, "Path must not exceed 200 characters")
.describe("Documentation path (e.g., 'function-calling', 'embeddings', 'quickstart'). Use empty string for the main overview page."),
response_format: z.nativeEnum(ResponseFormat)
.default(ResponseFormat.MARKDOWN)
.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable"),
}).strict();
type FetchDocInput = z.infer<typeof FetchDocInputSchema>;
server.registerTool(
"fetch_gemini_doc",
{
title: "Fetch Gemini API Documentation Page",
description: `Fetch and parse a specific Gemini API documentation page.
This tool fetches a documentation page from the Gemini API docs and extracts its content including title, sections, and code examples.
Args:
- path (string): The documentation path (e.g., "function-calling", "embeddings"). Use empty string "" for the main overview page.
- response_format ('markdown' | 'json'): Output format (default: 'markdown')
Returns:
For JSON format: Structured data with schema:
{
"url": string, // Full URL of the page
"title": string, // Page title
"description": string, // Brief description
"sections": [
{
"level": number, // Heading level (1-6)
"title": string, // Section title
"content": string // Section content
}
],
"codeExamples": [
{
"language": string, // Programming language
"code": string // Code snippet
}
],
"fullText": string // Full text content (truncated if too long)
}
Common paths:
- "" (empty) - Main overview page
- "quickstart" - Getting started guide
- "function-calling" - Function calling / tool use
- "embeddings" - Text embeddings
- "structured-output" - JSON structured output
- "text-generation" - Text generation basics
- "image-understanding" - Vision / image analysis
- "audio" - Audio understanding
- "live" - Live API (real-time streaming)
- "api-key" - API key setup
- "models" - Available models
- "pricing" - Pricing information
- "rate-limits" - Rate limits and quotas
Use search_gemini_docs first to find the correct path if you're unsure.`,
inputSchema: FetchDocInputSchema,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
async (params: FetchDocInput) => {
try {
// Normalize path
const cleanPath = params.path.replace(/^\/+|\/+$/g, "").replace(/^gemini-api\/docs\/?/, "");
const url = cleanPath ? `${DOCS_BASE_URL}/${cleanPath}` : DOCS_BASE_URL;
// Fetch and parse
const html = await fetchHtml(url);
const doc = parseDocPage(html, url);
// Format response
let textContent: string;
if (params.response_format === ResponseFormat.MARKDOWN) {
textContent = formatAsMarkdown(doc);
} else {
textContent = formatAsJson(doc);
}
// Check character limit
if (textContent.length > CHARACTER_LIMIT) {
textContent = textContent.substring(0, CHARACTER_LIMIT) +
"\n\n[Content truncated due to size limit. Use specific section queries for more detail.]";
}
return {
content: [{ type: "text", text: textContent }],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{
type: "text",
text: `Error fetching documentation: ${errorMessage}\n\nMake sure the path is correct. Use search_gemini_docs to find valid paths.`,
}],
isError: true,
};
}
}
);
// ============================================================================
// Main
// ============================================================================
async function main(): Promise<void> {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Gemini Docs MCP server running via stdio");
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});