#
tokens: 4614/50000 7/7 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/rendyfebry-google-pse-mcp-badge.png)](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 | 
```