Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{
"name": "jfrog",
"version": "0.1.0",
"description": "JFrog skills for Codex — interact with the JFrog Platform.",
"version": "0.1.1",
"description": "JFrog skills and the JFrog MCP server for Codex — interact with the JFrog Platform.",
"author": { "name": "JFrog", "email": "michaelto@jfrog.com" },
"license": "Apache-2.0",
"repository": "https://github.com/jfrog/codex-plugin",
"keywords": ["jfrog", "artifactory", "codex", "plugin", "skills"],
"keywords": ["jfrog", "artifactory", "codex", "plugin", "skills", "mcp"],
"skills": "./skills/",
"mcpServers": "./.mcp.json",
Comment thread
YoniMelki marked this conversation as resolved.
"interface": {
"displayName": "JFrog",
"shortDescription": "JFrog Platform skills for Codex",
Expand Down
5 changes: 5 additions & 0 deletions .mcp.json
Comment thread
YoniMelki marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"jfrog": {
"url": "https://<JFROG_PLATFORM_URL>/mcp"
}
}
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# JFrog Plugin for OpenAI Codex

Delivers the JFrog skills to OpenAI Codex. **Phase 1: skills only** (no MCP yet).
Delivers the JFrog skills **and the JFrog MCP server** to OpenAI Codex.

## Install (git marketplace)

Expand All @@ -10,11 +10,25 @@ Delivers the JFrog skills to OpenAI Codex. **Phase 1: skills only** (no MCP yet)
/reload-plugins
```

## MCP server

The plugin bundles the `jfrog` MCP server ([`.mcp.json`](.mcp.json)). After
installing, do two things:

1. **Set your host.** Find the install path with `codex plugin list` (the
`jfrog@codex-plugin` row) and edit `<PATH>/.mcp.json`. Replace
`<JFROG_PLATFORM_URL>` in the `url` with your full JFrog Platform host - e.g.
`mycompany.jfrog.io` (or your self-hosted / custom domain).
2. **Log in (OAuth).** Run `codex mcp login jfrog` and finish the browser
sign-in.

Restart Codex; `/mcp` now lists `jfrog` with its tools.

## Skills

- `jfrog` interact with the JFrog Platform (CLI, MCP, REST/GraphQL).
- `jfrog-ai-catalog-skills` discover, install, manage, and publish agent skills from the JFrog AI Catalog via `jf skills` and Agent Guard.
- `jfrog-package-safety-and-download` package safety checks and Artifactory-routed downloads.
- `jfrog` - interact with the JFrog Platform (CLI, MCP, REST/GraphQL).
- `jfrog-ai-catalog-skills` - discover, install, manage, and publish agent skills from the JFrog AI Catalog via `jf skills` and Agent Guard.
- `jfrog-package-safety-and-download` - package safety checks and Artifactory-routed downloads.
Comment thread
YoniMelki marked this conversation as resolved.

Skills are vendored from [`jfrog/jfrog-skills`](https://github.com/jfrog/jfrog-skills), pinned in `scripts/sync-skills-vendor.json`. Bump the pin and run `npm run sync-skills` to update. See [`VENDOR.md`](VENDOR.md) for the full picture.

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"name": "codex-plugin",
"version": "0.1.0",
"version": "0.1.1",
"private": true,
"type": "module",
"description": "JFrog skills plugin for OpenAI Codex (Phase 1: skills only).",
"description": "JFrog skills and MCP server plugin for OpenAI Codex.",
"license": "Apache-2.0",
"scripts": {
"sync-skills": "node scripts/sync-skills.mjs",
Expand Down
36 changes: 34 additions & 2 deletions scripts/validate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,30 @@ export function validateManifest(obj) {
if (obj?.skills != null && obj.skills !== './skills/') {
errors.push('plugin.json: "skills" must be "./skills/"');
}
if (obj?.mcpServers != null) errors.push('plugin.json: "mcpServers" must not be set in Phase 1');
if (obj?.mcpServers != null && obj.mcpServers !== './.mcp.json') {
errors.push('plugin.json: "mcpServers" must be "./.mcp.json"');
}
return errors;
}

// Validates the bundled MCP config referenced by plugin.json's "mcpServers".
// Accepts a direct server map ({ "<name>": {...} }) or a wrapped object
// ({ "mcp_servers": {...} }) — both forms Codex supports. Each server must
// declare a "url" (streamable HTTP) or a "command" (stdio).
export function validateMcp(obj) {
if (obj == null || typeof obj !== 'object' || Array.isArray(obj)) {
return ['.mcp.json: must be a JSON object'];
}
const map = obj.mcp_servers && typeof obj.mcp_servers === 'object' ? obj.mcp_servers : obj;
const names = Object.keys(map);
if (names.length === 0) return ['.mcp.json: no MCP servers defined'];
const errors = [];
for (const n of names) {
const s = map[n];
if (s == null || typeof s !== 'object' || (s.url == null && s.command == null)) {
errors.push(`.mcp.json: server "${n}" must set "url" or "command"`);
}
}
return errors;
}

Expand Down Expand Up @@ -82,7 +105,16 @@ function main() {
if (dirs.length === 0) errors.push('skills/: no skill directories found');
for (const d of dirs) errors.push(...validateSkillDir(skillsRoot, d));

errors.push(...validateManifest(JSON.parse(readFileSync(join(root, '.codex-plugin/plugin.json'), 'utf8'))));
const manifest = JSON.parse(readFileSync(join(root, '.codex-plugin/plugin.json'), 'utf8'));
errors.push(...validateManifest(manifest));
if (manifest?.mcpServers) {
const mcpPath = join(root, '.mcp.json');
if (!existsSync(mcpPath)) {
errors.push('.mcp.json: referenced by plugin.json but not found');
} else {
errors.push(...validateMcp(JSON.parse(readFileSync(mcpPath, 'utf8'))));
}
}
errors.push(...validateMarketplace(JSON.parse(readFileSync(join(root, '.agents/plugins/marketplace.json'), 'utf8'))));

if (errors.length) {
Expand Down
17 changes: 15 additions & 2 deletions scripts/validate.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
validateSkillDir,
validateManifest,
validateMarketplace,
validateMcp,
} from './validate.mjs';

function writeSkill(root, dir, body) {
Expand Down Expand Up @@ -58,18 +59,30 @@ test('validateSkillDir flags an empty block-scalar description', () => {
assert.ok(errors.some((e) => e.includes('description')));
});

test('validateManifest requires fields, the skills pointer, and forbids mcpServers (Phase 1)', () => {
test('validateManifest requires fields, the skills pointer, and accepts the mcpServers pointer', () => {
assert.deepEqual(
validateManifest({ name: 'jfrog', version: '0.1.0', description: 'd', skills: './skills/' }),
[]
);
assert.ok(validateManifest({ name: 'jfrog' }).some((e) => e.includes('version')));
assert.deepEqual(
validateManifest({ name: 'jfrog', version: '0.1.0', description: 'd', skills: './skills/', mcpServers: './.mcp.json' }),
[]
);
assert.ok(
validateManifest({ name: 'jfrog', version: '0.1.0', description: 'd', skills: './skills/', mcpServers: './.mcp.json' })
validateManifest({ name: 'jfrog', version: '0.1.0', description: 'd', skills: './skills/', mcpServers: './mcp.json' })
.some((e) => e.includes('mcpServers'))
);
});

test('validateMcp accepts direct and wrapped server maps, flags servers without url/command', () => {
assert.deepEqual(validateMcp({ jfrog: { url: 'https://x/mcp' } }), []);
assert.deepEqual(validateMcp({ mcp_servers: { jfrog: { url: 'https://x/mcp' } } }), []);
assert.deepEqual(validateMcp({ local: { command: 'node', args: ['s.js'] } }), []);
assert.ok(validateMcp({}).some((e) => e.includes('no MCP servers')));
assert.ok(validateMcp({ jfrog: {} }).some((e) => e.includes('url') && e.includes('command')));
});

test('validateMarketplace requires a local source with a ./ path', () => {
assert.deepEqual(
validateMarketplace({ name: 'm', plugins: [{ name: 'jfrog', source: { source: 'local', path: './' } }] }),
Expand Down
Loading