# 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: -------------------------------------------------------------------------------- ``` 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules/ 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # CDK asset staging directory 23 | .cdk.staging 24 | cdk.out 25 | __pycache__/ 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # local env files 33 | .env*.local 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | # editor 40 | .idea 41 | 42 | newrelic_agent.log 43 | .env 44 | *.log 45 | 46 | reports/ 47 | /.vs 48 | /.infrastructure/app/cdk.out 49 | /.infrastructure/app/__pycache__ 50 | 51 | assets/ 52 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | [](https://mseep.ai/app/rendyfebry-google-pse-mcp) 2 | 3 | # Google Programmable Search Engine (PSE) MCP Server 4 | 5 | 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. 6 | 7 | ## Installation Steps 8 | 9 | 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. 10 | 11 | ### VS Code Copilot Configuration 12 | 13 | Open Command Palette → Preferences: Open Settings (JSON), then add: 14 | 15 | `settings.json` 16 | ```jsonc 17 | { 18 | // Other settings... 19 | "mcp": { 20 | "servers": { 21 | "google-pse-mcp": { 22 | "command": "npx", 23 | "args": [ 24 | "-y", 25 | "google-pse-mcp", 26 | "https://www.googleapis.com/customsearch", 27 | "<api_key>", 28 | "<cx>", 29 | "<siteRestricted>" // optional: true/false, defaults to true 30 | ] 31 | } 32 | } 33 | } 34 | } 35 | ``` 36 | 37 | ### Cline MCP Configuration Example 38 | 39 | 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): 40 | 41 | - macOS: `~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` 42 | - Windows: `%APPDATA%\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json` 43 | 44 | ```json 45 | { 46 | "mcpServers": { 47 | "google-pse-mcp": { 48 | "disabled": false, 49 | "timeout": 60, 50 | "command": "npx", 51 | "args": [ 52 | "-y", 53 | "google-pse-mcp", 54 | "https://www.googleapis.com/customsearch", 55 | "<api_key>", 56 | "<cx>", 57 | "<siteRestricted>" // optional flag, true/false, defaults to true 58 | ], 59 | "transportType": "stdio" 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | 66 | ### Important Notes 67 | 68 | Don't forget to replace `<api_key>` and `<cx>` with your credentials in the configuration above. 69 | 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`. 70 | 71 | 72 | ## Available Tools 73 | 74 | This MCP server provides the following tool: 75 | 76 | 1. `search`: Search the web with Google Programmable Search Engine 77 | 78 | - Parameters: 79 | - `q` (string, required): Search query 80 | - `page` (integer, optional): Page number 81 | - `size` (integer, optional): Number of search results to return per page (1-10) 82 | - `sort` (string, optional): Sort expression (only 'date' is supported) 83 | - `safe` (boolean, optional): Enable safe search filtering 84 | - `lr` (string, optional): Restrict search to a particular language (e.g., lang_en) 85 | - `siteRestricted` (boolean, optional): Use the Site Restricted API endpoint; defaults to true unless overridden via CLI flag 86 | 87 | ## Example Usage 88 | 89 | ```python 90 | # Search for "artificial intelligence" 91 | result = await use_mcp_tool( 92 | server_name="google-pse-mcp", 93 | tool_name="search", 94 | arguments={ 95 | "q": "artificial intelligence", 96 | "size": 5, 97 | "safe": True 98 | } 99 | ) 100 | ``` 101 | 102 | ## Useful Links 103 | 104 | - [Model Context Protocol Servers](https://github.com/modelcontextprotocol/servers) 105 | - [Google Programmable Search Engine Intro](https://developers.google.com/custom-search/v1/overview) 106 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": [ 14 | "src/**/*" 15 | ], 16 | "exclude": [ 17 | "node_modules" 18 | ] 19 | } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "google-pse-mcp", 3 | "version": "0.2.1", 4 | "description": "A Model Context Protocol server for Google Programmable Search Engine (PSE)", 5 | "type": "module", 6 | "bin": { 7 | "google-pse-mcp": "build/index.js" 8 | }, 9 | "files": [ 10 | "build" 11 | ], 12 | "scripts": { 13 | "build": "tsc && shx chmod +x build/*.js", 14 | "prepare": "npm run build", 15 | "watch": "tsc --watch", 16 | "test": "echo \"All Good\" && exit 0" 17 | }, 18 | "devDependencies": { 19 | "typescript": "^5.8.3", 20 | "shx": "^0.3.4" 21 | }, 22 | "dependencies": { 23 | "@modelcontextprotocol/sdk": "^1.9.0", 24 | "@types/node": "^22.14.1" 25 | } 26 | } ``` -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- ```yaml 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 20 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: actions/setup-node@v4 27 | with: 28 | node-version: 20 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 34 | ``` -------------------------------------------------------------------------------- /llm-install.md: -------------------------------------------------------------------------------- ```markdown 1 | # AI Agents -- Read this! 2 | 3 | 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. 4 | 5 | ## Prerequisites 6 | - Node and npx installed 7 | - Google Programmable Search Engine API Key (`api_key`) 8 | - Google Custom Search Engine ID (`cx`) 9 | 10 | ## Installation Steps 11 | 12 | 1. **No manual installation or cloning is required.** 13 | 2. **Add the following configuration to your MCP client:** 14 | 15 | ### For VSCode Copilot 16 | 17 | Add to your `settings.json`: 18 | ```jsonc 19 | { 20 | "mcp": { 21 | "servers": { 22 | "google-pse-mcp": { 23 | "command": "npx", 24 | "args": [ 25 | "-y", 26 | "google-pse-mcp", 27 | "https://www.googleapis.com/customsearch", 28 | "<api_key>", 29 | "<cx>" 30 | ] 31 | } 32 | } 33 | } 34 | } 35 | ``` 36 | 37 | ### For Cline VSCode Extension 38 | 39 | Add to your `cline_mcp_settings.json`: 40 | ```json 41 | { 42 | "mcpServers": { 43 | "google-pse-mcp": { 44 | "disabled": false, 45 | "timeout": 60, 46 | "command": "npx", 47 | "args": [ 48 | "-y", 49 | "google-pse-mcp", 50 | "https://www.googleapis.com/customsearch", 51 | "<api_key>", 52 | "<cx>" 53 | ], 54 | "transportType": "stdio" 55 | } 56 | } 57 | } 58 | ``` 59 | 60 | **Important Notes:** 61 | 62 | - 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. 63 | - You do NOT need to run `npx -y google-pse-mcp ...` manually. The MCP client will handle installation and launching automatically. 64 | - Verify that the server is installed correctly by search for "Artificial Intelligence" or any other query. 65 | - All above steps are required before completing the task. 66 | 67 | ## Verification 68 | 69 | Perform a test search using your MCP client to confirm the server is working. 70 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; 5 | 6 | /** 7 | * Read API config from process.argv: 8 | * argv[2]: api_host (default: https://www.googleapis.com/customsearch) 9 | * argv[3]: api_key 10 | * argv[4]: cx 11 | */ 12 | const [ 13 | , 14 | , 15 | API_HOST = "https://www.googleapis.com/customsearch", 16 | API_KEY, 17 | CX, 18 | SITE_RESTRICTED_ARG 19 | ] = process.argv; 20 | 21 | // Parse optional siteRestricted CLI flag; default to true when omitted. 22 | const SITE_RESTRICTED_DEFAULT = SITE_RESTRICTED_ARG !== undefined 23 | ? SITE_RESTRICTED_ARG.toLowerCase() === "true" 24 | : true; 25 | 26 | const server = new Server( 27 | { 28 | name: "google-pse", 29 | version: "0.2.1" 30 | }, 31 | { 32 | capabilities: { 33 | tools: {}, 34 | resources: {}, 35 | } 36 | } 37 | ); 38 | 39 | // ListToolsRequestSchema handler: define both search tools 40 | server.setRequestHandler(ListToolsRequestSchema, async () => { 41 | return { 42 | tools: [ 43 | { 44 | name: "search", 45 | description: "Search the Web using Google Custom Search API", 46 | inputSchema: { 47 | type: "object", 48 | properties: { 49 | q: { type: "string", description: "Search query" }, 50 | page: { type: "integer", description: "Page number" }, 51 | size: { type: "integer", description: "Number of search results to return per page. Valid values are integers between 1 and 10, inclusive." }, 52 | sort: { 53 | type: "string", 54 | description: "Sort expression (e.g., 'date'). Only 'date' is supported by the API." 55 | }, 56 | safe: { 57 | type: "boolean", 58 | description: "Enable safe search filtering. Default: false." 59 | }, 60 | lr: { type: "string", description: "Restricts the search to documents written in a particular language (e.g., lang_en, lang_ja)" }, 61 | siteRestricted: { 62 | type: "boolean", 63 | description: "If true, use the Site Restricted API endpoint (/v1/siterestrict). If false, use the standard API endpoint (/v1). Default: true." 64 | }, 65 | }, 66 | required: ["q"] 67 | } 68 | } 69 | ] 70 | }; 71 | }); 72 | 73 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 74 | if (!request.params.arguments) { 75 | throw new Error("No arguments provided"); 76 | } 77 | 78 | // --- search tool implementation --- 79 | if (request.params.name === "search") { 80 | const args = request.params.arguments as any; 81 | const { 82 | q, 83 | page = 1, 84 | size = 10, 85 | lr, 86 | safe = false, 87 | sort 88 | } = args; 89 | 90 | if (!q) { 91 | throw new Error("Missing required argument: q"); 92 | } 93 | if (!API_KEY) { 94 | throw new Error("API_KEY is not configured"); 95 | } 96 | if (!CX) { 97 | throw new Error("CX is not configured"); 98 | } 99 | 100 | // Build query params 101 | const params = new URLSearchParams(); 102 | params.append("key", API_KEY); 103 | params.append("cx", CX); 104 | params.append("q", q); 105 | params.append("fields", "items(title,htmlTitle,link,snippet,htmlSnippet)"); 106 | 107 | // Language restriction 108 | if (lr !== undefined) { 109 | params.append("lr", String(lr)); 110 | } 111 | 112 | // SafeSearch mapping (boolean only) 113 | if (safe !== undefined) { 114 | if (typeof safe !== "boolean") { 115 | throw new Error("SafeSearch (safe) must be a boolean"); 116 | } 117 | params.append("safe", safe ? "active" : "off"); 118 | } 119 | 120 | // Sort validation 121 | if (sort !== undefined) { 122 | if (sort === "date") { 123 | params.append("sort", "date"); 124 | } else { 125 | throw new Error("Only 'date' is supported for sort"); 126 | } 127 | } 128 | 129 | // Pagination 130 | params.append("num", String(size)); 131 | 132 | if (page > 0 && size > 0) { 133 | const start = ((page - 1) * size) + 1; 134 | params.append("start", String(start)); 135 | } else { 136 | params.append("start", "1"); 137 | } 138 | 139 | const siteRestricted = args.siteRestricted !== undefined ? args.siteRestricted : SITE_RESTRICTED_DEFAULT; 140 | const endpoint = siteRestricted ? "/v1/siterestrict" : "/v1"; 141 | const url = `${API_HOST}${endpoint}?${params.toString()}`; 142 | const response = await fetch(url, { 143 | method: "GET" 144 | }); 145 | 146 | if (!response.ok) { 147 | throw new Error(`Search API request failed: ${response.status} ${response.statusText}`); 148 | } 149 | 150 | const result = await response.json(); 151 | 152 | // Return the items array (list of articles) 153 | const items = result?.items ?? []; 154 | return { 155 | content: [{ 156 | type: "text", 157 | text: JSON.stringify(items, null, 2) 158 | }] 159 | }; 160 | } 161 | 162 | throw new Error(`Unknown tool: ${request.params.name}`); 163 | }); 164 | 165 | async function runServer() { 166 | const transport = new StdioServerTransport(); 167 | await server.connect(transport); 168 | } 169 | 170 | runServer().catch(console.error); 171 | ```