* feat(plugins): add Context7 plugin for OpenCode
Adds @upstash/context7-opencode, an OpenCode plugin that registers the
hosted Context7 MCP server, the context7-mcp skill, a docs-researcher
subagent, and the /context7-docs command through the config hook.
* docs(opencode): keep ctx7 setup as the primary install path
* refactor(opencode): drop the component toggles and tighten the option boundary
The skill/agent/command toggles were speculative: OpenCode dedupes skills by
name, so running ctx7 setup alongside the plugin is already safe, and a user
who wants a component gone can define it themselves, which the plugin never
overwrites. Removing them collapses resolveOptions into resolveApiKey and
deletes the duplicated defaulting between option parsing and config assembly.
Also assert the MCP endpoints as literals. The tests compared against the same
constants they exercised, so a wrong URL would have passed.
* refactor(opencode): drop tests and collapse the plugin into one file
config.ts existed to give the tests a seam. With the tests gone the split was
indirection with no consumer, so the constants, the config assembly, and the
plugin entry now live in src/index.ts and nothing but the default export is
public. Passing skillsDir through an ApplyInput object also stops being
necessary once it is a module constant.
* fix(opencode): run the MCP server over stdio and drop the slash command
Verified against a real OpenCode 1.18.11 session: the remote transport does
not work. OpenCode opens the optional GET SSE stream, mcp.context7.com answers
405, and OpenCode marks the server failed and registers no tools, so the model
never sees resolve-library-id and falls back to inventing bash calls. A control
project with a plain remote mcp block and no plugin fails the same way, so this
is not plugin-specific. Running the server over stdio connects and the model
calls the tools.
The bundled skill and the agent prompt also referenced the bare tool names.
OpenCode prefixes MCP tools with the server name, so they are now
context7_resolve-library-id and context7_query-docs.
Also removes the /context7-docs command. The skill already triggers on its own
and the subagent covers focused lookups.
* refactor(opencode): use the remote MCP server and drop the subagent
The plugin now adds two things: the hosted MCP server over the remote
transport, and the context7-mcp skill.
Known limitation: OpenCode opens the optional GET SSE stream on the endpoint
and mcp.context7.com answers 405, so OpenCode marks the server failed and
registers no tools. Reproduced on 1.18.11 and 1.18.16, and with a plain remote
mcp block and no plugin, so it is not plugin-specific. It resolves once
mcp.context7.com answers GET /mcp with a 200 SSE stream instead of 405.
* refactor(opencode): ship the canonical skill verbatim and drop the phantom peer dep
The bundled skill had been edited to hard-code OpenCode's context7_ tool
prefix. That divergence was unnecessary and counterproductive:
- It was fixing a misdiagnosis. The model only invented shell commands when
the MCP server had failed to connect and no context7 tools existed at all.
With the tools present, claude-haiku-4-5 calls context7_resolve-library-id
correctly from the bare-name canonical skill.
- It was defeated where it mattered. OpenCode dedupes skills by name and scans
~/.claude/skills first, so on any machine that ran ctx7 setup the bundled
copy is shadowed by the canonical one anyway.
- It was the only client copy to diverge in content. claude, copilot, codex
and cursor all ship skills/context7-mcp/SKILL.md verbatim, and there is no
tooling to keep copies in sync.
Also removes peerDependencies on @opencode-ai/plugin. The package is a
type-only import that the build erases, so the published dist has zero
references to it, and peerDependenciesMeta.optional made the declaration inert
anyway. The devDependency is what typechecking actually needs.
* refactor(opencode): strip commentary and inline the plugin function
Keeps only the two comments that stop someone breaking the plugin: why the
skills cast is needed, and why nothing but the default export may be exported.
Inlining the server function into the default export drops the Context7Plugin
binding and the Plugin type import, since satisfies PluginModule already types
the callback. 85 lines down to 56.
* docs(opencode): use the prefixed tool names consistently in the README
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
* chore(deps): bump dependencies (combined dependabot updates)
Combines the safe dependabot dependency bumps into a single change:
- @modelcontextprotocol/sdk 1.25.2 -> 1.29.0 (mcp)
- undici 6.26.0 -> 8.3.0 (mcp)
- zod 4.3.5 -> 4.4.3 (mcp, tools-ai-sdk)
- commander 13.1.0 -> 15.0.0 (cli)
- ora 9.0.0 -> 9.4.0 (cli)
- dotenv 17.2.3 -> 17.4.2 (sdk, tools-ai-sdk, pi)
- @earendil-works/pi-coding-agent 0.75.5 -> 0.78.0 (pi)
eslint 9 -> 10 (#2703) is excluded: it is incompatible with the
pinned typescript-eslint v8 and breaks lint.
Verified: build, typecheck, lint, and tests pass.
* chore: add changesets for runtime dependency bumps
* fix(deps): pin undici to 7.x for Node 20 compatibility
undici 8 requires Node >=22.19.0 (it calls worker_threads.markAsUncloneable
unconditionally at module load), but CI and the release pipeline run Node 20,
which crashed the mcp test suite with 'markAsUncloneable is not a function'.
undici 7.27.0 guards that call and supports Node >=20.18.1.
* feat(cli): OAuth 2.0 device authorization flow
Adds RFC 8628 device-code login for headless / remote hosts (SSH,
Codespaces, Docker, CI) where the existing localhost-callback flow
can't work — the browser opens on the user's laptop while the
callback listener runs on the remote host, so the redirect target
is unreachable.
Device flow prints a verification URL and short code, then polls the
new /api/oauth/device/token endpoint. The user visits the URL on any
device, signs in, and approves; the CLI receives the same ctx7sk- API
key it would have gotten from the legacy flow.
- shouldUseDeviceFlow() auto-detects via SSH_CONNECTION /
SSH_CLIENT / SSH_TTY and missing $DISPLAY on Linux.
- ctx7 login --device forces it. ctx7 setup picks it up
automatically when resolveCliAuth needs to authenticate.
- pollDeviceToken returns a "transient" status for network errors
and 5xx responses so a flaky backend or Upstash blip doesn't end
the session — keeps polling until the device_code TTL elapses.
* polish(cli): boxed device-code prompt, Press-Enter, whoami success line
Tightens up the device-flow UX so it matches the patterns from gh /
stripe / wrangler:
- Wrap the user_code + verification URL in a boxen rounded box with
a title, gray border, and the code as the visual headline (green
bold, indented on its own line).
- Add a "Press Enter to open the browser, or Ctrl-C to quit..."
confirmation step in TTY mode so the user can read the code before
the browser steals focus. Skipped under --no-browser or non-TTY.
- Replace the generic "Login successful!" line with
"Logged in as <email> (<team>)" by fetching /api/dashboard/whoami
with the freshly minted token. Falls back to the old text if the
call fails.
- Tighten the mockShouldUseDeviceFlow signature in the test mock so
the spread-into-mock pattern typechecks.
No behavior change to the localhost-callback flow.
* test(cli): cover shouldUseDeviceFlow + start/poll + performDeviceLogin
auth-utils: SSH/$DISPLAY heuristics for shouldUseDeviceFlow; the
form-encoded start-device-authorization request shape and error
propagation; pollDeviceToken status mapping for each RFC 8628 code,
5xx -> transient, network error -> transient, and unknown 4xx ->
throw.
auth-commands: performDeviceLogin happy path (approved -> saveTokens
called), denied/expired return null and don't save, transient errors
keep polling instead of bailing, slow_down bumps the interval
(verified with fake timers), startDeviceAuthorization throwing exits
without polling, browser-open behavior under openBrowser=true/false.
Also covers the performLogin selector: forceDevice=true and
shouldUseDeviceFlow=true both route through performDeviceLogin
without hitting the localhost callback path.
28 new tests; suite is 235/235.
* docs(cli): tighten device-flow comments
Drop restatement; keep only WHY-bearing notes.
* fix(cli): default poll interval to 5s per RFC 8628 §3.2
The CLI was treating `interval` as required and would NaN-crash if
a future server omitted it. Spec requires clients to default to 5
when absent.
DeviceAuthorizationResponse.interval is now optional, and
performDeviceLogin uses DEFAULT_DEVICE_POLL_INTERVAL_SECONDS (5) as
the fallback. Test covers the missing-interval path.
* fix(cli): rfc 8628 spec gaps — backoff, hostname, bare verification_uri
Three small spec-compliance fixes from the §3.5 / §3.3 / §5.4 audit:
- §3.5: poll loop now bumps intervalMs by 5s on `transient` results
(network errors and 5xx) — the RFC requires unilateral backoff on
connection timeouts, and mirroring the slow_down handler is the
simplest correct response.
- §3.3: the boxed prompt now prints the bare verification_uri
alongside verification_uri_complete so screen readers / paper /
another device can still type the short form.
- §5.4: startDeviceAuthorization sends `os.hostname()` so the server
can show it on the verification page; the user can confirm the
device they're authorizing matches the one running the CLI.
Transient-backoff test rewritten with fake timers (the new +5s wait
made the old real-timer assertion blow past the 5s default timeout).
* fix(cli): declare @inquirer/core as direct dependency
selectOrInput.ts imports from @inquirer/core directly, but the package
was only resolvable as a transitive of @inquirer/prompts. Under pnpm's
isolated linker this fails with ERR_MODULE_NOT_FOUND at startup.
Fixes#2651
* chore: add changeset
* fix(mcp): support NODE_EXTRA_CA_CERTS for enterprise MITM proxies
When NODE_EXTRA_CA_CERTS is set, reads the CA certificate file and
injects it into undici's global dispatcher. This fixes fetch failures
behind enterprise transparent SSL intercept proxies (Zscaler, etc.)
where the default TLS context does not trust the corporate CA.
The CA certs are also passed through when an explicit HTTPS_PROXY is
configured, so both proxy modes work with custom certificates.
Fixes#2268
This contribution was developed with AI assistance (Claude Code).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: add changeset for NODE_EXTRA_CA_CERTS support
* fix(mcp): preserve default CAs with NODE_EXTRA_CA_CERTS
* chore: add changeset for CA trust fix
* chore: remove superseded changeset
* fix(mcp): lint test files with test tsconfig
* fix(mcp): use explicit test file path
* fix(mcp): support older Node TLS CA APIs
* test(mcp): run certificate test with vitest
---------
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(cli): add token refresh support and centralize auth constants
Expired OAuth tokens are now automatically refreshed via getValidAccessToken()
instead of silently failing. CLI_CLIENT_ID moved to constants.ts to avoid
duplication across auth.ts and setup.ts.
* refactor(cli): internalize baseUrl and clientId in refreshAccessToken
Make refreshAccessToken a private function that resolves getBaseUrl()
and CLI_CLIENT_ID internally instead of requiring them as parameters.
* test(cli): add unit tests for auth utilities and commands
Add comprehensive vitest test suite covering OAuth PKCE flow, token
persistence, token refresh, and CLI auth commands (login/logout/whoami).
* chore: add changeset for CLI auth improvements
* refactor: move CLI auth tests to src/__tests__/
Move auth test files from colocated __tests__ directories to a
centralized src/__tests__/ directory, adjusting mock import paths
accordingly.
* feat: add oauth 2.0 support with login,whoami and logout commands
* ci: lockfile
* fix: address security and port issues in OAuth flow
- Fix XSS vulnerability by escaping HTML in error page messages
- Change callback port from 9876 to 52417 (high ephemeral range, less likely to conflict)
* refactor: clean up auth code and remove unnecessary comments
- Remove obvious comments that duplicate function names
- Extract duplicate expiration display logic into printTokenExpiration helper
- Simplify logoutCommand (remove async, inline condition)
- Remove unused saved_at field from token storage
* fix: address auth bugs and remove unused code
Bug fixes:
- whoami now shows "Session expired" instead of misleading "Logged in" when token is invalid
- expires_at calculation preserves existing value instead of overwriting
- Login check now allows re-login when tokens are expired with no refresh token
Simplifications:
- Remove unused getAccessToken and refreshAccessToken functions
- Simplify printTokenExpiration (remove unused showExpired parameter)
* refactor: improve whoami output formatting
- Align labels using padEnd for consistent output
- Remove expiration time display from whoami
- Show login status first, then fetch user details
* chore: add changeset for OAuth feature
* feat: add skills cli
* chore: cleanup comments
* feat: update commands with new apis
* chore: remove unnecessary deps
* chore: update aliases and package name
* feat: remove /owner/repo/skill convention
* fix: cleanup, path and param handling
* feat(cli): multi-keyword search and Claude symlink priority
- Support multi-keyword search: `ctx7 skills search react hooks`
- Prioritize Claude to receive original files when installing to multiple IDEs
* refactor(cli): rename project to repository, reorder options
- Rename "project" argument to "repository" in help text
- Reorder options: --all and --global first, then IDE options
- Simplify IDE option descriptions to show client name and path
- Update internal variable names (project → repo)
* refactor(cli): flatten search results display
- Remove grouping by repository in search results
- Remove "Select all from repo" option
- Show each skill on single line with repo name in parentheses
- Simplify selection handling
* docs(cli): improve main help output with detailed examples
- Add section headers for commands and examples
- Include npx prefix in all example commands
- Group examples by use case (search, install, manage)
- Add comments explaining each example group
- Add link to context7.com
* refactor(cli): simplify banner, add examples to --help
Move detailed examples to --help output using addHelpText().
Simplify the no-argument banner to show only quick start examples.
* chore: remove unnecessary package
* feat: context7 vercel ai sdk tools package
* ci: remove master target condition on test action
* ci: manual actions trigger for test
* ci: always run all tests
* update tests and imports
* ci: typecheck command
* fix: tests
* docs: add readme
* update folder structure and tool descriptions
* tests: fix test env vars
* ci: bump pnpm version
* update ai sdk step count api
* Add stopWhen to docstring examples
* update tests
* update prompt name
* update context7 agent name
* update tool description var to description
* make context7agent config optional
* ci: add changeset
* make context7-sdk peer deps
* update pnpm-lock.yaml
* update package name to @upstash/context7-tools-ai-sdk
* remove check workflow
* update pnpm lock
* make the agent a class instead of function
* replace all name instances
* add agent generate test
* simplify agent config
* fix refs