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

```