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..589c623 --- /dev/null +++ b/plugin/scripts/inject-instructions.mjs @@ -0,0 +1,176 @@ +#!/usr/bin/env node +// Copyright (c) JFrog Ltd. 2026 +// Licensed under the Apache License, Version 2.0 + +import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"; +import os from "node:os"; +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"; + +/** + * Resolve {baseUrl, token} following strict authentication precedence: + * 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) + */ +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: Environment variables + const baseUrl = env("JFROG_URL", "JF_URL"); + const token = env("JFROG_ACCESS_TOKEN", "JF_ACCESS_TOKEN"); + if (baseUrl && token) { + debug("Resolved credentials from environment variables"); + return { baseUrl, token }; + } + + // 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 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 2.b: 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"; + + 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(), SETTINGS_TIMEOUT_MS); + 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", "copilot-instructions.md"), + "utf8", + ); +} 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: { + hookEventName: "SessionStart", + additionalContext: template, + }, + }), +); diff --git a/plugin/templates/copilot-instructions.md b/plugin/templates/copilot-instructions.md index ebd361e..f6c1988 100644 --- a/plugin/templates/copilot-instructions.md +++ b/plugin/templates/copilot-instructions.md @@ -1,47 +1,138 @@ -# 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 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. + +- **`` 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: 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** + +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. + +**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 +144,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 +232,58 @@ 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 - `description` field. + 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. 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. -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 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`. -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 +295,98 @@ 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 +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. -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. - -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). ## 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. ## Key Rules @@ -296,41 +396,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.