# Directory Structure ``` ├── .github │ └── workflows │ └── npm-publish.yml ├── .gitignore ├── LICENSE ├── llm-install.md ├── package-lock.json ├── package.json ├── README.md ├── src │ └── index.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies node_modules/ /.pnp .pnp.js # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # CDK asset staging directory .cdk.staging cdk.out __pycache__/ # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env*.local # typescript *.tsbuildinfo next-env.d.ts # editor .idea newrelic_agent.log .env *.log reports/ /.vs /.infrastructure/app/cdk.out /.infrastructure/app/__pycache__ assets/ ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown [](https://mseep.ai/app/rendyfebry-google-pse-mcp) # Google Programmable Search Engine (PSE) MCP Server A Model Context Protocol (MCP) server for the Google Programmable Search Engine (PSE) API. This server exposes tools for searching the web with Google Custom Search engine, making them accessible to MCP-compatible clients such as VSCode, Copilot, and Claude Desktop. ## Installation Steps You do NOT need to clone this repository manually or run any installation commands yourself. Simply add the configuration below to your respective MCP client—your client will automatically install and launch the server as needed. ### VS Code Copilot Configuration Open Command Palette → Preferences: Open Settings (JSON), then add: `settings.json` ```jsonc { // Other settings... "mcp": { "servers": { "google-pse-mcp": { "command": "npx", "args": [ "-y", "google-pse-mcp", "https://www.googleapis.com/customsearch", "<api_key>", "<cx>", "<siteRestricted>" // optional: true/false, defaults to true ] } } } } ``` ### Cline MCP Configuration Example If you are using [Cline](https://github.com/saoudrizwan/cline), add the following to your `cline_mcp_settings.json` (usually found in your VSCode global storage or Cline config directory): - macOS: `~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` - Windows: `%APPDATA%\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json` ```json { "mcpServers": { "google-pse-mcp": { "disabled": false, "timeout": 60, "command": "npx", "args": [ "-y", "google-pse-mcp", "https://www.googleapis.com/customsearch", "<api_key>", "<cx>", "<siteRestricted>" // optional flag, true/false, defaults to true ], "transportType": "stdio" } } } ``` ### Important Notes Don't forget to replace `<api_key>` and `<cx>` with your credentials in the configuration above. You can also provide an optional `<siteRestricted>` flag (`true` or `false`) as the last argument to control which Google Custom Search endpoint is used. If omitted, it defaults to `true`. ## Available Tools This MCP server provides the following tool: 1. `search`: Search the web with Google Programmable Search Engine - Parameters: - `q` (string, required): Search query - `page` (integer, optional): Page number - `size` (integer, optional): Number of search results to return per page (1-10) - `sort` (string, optional): Sort expression (only 'date' is supported) - `safe` (boolean, optional): Enable safe search filtering - `lr` (string, optional): Restrict search to a particular language (e.g., lang_en) - `siteRestricted` (boolean, optional): Use the Site Restricted API endpoint; defaults to true unless overridden via CLI flag ## Example Usage ```python # Search for "artificial intelligence" result = await use_mcp_tool( server_name="google-pse-mcp", tool_name="search", arguments={ "q": "artificial intelligence", "size": 5, "safe": True } ) ``` ## Useful Links - [Model Context Protocol Servers](https://github.com/modelcontextprotocol/servers) - [Google Programmable Search Engine Intro](https://developers.google.com/custom-search/v1/overview) ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./build", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": [ "src/**/*" ], "exclude": [ "node_modules" ] } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "google-pse-mcp", "version": "0.2.1", "description": "A Model Context Protocol server for Google Programmable Search Engine (PSE)", "type": "module", "bin": { "google-pse-mcp": "build/index.js" }, "files": [ "build" ], "scripts": { "build": "tsc && shx chmod +x build/*.js", "prepare": "npm run build", "watch": "tsc --watch", "test": "echo \"All Good\" && exit 0" }, "devDependencies": { "typescript": "^5.8.3", "shx": "^0.3.4" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.9.0", "@types/node": "^22.14.1" } } ``` -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- ```yaml # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages name: Node.js Package on: release: types: [created] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npm test publish-npm: needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 registry-url: https://registry.npmjs.org/ - run: npm ci - run: npm publish env: NODE_AUTH_TOKEN: ${{secrets.npm_token}} ``` -------------------------------------------------------------------------------- /llm-install.md: -------------------------------------------------------------------------------- ```markdown # AI Agents -- Read this! This guide provides instructions for AI agents to set up the Uber Eats Google Programmable Search Engine (PSE) MCP Server on behalf of the user. ## Prerequisites - Node and npx installed - Google Programmable Search Engine API Key (`api_key`) - Google Custom Search Engine ID (`cx`) ## Installation Steps 1. **No manual installation or cloning is required.** 2. **Add the following configuration to your MCP client:** ### For VSCode Copilot Add to your `settings.json`: ```jsonc { "mcp": { "servers": { "google-pse-mcp": { "command": "npx", "args": [ "-y", "google-pse-mcp", "https://www.googleapis.com/customsearch", "<api_key>", "<cx>" ] } } } } ``` ### For Cline VSCode Extension Add to your `cline_mcp_settings.json`: ```json { "mcpServers": { "google-pse-mcp": { "disabled": false, "timeout": 60, "command": "npx", "args": [ "-y", "google-pse-mcp", "https://www.googleapis.com/customsearch", "<api_key>", "<cx>" ], "transportType": "stdio" } } } ``` **Important Notes:** - Replace `<api_key>` and `<cx>` with the actual path to your installation, either user edit them manually or use the MCP client to set them. - You do NOT need to run `npx -y google-pse-mcp ...` manually. The MCP client will handle installation and launching automatically. - Verify that the server is installed correctly by search for "Artificial Intelligence" or any other query. - All above steps are required before completing the task. ## Verification Perform a test search using your MCP client to confirm the server is working. ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; /** * Read API config from process.argv: * argv[2]: api_host (default: https://www.googleapis.com/customsearch) * argv[3]: api_key * argv[4]: cx */ const [ , , API_HOST = "https://www.googleapis.com/customsearch", API_KEY, CX, SITE_RESTRICTED_ARG ] = process.argv; // Parse optional siteRestricted CLI flag; default to true when omitted. const SITE_RESTRICTED_DEFAULT = SITE_RESTRICTED_ARG !== undefined ? SITE_RESTRICTED_ARG.toLowerCase() === "true" : true; const server = new Server( { name: "google-pse", version: "0.2.1" }, { capabilities: { tools: {}, resources: {}, } } ); // ListToolsRequestSchema handler: define both search tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "search", description: "Search the Web using Google Custom Search API", inputSchema: { type: "object", properties: { q: { type: "string", description: "Search query" }, page: { type: "integer", description: "Page number" }, size: { type: "integer", description: "Number of search results to return per page. Valid values are integers between 1 and 10, inclusive." }, sort: { type: "string", description: "Sort expression (e.g., 'date'). Only 'date' is supported by the API." }, safe: { type: "boolean", description: "Enable safe search filtering. Default: false." }, lr: { type: "string", description: "Restricts the search to documents written in a particular language (e.g., lang_en, lang_ja)" }, siteRestricted: { type: "boolean", description: "If true, use the Site Restricted API endpoint (/v1/siterestrict). If false, use the standard API endpoint (/v1). Default: true." }, }, required: ["q"] } } ] }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { if (!request.params.arguments) { throw new Error("No arguments provided"); } // --- search tool implementation --- if (request.params.name === "search") { const args = request.params.arguments as any; const { q, page = 1, size = 10, lr, safe = false, sort } = args; if (!q) { throw new Error("Missing required argument: q"); } if (!API_KEY) { throw new Error("API_KEY is not configured"); } if (!CX) { throw new Error("CX is not configured"); } // Build query params const params = new URLSearchParams(); params.append("key", API_KEY); params.append("cx", CX); params.append("q", q); params.append("fields", "items(title,htmlTitle,link,snippet,htmlSnippet)"); // Language restriction if (lr !== undefined) { params.append("lr", String(lr)); } // SafeSearch mapping (boolean only) if (safe !== undefined) { if (typeof safe !== "boolean") { throw new Error("SafeSearch (safe) must be a boolean"); } params.append("safe", safe ? "active" : "off"); } // Sort validation if (sort !== undefined) { if (sort === "date") { params.append("sort", "date"); } else { throw new Error("Only 'date' is supported for sort"); } } // Pagination params.append("num", String(size)); if (page > 0 && size > 0) { const start = ((page - 1) * size) + 1; params.append("start", String(start)); } else { params.append("start", "1"); } const siteRestricted = args.siteRestricted !== undefined ? args.siteRestricted : SITE_RESTRICTED_DEFAULT; const endpoint = siteRestricted ? "/v1/siterestrict" : "/v1"; const url = `${API_HOST}${endpoint}?${params.toString()}`; const response = await fetch(url, { method: "GET" }); if (!response.ok) { throw new Error(`Search API request failed: ${response.status} ${response.statusText}`); } const result = await response.json(); // Return the items array (list of articles) const items = result?.items ?? []; return { content: [{ type: "text", text: JSON.stringify(items, null, 2) }] }; } throw new Error(`Unknown tool: ${request.params.name}`); }); async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); } runServer().catch(console.error); ```