Skip to content

Commit d5e7e2f

Browse files
author
sergey-ermakovich
committed
Add npm and PyPI wrapper packages, packages metadata, Glama badge
1 parent 3fb8ad3 commit d5e7e2f

7 files changed

Lines changed: 175 additions & 5 deletions

File tree

.github/workflows/publish.yml

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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-search-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-search-mcp, workflow publish.yml
12+
#
13+
# The MCP registry entry (com.hasdata/google-search) 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+
- uses: actions/setup-node@v4
37+
with:
38+
node-version: '22'
39+
registry-url: 'https://registry.npmjs.org'
40+
- name: Set version from the tag
41+
run: npm version "${GITHUB_REF_NAME#v}" --no-git-tag-version --allow-same-version
42+
- name: Publish to npm (OIDC, no token)
43+
run: npm publish --access public
44+
45+
pypi:
46+
runs-on: ubuntu-latest
47+
steps:
48+
- uses: actions/checkout@v4
49+
- uses: actions/setup-python@v5
50+
with:
51+
python-version: '3.12'
52+
- name: Set version from the tag
53+
run: |
54+
python - "${GITHUB_REF_NAME#v}" <<'PY'
55+
import re, sys
56+
v = sys.argv[1]
57+
p = "pyproject.toml"
58+
t = open(p, encoding="utf-8").read()
59+
t = re.sub(r'(?m)^version = ".*"$', f'version = "{v}"', t, count=1)
60+
open(p, "w", encoding="utf-8", newline="\n").write(t)
61+
PY
62+
- name: Build the wheel and sdist
63+
run: pipx run build
64+
- name: Publish to PyPI (OIDC, no token)
65+
uses: pypa/gh-action-pypi-publish@release/v1

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
# Google Search MCP Server (SERP)
22

3+
<!-- mcp-name: com.hasdata/google-search -->
4+
35
A hosted Model Context Protocol (MCP) server that gives Claude, Cursor, Windsurf and any other MCP client eight read-only Google Search tools. Pull the live SERP with its AI Overview and People Also Ask, run a Google AI Mode query, and read news, shopping, product detail and short-video results, all as structured JSON, with no Google Cloud project and no search-engine setup.
46

57
```
68
https://mcp.hasdata.com/api/mcp?apis=google_serp
79
```
810

11+
[![Glama score](https://glama.ai/mcp/servers/HasData/google-search-mcp/badges/score.svg)](https://glama.ai/mcp/servers/HasData/google-search-mcp)
912
[![tool contract](https://github.com/HasData/google-search-mcp/actions/workflows/contract.yml/badge.svg)](https://github.com/HasData/google-search-mcp/actions/workflows/contract.yml)
1013
[![MCP](https://img.shields.io/badge/MCP-remote%20%7C%20streamable%20HTTP-6366f1?style=flat-square)](https://modelcontextprotocol.io)
1114
[![Tools](https://img.shields.io/badge/tools-8-10b981?style=flat-square)](#tools)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""Thin launcher for HasData's hosted Google Search (SERP) 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_serp"
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)

index.mjs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/env node
2+
// Thin launcher: connects an MCP client to HasData's hosted Google Search 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_serp';
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+
});

package.json

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
{
22
"name": "@hasdata/google-search-mcp",
3+
"mcpName": "com.hasdata/google-search",
34
"version": "1.0.0",
4-
"private": true,
5-
"description": "Configuration and documentation for the HasData Google Search (SERP) MCP server",
5+
"description": "MCP server for Google Search (SERP) through HasData's hosted API: organic results, AI Overview, People Also Ask, AI Mode, news, shopping, product and short videos. No Google Cloud project.",
6+
"type": "module",
7+
"bin": { "hasdata-google-search-mcp": "index.mjs" },
8+
"files": ["index.mjs", "README.md", "LICENSE"],
9+
"scripts": { "test": "node --test" },
10+
"dependencies": { "mcp-remote": "^0.1.43" },
11+
"engines": { "node": ">=18" },
612
"license": "MIT",
713
"repository": { "type": "git", "url": "https://github.com/HasData/google-search-mcp.git" },
814
"homepage": "https://hasdata.com/apis/google-serp-api",
9-
"scripts": { "test": "node --test" },
10-
"engines": { "node": ">=22" }
15+
"keywords": ["mcp", "google-search", "google-serp", "serp", "google-search-mcp", "serp-mcp", "model-context-protocol", "hasdata", "scraper", "ai-overview"]
1116
}

pyproject.toml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[project]
2+
name = "hasdata-google-search-mcp"
3+
version = "1.0.0"
4+
description = "MCP server for Google Search (SERP) through HasData's hosted API. No Google Cloud project, no search-engine setup."
5+
readme = "README.md"
6+
requires-python = ">=3.10"
7+
license = { text = "MIT" }
8+
keywords = ["mcp", "google-search", "google-serp", "serp", "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-serp-api"
14+
Repository = "https://github.com/HasData/google-search-mcp"
15+
16+
[project.scripts]
17+
hasdata-google-search-mcp = "hasdata_google_search_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_search_mcp"]

server.json

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"name": "com.hasdata/google-search",
44
"title": "HasData Google Search (SERP)",
55
"description": "Google SERP as JSON: organic, AI Overview, People Also Ask, AI Mode, news, shopping. No Cloud setup.",
6-
"version": "1.0.0",
6+
"version": "1.0.1",
77
"websiteUrl": "https://hasdata.com/apis/google-serp-api",
88
"repository": { "url": "https://github.com/HasData/google-search-mcp", "source": "github" },
99
"remotes": [
@@ -14,5 +14,26 @@
1414
{ "name": "x-api-key", "description": "HasData API key, created at app.hasdata.com. The free trial needs no card.", "isRequired": true, "isSecret": true }
1515
]
1616
}
17+
],
18+
"packages": [
19+
{
20+
"registryType": "npm",
21+
"identifier": "@hasdata/google-search-mcp",
22+
"version": "1.0.0",
23+
"transport": { "type": "stdio" },
24+
"environmentVariables": [
25+
{ "name": "HASDATA_API_KEY", "description": "HasData API key, created at app.hasdata.com. The free trial needs no card.", "isRequired": true, "isSecret": true }
26+
]
27+
},
28+
{
29+
"registryType": "pypi",
30+
"identifier": "hasdata-google-search-mcp",
31+
"version": "1.0.0",
32+
"runtimeHint": "uvx",
33+
"transport": { "type": "stdio" },
34+
"environmentVariables": [
35+
{ "name": "HASDATA_API_KEY", "description": "HasData API key, created at app.hasdata.com. The free trial needs no card.", "isRequired": true, "isSecret": true }
36+
]
37+
}
1738
]
1839
}

0 commit comments

Comments
 (0)