Skip to content

feat: ship as a Claude Code plugin#114

Open
GuyNachshon wants to merge 2 commits intohuggingface:mainfrom
GuyNachshon:claude-code-plugin
Open

feat: ship as a Claude Code plugin#114
GuyNachshon wants to merge 2 commits intohuggingface:mainfrom
GuyNachshon:claude-code-plugin

Conversation

@GuyNachshon
Copy link
Copy Markdown

feat: ship as a Claude Code plugin

Depends on # (Claude Code project mode). This PR builds on those files.

Summary

Packages the project-mode files added in # as a redistributable Claude Code plugin. After this PR merges, anyone can install ml-intern into Claude Code with one command and use /ml-intern, /research, /finetune, etc. in any repository:

/plugin marketplace add huggingface/ml-intern
/plugin install ml-intern@ml-intern

Why a separate plugin/ dir?

Plugins must be self-contained — when a user installs, they get a directory in their ~/.claude/plugins/cache/, not a clone of this repo. The MCP server inside the plugin can't from agent.tools.* import ….

Two ways to solve this:

  1. Vendor a slim subset of agent/tools/ and agent/core/redact.py under plugin/lib/ml_intern_lib/. This PR ships option 1.
  2. Publish ml-intern-tools to PyPI as a clean sub-package and have the plugin depend on it. Cleaner long-term; requires extracting agent/tools/ with its own pyproject and changing how the standalone CLI imports tools. Happy to do as a follow-up if maintainers prefer.

I went with option 1 because it unblocks distribution today. Open to switching to option 2 — see the related issue for discussion.

How vendoring stays in sync

