Root cause of the 2026-08-11 stream leak, reproduced deterministically: a
2025-era JSON-RPC batch carrying a request plus notifications/cancelled
for that same request never terminates. Per spec a cancelled request gets
no response, but the SDK's legacy stateless transport only closes the
POST's SSE stream once every request in it has been answered — so the
exchange hangs. The 15s keepalive heartbeats then kept the hung stream
"active" forever: no proxy idle timeout could fire, and only the
gateway's 1200s hard cap reaped it. v1 hung on the same batch but sent
no keepalives, so proxy idle timeouts self-healed it within the hour —
which is why the leak only became an outage with v2.
Fix: keepAliveMs: 0. No legitimate exchange here needs a heartbeat (the
tools are millisecond vector queries, p100 ~28s, zero requests over 30s
in 62.7M/day), so the only streams keepalives were keeping alive were
dead ones. Hung exchanges now go silent and the gateway reaps them at
streamIdleTimeout (300s, deployed in context7parser#655) instead of
accumulating for 1200s. This covers the whole class of silent hangs, not
just the cancellation shape.
Validated:
- cancel-batch hang emits 0 bytes over 35s (was: keepalive every 15s);
gateway idle-reap of silent streams was proven separately on a local
Envoy Gateway v1.8.1 (silent stream cut at the idle timeout,
heartbeating stream never)
- tools/call, [req,req] batches, modern-era requests all unchanged
- typecheck, eslint, prettier clean
The SDK accounting bug (cancelled requests should count as settled for
stream close) remains to be filed upstream.
Both fetch calls in packages/mcp/src/lib/api.ts ran without a signal, so a
stalled backend call rode undici's ~300s implicit default before failing.
An explicit 60s AbortSignal.timeout() makes the ceiling deliberate: the
call fails fast with a logged, proper JSON-RPC error result instead of
hanging for five minutes on an implicit dependency default.
60s is generous for these vector queries: p99.9 is ~3.2s and no request
exceeded 30s across a full day of production traffic. It also keeps the
longest legitimate silent window on an SSE exchange well under the
gateway's 300s streamIdleTimeout, which pairs with disabling SSE
keepalives (the stream-leak fix): legit exchanges stay 5x clear of the
idle reaper while hung ones get reaped.
Validated against a backend that accepts connections and never responds:
before, tools/call stalled ~300s before erroring; after, it returns a
JSON-RPC error result at the timeout and logs it. Happy path unaffected;
typecheck, eslint, prettier clean.
Forcing responseMode: "sse" put every MCP response on an SSE stream. Those
streams were not being released: concurrent upstream streams went from ~10
before the v4.0.0 deploy on 08/07 to over 5000 by 08/11, exhausting Envoy's
1024-connection pool and returning 503 "reset reason: overflow" on
mcp.context7.com, including /ping and /mcp/oauth.
Traffic and latency were unchanged across that window (~800 req/s, ~10ms
mean), so this was not load. Little's Law puts healthy concurrency at
834 req/s x 13.4ms = 11 streams, which is exactly what was observed before
v4.0.0.
The SDK default "auto" answers with a single JSON body and upgrades to SSE
only when a handler emits a related message before its result. No tool here
emits progress, so every response becomes JSON.
Verified locally against a running server:
- modern (2026-07-28) requests now return content-type: application/json
- tools/list, resolve-library-id and query-docs all dispatch correctly
- typecheck, eslint and prettier clean
Known limitation: the 2025-era legacy fallback is constructed as
createLegacyStatelessFallback(factory, reportError, options.keepAliveMs) and
never receives responseMode, so legacy requests still stream over SSE. This
change only affects modern-protocol clients.
* fix(cli): refresh expired tokens for documentation commands
- Reuse getValidAccessToken in library and docs commands
- Avoid anonymous fallback when OAuth access tokens expire
* fix(cli): refresh expired tokens in skills suggest and generate
Route the remaining hand-rolled loadTokens/isTokenExpired checks through
getValidAccessToken so an expired token refreshes instead of silently
falling back to anonymous (skills suggest) or forcing a full re-login
(generate). Return undefined instead of null to match the optional
accessToken parameter on the API surface.
* fix(cli): preserve refresh_token and pin the auth wiring
RFC 6749 §6 permits a refresh response that omits refresh_token, in which
case the client keeps the one it holds. getValidAccessToken wrote the
response verbatim, dropping the stored token and silently logging the user
out at the next expiry. This PR widened that path from 2 commands to 6, so
fix it here.
Add a wiring test asserting each command passes a refreshed token to its
API call, and an eslint rule blocking loadTokens/isTokenExpired imports in
src/commands so the inline check cannot come back.
---------
Co-authored-by: Fahreddin Özcan <ozcanfahrettinn@gmail.com>
* docs: remove broken Smithery badge from READMEs
The smithery.ai/badge endpoint returns HTTP 500 with an empty body for
every server, not just Context7. Smithery also removed all badge
documentation from their docs, so the feature looks retired.
The Smithery listing itself is still live and the install section keeps
linking to it, which satisfies Smithery's backlink verification.
* docs: remove broken Star History chart from READMEs
GitHub restricted the stargazers API to repo admins and collaborators on
2026-06-30, so star-history.com can no longer build the chart. The SVG
still returns 200 but renders "GitHub restricted access to star data".
Restoring it would mean embedding a GitHub access token in the chart URL
and handing it to a third party, which we do not want to do.
The 18 links reported by `mint broken-links` were a bug in CLI 4.2.212;
4.2.762 reports zero. Those pages are auto-generated from openapi.json
and all return 200. Fixes the real issues found while checking instead:
- Swagger Petstore URL in the GitOps manifest example returned 404
- `/websites/uploadcare_com` is a stale library ID (404); it is now
`/websites/uploadcare`
- API methods table omitted the GitLab, Bitbucket, other-Git, Notion,
and metrics endpoints, and collapsed the four repo endpoints into one
`{provider}` row that does not match the spec
* fix(cli): write the API key as an Authorization header
Codex resolves a server's auth mode by checking only for
`bearer_token_env_var` or a header literally named `Authorization`
(`auth_status_before_discovery` in codex-rs/rmcp-client/src/auth_status.rs,
mirrored in `create_transport` in rmcp_client.rs). The custom
`CONTEXT7_API_KEY` header matched neither, so Codex fell through to any OAuth
credential stored for the same server name and URL and refreshed it during
startup. A dead refresh token then failed the server with `invalid_grant`
before the API key was ever sent, and re-running setup could not recover it
because setup writes config.toml and never touches the credential store.
The hosted endpoint accepts both header forms, so existing configs keep
working.
Two places keep the legacy header deliberately: the plugin .mcp.json files
default to `${CONTEXT7_API_KEY:-}`, and the server rejects `Bearer` with an
empty token while treating a missing header as anonymous; and `env` blocks in
stdio configs, where the name is an environment variable rather than a header.
* fix(plugins): send the API key via the Authorization header
The Claude and Copilot plugin configs default to `${CONTEXT7_API_KEY:-}`, and
both plugins document that an unset key still works over the anonymous tier.
The Bearer form cannot express that: the server rejects `Bearer` with an empty
token while treating an empty or missing Authorization header as anonymous.
The raw-key form satisfies both states. It is genuinely parsed rather than
ignored, verified by an invalid raw key being rejected, so a set key still
authenticates while an unset one falls back to anonymous as documented.
Once the server treats an empty-token Bearer as no header, these can move to
the `Bearer <key>` form used everywhere else.
* refactor(cli): narrow the Codex OAuth probe and trim its surface
Only `oauth` proves a stored credential exists. `not_logged_in` also covers
"no credential, server merely advertises OAuth", which is the normal state for
anyone who never logged in, so treating it as stale told most users their
config held a credential it did not.
Collapse the module to the two functions the call site needs, derive nothing
from a hand-maintained status list, and skip the subprocess entirely when the
server is not already in Codex's config. Drop the probe timeout to 1.5s and
kill with SIGKILL so it is a real ceiling rather than an intent, since the
result is only an advisory hint.
Lock the plugin manifests' raw-key form behind a test, so normalizing them to
`Bearer` for consistency with the CLI fails loudly instead of silently
breaking anonymous access.
* refactor(cli): drop the Codex OAuth cleanup note
The note existed because re-running setup could not rescue a stuck user. The
Authorization header change in this same branch makes it rescue them: Codex
never reads the stored credential once that header is present, so the
credential is inert and the hint only offered cosmetic cleanup.
Removing it drops a subprocess spawn from a user-facing path and a dependency
on the shape of `codex mcp get --json`, an external contract this repo does not
pin. The reason the header name matters moves to `withHeaders`, where the
decision is encoded.
* focus Context7 documentation queries
* narrow documentation query prompt changes
* remove focused from query prompts
* use lookup wording in query prompts
* distinguish documentation lookup from task
* allow live Pi test more time
* add prompt guidance changeset
* docs(enterprise): add multi-container scaling guide
Document running On-Premise as multiple replicas with PostgreSQL + object
storage. Add the Scaling page under Deployment and cross-reference it from the
Kubernetes single-replica notes.
* docs(enterprise): migration guide for existing deployments + docker scaling pointer
- Scaling page: step-by-step migration using the built-in migrate command
(Docker one-shot and Kubernetes Job), vector sync, encryption-key reuse
- Docker page: add a Scaling pointer
* docs(enterprise): add Settings > Scaling helper screenshots
Show the migration helper (single-container) and the multi-replica confirmation
in the migration section of the Scaling guide.
* docs(enterprise): point Scaling guide at the Helm chart and turnkey compose
- Docker Compose: reference the one-command bundled stack (Postgres + MinIO + LB)
- Kubernetes: use the Helm chart (single default, scaling.enabled to scale out)
- add on-prem / S3-compatible object storage (VECTOR_STORE_ENDPOINT)
* docs(enterprise): make Scaling deployment sections self-contained
On-prem customers get the image and docs, not the source repo, so inline the
full Docker Compose stack (with nginx.conf and .env) and the scaled Kubernetes
manifests (Secret + Deployment) instead of referencing repo files. Note the Helm
chart ships with the enterprise distribution.
* docs(enterprise): pgvector default for scaling, Postgres the only dependency
Vectors go to pgvector in the same Postgres, so object storage is no longer
required. Update config, compose (pgvector image, no MinIO), k8s secret, and the
migration (copies vectors into pgvector, no bucket sync). Object storage is now
an optional escape hatch for very large indexes.
* docs(enterprise): refresh Settings > Scaling screenshot for pgvector migration command
* docs(enterprise): pgvector only, drop the object storage option from the guide
Remove the VECTOR_STORE_URI config row and the 'Vectors on object storage'
section. Multi-replica uses Postgres + pgvector with no object storage.
* docs(enterprise): detailed pgvector provisioning guide
Expand the provisioning step with per-provider instructions (RDS/Aurora, Cloud
SQL, Azure Flexible Server, self-hosted/Docker), the 0.5.0 HNSW requirement,
CREATE EXTENSION, version verification, permission notes, and references. Add a
note on how vectors are stored (HNSW cosine, dimension from the model).
* docs(enterprise): explain the scaling model in Scale out
Every replica serves traffic and indexes; adding replicas grows both. Note how to
bound parse-vs-query contention with Max concurrent parses.
* docs(enterprise): drop SESSION_SECRET; ENCRYPTION_KEY now signs sessions
* docs(enterprise): add scaling sections to docker/kubernetes, link scaling page
- kubernetes: new Scaling section (StatefulSet -> Deployment + pgvector, Helm note)
- docker: refresh Scaling section (pgvector, turnkey compose)
- drop stale object-storage wording, both link to the Scaling guide
* docs(enterprise): add architecture diagram and a Helm page
- scaling: add an Architecture section with a Mermaid multi-replica diagram
- new Helm deployment page (install, scale, ingress, migration, values)
- add Helm to deployment nav; link it from the kubernetes and scaling pages
* docs(enterprise): move Helm page to its own PR (CTX7-1846)
* docs(enterprise): describe per-run Postgres lock instead of leader election
Match the code: scheduled jobs take a short advisory lock at fire time so one
replica runs each, rather than a persistent elected leader. Drop the leader
highlight from the architecture diagram; replicas are interchangeable.
* docs(enterprise): add Vector Stores page covering LanceDB, pgvector, and Milvus
Dedicated vector-store configuration page: backend comparison, VECTOR_STORE
selection, and Milvus / Zilliz Cloud setup. Cross-link from the scaling guide.
* docs(enterprise): note library access + SSO groups carry over in migration
The migrate command now copies per-library access rules and SSO group
memberships (and session epochs) alongside the other tables, so list them
in the migration step.
The skill download step in `ctx7 setup` hits the git tree API on
api.github.com to enumerate a skill's files. When that host is blocked
or unreachable (while the docs host is fine), the fetch throws and setup
reports "Skill failed / fetch failed" (#2936).
Fall back to fetching the single SKILL.md directly from
raw.githubusercontent.com — the URL the docs API already resolves — so
single-file skills install even when api.github.com is not reachable.
Node 26 bundles undici 8, whose built-in fetch reads a global-dispatcher symbol
(Symbol.for('undici.globalDispatcher.2')) that the bundled undici 6
setGlobalDispatcher never wrote. The ProxyAgent and custom-CA Agent in api.ts
were therefore ignored, so HTTPS_PROXY and NODE_EXTRA_CA_CERTS were silently
dropped and requests failed with ENOTFOUND behind CONNECT proxies (#2935).
undici 7 writes both the legacy and current symbols, restoring proxy and CA
support across Node 20-26. It requires Node >=20.18.1, so Node 18 (EOL) is no
longer supported; the engines field and README are updated accordingly.
Fixes#2935
- Document the By page URL mode on the Confluence integration page (with screenshot)
alongside Browse spaces, including per-page Sub-pages and cross-space support.
- Document the confluence pageUrls / includeSubPages manifest fields in GitOps.
- Correct the space picker step to the current lazy-loaded dropdown.
* docs(enterprise): add Other Git integration page (HTTPS + SSH)
Document ingesting private repos from self-managed Git hosts (Gerrit, Gitea,
self-hosted Bitbucket) over HTTPS basic auth and SSH deploy keys, with Docker
and Kubernetes mount examples. Clarifies that SSH keys are mounted into the
container and never stored by Context7.
* docs(enterprise): SSH deploy key is configurable in the UI (mount is the alternative)
* docs(enterprise): add Other Git screenshots + Add a repository steps
- HTTPS and SSH tab screenshots
- 'Add a repository' Steps showing the clone-URL scheme selects auth
- ssh-keyscan snippet for known_hosts
* docs(enterprise): remove em dashes from Other Git page
* fix(cli): avoid shell for GitHub auth token
* fix(cli): document the shell-free constraint and harden gh token tests
Record why `gh auth token` must stay shell-free so the .cmd/.bat shim gap
is not "fixed" by re-adding `shell`, which would restore the cmd.exe
process that #2918 is about.
- reset mock implementations between tests so they stop leaking
- assert listSkillsFromGitHub's result; the tests passed green without it
- cover the GH_TOKEN fallback, which was previously untested
- reword the changeset: execSync spawned a shell on every platform, and
on Windows that shell was load-bearing rather than "unnecessary"
---------
Co-authored-by: Fahreddin Özcan <ozcanfahrettinn@gmail.com>
- Document the manifest 'type' field routing entries to Confluence, website,
llms.txt, and OpenAPI parsers (bare entries stay git)
- Add the per-type field table and the no-secrets/strict-validation notes
- Confluence: document the space URL field, 'index entire space' scope, and
paginated page browsing; cross-link GitOps confluence entries
* docs(enterprise): document programmatic library import/export API
- Add POST /import-libraries endpoint reference (openapi-enterprise.json + page)
- Register it under API Reference → Parse in the nav
- Add an Automating with the API section to the Library Import feature doc,
covering the cloud license-key export and the on-prem JSON import
* docs: update export endpoint to /api/v1/enterprise/export, plain language
Match the renamed cloud export path, move the license key into the request
body, and reword the automation section without em dashes.
* docs(enterprise): export uses Authorization header; import requires an API key
* docs(enterprise): document the force query param on import
On issues.opened for 'Library Report' titles, forward the issue number to the
triage service (context7app), which runs the read-only agent and comments its
findings. Requires repo secrets TRIAGE_WEBHOOK_URL and TRIAGE_WEBHOOK_SECRET.
* docs(enterprise): add Confluence integration page
Document connecting Confluence (Cloud and self-hosted Data Center) and
indexing a space from Add. Includes screenshots and a nav entry under
Enterprise > Integrations.
* docs(enterprise): wider Confluence settings screenshot with mock site URL
* fix(mcp): skip loopback and IPv6 private IPs in getClientIp
Extract getClientIp into lib/client-ip.ts and extend the private/local
IP filter to cover 127.0.0.0/8, 169.254.0.0/16, ::1, fe80::/10, and
fc00::/7 when walking X-Forwarded-For. Proxies that prepend loopback or
health-check addresses no longer pollute mcp-client-ip analytics.
Fixes#2874
* fix(mcp): tighten private IP detection and add changeset
Anchor the fe80::/10 and fc00::/7 regexes to full 4-digit first hextets
so abbreviated hextets like fe8::1 or fc::1 are no longer misclassified
as private. Match IPv6 loopback in any textual form (0::1,
0:0:0:0:0:0:0:1), add CGNAT (100.64.0.0/10) to the skip list, and add a
patch changeset.
---------
Co-authored-by: syf2211 <syf2211@users.noreply.github.com>
Co-authored-by: Fahreddin Özcan <ozcanfahrettinn@gmail.com>
Fixes#2860
- Use npx ctx7@latest as the canonical CLI invocation in find-docs SKILL.md
- Add official library naming guidance matching rules/context7-cli.md
- De-emphasize global npm install as the primary workflow
- Add regression tests to keep skill and rule guidance aligned
Co-authored-by: syf2211 <syf2211@users.noreply.github.com>