detached 923866d

feat: add optional serply search template to mcp-bridge

Registers the Serply MCP server as a /mcp setup template, alongside the
existing grep, filesystem, context7, playwright and you entries.

Serply has no keyless profile, so this is the first template to write a
headers entry. It writes the ${SERPLY_API_KEY} variable reference rather
than a key, so mcp.json carries no secret; the bridge expands it at load
time and drops only this server, by name, when the variable is unset.

Covers verticals the existing templates do not: Google News, Google
Scholar with citation counts, jobs, maps, Bing, Amazon products, Reddit
and URL scraping.
commit
923866d2a356fdd9214e5303bb574dac76a500c2
author
serply · · 2026-09-15 22:40 UTC
parent
64117f3
signature
unsigned
3 files · +99 -0
M examples/extensions/mcp-bridge/README.md +56 -0
@@ -229,6 +229,15 @@ Standard MCP config — same as Claude Desktop, with zot-specific extensions:
229229 "you": {
230230 "transport": "streamable-http",
231231 "url": "https://api.you.com/mcp?profile=free"
232+ },
233+
234+ // Serply Google-backed search (streamable-http, keyed)
235+ "serply": {
236+ "transport": "streamable-http",
237+ "url": "https://api.serply.io/mcp",
238+ "headers": {
239+ "X-Api-Key": "${SERPLY_API_KEY}"
240+ }
232241 }
233242 }
234243 }
@@ -258,6 +267,53 @@ the template:
258267 The authenticated endpoint does not expose `you-finance` by default. Request
259268 it explicitly with the `?tools=` URL parameter or the `X-Allowed-Tools` header.
260269
270+### Serply template
271+
272+`/mcp setup add serply` registers the [Serply](https://serply.io) MCP server,
273+which returns Google-backed results. It has no keyless profile, so the template
274+is the first one that writes a `headers` entry:
275+
276+```jsonc
277+{
278+ "mcpServers": {
279+ "serply": {
280+ "transport": "streamable-http",
281+ "url": "https://api.serply.io/mcp",
282+ "headers": {
283+ "X-Api-Key": "${SERPLY_API_KEY}"
284+ }
285+ }
286+ }
287+}
288+```
289+
290+The template writes the variable reference, not the key, so `mcp.json` holds no
291+secret and stays safe to commit or sync. The bridge expands it from its own
292+environment at load time (see [Environment variables](#environment-variables));
293+with `SERPLY_API_KEY` unset, this server is disabled and reported by name while
294+every other server keeps working. Get a key at
295+[serply.io](https://serply.io); the API reference is at
296+[serply.io/docs](https://serply.io/docs).
297+
298+The server exposes 14 tools, so it covers several verticals rather than one
299+general search:
300+
301+| tool | returns |
302+|------|---------|
303+| `google_search` | Google organic results |
304+| `google_news_search` | Google News |
305+| `google_scholar_search` | Google Scholar, including citation counts |
306+| `google_jobs_search` | Google Jobs |
307+| `google_maps_search` | Google Maps places |
308+| `google_video_search` | Google video results |
309+| `bing_search` | Bing organic results |
310+| `amazon_product_search` | Amazon product listings |
311+| `scrape_url` | a URL's content through the proxy layer |
312+| `reddit_subreddit_posts`, `reddit_subreddit_about`, `reddit_user_posts`, `reddit_post_comments`, `reddit_post` | Reddit subreddits, users, posts, and comments |
313+
314+They reach the model as `mcp__serply__google_scholar_search` and so on, per
315+[Tool Naming](#tool-naming).
316+
261317 ## How It Works
262318
263319 ```
@@ -229,6 +229,15 @@ Standard MCP config — same as Claude Desktop, with zot-specific extensions:
229 "you": {229 "you": {
230 "transport": "streamable-http",230 "transport": "streamable-http",
231 "url": "https://api.you.com/mcp?profile=free"231 "url": "https://api.you.com/mcp?profile=free"
232+ },
233+
234+ // Serply Google-backed search (streamable-http, keyed)
235+ "serply": {
236+ "transport": "streamable-http",
237+ "url": "https://api.serply.io/mcp",
238+ "headers": {
239+ "X-Api-Key": "${SERPLY_API_KEY}"
240+ }
232 }241 }
233 }242 }
234 }243 }
@@ -258,6 +267,53 @@ the template:
258 The authenticated endpoint does not expose `you-finance` by default. Request267 The authenticated endpoint does not expose `you-finance` by default. Request
259 it explicitly with the `?tools=` URL parameter or the `X-Allowed-Tools` header.268 it explicitly with the `?tools=` URL parameter or the `X-Allowed-Tools` header.
260 269
270+### Serply template
271+
272+`/mcp setup add serply` registers the [Serply](https://serply.io) MCP server,
273+which returns Google-backed results. It has no keyless profile, so the template
274+is the first one that writes a `headers` entry:
275+
276+```jsonc
277+{
278+ "mcpServers": {
279+ "serply": {
280+ "transport": "streamable-http",
281+ "url": "https://api.serply.io/mcp",
282+ "headers": {
283+ "X-Api-Key": "${SERPLY_API_KEY}"
284+ }
285+ }
286+ }
287+}
288+```
289+
290+The template writes the variable reference, not the key, so `mcp.json` holds no
291+secret and stays safe to commit or sync. The bridge expands it from its own
292+environment at load time (see [Environment variables](#environment-variables));
293+with `SERPLY_API_KEY` unset, this server is disabled and reported by name while
294+every other server keeps working. Get a key at
295+[serply.io](https://serply.io); the API reference is at
296+[serply.io/docs](https://serply.io/docs).
297+
298+The server exposes 14 tools, so it covers several verticals rather than one
299+general search:
300+
301+| tool | returns |
302+|------|---------|
303+| `google_search` | Google organic results |
304+| `google_news_search` | Google News |
305+| `google_scholar_search` | Google Scholar, including citation counts |
306+| `google_jobs_search` | Google Jobs |
307+| `google_maps_search` | Google Maps places |
308+| `google_video_search` | Google video results |
309+| `bing_search` | Bing organic results |
310+| `amazon_product_search` | Amazon product listings |
311+| `scrape_url` | a URL's content through the proxy layer |
312+| `reddit_subreddit_posts`, `reddit_subreddit_about`, `reddit_user_posts`, `reddit_post_comments`, `reddit_post` | Reddit subreddits, users, posts, and comments |
313+
314+They reach the model as `mcp__serply__google_scholar_search` and so on, per
315+[Tool Naming](#tool-naming).
316+
261 ## How It Works317 ## How It Works
262 318
263 ```319 ```
M examples/extensions/mcp-bridge/setup.go +11 -0
@@ -72,6 +72,17 @@ func setupTemplates() map[string]serverTemplate {
7272 }
7373 },
7474 },
75+ "serply": {
76+ Name: "serply",
77+ Description: "Google-backed search via the Serply MCP server: web, news, and scholar results with citation counts, plus maps, jobs, video, Bing, Amazon products, Reddit, and URL scraping. Requires a SERPLY_API_KEY.",
78+ Config: func(cwd string) ServerConfig {
79+ return ServerConfig{
80+ Transport: "streamable-http",
81+ URL: "https://api.serply.io/mcp",
82+ Headers: map[string]string{"X-Api-Key": "${SERPLY_API_KEY}"},
83+ }
84+ },
85+ },
7586 }
7687 }
7788
@@ -72,6 +72,17 @@ func setupTemplates() map[string]serverTemplate {
72 }72 }
73 },73 },
74 },74 },
75+ "serply": {
76+ Name: "serply",
77+ Description: "Google-backed search via the Serply MCP server: web, news, and scholar results with citation counts, plus maps, jobs, video, Bing, Amazon products, Reddit, and URL scraping. Requires a SERPLY_API_KEY.",
78+ Config: func(cwd string) ServerConfig {
79+ return ServerConfig{
80+ Transport: "streamable-http",
81+ URL: "https://api.serply.io/mcp",
82+ Headers: map[string]string{"X-Api-Key": "${SERPLY_API_KEY}"},
83+ }
84+ },
85+ },
75 }86 }
76 }87 }
77 88
M examples/extensions/mcp-bridge/setup_test.go +32 -0
@@ -87,6 +87,38 @@ func TestHandleSetupAddYouGlobal(t *testing.T) {
8787 }
8888 }
8989
90+func TestHandleSetupAddSerplyGlobal(t *testing.T) {
91+ tmp := t.TempDir()
92+ t.Setenv("ZOT_HOME", tmp)
93+
94+ out, err := handleSetup([]string{"add", "serply"}, tmp)
95+ if err != nil {
96+ t.Fatalf("handleSetup add serply: %v", err)
97+ }
98+ if out == "" {
99+ t.Fatal("expected setup output")
100+ }
101+
102+ data, err := os.ReadFile(filepath.Join(tmp, "mcp.json"))
103+ if err != nil {
104+ t.Fatalf("read mcp.json: %v", err)
105+ }
106+ var cfg Config
107+ if err := json.Unmarshal(data, &cfg); err != nil {
108+ t.Fatalf("parse mcp.json: %v", err)
109+ }
110+ serply := cfg.MCPServers["serply"]
111+ if serply.Transport != "streamable-http" || serply.URL != "https://api.serply.io/mcp" {
112+ t.Fatalf("unexpected serply config: %+v", serply)
113+ }
114+ // The keyed template must reference the key by variable so the written
115+ // config carries no secret. expandServerEnv resolves it at load time, and
116+ // drops the server entirely when SERPLY_API_KEY is unset.
117+ if got := serply.Headers["X-Api-Key"]; got != "${SERPLY_API_KEY}" {
118+ t.Fatalf("api key must stay an unexpanded variable reference, got %q", got)
119+ }
120+}
121+
90122 func TestHandleSetupDuplicate(t *testing.T) {
91123 tmp := t.TempDir()
92124 t.Setenv("ZOT_HOME", tmp)
@@ -87,6 +87,38 @@ func TestHandleSetupAddYouGlobal(t *testing.T) {
87 }87 }
88 }88 }
89 89
90+func TestHandleSetupAddSerplyGlobal(t *testing.T) {
91+ tmp := t.TempDir()
92+ t.Setenv("ZOT_HOME", tmp)
93+
94+ out, err := handleSetup([]string{"add", "serply"}, tmp)
95+ if err != nil {
96+ t.Fatalf("handleSetup add serply: %v", err)
97+ }
98+ if out == "" {
99+ t.Fatal("expected setup output")
100+ }
101+
102+ data, err := os.ReadFile(filepath.Join(tmp, "mcp.json"))
103+ if err != nil {
104+ t.Fatalf("read mcp.json: %v", err)
105+ }
106+ var cfg Config
107+ if err := json.Unmarshal(data, &cfg); err != nil {
108+ t.Fatalf("parse mcp.json: %v", err)
109+ }
110+ serply := cfg.MCPServers["serply"]
111+ if serply.Transport != "streamable-http" || serply.URL != "https://api.serply.io/mcp" {
112+ t.Fatalf("unexpected serply config: %+v", serply)
113+ }
114+ // The keyed template must reference the key by variable so the written
115+ // config carries no secret. expandServerEnv resolves it at load time, and
116+ // drops the server entirely when SERPLY_API_KEY is unset.
117+ if got := serply.Headers["X-Api-Key"]; got != "${SERPLY_API_KEY}" {
118+ t.Fatalf("api key must stay an unexpanded variable reference, got %q", got)
119+ }
120+}
121+
90 func TestHandleSetupDuplicate(t *testing.T) {122 func TestHandleSetupDuplicate(t *testing.T) {
91 tmp := t.TempDir()123 tmp := t.TempDir()
92 t.Setenv("ZOT_HOME", tmp)124 t.Setenv("ZOT_HOME", tmp)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close