Skip to content

web search: Serply as an API key service - #107

Merged
1ay1 merged 2 commits into
agentty-org:masterfrom
googio:feat/serply-search
Oct 17, 2026
Merged

1ay1 merged 2 commits into
agentty-org:masterfrom
googio:feat/serply-search

Conversation

@googio

@googio googio commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Merge agentty-org/mcp-cpp#6 first. This branch moves third_party/mcp-cpp to 8663d3c, the commit in that PR. Until it is merged, that commit exists only on the fork (googio/mcp-cpp, branch feat/serply-search), so a fresh clone or a CI run can't fetch it. Once it is merged, this branch builds as is.

Settings > Web Search: Serply as an API key service

Disclosure: I work with Serply.

Serply returns Google results as JSON (docs). mcp-cpp#6 adds it to the search services catalogue as serply-api, next to serper-api, with Country and Freshness settings and a cap of 10 results per search. Country defaults to United States. That PR has the details and tests.

This PR is the agentty side:

  • third_party/mcp-cpp moves to the commit that adds the service.
  • serply-api is added to kServiceIds and kOptionalServiceIds, in the catalogue's menu order (after serper-api). tests/web_search_services_test.cpp checks that these lists match the catalogue.
  • docs/website/configuration.md lists the id, the AGENTTY_WEB_SEARCH_KEY_SERPLY_API variable, its cap of 10, that excluded sites go to it as -site:, and that its Country defaults to United States.
  • CHANGELOG.md has one entry under Unreleased.

Nothing changes for anyone who does not pick Serply in a slot. The defaults stay Brave Free, DuckDuckGo and Tavily Free.

Testing

  • mcp-cpp: the full mcp_tests suite passes with the new service (96 cases, 2269 assertions). The request URL it pins returns 200 from the live API.
  • agentty: I could not build the whole tree on this machine. It has GCC 13 only (maya needs C++26), and nghttp2 is not installed. So I checked what the registry test asserts directly: I printed the ids of the built catalogue, and they equal kServiceIds and kOptionalServiceIds without the leading none. The CI run after mcp-cpp#6 merges is the full check.

Moves third_party/mcp-cpp to the commit that adds serply-api to its
catalogue, lists the id where the registry and the docs list the
others, and notes it in the changelog.
@sail3r

sail3r commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Hi @googio,

Cheers for the PR and for the disclosure. The request and
reply code match Serply's docs, and the two rows (Country and Freshness)
are fine as they are. Two things need to change before this can be
reviewed for merge. Both are about what Serply does when nobody has
touched the settings, which is what most users will see.

Both changes go in mcp-cpp#6. #107 then moves the submodule to the new
commit and updates the wording in CHANGELOG.md and
docs/website/configuration.md.

1. Country should default to United States

Serply's Google Search reference says:

If you send neither gl nor X-Proxy-Location, the request is served
from whichever proxy region is available, and results can differ
between calls. Set one of them when you need stable or localized
results.

The Webhooks guide measured this: six identical queries three seconds
apart returned the same nine results five times, and the sixth returned
"a German result set sharing zero links with the previous five".

With this PR the Country row starts at "Service default", so gl is not
sent and a user's results can switch country from one search to the
next. Serply sits in between, so the results are not tied to the user's
own location either. Please make United States the default:

  • Give serply-api its own Country dial. Keep values[0] with id "",
    as include/mcp/tools/web_search.hpp requires of every list dial,
    label it "United States (default)", and drop the separate "US" entry
    so that two rows do not mean the same thing.
  • In req_serply, send gl=us when the Country dial is empty.
  • Test: a request with no dials contains &gl=us, and the existing NZ
    case still sends gl=nz.
  • Say "Country defaults to United States" in the CHANGELOG entry and in
    configuration.md.

One question: the Webhooks measurement pinned X-Proxy-Location, not
gl. The reference says either one is enough. Can you confirm that gl
alone keeps the results stable between calls?

2. Error replies: running out of credits

Most keyed services in the catalogue document what they return when the
account runs out of credits or quota:

Service Out of credits or quota Source
Tavily 432 (key or plan limit), 433 (PayGo limit) docs.tavily.com/documentation/api-reference/endpoint/search.md
Exa 402, "out of credits or a spending budget is exceeded" exa.ai/docs/exa-spec.yaml, POST /search
Firecrawl 402 at zero balance docs.firecrawl.dev/billing.md, "Running Out of Credits"
Perplexity 401, which also covers "an account which ran out of credits" docs.perplexity.ai/docs/resources/faq.md
Brave 429 when a limit window is exceeded, the monthly quota included; X-RateLimit-Remaining and -Reset report each window api-dashboard.search.brave.com/documentation/guides/rate-limiting/index.html.md

