Skip to content

Commit 6f419a6

Browse files
author
Vlad M
committed
Initial release: SerpApi plugin for Cursor
Port of serpapi-claude-plugin adapted for Cursor conventions: - .cursor-plugin/ manifest and marketplace config - SKILL.md with Cursor-compatible frontmatter (no allowed-tools) - Shell tool instructions instead of Bash(curl *) syntax - Local install docs (~/.cursor/plugins/local/) - CI: validate.yml for test.sh on push/PR, update-engines.yml weekly - 112 engine schemas, build script, and examples Made-with: Cursor
0 parents  commit 6f419a6

124 files changed

Lines changed: 54214 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cursor-plugin/marketplace.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "serpapi-plugins",
3+
"owner": {
4+
"name": "SerpApi",
5+
"email": "support@serpapi.com"
6+
},
7+
"metadata": {
8+
"description": "Official SerpApi plugins for Cursor"
9+
},
10+
"plugins": [
11+
{
12+
"name": "serpapi",
13+
"source": ".",
14+
"description": "Search Google, Amazon, Walmart, YouTube, and 50+ engines via SerpApi",
15+
"homepage": "https://serpapi.com",
16+
"repository": "https://github.com/serpapi/serpapi-cursor-plugin",
17+
"license": "MIT",
18+
"keywords": ["search", "google", "amazon", "walmart", "serp", "seo", "scraping"],
19+
"category": "integrations"
20+
}
21+
]
22+
}

.cursor-plugin/plugin.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "serpapi",
3+
"description": "Search Google, Amazon, Walmart, YouTube, and 50+ engines via SerpApi",
4+
"version": "1.0.0",
5+
"author": {
6+
"name": "SerpApi"
7+
},
8+
"homepage": "https://serpapi.com",
9+
"repository": "https://github.com/serpapi/serpapi-cursor-plugin",
10+
"license": "MIT",
11+
"keywords": ["search", "google", "amazon", "walmart", "serp", "seo", "scraping"]
12+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Update Engine Schemas
2+
3+
on:
4+
schedule:
5+
- cron: "0 6 * * 1" # Every Monday at 06:00 UTC
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
update-engines:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v4
18+
19+
- name: Set up Python
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: "3.12"
23+
24+
- name: Install dependencies
25+
run: pip install -r requirements.txt
26+
27+
- name: Regenerate engine schemas
28+
run: python build-engines.py
29+
30+
- name: Commit changes if any
31+
run: |
32+
git config user.name "github-actions[bot]"
33+
git config user.email "github-actions[bot]@users.noreply.github.com"
34+
git add engines/
35+
if git diff --cached --quiet; then
36+
echo "No engine changes detected"
37+
else
38+
git commit -m "chore: update engine schemas"
39+
git push
40+
fi

