diff --git a/README.md b/README.md index d4ed663..74ad9c8 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ The JFrog plugin provides the following capabilities, grouped by component: | Component | Feature | Description | | --- | --- | --- | +| **MCP** | JFrog MCP server | Remote JFrog MCP server auto-attached to every session via `.mcp.json` at `${JFROG_URL}/mcp` (OAuth, no API keys). | | **Hook** | Agent Guard | Copilot manage MCPs through the JFrog Agent Guard. Through it you can discover, install, configure, update, and remove MCP servers from the JFrog AI Catalog approved for your project, and authenticate to remote HTTP MCPs via OAuth, API key, or bearer token. | --- @@ -82,7 +83,7 @@ VS Code opens, prompts you to install the plugin, and asks you to **Trust** the | Variable | Description | | --- | --- | -| `JFROG_URL` | Your JFrog platform URL, e.g. `https://mycompany.jfrog.io` | +| `JFROG_URL` | Your JFrog platform URL, e.g. `https://mycompany.jfrog.io` (no trailing `/`) | | `JFROG_ACCESS_TOKEN` | Your JFrog access token | ### 2. Configure the JFrog CLI diff --git a/marketplace.json b/marketplace.json index 655c1b2..b956269 100644 --- a/marketplace.json +++ b/marketplace.json @@ -9,7 +9,7 @@ { "name": "jfrog", "description": "JFrog Platform integration with MCP, security skills, and supply-chain best practices", - "version": "1.0.4", + "version": "1.0.5", "source": "plugin", "categories": ["security", "artifact-management", "supply-chain", "devops", "mcp", "mlops", "agent-guard", "ai-catalog"], "platforms": ["darwin", "linux", "windows"], diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 095ca0a..6c0de44 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "jfrog", "description": "JFrog Platform integration with MCP, security skills, and supply-chain best practices", - "version": "1.0.4", + "version": "1.0.5", "author": { "name": "JFrog", "url": "https://jfrog.com" }, "hooks": "hooks/hooks.json" } diff --git a/plugin/.mcp.json b/plugin/.mcp.json new file mode 100644 index 0000000..d7b7ef3 --- /dev/null +++ b/plugin/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "jfrog": { + "type": "http", + "url": "${JFROG_URL}/mcp" + } + } +} diff --git a/plugin/scripts/inject-instructions.mjs b/plugin/scripts/inject-instructions.mjs index 365f657..1539b52 100644 --- a/plugin/scripts/inject-instructions.mjs +++ b/plugin/scripts/inject-instructions.mjs @@ -11,7 +11,7 @@ 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 log = (message) => console.error(message); const debug = (message) => { if (debugEnabled) log(message); }; @@ -112,13 +112,40 @@ if (forceDisabled) { debug("Force-disable flag is set."); process.stdout.write("{}"); process.exit(0); -} else if (forceEnabled) { +} + +// Validate JFROG_URL early to surface misconfigurations before the MCP server +// attempts to connect and fails with a confusing DNS or double-slash error. +if (!process.env.JFROG_URL) { + const missingUrlWarning = "WARNING: JFROG_URL is not set. The JFrog MCP server will be unreachable — set JFROG_URL to your Artifactory base URL (e.g. https://mycompany.jfrog.io) and restart."; + log(missingUrlWarning); + process.stdout.write(JSON.stringify({ + hookSpecificOutput: { + hookEventName: "SessionStart", + additionalContext: `IMPORTANT: Immediately tell the user: "${missingUrlWarning}"`, + }, + })); + process.exit(0); +} else if (process.env.JFROG_URL.endsWith("/")) { + const trailingSlashWarning = "WARNING: JFROG_URL has a trailing slash. This produces a double-slash in the MCP URL and will silently fail — remove the trailing slash and restart."; + log(trailingSlashWarning); + process.stdout.write(JSON.stringify({ + hookSpecificOutput: { + hookEventName: "SessionStart", + additionalContext: `IMPORTANT: Immediately tell the user: "${trailingSlashWarning}"`, + }, + })); + process.exit(0); +} + +if (forceEnabled) { debug("Force-enable flag is set."); } else if (!(await isAgentGuardEnabledViaSettings())) { debug("Agent Guard not enabled; exiting without injecting instructions"); process.stdout.write("{}"); process.exit(0); } + debug("Injecting instructions"); const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); diff --git a/scripts/validate-hook-injector.mjs b/scripts/validate-hook-injector.mjs index 9c9c0e6..c30984b 100644 --- a/scripts/validate-hook-injector.mjs +++ b/scripts/validate-hook-injector.mjs @@ -140,7 +140,7 @@ function main() { section("Format (injected payload shape)"); let injectedContext; check("force-enable emits valid JSON with a SessionStart additionalContext", () => { - const stdout = runInjector({ JF_AGENT_GUARD_FORCE_ENABLE: "true" }); + const stdout = runInjector({ JF_AGENT_GUARD_FORCE_ENABLE: "true", JFROG_URL: "https://example.jfrog.io" }); if (!stdout.trim()) throw new Error("stdout was empty"); let payload; try {