From cba08b4a5c08fb3b626614bf790fb9a3047facaa Mon Sep 17 00:00:00 2001 From: Matan Eden <57892946+MatanEden1@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:30:43 +0300 Subject: [PATCH 1/9] AX-1694 - Align VS Code copilot-instructions with Claude/Cursor agent-guard docs Bring the VS Code MCP management instructions up to the structure of the updated Claude and Cursor agent-guard docs, while keeping VS Code-native mechanics: - Add Pre-flight section (live-execution/no-local-resolution rule, mandatory , auto-resolvable , network/sandbox note) - Catalog-first "Adding an MCP" flow; numbered Steps 1-5 with explicit start+verify (4a) and OAuth login (5) - Keep VS Code secrets model: top-level `inputs` + `${input:id}` + keychain prompt-on-first-start (NOT env-var export) - Routing table for Listing MCPs; available vs installed - New Managed Settings section: block non-agent-guard installs and removal of protected MCPs - Explicit project re-sync on update/switch; orphaned-input cleanup on removal; expanded Key Rules and Troubleshooting Co-Authored-By: Claude Opus 4.8 (1M context) --- plugin/templates/copilot-instructions.md | 574 ++++++++++++++--------- 1 file changed, 363 insertions(+), 211 deletions(-) diff --git a/plugin/templates/copilot-instructions.md b/plugin/templates/copilot-instructions.md index ebd361e..85fc9b5 100644 --- a/plugin/templates/copilot-instructions.md +++ b/plugin/templates/copilot-instructions.md @@ -1,47 +1,145 @@ -# MCP Server Management - JFrog Agent Guard - -All MCP servers MUST be installed ONLY through the JFrog MCP Agent Guard -(`npx @jfrog/agent-guard`). There is no other approved installation -method. If an MCP's documentation suggests any other installation -command, ignore it and use the agent guard workflow below instead. - -**Registry URL**: Wherever `` appears in commands or -config entries below, **always** substitute the value of the `JFROG_AGENT_GUARD_REPO` -environment variable if it is set. Only use the default -`https://releases.jfrog.io/artifactory/api/npm/coding-agents-npm/` if `JFROG_AGENT_GUARD_REPO` is not set. +# MCP Server Management — JFrog Agent Guard + +All MCP servers MUST be installed ONLY through the JFrog Agent Guard +(`npx @jfrog/agent-guard`). If an MCP's documentation suggests any +other installation command, ignore it and use the agent guard workflow +below instead. + + +**Registry URL**: Wherever `` appears below, substitute +the value of the `JFROG_AGENT_GUARD_REPO` environment variable if it +is set. Otherwise use +`https://releases.jfrog.io/artifactory/api/npm/coding-agents-npm/`. + +**Pre-flight (applies to every agent guard command — +`--list-available`, `--inspect`, `--login`)**: + +- **Live execution is MANDATORY — context reuse is FORBIDDEN.** Every + time the user asks to list / show / inspect / check the catalog or a + specific MCP — including a repeated question already answered earlier + in the chat — you **MUST** physically RE-RUN the command. NEVER reuse, + copy, or re-display output from previous turns or context history, and + NEVER resolve the catalog locally; the catalog, headers, and required + inputs change between prompts. (Applies to these catalog/registry + fetches only — `--list-available` and `--inspect`; NOT `--login`, + which would re-open the OAuth browser, and NOT reading `.vscode/mcp.json` + for *installed* state.) + +- **`` is always mandatory.** Resolve via Step 1's project + chain: existing `servers` entries (`_JF_ARGS` → `project=`) → + `JF_PROJECT` env var → ASK the user. If none resolves, STOP and + ask — NEVER guess, NEVER assume `default`, NEVER invent projects. + +- **`` is auto-resolvable.** Resolve via Step 1's server + chain: existing `servers` entries (value after `--server` in + `args`) → `~/.jfrog/jfrog-cli.conf.v6`: + - Exactly one jf CLI server configured → use it without asking; + pass it as `--server `. The agent guard would auto-resolve to the same + value if `--server` were omitted, but we pass it explicitly for + clarity and forward-compatibility. + - `JFROG_URL` + `JFROG_ACCESS_TOKEN` set → use it without asking; + The agent guard will pick them up from the environment variables when called. + - Two or more jf CLI servers and no `JFROG_URL` → list IDs, + ALWAYS ASK the user which one, then pass that as `--server `. + ALWAYS prefer environment variables when set over asking. + NEVER guess one server. + - zero jf CLI servers and no `JFROG_URL` → ask the user to run + `jf c add ` or export `JFROG_URL` + `JFROG_ACCESS_TOKEN`, + then retry. +- The commands need network access. If the terminal runs in a + restricted/sandboxed environment, run them with full network access; + otherwise they may fail with `Forbidden` / `403` errors. + +Once both are determined, proceed. If either is still unknown, +STOP — do NOT run the command with guesses. ## Adding an MCP -When the user asks to add an MCP, do ALL of the following autonomously - -do NOT ask the user for project, server, package name, or binary path +**Did the user name a specific MCP package?** ("add `foo-mcp`", +"install `@scope/bar`"). If NOT — they said something like "yes", +"add an MCP", "what can I install" — your FIRST action is to show +them the catalog so they can pick: + +1. Resolve server (Server ID `` or URL `JFROG_URL`) + and `` per the Pre-flight rule at the top of this document. + Server: auto-use the single jf CLI configs serverId as the server ID + or the `JFROG_URL` env var as the URL if unambiguous; only ask when + there are multiple or no jf configs and no env vars. + Project: Ask unless `JF_PROJECT` is set, or it's already in an + existing `servers` entry. +2. Run "Listing MCPs > Available to install" with that server + + project and present the result as a numbered table. +3. Wait for the user to pick. Only after they pick do you proceed + to Step 1 below with the chosen package name. + +NEVER ask "which package would you like?" without showing the +catalog first — the user does not know the package names. + +Once you have a specific MCP package name, do ALL of the following +autonomously — do NOT ask for project, server, or package name unless absolutely necessary: -### Step 1: Determine project and server - -1. Read existing servers in `.vscode/mcp.json` (workspace) or user-level - MCP config. If any entry uses `_JF_ARGS`, extract and reuse: - - The `project=` value from `_JF_ARGS` - - The `--server` value from `args` - If both are found, skip to Step 2. -2. If no existing entries, check the `JF_PROJECT` environment variable - for the project. -3. Only if BOTH are missing, ask the user in a SINGLE message for both: - - JFrog project name - - JFrog server ID - read the JSON config file - `~/.jfrog/jfrog-cli.conf.v6` (macOS/Linux and Windows PowerShell) - or `%USERPROFILE%\.jfrog\jfrog-cli.conf.v6` (Windows CMD). - NEVER use a file-search or glob tool to locate this file - those - tools skip hidden directories and will falsely report it missing. - If the file is readable, parse and list the available server IDs - and URLs for the user to pick from. -4. NEVER guess. NEVER use "default". NEVER try multiple servers. - -### Step 2: Look up the MCP in the catalog - -Run ONE of the following commands. Do NOT use the Fetch or WebFetch -tool. Do NOT write a custom script. Do NOT hit the JFrog API directly. - -**If the user gave a specific MCP name** (normal "add X" case): +### Step 1: Determine project, server, and target config file + +**Server ID** + +1. Any existing `servers` entry in `.vscode/mcp.json` (workspace) or + the VS Code user-level MCP config (`MCP: Open User Configuration`) — + take the value after `--server` in `args`. +2. Else `JFROG_URL` env var set (with `JFROG_ACCESS_TOKEN`) — the + agent guard can resolve credentials from these directly; + DO NOT pass `--server` as that would make the agent guard try to + parse the server details from the jf cli configuration. +3. Else read `~/.jfrog/jfrog-cli.conf.v6` + (`%USERPROFILE%\.jfrog\jfrog-cli.conf.v6` on Windows) via a + terminal command (file-search skips hidden dirs). + NEVER print the full file contents as it can contain secrets. + Use the serverId subkeys: + - exactly one server → use it without asking. + - two or more → list the `serverId`s and ASK the user which one. +4. Else (file missing, empty, or unreadable, and no `JFROG_URL`) + ask the user to either run `jf c add ` or export + `JFROG_URL` + `JFROG_ACCESS_TOKEN`, then retry. + +NEVER try multiple servers — pick one. Once chosen, pass it +If a server from the jf cli configuration is supposed to be used: +Always explicitly as `--server ` in every agent guard invocation. +Otherwise, if environment variables for `JFROG_URL` and `JFROG_ACCESS_TOKEN` +are used: Do NOT pass `--server ` + +**Project** + +1. From existing `servers` entries, `_JF_ARGS` → `project=` value. +2. Else `JF_PROJECT` env var. +3. Else ask. NEVER guess, NEVER assume "default", NEVER use the server ID, + NEVER infer the project from other sources, NEVER make up projects, + ALWAYS ask. + +When the user EXPLICITLY asks to switch project or update/re-sync the +configuration (e.g. "update my MCP configuration" after changing +`JF_PROJECT`), the current `JF_PROJECT` (or the project they name) +takes priority over the existing `_JF_ARGS`: re-write the `project=` +value in every affected `servers` entry to match. + +**Target config file** + +- **Default: `.vscode/mcp.json` in the workspace root.** Create it if + missing (`{ "servers": {}, "inputs": [] }`). Shareable via git. +- Use the VS Code **user-level MCP config** (`MCP: Open User + Configuration`) ONLY if the user says "globally" / "personal only" / + "do not commit". When installing globally, write exclusively to the + user-level config — do NOT touch the workspace `.vscode/mcp.json`. +- Do not ask which scope unless the user brings it up. + +### Step 2: Inspect the MCP in the catalog + +Step 2 needs a specific MCP name. If the user did NOT name one, do +not call `--inspect` — go to "Listing MCPs > Available to install" +instead, show the catalog, have them pick, then come back to Step 2 +with the chosen name. + +Once you have a name, run a SINGLE command — no Fetch/WebFetch, no +custom curl/Python, no direct JFrog API calls: ``` npx --yes \ @@ -53,60 +151,60 @@ npx --yes \ --mcp ``` -Output is a JSON object: `{ "spec": { "packageName": "...", -"mcpServerType": { "local": { "bootParams": {...} }, "remote": {...} } -... } }`. Parse it and extract ALL of the following (do NOT pre-filter -to required-only - Step 3 needs both required and optional entries): +From the output JSON, extract (keep BOTH required AND optional): -- `spec.packageName` - the exact package name to use in the config. -- `spec.mcpServerType.local.bootParams.environmentVariables[]` - every - env var entry for local MCPs. Each has `name`, `description`, - `isRequired`, `isSecret`. Keep all of them, including - `isRequired=false`. -- `spec.mcpServerType.remote.endpoints[].headers[]` - every HTTP - header entry for remote MCPs. Each has a `name` and an - `mcpInput.mcpInputDetails` object with `description`, `isRequired`, - `isSecret`. Keep all of them, including `isRequired=false`. +- `spec.packageName` — exact package name for the config. +- `spec.mcpServerType.local.bootParams.environmentVariables[]` for + local MCPs (each has `name`, `description`, `isRequired`, `isSecret`). +- `spec.mcpServerType.remote.endpoints[].headers[]` for remote MCPs + (each has `name` plus `mcpInput.mcpInputDetails` with the same + fields). -If the command exits non-zero (MCP not found, network error, bad -credentials), show the error message to the user and then run -`--list-available` (see below) to offer the valid alternatives. +On non-zero exit (typo, MCP not in catalog, network error, etc.), +show the error verbatim, then run `--list-available` (see "Listing +MCPs") so the user can pick a valid name and retry. If the failure is +because the MCP is **not allowed in the current project**, say so +conversationally and name the project — do NOT retry against a +different project. -**If the user did NOT specify a name** (e.g. "what can I install?"), -run `--list-available` instead (see "Listing MCPs" below). +If the user gave a name that is NOT in the catalog (typo, near-miss), +do NOT auto-install or guess. Run `--list-available`, present the +nearest matching names, and ask the user to confirm which one they +meant before proceeding. ### Step 3: Plan inputs -Take the inputs you collected in Step 2 and split them into two -groups by `isRequired`. You will NOT ask the user for the *values* -here - VS Code will prompt for those the first time the server -starts, using its native secure-input mechanism (values are stored -in the OS keychain, never in the file). - -1. **Required inputs** (`isRequired=true`) - always include them in - Step 4. Record `name`, `description`, and `isSecret`. -2. **Optional inputs** (`isRequired=false`) - if Step 2 returned - even ONE optional input, you MUST stop and ask the user before - continuing to Step 4. The message you send the user should: - - First list each REQUIRED input (so the user knows what will - be added without asking). - - Then list each OPTIONAL input by name, with its description, - and ask which (if any) they want to configure. - - Wait for the user's answer. - - Do NOT skip this question. Do NOT include optional inputs by - default. Do NOT decide on the user's behalf. Continue to Step 4 - only after the user answers, and include exactly the inputs they +You will NOT collect the input *values* here. VS Code prompts for them +the first time the server starts, using its native secure-input +mechanism, and stores them in the OS keychain (never in the file). +Step 3 only decides which inputs go into the config. + +Split Step 2 inputs by `isRequired`: + +1. **Required** (`isRequired=true`) — always include in Step 4. Record + `name`, `description`, and `isSecret`. +2. **Optional** (`isRequired=false`) — if even ONE exists, STOP and + ask. First list each required input (informational, so the user + knows what will be added without being asked), then list each + optional input by name + description and ask which (if any) they + want to configure. Do NOT skip this question, do NOT include + optional inputs by default, do NOT decide for the user. Continue to + Step 4 only after they answer, including exactly the inputs they opted into. -3. If Step 2 returned no inputs at all (neither required nor - optional), skip the `inputs` block within Step 4. +3. No inputs at all → skip the `inputs` block in Step 4. ### Step 4: Write the config entry -Add the entry to `.vscode/mcp.json` under `servers`, and declare every -required input under a top-level `inputs` array. **Secrets MUST use -`${input:...}` substitution - never write a raw secret value into the +Add the entry under `servers` in the target config (default +`.vscode/mcp.json` — see Step 1), and declare every input you are +configuring under the top-level `inputs` array. **Secrets MUST use +`${input:...}` substitution — never write a raw secret value into the JSON file.** +**Both `--yes` and `--registry ` MUST come BEFORE +`@jfrog/agent-guard`** or `npx` falls back to the default +registry (404) and may block on a no-TTY prompt. Use +`"type": "stdio"` — never `"http"`, `"sse"`, or a top-level `"url"` +(those bypass the agent guard). ```json { @@ -141,46 +239,56 @@ JSON file.** Rules for the `inputs` block: -- One entry per required env var / header from Step 3. +- One entry per env var / header you are configuring from Step 3. - `id` is a workspace-unique identifier in the form - `-`, all lowercase, words separated by + `-`, all lowercase, words separated by hyphens. Re-use the same `id` across servers only when the value truly is shared. - `type` is always `"promptString"`. -- `password: true` for secret inputs (catalog `isSecret=true`) - hides +- `password: true` for secret inputs (catalog `isSecret=true`) — hides the characters VS Code shows while typing and stores the value - encrypted. Omit `password` for non-secret values like URLs. -- `description` shows in the VS Code prompt - use the catalog's + encrypted. OMIT `password` for non-secret values like URLs or flags + (VS Code still prompts, but does not mask the typing). +- `description` shows in the VS Code prompt — use the catalog's `description` field. - Reference the input from `env` with `"${input:}"`. For HTTP headers with a `Bearer` prefix, either put the prefix in the description and ask the user to include it, or use `"Bearer ${input:}"` and ask only for the token. -For non-secret env vars (catalog `isSecret=false`), use an input -entry WITHOUT `password: true` so VS Code still prompts but doesn't -mask the typing. +VS Code substitutes every `${input:}` with the stored value before +handing the env to the process — so the agent guard sees the real +value, while the file on disk shows only the placeholder. -The loader reads these env vars at startup. VS Code substitutes every -`${input:}` with the stored value before handing the env to the -process - so the agent guard sees the real value, the file on disk shows -only the placeholder. +### 4a: Start and verify the entry (mandatory) -### Step 5: Authenticate OAuth MCPs (run automatically after Step 4) +Writing the entry to `.vscode/mcp.json` is not enough — the server has +to start and actually expose tools. -Run this step ONLY when both conditions hold: +1. VS Code detects the edited `mcp.json` and offers a **Start** action + (CodeLens above the server entry, or via `MCP: List Servers` → + select the server → **Start Server**). Start it. +2. On first start, VS Code prompts for any `${input:...}` values + (Step 3) using its native secure input and stores them in the OS + keychain. Required values must be supplied or the server fails. +3. **Verify (mandatory):** open `MCP: List Servers` and confirm the + server is **Running** AND exposes **at least one tool**. A server + shown as Running but reporting **0 tools** ("Discovered 0 tools") is + NOT healthy — the agent guard connected but the upstream MCP did + not come up, so no tools were exposed. NEVER report success when + there are 0 tools; treat 0 tools as Failed and follow Troubleshooting + "Running but 0 tools". -- The Step 4 entry has NO `${input:...}` references (no `inputs` - block was needed), AND -- The `--inspect` output had a `remote` section. +### Step 5: Authenticate OAuth MCPs (auto, after Step 4) -Otherwise (local-only MCP, or static-token MCP with `inputs`), skip -Step 5 entirely. +Run ONLY for OAuth-style remote MCPs — i.e. `--inspect` showed a +`remote` section AND Step 4 wrote no `${input:...}` auth header into +`env` (no static token). Skip for local MCPs and for remote MCPs whose +auth comes from a static token configured via `inputs`. -The agent guard's `--login` command opens the user's browser, runs the -OAuth flow, and caches the tokens in `~/.jfrog/jfrogmcp.conf.json`. -Tell the user "I'm going to open your browser to sign you in to -``" before running it: +`--login` opens the browser, runs OAuth, caches tokens in +`~/.jfrog/jfrogmcp.conf.json`. Warn the user "I'm going to open your +browser to sign you in to ``" before: ``` npx --yes \ @@ -192,101 +300,117 @@ npx --yes \ --mcp ``` -Outcomes: - -- Exits 0 - OAuth completed, tokens cached. Tell the user the - server is ready to start. -- Exits with `expected 401, got 200` - the MCP is anonymous, no - auth needed. Ignore the error; the server is ready to start. -- Any other error - paste it to the user verbatim and stop. - -## Troubleshooting - -### How to know a server actually failed - -VS Code labels MCP servers as Running, Stopped, or Failed in -`MCP: List Servers`. There is also a silent failure mode: - -- A server reporting **0 tools** (or **"Discovered 0 tools"**) while - shown as Running is NOT a healthy server with no tools - it means - the agent guard connected but the underlying MCP did not come up, so - no tools were exposed. Treat 0 tools the same as a Failed status. - -If the user says "the MCP isn't doing anything" or "tools aren't -showing up", check for both states before assuming the server is -working. - -### What to do - -1. **Previously-working OAuth MCP suddenly failing** - the cached - refresh token is likely dead. Re-run Step 5; the new tokens - overwrite the old ones. +Note: This may require full network access when run in a +restricted/sandboxed environment. -2. **Anything else** - ask the user to open `MCP: List Servers`, - right-click the failed (or 0-tools) server, choose **Show - Output**, and paste the last 50 lines. Read the output before - guessing at a cause. Common recoveries based on what the output - shows: +Outcomes: - - HTTP 401 / 403 / authentication error on a server with - `${input:...}` in its entry - the stored secret is wrong. Tell - the user to click the **Clear** CodeLens above the matching - `inputs` entry in `.vscode/mcp.json`, then restart the server; - VS Code will re-prompt for the secret. - - `Failed to refresh OAuth token` / `invalid_grant` / - `No such refresh token found` - re-run Step 5. - - Network / proxy / DNS error - outside the agent guard's scope; - tell the user and stop. +- **Exit 0** — OAuth completed; tokens cached; server ready. +- **`expected 401, got 200`** — MCP is anonymous (no auth needed); + ignore. +- **Any other error** — paste it to the user verbatim and stop. ## Removing an MCP -Delete the entry from `servers` in `.vscode/mcp.json` and any now-unused -entries from the top-level `inputs` array. +1. Delete the entry from `servers` in the file it was installed in + (`.vscode/mcp.json` or the VS Code user-level MCP config). +2. Delete any now-unused entries from the top-level `inputs` array — + leave NO orphaned input entries for the removed server. For remote + MCPs, also remove any HTTP header entries that were configured for + it. +3. If OAuth was used (Step 5), also remove its entry from + `~/.jfrog/jfrogmcp.conf.json` so cached login tokens are wiped. +4. Tell the user to reload (`Developer: Reload Window`) or restart the + server from `MCP: List Servers` so the removed entry stops loading + (the config is read at session start only). + +Before removing, check Managed Settings — if the target MCP is +protected, do NOT remove it (see "Managed Settings"). ## Listing MCPs -### Installed MCPs - -Read the `servers` entries from the VS Code MCP config file (workspace -`.vscode/mcp.json` or in the user profile settings) and list each entry -by display name, showing its package name (from `_JF_ARGS`) -and server ID. - -### Available MCPs (JFrog AI Catalog) - -1. Determine project and server ID using the same fallback chain as - "Adding an MCP -> Step 1": - - Try to extract from existing `_JF_ARGS` entries in - `.vscode/mcp.json`. - - If not found, check the `JF_PROJECT` environment variable for the - project. - - If still missing, read `~/.jfrog/jfrog-cli.conf.v6` via a terminal - command (NEVER via file-search/glob - hidden directories are - skipped) for available server IDs and ask the user to pick project - and server in a SINGLE message. -2. Run the agent guard with `--list-available`: +**Route the request first** — pick which subsection to run BEFORE +touching any file or shell: + +| User said… | Run | +| --- | --- | +| "available", "what can I install", "what's in the catalog", "list MCPs" without other context | **Available to install** below — go straight to `--list-available`; do NOT inspect local files first | +| "installed", "configured", "connected", "running", "what MCPs do I have" | **Currently installed** below | +| ambiguous / both | run **both** subsections in order: Currently installed first, then Available to install, and present them as separate tables | + +NEVER invent MCP integrations from outside the catalog. The only +authoritative source for what's available is `--list-available` +against the configured server + project. If that command returns +nothing or errors, say so — do not pad the answer with names from +elsewhere. If the project has zero allowed MCPs, say so conversationally +and name the project. + +### Currently installed + +1. Open `MCP: List Servers` for connection status (one row per + server: Running / Stopped / Failed). +2. For JFrog metadata, read `servers` directly from `.vscode/mcp.json` + (workspace) and the VS Code user-level MCP config — use the + file-read tool or a single `jq` invocation, NOT chained + `python3 -c "..."` pipes. For each entry whose `command` is `npx` + and whose `args` include `@jfrog/agent-guard`, show: display name + (the JSON key), package (`mcp=` in `_JF_ARGS`), server ID (value + after `--server`), scope (workspace / user), and its installed + status. Your output should structurally mirror the config. +3. If a configured entry does not appear in `MCP: List Servers`, it + was never started — re-run Step 4a. + +### Available to install + +1. Determine **server** and **project** per the Pre-flight rule at + the top of this document. `--list-available` does NOT require + any existing `servers` entry or pre-installed agent guard — + `npx --yes` fetches the agent guard on demand, so this works on a + fresh machine too. +2. Run EXACTLY this command — `--project` is passed as a CLI flag. + To configure the server, either use the serverId from a jf cli + config with `--server` or omit `--server` if env vars are used to + configure URL and Access Token. **no additional env vars needed**: ``` npx --yes \ --registry \ @jfrog/agent-guard \ --list-available \ - --server \ - --project + --project \ + [--server ] ``` The output is a compact TSV: a header line, then one server per line, tab-separated: `nametypeversiondescription`. Run the command ONCE and present the rows directly as a numbered -table - do NOT re-run it, redirect it, or parse it with `python3`/`jq`. +table — do NOT re-run it, redirect it, or parse it with `python3`/`jq`. The `name` column is the install identifier (the value you pass to `--inspect --mcp` and to install); `packageName` is NOT a separate -column - for remote/http MCPs there is no package name, so `name` is +column — for remote/http MCPs there is no package name, so `name` is the display name. -3. Compare each `name` against the `_JF_ARGS` values - already present in `.vscode/mcp.json` to mark each one as - "available to install" or "already installed". +3. Filter out any `name` already present in the installed list + (compare against `mcp=` in `_JF_ARGS`). Present the available + (not-yet-installed) MCPs, and also show which ones are already + installed so the user has the full picture. + +## Managed Settings + +Managed settings (`managed-settings.json`) can enforce +organization-level policy. When managed settings are in effect, they +override everything below and CANNOT be bypassed: + +- **Agent-guard-only installation is enforced.** If the user asks to + install an MCP by any other means (`npm install`, `pip`, `docker`, + hand-editing `.vscode/mcp.json` with a direct `url`/`http`/`sse` + entry, etc.), BLOCK it immediately, run nothing, and respond + conversationally that all MCP installations must go through the + JFrog Agent Guard. +- **Protected MCPs cannot be removed.** If the target of a removal is + protected by managed settings, BLOCK the uninstall immediately and + respond conversationally that the MCP is protected by managed + settings and cannot be removed. Do not edit any config file. ## Key Rules @@ -296,41 +420,69 @@ the display name. `@jfrog/agent-guard`. Capitalizing the brand (`@JFrog`) points at a different/nonexistent scope and breaks the command. Use the exact lowercase string in every command and config entry. -- **`npx` argument order (required):** `--yes`, `--registry `, - `@jfrog/agent-guard`, then the agent guard flags (`--inspect`, - `--login`, `--list-available`, or `--server ` for loader - mode). Both `--yes` and `--registry` MUST come BEFORE - `@jfrog/agent-guard` so `npx` picks them up; otherwise `npx` falls - back to the user's default registry (resolves to 404) and may - block on a confirmation prompt with no TTY. -- **OAuth login** uses `npx @jfrog/agent-guard --login` (Step 5). - Run it automatically after Step 4 for remote MCPs that have no - required headers, and again later if a previously-working OAuth - MCP starts failing with refresh errors. Never tell the user to - authenticate via the IDE's native OAuth dialog or by hand-editing - `~/.jfrog/jfrogmcp.conf.json`. -- `_JF_ARGS` MUST contain `project=&mcp=`. -- Package name MUST come from the catalog API. NEVER guess. +- **`npx` arg order:** `--yes`, `--registry `, + `@jfrog/agent-guard`, then agent guard flags. Both `--yes` and + `--registry` MUST precede the package name or `npx` falls back to + the default registry (404) and may block on a no-TTY prompt. +- **Always `"type": "stdio"`** pointing at `npx @jfrog/agent-guard`, + even for remote-only catalog MCPs (the agent guard proxies them). + `"http"`, `"sse"`, or a top-level `"url"` bypass the agent guard and + trigger VS Code's native remote-MCP OAuth dialog instead of using the + configured `${input:...}` secret. +- `_JF_ARGS` is **only** for the entry VS Code launches at session + start (Step 4's `servers.*.env`); MUST contain + `project=&mcp=`. NEVER pass `_JF_ARGS` to + `--list-available`, `--inspect`, or `--login` — those take + `--server` / `--project` as CLI flags only. +- NEVER assume `default` as a project name. If the project is unknown + after Step 1's chain (existing `servers` entries → `JF_PROJECT` + env var), STOP and ask the user. Same for server ID if used. + NEVER invent or guess projects or server IDs. +- Package name MUST come from the catalog (`--inspect` / + `--list-available`). NEVER guess. NEVER install MCPs outside the + agent guard. NEVER use Fetch/WebFetch for catalog calls. - NEVER pipe a catalog command through `python3`, and NEVER capture it - with `2>&1` - `npx`/`npm` writes progress to stderr, which corrupts + with `2>&1` — `npx`/`npm` writes progress to stderr, which corrupts the output stream. For `--list-available` present the compact TSV it prints; for `--inspect` read the JSON it prints on stdout directly (or with a single `jq` filter), never via `python3`. -- NEVER install MCPs directly via `npx`/`pip`/`docker` - always use the - agent guard pattern above. -- NEVER write `"type": "sse"`, `"type": "http"`, or a top-level `"url"` - field in `.vscode/mcp.json`. Every server entry is `"type": "stdio"` - pointing at `npx @jfrog/agent-guard`, even when the catalog MCP is - remote-only - the agent guard proxies remote transports for you. Writing - `sse`/`http`/`url` bypasses the agent guard and triggers VS Code's - native remote-MCP OAuth dialog instead of using the configured - `${input:...}` secret. -- NEVER use Fetch/WebFetch for API calls that require authentication. -- NEVER show access tokens or API keys in any output or message. -- NEVER ask for info you can find in existing config or in - `~/.jfrog/jfrog-cli.conf.v6` (macOS/Linux and Windows PowerShell) or - `%USERPROFILE%\.jfrog\jfrog-cli.conf.v6` (Windows CMD). Always read - this file via a terminal command - never via file-search or glob - tools, which skip hidden directories. -- NEVER try multiple servers - always ask the user to pick one. -- To list installed MCPs: read `.vscode/mcp.json` and show the servers. +- NEVER write a raw secret into `.vscode/mcp.json` — always use an + `${input:}` reference. NEVER show tokens / API keys. +- NEVER try multiple servers — ask the user to pick one. + +## Troubleshooting + +- **Running but 0 tools (`MCP: List Servers` shows the server Running + but it reports "Discovered 0 tools")** — agent guard proxy started, + upstream MCP did not. The Running label is misleading here. NEVER + report success when there are 0 tools. + 1. In `MCP: List Servers`, right-click the server and choose **Show + Output**; read the last ~50 lines of agent guard stderr before + guessing, then diagnose by MCP type: + - **OAuth (remote)** — re-run Step 5 (`--login`); refresh token + likely expired. + - **Static-token (remote)** — confirm the `${input:...}` value is + set (re-prompt via the **Clear** CodeLens, then restart) and the + token is still valid. + - **Local (stdio)** — check that the bundled binary actually + launched (agent guard stderr will show the spawn error). + 2. Verify that the mcp server is still allowed. + See "Listing MCPs > Available to install". +- **`.vscode/mcp.json` server missing from `MCP: List Servers`** — + never started, or a JSON parse failure (often an undefined + `${input:...}` id). Fix the config and re-run Step 4a. +- **HTTP 401 / 403 on a server with `${input:...}`** — the stored + secret is wrong. Tell the user to click the **Clear** CodeLens above + the matching `inputs` entry in `.vscode/mcp.json`, then restart the + server; VS Code re-prompts for the secret. +- **Agent Guard: `multiple/no JFrog server configured`** (the agent guard + cannot pick a JFrog server) — pass `--server ` (after + `jf c add `) OR export both `JFROG_URL` and + `JFROG_ACCESS_TOKEN` in the launching shell, then reload VS Code. +- **OAuth MCP failing / `invalid_grant` / `No such refresh token`** — + refresh token expired; re-run Step 5. +- **Network / proxy / DNS error** — outside the agent guard's scope; + tell the user and stop. +- **npx package fetch returns 403 in-agent** — often a network + sandbox/egress policy. Run with full network access. If it still + fails, troubleshoot registry/auth/package/curation policy as usual. From 5f4db9a3648d7441d02eb1a42ed44fa429f8cfde Mon Sep 17 00:00:00 2001 From: Matan Eden <57892946+MatanEden1@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:46:02 +0300 Subject: [PATCH 2/9] AX-1694 - Apply review feedback: generalize installed-state read, mirror project-switch priority into Pre-flight - "live execution" exemption now says "local config files" (covers both workspace .vscode/mcp.json and the user-level MCP config) instead of naming only the workspace file - mirror the explicit project-switch priority into the Pre-flight bullet so it also governs listing/inspecting, not just writes Co-Authored-By: Claude Opus 4.8 (1M context) --- plugin/templates/copilot-instructions.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/plugin/templates/copilot-instructions.md b/plugin/templates/copilot-instructions.md index 85fc9b5..ad14f13 100644 --- a/plugin/templates/copilot-instructions.md +++ b/plugin/templates/copilot-instructions.md @@ -22,13 +22,16 @@ is set. Otherwise use NEVER resolve the catalog locally; the catalog, headers, and required inputs change between prompts. (Applies to these catalog/registry fetches only — `--list-available` and `--inspect`; NOT `--login`, - which would re-open the OAuth browser, and NOT reading `.vscode/mcp.json` - for *installed* state.) + which would re-open the OAuth browser, and NOT reading local config + files for *installed* state.) - **`` is always mandatory.** Resolve via Step 1's project chain: existing `servers` entries (`_JF_ARGS` → `project=`) → - `JF_PROJECT` env var → ASK the user. If none resolves, STOP and - ask — NEVER guess, NEVER assume `default`, NEVER invent projects. + `JF_PROJECT` env var → ASK the user. Exception: when the user + EXPLICITLY asks to switch project or re-sync the configuration, the + project they name (or the current `JF_PROJECT`) takes priority over + existing `_JF_ARGS`. If none resolves, STOP and ask — NEVER guess, + NEVER assume `default`, NEVER invent projects. - **`` is auto-resolvable.** Resolve via Step 1's server chain: existing `servers` entries (value after `--server` in From e722a814ea9b3ceeb0d7835b463c48222b515095 Mon Sep 17 00:00:00 2001 From: Matan Eden <57892946+MatanEden1@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:53:00 +0300 Subject: [PATCH 3/9] AX-1694 - Restore type:"http" qualifier on Step 5 OAuth condition Regression check vs the Claude/Cursor sources found the only dropped canonical detail: both gate --login on a remote section *with type:"http"*. VS Code had generalized it to "a remote section", which could fire --login for non-http remotes. Restore the qualifier. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugin/templates/copilot-instructions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/templates/copilot-instructions.md b/plugin/templates/copilot-instructions.md index ad14f13..f5bb044 100644 --- a/plugin/templates/copilot-instructions.md +++ b/plugin/templates/copilot-instructions.md @@ -285,8 +285,8 @@ to start and actually expose tools. ### Step 5: Authenticate OAuth MCPs (auto, after Step 4) Run ONLY for OAuth-style remote MCPs — i.e. `--inspect` showed a -`remote` section AND Step 4 wrote no `${input:...}` auth header into -`env` (no static token). Skip for local MCPs and for remote MCPs whose +`remote` section with `type: "http"` AND Step 4 wrote no `${input:...}` +auth header into `env` (no static token). Skip for local MCPs and for remote MCPs whose auth comes from a static token configured via `inputs`. `--login` opens the browser, runs OAuth, caches tokens in From a033dca02d39b711e3c5c9509105caf74e44fe95 Mon Sep 17 00:00:00 2001 From: Matan Eden <57892946+MatanEden1@users.noreply.github.com> Date: Mon, 15 Jun 2026 21:22:41 +0300 Subject: [PATCH 4/9] Refine copilot instructions: clarify project switching and input handling --- plugin/templates/copilot-instructions.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/plugin/templates/copilot-instructions.md b/plugin/templates/copilot-instructions.md index f5bb044..363597e 100644 --- a/plugin/templates/copilot-instructions.md +++ b/plugin/templates/copilot-instructions.md @@ -118,12 +118,6 @@ are used: Do NOT pass `--server ` NEVER infer the project from other sources, NEVER make up projects, ALWAYS ask. -When the user EXPLICITLY asks to switch project or update/re-sync the -configuration (e.g. "update my MCP configuration" after changing -`JF_PROJECT`), the current `JF_PROJECT` (or the project they name) -takes priority over the existing `_JF_ARGS`: re-write the `project=` -value in every affected `servers` entry to match. - **Target config file** - **Default: `.vscode/mcp.json` in the workspace root.** Create it if @@ -250,11 +244,13 @@ Rules for the `inputs` block: - `type` is always `"promptString"`. - `password: true` for secret inputs (catalog `isSecret=true`) — hides the characters VS Code shows while typing and stores the value - encrypted. OMIT `password` for non-secret values like URLs or flags - (VS Code still prompts, but does not mask the typing). + encrypted. OMIT the `password` key entirely (never set it to `false`) + for non-secret values like URLs or flags (VS Code still prompts, but + does not mask the typing). - `description` shows in the VS Code prompt — use the catalog's - `description` field. -- Reference the input from `env` with `"${input:}"`. For HTTP + `description` field. If the catalog leaves the description as an empty + string `""`, construct a brief context-appropriate description instead. +- Reference the input from `env` with `"${input:}"`.For HTTP headers with a `Bearer` prefix, either put the prefix in the description and ask the user to include it, or use `"Bearer ${input:}"` and ask only for the token. From 06064233fee8e9a28d0d4867fbe147d07fc219a8 Mon Sep 17 00:00:00 2001 From: Matan Eden <57892946+MatanEden1@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:56:34 +0300 Subject: [PATCH 5/9] AX-1694 - Apply review feedback for cross-plugin alignment Address YoniMelki's review on PR #21: - Remove the VS-Code-only switch/re-sync Exception clause from the Pre-flight bullet (not present in Claude/Cursor). - Reword the broken server-resolution prose into a single clear sentence. - Fix two regressions in the Step 4 inputs rules: trailing whitespace and a missing space ("${input:}".For -> . For). - Remove the net-new Managed Settings section (and its cross-reference in Removing an MCP); not present in Claude/Cursor. Step 2's richer not-allowed/near-miss handling is kept intentionally (to be back-ported to Claude/Cursor for alignment). Co-Authored-By: Claude Opus 4.8 (1M context) --- plugin/templates/copilot-instructions.md | 40 +++++------------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/plugin/templates/copilot-instructions.md b/plugin/templates/copilot-instructions.md index 363597e..4cdfe41 100644 --- a/plugin/templates/copilot-instructions.md +++ b/plugin/templates/copilot-instructions.md @@ -27,11 +27,8 @@ is set. Otherwise use - **`` is always mandatory.** Resolve via Step 1's project chain: existing `servers` entries (`_JF_ARGS` → `project=`) → - `JF_PROJECT` env var → ASK the user. Exception: when the user - EXPLICITLY asks to switch project or re-sync the configuration, the - project they name (or the current `JF_PROJECT`) takes priority over - existing `_JF_ARGS`. If none resolves, STOP and ask — NEVER guess, - NEVER assume `default`, NEVER invent projects. + `JF_PROJECT` env var → ASK the user. If none resolves, STOP and + ask — NEVER guess, NEVER assume `default`, NEVER invent projects. - **`` is auto-resolvable.** Resolve via Step 1's server chain: existing `servers` entries (value after `--server` in @@ -104,11 +101,10 @@ unless absolutely necessary: ask the user to either run `jf c add ` or export `JFROG_URL` + `JFROG_ACCESS_TOKEN`, then retry. -NEVER try multiple servers — pick one. Once chosen, pass it -If a server from the jf cli configuration is supposed to be used: -Always explicitly as `--server ` in every agent guard invocation. -Otherwise, if environment variables for `JFROG_URL` and `JFROG_ACCESS_TOKEN` -are used: Do NOT pass `--server ` +NEVER try multiple servers — pick one. Once chosen: if using a jf CLI +server, always pass it explicitly as `--server ` in every agent +guard invocation; if using `JFROG_URL` + `JFROG_ACCESS_TOKEN` instead, +do NOT pass `--server `. **Project** @@ -248,9 +244,9 @@ Rules for the `inputs` block: for non-secret values like URLs or flags (VS Code still prompts, but does not mask the typing). - `description` shows in the VS Code prompt — use the catalog's - `description` field. If the catalog leaves the description as an empty + `description` field. If the catalog leaves the description as an empty string `""`, construct a brief context-appropriate description instead. -- Reference the input from `env` with `"${input:}"`.For HTTP +- Reference the input from `env` with `"${input:}"`. For HTTP headers with a `Bearer` prefix, either put the prefix in the description and ask the user to include it, or use `"Bearer ${input:}"` and ask only for the token. @@ -323,9 +319,6 @@ Outcomes: server from `MCP: List Servers` so the removed entry stops loading (the config is read at session start only). -Before removing, check Managed Settings — if the target MCP is -protected, do NOT remove it (see "Managed Settings"). - ## Listing MCPs **Route the request first** — pick which subsection to run BEFORE @@ -394,23 +387,6 @@ the display name. (not-yet-installed) MCPs, and also show which ones are already installed so the user has the full picture. -## Managed Settings - -Managed settings (`managed-settings.json`) can enforce -organization-level policy. When managed settings are in effect, they -override everything below and CANNOT be bypassed: - -- **Agent-guard-only installation is enforced.** If the user asks to - install an MCP by any other means (`npm install`, `pip`, `docker`, - hand-editing `.vscode/mcp.json` with a direct `url`/`http`/`sse` - entry, etc.), BLOCK it immediately, run nothing, and respond - conversationally that all MCP installations must go through the - JFrog Agent Guard. -- **Protected MCPs cannot be removed.** If the target of a removal is - protected by managed settings, BLOCK the uninstall immediately and - respond conversationally that the MCP is protected by managed - settings and cannot be removed. Do not edit any config file. - ## Key Rules - **Package scope is case-sensitive — ALWAYS write it lowercase as From 6a626ac343c03b474b8dcc702b01f4f6516d8989 Mon Sep 17 00:00:00 2001 From: Matan Eden <57892946+MatanEden1@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:42:40 +0300 Subject: [PATCH 6/9] AX-1694 - Replace platform-specific scripts with cross-platform Node.js injector Consolidate ensure-instructions.sh and ensure-instructions.ps1 into a single inject-instructions.mjs that works on all platforms. Co-Authored-By: Claude Sonnet 4.6 --- plugin/hooks/hooks.json | 13 +-- plugin/scripts/ensure-instructions.ps1 | 15 ---- plugin/scripts/ensure-instructions.sh | 16 ---- plugin/scripts/inject-instructions.mjs | 106 +++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 36 deletions(-) delete mode 100644 plugin/scripts/ensure-instructions.ps1 delete mode 100755 plugin/scripts/ensure-instructions.sh create mode 100644 plugin/scripts/inject-instructions.mjs diff --git a/plugin/hooks/hooks.json b/plugin/hooks/hooks.json index 08fc3f8..c303316 100644 --- a/plugin/hooks/hooks.json +++ b/plugin/hooks/hooks.json @@ -1,11 +1,14 @@ { "hooks": { - "sessionStart": [ + "SessionStart": [ { - "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/scripts/ensure-instructions.sh", - "windows": "powershell -ExecutionPolicy Bypass -File ${CLAUDE_PLUGIN_ROOT}/scripts/ensure-instructions.ps1" + "hooks": [ + { + "type": "command", + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/inject-instructions.mjs\"" + } + ] } ] } -} \ No newline at end of file +} diff --git a/plugin/scripts/ensure-instructions.ps1 b/plugin/scripts/ensure-instructions.ps1 deleted file mode 100644 index be25b0d..0000000 --- a/plugin/scripts/ensure-instructions.ps1 +++ /dev/null @@ -1,15 +0,0 @@ -$Target = ".github\copilot-instructions.md" -$Template = "$env:CLAUDE_PLUGIN_ROOT\templates\copilot-instructions.md" - -if (-not (Test-Path ".github")) { - New-Item -ItemType Directory -Path ".github" | Out-Null -} - -if (-not (Test-Path $Target)) { - Copy-Item $Template $Target - - $content = Get-Content $Template -Raw - $escaped = $content -replace '\\', '\\\\' -replace '"', '\"' -replace "`r`n", '\n' -replace "`n", '\n' - $notice = "JFrog MCP governance: .github/copilot-instructions.md installed by the JFrog plugin.`n`n" - Write-Output "{`"hookSpecificOutput`":{`"hookEventName`":`"SessionStart`",`"additionalContext`":`"$notice$escaped`"}}" -} diff --git a/plugin/scripts/ensure-instructions.sh b/plugin/scripts/ensure-instructions.sh deleted file mode 100755 index 01afeb5..0000000 --- a/plugin/scripts/ensure-instructions.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -TARGET=".github/copilot-instructions.md" -TEMPLATE="${CLAUDE_PLUGIN_ROOT}/templates/copilot-instructions.md" - -if [ ! -d ".github" ]; then - mkdir -p .github -fi - -if [ ! -f "$TARGET" ]; then - cp "$TEMPLATE" "$TARGET" - - CONTENT=$(sed 's/\\/\\\\/g; s/"/\\"/g' "$TEMPLATE" | awk '{printf "%s\\n", $0}') - NOTICE="JFrog MCP governance: .github/copilot-instructions.md installed by the JFrog plugin.\\n\\n" - - printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s%s"}}' "$NOTICE" "$CONTENT" -fi diff --git a/plugin/scripts/inject-instructions.mjs b/plugin/scripts/inject-instructions.mjs new file mode 100644 index 0000000..39981ab --- /dev/null +++ b/plugin/scripts/inject-instructions.mjs @@ -0,0 +1,106 @@ +#!/usr/bin/env node +// Copyright (c) JFrog Ltd. 2026 +// Licensed under the Apache License, Version 2.0 + +import { readFileSync } from "node:fs"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +// Logs go to stderr; stdout is reserved for the hook JSON payload. +const debugEnabled = process.env.JF_AGENT_GUARD_DEBUG === "true"; +const log = (message) => console.error(`[jfrog-agent-guard] ${message}`); +const debug = (message) => { + if (debugEnabled) log(message); +}; + +// New JFROG_* env vars take precedence over the legacy JF_* names. +const env = (newName, oldName) => + process.env[newName] ?? process.env[oldName]; + +const forceDisabled = + env("_JF_AGENT_GUARD_FORCE_DISABLE", "_JF_MCP_GATEWAY_FORCE_DISABLE") === "true"; +const forceEnabled = + env("JF_AGENT_GUARD_FORCE_ENABLE", "JF_MCP_GATEWAY_FORCE_ENABLE") === "true"; + +async function isGatewayEnabledViaSettings() { + const baseUrl = env("JFROG_URL", "JF_URL"); + const token = env("JFROG_ACCESS_TOKEN", "JF_ACCESS_TOKEN"); + if (!baseUrl) { + debug("JFROG_URL/JF_URL is not set; skipping settings check"); + return false; + } + if (!token) { + debug("JFROG_ACCESS_TOKEN/JF_ACCESS_TOKEN is not set; skipping settings check"); + return false; + } + + const url = + baseUrl.replace(/\/+$/, "") + + "/ml/core/api/v1/administration/account-settings/mcp_gateway_plugin_enabled"; + + debug(`Fetching gateway setting from ${url}`); + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + try { + const response = await fetch(url, { + method: "GET", + headers: { + Accept: "application/json", + Authorization: `Bearer ${token}`, + }, + signal: controller.signal, + }); + if (!response.ok) { + const body = await response.text().catch(() => ""); + debug(`Settings request returned HTTP ${response.status}; body: ${body || ""}`); + return false; + } + const data = await response.json(); + const enabled = data?.settings?.mcpGatewayPluginEnabled?.value === true; + debug(`Settings response indicates gateway enabled=${enabled}`); + return enabled; + } catch (error) { + const reason = error?.name === "AbortError" ? "timeout" : error?.message ?? "unknown error"; + debug(`Settings request failed: ${reason}`); + return false; + } finally { + clearTimeout(timeout); + } +} + +if (forceDisabled) { + debug("Force-disable flag is set."); + process.exit(0); +} else if (forceEnabled) { + debug("Force-enable flag is set."); +} else if (!(await isGatewayEnabledViaSettings())) { + debug("Gateway not enabled; exiting without injecting instructions"); + process.exit(0); +} +debug("Injecting instructions"); + +// Derive the plugin root from this script's own location instead of relying +// on CLAUDE_PLUGIN_ROOT, which Claude Code interpolates into the hook command +// string but does not always export to the subprocess. +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); + +let template; +try { + template = readFileSync( + path.join(root, "templates", "jfrog-mcp-management.md"), + "utf8", + ); +} catch { + process.exit(0); +} + +process.stdout.write( + JSON.stringify({ + hookSpecificOutput: { + hookEventName: "SessionStart", + additionalContext: template, + }, + }), +); From 3585b81d7d98836b2e12d5c41c61135ffce7decc Mon Sep 17 00:00:00 2001 From: Matan Eden <57892946+MatanEden1@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:02:58 +0300 Subject: [PATCH 7/9] AX-1694 - Implement full authentication precedence in inject-instructions Add resolveCredentials() with strict priority chain: --server flag, env vars, CLI config default profile, and single-profile fallback. Co-Authored-By: Claude Sonnet 4.6 --- plugin/scripts/inject-instructions.mjs | 92 ++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 7 deletions(-) diff --git a/plugin/scripts/inject-instructions.mjs b/plugin/scripts/inject-instructions.mjs index 39981ab..5d0da66 100644 --- a/plugin/scripts/inject-instructions.mjs +++ b/plugin/scripts/inject-instructions.mjs @@ -3,6 +3,7 @@ // Licensed under the Apache License, Version 2.0 import { readFileSync } from "node:fs"; +import os from "node:os"; import path from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; @@ -23,18 +24,95 @@ const forceDisabled = const forceEnabled = env("JF_AGENT_GUARD_FORCE_ENABLE", "JF_MCP_GATEWAY_FORCE_ENABLE") === "true"; -async function isGatewayEnabledViaSettings() { +/** + * Parses process arguments to extract the value of the `--server` flag. + * Supports both `--server=my-id` and `--server my-id`. + */ +function getServerFlagValue() { + const args = process.argv; + for (let i = 0; i < args.length; i++) { + if (args[i].startsWith("--server=")) { + return args[i].split("=")[1]; + } + if (args[i] === "--server" && i + 1 < args.length) { + return args[i + 1]; + } + } + return null; +} + +/** + * Resolve {baseUrl, token} following strict authentication precedence: + * 1. The --server flag (matched against profiles in the JF CLI config) + * 2. Environment variables (JFROG_URL/JF_URL and JFROG_ACCESS_TOKEN/JF_ACCESS_TOKEN) + * 3. Configuration File created by the JF CLI (~/.jfrog/jfrog-cli.conf.v6) + * a. Profile marked isDefault: true + * b. The only profile that exists (if exactly one is defined) + */ +function resolveCredentials() { + // Read and parse JF CLI config safely, as multiple layers depend on it + const confPath = path.join(os.homedir(), ".jfrog", "jfrog-cli.conf.v6"); + let conf = null; + try { + conf = JSON.parse(readFileSync(confPath, "utf8")); + } catch (error) { + debug(`Could not read or parse JF CLI config at ${confPath}: ${error.message}`); + } + + const servers = Array.isArray(conf?.servers) ? conf.servers.filter((s) => s.url && s.accessToken) : []; + + // Priority 1: --server flag + const serverFlagId = getServerFlagValue(); + if (serverFlagId) { + debug(`--server flag detected with value: "${serverFlagId}". Searching config...`); + const flaggedProfile = servers.find((s) => s.serverId === serverFlagId); + if (flaggedProfile) { + debug(`Resolved credentials via --server flag using profile: ${flaggedProfile.serverId}`); + return { baseUrl: flaggedProfile.url, token: flaggedProfile.accessToken }; + } + debug(`Warning: --server flag specified ID "${serverFlagId}" but no matching profile was found in config.`); + // Fall through to next authentication method + } + + // Priority 2: Environment variables const baseUrl = env("JFROG_URL", "JF_URL"); const token = env("JFROG_ACCESS_TOKEN", "JF_ACCESS_TOKEN"); - if (!baseUrl) { - debug("JFROG_URL/JF_URL is not set; skipping settings check"); - return false; + if (baseUrl && token) { + debug("Resolved credentials from environment variables"); + return { baseUrl, token }; } - if (!token) { - debug("JFROG_ACCESS_TOKEN/JF_ACCESS_TOKEN is not set; skipping settings check"); - return false; + + // If config file couldn't be loaded/parsed earlier, we can't proceed with priorities 3 & 4 + if (!conf || servers.length === 0) { + debug("No server profiles available via JF CLI config; authentication resolution failed."); + return null; } + // Priority 3: Default profile in config + let profile = servers.find((s) => s.isDefault); + if (profile) { + debug(`Resolved credentials using default profile: ${profile.serverId}`); + return { baseUrl: profile.url, token: profile.accessToken }; + } + + // Priority 4: The only profile that exists + if (servers.length === 1) { + profile = servers[0]; + debug(`Resolved credentials using the single available profile: ${profile.serverId}`); + return { baseUrl: profile.url, token: profile.accessToken }; + } + + debug("Authentication resolution failed: Multiple profiles exist but none are marked default."); + return null; +} + +async function isGatewayEnabledViaSettings() { + const credentials = resolveCredentials(); + if (!credentials) { + debug("No credentials resolved; skipping settings check"); + return false; + } + const { baseUrl, token } = credentials; const url = baseUrl.replace(/\/+$/, "") + "/ml/core/api/v1/administration/account-settings/mcp_gateway_plugin_enabled"; From e34438917361f5606b68311a052e0db3707beb1f Mon Sep 17 00:00:00 2001 From: Matan Eden <57892946+MatanEden1@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:47:25 +0300 Subject: [PATCH 8/9] AX-1694 - Apply review feedback to inject-instructions - Fix template filename to copilot-instructions.md (was injecting nothing) - Restore .github/copilot-instructions.md write for Copilot delivery - Align force-enable env vars with underscore-prefixed convention - Remove unreachable --server resolution branch and fix precedence docs - Lower gateway settings-check timeout from 5s to 3s Co-authored-by: Cursor --- plugin/scripts/inject-instructions.mjs | 76 ++++++++++++-------------- 1 file changed, 34 insertions(+), 42 deletions(-) diff --git a/plugin/scripts/inject-instructions.mjs b/plugin/scripts/inject-instructions.mjs index 5d0da66..589c623 100644 --- a/plugin/scripts/inject-instructions.mjs +++ b/plugin/scripts/inject-instructions.mjs @@ -2,7 +2,7 @@ // Copyright (c) JFrog Ltd. 2026 // Licensed under the Apache License, Version 2.0 -import { readFileSync } from "node:fs"; +import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import process from "node:process"; @@ -22,30 +22,12 @@ const env = (newName, oldName) => const forceDisabled = env("_JF_AGENT_GUARD_FORCE_DISABLE", "_JF_MCP_GATEWAY_FORCE_DISABLE") === "true"; const forceEnabled = - env("JF_AGENT_GUARD_FORCE_ENABLE", "JF_MCP_GATEWAY_FORCE_ENABLE") === "true"; - -/** - * Parses process arguments to extract the value of the `--server` flag. - * Supports both `--server=my-id` and `--server my-id`. - */ -function getServerFlagValue() { - const args = process.argv; - for (let i = 0; i < args.length; i++) { - if (args[i].startsWith("--server=")) { - return args[i].split("=")[1]; - } - if (args[i] === "--server" && i + 1 < args.length) { - return args[i + 1]; - } - } - return null; -} + env("_JF_AGENT_GUARD_FORCE_ENABLE", "_JF_MCP_GATEWAY_FORCE_ENABLE") === "true"; /** * Resolve {baseUrl, token} following strict authentication precedence: - * 1. The --server flag (matched against profiles in the JF CLI config) - * 2. Environment variables (JFROG_URL/JF_URL and JFROG_ACCESS_TOKEN/JF_ACCESS_TOKEN) - * 3. Configuration File created by the JF CLI (~/.jfrog/jfrog-cli.conf.v6) + * 1. Environment variables (JFROG_URL/JF_URL and JFROG_ACCESS_TOKEN/JF_ACCESS_TOKEN) + * 2. Configuration File created by the JF CLI (~/.jfrog/jfrog-cli.conf.v6) * a. Profile marked isDefault: true * b. The only profile that exists (if exactly one is defined) */ @@ -61,20 +43,7 @@ function resolveCredentials() { const servers = Array.isArray(conf?.servers) ? conf.servers.filter((s) => s.url && s.accessToken) : []; - // Priority 1: --server flag - const serverFlagId = getServerFlagValue(); - if (serverFlagId) { - debug(`--server flag detected with value: "${serverFlagId}". Searching config...`); - const flaggedProfile = servers.find((s) => s.serverId === serverFlagId); - if (flaggedProfile) { - debug(`Resolved credentials via --server flag using profile: ${flaggedProfile.serverId}`); - return { baseUrl: flaggedProfile.url, token: flaggedProfile.accessToken }; - } - debug(`Warning: --server flag specified ID "${serverFlagId}" but no matching profile was found in config.`); - // Fall through to next authentication method - } - - // Priority 2: Environment variables + // Priority 1: Environment variables const baseUrl = env("JFROG_URL", "JF_URL"); const token = env("JFROG_ACCESS_TOKEN", "JF_ACCESS_TOKEN"); if (baseUrl && token) { @@ -82,20 +51,20 @@ function resolveCredentials() { return { baseUrl, token }; } - // If config file couldn't be loaded/parsed earlier, we can't proceed with priorities 3 & 4 + // If config file couldn't be loaded/parsed earlier, we can't proceed with priorities 2.a & 2.b if (!conf || servers.length === 0) { debug("No server profiles available via JF CLI config; authentication resolution failed."); return null; } - // Priority 3: Default profile in config + // Priority 2.a: Default profile in config let profile = servers.find((s) => s.isDefault); if (profile) { debug(`Resolved credentials using default profile: ${profile.serverId}`); return { baseUrl: profile.url, token: profile.accessToken }; } - // Priority 4: The only profile that exists + // Priority 2.b: The only profile that exists if (servers.length === 1) { profile = servers[0]; debug(`Resolved credentials using the single available profile: ${profile.serverId}`); @@ -119,8 +88,11 @@ async function isGatewayEnabledViaSettings() { debug(`Fetching gateway setting from ${url}`); + // Cap the worst-case session-start delay when the JFrog server is slow or + // unreachable; the check fails closed on timeout. + const SETTINGS_TIMEOUT_MS = 3000; const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 5000); + const timeout = setTimeout(() => controller.abort(), SETTINGS_TIMEOUT_MS); try { const response = await fetch(url, { method: "GET", @@ -167,13 +139,33 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); let template; try { template = readFileSync( - path.join(root, "templates", "jfrog-mcp-management.md"), + path.join(root, "templates", "copilot-instructions.md"), "utf8", ); -} catch { +} catch (error) { + debug(`Could not read instructions template: ${error.message}`); process.exit(0); } +// Materialize the template into the workspace at .github/copilot-instructions.md, +// which is the file VS Code / GitHub Copilot actually reads. This mirrors the +// legacy ensure-instructions scripts and is the primary delivery path for +// Copilot; the additionalContext payload below additionally covers Claude Code +// sessions. Only write when absent so we never clobber a user-edited file. +try { + const targetDir = path.join(process.cwd(), ".github"); + const targetFile = path.join(targetDir, "copilot-instructions.md"); + if (!existsSync(targetFile)) { + mkdirSync(targetDir, { recursive: true }); + writeFileSync(targetFile, template, "utf8"); + debug(`Wrote instructions to ${targetFile}`); + } else { + debug(`Instructions already present at ${targetFile}; leaving as-is`); + } +} catch (error) { + debug(`Failed to write .github/copilot-instructions.md: ${error.message}`); +} + process.stdout.write( JSON.stringify({ hookSpecificOutput: { From 5cc4d1dc8e79d3be858ed32d0f179705af03a81c Mon Sep 17 00:00:00 2001 From: Matan Eden <57892946+MatanEden1@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:35:16 +0300 Subject: [PATCH 9/9] AX-1694 - Clarify --login requires full permissions, not just network --login launches the system browser and runs a local OAuth callback server, so a sandboxed run needs unrestricted permissions. Co-authored-by: Cursor --- plugin/templates/copilot-instructions.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugin/templates/copilot-instructions.md b/plugin/templates/copilot-instructions.md index 4cdfe41..f6c1988 100644 --- a/plugin/templates/copilot-instructions.md +++ b/plugin/templates/copilot-instructions.md @@ -295,8 +295,9 @@ npx --yes \ --mcp ``` -Note: This may require full network access when run in a -restricted/sandboxed environment. +Note: `--login` launches the system browser and runs a local OAuth +callback server, so in a restricted/sandboxed environment it needs full +(unrestricted) permissions — network access alone is not enough. Outcomes: