Skip to content

Commit a42580a

Browse files
committed
fix rule injection
1 parent 685ba62 commit a42580a

4 files changed

Lines changed: 110 additions & 42 deletions

File tree

README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,19 @@ Skills are vendored from [jfrog/jfrog-skills](https://github.com/jfrog/jfrog-ski
1818

1919
## JFrog MCP Gateway integration
2020

21-
When the plugin is enabled, every Claude Code session starts with the rules in [`templates/jfrog-mcp-management.md`](templates/jfrog-mcp-management.md) injected into the model's context (via the [`additionalContext`](https://docs.anthropic.com/en/docs/claude-code/hooks#sessionstart) `SessionStart` hook output). **No instructions file is written to your repo** — the rules live only in the plugin and reach the model directly through the hook.
21+
When the plugin is enabled, every Claude Code session starts by asking the JFrog MCP Gateway whether the current tenant is entitled to the AI Catalog feature (`npx @jfrog/mcp-gateway --should-inject`). If the gateway returns **entitled**, the rules in [`templates/jfrog-mcp-management.md`](templates/jfrog-mcp-management.md) are injected into the model's context via the [`additionalContext`](https://docs.anthropic.com/en/docs/claude-code/hooks#sessionstart) `SessionStart` hook output. **No instructions file is written to your repo** — the rules live only in the plugin and reach the model directly through the hook.
22+
23+
The check fails closed: anything other than a clean "entitled" response (entitlement denied, no JFrog server resolvable, network/`npx` failure, timeout) skips the injection so the plugin stays inert when the gateway can't authoritatively say "yes".
24+
25+
You can short-circuit the check with the `JF_MCP_GATEWAY_FORCE_ENABLE` env var in the shell that launches Claude Code — this is mainly for tests, demos, and air-gapped setups:
26+
27+
| Value | Effect |
28+
| --- | --- |
29+
| `true` | Always inject the instructions; skip the entitlement check entirely. |
30+
| `false` | Never inject; skip the entitlement check entirely. |
31+
| unset / anything else | Run `--should-inject` and respect its decision (default). |
32+
33+
Set `JF_MCP_GATEWAY_DEBUG=1` to have the hook log its decision to stderr (visible in Claude Code's logs panel) for troubleshooting.
2234

2335
The rules tell Claude to:
2436

@@ -27,7 +39,9 @@ The rules tell Claude to:
2739
- Write the entry to **`.mcp.json` (project scope) by default** — creating the file if it doesn't exist — so the entry stays alongside the project and the team can share it via git. Only fall back to `~/.claude.json` (user scope) when you ask for it explicitly. Either way, secrets stay out of the file via `${ENV_VAR}` expansion (Claude Code has no native interactive secret prompt).
2840
- Run the gateway's `--login` automatically for OAuth-only remote MCPs.
2941

30-
After a new MCP entry is written, you must (1) **export every `${VAR}` referenced by the entry** in the shell that will launch Claude Code so the gateway has them at server-start time (an unset variable shows as `[Contains warnings]` in `/mcp` — informational only — and any tool call needing that value will fail at runtime), (2) **quit and relaunch Claude Code** in the same directory, and (3) **approve the server** at the `Approve MCP server <name> from .mcp.json?` prompt. Claude Code only reads `mcpServers` at session start, and `.mcp.json` entries require explicit per-project approval (stored under `projects.<cwd>.enabledMcpjsonServers` in `~/.claude.json`). The "Project MCPs (.../.mcp.json)" section in `/mcp` only appears once at least one server in the file is approved. Verify with `/mcp` or `claude mcp list`.
42+
After a new MCP entry is written, you must (1) **export every `${VAR}` referenced by the entry** in the shell that will launch Claude Code so the gateway has them at server-start time (an unset variable shows as `[Contains warnings]` in `/mcp` — informational only — and any tool call needing that value will fail at runtime), and (2) **quit and relaunch Claude Code** in the same directory. The first time you launch in a project, Claude Code prompts you both for workspace trust and once per `.mcp.json` server (`Approve MCP server <name> from .mcp.json?`); accept each one. On subsequent launches the `mcpServers` block loads silently. Verify with `/mcp` (under "Project MCPs (.../.mcp.json)") or `claude mcp list`.
43+
44+
Note: in Claude Code v2.1.x, per-project approve/reject decisions are persisted in **`<cwd>/.claude/settings.local.json`** (`enabledMcpjsonServers` / `disabledMcpjsonServers`), **not** in `~/.claude.json`. `claude mcp reset-project-choices` and deleting `projects.<cwd>` from `~/.claude.json` do **not** touch that file, so a previously-rejected `.mcp.json` server stays silently disabled and is never re-prompted. To re-enable it, edit `<cwd>/.claude/settings.local.json`, remove the entry from `disabledMcpjsonServers`, append it to `enabledMcpjsonServers`, and relaunch.
3145

3246
To **enforce** the gateway as the only allowed transport in a project, add an `allowedMcpServers` policy to `.claude/settings.json`:
3347

hooks/hooks.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
{
77
"type": "command",
88
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/inject-instructions.mjs",
9-
"timeout": 10
9+
"timeout": 30
1010
}
1111
]
1212
}

