Repository navigation
Releases
v0.9.46
OpenAI plugin review release. Search, research, and Google scraping return real destination URLs instead of Google's relative click-tracking links; tool descriptions drop comparative wording; and the hosted endpoint serves ChatGPT/OpenAI clients a reviewed 17-tool surface on the same URL.
Changed
- Tool descriptions no longer compare options with evaluative wording (OpenAI plugin review flagged
novada_search's description as comparing/preferring; applied to every client, not only OpenAI). Removed superlatives, magnitude claims, and "cheaper/faster than X" phrasing — e.g.novada_search's "(faster, more thorough)" and "fastest/most reliable … slower fallbacks",novada_extract's "15-100x faster on static sites",novada_crawl's "novada_map — much faster",novada_research's "novada_search — faster/cheaper", the SERP scrapers' "cheaper than a raw-SERP scrape" / "raw SERP dump",novada_proxy's "strongest"/"fastest/cheapest", and "slower" in threerender/max_depthparameter descriptions. Routing guidance stays ("use novada_research for…"), and neutral facts about our own options stay ("adds rendering time").novada_scrape_perplexityno longer mentions the broken chatgpt.com scraper. Tier A tool-selection eval unchanged (60% exact / 64% acceptable, identical per-task picks). - Cross-references to sibling tools are marked up for per-surface rendering (
{{sibling:…}},_core/description_surface.ts): mentions ofnovada_scrape,novada_map, andnovada_monitorin other tools' descriptions are dropped on a surface that does not list those tools. No visible change on npm or the default hosted surface.
Added (hosted server — mcp.novada.com, not part of the npm package)
- Per-client tool surface for OpenAI (
hosted-server/vercel/api/_surface.ts). The samehttps://mcp.novada.com/mcpURL now serves OAuth clients whose registered redirect URIs are onchatgpt.comoropenai.com(exact host or subdomain, https only) a 17-tool surface: search, extract, crawl, research, the Google/Bing/DuckDuckGo/Yandex/Amazon/Walmart/Perplexity/YouTube/GitHub scrapers, proxy, ai_monitor, account, proxy_account_list. On that surface the platform scrapers are annotatedreadOnlyHint: false(each call queues an upstream scrape job), descriptions drop mentions of tools it does not list, hidden backward-compat aliases are not callable, the one-time onboarding notice (it advertises free credits) is not appended, error guidance does not point at the local npm server, andinitializereturns routinginstructions. The surface is resolved per request from the access token'sclient_id(cached per process; client records are immutable), so already-connected clients switch without reconnecting. API-key callers and every other OAuth client keep the unchanged 29-tool surface.config/surfaces.jsongains anopenaientry;scripts/check-hosted-drift.mjsgate (d) requires it to equal the code list and stay a subset ofhosted.
Changed (hosted server)
POST /registeracceptstoken_endpoint_auth_methodclient_secret_post/client_secret_basicand registers the client as public ("none"in the response, no secret issued) instead of returning 400;/tokentakesclient_idfrom an HTTP Basic header when the body omits it./mcpCORS preflight allows themcp-protocol-versionheader.- 401 responses no longer mention "$10 free credits" (they still point to https://novada.com for a key): these are pre-auth responses, so the caller's client is unknown and the wording cannot be scoped to non-OpenAI clients.
- The hosted wall-clock timeout's resume hint names the tool to re-call (the platform scraper itself), instead of always
novada_scrape.
Fixed
novada_search,novada_research, andnovada_scrape_googleno longer return Google click-tracking "goto" redirects as a relative path (e.g.link: "/goto?url=CAES..."). Reproduced live against productionscraper.novada.com(2026-11-04): a 5-resultnovada_searchcall returned 9/goto?url=occurrences, andnovada_research's extraction step — which callsnovadaExtract({url: source.url})on the raw link — failed for every such source (sources_extracted: 0 full + N snippet-only), since a relative path is not a fetchable URL. A new shared resolver (src/utils/google_redirect.ts) resolves every Google-wrapped link to its real destination. When the upstream result carries aredirect_link(https://www.google.com/url?sa=t&...&url=<token>&ved=...), its full query is replayed verbatim against Google's/urlendpoint and the destination is read from the page'svar redirectUrl/<meta refresh>— one request per result. Live-measured 2026-11-05: for fresh tokens, Google's/goto?url=<token>answered with its "verifying your request" interstitial instead of a 302 for 4/4 tokens, while the/urlreplay resolved 4/4 from both a residential IP and the production EC2 host; end-to-end, 3 spaced searches resolved 12 of 13 wrapped results (the same queries before this change: 6 of 14). The/goto?url=<token>302 remains the second path, and when it returns the interstitial the resolver follows its embedded continuation token exactly once (the token is JS-escaped asurl\x3d...in the page). A bare/url?url=<token>request (without the rest of the query) is never sent — it only ever returns a generic notice page. A result that still cannot be resolved is KEPT with its title and snippet but no link (url_unavailable: true) — never a relative path; when every result in a batch is unresolvable the response says so explicitly (url_resolution_warning). Decoding the opaque token locally (no network call) was investigated and rejected — it base64url-decodes to an encrypted protobuf blob with no recoverable URL. Wired intonovada_search/novada_research(search.ts'sparseAndResolveScraperSearchResults, the one choke point both share) and intonovada_scrape_google/novada_scrape(platform="google.com")(the genericscrape.tsengine, a separate raw-record code path). Unresolved links are counted (goto_links_unresolvedin json, an Agent Hints line in markdown/csv/html/toon). Every outbound request targets hardcodedhttps://www.google.com/{goto,url}URLs; a token is only taken from a Google redirect host, so an upstream link on any other host is never fetched. Zero network-call / zero-diff for every record whose link is already a plain absolute URL — i.e. every other platform's scrape output, and most Google results too.novada_scrape_google(andnovada_scrape(platform="google.com")) no longer leak raw base64 favicon/image data injsonandmarkdownoutput. A Google organic result'sfaviconfield (and the nestedabout_this_result.source.icon) is adata:image/png;base64,...blob, live-measured at ~19KB EACH —csv/excel/htmlalready dropped these columns viacurateTabularRecords;jsonandmarkdownrendered them raw. A new recursivestripBase64Blobs()(reusing the sameisBase64Blobdetector) replaces any base64-blob value with a short[base64 image data stripped]marker in those two formats only;csv/excel/html's existing column-drop behavior is unchanged.novada_search'stime_range/start_date/end_dateparams now actually filter Google and DuckDuckGo results. Live-verified (2026-11-04) that the previous implementation sent a literaltime_range=weekform field to the upstreamgoogle_searchscraper, which silently ignored it — atime_range="week"query returned results dated from 2025 and 2026 months out, not the last 7 days. The google.com engine now maps to Google's owntbs=qdr:{d|w|m|y}(relative) ortbs=cdr:1,cd_min:M/D/YYYY,cd_max:M/D/YYYY(custom range, whenstart_date/end_dateis given — wins overtime_range); both shapes were live-verified against known date windows. The duckduckgo.com engine now maps todf={d|w|m|y}(the catalog's own declared enum for this op). The yandex.com engine's equivalent (within, enum["0","77","1","2"]) is NOT self-evidently a day/week/month/year encoding and was left unmapped (same pre-existing, documented no-op) rather than guess at an unverified code — seereports/search-goto-links-2026-11-04.mdfor the open follow-up.
v0.9.45
Credential-safety and pricing-truth release. Proxy dial credentials are masked everywhere by default and can be revealed only on the local server with an explicit user confirmation; the hosted endpoint drops the obsolete monthly free-call cap and serves the read-only static-IP functions.
Added
novada_static_ip_mgmtgains areveal_credentialsaction, local MCP server only (owner decision 2026-11-01, option A; final scope per a same-day follow-up decision below): static-ISP dial credentials stay masked everywhere by default, but a user who explicitly asks (e.g. to configure their own static IPs in AdsPower or another proxy-aware tool) can get the REALaccount/passwordpair through this read-only action — gated behind the same two-stepapproval_tokenflow asopen/renew(evaluateApprovalGate,../utils/approval.ts), or a single MCP-elicitation-confirmed call on clients that support it (see the Security entry below). Filters are identical tolist(status/region/key_word/page/limit/is_auto_renew);limitis additionally capped at 50 (the response says so when it clamps a higher requested value). The preview call (noapproval_token) shows maskedip/port/region/expire_timerows only — no credentials — plus a count and a warning that the real credentials may be retained in the AI client's own chat history. The execute call (with the matchingapproval_token) returns{ip, port, region, expire_time, username, password}per row plus two ready-to-paste formats,dial_string(ip:port:username:password) andproxy_url(http://username:password@ip:port), built only from fields the API actually returned. Not available on the hosted endpoint — see below.
Changed
novada_static_ip_mgmt's plain read-only actions are now served on the hosted endpoint (mcp.novada.com):list/exportare dispatchable there like any other tool (hosted tool count 28 → 29). Its two billable WRITE actions,openandrenew, are still refused on hosted — and so, per a 2026-11-01 FOLLOW-UP, FINAL owner decision, isreveal_credentials: real dial credentials need a human-in-the-loop confirmation (MCP elicitation) that the stateless hosted endpoint cannot perform (see the Security entry below), and falling back to an agent-carriedapproval_tokenalone there would be the exact hole elicitation exists to close locally, so it is refused outright rather than served with a weaker guarantee. All three actions are refused at the ACTION level, inside the tool itself (assertActionAvailableOnRuntime(), thrown as aNovadaErrorbefore either the approval gate, elicitation, or any upstream call runs), not via whole-tool hiding.novada_static_ip_mgmtwas removed fromHOSTED_HIDDEN(hosted-server/vercel/api/mcp.ts) and its mirror (novada_discover'sHOSTED_HIDDEN_TOOLS,npm-package/src/tools/discover.ts).
Changed (hosted server — mcp.novada.com, not part of the npm package)
- The monthly free-call gateway cap is removed entirely (owner decision, 2026-10-29).
mcp.novada.com's "1,000 free calls per token per month" limit was a one-time new-user promotion that has ended, not a recurring allowance — every call is now billed to the caller's own Novada account, same as calling the REST API directly; new accounts still get $10 free credits. Removed: the per-token monthly KV quota counter (decrementQuota/refundQuota/monthKey), the paid-tier plan resolution and over-cap exemption (enforceGatewayCap,resolvePlan,shouldAllowOverCap,classifyPlanFromUsageRecord,fetchAggregateBalance/parseBalanceValue, the approval-gate preview-charge exemption), and theFREE_PLAN_MONTHLY_QUOTAenv var (no longer read).CAP_EXEMPT_TOOLS/ACCOUNT_TOOL_NAMESsurvive unchanged and now drive only the gateway status footer. Auth, rate limiting (60 req/min/IP), OAuth, and token caching are unchanged. - Gateway status footer collapses from three variants to two. The old
"⚠ gateway: N/M free calls remaining this month · cost: unknown…"and"gateway: uncapped (paid account) · cost: unknown…"lines are gone. Billable tools now carry"gateway: usage is billed to your Novada account — balance and charges at dashboard.novada.com";CAP_EXEMPT_TOOLS(novada_setup,novada_discover,novada_account+ aliases) carry"gateway: no charge for this call". The over-cap rejection message ("## Free Gateway Cap Reached") and its_meta.quota_remainingsuccess-path field are both gone. - Telemetry rows keep their schema (no DB migration) —
charged/over_cap_allowed/quota_remaining/planare now alwaysnull("no cap applies") instead of a free/pro classification;validateTokenno longer returns a meaninglessquota_remaining. The HQ push payload shape is unchanged (buildHqPayloadalready defaulted these to0/""for anullrow). - Updated the gateway's nightly contract canary (
contract-test.py):COST_VISIBILITYnow checks the two-variant footer contract;BILLING_TRUTH(which guarded a funded key against the now-removed cap) is deleted. - Docs/landing pages that claimed a free monthly call count (
hosted-server/docs/*,hosted-server/landing/*,hosted-server/vercel/README.md,hosted-server/smithery.yaml) now describe usage-based billing with a $10 new-account credit instead.
Security
- Static ISP IP dial credentials (account/password) are now masked in
novada_static_ip_mgmt(list/export/open/renew) and innovada_plan_balance_all/novada_account(section="plans"/"summary")'s per-IPrawfield — both hit the same/v1/static_house/listendpoint, which returns a per-IP dial username/password pair in cleartext. A new shared helper,maskCredentials()(_core/developer_api.ts), recursively masks a table-driven set of credential-bearing field names (password/passwd/pwd/pass/secret/token/api_key/apikey →***; account/username/user_name →<PROXY_USER>, matchingnovada_proxy's existing convention) without touching IPs, ports, regions, statuses, or expiry dates.sanitizeServerMsg(_core/errors.ts) gained a matching rule so a credential echoed back in an UPSTREAM ERROR message is masked too, not just the success path.novada_capture_apikey's own existing****<last4>masking is unchanged (out of scope — already correct for both itsgetandresetactions). access_tokenadded to the masked fields, plus a drift guard: a test reads the local developer-API/scraper-API reference mirror and fails if any credential-looking field name there is neither masked nor explicitly listed as safe (with a reason), so a future field such asdial_passwordcannot slip past the exact-match table unnoticed.evaluateApprovalGate()'s signed payload now binds the caller's own tool name (../utils/approval.ts, 2026-11-01 follow-up to an independent security review ofreveal_credentials): a new requiredtoolNameargument (e.g."novada_static_ip_mgmt") is folded into the HMAC-signed canonical payload as{ ...params, __tool: toolName }for all four gated tools (proxy_account_create,ip_whitelistadd/del/remark,capture_apikeyreset,static_ip_mgmtopen/renew/reveal_credentials). Previously cross-tool replay was prevented only because each tool's gated params happened to differ in shape — a token minted for one tool could, in principle, verify unmodified against a different tool whose params object reduced to the same bytes (most exposed:proxy_account_create, which has noactiondiscriminator field at all). A table-driven test now cross-checks every real gated call site pairwise.novada_static_ip_mgmt'sreveal_credentialsaction now uses real MCP elicitation as the human confirmation on the local stdio server, when the connected MCP client advertises elicitation support (clientCapabilities.elicitation.form, negotiated atinitialize). Previously the only confirmation mechanism was a two-stepapproval_tokenround-trip that passes THROUGH the agent — a prompt-injected or careless agent could simply make both calls itself with no human ever seeing the request. With elicitation: a single tool call fetches the filtered IP count, sends the client a human-facing confirmation (count + a warning that credentials will appear in the conversation and may persist in the client's own chat history) viaserver.elicitInput, and only an explicitacceptreveals the realusername/password;decline/cancel/an elicitation-transport failure all return a non-throwingstatus:"declined"response with no credentials anywhere in it.approval_tokenis not read at all on this path — a pre-minted or replayed token has zero effect and cannot bypass the human-in-the-loop step. Local clients without elicitation support keep the unchangedapproval_tokenfallback. The hosted endpoint, always —mcp.novada.combuilds a freshServer+StreamableHTTPServerTransportper stateless HTTP request and closes it again before any client reply could arrive, so a server-initiated request needing a later reply cannot complete there — cannot perform elicitation at all; per a same-day FOLLOW-UP, FINAL owner decision, falling back to the agent-carriedapproval_tokenalone on hosted was judged NOT an acceptable substitute (it would reopen the exact hole elicitation exists to close), soreveal_credentialsis refused outright on hosted instead (see the Changed entry above) rather than left on that fallback. The capability check andelicitInputcall live only in the stdio entry point (index.ts) and are injected into the tool as a plain callback (ElicitConfirmFn) —static_ip_mgmt.tshas no MCP-SDK/transport dependency and no forked logic between stdio and hosted.
Fixed
novada_static_ip_mgmt'slist/exportdefault tostatus="1"(in-use) when the caller omitsstatus, instead of returning every IP the account has ever owned (including long-expired/released ones). A newsummaryfield re...
v0.9.44
Error-honesty and listing release. Scraper HTTP failures now come back as structured errors on both the npm package and mcp.novada.com, and the package/registry listing carries the new one-line description.
Changed (npm package)
- New package description and MCP Registry listing.
package.jsonandserver.jsonnow read "One MCP server for all web data. Scrape, search, crawl, and AI research in one install.";server.jsonaddstitle("Novada MCP") andwebsiteUrl. The registry description is generated byscripts/gen-server-json.mjs, so the change lives there.
Fixed
novada_scrapeHTTP errors (429/5xx) from the submit and result-retrieval steps are now structured, honest errors instead of a plain, unclassified string.scrape.ts's submit/poll catch blocks now classify an AxiosError with a real HTTP response through the sharedclassifyError()/classifyScraperHttpError()taxonomy (429 →RATE_LIMITEDwith retry guidance; 402/11006 →PRODUCT_UNAVAILABLE; 5xx →API_DOWN; 401 →INVALID_API_KEY; 403 → transientAPI_DOWNat both steps — previously only at submit, so a result-step 403 was reported as a permanent unknown error) at the throw site, with resume-by-task_idguidance when a task already exists. Root cause: the hosted gateway (hosted-server/vercel/api/mcp.ts) never calledclassifyError()on a thrown plainError— only the npm-package's local stdio dispatch did — so a 429 from the result-retrieval step reached hosted callers as a bare"Tool error: Failed to retrieve scraper results: Request failed with status code 429"with no code, failure class, oragent_instruction.mcp.ts's generic error fallback now routes through the SAMEclassifyError()the stdio dispatch already uses, fixing this class of gap for every tool, not justnovada_scrape.- Nightly canary's
NO_LYING_ZERO(invariant 3) upstream-signal recognizer now includes 429/rate-limit (hosted-server/scripts/contract/contract-test.py) — it already promised "Upstream flakiness (timeout, backend error, rate-limit) → SKIP" in its own docstring but never implemented the rate-limit case. Covered by two new--self-testscenarios ((h)/(i), a mirror pair).
Fixed (hosted deploy tooling — not part of the npm package)
deploy-hosted.shrecords the real rollback target. It used to print the deployment it had just shipped as the "rollback target"; it now reads the deployment servingmcp.novada.combefore deploying. Its live-version probe now sends theAcceptheader Streamable HTTP requires (it previously always printedUNKNOWN).
v0.9.43
Documentation-truth and hygiene release after an end-to-end product evaluation. Tool behaviour is unchanged except where a tool's own text or output framing was wrong.
Changed (npm package)
novada_proxysays what it returns. Its description, thenovada_discovercatalog line, the CLI help and thenovada-agentskill now state that the tool returns a proxy config template — username shown as<PROXY_USER>, password as***— and never prints credentials. (The output was already masked; the text claimed otherwise.)novada_setupnever names a tool the session cannot call. On the hosted surface it no longer listsnovada_browseror the Browser-WS add-on.novadaSetup()takes an optional visible-tool set as a third argument, the same mechanismnovada_discoveralready used.novada_discoverreports 9 hosted-hidden tools (includingnovada_browser), derived from one exported list (HOSTED_HIDDEN_TOOLS) that a test keeps equal to the hosted server'sHOSTED_HIDDENset.- Developer-API 401 errors point at
https://dashboard.novada.com/api-key/instead of an internal host that the redactor turned into[novada-internal-host]. - The shipped
novada-proxyskill refers tonovada_proxywithtype=…instead of six tool names that no longer exist. - Smaller tarball: sourcemaps are no longer published (
!build/**/*.map), 439 → 225 files. The deadbenchmarknpm script (no directory,tsxnever a dependency) is removed. - Removed
scripts/verify-c1-live.mjs(one-off probe with a hard-coded macOS path; tracked despite the repo's own.gitignorerule, unreferenced).
Added
- Official MCP Registry publishing via GitHub Actions OIDC. A manually triggered, secretless workflow validates and publishes
server.jsonunder the repository-ownedio.github.NovadaLabs/*namespace with onlycontents: readandid-token: writepermissions. - Root
LICENSE(MIT) so GitHub detects the license. - Tests: hosted-surface
novada_setupvisibility, hosted-hidden list parity, 401 message wording,novada_extract format=jsonoutput parses as JSON.
Fixed (hosted server — mcp.novada.com, not part of the npm package)
- JSON results are no longer corrupted by the
gateway:footer. When a tool result is JSON (format=json), the footer is returned as a second content block instead of being appended to the JSON text (api/_footer.ts, 7 tests). - Nightly canary no longer reports the canary account's own expired plan as a production failure. A truthful
PRODUCT_UNAVAILABLErefusal with ledger evidence is classifiedXFAIL: CANARY_ACCOUNT_PLAN(issue #18) inscripts/contract/contract-test.py; every other error still fails. vendor/package.jsonversion drift.deploy-hosted.shandsync-to-hosted.mjsnow keep the vendor shim's version equal to the npm package (it had reported 0.9.3 since June; the vendoredconfig.jsreads it at runtime). Vendor re-synced in this release.
Docs
- README rewritten (164 → 81 lines): outcome line, what the server is good at, example prompts, hosted quickstart, an honest comparison with Firecrawl, Tavily and Bright Data. Removed claims that did not match the code: the
confirm:truewrite-gate (writes use a two-step approval token), "6 categories", "4.9× more compact", and browser automation on the hosted surface.npm-package/README.mdlinks are absolute so they work on npmjs.com. docs/TOOLS.mdandARCHITECTURE.mdcorrected (14 working platforms / ~76 operations, hosted browser exclusion, approval-token wording);CLAUDE.mdlists the real top-level directories;HANDOFF.md/PROJECT_STATE.mdmarked historical;CONTRIBUTING.mdcarries the release checklist (the pre-monorepopromote-to-public.shis no longer referenced).
Full evaluation that drove this release: see PR #27.