From c8e704f5f502c2a42f91e4cdaf78b1d05f7340d4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 12 Aug 2026 22:09:45 +0000 Subject: [PATCH 01/11] feat(mcp): require auth on /mcp when client is a plugin Plugin hosts such as Claude Code only start OAuth for servers that 401 at connect time. Matching Exa MCP, ?client=claude-code-plugin (any client value containing "plugin") now gates /mcp the same way /mcp/oauth does, while anonymous access on the public URL is unchanged. Co-authored-by: Enes Gules --- .changeset/mcp-plugin-client-auth.md | 5 ++ .claude-plugin/marketplace.json | 2 +- docs/howto/oauth.mdx | 20 +++++--- packages/mcp/README.md | 9 +++- packages/mcp/src/index.ts | 9 +++- packages/mcp/src/lib/auth/plugin-client.ts | 13 +++++ packages/mcp/test/integration.test.ts | 56 ++++++++++++++++++++++ packages/mcp/test/plugin-client.test.ts | 24 ++++++++++ plugins/claude/context7/.mcp.json | 2 +- plugins/claude/context7/README.md | 6 ++- 10 files changed, 133 insertions(+), 13 deletions(-) create mode 100644 .changeset/mcp-plugin-client-auth.md create mode 100644 packages/mcp/src/lib/auth/plugin-client.ts create mode 100644 packages/mcp/test/plugin-client.test.ts diff --git a/.changeset/mcp-plugin-client-auth.md b/.changeset/mcp-plugin-client-auth.md new file mode 100644 index 000000000..5256d8ab1 --- /dev/null +++ b/.changeset/mcp-plugin-client-auth.md @@ -0,0 +1,5 @@ +--- +"@upstash/context7-mcp": patch +--- + +Require authentication on `/mcp` when the `client` query parameter identifies a plugin (for example `?client=claude-code-plugin`). Plugin hosts such as Claude Code only start OAuth for servers that 401 at connect time; this matches that connect-time challenge without changing anonymous access on the public `/mcp` URL. diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 9a32fe78f..063a0fade 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,7 +8,7 @@ "name": "context7", "source": "./plugins/claude/context7", "description": "Up-to-date documentation lookup. Pull version-specific documentation and code examples directly from source repositories into your LLM context.", - "version": "1.0.2" + "version": "1.0.3" } ] } diff --git a/docs/howto/oauth.mdx b/docs/howto/oauth.mdx index 1fa6ce552..c4158f672 100644 --- a/docs/howto/oauth.mdx +++ b/docs/howto/oauth.mdx @@ -20,22 +20,30 @@ Context7 MCP server supports OAuth 2.0 authentication for MCP clients that imple ## Configuration -To use OAuth, change the endpoint from `/mcp` to `/mcp/oauth` in your client configuration: +To use OAuth, either change the endpoint from `/mcp` to `/mcp/oauth`, or keep `/mcp` and add a plugin client query parameter (this is what the Claude Code plugin uses): ```diff - "url": "https://mcp.context7.com/mcp" + "url": "https://mcp.context7.com/mcp/oauth" ``` +```diff +- "url": ".../mcp" ++ "url": ".../mcp?client=claude-code-plugin" +``` + +Both require authentication on connect. `/mcp` without those options stays anonymous. Any `client` value containing `plugin` (for example `claude-code-plugin`) triggers the same 401 + `WWW-Authenticate` challenge so the host can start its OAuth flow. + ## How It Works -1. Your MCP client connects to the OAuth endpoint -2. You're redirected to Context7 to sign in -3. After signing in, you're redirected back to your client -4. Your client automatically handles token refresh +1. Your MCP client connects to `/mcp/oauth` or `/mcp?client=claude-code-plugin` +2. The server responds with `401` and a `WWW-Authenticate` challenge pointing at Context7's authorization server +3. You're redirected to Context7 to sign in +4. After signing in, you're redirected back to your client +5. Your client automatically handles token refresh -**Authentication required after setup.** Most clients won't authenticate automatically. After adding the OAuth endpoint, you'll need to explicitly authenticate through your client's MCP settings. For example, in Claude Code run `/mcp`, select the server, and choose "Authenticate". +**Some clients still need an explicit authenticate step.** After adding `/mcp/oauth`, you may need to authenticate through your client's MCP settings — for example, in Claude Code run `/mcp`, select the server, and choose "Authenticate". The `?client=claude-code-plugin` URL is the exception: Claude Code treats the connect-time 401 as a signal to start OAuth immediately. ## Client Support diff --git a/packages/mcp/README.md b/packages/mcp/README.md index c8abc4d29..ab368ab7e 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -1510,13 +1510,20 @@ CONTEXT7_API_KEY=your_api_key_here Context7 MCP server supports OAuth 2.0 authentication for MCP clients that implement the [MCP OAuth specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization). -To use OAuth, change the endpoint from `/mcp` to `/mcp/oauth` in your client configuration: +To use OAuth, either change the endpoint from `/mcp` to `/mcp/oauth`, or keep `/mcp` and add a plugin client query parameter (this is what the Claude Code plugin uses): ```diff - "url": "https://mcp.context7.com/mcp" + "url": "https://mcp.context7.com/mcp/oauth" ``` +```diff +- "url": ".../mcp" ++ "url": ".../mcp?client=claude-code-plugin" +``` + +Both require authentication on connect. `/mcp` without those options stays anonymous. Any `client` value containing `plugin` (for example `claude-code-plugin`) triggers the same 401 + `WWW-Authenticate` challenge so the host can start its OAuth flow. + > **Note:** OAuth is not supported with stdio transport. For local MCP connections, use API key authentication instead.
diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index fc2301da3..e213ad6d8 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -23,6 +23,7 @@ import { OPENAI_APPS_CHALLENGE_TOKEN, } from "./lib/constants.js"; import { maybeElicitAuthSignIn } from "./lib/auth/auth-prompt.js"; +import { isPluginClientQuery } from "./lib/auth/plugin-client.js"; import { getClientIp } from "./lib/client-ip.js"; /** Default HTTP server port */ @@ -452,9 +453,13 @@ async function main() { } }; - // Anonymous access endpoint - no authentication required + // Public endpoint: anonymous by default. Plugin clients pass + // `?client=claude-code-plugin` (any `client` value containing "plugin") + // so this connection 401s at initialize and the host can start OAuth — + // Claude Code only exposes authorize helpers for servers flagged when + // the session starts. app.all("/mcp", async (req, res) => { - await handleMcpRequest(req, res, false); + await handleMcpRequest(req, res, isPluginClientQuery(req.query.client)); }); // OAuth-protected endpoint - requires authentication diff --git a/packages/mcp/src/lib/auth/plugin-client.ts b/packages/mcp/src/lib/auth/plugin-client.ts new file mode 100644 index 000000000..5eb889352 --- /dev/null +++ b/packages/mcp/src/lib/auth/plugin-client.ts @@ -0,0 +1,13 @@ +/** + * True when the `client` query parameter identifies a plugin, e.g. + * `?client=claude-code-plugin`. + * + * Plugin hosts (Claude Code marketplace plugins in particular) only start + * OAuth for servers that 401 at connect time. The public `/mcp` endpoint stays + * anonymous for everyone else; a `client` value containing `"plugin"` opts + * that connection into the same auth gate as `/mcp/oauth`. + */ +export function isPluginClientQuery(client: unknown): boolean { + const value = Array.isArray(client) ? client[0] : client; + return typeof value === "string" && value.includes("plugin"); +} diff --git a/packages/mcp/test/integration.test.ts b/packages/mcp/test/integration.test.ts index 0c9d68401..1cb0b0fb0 100644 --- a/packages/mcp/test/integration.test.ts +++ b/packages/mcp/test/integration.test.ts @@ -230,3 +230,59 @@ describe.each([ expect(apiCall.headers["x-context7-client-version"]).toBe(expected.version); }); }); + +const INITIALIZE = { + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: "2025-06-18", + capabilities: {}, + clientInfo: { name: "t", version: "1" }, + }, +}; + +async function postMcp(target: string, headers: Record = {}) { + const res = await fetch(target, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/event-stream", + ...headers, + }, + body: JSON.stringify(INITIALIZE), + }); + return { status: res.status, wwwAuthenticate: res.headers.get("www-authenticate") }; +} + +describe("plugin client auth gate", () => { + test("keeps /mcp anonymous when client is not a plugin", async () => { + const res = await postMcp(httpUrl); + expect(res.status).toBe(200); + }); + + test("challenges /mcp?client=claude-code-plugin so the host can start OAuth", async () => { + const res = await postMcp(`${httpUrl}?client=claude-code-plugin`); + expect(res.status).toBe(401); + expect(res.wwwAuthenticate).toContain("resource_metadata="); + expect(res.wwwAuthenticate).toContain("/.well-known/oauth-protected-resource"); + }); + + test("lets a credential through the plugin client gate", async () => { + const res = await postMcp(`${httpUrl}?client=claude-code-plugin`, { + Authorization: "Bearer ctx7sk-test", + }); + expect(res.status).toBe(200); + }); + + test("does not challenge a non-plugin client query param", async () => { + const res = await postMcp(`${httpUrl}?client=claude-code`); + expect(res.status).toBe(200); + }); + + test("still requires auth on /mcp/oauth", async () => { + const oauthUrl = httpUrl.replace(/\/mcp$/, "/mcp/oauth"); + const res = await postMcp(oauthUrl); + expect(res.status).toBe(401); + }); +}); diff --git a/packages/mcp/test/plugin-client.test.ts b/packages/mcp/test/plugin-client.test.ts new file mode 100644 index 000000000..9c5bbabe8 --- /dev/null +++ b/packages/mcp/test/plugin-client.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from "vitest"; +import { isPluginClientQuery } from "../src/lib/auth/plugin-client.js"; + +describe("isPluginClientQuery", () => { + test("matches claude-code-plugin and other plugin client ids", () => { + expect(isPluginClientQuery("claude-code-plugin")).toBe(true); + expect(isPluginClientQuery("cursor-plugin")).toBe(true); + expect(isPluginClientQuery("plugin")).toBe(true); + }); + + test("ignores non-plugin client ids and missing values", () => { + expect(isPluginClientQuery("claude-code")).toBe(false); + expect(isPluginClientQuery("claude-desktop")).toBe(false); + expect(isPluginClientQuery("")).toBe(false); + expect(isPluginClientQuery(undefined)).toBe(false); + expect(isPluginClientQuery(null)).toBe(false); + expect(isPluginClientQuery(1)).toBe(false); + }); + + test("uses the first value when the query param is repeated", () => { + expect(isPluginClientQuery(["claude-code-plugin", "other"])).toBe(true); + expect(isPluginClientQuery(["claude-code", "plugin"])).toBe(false); + }); +}); diff --git a/plugins/claude/context7/.mcp.json b/plugins/claude/context7/.mcp.json index 7fda704c8..e4b052d1f 100644 --- a/plugins/claude/context7/.mcp.json +++ b/plugins/claude/context7/.mcp.json @@ -2,7 +2,7 @@ "mcpServers": { "context7": { "type": "http", - "url": "https://mcp.context7.com/mcp", + "url": "https://mcp.context7.com/mcp?client=claude-code-plugin", "$comment": "// pragma: allowlist secret", "headers": { "Authorization": "${CONTEXT7_API_KEY:-}" } diff --git a/plugins/claude/context7/README.md b/plugins/claude/context7/README.md index 32f77b242..709fd14c8 100644 --- a/plugins/claude/context7/README.md +++ b/plugins/claude/context7/README.md @@ -20,9 +20,11 @@ claude plugin marketplace add upstash/context7 claude plugin install context7@context7-marketplace ``` -## API Key (Recommended) +## Authentication -Without an API key, the plugin connects anonymously and shares the anonymous rate limits. To use your own plan, create an API key in the [Context7 dashboard](https://context7.com/dashboard) and export it as an environment variable before launching Claude Code: +The plugin URL includes `?client=claude-code-plugin`. That `client` parameter tells the Context7 MCP server to require authentication, so Claude Code can start its OAuth sign-in flow when the plugin connects. + +To use an API key instead of OAuth, create one in the [Context7 dashboard](https://context7.com/dashboard) and export it as an environment variable before launching Claude Code: ```bash # e.g. in ~/.zshrc or ~/.bashrc From 7397890e99fe997b292702c8b1ea335883a6b175 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 12 Aug 2026 22:12:11 +0000 Subject: [PATCH 02/11] chore(mcp): drop the SDK OAuth-helpers TODO The v2 helpers (bearerAuthChallengeResponse, oauthMetadataResponse) assume Bearer-only OAuth on a fetch() handler. This server also accepts API keys, mixes anonymous and required routes, returns JSON-RPC 401 bodies, and proxies authorization-server metadata live, so they are not a drop-in. Co-authored-by: Enes Gules --- packages/mcp/src/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index e213ad6d8..3ede609d4 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -394,11 +394,11 @@ async function main() { const apiKey = extractApiKey(req); const baseUrl = new URL(RESOURCE_URL).origin; - // OAuth discovery info header, used by MCP clients to discover the authorization server - // TODO: @modelcontextprotocol/server now ships canonical OAuth helpers - // (bearerAuthChallengeResponse, buildOAuthProtectedResourceMetadata, - // oauthMetadataResponse) — replace this hand-rolled header and the - // /.well-known/oauth-protected-resource route with them. + // OAuth discovery info header, used by MCP clients to discover the authorization server. + // Hand-rolled rather than the SDK's bearerAuthChallengeResponse / + // oauthMetadataResponse: those assume Bearer-only OAuth on a fetch() + // handler. This server also accepts API keys, mixes anonymous and + // required routes, returns JSON-RPC 401 bodies, and proxies AS metadata. res.set( "WWW-Authenticate", `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"` From 4acd8b2a688798b70ce10d7d2a3445ff8a9f8dc5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 13 Aug 2026 05:43:52 +0000 Subject: [PATCH 03/11] docs: keep the plugin client auth gate out of user-facing docs The ?client=claude-code-plugin gate stays in the server and Claude plugin URL. OAuth docs continue to describe /mcp/oauth only. Co-authored-by: Enes Gules --- docs/howto/oauth.mdx | 20 ++++++-------------- packages/mcp/README.md | 9 +-------- plugins/claude/context7/README.md | 6 ++---- 3 files changed, 9 insertions(+), 26 deletions(-) diff --git a/docs/howto/oauth.mdx b/docs/howto/oauth.mdx index c4158f672..1fa6ce552 100644 --- a/docs/howto/oauth.mdx +++ b/docs/howto/oauth.mdx @@ -20,30 +20,22 @@ Context7 MCP server supports OAuth 2.0 authentication for MCP clients that imple ## Configuration -To use OAuth, either change the endpoint from `/mcp` to `/mcp/oauth`, or keep `/mcp` and add a plugin client query parameter (this is what the Claude Code plugin uses): +To use OAuth, change the endpoint from `/mcp` to `/mcp/oauth` in your client configuration: ```diff - "url": "https://mcp.context7.com/mcp" + "url": "https://mcp.context7.com/mcp/oauth" ``` -```diff -- "url": ".../mcp" -+ "url": ".../mcp?client=claude-code-plugin" -``` - -Both require authentication on connect. `/mcp` without those options stays anonymous. Any `client` value containing `plugin` (for example `claude-code-plugin`) triggers the same 401 + `WWW-Authenticate` challenge so the host can start its OAuth flow. - ## How It Works -1. Your MCP client connects to `/mcp/oauth` or `/mcp?client=claude-code-plugin` -2. The server responds with `401` and a `WWW-Authenticate` challenge pointing at Context7's authorization server -3. You're redirected to Context7 to sign in -4. After signing in, you're redirected back to your client -5. Your client automatically handles token refresh +1. Your MCP client connects to the OAuth endpoint +2. You're redirected to Context7 to sign in +3. After signing in, you're redirected back to your client +4. Your client automatically handles token refresh -**Some clients still need an explicit authenticate step.** After adding `/mcp/oauth`, you may need to authenticate through your client's MCP settings — for example, in Claude Code run `/mcp`, select the server, and choose "Authenticate". The `?client=claude-code-plugin` URL is the exception: Claude Code treats the connect-time 401 as a signal to start OAuth immediately. +**Authentication required after setup.** Most clients won't authenticate automatically. After adding the OAuth endpoint, you'll need to explicitly authenticate through your client's MCP settings. For example, in Claude Code run `/mcp`, select the server, and choose "Authenticate". ## Client Support diff --git a/packages/mcp/README.md b/packages/mcp/README.md index ab368ab7e..c8abc4d29 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -1510,20 +1510,13 @@ CONTEXT7_API_KEY=your_api_key_here Context7 MCP server supports OAuth 2.0 authentication for MCP clients that implement the [MCP OAuth specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization). -To use OAuth, either change the endpoint from `/mcp` to `/mcp/oauth`, or keep `/mcp` and add a plugin client query parameter (this is what the Claude Code plugin uses): +To use OAuth, change the endpoint from `/mcp` to `/mcp/oauth` in your client configuration: ```diff - "url": "https://mcp.context7.com/mcp" + "url": "https://mcp.context7.com/mcp/oauth" ``` -```diff -- "url": ".../mcp" -+ "url": ".../mcp?client=claude-code-plugin" -``` - -Both require authentication on connect. `/mcp` without those options stays anonymous. Any `client` value containing `plugin` (for example `claude-code-plugin`) triggers the same 401 + `WWW-Authenticate` challenge so the host can start its OAuth flow. - > **Note:** OAuth is not supported with stdio transport. For local MCP connections, use API key authentication instead.
diff --git a/plugins/claude/context7/README.md b/plugins/claude/context7/README.md index 709fd14c8..ba3dee9a3 100644 --- a/plugins/claude/context7/README.md +++ b/plugins/claude/context7/README.md @@ -20,11 +20,9 @@ claude plugin marketplace add upstash/context7 claude plugin install context7@context7-marketplace ``` -## Authentication +## API Key (Recommended) -The plugin URL includes `?client=claude-code-plugin`. That `client` parameter tells the Context7 MCP server to require authentication, so Claude Code can start its OAuth sign-in flow when the plugin connects. - -To use an API key instead of OAuth, create one in the [Context7 dashboard](https://context7.com/dashboard) and export it as an environment variable before launching Claude Code: +To use your own plan, create an API key in the [Context7 dashboard](https://context7.com/dashboard) and export it as an environment variable before launching Claude Code: ```bash # e.g. in ~/.zshrc or ~/.bashrc From d12b083bffbf0833ad858f29ab65ecfe19b607bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fahreddin=20=C3=96zcan?= Date: Thu, 13 Aug 2026 09:48:09 +0300 Subject: [PATCH 04/11] simplify Claude plugin auth tracking --- .changeset/mcp-plugin-client-auth.md | 2 +- .claude-plugin/marketplace.json | 1 + packages/mcp/src/index.ts | 42 +++++++++++------- packages/mcp/src/lib/auth/plugin-client.ts | 13 ------ packages/mcp/test/integration.test.ts | 44 +++++++++++-------- packages/mcp/test/plugin-client.test.ts | 24 ---------- .../context7/.claude-plugin/plugin.json | 1 + plugins/claude/context7/.mcp.json | 3 +- 8 files changed, 56 insertions(+), 74 deletions(-) delete mode 100644 packages/mcp/src/lib/auth/plugin-client.ts delete mode 100644 packages/mcp/test/plugin-client.test.ts diff --git a/.changeset/mcp-plugin-client-auth.md b/.changeset/mcp-plugin-client-auth.md index 5256d8ab1..3eaf1f52a 100644 --- a/.changeset/mcp-plugin-client-auth.md +++ b/.changeset/mcp-plugin-client-auth.md @@ -2,4 +2,4 @@ "@upstash/context7-mcp": patch --- -Require authentication on `/mcp` when the `client` query parameter identifies a plugin (for example `?client=claude-code-plugin`). Plugin hosts such as Claude Code only start OAuth for servers that 401 at connect time; this matches that connect-time challenge without changing anonymous access on the public `/mcp` URL. +Require authentication and track usage separately for the Claude Code plugin. diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 063a0fade..4b7f7ed07 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -3,6 +3,7 @@ "owner": { "name": "Upstash" }, + "description": "Context7 plugins for coding agents.", "plugins": [ { "name": "context7", diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index 3ede609d4..b8e72b571 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -23,11 +23,11 @@ import { OPENAI_APPS_CHALLENGE_TOKEN, } from "./lib/constants.js"; import { maybeElicitAuthSignIn } from "./lib/auth/auth-prompt.js"; -import { isPluginClientQuery } from "./lib/auth/plugin-client.js"; import { getClientIp } from "./lib/client-ip.js"; /** Default HTTP server port */ const DEFAULT_PORT = 3000; +const CLAUDE_CODE_PLUGIN_CLIENT = "claude-code-plugin"; // Parse CLI arguments using commander const program = new Command() @@ -78,7 +78,11 @@ const CLI_PORT = (() => { return isNaN(parsed) ? undefined : parsed; })(); -const requestContext = new AsyncLocalStorage(); +interface HttpClientContext extends ClientContext { + pluginClient?: typeof CLAUDE_CODE_PLUGIN_CLIENT; +} + +const requestContext = new AsyncLocalStorage(); // Global state for stdio mode only let stdioApiKey: string | undefined; @@ -93,9 +97,13 @@ function getClientContext(toolCtx: ServerContext): ClientContext { const ctx = requestContext.getStore(); const requestClientInfo = envelopeClientInfo(toolCtx.mcpReq.envelope); - // Use protocol client info when available; fall back to the HTTP User-Agent. + // Plugin identity wins; otherwise protocol info beats the HTTP User-Agent. if (ctx) { - return { ...ctx, clientInfo: requestClientInfo ?? ctx.clientInfo }; + const clientInfo = requestClientInfo ?? ctx.clientInfo; + return { + ...ctx, + clientInfo: ctx.pluginClient ? { ...clientInfo, ide: ctx.pluginClient } : clientInfo, + }; } // stdio mode: envelope (modern clients) or globals (legacy initialize) @@ -388,17 +396,18 @@ async function main() { const handleMcpRequest = async ( req: express.Request, res: express.Response, - requireAuth: boolean + requireAuth: boolean, + pluginClient?: typeof CLAUDE_CODE_PLUGIN_CLIENT ) => { try { const apiKey = extractApiKey(req); const baseUrl = new URL(RESOURCE_URL).origin; - // OAuth discovery info header, used by MCP clients to discover the authorization server. - // Hand-rolled rather than the SDK's bearerAuthChallengeResponse / - // oauthMetadataResponse: those assume Bearer-only OAuth on a fetch() - // handler. This server also accepts API keys, mixes anonymous and - // required routes, returns JSON-RPC 401 bodies, and proxies AS metadata. + // OAuth discovery info header, used by MCP clients to discover the authorization server + // TODO: @modelcontextprotocol/server now ships canonical OAuth helpers + // (bearerAuthChallengeResponse, buildOAuthProtectedResourceMetadata, + // oauthMetadataResponse) — replace this hand-rolled header and the + // /.well-known/oauth-protected-resource route with them. res.set( "WWW-Authenticate", `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"` @@ -431,10 +440,11 @@ async function main() { } } - const context: ClientContext = { + const context: HttpClientContext = { clientIp: getClientIp(req), apiKey: apiKey, clientInfo: extractClientInfoFromUserAgent(req.headers["user-agent"]), + pluginClient, transport: "http", }; @@ -453,13 +463,11 @@ async function main() { } }; - // Public endpoint: anonymous by default. Plugin clients pass - // `?client=claude-code-plugin` (any `client` value containing "plugin") - // so this connection 401s at initialize and the host can start OAuth — - // Claude Code only exposes authorize helpers for servers flagged when - // the session starts. + // The Claude Code plugin requires auth and gets its own metrics identifier. app.all("/mcp", async (req, res) => { - await handleMcpRequest(req, res, isPluginClientQuery(req.query.client)); + const pluginClient = + req.query.client === CLAUDE_CODE_PLUGIN_CLIENT ? CLAUDE_CODE_PLUGIN_CLIENT : undefined; + await handleMcpRequest(req, res, Boolean(pluginClient), pluginClient); }); // OAuth-protected endpoint - requires authentication diff --git a/packages/mcp/src/lib/auth/plugin-client.ts b/packages/mcp/src/lib/auth/plugin-client.ts deleted file mode 100644 index 5eb889352..000000000 --- a/packages/mcp/src/lib/auth/plugin-client.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * True when the `client` query parameter identifies a plugin, e.g. - * `?client=claude-code-plugin`. - * - * Plugin hosts (Claude Code marketplace plugins in particular) only start - * OAuth for servers that 401 at connect time. The public `/mcp` endpoint stays - * anonymous for everyone else; a `client` value containing `"plugin"` opts - * that connection into the same auth gate as `/mcp/oauth`. - */ -export function isPluginClientQuery(client: unknown): boolean { - const value = Array.isArray(client) ? client[0] : client; - return typeof value === "string" && value.includes("plugin"); -} diff --git a/packages/mcp/test/integration.test.ts b/packages/mcp/test/integration.test.ts index 1cb0b0fb0..4f92cc118 100644 --- a/packages/mcp/test/integration.test.ts +++ b/packages/mcp/test/integration.test.ts @@ -256,33 +256,41 @@ async function postMcp(target: string, headers: Record = {}) { } describe("plugin client auth gate", () => { - test("keeps /mcp anonymous when client is not a plugin", async () => { - const res = await postMcp(httpUrl); - expect(res.status).toBe(200); + beforeEach(() => { + requests.length = 0; }); - test("challenges /mcp?client=claude-code-plugin so the host can start OAuth", async () => { + test("only challenges the supported plugin client", async () => { + expect((await postMcp(`${httpUrl}?client=other-plugin`)).status).toBe(200); + const res = await postMcp(`${httpUrl}?client=claude-code-plugin`); expect(res.status).toBe(401); expect(res.wwwAuthenticate).toContain("resource_metadata="); expect(res.wwwAuthenticate).toContain("/.well-known/oauth-protected-resource"); }); - test("lets a credential through the plugin client gate", async () => { - const res = await postMcp(`${httpUrl}?client=claude-code-plugin`, { - Authorization: "Bearer ctx7sk-test", - }); - expect(res.status).toBe(200); - }); + test("tracks authenticated plugin requests separately", async () => { + const client = new Client( + { name: "claude-code", version: "1.0.0" }, + { versionNegotiation: { mode: { pin: "2026-07-28" } } } + ); + await client.connect( + new StreamableHTTPClientTransport(new URL(`${httpUrl}?client=claude-code-plugin`), { + requestInit: { headers: { Authorization: "Bearer ctx7sk-test" } }, + }) + ); - test("does not challenge a non-plugin client query param", async () => { - const res = await postMcp(`${httpUrl}?client=claude-code`); - expect(res.status).toBe(200); - }); + try { + await client.callTool({ + name: "query-docs", + arguments: { libraryId: "/vercel/next.js", query: "app router" }, + }); + } finally { + await client.close(); + } - test("still requires auth on /mcp/oauth", async () => { - const oauthUrl = httpUrl.replace(/\/mcp$/, "/mcp/oauth"); - const res = await postMcp(oauthUrl); - expect(res.status).toBe(401); + const apiCall = requests.find((request) => request.path === "/v2/context"); + expect(apiCall?.headers["x-context7-client-ide"]).toBe("claude-code-plugin"); + expect(apiCall?.headers["x-context7-client-version"]).toBe("1.0.0"); }); }); diff --git a/packages/mcp/test/plugin-client.test.ts b/packages/mcp/test/plugin-client.test.ts deleted file mode 100644 index 9c5bbabe8..000000000 --- a/packages/mcp/test/plugin-client.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { describe, expect, test } from "vitest"; -import { isPluginClientQuery } from "../src/lib/auth/plugin-client.js"; - -describe("isPluginClientQuery", () => { - test("matches claude-code-plugin and other plugin client ids", () => { - expect(isPluginClientQuery("claude-code-plugin")).toBe(true); - expect(isPluginClientQuery("cursor-plugin")).toBe(true); - expect(isPluginClientQuery("plugin")).toBe(true); - }); - - test("ignores non-plugin client ids and missing values", () => { - expect(isPluginClientQuery("claude-code")).toBe(false); - expect(isPluginClientQuery("claude-desktop")).toBe(false); - expect(isPluginClientQuery("")).toBe(false); - expect(isPluginClientQuery(undefined)).toBe(false); - expect(isPluginClientQuery(null)).toBe(false); - expect(isPluginClientQuery(1)).toBe(false); - }); - - test("uses the first value when the query param is repeated", () => { - expect(isPluginClientQuery(["claude-code-plugin", "other"])).toBe(true); - expect(isPluginClientQuery(["claude-code", "plugin"])).toBe(false); - }); -}); diff --git a/plugins/claude/context7/.claude-plugin/plugin.json b/plugins/claude/context7/.claude-plugin/plugin.json index a53438cbe..873f1802d 100644 --- a/plugins/claude/context7/.claude-plugin/plugin.json +++ b/plugins/claude/context7/.claude-plugin/plugin.json @@ -1,5 +1,6 @@ { "name": "context7", + "version": "1.0.3", "description": "Upstash Context7 MCP server for up-to-date documentation lookup. Pull version-specific documentation and code examples directly from source repositories into your LLM context.", "author": { "name": "Upstash" diff --git a/plugins/claude/context7/.mcp.json b/plugins/claude/context7/.mcp.json index e4b052d1f..046281e4b 100644 --- a/plugins/claude/context7/.mcp.json +++ b/plugins/claude/context7/.mcp.json @@ -2,7 +2,8 @@ "mcpServers": { "context7": { "type": "http", - "url": "https://mcp.context7.com/mcp?client=claude-code-plugin", "$comment": "// pragma: allowlist secret", + "url": "https://mcp.context7.com/mcp?client=claude-code-plugin", + "$comment": "// pragma: allowlist secret", "headers": { "Authorization": "${CONTEXT7_API_KEY:-}" } From 319a89d304374c87671f96f39aba5ee927e09e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fahreddin=20=C3=96zcan?= Date: Thu, 13 Aug 2026 10:10:08 +0300 Subject: [PATCH 05/11] separate plugin and client metrics --- packages/mcp/src/index.ts | 29 +++++++++------------------ packages/mcp/src/lib/encryption.ts | 3 +++ packages/mcp/src/lib/types.ts | 1 + packages/mcp/test/integration.test.ts | 3 ++- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index b8e72b571..fb9713d13 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -27,7 +27,7 @@ import { getClientIp } from "./lib/client-ip.js"; /** Default HTTP server port */ const DEFAULT_PORT = 3000; -const CLAUDE_CODE_PLUGIN_CLIENT = "claude-code-plugin"; +const CLAUDE_CODE_PLUGIN = "claude-code-plugin"; // Parse CLI arguments using commander const program = new Command() @@ -78,11 +78,7 @@ const CLI_PORT = (() => { return isNaN(parsed) ? undefined : parsed; })(); -interface HttpClientContext extends ClientContext { - pluginClient?: typeof CLAUDE_CODE_PLUGIN_CLIENT; -} - -const requestContext = new AsyncLocalStorage(); +const requestContext = new AsyncLocalStorage(); // Global state for stdio mode only let stdioApiKey: string | undefined; @@ -97,13 +93,9 @@ function getClientContext(toolCtx: ServerContext): ClientContext { const ctx = requestContext.getStore(); const requestClientInfo = envelopeClientInfo(toolCtx.mcpReq.envelope); - // Plugin identity wins; otherwise protocol info beats the HTTP User-Agent. + // Use protocol client info when available; fall back to the HTTP User-Agent. if (ctx) { - const clientInfo = requestClientInfo ?? ctx.clientInfo; - return { - ...ctx, - clientInfo: ctx.pluginClient ? { ...clientInfo, ide: ctx.pluginClient } : clientInfo, - }; + return { ...ctx, clientInfo: requestClientInfo ?? ctx.clientInfo }; } // stdio mode: envelope (modern clients) or globals (legacy initialize) @@ -397,7 +389,7 @@ async function main() { req: express.Request, res: express.Response, requireAuth: boolean, - pluginClient?: typeof CLAUDE_CODE_PLUGIN_CLIENT + plugin?: typeof CLAUDE_CODE_PLUGIN ) => { try { const apiKey = extractApiKey(req); @@ -440,11 +432,11 @@ async function main() { } } - const context: HttpClientContext = { + const context: ClientContext = { clientIp: getClientIp(req), apiKey: apiKey, clientInfo: extractClientInfoFromUserAgent(req.headers["user-agent"]), - pluginClient, + plugin, transport: "http", }; @@ -463,11 +455,10 @@ async function main() { } }; - // The Claude Code plugin requires auth and gets its own metrics identifier. + // The Claude Code plugin requires auth and is tracked separately from its host client. app.all("/mcp", async (req, res) => { - const pluginClient = - req.query.client === CLAUDE_CODE_PLUGIN_CLIENT ? CLAUDE_CODE_PLUGIN_CLIENT : undefined; - await handleMcpRequest(req, res, Boolean(pluginClient), pluginClient); + const plugin = req.query.client === CLAUDE_CODE_PLUGIN ? CLAUDE_CODE_PLUGIN : undefined; + await handleMcpRequest(req, res, Boolean(plugin), plugin); }); // OAuth-protected endpoint - requires authentication diff --git a/packages/mcp/src/lib/encryption.ts b/packages/mcp/src/lib/encryption.ts index 91ceb1f4e..c13a61b21 100644 --- a/packages/mcp/src/lib/encryption.ts +++ b/packages/mcp/src/lib/encryption.ts @@ -54,6 +54,9 @@ export function generateHeaders(context: ClientContext): Record if (context.clientInfo?.version) { headers["X-Context7-Client-Version"] = context.clientInfo.version; } + if (context.plugin) { + headers["X-Context7-Plugin"] = context.plugin; + } if (context.transport) { headers["X-Context7-Transport"] = context.transport; } diff --git a/packages/mcp/src/lib/types.ts b/packages/mcp/src/lib/types.ts index 3b5a024f0..47cacce2e 100644 --- a/packages/mcp/src/lib/types.ts +++ b/packages/mcp/src/lib/types.ts @@ -39,6 +39,7 @@ export interface ClientContext { ide?: string; version?: string; }; + plugin?: string; transport?: "stdio" | "http"; sessionId?: string; /** Mutable: set by the upstream API layer when the backend signals the diff --git a/packages/mcp/test/integration.test.ts b/packages/mcp/test/integration.test.ts index 4f92cc118..839edb7cc 100644 --- a/packages/mcp/test/integration.test.ts +++ b/packages/mcp/test/integration.test.ts @@ -290,7 +290,8 @@ describe("plugin client auth gate", () => { } const apiCall = requests.find((request) => request.path === "/v2/context"); - expect(apiCall?.headers["x-context7-client-ide"]).toBe("claude-code-plugin"); + expect(apiCall?.headers["x-context7-client-ide"]).toBe("claude-code"); expect(apiCall?.headers["x-context7-client-version"]).toBe("1.0.0"); + expect(apiCall?.headers["x-context7-plugin"]).toBe("claude-code-plugin"); }); }); From 57f64e2a82c2b5e465d0140df4ccee404680af03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fahreddin=20=C3=96zcan?= Date: Thu, 13 Aug 2026 10:14:53 +0300 Subject: [PATCH 06/11] extract plugin request detection --- packages/mcp/src/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index fb9713d13..1139e4e7c 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -29,6 +29,10 @@ import { getClientIp } from "./lib/client-ip.js"; const DEFAULT_PORT = 3000; const CLAUDE_CODE_PLUGIN = "claude-code-plugin"; +function getPluginFromRequest(req: express.Request): typeof CLAUDE_CODE_PLUGIN | undefined { + return req.query.client === CLAUDE_CODE_PLUGIN ? CLAUDE_CODE_PLUGIN : undefined; +} + // Parse CLI arguments using commander const program = new Command() .version(SERVER_VERSION, "-v, --version", "output the current version") @@ -457,7 +461,7 @@ async function main() { // The Claude Code plugin requires auth and is tracked separately from its host client. app.all("/mcp", async (req, res) => { - const plugin = req.query.client === CLAUDE_CODE_PLUGIN ? CLAUDE_CODE_PLUGIN : undefined; + const plugin = getPluginFromRequest(req); await handleMcpRequest(req, res, Boolean(plugin), plugin); }); From 7519c1ecb5d2a151f692518691d1d81663f6b698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fahreddin=20=C3=96zcan?= Date: Thu, 13 Aug 2026 10:40:03 +0300 Subject: [PATCH 07/11] simplify MCP request handling --- packages/mcp/src/index.ts | 16 ++++++---------- packages/mcp/test/integration.test.ts | 6 ++++++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index 1139e4e7c..87ba7bed4 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -389,13 +389,10 @@ async function main() { onerror: (error) => console.error("MCP node adapter error:", error), }); - const handleMcpRequest = async ( - req: express.Request, - res: express.Response, - requireAuth: boolean, - plugin?: typeof CLAUDE_CODE_PLUGIN - ) => { + const handleMcpRequest = async (req: express.Request, res: express.Response) => { try { + const plugin = getPluginFromRequest(req); + const requiresAuth = req.path === "/mcp/oauth" || Boolean(plugin); const apiKey = extractApiKey(req); const baseUrl = new URL(RESOURCE_URL).origin; @@ -409,7 +406,7 @@ async function main() { `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"` ); - if (requireAuth) { + if (requiresAuth) { if (!apiKey) { return res.status(401).json({ jsonrpc: "2.0", @@ -461,13 +458,12 @@ async function main() { // The Claude Code plugin requires auth and is tracked separately from its host client. app.all("/mcp", async (req, res) => { - const plugin = getPluginFromRequest(req); - await handleMcpRequest(req, res, Boolean(plugin), plugin); + await handleMcpRequest(req, res); }); // OAuth-protected endpoint - requires authentication app.all("/mcp/oauth", async (req, res) => { - await handleMcpRequest(req, res, true); + await handleMcpRequest(req, res); }); app.get("/ping", (_req: express.Request, res: express.Response) => { diff --git a/packages/mcp/test/integration.test.ts b/packages/mcp/test/integration.test.ts index 839edb7cc..f8a1b623f 100644 --- a/packages/mcp/test/integration.test.ts +++ b/packages/mcp/test/integration.test.ts @@ -269,6 +269,12 @@ describe("plugin client auth gate", () => { expect(res.wwwAuthenticate).toContain("/.well-known/oauth-protected-resource"); }); + test("keeps the OAuth endpoint protected", async () => { + const res = await postMcp(httpUrl.replace(/\/mcp$/, "/mcp/oauth")); + + expect(res.status).toBe(401); + }); + test("tracks authenticated plugin requests separately", async () => { const client = new Client( { name: "claude-code", version: "1.0.0" }, From da5b6cf9cb3cab98e5d1f92a92e263f1e9926dac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fahreddin=20=C3=96zcan?= Date: Thu, 13 Aug 2026 10:46:29 +0300 Subject: [PATCH 08/11] extract authentication policy --- packages/mcp/src/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index 87ba7bed4..ddb446683 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -33,6 +33,10 @@ function getPluginFromRequest(req: express.Request): typeof CLAUDE_CODE_PLUGIN | return req.query.client === CLAUDE_CODE_PLUGIN ? CLAUDE_CODE_PLUGIN : undefined; } +function requiresAuthentication(req: express.Request, plugin?: typeof CLAUDE_CODE_PLUGIN): boolean { + return req.path === "/mcp/oauth" || Boolean(plugin); +} + // Parse CLI arguments using commander const program = new Command() .version(SERVER_VERSION, "-v, --version", "output the current version") @@ -392,7 +396,6 @@ async function main() { const handleMcpRequest = async (req: express.Request, res: express.Response) => { try { const plugin = getPluginFromRequest(req); - const requiresAuth = req.path === "/mcp/oauth" || Boolean(plugin); const apiKey = extractApiKey(req); const baseUrl = new URL(RESOURCE_URL).origin; @@ -406,7 +409,7 @@ async function main() { `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"` ); - if (requiresAuth) { + if (requiresAuthentication(req, plugin)) { if (!apiKey) { return res.status(401).json({ jsonrpc: "2.0", From 56b9064f280e5b9437720810e83cc8bc621d71e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fahreddin=20=C3=96zcan?= Date: Thu, 13 Aug 2026 11:24:53 +0300 Subject: [PATCH 09/11] use OAuth for Claude Code plugin --- plugins/claude/context7/.mcp.json | 6 +----- plugins/claude/context7/README.md | 11 +++++------ 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/plugins/claude/context7/.mcp.json b/plugins/claude/context7/.mcp.json index 046281e4b..30c7cd96a 100644 --- a/plugins/claude/context7/.mcp.json +++ b/plugins/claude/context7/.mcp.json @@ -2,11 +2,7 @@ "mcpServers": { "context7": { "type": "http", - "url": "https://mcp.context7.com/mcp?client=claude-code-plugin", - "$comment": "// pragma: allowlist secret", - "headers": { - "Authorization": "${CONTEXT7_API_KEY:-}" - } + "url": "https://mcp.context7.com/mcp?client=claude-code-plugin" } } } diff --git a/plugins/claude/context7/README.md b/plugins/claude/context7/README.md index ba3dee9a3..69695dd9c 100644 --- a/plugins/claude/context7/README.md +++ b/plugins/claude/context7/README.md @@ -20,16 +20,15 @@ claude plugin marketplace add upstash/context7 claude plugin install context7@context7-marketplace ``` -## API Key (Recommended) +## Authentication -To use your own plan, create an API key in the [Context7 dashboard](https://context7.com/dashboard) and export it as an environment variable before launching Claude Code: +After installing the plugin, restart Claude Code and run: -```bash -# e.g. in ~/.zshrc or ~/.bashrc -export CONTEXT7_API_KEY="your-api-key" +``` +/mcp ``` -The plugin's MCP server configuration picks up `CONTEXT7_API_KEY` automatically. Restart Claude Code after setting it, then verify the key is being used by checking your usage in the [dashboard](https://context7.com/dashboard). +Select Context7 and follow the browser sign-in flow. The plugin uses OAuth, so no API key is required. ## Available Tools From c0dfa78a493d0400a5e9e023fce90e445c1d628f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fahreddin=20=C3=96zcan?= Date: Thu, 13 Aug 2026 11:32:14 +0300 Subject: [PATCH 10/11] support API key or OAuth in Claude plugin --- .../src/__tests__/plugin-manifests.test.ts | 51 +++++++++++++------ plugins/claude/context7/.mcp.json | 3 +- plugins/claude/context7/README.md | 4 +- plugins/claude/context7/scripts/headers.mjs | 3 ++ 4 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 plugins/claude/context7/scripts/headers.mjs diff --git a/packages/cli/src/__tests__/plugin-manifests.test.ts b/packages/cli/src/__tests__/plugin-manifests.test.ts index 60b80fc1c..7e323e96f 100644 --- a/packages/cli/src/__tests__/plugin-manifests.test.ts +++ b/packages/cli/src/__tests__/plugin-manifests.test.ts @@ -1,24 +1,43 @@ import { describe, test, expect } from "vitest"; import { readFile } from "fs/promises"; import { join } from "path"; +import { execFile } from "child_process"; +import { promisify } from "util"; const REPO_ROOT = join(import.meta.dirname, "..", "..", "..", ".."); +const execFileAsync = promisify(execFile); describe("plugin MCP manifests", () => { - // Deliberately the raw key, not `Bearer ` as the CLI writes. Both plugins - // document that an unset key still works over the anonymous tier, and this is - // the only form that survives both states: the server rejects `Bearer` with an - // empty token but treats an empty Authorization as anonymous. - test.each(["plugins/claude/context7/.mcp.json", "plugins/copilot/context7/.mcp.json"])( - "%s passes the raw key via Authorization", - async (relPath) => { - const raw = await readFile(join(REPO_ROOT, relPath), "utf-8"); - const config = JSON.parse(raw) as { - mcpServers: { context7: { headers: Record } }; - }; - expect(config.mcpServers.context7.headers).toEqual({ - Authorization: "${CONTEXT7_API_KEY:-}", - }); - } - ); + test("Claude uses an API key only when one is set", async () => { + const relPath = "plugins/claude/context7/.mcp.json"; + const raw = await readFile(join(REPO_ROOT, relPath), "utf-8"); + const config = JSON.parse(raw) as { + mcpServers: { context7: { headers?: Record; headersHelper: string } }; + }; + expect(config.mcpServers.context7.headers).toBeUndefined(); + expect(config.mcpServers.context7.headersHelper).toBe( + 'node "${CLAUDE_PLUGIN_ROOT}/scripts/headers.mjs"' + ); + + const helper = join(REPO_ROOT, "plugins/claude/context7/scripts/headers.mjs"); + const withoutKey = await execFileAsync(process.execPath, [helper], { + env: { ...process.env, CONTEXT7_API_KEY: "" }, + }); + expect(JSON.parse(withoutKey.stdout)).toEqual({}); + + const withKey = await execFileAsync(process.execPath, [helper], { + env: { ...process.env, CONTEXT7_API_KEY: "ctx7sk-test" }, + }); + expect(JSON.parse(withKey.stdout)).toEqual({ Authorization: "ctx7sk-test" }); + }); + + test("Copilot passes the raw API key via Authorization", async () => { + const raw = await readFile(join(REPO_ROOT, "plugins/copilot/context7/.mcp.json"), "utf-8"); + const config = JSON.parse(raw) as { + mcpServers: { context7: { headers: Record } }; + }; + expect(config.mcpServers.context7.headers).toEqual({ + Authorization: "${CONTEXT7_API_KEY:-}", + }); + }); }); diff --git a/plugins/claude/context7/.mcp.json b/plugins/claude/context7/.mcp.json index 30c7cd96a..41153c881 100644 --- a/plugins/claude/context7/.mcp.json +++ b/plugins/claude/context7/.mcp.json @@ -2,7 +2,8 @@ "mcpServers": { "context7": { "type": "http", - "url": "https://mcp.context7.com/mcp?client=claude-code-plugin" + "url": "https://mcp.context7.com/mcp?client=claude-code-plugin", + "headersHelper": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/headers.mjs\"" } } } diff --git a/plugins/claude/context7/README.md b/plugins/claude/context7/README.md index 69695dd9c..14044442c 100644 --- a/plugins/claude/context7/README.md +++ b/plugins/claude/context7/README.md @@ -28,7 +28,9 @@ After installing the plugin, restart Claude Code and run: /mcp ``` -Select Context7 and follow the browser sign-in flow. The plugin uses OAuth, so no API key is required. +Select Context7 and follow the browser sign-in flow. No API key is required. + +To use an API key instead, set `CONTEXT7_API_KEY` before starting Claude Code. The plugin sends the key only when it is present; otherwise it uses OAuth. ## Available Tools diff --git a/plugins/claude/context7/scripts/headers.mjs b/plugins/claude/context7/scripts/headers.mjs new file mode 100644 index 000000000..5a51464ca --- /dev/null +++ b/plugins/claude/context7/scripts/headers.mjs @@ -0,0 +1,3 @@ +const apiKey = process.env.CONTEXT7_API_KEY; + +process.stdout.write(JSON.stringify(apiKey ? { Authorization: apiKey } : {})); From d59447bba828f830ce9ea03a7fde9753c3f8670e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fahreddin=20=C3=96zcan?= Date: Thu, 13 Aug 2026 11:58:15 +0300 Subject: [PATCH 11/11] simplify Claude plugin authentication --- packages/mcp/src/index.ts | 3 +-- packages/mcp/test/integration.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index ddb446683..850aad514 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -438,7 +438,7 @@ async function main() { const context: ClientContext = { clientIp: getClientIp(req), - apiKey: apiKey, + apiKey, clientInfo: extractClientInfoFromUserAgent(req.headers["user-agent"]), plugin, transport: "http", @@ -459,7 +459,6 @@ async function main() { } }; - // The Claude Code plugin requires auth and is tracked separately from its host client. app.all("/mcp", async (req, res) => { await handleMcpRequest(req, res); }); diff --git a/packages/mcp/test/integration.test.ts b/packages/mcp/test/integration.test.ts index f8a1b623f..941b11b49 100644 --- a/packages/mcp/test/integration.test.ts +++ b/packages/mcp/test/integration.test.ts @@ -255,12 +255,12 @@ async function postMcp(target: string, headers: Record = {}) { return { status: res.status, wwwAuthenticate: res.headers.get("www-authenticate") }; } -describe("plugin client auth gate", () => { +describe("plugin authentication", () => { beforeEach(() => { requests.length = 0; }); - test("only challenges the supported plugin client", async () => { + test("only challenges the supported plugin", async () => { expect((await postMcp(`${httpUrl}?client=other-plugin`)).status).toBe(200); const res = await postMcp(`${httpUrl}?client=claude-code-plugin`);