scripts/inject-instructions.mjs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,73 @@
44
// https://www.apache.org/licenses/LICENSE-2.0
55

66
import { readFileSync } from "node:fs";
7+
import { spawnSync } from "node:child_process";
78
import path from "node:path";
89
import process from "node:process";
910

11+
const debug = (msg) => {
12+
if (process.env.JF_MCP_GATEWAY_DEBUG) {
13+
process.stderr.write(`[jfrog-plugin] ${msg}\n`);
14+
}
15+
};
16+
1017
const root = process.env.CLAUDE_PLUGIN_ROOT;
1118
if (!root) {
1219
process.exit(0);
1320
}
1421

15-
const templatePath = path.join(root, "templates", "jfrog-mcp-management.md");
22+
function shouldInject() {
23+
const force = process.env.JF_MCP_GATEWAY_FORCE_ENABLE;
24+
if (force === "true") {
25+
debug("JF_MCP_GATEWAY_FORCE_ENABLE=true -> injecting (skip entitlement)");
26+
return true;
27+
}
28+
if (force === "false") {
29+
debug("JF_MCP_GATEWAY_FORCE_ENABLE=false -> skipping (skip entitlement)");
30+
return false;
31+
}
1632

33+
const registry =
34+
process.env.JFROG_MCP_GATEWAY_REPO ||
35+
"https://releases.jfrog.io/artifactory/api/npm/coding-agents-npm/";
36+
37+
debug(`spawning gateway --should-inject (registry=${registry})`);
38+
const result = spawnSync(
39+
"npx",
40+
[
41+
"--yes",
42+
"--registry",
43+
registry,
44+
"@jfrog/mcp-gateway",
45+
"--should-inject",
46+
],
47+
{
48+
encoding: "utf8",
49+
timeout: 25_000,
50+
shell: true,
51+
stdio: ["ignore", "pipe", "pipe"],
52+
},
53+
);
54+
55+
if (result.error) {
56+
debug(`spawn error: ${result.error.message} -> skipping`);
57+
return false;
58+
}
59+
if (result.status === 0) {
60+
debug("gateway: entitled -> injecting");
61+
return true;
62+
}
63+
debug(
64+
`gateway exit ${result.status} (3 = not entitled, other = error) -> skipping`,
65+
);
66+
return false;
67+
}
68+
69+
if (!shouldInject()) {
70+
process.exit(0);
71+
}
72+
73+
const templatePath = path.join(root, "templates", "jfrog-mcp-management.md");
1774
let template;
1875
try {
1976
template = readFileSync(templatePath, "utf8");

templates/jfrog-mcp-management.md

Lines changed: 35 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,12 @@ Split Step 2 inputs by `isRequired`:
9292

9393
For each input in Step 4:
9494

95-
- **Secrets** (`isSecret=true`, tokens, keys, passwords): use
96-
`${VAR_NAME}` in the config; tell the user to export it via
97-
`read -rs VAR_NAME && export VAR_NAME && echo exported` and to add
98-
the equivalent `export` to `~/.zshrc` for persistence. Values pick
99-
up on next launch (Step 4a). NEVER take secrets in chat, echo
100-
them back, or write raw values into config.
101-
- **Non-secrets** (URLs, regions): literal in `env` or `${VAR_NAME}`
102-
— ask if unclear.
95+
- **Secrets** (`isSecret=true`): use `${VAR_NAME}` in the config; tell
96+
the user to export it via `read -rs VAR_NAME && export VAR_NAME &&
97+
echo exported` (and add to `~/.zshrc` for persistence). Picked up on
98+
next launch (Step 4a). NEVER take secrets in chat, echo them back,
99+
or write raw values into config.
100+
- **Non-secrets**: literal in `env` or `${VAR_NAME}` — ask if unclear.
103101

104102
### Step 4: Write the config entry
105103

@@ -140,25 +138,30 @@ Notes:
140138
- For `Bearer`-prefixed headers, either include the prefix in the env
141139
var or hard-code it: `"Bearer ${TOKEN}"`.
142140

143-
### Step 4a: Activate the entry (mandatory)
141+
### Step 4a: Pre-approve and activate (mandatory)
144142

145-
Writing the file is NOT enough. After Step 4, ALWAYS tell the user:
143+
First, try to pre-approve the entry so the per-server prompt is
144+
skipped: edit `<cwd>/.claude/settings.local.json` (create as `{}` if
145+
missing), remove `<spec.packageName>` from `disabledMcpjsonServers`,
146+
append it to `enabledMcpjsonServers`. If that write fails for any
147+
reason (permissions, missing dir), continue anyway — the user will
148+
just see the prompt on relaunch (step 3).
146149

147-
1. Export every `${VAR}` from the new entry in the launching shell —
148-
the gateway needs them at server-start. Unset vars show as
149-
`[Contains warnings]` in `/mcp` (informational, does NOT hide the
150-
section) and any tool call needing them will fail at runtime.
151-
2. `/exit` and re-run the same `claude` command in the same directory.
152-
3. Approve at the "Approve MCP server `<name>` from .mcp.json?"
153-
prompt — saved to `projects.<cwd>.enabledMcpjsonServers`. The
154-
"Project MCPs (...)" section in `/mcp` only appears once at least
155-
one server in the file is approved.
156-
4. Verify with `/mcp` or `claude mcp list` — must show `✓ connected`.
150+
Then tell the user:
157151

158-
If the prompt is skipped or a reject is cached, run
159-
`claude mcp reset-project-choices` and relaunch. Last resort: edit
160-
`projects.<cwd>.enabledMcpjsonServers` directly. NEVER call the
161-
install "done" without `✓ connected`.
152+
1. Export every `${VAR}` from the new entry in the launching shell.
153+
Unset vars show as `[Contains warnings]` in `/mcp` (informational)
154+
and tool calls needing them will fail at runtime.
155+
2. `/exit` and relaunch the same `claude` in the same directory.
156+
3. On the FIRST launch, Claude Code prompts for workspace trust —
157+
accept. If pre-approval succeeded, the per-server prompt is
158+
skipped; otherwise approve "Approve MCP server `<name>`?".
159+
4. Verify with `/mcp` ("Project MCPs (...)", `✓ connected`) or
160+
`claude mcp list`. NEVER call it done without `✓ connected`.
161+
162+
Rejections persist in the same `enabledMcpjsonServers` /
163+
`disabledMcpjsonServers` arrays — `reset-project-choices` does NOT
164+
clear them. Fix: move the entry to `enabledMcpjsonServers`, relaunch.
162165

163166
### Step 5: Authenticate OAuth MCPs (auto, after Step 4)
164167

@@ -228,16 +231,13 @@ Output is a JSON array; each element has `name`, `packageName`,
228231

229232
## Key Rules
230233

231-
- **`npx` argument order:** `--yes`, `--registry <URL>`,
232-
`@jfrog/mcp-gateway`, then gateway flags. `--yes` and `--registry`
233-
MUST precede `@jfrog/mcp-gateway` or `npx` falls back to the
234-
default registry (404) and may block on a no-TTY prompt.
235-
- **Loader mode is the default** — do NOT add `--loader`.
236-
- **Always `"type": "stdio"`** — never write `"type": "http"`,
237-
`"type": "sse"`, or a top-level `"url"` field. Every entry is stdio
238-
pointing at `npx @jfrog/mcp-gateway`, even when the catalog MCP is
239-
remote-only — the gateway proxies remote transports for you.
240-
Anything else bypasses the gateway and the governance it provides.
234+
- **`npx` arg order:** `--yes`, `--registry <URL>`,
235+
`@jfrog/mcp-gateway`, then gateway flags. Both `--yes` and
236+
`--registry` MUST precede the package name or `npx` falls back to
237+
the default registry (404) and may block on a no-TTY prompt.
238+
- **Always `"type": "stdio"`** pointing at `npx @jfrog/mcp-gateway`,
239+
even for remote-only catalog MCPs (the gateway proxies them).
240+
`"http"`, `"sse"`, or a top-level `"url"` bypass the gateway.
241241
- `_JF_MCP_LOADER_ARGS` MUST contain `project=<NAME>&mcp=<PACKAGE_NAME>`.
242242
- Package name MUST come from the catalog (`--inspect` /
243243
`--list-available`). NEVER guess. NEVER install MCPs outside the
@@ -255,10 +255,7 @@ Output is a JSON array; each element has `name`, `packageName`,
255255
MCP did not. Treat as Failed: re-run Step 5 for OAuth MCPs, check
256256
the secret env var for static-token MCPs, read gateway stderr in
257257
Claude Code's logs for local MCPs.
258-
- **Project section missing from `/mcp`** — no `.mcp.json` entries
259-
are approved yet. Approve on next launch, or edit
260-
`projects.<cwd>.enabledMcpjsonServers`. (`[Contains warnings]` is
261-
informational; it does not hide the section.)
258+
- **`.mcp.json` server missing from `/mcp`** — rejected. See Step 4a.
262259
- **Missing from `claude mcp list`** — JSON parse failure (often an
263260
undefined `${VAR}`), or a server-side `allowedMcpServers` policy in
264261
`.claude/settings.json` / managed settings filtering the entry.

0 commit comments

Comments
 (0)