hasdata-google-images-mcp 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hasdata_google_images_mcp-1.0.0/.github/ISSUE_TEMPLATE/inaccuracy.md +21 -0
- hasdata_google_images_mcp-1.0.0/.github/workflows/contract.yml +31 -0
- hasdata_google_images_mcp-1.0.0/.github/workflows/publish.yml +75 -0
- hasdata_google_images_mcp-1.0.0/.gitignore +11 -0
- hasdata_google_images_mcp-1.0.0/LICENSE +21 -0
- hasdata_google_images_mcp-1.0.0/PKG-INFO +345 -0
- hasdata_google_images_mcp-1.0.0/README.md +331 -0
- hasdata_google_images_mcp-1.0.0/glama.json +7 -0
- hasdata_google_images_mcp-1.0.0/hasdata_google_images_mcp/__init__.py +28 -0
- hasdata_google_images_mcp-1.0.0/index.mjs +24 -0
- hasdata_google_images_mcp-1.0.0/mcp.json +12 -0
- hasdata_google_images_mcp-1.0.0/package.json +41 -0
- hasdata_google_images_mcp-1.0.0/plugin.json +24 -0
- hasdata_google_images_mcp-1.0.0/pyproject.toml +24 -0
- hasdata_google_images_mcp-1.0.0/server.json +61 -0
- hasdata_google_images_mcp-1.0.0/test/tools.test.mjs +170 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Something in the README is wrong
|
|
3
|
+
about: A tool table, a response sample or a documented behaviour does not match reality
|
|
4
|
+
labels: documentation
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
**Where in the README**
|
|
8
|
+
|
|
9
|
+
Section or heading.
|
|
10
|
+
|
|
11
|
+
**What it says**
|
|
12
|
+
|
|
13
|
+
Quote the line.
|
|
14
|
+
|
|
15
|
+
**The call you made**
|
|
16
|
+
|
|
17
|
+
Tool name and arguments, or the equivalent REST URL with your key removed.
|
|
18
|
+
|
|
19
|
+
**What came back**
|
|
20
|
+
|
|
21
|
+
Trimmed response, with anything private removed.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# The tool contract is checked on a schedule as well as on push, because the upstream tool list
|
|
2
|
+
# can change without a single commit in this repository.
|
|
3
|
+
name: tool contract
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
pull_request:
|
|
9
|
+
schedule:
|
|
10
|
+
- cron: '0 6 * * 1'
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
contract:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
timeout-minutes: 5
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- uses: actions/setup-node@v4
|
|
23
|
+
with:
|
|
24
|
+
node-version: '22'
|
|
25
|
+
# Forks cannot read repository secrets. The suite skips its live checks when the key is
|
|
26
|
+
# absent, so a pull request from a fork stays green instead of failing for a reason the
|
|
27
|
+
# contributor cannot fix.
|
|
28
|
+
- name: Assert the tool list still matches the README
|
|
29
|
+
env:
|
|
30
|
+
HASDATA_API_KEY: ${{ secrets.HASDATA_API_KEY }}
|
|
31
|
+
run: npm test
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Publishes the npm and PyPI wrapper packages on a version tag, using OIDC
|
|
2
|
+
# trusted publishing. No NPM_TOKEN or PYPI_TOKEN is stored anywhere: GitHub
|
|
3
|
+
# mints a short-lived OIDC token per run, and npmjs.org / pypi.org accept it
|
|
4
|
+
# because this repo + workflow are configured as trusted publishers.
|
|
5
|
+
#
|
|
6
|
+
# One-time setup, done once per package on the registries (not in this repo):
|
|
7
|
+
# npmjs.org -> package settings -> Trusted Publisher -> GitHub Actions,
|
|
8
|
+
# repo HasData/google-images-mcp, workflow publish.yml
|
|
9
|
+
# pypi.org -> the hasdata org -> Publishing -> add a trusted publisher
|
|
10
|
+
# (pending publisher works before the first release),
|
|
11
|
+
# repo HasData/google-images-mcp, workflow publish.yml
|
|
12
|
+
#
|
|
13
|
+
# The MCP registry entry (com.hasdata/google-images) is NOT published here. It uses
|
|
14
|
+
# domain auth, which would need the namespace-wide Ed25519 key as a secret in
|
|
15
|
+
# every repo. That key stays off CI; the registry entry is published by hand
|
|
16
|
+
# when server.json changes, after the package versions below are live.
|
|
17
|
+
#
|
|
18
|
+
# Release: bump nothing by hand. Tag the commit `vX.Y.Z` and push the tag; the
|
|
19
|
+
# tag is the single source of the version and is written into both manifests.
|
|
20
|
+
|
|
21
|
+
name: publish
|
|
22
|
+
|
|
23
|
+
on:
|
|
24
|
+
push:
|
|
25
|
+
tags: ['v*.*.*']
|
|
26
|
+
|
|
27
|
+
permissions:
|
|
28
|
+
contents: read
|
|
29
|
+
id-token: write
|
|
30
|
+
|
|
31
|
+
jobs:
|
|
32
|
+
npm:
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v4
|
|
36
|
+
# No registry-url here on purpose. With it, setup-node writes an .npmrc
|
|
37
|
+
# carrying _authToken=${NODE_AUTH_TOKEN}, which resolves to a placeholder
|
|
38
|
+
# when no token is passed. npm then authenticates with that garbage instead
|
|
39
|
+
# of falling back to OIDC, and a scoped package answers 404.
|
|
40
|
+
- uses: actions/setup-node@v4
|
|
41
|
+
with:
|
|
42
|
+
node-version: '24'
|
|
43
|
+
# OIDC trusted publishing landed in npm 11.5.1. Node 24 already ships a
|
|
44
|
+
# newer npm than that, but pinning the upgrade here keeps the job working
|
|
45
|
+
# if the runner image drifts back.
|
|
46
|
+
- name: Upgrade npm for OIDC trusted publishing
|
|
47
|
+
run: |
|
|
48
|
+
npm install -g npm@latest
|
|
49
|
+
npm -v
|
|
50
|
+
- name: Set version from the tag
|
|
51
|
+
run: npm version "${GITHUB_REF_NAME#v}" --no-git-tag-version --allow-same-version
|
|
52
|
+
- name: Publish to npm (OIDC, no token)
|
|
53
|
+
run: npm publish --access public
|
|
54
|
+
|
|
55
|
+
pypi:
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
steps:
|
|
58
|
+
- uses: actions/checkout@v4
|
|
59
|
+
- uses: actions/setup-python@v5
|
|
60
|
+
with:
|
|
61
|
+
python-version: '3.12'
|
|
62
|
+
- name: Set version from the tag
|
|
63
|
+
run: |
|
|
64
|
+
python - "${GITHUB_REF_NAME#v}" <<'PY'
|
|
65
|
+
import re, sys
|
|
66
|
+
v = sys.argv[1]
|
|
67
|
+
p = "pyproject.toml"
|
|
68
|
+
t = open(p, encoding="utf-8").read()
|
|
69
|
+
t = re.sub(r'(?m)^version = ".*"$', f'version = "{v}"', t, count=1)
|
|
70
|
+
open(p, "w", encoding="utf-8", newline="\n").write(t)
|
|
71
|
+
PY
|
|
72
|
+
- name: Build the wheel and sdist
|
|
73
|
+
run: pipx run build
|
|
74
|
+
- name: Publish to PyPI (OIDC, no token)
|
|
75
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HasData
|
|
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.
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: hasdata-google-images-mcp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: MCP server for Google Images through HasData's hosted API. 1,000 free credits every month.
|
|
5
|
+
Project-URL: Homepage, https://hasdata.com/apis/google-images-api
|
|
6
|
+
Project-URL: Repository, https://github.com/HasData/google-images-mcp
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: google-images,hasdata,image-search,mcp,model-context-protocol,seo,serp
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Requires-Dist: mcp-proxy>=0.12.0
|
|
12
|
+
Requires-Dist: mcp<2,>=1.17
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Google Images MCP Server
|
|
16
|
+
|
|
17
|
+
<!-- mcp-name: com.hasdata/google-images -->
|
|
18
|
+
|
|
19
|
+
A hosted Model Context Protocol (MCP) server that gives Claude, Cursor, Windsurf and any other MCP client one read-only Google Images tool. Run an image search with size, colour and type filters and get 100 results a call, each with the page it came from and the full-resolution file URL, as structured JSON, with nothing to host.
|
|
20
|
+
|
|
21
|
+
It reads the Google Images results page a signed-out visitor sees.
|
|
22
|
+
|
|
23
|
+
**1,000 free credits every month, no card required**, which is 200 Google Images calls at the 5-credit rate.
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
https://mcp.hasdata.com/api/mcp?apis=google_images
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
[](https://glama.ai/mcp/servers/HasData/google-images-mcp)
|
|
30
|
+
[](https://github.com/HasData/google-images-mcp/actions/workflows/contract.yml)
|
|
31
|
+
[](https://mcp.hasdata.com/api/mcp?apis=google_images)
|
|
32
|
+
[](#tools)
|
|
33
|
+
[](https://www.npmjs.com/package/@hasdata/google-images-mcp)
|
|
34
|
+
[](https://pypi.org/project/hasdata-google-images-mcp/)
|
|
35
|
+
[](LICENSE)
|
|
36
|
+
|
|
37
|
+
## Contents
|
|
38
|
+
|
|
39
|
+
- [What you need](#what-you-need)
|
|
40
|
+
- [Quick start](#quick-start)
|
|
41
|
+
- [Example prompts](#example-prompts)
|
|
42
|
+
- [Tools](#tools)
|
|
43
|
+
- [Errors and failure paths](#errors-and-failure-paths)
|
|
44
|
+
- [Pricing, free tier and limits](#pricing-free-tier-and-limits)
|
|
45
|
+
- [How it compares](#how-it-compares)
|
|
46
|
+
- [FAQ](#faq)
|
|
47
|
+
- [HasData links](#hasdata-links)
|
|
48
|
+
- [Development](#development)
|
|
49
|
+
- [Contributing](#contributing)
|
|
50
|
+
- [License](#license)
|
|
51
|
+
|
|
52
|
+
## What you need
|
|
53
|
+
|
|
54
|
+
An MCP client and a HasData API key from the [dashboard](https://app.hasdata.com/sign-up?utm_source=github&utm_medium=syndication&utm_campaign=google-images-mcp), free to create with no card, and the free tier covers about 200 calls a month at the 5-credit rate. This is a remote server, so the simplest path is a URL and an `x-api-key` header, with no container to run. A client that only speaks stdio reaches it through a thin launcher, published as `@hasdata/google-images-mcp` on npm and `hasdata-google-images-mcp` on PyPI, shown below.
|
|
55
|
+
|
|
56
|
+
## Quick start
|
|
57
|
+
|
|
58
|
+
The server URL is the same for every client. We run it hands-on in Claude Code and Claude Desktop. The other blocks follow each client's own documented format for a remote server.
|
|
59
|
+
|
|
60
|
+
| Field | Value |
|
|
61
|
+
| :--- | :--- |
|
|
62
|
+
| URL | `https://mcp.hasdata.com/api/mcp?apis=google_images` |
|
|
63
|
+
| Transport | HTTP, streamable |
|
|
64
|
+
| Auth header | `x-api-key: HASDATA_API_KEY` |
|
|
65
|
+
|
|
66
|
+
Clients with OAuth support can add the same URL as a connector and sign in without putting a key in a config file.
|
|
67
|
+
|
|
68
|
+
<details>
|
|
69
|
+
<summary><b>Claude Code</b></summary>
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
claude mcp add --transport http google-images "https://mcp.hasdata.com/api/mcp?apis=google_images" \
|
|
73
|
+
--header "x-api-key: HASDATA_API_KEY"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
</details>
|
|
77
|
+
|
|
78
|
+
<details>
|
|
79
|
+
<summary><b>Claude Desktop</b></summary>
|
|
80
|
+
|
|
81
|
+
Settings, then Connectors, then Add custom connector, then paste `https://mcp.hasdata.com/api/mcp?apis=google_images` and sign in.
|
|
82
|
+
|
|
83
|
+
For the config-file route, Claude Desktop loads only local (stdio) servers, so it reaches a remote server through a stdio launcher. The `@hasdata/google-images-mcp` package is that launcher, and it reads the key from the environment. Add this to `claude_desktop_config.json`:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"mcpServers": {
|
|
88
|
+
"google-images": {
|
|
89
|
+
"command": "npx",
|
|
90
|
+
"args": ["-y", "@hasdata/google-images-mcp"],
|
|
91
|
+
"env": { "HASDATA_API_KEY": "YOUR_KEY" }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
For Python instead of Node, swap the launcher for the PyPI package, which `uvx` runs without a manual install:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"mcpServers": {
|
|
102
|
+
"google-images": {
|
|
103
|
+
"command": "uvx",
|
|
104
|
+
"args": ["hasdata-google-images-mcp"],
|
|
105
|
+
"env": { "HASDATA_API_KEY": "YOUR_KEY" }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
</details>
|
|
112
|
+
|
|
113
|
+
<details>
|
|
114
|
+
<summary><b>Cursor</b></summary>
|
|
115
|
+
|
|
116
|
+
`~/.cursor/mcp.json` for every project, or `.cursor/mcp.json` for one:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"mcpServers": {
|
|
121
|
+
"google-images": {
|
|
122
|
+
"url": "https://mcp.hasdata.com/api/mcp?apis=google_images",
|
|
123
|
+
"headers": { "x-api-key": "HASDATA_API_KEY" }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
</details>
|
|
130
|
+
|
|
131
|
+
<details>
|
|
132
|
+
<summary><b>Windsurf</b></summary>
|
|
133
|
+
|
|
134
|
+
`~/.codeium/windsurf/mcp_config.json`. Windsurf calls the field `serverUrl`, not `url`:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"mcpServers": {
|
|
139
|
+
"google-images": {
|
|
140
|
+
"serverUrl": "https://mcp.hasdata.com/api/mcp?apis=google_images",
|
|
141
|
+
"headers": { "x-api-key": "HASDATA_API_KEY" }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
</details>
|
|
148
|
+
|
|
149
|
+
<details>
|
|
150
|
+
<summary><b>VS Code</b></summary>
|
|
151
|
+
|
|
152
|
+
`.vscode/mcp.json` in the workspace:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"servers": {
|
|
157
|
+
"google-images": {
|
|
158
|
+
"type": "http",
|
|
159
|
+
"url": "https://mcp.hasdata.com/api/mcp?apis=google_images",
|
|
160
|
+
"headers": { "x-api-key": "HASDATA_API_KEY" }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
</details>
|
|
167
|
+
|
|
168
|
+
## Example prompts
|
|
169
|
+
|
|
170
|
+
- Find images of the Austin skyline and give me the ones over 1500 pixels wide.
|
|
171
|
+
- Which sites are ranking images for this product name?
|
|
172
|
+
- Get black and white photographs for this query, skipping clipart and line art.
|
|
173
|
+
- Search this query from the German Google and compare the sources with the US ones.
|
|
174
|
+
- Find images of this landmark and tell me which are stock photos.
|
|
175
|
+
- Pull the next page of results for this query.
|
|
176
|
+
|
|
177
|
+
One call answers each of these, because a page carries 100 results. Paging is only needed past that.
|
|
178
|
+
|
|
179
|
+
## Tools
|
|
180
|
+
|
|
181
|
+
One tool, 5 credits per successful call.
|
|
182
|
+
|
|
183
|
+
### Get image search results
|
|
184
|
+
|
|
185
|
+
[`hasdata_google_images_images_getImageSearchResults`](https://docs.hasdata.com/apis/google-images/images?utm_source=github&utm_medium=syndication&utm_campaign=google-images-mcp)
|
|
186
|
+
|
|
187
|
+
One page of Google Images results.
|
|
188
|
+
|
|
189
|
+
| Parameter | Type | Required | Notes |
|
|
190
|
+
| :--- | :--- | :--- | :--- |
|
|
191
|
+
| `q` | string | yes | The search term |
|
|
192
|
+
| `tbs` | string | | Google's filter string for size, colour and type. See below |
|
|
193
|
+
| `location` | string | | Google canonical location for the search |
|
|
194
|
+
| `uule` | string | | The encoded location, if you already have one |
|
|
195
|
+
| `domain` | string | | Google domain, one of 195, defaults to `google.com` |
|
|
196
|
+
| `gl` | string | | Country code, one of 245 |
|
|
197
|
+
| `hl` | string | | Interface language, one of 159 |
|
|
198
|
+
| `safe` | string | | `active` or `off` |
|
|
199
|
+
| `filter` | number | | `1` keeps Google's similar and omitted result filters, `0` drops them |
|
|
200
|
+
| `deviceType` | string | | `desktop`, `mobile` or `tablet` |
|
|
201
|
+
| `ijn` | number | | Page number, where `0` is the first page |
|
|
202
|
+
|
|
203
|
+
Returns an `imagesResults` array. Each entry carries `position`, `title`, `link` to the page hosting the image, `source` as the site name, `thumbnail` on Google's CDN, `original` as the full-resolution file, and `originalWidth` and `originalHeight`.
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"position": 1,
|
|
208
|
+
"title": "The city skyline of Austin, Texas - Backyard Image",
|
|
209
|
+
"link": "https://www.backyardimage.com/the-city-skyline-of-austin-texas/",
|
|
210
|
+
"source": "Backyard Image",
|
|
211
|
+
"thumbnail": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSOGtSVdgWtQtW1_x8-gi7iQckNHXBQ5FdKqEpCKXsbWg&s=10",
|
|
212
|
+
"original": "https://www.backyardimage.com/wp-content/uploads/2025/01/2025-01-12-drone-Heap-0015-Pano-Edit.jpg",
|
|
213
|
+
"originalWidth": 1200,
|
|
214
|
+
"originalHeight": 615
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The `tbs` parameter is Google's own filter string, and several filters combine with commas.
|
|
219
|
+
|
|
220
|
+
| Filter | Value |
|
|
221
|
+
| :--- | :--- |
|
|
222
|
+
| Large, medium or icon | `isz:l`, `isz:m`, `isz:i` |
|
|
223
|
+
| Larger than a size | `isz:lt,islt:qsvga` for 400×300, through `islt:4mp` for 2272×1704 |
|
|
224
|
+
| Exact size | `isz:ex,iszw:1000,iszh:1000` |
|
|
225
|
+
| Full colour or black and white | `ic:color`, `ic:gray` |
|
|
226
|
+
| A specific colour | `ic:specific,isc:red`, and the other colour names |
|
|
227
|
+
| Type | `itp:photo`, `itp:face`, `itp:clipart`, `itp:lineart`, `itp:animated` |
|
|
228
|
+
|
|
229
|
+
## Errors and failure paths
|
|
230
|
+
|
|
231
|
+
Plan for these rather than assuming a happy path.
|
|
232
|
+
|
|
233
|
+
**`original` is a third-party URL and it can be dead, hotlink-blocked or slow.** It points at the file on whichever site hosts it, so it carries none of Google's availability. Fetch it defensively, and fall back to `thumbnail`, which is served from Google's CDN.
|
|
234
|
+
|
|
235
|
+
**`thumbnail` is a Google CDN link, not an embedded image.** It is a real URL rather than a data URI, so displaying one still costs a request to `gstatic.com`.
|
|
236
|
+
|
|
237
|
+
**A page carries 100 results, not 10.** Cost is per call rather than per result, so one call covering 100 images is the cheapest shape here. Reach for `ijn` only past the first hundred.
|
|
238
|
+
|
|
239
|
+
**`ijn` starts at zero.** Passing `1` gets the second page, not the first.
|
|
240
|
+
|
|
241
|
+
**Dimensions describe the original, not the thumbnail.** `originalWidth` and `originalHeight` are the file behind `original`, so a size filter applied locally has to read those rather than measure what you displayed.
|
|
242
|
+
|
|
243
|
+
**`tbs` values are Google's, and an unrecognised one is ignored rather than rejected.** A filter that silently does nothing looks the same as a filter that found everything, so check that the results actually narrowed before trusting a `tbs` string you assembled by hand.
|
|
244
|
+
|
|
245
|
+
**`domain`, `gl` and `hl` are three separate axes.** The domain is which Google answers, the country code is where the search comes from, and the language is the interface. Changing one moves the results less than you expect.
|
|
246
|
+
|
|
247
|
+
Results that carry data also carry a `requestMetadata.id` worth quoting in support.
|
|
248
|
+
|
|
249
|
+
## Pricing, free tier and limits
|
|
250
|
+
|
|
251
|
+
The Google Images tool costs **5 credits per successful call**. Response size does not change the price, and a call returns 100 results, which makes this one of the cheaper tools in the catalogue per row returned.
|
|
252
|
+
|
|
253
|
+
The free tier is **1,000 credits every month with no card**, which is 200 Google Images calls at the base rate. It renews with the billing cycle, so a low-volume agent runs on the free tier indefinitely.
|
|
254
|
+
|
|
255
|
+
Paid plans start at **$49 a month** for 200,000 credits, which is 40,000 calls. The unit price falls with volume, from **$1.23 per 1,000 calls** on the entry plan to **$0.50** on Business, **$0.42** on Growth and **$0.37** on the largest [high-volume plans](https://hasdata.com/prices?utm_source=github&utm_medium=syndication&utm_campaign=google-images-mcp).
|
|
256
|
+
|
|
257
|
+
Your plan also sets concurrency. The free tier allows 1 request at a time, Startup 15, Business 30, Growth 50, and the high-volume plans run from 200 to 1,500. Retry on the 429 with a backoff in anything unattended, because an agent that sweeps a keyword list will reach the ceiling before you do.
|
|
258
|
+
|
|
259
|
+
A request that comes back non-200 is not billed. A successful call that finds nothing is still a call.
|
|
260
|
+
|
|
261
|
+
## How it compares
|
|
262
|
+
|
|
263
|
+
Google's Custom Search JSON API is the official route to image results, and it is a narrower instrument.
|
|
264
|
+
|
|
265
|
+
| | Custom Search JSON API | This server |
|
|
266
|
+
| :--- | :--- | :--- |
|
|
267
|
+
| Eligibility | A Google Cloud project and a search engine you configure | An API key |
|
|
268
|
+
| Results per call | 10 | 100 |
|
|
269
|
+
| Daily ceiling | 10,000 queries a day, free tier of 100 | Your plan's credits |
|
|
270
|
+
| Scope | The sites your programmable engine covers | The public Google Images page |
|
|
271
|
+
| Filter set | A documented subset | Google's own `tbs` string |
|
|
272
|
+
| Full-resolution URL | Returned | Returned |
|
|
273
|
+
|
|
274
|
+
The row that decides it is scope. A programmable search engine covers what you configure it to cover, and searching the open web with it takes extra setup and still behaves differently from the page a person sees. When you control the sites you want to search, the official API is free at low volume and the better fit.
|
|
275
|
+
|
|
276
|
+
## FAQ
|
|
277
|
+
|
|
278
|
+
### Is there an official Google Images MCP server?
|
|
279
|
+
|
|
280
|
+
Google does not publish one. This one is maintained by HasData and reads public Google Images pages.
|
|
281
|
+
|
|
282
|
+
### What is a Google Images MCP server?
|
|
283
|
+
|
|
284
|
+
An MCP server exposes tools an AI client can call. This one turns a Google Images results page into JSON an agent can reason over, without a browser or a scraping library in your stack.
|
|
285
|
+
|
|
286
|
+
### Do I need a Google account or a Cloud project?
|
|
287
|
+
|
|
288
|
+
No. The only credential is your HasData key.
|
|
289
|
+
|
|
290
|
+
### Can I download the images?
|
|
291
|
+
|
|
292
|
+
The response gives you `original`, the URL of the file on the site that hosts it. Whether you may download, store or reuse that file is a copyright question about that image, and it is not answered by the fact that Google indexed it. Assume an image is protected unless you have checked otherwise.
|
|
293
|
+
|
|
294
|
+
### Can I search by image rather than by text?
|
|
295
|
+
|
|
296
|
+
No. This tool takes a text query. Reverse image search is a different endpoint.
|
|
297
|
+
|
|
298
|
+
### How do I filter by size?
|
|
299
|
+
|
|
300
|
+
Through `tbs`. `isz:l` gets large images, `isz:lt,islt:2mp` gets anything above 1600×1200, and `isz:ex,iszw:1000,iszh:1000` gets exactly 1000×1000. Filters combine with commas.
|
|
301
|
+
|
|
302
|
+
### Can I use this together with other HasData APIs?
|
|
303
|
+
|
|
304
|
+
Yes. One key covers everything, and one endpoint serves them all through the `apis` parameter. Point a client at `?apis=google_images,google_serp` to get both tool sets in one connection, or at [`mcp.hasdata.com/api/mcp`](https://docs.hasdata.com/mcp-server?utm_source=github&utm_medium=syndication&utm_campaign=google-images-mcp) for the full catalogue.
|
|
305
|
+
|
|
306
|
+
### Is HasData affiliated with Google?
|
|
307
|
+
|
|
308
|
+
No. HasData is an independent service and is not affiliated with, endorsed by, or sponsored by Google. Google is a trademark of its respective owner. The tools work with publicly available data only, and you are responsible for using the results in line with Google's terms and the law that applies to you.
|
|
309
|
+
|
|
310
|
+
### Compliance and personal data
|
|
311
|
+
|
|
312
|
+
Image search returns whatever the query matched, so a query naming a person returns pictures of that person, and `itp:face` asks for faces specifically. Images of identifiable people are personal data, and in several jurisdictions biometric processing is regulated separately and more strictly again. Building a face set from these results is the case to think hardest about before you start, rather than after.
|
|
313
|
+
|
|
314
|
+
## HasData links
|
|
315
|
+
|
|
316
|
+
- [Google Images API](https://hasdata.com/apis/google-images-api?utm_source=github&utm_medium=syndication&utm_campaign=google-images-mcp), the REST endpoint behind this tool
|
|
317
|
+
- [API documentation](https://docs.hasdata.com/apis/google-images/images?utm_source=github&utm_medium=syndication&utm_campaign=google-images-mcp)
|
|
318
|
+
- [MCP server documentation](https://docs.hasdata.com/mcp-server?utm_source=github&utm_medium=syndication&utm_campaign=google-images-mcp)
|
|
319
|
+
- [Pricing](https://hasdata.com/prices?utm_source=github&utm_medium=syndication&utm_campaign=google-images-mcp)
|
|
320
|
+
- [Dashboard](https://app.hasdata.com/sign-up?utm_source=github&utm_medium=syndication&utm_campaign=google-images-mcp)
|
|
321
|
+
|
|
322
|
+
Other HasData MCP servers: [Google Search](https://github.com/HasData/google-search-mcp), [Google Maps](https://github.com/HasData/google-maps-mcp), [Google Trends](https://github.com/HasData/google-trends-mcp), [Google Flights](https://github.com/HasData/google-flights-mcp), [Bing](https://github.com/HasData/bing-mcp), [DuckDuckGo](https://github.com/HasData/duckduckgo-mcp), [YouTube](https://github.com/HasData/youtube-mcp), [TikTok](https://github.com/HasData/tiktok-mcp), [Instagram](https://github.com/HasData/instagram-mcp), [Amazon](https://github.com/HasData/amazon-mcp), [Walmart](https://github.com/HasData/walmart-mcp), [Shopify](https://github.com/HasData/shopify-mcp), [Yelp](https://github.com/HasData/yelp-mcp), [Yellow Pages](https://github.com/HasData/yellowpages-mcp), [Zillow](https://github.com/HasData/zillow-mcp), [Redfin](https://github.com/HasData/redfin-mcp), [Airbnb](https://github.com/HasData/airbnb-mcp), [Booking.com](https://github.com/HasData/booking-mcp), [Indeed](https://github.com/HasData/indeed-mcp), [Glassdoor](https://github.com/HasData/glassdoor-mcp).
|
|
323
|
+
|
|
324
|
+
## Development
|
|
325
|
+
|
|
326
|
+
The launcher is a thin stdio bridge to the remote server, so there is nothing to build.
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
npm install
|
|
330
|
+
HASDATA_API_KEY=your_key_here npm test
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
The tests in `test/` assert the tool contract, the part that can break without a commit here. They check that `?apis=google_images` returns the one expected tool, that its name has not changed, that it still requires `q` and carries a description, that the filter and targeting parameters this README documents are still in the schema, and that the key in use is actually accepted. That last check calls the tool for real and costs 5 credits, which is the price of a canary that can fail for the right reason.
|
|
334
|
+
|
|
335
|
+
One test asserts that a live search returns results carrying both `original` and its dimensions. Those three fields are what makes this tool worth calling over a plain SERP, and a parser change that dropped them would leave a green tools list behind it.
|
|
336
|
+
|
|
337
|
+
The contract suite also runs weekly on a schedule, because the upstream tool list can change without anyone touching this repository.
|
|
338
|
+
|
|
339
|
+
## Contributing
|
|
340
|
+
|
|
341
|
+
A tool table, a response sample or a documented behaviour that does not match reality is worth an issue. There is a template for exactly that. Pull requests are welcome for the same, and for anything in the launcher.
|
|
342
|
+
|
|
343
|
+
## License
|
|
344
|
+
|
|
345
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# Google Images MCP Server
|
|
2
|
+
|
|
3
|
+
<!-- mcp-name: com.hasdata/google-images -->
|
|
4
|
+
|
|
5
|
+
A hosted Model Context Protocol (MCP) server that gives Claude, Cursor, Windsurf and any other MCP client one read-only Google Images tool. Run an image search with size, colour and type filters and get 100 results a call, each with the page it came from and the full-resolution file URL, as structured JSON, with nothing to host.
|
|
6
|
+
|
|
7
|
+
It reads the Google Images results page a signed-out visitor sees.
|
|
8
|
+
|
|
9
|
+
**1,000 free credits every month, no card required**, which is 200 Google Images calls at the 5-credit rate.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
https://mcp.hasdata.com/api/mcp?apis=google_images
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
[](https://glama.ai/mcp/servers/HasData/google-images-mcp)
|
|
16
|
+
[](https://github.com/HasData/google-images-mcp/actions/workflows/contract.yml)
|
|
17
|
+
[](https://mcp.hasdata.com/api/mcp?apis=google_images)
|
|
18
|
+
[](#tools)
|
|
19
|
+
[](https://www.npmjs.com/package/@hasdata/google-images-mcp)
|
|
20
|
+
[](https://pypi.org/project/hasdata-google-images-mcp/)
|
|
21
|
+
[](LICENSE)
|
|
22
|
+
|
|
23
|
+
## Contents
|
|
24
|
+
|
|
25
|
+
- [What you need](#what-you-need)
|
|
26
|
+
- [Quick start](#quick-start)
|
|
27
|
+
- [Example prompts](#example-prompts)
|
|
28
|
+
- [Tools](#tools)
|
|
29
|
+
- [Errors and failure paths](#errors-and-failure-paths)
|
|
30
|
+
- [Pricing, free tier and limits](#pricing-free-tier-and-limits)
|
|
31
|
+
- [How it compares](#how-it-compares)
|
|
32
|
+
- [FAQ](#faq)
|
|
33
|
+
- [HasData links](#hasdata-links)
|
|
34
|
+
- [Development](#development)
|
|
35
|
+
- [Contributing](#contributing)
|
|
36
|
+
- [License](#license)
|
|
37
|
+
|
|
38
|
+
## What you need
|
|
39
|
+
|
|
40
|
+
An MCP client and a HasData API key from the [dashboard](https://app.hasdata.com/sign-up?utm_source=github&utm_medium=syndication&utm_campaign=google-images-mcp), free to create with no card, and the free tier covers about 200 calls a month at the 5-credit rate. This is a remote server, so the simplest path is a URL and an `x-api-key` header, with no container to run. A client that only speaks stdio reaches it through a thin launcher, published as `@hasdata/google-images-mcp` on npm and `hasdata-google-images-mcp` on PyPI, shown below.
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
The server URL is the same for every client. We run it hands-on in Claude Code and Claude Desktop. The other blocks follow each client's own documented format for a remote server.
|
|
45
|
+
|
|
46
|
+
| Field | Value |
|
|
47
|
+
| :--- | :--- |
|
|
48
|
+
| URL | `https://mcp.hasdata.com/api/mcp?apis=google_images` |
|
|
49
|
+
| Transport | HTTP, streamable |
|
|
50
|
+
| Auth header | `x-api-key: HASDATA_API_KEY` |
|
|
51
|
+
|
|
52
|
+
Clients with OAuth support can add the same URL as a connector and sign in without putting a key in a config file.
|
|
53
|
+
|
|
54
|
+
<details>
|
|
55
|
+
<summary><b>Claude Code</b></summary>
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
claude mcp add --transport http google-images "https://mcp.hasdata.com/api/mcp?apis=google_images" \
|
|
59
|
+
--header "x-api-key: HASDATA_API_KEY"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
</details>
|
|
63
|
+
|
|
64
|
+
<details>
|
|
65
|
+
<summary><b>Claude Desktop</b></summary>
|
|
66
|
+
|
|
67
|
+
Settings, then Connectors, then Add custom connector, then paste `https://mcp.hasdata.com/api/mcp?apis=google_images` and sign in.
|
|
68
|
+
|
|
69
|
+
For the config-file route, Claude Desktop loads only local (stdio) servers, so it reaches a remote server through a stdio launcher. The `@hasdata/google-images-mcp` package is that launcher, and it reads the key from the environment. Add this to `claude_desktop_config.json`:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"mcpServers": {
|
|
74
|
+
"google-images": {
|
|
75
|
+
"command": "npx",
|
|
76
|
+
"args": ["-y", "@hasdata/google-images-mcp"],
|
|
77
|
+
"env": { "HASDATA_API_KEY": "YOUR_KEY" }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
For Python instead of Node, swap the launcher for the PyPI package, which `uvx` runs without a manual install:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"mcpServers": {
|
|
88
|
+
"google-images": {
|
|
89
|
+
"command": "uvx",
|
|
90
|
+
"args": ["hasdata-google-images-mcp"],
|
|
91
|
+
"env": { "HASDATA_API_KEY": "YOUR_KEY" }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
</details>
|
|
98
|
+
|
|
99
|
+
<details>
|
|
100
|
+
<summary><b>Cursor</b></summary>
|
|
101
|
+
|
|
102
|
+
`~/.cursor/mcp.json` for every project, or `.cursor/mcp.json` for one:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"mcpServers": {
|
|
107
|
+
"google-images": {
|
|
108
|
+
"url": "https://mcp.hasdata.com/api/mcp?apis=google_images",
|
|
109
|
+
"headers": { "x-api-key": "HASDATA_API_KEY" }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
</details>
|
|
116
|
+
|
|
117
|
+
<details>
|
|
118
|
+
<summary><b>Windsurf</b></summary>
|
|
119
|
+
|
|
120
|
+
`~/.codeium/windsurf/mcp_config.json`. Windsurf calls the field `serverUrl`, not `url`:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"mcpServers": {
|
|
125
|
+
"google-images": {
|
|
126
|
+
"serverUrl": "https://mcp.hasdata.com/api/mcp?apis=google_images",
|
|
127
|
+
"headers": { "x-api-key": "HASDATA_API_KEY" }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
</details>
|
|
134
|
+
|
|
135
|
+
<details>
|
|
136
|
+
<summary><b>VS Code</b></summary>
|
|
137
|
+
|
|
138
|
+
`.vscode/mcp.json` in the workspace:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"servers": {
|
|
143
|
+
"google-images": {
|
|
144
|
+
"type": "http",
|
|
145
|
+
"url": "https://mcp.hasdata.com/api/mcp?apis=google_images",
|
|
146
|
+
"headers": { "x-api-key": "HASDATA_API_KEY" }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
</details>
|
|
153
|
+
|
|
154
|
+
## Example prompts
|
|
155
|
+
|
|
156
|
+
- Find images of the Austin skyline and give me the ones over 1500 pixels wide.
|
|
157
|
+
- Which sites are ranking images for this product name?
|
|
158
|
+
- Get black and white photographs for this query, skipping clipart and line art.
|
|
159
|
+
- Search this query from the German Google and compare the sources with the US ones.
|
|
160
|
+
- Find images of this landmark and tell me which are stock photos.
|
|
161
|
+
- Pull the next page of results for this query.
|
|
162
|
+
|
|
163
|
+
One call answers each of these, because a page carries 100 results. Paging is only needed past that.
|
|
164
|
+
|
|
165
|
+
## Tools
|
|
166
|
+
|
|
167
|
+
One tool, 5 credits per successful call.
|
|
168
|
+
|
|
169
|
+
### Get image search results
|
|
170
|
+
|
|
171
|
+
[`hasdata_google_images_images_getImageSearchResults`](https://docs.hasdata.com/apis/google-images/images?utm_source=github&utm_medium=syndication&utm_campaign=google-images-mcp)
|
|
172
|
+
|
|
173
|
+
One page of Google Images results.
|
|
174
|
+
|
|
175
|
+
| Parameter | Type | Required | Notes |
|
|
176
|
+
| :--- | :--- | :--- | :--- |
|
|
177
|
+
| `q` | string | yes | The search term |
|
|
178
|
+
| `tbs` | string | | Google's filter string for size, colour and type. See below |
|
|
179
|
+
| `location` | string | | Google canonical location for the search |
|
|
180
|
+
| `uule` | string | | The encoded location, if you already have one |
|
|
181
|
+
| `domain` | string | | Google domain, one of 195, defaults to `google.com` |
|
|
182
|
+
| `gl` | string | | Country code, one of 245 |
|
|
183
|
+
| `hl` | string | | Interface language, one of 159 |
|
|
184
|
+
| `safe` | string | | `active` or `off` |
|
|
185
|
+
| `filter` | number | | `1` keeps Google's similar and omitted result filters, `0` drops them |
|
|
186
|
+
| `deviceType` | string | | `desktop`, `mobile` or `tablet` |
|
|
187
|
+
| `ijn` | number | | Page number, where `0` is the first page |
|
|
188
|
+
|
|
189
|
+
Returns an `imagesResults` array. Each entry carries `position`, `title`, `link` to the page hosting the image, `source` as the site name, `thumbnail` on Google's CDN, `original` as the full-resolution file, and `originalWidth` and `originalHeight`.
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"position": 1,
|
|
194
|
+
"title": "The city skyline of Austin, Texas - Backyard Image",
|
|
195
|
+
"link": "https://www.backyardimage.com/the-city-skyline-of-austin-texas/",
|
|
196
|
+
"source": "Backyard Image",
|
|
197
|
+
"thumbnail": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSOGtSVdgWtQtW1_x8-gi7iQckNHXBQ5FdKqEpCKXsbWg&s=10",
|
|
198
|
+
"original": "https://www.backyardimage.com/wp-content/uploads/2025/01/2025-01-12-drone-Heap-0015-Pano-Edit.jpg",
|
|
199
|
+
"originalWidth": 1200,
|
|
200
|
+
"originalHeight": 615
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The `tbs` parameter is Google's own filter string, and several filters combine with commas.
|
|
205
|
+
|
|
206
|
+
| Filter | Value |
|
|
207
|
+
| :--- | :--- |
|
|
208
|
+
| Large, medium or icon | `isz:l`, `isz:m`, `isz:i` |
|
|
209
|
+
| Larger than a size | `isz:lt,islt:qsvga` for 400×300, through `islt:4mp` for 2272×1704 |
|
|
210
|
+
| Exact size | `isz:ex,iszw:1000,iszh:1000` |
|
|
211
|
+
| Full colour or black and white | `ic:color`, `ic:gray` |
|
|
212
|
+
| A specific colour | `ic:specific,isc:red`, and the other colour names |
|
|
213
|
+
| Type | `itp:photo`, `itp:face`, `itp:clipart`, `itp:lineart`, `itp:animated` |
|
|
214
|
+
|
|
215
|
+
## Errors and failure paths
|
|
216
|
+
|
|
217
|
+
Plan for these rather than assuming a happy path.
|
|
218
|
+
|
|
219
|
+
**`original` is a third-party URL and it can be dead, hotlink-blocked or slow.** It points at the file on whichever site hosts it, so it carries none of Google's availability. Fetch it defensively, and fall back to `thumbnail`, which is served from Google's CDN.
|
|
220
|
+
|
|
221
|
+
**`thumbnail` is a Google CDN link, not an embedded image.** It is a real URL rather than a data URI, so displaying one still costs a request to `gstatic.com`.
|
|
222
|
+
|
|
223
|
+
**A page carries 100 results, not 10.** Cost is per call rather than per result, so one call covering 100 images is the cheapest shape here. Reach for `ijn` only past the first hundred.
|
|
224
|
+
|
|
225
|
+
**`ijn` starts at zero.** Passing `1` gets the second page, not the first.
|
|
226
|
+
|
|
227
|
+
**Dimensions describe the original, not the thumbnail.** `originalWidth` and `originalHeight` are the file behind `original`, so a size filter applied locally has to read those rather than measure what you displayed.
|
|
228
|
+
|
|
229
|
+
**`tbs` values are Google's, and an unrecognised one is ignored rather than rejected.** A filter that silently does nothing looks the same as a filter that found everything, so check that the results actually narrowed before trusting a `tbs` string you assembled by hand.
|
|
230
|
+
|
|
231
|
+
**`domain`, `gl` and `hl` are three separate axes.** The domain is which Google answers, the country code is where the search comes from, and the language is the interface. Changing one moves the results less than you expect.
|
|
232
|
+
|
|
233
|
+
Results that carry data also carry a `requestMetadata.id` worth quoting in support.
|
|
234
|
+
|
|
235
|
+
## Pricing, free tier and limits
|
|
236
|
+
|
|
237
|
+
The Google Images tool costs **5 credits per successful call**. Response size does not change the price, and a call returns 100 results, which makes this one of the cheaper tools in the catalogue per row returned.
|
|
238
|
+
|
|
239
|
+
The free tier is **1,000 credits every month with no card**, which is 200 Google Images calls at the base rate. It renews with the billing cycle, so a low-volume agent runs on the free tier indefinitely.
|
|
240
|
+
|
|
241
|
+
Paid plans start at **$49 a month** for 200,000 credits, which is 40,000 calls. The unit price falls with volume, from **$1.23 per 1,000 calls** on the entry plan to **$0.50** on Business, **$0.42** on Growth and **$0.37** on the largest [high-volume plans](https://hasdata.com/prices?utm_source=github&utm_medium=syndication&utm_campaign=google-images-mcp).
|
|
242
|
+
|
|
243
|
+
Your plan also sets concurrency. The free tier allows 1 request at a time, Startup 15, Business 30, Growth 50, and the high-volume plans run from 200 to 1,500. Retry on the 429 with a backoff in anything unattended, because an agent that sweeps a keyword list will reach the ceiling before you do.
|
|
244
|
+
|
|
245
|
+
A request that comes back non-200 is not billed. A successful call that finds nothing is still a call.
|
|
246
|
+
|
|
247
|
+
## How it compares
|
|
248
|
+
|
|
249
|
+
Google's Custom Search JSON API is the official route to image results, and it is a narrower instrument.
|
|
250
|
+
|
|
251
|
+
| | Custom Search JSON API | This server |
|
|
252
|
+
| :--- | :--- | :--- |
|
|
253
|
+
| Eligibility | A Google Cloud project and a search engine you configure | An API key |
|
|
254
|
+
| Results per call | 10 | 100 |
|
|
255
|
+
| Daily ceiling | 10,000 queries a day, free tier of 100 | Your plan's credits |
|
|
256
|
+
| Scope | The sites your programmable engine covers | The public Google Images page |
|
|
257
|
+
| Filter set | A documented subset | Google's own `tbs` string |
|
|
258
|
+
| Full-resolution URL | Returned | Returned |
|
|
259
|
+
|
|
260
|
+
The row that decides it is scope. A programmable search engine covers what you configure it to cover, and searching the open web with it takes extra setup and still behaves differently from the page a person sees. When you control the sites you want to search, the official API is free at low volume and the better fit.
|
|
261
|
+
|
|
262
|
+
## FAQ
|
|
263
|
+
|
|
264
|
+
### Is there an official Google Images MCP server?
|
|
265
|
+
|
|
266
|
+
Google does not publish one. This one is maintained by HasData and reads public Google Images pages.
|
|
267
|
+
|
|
268
|
+
### What is a Google Images MCP server?
|
|
269
|
+
|
|
270
|
+
An MCP server exposes tools an AI client can call. This one turns a Google Images results page into JSON an agent can reason over, without a browser or a scraping library in your stack.
|
|
271
|
+
|
|
272
|
+
### Do I need a Google account or a Cloud project?
|
|
273
|
+
|
|
274
|
+
No. The only credential is your HasData key.
|
|
275
|
+
|
|
276
|
+
### Can I download the images?
|
|
277
|
+
|
|
278
|
+
The response gives you `original`, the URL of the file on the site that hosts it. Whether you may download, store or reuse that file is a copyright question about that image, and it is not answered by the fact that Google indexed it. Assume an image is protected unless you have checked otherwise.
|
|
279
|
+
|
|
280
|
+
### Can I search by image rather than by text?
|
|
281
|
+
|
|
282
|
+
No. This tool takes a text query. Reverse image search is a different endpoint.
|
|
283
|
+
|
|
284
|
+
### How do I filter by size?
|
|
285
|
+
|
|
286
|
+
Through `tbs`. `isz:l` gets large images, `isz:lt,islt:2mp` gets anything above 1600×1200, and `isz:ex,iszw:1000,iszh:1000` gets exactly 1000×1000. Filters combine with commas.
|
|
287
|
+
|
|
288
|
+
### Can I use this together with other HasData APIs?
|
|
289
|
+
|
|
290
|
+
Yes. One key covers everything, and one endpoint serves them all through the `apis` parameter. Point a client at `?apis=google_images,google_serp` to get both tool sets in one connection, or at [`mcp.hasdata.com/api/mcp`](https://docs.hasdata.com/mcp-server?utm_source=github&utm_medium=syndication&utm_campaign=google-images-mcp) for the full catalogue.
|
|
291
|
+
|
|
292
|
+
### Is HasData affiliated with Google?
|
|
293
|
+
|
|
294
|
+
No. HasData is an independent service and is not affiliated with, endorsed by, or sponsored by Google. Google is a trademark of its respective owner. The tools work with publicly available data only, and you are responsible for using the results in line with Google's terms and the law that applies to you.
|
|
295
|
+
|
|
296
|
+
### Compliance and personal data
|
|
297
|
+
|
|
298
|
+
Image search returns whatever the query matched, so a query naming a person returns pictures of that person, and `itp:face` asks for faces specifically. Images of identifiable people are personal data, and in several jurisdictions biometric processing is regulated separately and more strictly again. Building a face set from these results is the case to think hardest about before you start, rather than after.
|
|
299
|
+
|
|
300
|
+
## HasData links
|
|
301
|
+
|
|
302
|
+
- [Google Images API](https://hasdata.com/apis/google-images-api?utm_source=github&utm_medium=syndication&utm_campaign=google-images-mcp), the REST endpoint behind this tool
|
|
303
|
+
- [API documentation](https://docs.hasdata.com/apis/google-images/images?utm_source=github&utm_medium=syndication&utm_campaign=google-images-mcp)
|
|
304
|
+
- [MCP server documentation](https://docs.hasdata.com/mcp-server?utm_source=github&utm_medium=syndication&utm_campaign=google-images-mcp)
|
|
305
|
+
- [Pricing](https://hasdata.com/prices?utm_source=github&utm_medium=syndication&utm_campaign=google-images-mcp)
|
|
306
|
+
- [Dashboard](https://app.hasdata.com/sign-up?utm_source=github&utm_medium=syndication&utm_campaign=google-images-mcp)
|
|
307
|
+
|
|
308
|
+
Other HasData MCP servers: [Google Search](https://github.com/HasData/google-search-mcp), [Google Maps](https://github.com/HasData/google-maps-mcp), [Google Trends](https://github.com/HasData/google-trends-mcp), [Google Flights](https://github.com/HasData/google-flights-mcp), [Bing](https://github.com/HasData/bing-mcp), [DuckDuckGo](https://github.com/HasData/duckduckgo-mcp), [YouTube](https://github.com/HasData/youtube-mcp), [TikTok](https://github.com/HasData/tiktok-mcp), [Instagram](https://github.com/HasData/instagram-mcp), [Amazon](https://github.com/HasData/amazon-mcp), [Walmart](https://github.com/HasData/walmart-mcp), [Shopify](https://github.com/HasData/shopify-mcp), [Yelp](https://github.com/HasData/yelp-mcp), [Yellow Pages](https://github.com/HasData/yellowpages-mcp), [Zillow](https://github.com/HasData/zillow-mcp), [Redfin](https://github.com/HasData/redfin-mcp), [Airbnb](https://github.com/HasData/airbnb-mcp), [Booking.com](https://github.com/HasData/booking-mcp), [Indeed](https://github.com/HasData/indeed-mcp), [Glassdoor](https://github.com/HasData/glassdoor-mcp).
|
|
309
|
+
|
|
310
|
+
## Development
|
|
311
|
+
|
|
312
|
+
The launcher is a thin stdio bridge to the remote server, so there is nothing to build.
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
npm install
|
|
316
|
+
HASDATA_API_KEY=your_key_here npm test
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
The tests in `test/` assert the tool contract, the part that can break without a commit here. They check that `?apis=google_images` returns the one expected tool, that its name has not changed, that it still requires `q` and carries a description, that the filter and targeting parameters this README documents are still in the schema, and that the key in use is actually accepted. That last check calls the tool for real and costs 5 credits, which is the price of a canary that can fail for the right reason.
|
|
320
|
+
|
|
321
|
+
One test asserts that a live search returns results carrying both `original` and its dimensions. Those three fields are what makes this tool worth calling over a plain SERP, and a parser change that dropped them would leave a green tools list behind it.
|
|
322
|
+
|
|
323
|
+
The contract suite also runs weekly on a schedule, because the upstream tool list can change without anyone touching this repository.
|
|
324
|
+
|
|
325
|
+
## Contributing
|
|
326
|
+
|
|
327
|
+
A tool table, a response sample or a documented behaviour that does not match reality is worth an issue. There is a template for exactly that. Pull requests are welcome for the same, and for anything in the launcher.
|
|
328
|
+
|
|
329
|
+
## License
|
|
330
|
+
|
|
331
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Thin launcher for HasData's hosted Google Images MCP server.
|
|
2
|
+
|
|
3
|
+
Connects an MCP client to the remote streamable-HTTP endpoint through mcp-proxy.
|
|
4
|
+
The server runs on HasData's infrastructure. This package only proxies stdio to it.
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import subprocess
|
|
9
|
+
|
|
10
|
+
URL = "https://mcp.hasdata.com/api/mcp?apis=google_images"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main() -> None:
|
|
14
|
+
key = os.environ.get("HASDATA_API_KEY")
|
|
15
|
+
if not key:
|
|
16
|
+
sys.stderr.write(
|
|
17
|
+
"HASDATA_API_KEY is not set. Create a free key at https://app.hasdata.com "
|
|
18
|
+
"and set HASDATA_API_KEY.\n"
|
|
19
|
+
)
|
|
20
|
+
raise SystemExit(1)
|
|
21
|
+
args = [
|
|
22
|
+
sys.executable, "-m", "mcp_proxy", URL,
|
|
23
|
+
"--transport=streamablehttp",
|
|
24
|
+
"--headers", "x-api-key", key,
|
|
25
|
+
]
|
|
26
|
+
rc = subprocess.call(args)
|
|
27
|
+
# subprocess returns -N when mcp_proxy is killed by signal N; map to 128+N.
|
|
28
|
+
raise SystemExit(rc if rc >= 0 else 128 - rc)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Thin launcher: connects an MCP client to HasData's hosted Google Images MCP server
|
|
3
|
+
// (streamable HTTP) through the mcp-remote stdio bridge. The server runs remotely.
|
|
4
|
+
// This package only proxies, so nothing here scrapes anything.
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
const URL = 'https://mcp.hasdata.com/api/mcp?apis=google_images';
|
|
10
|
+
const key = process.env.HASDATA_API_KEY;
|
|
11
|
+
if (!key) {
|
|
12
|
+
process.stderr.write('HASDATA_API_KEY is not set. Create a free key at https://app.hasdata.com and set HASDATA_API_KEY.\n');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
// Resolve mcp-remote's CLI from its own package.json bin, so a future layout change
|
|
16
|
+
// or an exports map does not break a hardcoded deep path.
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const pkg = require('mcp-remote/package.json');
|
|
19
|
+
const proxy = join(dirname(require.resolve('mcp-remote/package.json')), pkg.bin['mcp-remote']);
|
|
20
|
+
const child = spawn(process.execPath, [proxy, URL, '--header', `x-api-key:${key}`], { stdio: 'inherit' });
|
|
21
|
+
child.on('exit', (code, signal) => {
|
|
22
|
+
if (signal) process.kill(process.pid, signal);
|
|
23
|
+
else process.exit(code ?? 0);
|
|
24
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
|
|
3
|
+
"mcpServers": {
|
|
4
|
+
"google-images": {
|
|
5
|
+
"type": "streamable-http",
|
|
6
|
+
"url": "https://mcp.hasdata.com/api/mcp?apis=google_images",
|
|
7
|
+
"headers": {
|
|
8
|
+
"x-api-key": "YOUR_HASDATA_API_KEY"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hasdata/google-images-mcp",
|
|
3
|
+
"mcpName": "com.hasdata/google-images",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "MCP server for Google Images through HasData's hosted API: image search with size, colour and type filters, returning 100 results a call with the source page and the full-resolution URL. 1,000 free credits every month.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"hasdata-google-images-mcp": "index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.mjs",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"mcp-remote": "^0.1.43"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/HasData/google-images-mcp.git"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://hasdata.com/apis/google-images-api",
|
|
30
|
+
"keywords": [
|
|
31
|
+
"mcp",
|
|
32
|
+
"google-images",
|
|
33
|
+
"image-search",
|
|
34
|
+
"serp",
|
|
35
|
+
"reverse-image",
|
|
36
|
+
"seo",
|
|
37
|
+
"model-context-protocol",
|
|
38
|
+
"hasdata",
|
|
39
|
+
"scraper"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
|
+
"name": "hasdata-google-images-mcp",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Google Images MCP server for Cursor. Image search results with source pages, thumbnails and full-resolution URLs, filtered by size, colour and type, as structured JSON. Remote MCP server hosted by HasData, connects over streamable HTTP with your HasData API key.",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "HasData",
|
|
8
|
+
"url": "https://hasdata.com"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://hasdata.com/apis/google-images-api",
|
|
11
|
+
"repository": "https://github.com/HasData/google-images-mcp",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"mcp",
|
|
15
|
+
"google-images",
|
|
16
|
+
"image-search",
|
|
17
|
+
"serp",
|
|
18
|
+
"reverse-image",
|
|
19
|
+
"seo",
|
|
20
|
+
"model-context-protocol",
|
|
21
|
+
"hasdata",
|
|
22
|
+
"scraper"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hasdata-google-images-mcp"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "MCP server for Google Images through HasData's hosted API. 1,000 free credits every month."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
keywords = ["mcp","google-images","image-search","serp","seo","model-context-protocol","hasdata"]
|
|
9
|
+
# mcp is pinned below 2 because mcp-proxy 0.12.0 imports request_ctx, removed in the mcp 2.x SDK.
|
|
10
|
+
dependencies = ["mcp-proxy>=0.12.0", "mcp>=1.17,<2"]
|
|
11
|
+
|
|
12
|
+
[project.urls]
|
|
13
|
+
Homepage = "https://hasdata.com/apis/google-images-api"
|
|
14
|
+
Repository = "https://github.com/HasData/google-images-mcp"
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
hasdata-google-images-mcp = "hasdata_google_images_mcp:main"
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["hatchling"]
|
|
21
|
+
build-backend = "hatchling.build"
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.wheel]
|
|
24
|
+
packages = ["hasdata_google_images_mcp"]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "com.hasdata/google-images",
|
|
4
|
+
"title": "HasData Google Images",
|
|
5
|
+
"description": "Google Images results with source pages, thumbnails and full-resolution URLs, as JSON.",
|
|
6
|
+
"version": "1.0.0",
|
|
7
|
+
"websiteUrl": "https://hasdata.com/apis/google-images-api",
|
|
8
|
+
"repository": {
|
|
9
|
+
"url": "https://github.com/HasData/google-images-mcp",
|
|
10
|
+
"source": "github"
|
|
11
|
+
},
|
|
12
|
+
"remotes": [
|
|
13
|
+
{
|
|
14
|
+
"type": "streamable-http",
|
|
15
|
+
"url": "https://mcp.hasdata.com/api/mcp?apis=google_images",
|
|
16
|
+
"headers": [
|
|
17
|
+
{
|
|
18
|
+
"name": "x-api-key",
|
|
19
|
+
"description": "HasData API key, created at app.hasdata.com. 1,000 free credits every month, no card.",
|
|
20
|
+
"isRequired": true,
|
|
21
|
+
"isSecret": true
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"packages": [
|
|
27
|
+
{
|
|
28
|
+
"registryType": "npm",
|
|
29
|
+
"identifier": "@hasdata/google-images-mcp",
|
|
30
|
+
"version": "1.0.0",
|
|
31
|
+
"transport": {
|
|
32
|
+
"type": "stdio"
|
|
33
|
+
},
|
|
34
|
+
"environmentVariables": [
|
|
35
|
+
{
|
|
36
|
+
"name": "HASDATA_API_KEY",
|
|
37
|
+
"description": "HasData API key, created at app.hasdata.com. 1,000 free credits every month, no card.",
|
|
38
|
+
"isRequired": true,
|
|
39
|
+
"isSecret": true
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"registryType": "pypi",
|
|
45
|
+
"identifier": "hasdata-google-images-mcp",
|
|
46
|
+
"version": "1.0.0",
|
|
47
|
+
"runtimeHint": "uvx",
|
|
48
|
+
"transport": {
|
|
49
|
+
"type": "stdio"
|
|
50
|
+
},
|
|
51
|
+
"environmentVariables": [
|
|
52
|
+
{
|
|
53
|
+
"name": "HASDATA_API_KEY",
|
|
54
|
+
"description": "HasData API key, created at app.hasdata.com. 1,000 free credits every month, no card.",
|
|
55
|
+
"isRequired": true,
|
|
56
|
+
"isSecret": true
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Tool contract test.
|
|
2
|
+
//
|
|
3
|
+
// The README promises one tool with a specific name and parameter set. The upstream list can
|
|
4
|
+
// change without a single commit here, and the README would start lying silently. These checks
|
|
5
|
+
// catch that before a user does.
|
|
6
|
+
//
|
|
7
|
+
// One live call serves two checks. Listing tools accepts any non-empty key, so a contract check
|
|
8
|
+
// that only lists tools stays green with a revoked or mistyped key, and it would also stay green
|
|
9
|
+
// if the parser returned an empty page. The same response answers both. That call costs 10
|
|
10
|
+
// credits, which is the price of a canary that can fail for the right reason.
|
|
11
|
+
//
|
|
12
|
+
// Run: HASDATA_API_KEY=your_key_here npm test
|
|
13
|
+
|
|
14
|
+
import { test } from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
|
|
17
|
+
const ENDPOINT = 'https://mcp.hasdata.com/api/mcp?apis=google_images';
|
|
18
|
+
const KEY = process.env.HASDATA_API_KEY;
|
|
19
|
+
const TIMEOUT_MS = 30_000;
|
|
20
|
+
|
|
21
|
+
const TOOL = 'hasdata_google_images_images_getImageSearchResults';
|
|
22
|
+
const REQUIRED = ['q'];
|
|
23
|
+
|
|
24
|
+
// Targeting values the README lists by name.
|
|
25
|
+
const ENUMS = {
|
|
26
|
+
safe: ['active', 'off'],
|
|
27
|
+
deviceType: ['desktop', 'mobile', 'tablet'],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Parameters the README documents without listing every value.
|
|
31
|
+
const PARAMS = ['tbs', 'location', 'uule', 'domain', 'gl', 'hl', 'filter', 'ijn'];
|
|
32
|
+
|
|
33
|
+
// A streamable HTTP body arrives either as plain JSON or as server-sent events. One SSE event
|
|
34
|
+
// can span several data: lines, several events can share one response, and a server is free to
|
|
35
|
+
// send progress notifications before the answer. So collect every event and pick the message
|
|
36
|
+
// carrying our request id instead of trusting the first data: line.
|
|
37
|
+
function parseRpc(raw, id) {
|
|
38
|
+
const trimmed = raw.trim();
|
|
39
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) return JSON.parse(trimmed);
|
|
40
|
+
|
|
41
|
+
const messages = [];
|
|
42
|
+
for (const event of trimmed.split(/\r?\n\r?\n+/)) {
|
|
43
|
+
const data = event
|
|
44
|
+
.split(/\r?\n/)
|
|
45
|
+
.filter((l) => l.startsWith('data:'))
|
|
46
|
+
.map((l) => l.slice(5).replace(/^ /, ''))
|
|
47
|
+
.join('\n');
|
|
48
|
+
if (!data || data === '[DONE]') continue;
|
|
49
|
+
try {
|
|
50
|
+
messages.push(JSON.parse(data));
|
|
51
|
+
} catch {
|
|
52
|
+
// A keep-alive or a partial event is not our response.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
assert.ok(messages.length, `no JSON-RPC message in the response: ${raw.slice(0, 300)}`);
|
|
56
|
+
const match = messages.find((m) => m.id === id);
|
|
57
|
+
assert.ok(match, `no message with id ${id} in the response: ${raw.slice(0, 300)}`);
|
|
58
|
+
return match;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let nextId = 1;
|
|
62
|
+
|
|
63
|
+
async function rpc(method, params = {}) {
|
|
64
|
+
// The CI key sits on the free plan, where concurrency is 1. When several of
|
|
65
|
+
// these repos are pushed at once their contract runs collide, and HasData
|
|
66
|
+
// answers 429 with code concurrency_limit straight away rather than queueing.
|
|
67
|
+
// That is a plan limit, not a broken contract, so the call is retried before
|
|
68
|
+
// the test gives up. A 401 still fails on the first attempt.
|
|
69
|
+
for (let attempt = 1; ; attempt++) {
|
|
70
|
+
const id = nextId++;
|
|
71
|
+
const res = await fetch(ENDPOINT, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: {
|
|
74
|
+
'x-api-key': KEY,
|
|
75
|
+
'Content-Type': 'application/json',
|
|
76
|
+
// The server answers over streamable HTTP, so accept both a plain body and a stream.
|
|
77
|
+
Accept: 'application/json, text/event-stream',
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify({ jsonrpc: '2.0', id, method, params }),
|
|
80
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
81
|
+
});
|
|
82
|
+
assert.equal(res.status, 200, `${method} returned ${res.status}`);
|
|
83
|
+
const raw = await res.text();
|
|
84
|
+
if (raw.includes('concurrency_limit') && attempt < 5) {
|
|
85
|
+
await new Promise((r) => setTimeout(r, attempt * 4000));
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
return { raw, body: parseRpc(raw, id) };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// One network round trip for every test that needs the list.
|
|
93
|
+
let toolsPromise;
|
|
94
|
+
function listTools() {
|
|
95
|
+
toolsPromise ??= rpc('tools/list').then(({ body }) => {
|
|
96
|
+
assert.ok(body.result?.tools, 'the response carried no result.tools');
|
|
97
|
+
return body.result.tools;
|
|
98
|
+
});
|
|
99
|
+
return toolsPromise;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// One paid round trip, shared by the checks that need a real answer.
|
|
103
|
+
let searchPromise;
|
|
104
|
+
function liveSearch() {
|
|
105
|
+
searchPromise ??= rpc('tools/call', {
|
|
106
|
+
name: TOOL,
|
|
107
|
+
arguments: { q: 'austin skyline' },
|
|
108
|
+
});
|
|
109
|
+
return searchPromise;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const live = { skip: KEY ? false : 'HASDATA_API_KEY is not set, skipping the live checks' };
|
|
113
|
+
|
|
114
|
+
test('apis=google_images exposes the documented tool and nothing else', live, async () => {
|
|
115
|
+
const tools = await listTools();
|
|
116
|
+
const names = tools.map((t) => t.name).sort().join(', ');
|
|
117
|
+
assert.equal(tools.length, 1, `expected 1 tool, got ${tools.length}: ${names}`);
|
|
118
|
+
assert.equal(tools[0].name, TOOL, `the tool is now called ${tools[0].name}`);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('the tool still requires its search term and carries a description', live, async () => {
|
|
122
|
+
const [tool] = await listTools();
|
|
123
|
+
const required = tool.inputSchema?.required ?? [];
|
|
124
|
+
for (const param of REQUIRED) {
|
|
125
|
+
assert.ok(required.includes(param), `${TOOL} should require ${param}, declares: ${required.join(', ') || 'nothing'}`);
|
|
126
|
+
}
|
|
127
|
+
assert.ok((tool.description || '').trim().length > 20, `${TOOL} has an empty or near-empty description`);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('the targeting parameters the README documents are still in the schema', live, async () => {
|
|
131
|
+
const [tool] = await listTools();
|
|
132
|
+
const props = tool.inputSchema?.properties ?? {};
|
|
133
|
+
for (const param of PARAMS) {
|
|
134
|
+
assert.ok(props[param], `${TOOL} no longer accepts ${param}`);
|
|
135
|
+
}
|
|
136
|
+
for (const [param, values] of Object.entries(ENUMS)) {
|
|
137
|
+
const offered = props[param]?.enum ?? [];
|
|
138
|
+
for (const value of values) {
|
|
139
|
+
assert.ok(offered.includes(value), `${param} no longer accepts ${value}, offers: ${offered.join(', ') || 'no enum'}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('the key is accepted by HasData', live, async () => {
|
|
145
|
+
const { raw } = await liveSearch();
|
|
146
|
+
assert.ok(!raw.includes('401 Unauthorized'), 'HasData rejected the key');
|
|
147
|
+
assert.ok(!raw.includes('"isError":true'), `the tool call failed: ${raw.slice(0, 300)}`);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// A tools/list check passes while the parser returns an empty page. The fields below are what
|
|
151
|
+
// makes this tool worth calling over a plain SERP, so they are checked on a real answer rather
|
|
152
|
+
// than assumed from the schema.
|
|
153
|
+
test('a live search still returns images with their source page and full-resolution URL', live, async () => {
|
|
154
|
+
const { body } = await liveSearch();
|
|
155
|
+
const text = body.result?.content?.[0]?.text ?? '';
|
|
156
|
+
const payload = JSON.parse(text);
|
|
157
|
+
const results = payload.json?.imagesResults;
|
|
158
|
+
|
|
159
|
+
assert.ok(Array.isArray(results), `no imagesResults array in the response: ${text.slice(0, 300)}`);
|
|
160
|
+
assert.ok(results.length, 'imagesResults came back empty, so the page was not parsed');
|
|
161
|
+
|
|
162
|
+
for (const r of results) {
|
|
163
|
+
const shown = JSON.stringify(r).slice(0, 160);
|
|
164
|
+
assert.equal(typeof r.position, 'number', `a result carried no numeric position: ${shown}`);
|
|
165
|
+
assert.ok(r.link, `a result carried no source page link: ${shown}`);
|
|
166
|
+
assert.ok(r.original, `a result carried no original URL: ${shown}`);
|
|
167
|
+
assert.equal(typeof r.originalWidth, 'number', `a result carried no originalWidth: ${shown}`);
|
|
168
|
+
assert.equal(typeof r.originalHeight, 'number', `a result carried no originalHeight: ${shown}`);
|
|
169
|
+
}
|
|
170
|
+
});
|