octen-team/octen
Real-time web search, broad multi-angle research, news, and URL extraction through Octen's hosted MCP server, plus image and video search in invite-only Beta. Authorizes over OAuth on first use — nothing to install and no API key to paste.
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
Added
- Agent Plugins manifests (
plugin.json,mcp.json) at the repository root, so the plugin installs in Cursor and any other client that reads the open standard, and a.cursor-plugin/plugin.jsoncarrying the display fields a Cursor listing reads. scripts/sync-plugin-manifests.mjs, wired intonpm version, which writespackage.json's version into every manifest instead of leaving four copies to be edited by hand.
Changed
- Plugin keywords now name capabilities rather than the transport:
mcpand the duplicatesearchare gone, andbroad searchandfetchare listed alongsideextract. A saved search formcpno longer matches the plugin. - The listing description names the invite-only Beta tools the endpoint serves, and every manifest carries the same description and category.
[0.6.0] — 2026-09-29
Added
extracttakesmode—standard/advanced/auto. Omitted, it is omitted from the request too, so the API's own default (standard) applies; no client default is injected. The schema description carries the guidance for choosing: ordinary sites standard, known hard sites (anti-bot, SPAs, social, academic) advanced, mixed batches auto, and retry a failed or empty/skeletal standard result with advanced.advancedandautoget a wider client deadline (per-URLtimeout+ 120s instead of + 90s), under the same 180s cap.extracttakesinclude_links—{ scope?, max_links? }, passed through as given ({}uses the API defaults). The API answers an out-of-rangemax_linkswith a 400 rather than clamping it, so 1-1000 is enforced here with an error naming the field.- New response fields are rendered: each successful result shows
**Mode:**(itsresolved_mode, which can differ from the requested mode), and a### Linkssection listslinks[]with anchor text, external links marked. The meta line addsbilled: standard N, advanced Mfrommeta.usage.successful_by_mode, the authoritative billing count.
Fixed
extractnever rendered its meta line. The handler readdata.meta; the API returnsmetaat the top level, as it does on search. Usage counts, latency and the "failed and were not billed" warning were silently dropped. The comment insearch.tsthat described extract as nesting it was wrong too, and is corrected.
[0.5.2] — 2026-09-08
Fixed
- An empty
queryis refused by the schema.search,news_searchandbroad_searchdeclaredmaxLengthbut no minimum, soquery: ""passed validation and was caught only by the handler afterwards — a message from a different layer than the schema the caller read. They now declareminLength: 1, which this server's validator enforces. The handler's trim guard stays: it is what catches" ", which satisfiesminLength.
Documentation
-
timeoutis documented as the two different things it is. On search, news, broad, image and video search it is this server's HTTP deadline and is stripped from the request body; onextractit is the API's documented per-URL fetch budget and is sent. The internal comment claimed the first for all of them, which is how a published field could have been dropped without anyone noticing. The test suite now asserts both request bodies exactly. -
The tool roster is stated as deliberate. These six tools are the whole surface; embeddings, chat, Answer and Research are reachable through the SDK and CLI and are intentionally not exposed here.
-
Overlapping include/exclude domains. The API answers a domain listed in both with a validation error, while the published reference describes exclusion winning. Documented as the API's behaviour, with no client-side workaround: silently dropping or reordering a caller's filters would hide the disagreement rather than resolve it.
[0.5.0] — 2026-08-31
Added
-
Remote HTTP transport (preview) —
octen-mcp-httpruns the same server over stateless Streamable HTTP, for hosting behind a URL instead of spawning per client:- without an authorization server configured,
initializeandtools/listare unauthenticated and credentials are enforced attools/call, where a missing key fails with header guidance rather than an opaque handshake refusal. With one configured, an uncredentialed request is answered 401 +WWW-Authenticateinstead — see below; - both
x-api-key: <key>andAuthorization: Bearer <key>are accepted — some clients can only send the latter; - one server instance per request, key bound by closure. The environment's
OCTEN_API_KEYis deliberately not a fallback for HTTP callers: an unauthenticated request must fail, not silently ride the deployment's own credential. Guarded by tests including a deterministic staggered-body interleaving case, since the race window for shared-key-state bugs sits inside the body-read await where ordinary concurrent tests cannot land; GET /healthzliveness with no upstream round-trip.
The stdio entry is unchanged in behavior; both entries now assemble the same server from
src/server.ts, so the transports cannot drift apart. - without an authorization server configured,
-
OAuth 2.1 resource server for the HTTP transport — enabled only when both
OCTEN_OAUTH_AUTHORIZATION_SERVERandOCTEN_MCP_RESOURCEare set, so a self-hosted instance never advertises an authorization server it cannot honour. RS256 access tokens are verified against the AS's JWKS withnode:crypto(no new dependency), then exchanged for the grant's API key through an authenticated internal endpoint — the token itself never carries a key. RFC 9728 protected-resource metadata is served at both the bare and the resource-path-derived well-known, and any uncredentialedPOST /mcpanswers a 401 challenge, which is what makes a client start the flow at all.Token problems answer 401 +
WWW-Authenticateso clients auto-refresh; backend problems answer 503 so a client does not burn its refresh flow on our outage. Bare API keys never enter this path. -
Three ways to pass a key, tried in order:
x-api-key,Authorization: Bearer, and anoctenApiKeyquery parameter. The query form exists for clients that can be handed nothing but a URL, e.g. a hosted connector UI whose only input is the endpoint. It is not needed for Claude Desktop: that config spawnsmcp-remote, which does take--headerand--header-file(the latter keeps the key out of the process arguments). A header always wins, so nothing that can send one is downgraded. A key in a URL reaches proxy logs, browser history andRefererheaders — none of which this server controls — so the one thing it can do, it does: query values are redacted from its own logs, whole rather than truncated. -
POST /mcp/oauthand?loginforce the authorization challenge even when a credential is present. Without it there is no route from an already-configured API key to OAuth, because the key stops the challenge from ever being sent. (This is not the old?logingate returning: an uncredentialed request is still challenged everywhere.) -
?tools=a,bnarrows a connection's tool roster, enforced ontools/callas well astools/list— filtering only the listing would leave every tool callable and the filter decorative. Clients load every advertised tool's full schema into the model's context, so this is a context saving rather than a capability switch. An unknown name, or a selection that leaves nothing, is a 400 naming the bad entry and listing the real ones: silently narrowing produces a connection that looks healthy and is missing a tool, which sends whoever debugs it to look at the tool instead of at the spelling in their URL. Beta tools that the Beta switch has turned off read as names that do not exist, so a selection can never reach past that switch. -
Tool arguments are checked against the schema each tool publishes. The MCP SDK does not enforce
inputSchema, so every constraint in it was advertising: measured,count: 999999againstmaximum: 100, a 5000-characterqueryagainstmaxLength: 500, and a 5000-entry array againstmaxItems: 1000were each relayed to the upstream API intact. An agent that read the schema and respected it gained nothing over one that ignored it, and when it did get something wrong the answer came back as an opaque upstream 400 rather than a sentence naming the field.The validator reads the same schema object that is sent to clients, never a copy of its rules — a duplicated rule set drifts the first time someone edits a schema, and drifts silently. It implements exactly the vocabulary those schemas use, and a test asserts no schema ever declares a keyword it would skip, since an ignored keyword turns a published constraint back into advertising with nobody the wiser.
Parameters outside a tool's
propertiesare refused rather than relayed: the properties list is the published interface, and forwarding anything else made this server a pass-through for arbitrary JSON. Note for existing callers:news_searchpublishes notopic(it is fixed to news), so passing one is now an error instead of being silently overridden. -
The HTTP body cap now defaults to 6 MiB, so
image_search'simage_dataworks without configuration. A default below the largest argument a tool accepts means the tool is advertised and unusable over that transport, and the client sees a413with no obvious connection to the picture it sent. -
OCTEN_MCP_MAX_INFLIGHT_BODYbounds the total bytes all in-flight bodies hold at once, default 24 MiB. The per-request cap stops one enormous request and does nothing about many ordinary ones: with the per-request cap at 6 MiB, forty concurrent 5 MB bodies measured 772 MB resident against a 768 MiB container — an OOM kill reachable by anyone holding a key, with no single request breaking a rule. Over the budget, the newest request is shed with503+Retry-After, because this is capacity rather than a bad call.The budget is deliberately small relative to the bytes it guards: a 5 MB ASCII body is ~10 MB as a UTF-16 string and exists two or three times over (concatenated, parsed, re-serialised upstream), so resident cost runs about ten times the budget. Verified in a real 768 MiB container: sixty concurrent 5 MB requests peak at 342 MiB with no OOM kill.
-
Install documentation rebuilt around the hosted endpoint (
https://mcp.octen.ai/mcp, now live). Remote is the first path a reader meets; local stdio follows as the alternative for clients without remote support. Per-client configuration is spelled out because the JSON key differs between them —url,serverUrl,httpUrl, orserverswith an explicittype— and getting it wrong means the server is simply never contacted, with nothing in any log to say so. Stdio-only clients get themcp-remotebridge, including the detail that--headertakes no space after the colon.Added: a parameter table per tool, transcribed from the schemas the tools publish, and a troubleshooting section covering the failures that do not look like what they are — a
406from an incompleteAcceptheader reads as an auth problem, and a wrong config key looks like the server being down.Corrected: the README claimed
mcp-remotehas no--headeroption. It has had one for several releases; the claim came from a report citing 0.2.4 against a current 0.8.2. The query-string credential remains for clients that accept nothing but a URL, which is a smaller set than that claim implied.
Fixed
-
image_searchgainedimage_data. The API's image input takes either a URL or base64 bytes; only the URL form was offered, so an image you hold rather than one already on the web could not be searched at all. Up to 5MB encoded, matching the documented ceiling — measured on the encoded string, which is what the API bounds. Adata:URI is unwrapped rather than refused: its payload is the base64, and it is the form every browser and screenshot tool produces.Note for the HTTP transport: base64 travels inside the JSON-RPC body, so
OCTEN_MCP_MAX_BODY(default 1 MiB) must be raised above the image or the request is refused. The413now names that variable — a byte count alone does not tell an operator which knob to turn. -
image_searchadvertised the one input combination the API refuses, and could not express the one it documents. Theinputsarray takes exactly one entry — one text input or one image input, never both. This tool requiredqueryand describedimage_urlas usable "in addition" to it, so passing both (the advertised path) was answered upstream withInvalid params. Inputs exceeds 1 entries, while searching by reference image alone — documented and supported — could not be expressed at all.queryis no longer required, exactly one of the two is, and asking for both is refused locally with a message that names the choice instead of relaying a params error. -
include_videosremoved fromsearch/news_search/broad_search. The API reference documents no such parameter for that endpoint. It did have an effect when sent — responses carried avideosfield only with it set — but advertising a parameter the API does not document is promising something nobody has committed to, and since the schemas became a gate that promise is enforced against callers.extractkeeps all three of its media flags; those are documented. The video renderer stays too: if a response ever carriesvideosregardless, dropping them silently is the defect above. -
Declared parameter limits did not match the published API. Seven of them, across four tools. Two were rejecting calls the API serves:
exclude_domainswas capped at 150 against a documented 1200, and every domain longer than 30 characters was refused against a documented 60. Three were missing entirely (urlsitem length,max_age_secondsupper bound,html_snippet.max_tokensupper bound), so nothing bounded them at all.This mattered little while the schemas were advertising; it started costing users the moment they became a gate. A limit stricter than the API's refuses work the API would do, and a looser one defers the failure upstream where the message is worse.
test/apiContract.test.mjsnow pins every documented limit and enum, so the next divergence fails a test instead of a user's call. -
include_images/include_videosreturned no URLs. The tools promise, in their own descriptions, to "return media URLs per result"; what came back was a count. Measured against the live API: a search whose result carried 101 image URLs rendered as the single line**Images:** 101, and the only image links anywhere in the output were favicons.extractdid the same for its images, videos and audio — 37 / 14 / 3 URLs, none of them printed. Setting the flag cost an upstream fetch and returned nothing actionable.Each entry now prints as
url — description, capped at ten per list with the true total stated alongside. The caption matters as much as the link: on news results it carries what was photographed, where, when, and the photographer's credit, and a bare URL leaves a model with nothing it can say about the picture. The cap is there because one result really can carry a hundred images, and pasting all of them into a model's context costs more than the answer is worth — but a cap the reader cannot see is the same silent drop in a smaller size, so the total is always stated. -
cover_imagerendered as[object Object]. The field arrives as{url, description}and search interpolated the object.extracthad already been reading.urlcorrectly — the two had drifted, which is also how the counting bug above came to exist in two places. Both now share one renderer rather than a copy each. -
The OAuth flow never started. The 401 challenge — the only trigger MCP clients act on — fired solely on a
?loginentrypoint, so a client given the plain/mcpURL got 200 frominitialize, concluded the server needed no authorization, listed tools, and failed at the firsttools/callwith a message pointing at manual API-key setup. Connected, and never able to authorize. Confirmed on pre before the fix:initialize,tools/listandtools/callall answered 200 with noWWW-Authenticateon any of them.An uncredentialed request now gets 401 +
WWW-Authenticateon every method when an authorization server is configured. The trade is that anonymoustools/listis no longer possible against such a deployment; deployments without an authorization server are unchanged, since there would be nowhere to send the client. -
JWTs this server cannot verify were retried as API keys. The structural gate that routes a bearer value to the OAuth path or the API-key path required
alg: RS256and akid, so anything token-shaped but unverifiable fell through to the key path: measured,alg: none, an HS256 token, and one missingkidwere each forwarded verbatim to the upstream as an API key and answered HTTP 200 with a tool-level "Invalid API Key". A client holding a bearer token needs 401 + a challenge to know it should re-authorize; a tool error reads as a backend fault to retry, so it never does. It also put a whole bearer token — possibly minted for another audience — into the API gateway's logs as if it were a key. The gate is now shape-only (three segments, header with a stringalg), and the specific reason comes back as a 401. Values that are not token-shaped are still keys, and a wrong key still earns the tool-level "check your key" error.Not affected, verified rather than assumed: an expired token already took the OAuth path and answered 401, because tokens from the authorization server always carry a
kid. -
The request-size cap could be answered with a connection reset instead of a 413. Hanging up while the client is still sending resets the connection, and a reset discards the response bytes still in flight — measured both ways: a client that stopped after 128 KB received the 413, one that kept sending received only
ECONNRESET. The body is now drained and discarded (bounded byOCTEN_MCP_BODY_DRAIN_MS, default 2s) so the answer survives the close. -
Remote kill via a hostile token header. A
kidcontaining CRLF reachedres.writeHeadthrough theWWW-Authenticatechallenge; the resultingERR_INVALID_CHARescaped an async handler as an unhandled rejection and ended the process. Unauthenticated and one request per kill — with a single replica that is a full outage. Fixed in three places: the value is sanitised at its source,error_descriptionis filtered to the RFC 6750 charset, and the request handler now has an error boundary so no future throw can exceed one failed request. -
JWKS amplification. An unknown
kidtriggered a JWKS refetch per request, with no cross-request limit — measured at 11 fetches from 10 unauthenticated requests, aimed at our own authorization server. Refetches are now rate-limited and concurrent misses share one fetch. Rotation still picks up a new key, delayed by at most the cooldown. -
A misconfigured resource URL used to pass startup and every health probe, then kill the process on the first
?login— a pod that is healthy until someone uses it. Both OAuth URLs are now parsed at startup and an unusable value stops the process there. -
The OAuth path bypassed the tuned HTTP dispatcher, so JWKS and grant resolution ignored the proxy environment (the 0.4.0 fix, missing from the code added after it) and re-handshaked on every call.
-
Generic failures replaced with specific ones.
fetch failed,Authorization backend temporarily unavailable,Invalid or expired access tokenandInternal server erroreach fit a dozen unrelated causes, which is what makes a report unactionable. Every failure now names the dependency and the reason: which endpoint, which transport code or HTTP status, how long a token has been expired, both sides of an audience mismatch, and whether the caller should retry or re-authorize. Internal addresses are still never disclosed. -
Environment overrides could be disabled by a typo.
OCTEN_MCP_MAX_BODYandOCTEN_DRAIN_TIMEOUT_MSwere parsed with a bareNumber(), so a non-numeric value becameNaN— silently removing the body cap and turning the drain deadline into an infinite one. Both now fall back, and the drain logs the budget it actually resolved to. -
Requests blocked in credential resolution were invisible to the drain, so SIGTERM could exit underneath one. They are counted from arrival now.
-
A trailing slash on
OCTEN_OAUTH_AUTHORIZATION_SERVERrefused every token. The value is used two ways and a trailing slash breaks both: the JWKS URL becomes…//api/oauth/jwks, andissis compared byte-for-byte against a value no authorization server emits with a trailing slash. The process started, every health probe passed, and 100% of tokens were rejected — measured. Refused at startup now, because the rejection message prints two strings that differ by the one character nobody sees. -
A trailing slash on the endpoint answered 404.
…/mcp/is the commonest paste artifact in a URL a human copies into a client config; answering it with 404 makes a working deployment look broken for a reason nobody inspects. Both spellings are accepted now. -
A burst on one grant made one resolve-key call per request — measured at eight for eight simultaneous calls. That burst is what an agent firing several tools at once right after authorizing looks like, so the dependency's worst moment was also its most likely one. In-flight resolutions are shared now, as the JWKS path already did.
-
The advertised metadata URL was hardcoded to
/mcprather than derived from the resource path, so an instance mounted elsewhere pointed clients at a document it does not serve.
Changed
- The grant→key cache lifetime is now
OCTEN_OAUTH_RESOLVE_CACHE_TTL_MS(default 60000,0disables caching). This is the revocation-propagation window — measured on pre, a revoked token still worked at t=45s and was refused at t=60s — and it is a deliberate trade rather than an implementation detail: resolving on every call would put a synchronous dependency on the authorization server in front of every tool call. It is also an increment on top of the access token's own ~3600s lifetime, not the whole exposure.
0.4.2 — 2026-08-19
Fixed
-
Every call failed on Node 26+ (
code=UND_ERR_INVALID_ARG cause=invalid onError method). Since 0.4.0 the server built its tuned connection agent from the packaged undici 6 and handed it asdispatcherto the host's globalfetch— coupling two undici versions across the dispatch-handler protocol. undici 8, which Node embeds from 26, removed the legacy handler compatibility that v7 still carried, so the host's fetch rejects the v6 dispatcher at validation before a single byte is sent: 100% of tool calls fail on such hosts. 0.4.1 and below need Node <= 24; from 0.4.2 all supported Node versions (>= 18.17) work.The fix makes the HTTP stack self-contained —
fetchand the dispatcher both come from the packaged undici, so the host's embedded undici version is out of the picture entirely, for this incompatibility and for future protocol changes alike. Requests now also pinaccept-encodingto exactly what the packaged undici can decode (gzip, deflate, br) instead of inheriting a scheme- and version-dependent default. CI runs the suite on Node 18/20/22/24/26; a real-socket regression test drives the built server through the packaged stack on the host's own Node, and a lint-style guard keeps globalfetch/Response/Headers/Requestusage out ofsrc/.
0.4.1 — 2026-08-15
Fixed
-
Body-read failures were lumped into
returned non-JSON.resp.json()can fail three distinct ways, and two of them are not parse errors:- the client deadline aborting a stalled read — now
timed out while reading the response body (HTTP <status>); - the connection dying mid-body with no timeout involved (RST, FIN, or a
Content-Length the origin never honored; undici surfaces these as a bare
TypeError: terminatedwith the diagnosis oncause.code) — nowconnection lost while reading the response body (HTTP <status>, code=ECONNRESET|UND_ERR_SOCKET|…). This shape was missed by the first cut of this fix and caught by its review; - genuinely malformed bytes — keeps
returned non-JSON.
The classification is centralised in one helper rather than five copy-pasted catch blocks. Found by rebuilding the 0.4.0 field report's failure shapes on real sockets; regression tests drive the built server against an upstream that stalls, and one that RSTs, mid-body.
- the client deadline aborting a stalled read — now
-
The client-generated correlation UUID no longer appears in user-facing error messages. 0.4.0 echoed it as
request_id=<uuid>, which reads like an id Octen support can search. They cannot — the gateway does not record thex-request-idheader — so a ticket quoting it dead-ends: the same mutual-unaccountability failure the 0.4.0 incident was about, rebuilt in miniature. Error messages now carry only ids verified to be searchable on Octen's side — which, checked against the live infrastructure, is exactly one: the server'srequest_idfrom an API error envelope (confirmed retrievable in gateway logs). The client UUID remains in theOCTEN_MCP_DEBUGtrace, and thex-request-idheader still accompanies every call, so server-side recording can light it up later without a client change. -
The
x-azure-refedge reference is removed everywhere — the debug trace, the docs, and (briefly, within this release's own review cycle) error messages. Verified against the live infrastructure: edge access logging is not enabled, so the reference cannot be looked up at Octen, and an identifier surfaced anywhere in the product reads as a capability. A guard test now asserts infrastructure response headers never leak into user-facing text. -
README claimed
broad_search's default timeout is 60s; it has been 120s (raisable to 300s) since 0.4.0.
0.4.0 — 2026-08-14
Reliability and latency fixes in the HTTP layer. No tool, schema, or parameter changes — every tool behaves the same, it just gets there faster and says what went wrong when it doesn't.
Released as a minor rather than a patch, because two things change underneath a
caller who upgrades without reading: a runtime dependency appears where there
had been only the MCP SDK, and requests that previously ran untimed and
un-retried now do both — and a retry costs quota. Neither should arrive silently
via ~0.3.x. The declared Node floor also moves, but that one changes nothing
in practice; see Changed.
Fixed
- Network failures are now diagnosable.
fetchrejects with a bareTypeError: fetch failed; the actual reason lives onerr.cause, which every call site discarded. Errors now reportcause.code(ECONNRESET,UND_ERR_CONNECT_TIMEOUT,ENOTFOUND, …), the peer address, and a correlation id, and unwrapAggregateErrorwhen every address failed. Support tickets previously carried no information beyond "fetch failed". - Every request has a timeout.
timeoutwas only honoured when the caller passed it explicitly — otherwise noAbortSignalwas attached at all and a stalled request sat on undici's 300sheadersTimeout, which an agent sees as a tool call that never returns. Defaults: 30s forsearch/news_search/image_search/video_search, 120s forbroad_search(this entry originally said 60s in error — the shipped 0.4.0 code already defaulted to 120s, raisable to 300s; corrected in 0.4.1).extracthad no client timeout whatsoever; itstimeoutis the server-side per-URL budget, so the client ceiling is now derived from it with headroom. - Connections are reused between tool calls. The server used undici's global
dispatcher, whose
keepAliveTimeoutis 4s — longer than that idle and the socket is closed, so the next tool call re-pays TCP + TLS. Agent tool calls are almost always more than 4s apart, so nearly every call paid it. Measured againstapi.octen.ai: four calls spanning 16s of idle opened 3 connections at 785ms each; they now open 1, and calls after the first settle at ~270ms against a server-side latency of ~1ms. src/index.tsreported version0.3.6while the package shipped as0.3.7, making the version a client reports useless for triage. It is now read frompackage.json.
Added
-
HTTPS_PROXY/HTTP_PROXY/NO_PROXYsupport. Node's built-infetchignores proxy environment variables, unlike curl and most SDKs, so behind a corporate proxy the server could not reach the API at all while every other tool on the machine could.Note for MCP clients generally, and Claude Desktop specifically: the server does not inherit your shell environment. Claude Desktop passes
HOME,LOGNAME,PATH,SHELLandUSERplus the config'senvblock, and nothing else — so a system-wide proxy is not picked up automatically and has to be named inenvlike the API key. The README shows the config. -
One retry with backoff on connection-level failures (
ECONNRESET,UND_ERR_CONNECT_TIMEOUT, …), covering in particular the keep-alive race where the origin reaps an idle socket exactly as we dispatch on it — which holding sockets for 240s makes more likely, not less. Permanent failures are not retried, and neither is anything that produced a response.Worth stating precisely, because the obvious justification is not quite true:
ECONNREFUSED/EAI_AGAIN/UND_ERR_CONNECT_TIMEOUTgenuinely cannot have reached the server, butECONNRESET/UND_ERR_SOCKETare raised whenever the socket errors — before the request was written or after the server began acting on it — and the two are indistinguishable by error code. We retry both: these are read-only queries, so a duplicate costs quota rather than correctness. But it is a duplicate billed call, and forbroad_searchthe whole server-side fan-out runs twice. SetOCTEN_RETRY=offto disable. -
An
x-request-idcorrelation id per logical call, stable across the retry and echoed in error messages. Octen's ownrequest_idis generated server-side and only comes back on success, so a failed call previously had no identifier at all; this one at least ties a user's error message to the retry attempts in its debug output. Server-side lookup by this header is not available yet — the API does not record it — so it does not currently let a failed request be found in Octen's logs. -
OCTEN_MCP_DEBUG=1— request tracing on stderr (stdout carries MCP protocol framing), built around the questions a field report actually has to answer:- a UTC timestamp on every line, so the trace can be aligned against a client's session log and against server-side receive times;
- the moment each tool call reached this process, which is what separates
delay inside
octen-mcpfrom delay in the host or a relay ahead of it — a client-side stopwatch attributes all of it to us by default; - whether the call reused a connection or paid for a handshake
(
socket=new/socket=reused), and what the handshake cost; - connection failures by phase and error code, rather than after the fact;
x-azure-reffrom the edge. (Removed in 0.4.1: this entry originally claimed the ref was "already present in Octen's infrastructure logs" — edge access logging is in fact not enabled, so the reference could not be looked up at Octen and only misled.)
-
Tunables:
OCTEN_KEEP_ALIVE_MS,OCTEN_KEEP_ALIVE_MAX_MS,OCTEN_CONNECT_TIMEOUT_MS,OCTEN_HTTP2,OCTEN_RETRY. HTTP/2 is off by default: it measured no faster for the usual one-request-at-a-time pattern and is not reliable through every CONNECT proxy.
Changed
-
engines.nodetightened from>=18to>=18.17, to match undici 6.In practice this excludes nobody, and nobody needs to upgrade Node for it. npm treats
enginesas advisory: on Node 18.16 the install prints anEBADENGINEwarning and then proceeds, and the server runs normally — checked, including a live search call. It only blocks underengine-strict=true, and there the binding constraint is not ours:@hono/node-server, reached through@modelcontextprotocol/sdk, already requires Node>=20, so 0.3.7 fails to install on 18.16 for the same reason 0.4.0 does. The declaration is here to be truthful about what this package is tested against, not to force an upgrade. -
New runtime dependency on
undici(^6), required to configure a dispatcher — Node exposes no core API for it. It is the same project that Node bundles forfetch, though not necessarily the same major version: Node 18 ships undici 5 and Node 24 ships undici 7 internally. Passing adispatcherbuilt by one major to afetchimplemented by another is a supported-in-practice but unguaranteed combination, so the pin is deliberate and worth revisiting when the Node floor moves.
Notes
- The timeout is a deadline for the whole call, not per attempt: the retry draws down the same budget, so a 30s timeout cannot become 60s.
keepAliveTimeoutis 60s because that is whatapi.octen.aitolerates, not because 60s is a round number. Measured: a socket idle 30s or 60s is still usable; at 90s the origin has already closed it and the next call re-shakes hands (816ms against 268ms). The edge does not advertise aKeep-Alivehint to follow, so the ceiling has to be set client-side — and setting it too high is not a harmless over-reach, since every gap past the origin's threshold leaves us dispatching onto a socket the peer has already closed.
0.3.7 — 2026-07-30
Changed
- Tool descriptions rewritten for discovery & routing in deferred-loading
clients (no behavior/schema/param change):
- Every tool now opens with a generic, retrieval-friendly first sentence
(no brand name or internal jargon up front) and carries a
keywords:line —image_search/video_searchno longer open with "In Beta.". broad_searchgained an explicit COST signal, four concrete negative examples (single fact →search; don't re-run; known URL →extract; A-vs-B → twosearchcalls), amax_queriesproblem-type → number mapping, pronoun-resolution guidance, and abroad_search + topic=newsboundary.searchgained symmetric guidance pointing multi-subtopic questions tobroad_search; sibling cross-references added acrosssearch/broad_search/extract.
- Every tool now opens with a generic, retrieval-friendly first sentence
(no brand name or internal jargon up front) and carries a
- README: optional
alwaysLoad("keep the tools always on") section for clients with MCP tool search enabled.
0.3.6 — 2026-07-28
Added
OCTEN_ENABLE_BETA_TOOLSenv var. Defaults to enabled (existing behavior). Set it tofalse/0/off/noto omit the Betaimage_searchandvideo_searchtools from tool discovery and reject any direct call to them — lets a host expose only the four generally-available tools.
0.3.5 — 2026-07-27
Added
languageparameter (ISO 639-1 codes) for language filtering on the search / news_search / broad_search tools.
0.3.4 — 2026-07-24
Removed
countryparameter removed from the search / news_search / broad_search tool schemas.
0.3.3 — 2026-07-20
Changed
countryparameter description aligned with the official wording: "Follow ISO 3166, the International Standard for country codes and codes for their subdivisions".
0.3.2 — 2026-07-16
Added
countryinput parameter onsearch,news_search, andbroad_search— ISO 3166-1 alpha-2 country code (e.g.US,JP) orauto(server default) for region-specific results. Sent top-level onPOST /search; nested undersearch_optionsonPOST /broad-search. Omitted from the request when unset so the server default applies.- Minimal unit-test suite (
npm test, Node's built-innode:testrunner) covering request-body assembly forsearch/news_search/broad_search.
0.3.1 — 2026-07-06
Aligns the extract tool with the current Extract API reference
(https://docs.octen.ai/api-reference/extract).
Removed
include_faviconinput parameter. The pagefaviconis now returned by default when available, so the flag is no longer needed (or accepted by the API).
Added
cover_image({url}) is now surfaced in each result wheninclude_imagesis set and the page has a cover image.
0.3.0 — 2026-06-29
Added
broad_searchtool wrapping Octen Broad Search (POST /broad-search) — decomposes a query into up tomax_queries(1–30, default 5) sub-queries, searches them concurrently, and returns results grouped per sub-query. Accepts the same per-sub-query options assearch(flattened, assembled intosearch_options).image_searchtool wrapping Octen Image Search (POST /image-search) — flattenedquery+ optionalimage_url,topic(general/design;designreturns a stylesummary+html_snippet),count(1–10), domain/time filters,safesearch,html_snippet. In Beta — contact us for beta access.video_searchtool wrapping Octen Video Search (POST /video-search) — textquery,count(1–10), time filters,safesearch; results include the matched segment timestamps, duration, cover, and source. In Beta — contact us for beta access.
0.2.0 — 2026-06-23
Added
searchtool wrapping Octen Search API (POST https://api.octen.ai/search).- Supports
query,topic(general/news),count, domain and text include/exclude filters (include_domains≤1000,exclude_domains≤150,include_text/exclude_text≤5, each entry ≤30 chars), a time window (time_basis,time_range,start_time,end_time),format,safesearch,include_images/include_videos, and per-resulthighlight/full_contentoptions (highlight.max_tokens100–20000 default 512;full_content.max_tokens100–100000 default 2048). - Results render as a single markdown block (title, url, authors, timestamps,
favicon, image/video counts, highlight/content) — consistent with the
extracttool. news_searchtool —searchlocked totopic: newsfor current-events/headline queries. Accepts everysearchparameter excepttopic.
0.1.5 — 2026-05-20
Changed
- Align
mcpNameinpackage.jsonwith the canonical GitHub organization name (io.github.Octen-Team/octen-mcp), required by the MCP Registry's case-sensitive ownership check.
0.1.4 — 2026-05-20
Changed
- Tool response is now a single markdown text block instead of a compact
summary followed by the entire raw JSON. Some models (Claude included)
would otherwise reach for
jq/ file_search to dig through the JSON dump just to find a title — wasted turns and a worse UX. Markdown gives the model the structured info (title, category, page_structure, highlights/full_content) ready to use.
0.1.3 — 2026-05-20
Added
mcpNamefield inpackage.json(io.github.octen-team/octen-mcp), required by the official MCP Registry to link the npm package to a registry entry.
0.1.2 — 2026-05-20
Changed
- VS Code install button now prompts for the API key on click (no manual JSON editing needed). Added a VS Code Insiders variant alongside.
- Removed the Cursor install button. Cursor's deeplink format can't pre-prompt for credentials, so the manual JSON config block is the canonical path now.
0.1.1 — 2026-05-20
Fixed
- Align the MCP server's self-reported name (in the
initializehandshake and startup log) with the published package nameocten-mcp.
0.1.0 — 2026-05-20
Added
- Initial release.
extracttool wrapping Octen Extract API (POST https://api.octen.ai/extract).- Supports
urls,query,max_age_seconds,format,timeout,include_images,include_videos,include_audio,include_favicon. - Surfaces Octen-unique response fields:
category,page_structure,highlights. - stdio transport (works with Claude Desktop, Cursor, Claude Code, Windsurf).
OCTEN_API_KEYenv var for authentication.OCTEN_API_URLoverride for staging or self-hosted endpoints.