DriversRecommendedOutdated drivers can make a good PC feel brokenScan driver issues before chasing fixes manually.Scan NowOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsSlow PC?RecommendedPC slow today? Run a repair scan before it gets worseResolve common Windows issues and optimize system performance.Scan Now×
Skip to content
Custom Search JSON API

How to Build a Google Search MCP Server in Python

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Build a Python MCP server that exposes Google Custom Search as a typed google_search tool. You need two Google credentials—a Programmable Search Engine ID (cx) and an API key—plus Python 3.10 or newer. The example below uses the MCP Python SDK’s high-level API, validates input, applies bounded retries and a timeout, and returns only normalized search results rather than Google’s full response.

What you are building

The server sits between an MCP host, such as a desktop assistant or other MCP client, and Google’s Custom Search JSON API. The host calls a tool with a query; Python validates the arguments, makes the authenticated HTTP request, and returns a small, predictable result. The client does not need to know Google’s parameter names or parse its full response.

The flow is: MCP host → MCP transport → Python tool → HTTP client → Google Custom Search JSON API → normalized MCP result. Keep the MCP-facing tool contract separate from the Google-specific adapter. If you later replace Google, you can preserve the tool name and result shape while changing the adapter.

This is a search integration, not a browser or general web-crawling interface. It returns the search API’s result links and snippets; it does not fetch, render, or verify the content of each destination page.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Set up Google credentials

Google’s API request requires an API key, a Programmable Search Engine identifier called cx, and a query called q. The engine ID and API key are separate values; having one does not replace the other.

  1. Create a Programmable Search Engine and copy its identifier (cx). Configure the engine for the sites or scope you intend it to search.
  2. In Google Cloud, create or select a project, enable the Custom Search JSON API, and create an API key for that project.
  3. Where Google Cloud’s controls allow it, restrict the key to the API and usage environment you need. Do not commit it to source control or paste it into an MCP client configuration that is shared publicly.
  4. Keep both values in environment variables. The sample reads GOOGLE_API_KEY and GOOGLE_CSE_ID when the tool is called.

The API endpoint used below is https://www.googleapis.com/customsearch/v1. Google can change API availability, requirements, or behavior, so verify the current Google Cloud setup and API terms when deploying.

Install Python dependencies

The official MCP Python SDK v2 line requires Python 3.10 or newer. Install the CLI extra to get the development tooling, and pin the SDK below the next major version so a major API change does not silently alter this server. The code uses httpx for asynchronous HTTP requests.

python --version
python -m venv .venv
source .venv/bin/activate
python -m pip install "mcp[cli]>=2,<3" httpx

On Windows PowerShell, activate the environment with .venvScriptsActivate.ps1 instead of the Unix source command. If you use a dependency manager, declare the same SDK constraint and httpx dependency in its project file and commit the lockfile.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Create a local .env file only if your own launcher loads it; the server below does not load dotenv files automatically. For a shell session, set the variables directly:

export GOOGLE_API_KEY="replace-with-your-api-key"
export GOOGLE_CSE_ID="replace-with-your-search-engine-id"
python server.py

In PowerShell, use $env:GOOGLE_API_KEY="..." and $env:GOOGLE_CSE_ID="...". Add .env to .gitignore if a dotenv loader is part of your chosen development setup.

Implement the MCP server

Save the following as server.py. The high-level SDK derives the tool’s input schema from the Python type hints. The tool accepts a non-empty query and a bounded result count, requests Google asynchronously, and returns stable fields: title, link, and snippet.

import asyncio
import os
from typing import Any

import httpx
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("Google Search")
GOOGLE_ENDPOINT = "https://www.googleapis.com/customsearch/v1"
MAX_QUERY_LENGTH = 500
MAX_RESULTS = 10


async def google_request(query: str, num_results: int) -> dict[str, Any]:
    """Call Google, retrying a small number of transient failures."""
    api_key = os.getenv("GOOGLE_API_KEY")
    cse_id = os.getenv("GOOGLE_CSE_ID")
    if not api_key or not cse_id:
        return {
            "ok": False,
            "error": {
                "code": "missing_configuration",
                "message": "Set GOOGLE_API_KEY and GOOGLE_CSE_ID in the server environment.",
            },
        }

    params = {
        "key": api_key,
        "cx": cse_id,
        "q": query,
        "num": num_results,
    }
    timeout = httpx.Timeout(15.0, connect=5.0)
    last_error = "Google search could not be completed."

    async with httpx.AsyncClient(timeout=timeout) as client:
        for attempt in range(3):
            try:
                response = await client.get(GOOGLE_ENDPOINT, params=params)
                if response.status_code == 429 or response.status_code >= 500:
                    last_error = f"Google returned HTTP {response.status_code}."
                    if attempt < 2:
                        await asyncio.sleep(0.5 * (2 ** attempt))
                        continue
                    return {
                        "ok": False,
                        "error": {"code": "upstream_unavailable", "message": last_error},
                    }

                if response.is_error:
                    # Do not pass Google’s complete response, which may contain
                    # implementation details, through to every MCP client.
                    return {
                        "ok": False,
                        "error": {
                            "code": "upstream_rejected_request",
                            "message": (
                                f"Google returned HTTP {response.status_code}. "
                                "Check the API key, engine ID, API enablement, and quota."
                            ),
                        },
                    }

                payload = response.json()
                results = [
                    {
                        "title": item.get("title", ""),
                        "link": item.get("link", ""),
                        "snippet": item.get("snippet", ""),
                    }
                    for item in payload.get("items", [])
                ]
                return {"ok": True, "query": query, "results": results}

            except httpx.TimeoutException:
                last_error = "Google search timed out."
                if attempt < 2:
                    await asyncio.sleep(0.5 * (2 ** attempt))
                    continue
                return {
                    "ok": False,
                    "error": {"code": "timeout", "message": last_error},
                }
            except httpx.RequestError:
                last_error = "Could not connect to Google Custom Search."
                if attempt < 2:
                    await asyncio.sleep(0.5 * (2 ** attempt))
                    continue
                return {
                    "ok": False,
                    "error": {"code": "network_error", "message": last_error},
                }
            except ValueError:
                return {
                    "ok": False,
                    "error": {
                        "code": "invalid_upstream_response",
                        "message": "Google returned a response that was not valid JSON.",
                    },
                }

    return {"ok": False, "error": {"code": "upstream_error", "message": last_error}}


@mcp.tool()
async def google_search(query: str, num_results: int = 5) -> dict[str, Any]:
    """Search Google Custom Search and return titles, links, and snippets."""
    clean_query = query.strip()
    if not clean_query:
        return {
            "ok": False,
            "error": {"code": "invalid_query", "message": "Query must not be blank."},
        }
    if len(clean_query) > MAX_QUERY_LENGTH:
        return {
            "ok": False,
            "error": {
                "code": "invalid_query",
                "message": f"Query must be {MAX_QUERY_LENGTH} characters or fewer.",
            },
        }
    if not 1 <= num_results <= MAX_RESULTS:
        return {
            "ok": False,
            "error": {
                "code": "invalid_num_results",
                "message": f"num_results must be between 1 and {MAX_RESULTS}.",
            },
        }
    return await google_request(clean_query, num_results)


if __name__ == "__main__":
    mcp.run(transport="stdio")

The application-level query limit of 500 characters is a defensive bound for this tool, not a claim about Google’s maximum query length. Likewise, the result count is constrained to 1–10 because the API’s num parameter is bounded; change the tool contract only after checking Google’s current API reference.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The example treats a missing items field as an empty result list. HTTP 429 and 5xx responses, timeouts, and connection failures receive at most three attempts with short capped delays; other HTTP errors are returned without retrying. Error objects are ordinary structured tool results with ok: false, rather than exceptions that might expose upstream internals. Snippets and links are remote content: display or use them as untrusted data, and do not treat a snippet as verified page content.

Run it locally and choose a transport

Run python server.py to start the server in stdio mode. This is the most direct option for a local desktop host or another process that launches the server. The process lifetime is tied to that host, and tool traffic stays on the local process connection.

Transport Best fit Trade-off
stdio Local desktop or subprocess host Simple and private for a local integration; the host manages the server process.
Streamable HTTP Deployed server or remote MCP host Allows network clients to connect, so you must operate the service and secure its network access.
SSE Clients that specifically require server-sent events Supported by the SDK, but use it when the client integration requires that transport.

The SDK supports stdio, Streamable HTTP, and SSE. This sample explicitly selects stdio; selecting a network transport is not just a one-word deployment change. Before exposing a server over HTTP, provide authentication appropriate to your environment, TLS at the network boundary, rate limits, per-client quotas, and a managed process lifecycle. Never expose an unrestricted Google API key or an unauthenticated public search proxy.

Test the tool before connecting an assistant

Use the MCP CLI’s development workflow to launch server.py in MCP Inspector, or connect an MCP SDK Client configured to run the script over stdio. The host should discover a tool named google_search with a string query and optional integer result count. Test more than the happy path:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • A normal query should return ok: true and a results array whose objects contain title, link, and snippet.
  • A query that produces no results should still return success with an empty array, not a missing field or a fabricated result.
  • A blank query and a query longer than the local bound should return invalid_query without making an HTTP request.
  • A result count below 1 or above 10 should return invalid_num_results.
  • Temporarily unset either environment variable to confirm the missing-configuration result is actionable and no secret is printed.
  • Check an invalid key, disabled API, or invalid engine ID to see the upstream rejection path. Check quota exhaustion separately if possible; Google’s exact error details can differ.
  • Simulate a slow or unreachable upstream in a controlled test environment to verify timeout and network handling without relying on a production request.

An SDK Client exposes asynchronous tool calls and structured content, making it useful for an automated smoke test. For deployment, also test the host configuration itself: a valid Python script is not enough if the host points to a different interpreter, virtual environment, or file path.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Security, reliability, and cost considerations

Keep credentials out of logs and clients

The API key is sent as the request parameter key, so avoid logging the complete request URL: it can contain the key. The example does not log request URLs or forward the entire Google payload. Do not include the key in source code, a checked-in configuration file, exception text, or a client-visible tool result. Rotate it if it is accidentally exposed.

Protect the public boundary

A local stdio process typically has a narrower exposure than a network-accessible service. A remotely reachable transport needs authentication and rate limiting before it is exposed; otherwise, other people can spend your Google quota through your server. Add per-user quotas and monitoring for a shared deployment. Validate inputs on the server even if an MCP client also validates them, because clients are not a security boundary.

Plan for upstream failure

The explicit timeout bounds how long one HTTP attempt waits; a retry does not guarantee success, and transient retries add latency. The code retries only rate limits, server errors, timeouts, and connection errors, with three attempts total. If you change that policy, keep a cap and account for the extra upstream requests. For a higher-volume service, consider a shared HTTP client, concurrency limits, observability that excludes secrets, and a deliberate quota and retry policy.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Return only the data clients need

Google’s complete response contains more fields than this tool contract needs. Returning only title, link, and snippet reduces coupling to the upstream response and gives the model a stable shape. If a client needs additional metadata, add selected, documented fields rather than blindly relaying the raw payload. Treat every snippet and URL as untrusted input, particularly if downstream code renders HTML or automatically follows links.

When the high-level API is enough

A typed decorator-based tool is the right default for this one-function search server: it keeps validation and the callable function easy to read, while the SDK derives the tool schema from Python type hints. Use the lower-level MCP Server API only when you need an exact hand-authored schema, custom metadata, or full control of structured content and protocol error flags. The extra control comes with more protocol details for you to implement and maintain.

Or skip the browser setup

Google Search MCP returns search results; it does not capture website screenshots. If the agent also needs a rendered page image or PDF, ScreenshotNeo is a separate screenshot API and MCP server, not a substitute for this Google search tool. It can accept cookie banners and remove 60+ known consent platforms, newsletter popups, and chat widgets before capture; those steps can be turned off. Only clean shots are billed: bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, and responses identify the page verdict and billing status. Its MCP server offers take_screenshot, get_page_info, and capture_pdf for AI agents.

For example, one GET request captures a page as an image:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation for the request options. It supports PNG, JPEG, WebP, or PDF output, and its plans include full-page capture, selector capture, device and viewport settings, custom CSS and JavaScript, and other capture controls. One thousand screenshots a month are free with no card; paid plans start at $5 for 3,000. Every feature is on every plan. Learn about ScreenshotNeo or sign up for 1,000 free screenshots a month with no card.

Frequently Asked Questions

Can I add more MCP tools to this server later?

Yes. Add another typed function decorated with @mcp.tool(), keeping its contract and validation independent. For example, a separate tool could fetch a page only if you deliberately add that capability and its network-security controls.

Are the returned snippets a reliable substitute for opening the result?

No. A snippet is search-result text, not a verification that the linked page is current, accurate, safe, or still available. Treat it as untrusted context and follow the link when the task requires checking the page itself.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Read next

Recommended PC Tool
Recommended PC Tool
Windows Errors? Fix Them Before They SpreadFree repair scan
Outdated Drivers Are Slowing You DownFree scan - exact matches

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.