scripts/sync_plugin_vendored.py is idempotent. After editing agent/tools/* or agent/core/redact.py, run it and commit. The script:

  1. Copies agent/tools/*.py (minus research_tool, plan_tool, private_hf_repo_tools — first two replaced by Claude Code natives, third disabled upstream).
  2. Copies agent/core/redact.py.
  3. Rewrites imports: from agent.Xfrom ml_intern_lib.X.
  4. Verifies zero leftover agent.* imports.

A new GitHub Actions workflow (.github/workflows/plugin-vendor-sync.yml) runs the sync script on every PR that touches the source or the vendored copy and fails on diff. The plugin can never silently fall out of sync.

Plugin slim deps

The plugin's pyproject.toml deliberately drops litellm, boto3, fastapi, prompt-toolkit, rich — none are needed when Claude Code is the runtime. First-time install ~15s vs. ~60s for the full repo.

dependencies = [
    "mcp>=1.0.0",
    "huggingface-hub>=1.0.1",
    "httpx>=0.27.0",
    "requests>=2.33.0",
    "thefuzz>=0.22.1",
    "whoosh>=2.7.4",
    "beautifulsoup4>=4.12.0",
    "nbconvert>=7.16.6",
    "nbformat>=5.10.4",
]

Tools registered (15)

Local stdio MCP server (plugin/lib/mcp_server.py):
explore_hf_docs, fetch_hf_docs, hf_papers, hf_inspect_dataset, hf_jobs, hf_repo_files, hf_repo_git, github_find_examples, github_list_repos, github_read_file, plus sandbox bash/read/write/edit and sandbox_create.

Hosted HTTP MCP server: huggingface.co/mcp with Authorization: Bearer ${HF_TOKEN}.

These appear in Claude Code as mcp__ml-intern__ml-intern-tools__<name> (the plugin-prefixed form). Tool names match the standalone CLI exactly minus the prefix.

Test plan

  • cd plugin && uv sync resolves cleanly (~15s).
  • uv run python plugin/lib/mcp_server.py < /dev/null boots cleanly.
  • All 15 tools register via _build_registry().
  • Approval hook fail-safe behaviors verified inside plugin's venv.
  • scripts/sync_plugin_vendored.py is idempotent — running twice produces zero diff.
  • CI workflow plugin-vendor-sync.yml correctly fails when agent/tools/ is edited without re-running the script.
  • Standalone CLI + project mode (#PR1) + plugin all work, all share agent/tools/ source of truth.

What's in this PR

.claude-plugin/marketplace.json        # marketplace listing pointing at ./plugin
.github/workflows/plugin-vendor-sync.yml
README.md                              # add a /plugin install line + pointer
plugin/
├── .claude-plugin/plugin.json         # plugin manifest
├── .gitignore
├── .mcp.json
├── CLAUDE.md
├── README.md                          # plugin user docs
├── pyproject.toml                     # slim deps
├── agents/research.md
├── commands/{ml-intern,research,inspect-dataset,finetune,run-job}.md
├── hooks/{hooks.json, *.py}           # ports of project-mode hooks, plugin paths
└── lib/
    ├── mcp_server.py                  # MCP frontend
    └── ml_intern_lib/                 # vendored agent/tools + redact, no agent.* deps
        ├── redact.py
        ├── session_stub.py            # stand-in for agent.core.session.Event
        ├── tool_spec.py               # stand-in for agent.core.tools.ToolSpec
        ├── telemetry_stub.py          # no-op stand-in for agent.core.telemetry
        └── tools/*.py                 # 12 vendored modules
scripts/sync_plugin_vendored.py

Anticipated review feedback

  • "Vendoring is brittle." Acknowledged. The CI workflow makes drift impossible to merge silently. Long-term answer is the PyPI-package extraction (option 2 above) — happy to do that follow-up if you'd prefer.
  • "Why are there shim files (session_stub.py, tool_spec.py, telemetry_stub.py)?" A handful of vendored tools (jobs_tool, sandbox_tool) call session.send_event(Event(...)) for in-process telemetry and read session.hf_token. The MCP server has no Session — these shims swallow events and read tokens from env. Drop-in replacements; no behavior change.
  • "Plugin metadata says huggingface but I see guynachshon URLs in the fork." Fork URLs were development-only; this PR's branches use upstream URLs throughout. Verified with grep.
  • "Why drop plugin/uv.lock?" It gets generated on first install. Tracking it would create merge conflicts on every dependency change. Plugin install is fast enough (~15s) that lockfile reproducibility isn't critical — happy to add it back if maintainers prefer.

Marketplace listing

Once merged, this repo becomes a Claude Code marketplace. The .claude-plugin/marketplace.json at repo root is what /plugin marketplace add huggingface/ml-intern resolves. The single plugin entry points at ./plugin.

GuyNachshon and others added 2 commits April 25, 2026 17:12
Adds Claude Code as a second frontend alongside the standalone `ml-intern` CLI.
Both share the same tools under `agent/tools/` — no duplication, no changes to
agent runtime behavior.

What's new:

- `packages/mcp_server/server.py` — MCP server that re-exposes `agent/tools/*`
  to Claude Code via stdio. Uses `mcp.server.lowlevel.Server` to preserve the
  existing JSON schemas verbatim (FastMCP 3.x re-derives schemas from Python
  type hints, which would lose `oneOf`/operation-discriminated structures).
- `CLAUDE.md` — system prompt ported from `agent/prompts/system_prompt_v3.yaml`,
  with `plan_tool` → TodoWrite and the `research` tool → Task subagent
  substitutions noted.
- `.claude/agents/research.md` — research subagent (read-only HF tool subset).
- `.claude/commands/{ml-intern,research,inspect-dataset,finetune,run-job}.md`
- `.claude/hooks/`:
  - `pre_tool_use_approval.py` — port of `_needs_approval` from
    `agent/core/agent_loop.py`. Fail-safe on malformed input (forces a prompt
    rather than silently allowing).
  - `session_start_context.py` — injects HF username + local-mode banner.
  - `session_end_upload.py` — uploads transcripts to
    `smolagents/ml-intern-sessions` after running through `agent/core/redact.py`.
- `CLAUDE_CODE_GUIDE.md` — user docs.
- `.gitignore` — switch from blanket `.claude/` to specific exclusions
  (`.claude/settings.local.json`, `__pycache__`) so shared config is tracked
  but per-user overrides are not.

What's *not* changed:

- `agent/` is untouched. The standalone CLI still works exactly as before.
- No new Python deps. The MCP server uses `mcp` and `fastmcp` already in
  `pyproject.toml`.

Behavior parity vs. standalone CLI (env var → `Config` field):

  ML_INTERN_YOLO              → yolo_mode             (default 0)
  ML_INTERN_CONFIRM_CPU_JOBS  → confirm_cpu_jobs      (default 1)
  ML_INTERN_SAVE_SESSIONS     → save_sessions         (default 1)
  ML_INTERN_SESSION_REPO      → session_dataset_repo  (default smolagents/ml-intern-sessions)
  ML_INTERN_LOCAL_MODE        → --local               (default 0)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Packages the project-mode files added in the previous commit as a
redistributable Claude Code plugin. After merge, anyone can install
ml-intern into Claude Code with one command and use `/ml-intern`,
`/research`, `/finetune`, etc. in any repository:

  /plugin marketplace add huggingface/ml-intern
  /plugin install ml-intern@ml-intern

Why a separate `plugin/` dir and not just point Claude Code at this
repo's .claude/ directly:

Plugins must be self-contained — when a user installs, they get a
directory in their `~/.claude/plugins/cache/`, not a clone of this
repo. The MCP server inside the plugin can't `from agent.tools.*`.

Two ways to solve that. This PR ships option 1:

  1. Vendor a slim subset of agent/tools/ under plugin/lib/ml_intern_lib/
     (this PR — unblocks distribution today).
  2. Publish ml-intern-tools to PyPI and have the plugin depend on it
     (cleaner long-term, requires extracting agent/tools/ as a clean
     sub-package; happy to do as a follow-up if maintainers prefer).

How vendoring stays in sync:

`scripts/sync_plugin_vendored.py` is idempotent. After editing
`agent/tools/*` or `agent/core/redact.py`, run it and commit. The script:
  1. Copies tool files (minus research_tool, plan_tool,
     private_hf_repo_tools — first two replaced by Claude Code natives,
     third disabled upstream).
  2. Copies agent/core/redact.py.
  3. Rewrites imports `from agent.X` → `from ml_intern_lib.X`.
  4. Verifies zero leftover `agent.*` imports.

A new GitHub Actions workflow (`.github/workflows/plugin-vendor-sync.yml`)
runs the sync script on every PR that touches the source or the vendored
copy and fails on diff — guarantees the plugin can never silently fall
out of sync.

Plugin slim deps:

The plugin's pyproject.toml deliberately drops litellm, boto3, fastapi,
prompt-toolkit, rich — none are needed when Claude Code is the runtime.
First-time install ~15s vs ~60s for the full repo.

Tools registered (15):

  Local MCP server (plugin/lib/mcp_server.py):
    explore_hf_docs, fetch_hf_docs, hf_papers, hf_inspect_dataset,
    hf_jobs, hf_repo_files, hf_repo_git, github_find_examples,
    github_list_repos, github_read_file, plus sandbox bash/read/write/edit
    and sandbox_create.

  Hosted MCP server: huggingface.co/mcp (HTTP + Bearer ${HF_TOKEN}).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant