Skip to content
Open
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
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ Everything else is unchanged, which is worth knowing for two cases:

```
endpoints-submission-cli
├── check-submission Validate a submission folder (§9.1)
├── install-skill Install the Claude Code skill (see below)
├── runs
│ ├── list List all runs
│ ├── create Register a run from a local folder
Expand All @@ -145,6 +147,53 @@ Use `--help` on any command for full flag details:
endpoints-submission-cli submissions create --help
```

## Using with Claude

Two ways to let Claude drive the CLI. Both ship in the package.

**Claude Code skill.** Teaches Claude the commands, to use `--json`, and to confirm
every write (upload, create, withdraw, delete) with you first. It runs the CLI through
Claude Code's shell:

```bash
endpoints-submission-cli install-skill # ~/.claude/skills/mlperf-submissions/
endpoints-submission-cli install-skill --project # ./.claude/skills/ for this project only
```

The skill is versioned with the CLI. After upgrading, run `install-skill --force` to
replace the installed copy with the matching version. Without `--force` it refuses to
overwrite a copy you have edited.

**MCP server.** For any MCP client, including ones without a shell. Every command is
a tool:

| Tool | Command | Annotated |
|---|---|---|
| `check_submission` | `check-submission` | read-only |
| `list_runs`, `get_run` | `runs list`, `runs get` | read-only |
| `list_submissions`, `get_submission` | `submissions list`, `submissions get` | read-only |
| `download_run`, `download_submission` | `runs get` / `submissions get --download-to` | writes a local file |
| `install_skill` | `install-skill` | writes a local file |
| `create_run`, `pin_run`, `unpin_run` | `runs create`, `runs pin`, `runs unpin` | changes PRISM |
| `create_submission` | `submissions create` | changes PRISM |
| `update_submission`, `remove_run_from_submission` | `submissions update`, `submissions remove-run` | destructive |
| `withdraw_submission`, `delete_run` | `submissions withdraw`, `runs delete` | destructive, irreversible |

Clients use the annotations to decide what to confirm with you. `create_run` and
`create_submission` take `dry_run`. A provisional submission also needs
`confirm_public_provisional=true`, which stands in for the CLI's own yes/no prompt.
Auth comes from the server's environment, so no tool takes a token. A test fails if
the CLI gains a command or option that no tool reaches.

```bash
claude mcp add mlperf -e PRISM_USER_API_TOKEN=mlc_... \
-- uvx --from 'endpoints-submission-cli[mcp]' endpoints-submission-mcp
```

Or install the extra yourself with `pip install 'endpoints-submission-cli[mcp]'` and
run `endpoints-submission-mcp`, which serves over stdio. Each tool runs the installed
CLI with `--json`, so it returns exactly what the CLI prints.

---

# submission-checker
Expand Down
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies = [

[project.scripts]
endpoints-submission-cli = "endpoints_submission_cli.main:main"
endpoints-submission-mcp = "endpoints_submission_cli.mcp_server:main"

[project.optional-dependencies]
dev = [
Expand All @@ -29,12 +30,18 @@ dev = [
"ruff>=0.4",
"mypy>=1.10",
"types-PyYAML",
# So CI type-checks and tests the MCP server; users get it from the mcp extra.
"mcp>=2.0,<3",
]
docs = [
"sphinx>=7.0",
"sphinx-autodoc-typehints>=2.0",
"furo>=2024.1",
]
# The MCP server (endpoints-submission-mcp). Written against mcp 2.x's MCPServer.
mcp = [
"mcp>=2.0,<3",
]

[tool.setuptools_scm]

Expand All @@ -59,6 +66,7 @@ packages = [
# The published seed sets (§4.6) ship as data so a newly published set can be
# supplied without a checker release; see submission_checker.seed_sets.
submission_checker = ["data/*.yaml"]
endpoints_submission_cli = ["skills/*/SKILL.md"]

[tool.pytest.ini_options]
testpaths = ["tests"]
Expand Down
7 changes: 7 additions & 0 deletions src/endpoints_submission_cli/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# SPDX-FileCopyrightText: Copyright (c) 2024 MLCommons
# SPDX-License-Identifier: Apache-2.0
"""``python -m endpoints_submission_cli`` — the same CLI as the console script."""

from .main import main

main()
66 changes: 66 additions & 0 deletions src/endpoints_submission_cli/commands/install_skill.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# SPDX-FileCopyrightText: Copyright (c) 2024 MLCommons
# SPDX-License-Identifier: Apache-2.0
"""``install-skill`` command — copy the bundled Claude skill where Claude Code finds it.

The skill ships inside the package so that it is versioned with the CLI it describes.
``pip install`` cannot place it in ``~/.claude/skills/``, so this command does, and is
re-run after an upgrade to pick up the matching version.
"""

from __future__ import annotations

from importlib.resources import files
from pathlib import Path

import click

__all__ = ["SKILL_NAME", "install_skill"]

#: The bundled skill's directory name, which is also its ``name`` in SKILL.md.
SKILL_NAME = "mlperf-submissions"


def _bundled_skill() -> str:
return (files("endpoints_submission_cli") / "skills" / SKILL_NAME / "SKILL.md").read_text(
encoding="utf-8"
)


@click.command("install-skill")
@click.option(
"--project",
is_flag=True,
help="Install into ./.claude/skills/ (this project) instead of ~/.claude/skills/.",
)
@click.option(
"--dest",
type=click.Path(file_okay=False, path_type=Path),
help="Install into this skills directory instead.",
)
@click.option(
"--force",
is_flag=True,
help="Replace an installed copy that differs from this version's.",
)
def install_skill(project: bool, dest: Path | None, force: bool) -> None:
"""Install the mlperf-submissions skill for Claude Code."""
if dest is not None and project:
raise click.UsageError("--dest and --project are mutually exclusive.")
if dest is None:
dest = (Path.cwd() if project else Path.home()) / ".claude" / "skills"

target = dest / SKILL_NAME / "SKILL.md"
content = _bundled_skill()
if target.exists():
if target.read_text(encoding="utf-8") == content:
click.echo(f"{target} is already up to date.")
return
if not force:
raise click.ClickException(
f"{target} exists and differs from this version's skill. Re-run with"
" --force to replace it; any local edits to it will be lost."
)

target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(content, encoding="utf-8")
click.echo(f"Installed the {SKILL_NAME} skill to {target}")
2 changes: 2 additions & 0 deletions src/endpoints_submission_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from ._version_check import register_upgrade_notice
from .commands.check_submission import check_submission
from .commands.install_skill import install_skill
from .commands.runs import runs
from .commands.submissions import submissions

Expand All @@ -21,6 +22,7 @@ def app() -> None:
app.add_command(runs)
app.add_command(submissions)
app.add_command(check_submission)
app.add_command(install_skill)


def main() -> None:
Expand Down
Loading
Loading