(Serper, your competitor, has no public API reference for this error either. That is a gap in the catalogue, not a model to follow. Both of you would populate the catalogue, having the same flaw in error handling.

Serply's docs cover the other error cases, and mcp-cpp's classify_http
already maps them correctly:

Serply reply Documented in mcp-cpp reports
400 Errors guide the service rejected the request
401, {"detail":"API key missing"} or {"detail":"Invalid API key"} Errors guide; both bodies checked live on 2026-10-17 API key missing, invalid or not permitted
403 Errors guide API key missing, invalid or not permitted
404, 422 Errors guide, Google Search reference the service rejected the request
429 Errors guide, with x-ratelimit-requests-* headers rate limited, try again later
500, 502 Errors guide (500); MCP page and Pagination guide (502) service error, try again
zero balance, or the 30-day free credits expired not documented depends on the status, see below

I looked for the zero-balance reply in these places and did not find it:

  • the Errors guide and the Google Search reference;
  • the Pagination, Webhooks and MCP pages, the pricing page and
    llms-full.txt;
  • the OpenAPI spec at api.serply.io/openapi.json and the
    serply-inc/openapi spec, whose GET /v1/search lists 200, 404, 422 and
    429;
  • serply-inc/mcp and serply-inc/hermes-web-serply. Neither client has a
    case for it.

Serply bills prepaid credits, and the 2,500 free credits expire after 30
days. That makes this the failure a Serply user is most likely to hit.
Unless the reply is a 402, agentty will report something untrue: a bad
key (401, 403), a rate limit (429) or a rejected request (400, 404, 422).

Please:

a. Tell us the status and detail text for a zero balance and for
expired free credits. Add them to Serply's Errors guide, and add 502
to its table while you are there.
b. Map that reply in classify_http to WebSearchFailure::QuotaExhausted.
A 402 needs no new code. If Serply reuses 401, 403 or 429, match the
detail text, the way Brave's SUBSCRIPTION_TOKEN_INVALID is matched
on a 422.
c. Add the real reply body to the "web search: failures are told apart"
test, next to the Exa 402 case.
d. Optional, on Serply's side: returning 402 Payment Required, as Exa and
Firecrawl do, would reduce (b) to a test.

Two smaller questions in the same area:

  • Envelope. The Errors guide and the live API use {"detail": "..."},
    but client.py in serply-inc/mcp reads error.message and
    error.code. Which one is the contract? A matcher written for (b) has
    to read the right one.
  • Empty 200. The Pagination guide says an empty results array also
    appears during transient upstream failures. mcp-cpp reports that as
    "no results" and moves on to the next service. Is there a header or a
    field that tells the two apart? If there is not, please say so in the
    PR so that it is on record.

Moves third_party/mcp-cpp to 8663d3c, where serply-api sends gl=us when
no country is chosen, and says so in the CHANGELOG entry and
configuration.md.
@googio
googio force-pushed the feat/serply-search branch from 368807c to 2c1257d Compare October 17, 2026 22:17
@googio

googio commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the careful review. Both changes are in: mcp-cpp#6 is now at 8663d3c and this PR moves the submodule to it (2c1257d).

1. Country defaults to United States

  • serply-api has its own Country dial: values[0] keeps id "" with the label "United States (default)", and the separate US row is gone.
  • req_serply sends gl=us when the dial is empty.
  • Tests: a request with no dials contains &gl=us, the NZ case still sends gl=nz, and the dial has no US row.
  • CHANGELOG.md and configuration.md say Country defaults to United States.
  • Full mcp_tests: 96 cases, 2269 assertions, no warnings.

On gl alone: I measured it today, and a plain repeat test turned out not to be a fair test. Repeating the same query six times three seconds apart gave identical results with gl=us, with X-Proxy-Location: US and with neither. But calls 2 to 6 came back in about 0.2s, so they were cache hits. The no-geo series was stable too, just Irish (met.ie). So I ran six different uncached queries (weather tomorrow plus a nonce) side by side:

  • gl=us: 6 of 6 returned US results (forecast.weather.gov, accuweather.com, weather.com, US local stations).
  • neither: UK (metoffice.gov.uk, bbc.com) and Australian (bom.gov.au) results, and one reply with no usable links at all.

So gl alone keeps the country stable across calls. One caveat: for a location-sensitive query like weather, the US local results still varied by city (St. Louis, Baltimore and Seattle stations), since gl sets the country, not the city. X-Proxy-Location also pins the country, but it only takes a fixed list of regions and silently falls back for others, while gl takes any country code, so gl is the better fit for the dial.

2. Out of credits

The zero-balance reply is 402 with {"detail":"Insufficient prepaid credits"}. I read it from the API gateway's source. I had no zero-balance key to record it live, so it is neither [D] nor [L] yet. Until it is documented, treat it as source-level.

Expired free credits end in the same reply. Expiry removes what is left of the 30-day grant from the balance, so once the balance reaches zero the next call gets the 402. Purchased credits do not expire.

  • (b) classify_http already maps 402 to QuotaExhausted, so no code was needed.
  • (c) The body is pinned in "web search: failures are told apart", next to Exa's 402.
  • (a) and (d) are Serply's to change, not mine: adding the 402 and 502 rows to the Errors guide. I have passed both to the Serply team. (d) is already the case, since the reply is a 402.

One more case you may meet: an account that exists but is not activated yet (email not confirmed) gets 403 {"detail":"API access inactive"}. That maps to "API key missing, invalid or not permitted", which is accurate.

Smaller questions

Envelope. The contract for the REST API is {"detail": "..."}. Every error the gateway raises has that shape: the 401s you checked, 402, 403. serply-inc/mcp reads error.message and falls back to the raw body text when that is absent, so in practice it shows the detail JSON. That is a quirk in that client, not a second envelope. I have reported it.

Empty 200. There is no header or field that tells a transient empty page from a genuine "no results". I checked the reply headers, and the only Serply-specific one is a credits-remaining count on paid calls. The documented advice is to retry once or twice. Reporting it as no results and moving to the next slot is a reasonable reading. This is now stated in mcp-cpp#6's description so it is on record.

One more change, disclosed. During the measurement, one no-geo reply carried opaque tokens in link instead of URLs. This is a rare Serply-side issue. The parser now drops any row whose link is not http(s) before counting, with a test. Without that, those rows would have reached the model as hits with no usable URL.

@1ay1
1ay1 merged commit 6a74fa7 into agentty-org:master Oct 17, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants