diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 8c748c0..5f8a1d4 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "jfrog", "displayName": "JFrog", "description": "Official JFrog plugin. Connect Claude Code to JFrog to manage, secure, and govern your software supply chain. Give agents the context to build secure, compliant software.", - "version": "0.2.8", + "version": "0.2.9", "author": { "name": "JFrog Ltd.", "email": "devrel@jfrog.com", diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..d7b7ef3 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "jfrog": { + "type": "http", + "url": "${JFROG_URL}/mcp" + } + } +} diff --git a/README.md b/README.md index 3d1d95f..69c042b 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,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). | | **Skill** | JFrog Platform | Interact with Artifactory repositories, builds, permissions, users, access tokens, projects, release bundles, and platform administration via the JFrog CLI and REST/GraphQL APIs. Also covers security audits, CVE lookups, and Advanced Security exposure queries. | | **Skill** | Package safety & download | Check whether npm, Maven, PyPI, Go, and other packages are safe, curated, or allowed, then download them through Artifactory remote caches or curation-aware package managers. | | **Hook** | Agent Guard | Claude manages MCPs through the JFrog Agent Guard. Through the Agent Guard 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. | @@ -53,7 +54,7 @@ claude --plugin-dir /path/to/claude-plugin | 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/scripts/inject-instructions.mjs b/scripts/inject-instructions.mjs index b4ca105..d17262a 100755 --- a/scripts/inject-instructions.mjs +++ b/scripts/inject-instructions.mjs @@ -9,7 +9,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); }; @@ -73,12 +73,39 @@ async function isAgentGuardEnabledViaSettings() { if (forceDisabled) { debug("Force-disable flag is set."); 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.exit(0); } + debug("Injecting instructions"); // Derive the plugin root from this script's own location instead of relying