.github/workflows/validate.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Validate Plugin
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
validate:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout
15+
uses: actions/checkout@v4
16+
17+
- name: Set up Python
18+
uses: actions/setup-python@v5
19+
with:
20+
python-version: "3.12"
21+
22+
- name: Run validation
23+
run: bash test.sh

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.env
2+
.idea/
3+
__pycache__/
4+
*.pyc
5+
.venv/
6+
.ruff_cache/
7+
.DS_Store

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 SerpApi
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# <img src="https://user-images.githubusercontent.com/307597/154772945-1b7dba5f-21cf-41d0-bb2e-65b6eff4aaaf.png" width="30" height="30"/> SerpApi Plugin for Cursor
2+
3+
A [Cursor plugin](https://cursor.com/docs/plugins) that gives Cursor Agent the ability to search Google, Amazon, Walmart, YouTube, Google Maps, Google Scholar, and [100+ other engines](https://serpapi.com/search-engine-apis) via the [SerpApi](https://serpapi.com) REST API.
4+
5+
[![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
6+
[![Engines](https://img.shields.io/badge/engines-107-blue.svg)](engines/)
7+
[![CI](https://github.com/serpapi/serpapi-cursor-plugin/actions/workflows/update-engines.yml/badge.svg)](https://github.com/serpapi/serpapi-cursor-plugin/actions/workflows/update-engines.yml)
8+
9+
## Quick Start
10+
11+
### 1. Get an API key
12+
13+
Sign up at [serpapi.com](https://serpapi.com/users/sign_up?plan=free) and set the key:
14+
15+
> **Free tier** - 250 searches/month, no credit card required.
16+
17+
```bash
18+
export SERPAPI_API_KEY="your_key_here"
19+
```
20+
21+
### 2. Install the plugin
22+
23+
**Local install** (recommended for testing):
24+
25+
```bash
26+
# Option A: Copy the plugin
27+
cp -r /path/to/serpapi-cursor-plugin ~/.cursor/plugins/local/serpapi
28+
29+
# Option B: Clone directly
30+
git clone https://github.com/serpapi/serpapi-cursor-plugin.git ~/.cursor/plugins/local/serpapi
31+
```
32+
33+
Then restart Cursor or run `Developer: Reload Window` from the command palette.
34+
35+
**From the marketplace** (when available):
36+
37+
Search for "serpapi" in the Cursor marketplace panel, or visit [cursor.com/marketplace](https://cursor.com/marketplace).
38+
39+
### 3. Use it
40+
41+
Cursor Agent will automatically use SerpApi when you ask it to search for something. Just ask in natural language:
42+
43+
> *Search Google for the best Python web frameworks*
44+
>
45+
> *Compare prices for AirPods Pro on Amazon, Walmart, and eBay*
46+
>
47+
> *Find academic papers about transformer architectures published after 2020*
48+
49+
Or invoke the skill explicitly:
50+
51+
```
52+
/search coffee shops near Times Square
53+
```
54+
55+
You can also attach it as context:
56+
57+
```
58+
@search
59+
```
60+
61+
## Features
62+
63+
- **Single skill, all engines** - `/search` covers all 100+ SerpApi engines. Cursor Agent picks the right one based on your intent.
64+
- **Always up to date** - Engine parameter schemas are auto-generated and kept fresh by weekly CI.
65+
- **Auto-invocation** - Cursor Agent detects search-related requests and loads the skill automatically. No need to remember slash commands.
66+
- **Cost-aware** - Defaults to `google_light` (faster, cheaper) for simple web searches. Confirms before making API calls.
67+
- **Schema-driven** - The plugin ships a complete engine selection table and JSON schemas for every engine's parameters, so the agent can construct the right API call without guessing.
68+
69+
## Supported Engines
70+
71+
| Category | Engines |
72+
|----------|---------|
73+
| Web Search | Google, Google Light, Bing, DuckDuckGo, Yahoo, Yandex, Baidu, Naver |
74+
| AI Search | Google AI Mode, Google AI Overview, Bing Copilot, Brave AI Mode |
75+
| Shopping | Amazon, Walmart, eBay, Google Shopping, Home Depot |
76+
| Local / Maps | Google Maps, Google Local, Yelp, TripAdvisor, OpenTable |
77+
| Research | Google Scholar, Google Patents, Google Trends |
78+
| News | Google News, Bing News, DuckDuckGo News, Baidu News |
79+
| Media | Google Images, Google Videos, YouTube, Google Lens |
80+
| Travel | Google Flights, Google Hotels, Google Travel Explore |
81+
| Jobs | Google Jobs |
82+
| Finance | Google Finance |
83+
| Apps | Google Play, Apple App Store |
84+
85+
See the full list in [`engines/`](engines/).
86+
87+
## Troubleshooting
88+
89+
- **"Invalid API key"**: Verify at [serpapi.com/manage-api-key](https://serpapi.com/manage-api-key)
90+
- **Plugin not showing up**: Ensure files are in `~/.cursor/plugins/local/serpapi/` and restart Cursor
91+
- **Skill not loading**: Run `Developer: Reload Window` from the command palette
92+
- **Rate limit exceeded**: Wait or [upgrade your plan](https://serpapi.com/pricing)
93+
94+
## Development
95+
96+
Engine schemas are auto-generated by `build-engines.py` and updated weekly via [GitHub Actions](.github/workflows/update-engines.yml).
97+
98+
To regenerate manually:
99+
100+
```bash
101+
pip install -r requirements.txt
102+
python build-engines.py
103+
```
104+
105+
To validate the plugin structure:
106+
107+
```bash
108+
bash test.sh
109+
```
110+
111+
## Contributing
112+
113+
1. Fork the repository
114+
2. Create your feature branch: `git checkout -b feature/amazing-feature`
115+
3. Make your changes
116+
4. Run validation: `bash test.sh`
117+
5. Commit changes: `git commit -m 'Add amazing feature'`
118+
6. Push to branch: `git push origin feature/amazing-feature`
119+
7. Open a Pull Request
120+
121+
## Related
122+
123+
- [SerpApi Plugin for Claude Code](https://github.com/serpapi/serpapi-claude-plugin) - Claude Code plugin with the same capabilities
124+
- [SerpApi MCP Server](https://github.com/serpapi/serpapi-mcp) - MCP server integration for Claude Desktop, VS Code, and Cursor
125+
- [SerpApi Docs](https://serpapi.com/search-api) - Full API reference
126+
- [SerpApi Playground](https://serpapi.com/playground) - Interactive API explorer
127+
128+
## License
129+
130+
MIT License - see [LICENSE](LICENSE) file for details.

build-engines.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
#!/usr/bin/env python3
2+
"""Build SerpApi engine parameter data for MCP usage."""
3+
4+
from __future__ import annotations
5+
6+
import html
7+
import json
8+
from pathlib import Path
9+
from urllib.request import Request, urlopen
10+
11+
from bs4 import BeautifulSoup
12+
from markdownify import markdownify
13+
14+
PLAYGROUND_URL = "https://serpapi.com/playground"
15+
EXCLUDED_ENGINES = {
16+
"google_scholar_profiles",
17+
"google_light_fast",
18+
"google_lens_image_sources",
19+
}
20+
PARAM_KEEP_KEYS = {"html", "type", "options", "required"}
21+
OUTPUT_DIR = Path("engines")
22+
TIMEOUT_SECONDS = 30
23+
USER_AGENT = "Mozilla/5.0"
24+
25+
26+
def html_to_markdown(value: str) -> str:
27+
"""Convert HTML to markdown, normalizing whitespace."""
28+
md = markdownify(html.unescape(value), strip=["a"])
29+
return " ".join(md.split())
30+
31+
32+
def normalize_options(options: list[object]) -> list[object]:
33+
"""Normalize option values, simplifying [value, label] pairs where possible."""
34+
normalized = []
35+
for option in options:
36+
if isinstance(option, list) and option:
37+
value = option[0]
38+
label = option[1] if len(option) > 1 else None
39+
if (
40+
label is not None
41+
and (
42+
isinstance(value, (int, float))
43+
or (isinstance(value, str) and value.isdigit())
44+
)
45+
and value != label
46+
):
47+
normalized.append(option)
48+
else:
49+
normalized.append(value)
50+
else:
51+
normalized.append(option)
52+
return normalized
53+
54+
55+
def fetch_props(url: str) -> dict[str, object]:
56+
"""Fetch playground HTML and extract React props."""
57+
req = Request(url, headers={"User-Agent": USER_AGENT})
58+
with urlopen(req, timeout=TIMEOUT_SECONDS) as resp:
59+
page_html = resp.read().decode("utf-8", errors="ignore")
60+
soup = BeautifulSoup(page_html, "html.parser")
61+
node = soup.find(attrs={"data-react-props": True})
62+
if not node:
63+
raise RuntimeError("Failed to locate data-react-props in playground HTML.")
64+
return json.loads(html.unescape(node["data-react-props"]))
65+
66+
67+
def normalize_engine(engine: str, payload: dict[str, object]) -> dict[str, object]:
68+
"""Normalize engine payload, extracting relevant parameter metadata."""
69+
normalized_params: dict[str, dict[str, object]] = {}
70+
common_params: dict[str, dict[str, object]] = {}
71+
if isinstance(payload, dict):
72+
for group_name, group in payload.items():
73+
if not isinstance(group, dict):
74+
continue
75+
if not isinstance(params := group.get("parameters"), dict):
76+
continue
77+
for param_name, param in params.items():
78+
if not isinstance(param, dict):
79+
continue
80+
filtered = {k: v for k, v in param.items() if k in PARAM_KEEP_KEYS}
81+
if isinstance(options := filtered.get("options"), list):
82+
filtered["options"] = normalize_options(options)
83+
if isinstance(html_value := filtered.pop("html", None), str):
84+
filtered["description"] = html_to_markdown(html_value)
85+
if filtered:
86+
filtered["group"] = group_name
87+
if group_name == "serpapi_parameters":
88+
common_params[param_name] = filtered
89+
else:
90+
normalized_params[param_name] = filtered
91+
92+
return {
93+
"engine": engine,
94+
"params": normalized_params,
95+
"common_params": common_params,
96+
}
97+
98+
99+
def main() -> int:
100+
"""Main entry point: fetch playground data and generate engine files."""
101+
props = fetch_props(PLAYGROUND_URL)
102+
if not isinstance(params := props.get("parameters"), dict):
103+
raise RuntimeError("Playground props missing 'parameters' map.")
104+
105+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
106+
engines = []
107+
108+
for engine, payload in sorted(params.items()):
109+
if not isinstance(engine, str) or engine in EXCLUDED_ENGINES:
110+
continue
111+
if not isinstance(payload, dict):
112+
continue
113+
(OUTPUT_DIR / f"{engine}.json").write_text(
114+
json.dumps(normalize_engine(engine, payload), indent=2, ensure_ascii=False),
115+
encoding="utf-8",
116+
)
117+
engines.append(engine)
118+
119+
print(f"Wrote {len(engines)} engine files to {OUTPUT_DIR}")
120+
return 0
121+
122+
123+
if __name__ == "__main__":
124+
raise SystemExit(main())

0 commit comments

Comments
 (0)