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: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ A collection of [Model Context Protocol](https://modelcontextprotocol.io) server

## Install as a Claude Code plugin

This repo is also a plugin marketplace:
This repo is also a plugin marketplace. Run these as two separate commands, not as one paste: the first opens a prompt that expects only the `owner/repo`.

```
/plugin marketplace add walkerhughes/mcps
```

```
/plugin install harbor-mcp
```

Plugins require [`uv`](https://docs.astral.sh/uv/) on your PATH. See each server's README for credentials.
Plugins require [`uv`](https://docs.astral.sh/uv/) on your PATH. The first launch builds the server's environment, so give it a moment before the tools appear. See each server's README for credentials.

## Design

Expand Down
13 changes: 8 additions & 5 deletions harbor-mcp/.env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# This whole file is optional. If you have run `harbor auth login`, the server
# reads ~/.harbor/credentials.json on its own and needs nothing here.
# Variables for running the test suite and evals. The server does NOT read this
# file: authenticate with `harbor auth login` instead, which stores a key in
# ~/.harbor/credentials.json that harbor picks up on its own.
#
# Nothing sources this automatically. Export what you need, e.g.
# set -a && source .env && set +a

# Personal Harbor hub API key. Mint one with `harbor auth login` (it prints
# and stores the key), or copy it from ~/.harbor/credentials.json. Leaving it
# blank is fine: a blank value falls through to the credentials file.
# Overrides the stored credential. Intended for CI and scripting; interactively,
# prefer `harbor auth login` so the key stays scoped to you.
HARBOR_API_KEY=

# Write tools (upload_job, publish_task, delete_job, ...) refuse unless this
Expand Down
8 changes: 2 additions & 6 deletions harbor-mcp/.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@
"mcpServers": {
"harbor-hub": {
"type": "stdio",
"command": "uv",
"args": ["run", "--project", "${CLAUDE_PLUGIN_ROOT:-.}", "harbor-mcp"],
"env": {
"HARBOR_API_KEY": "${HARBOR_API_KEY:-}",
"HARBOR_MCP_ENABLE_WRITES": "${HARBOR_MCP_ENABLE_WRITES:-}"
}
"command": "bash",
"args": ["${CLAUDE_PLUGIN_ROOT:-.}/scripts/start-server.sh"]
}
}
}
34 changes: 21 additions & 13 deletions harbor-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,45 @@ This lives in the [`mcps`](../) monorepo, consolidated from a standalone repo wi

## Install as a Claude Code plugin

Two separate commands. The first opens a prompt that expects only the `owner/repo`, so do not paste both lines at once:

```
/plugin marketplace add walkerhughes/mcps
```

```
/plugin install harbor-mcp
```

Requires [`uv`](https://docs.astral.sh/uv/) on your PATH; the plugin builds its own environment from the checked-in `uv.lock` on first launch.
Requires [`uv`](https://docs.astral.sh/uv/) on your PATH; the plugin builds its own environment from the checked-in `uv.lock` on first launch, which takes a few seconds before the tools appear.

Then run `/harbor-mcp:setup`, which checks your credentials and walks you through whichever path you need.
Then run `/harbor-mcp:setup`. It checks your credentials, walks you through login if needed, and summarizes what is currently in your account.

## Credentials

The server resolves a key from the first source that has one:
Authenticate once:

| | Source | How |
|---|--------|-----|
| 1 | `HARBOR_API_KEY` in the environment | Export it in your shell |
| 2 | `.env` in your project root | `HARBOR_API_KEY=sk-harbor-...` (searched in the working directory and up to three parents) |
| 3 | `~/.harbor/credentials.json` | `harbor auth login` |
```bash
harbor auth login
```

That mints a key scoped to the logged-in user and stores it in `~/.harbor/credentials.json`, which harbor reads on its own. The plugin needs no further configuration, and no key ever goes into a config file or a `.env`.

So if you have already run `harbor auth login`, the plugin works with no configuration at all. A missing `.env` is normal, not an error. Nothing here ever writes your key back to disk or logs it, and `whoami` reports only the key *id* and its source.
You do not need a global harbor install: harbor ships inside the plugin's environment, so `uv run --project <plugin-dir> harbor auth login` works too, which is what `/harbor-mcp:setup` falls back to.

Verify with `whoami`. Restart the server after changing credentials.
`harbor auth status` shows what is stored, but it only reads that local file, so it still reports success for a key that has since been revoked. `whoami` is what confirms the credential is live; it returns the key *id* and source, never the key. Restart the server after changing credentials.

`HARBOR_API_KEY` also works as an override, since harbor itself checks it first. That is for CI and scripting; interactively, prefer `harbor auth login` so the key stays scoped to you.

## Local development

```bash
uv sync
cp .env.example .env # then set HARBOR_API_KEY
```

The same [`.mcp.json`](.mcp.json) serves both cases: it launches `uv run --project ${CLAUDE_PLUGIN_ROOT:-.}`, which resolves to the installed plugin directory when loaded as a plugin and to this directory when Claude Code is started from here.
The same [`.mcp.json`](.mcp.json) serves both cases: it runs [`scripts/start-server.sh`](scripts/start-server.sh) at `${CLAUDE_PLUGIN_ROOT:-.}`, which resolves to the installed plugin directory when loaded as a plugin and to this directory when Claude Code is started from here.

That wrapper exists for a reason worth knowing: naming `uv` directly as the command assumes it is on whatever PATH the MCP client spawns with, and it often is not. A Homebrew `uv` lives in `/opt/homebrew/bin`, which is missing from the minimal PATH some launch contexts provide, and the server then fails with an opaque JSON-RPC `-32000` and no explanation. The wrapper searches PATH plus the common install locations, resolves the plugin root from its own location, and prints an actionable message to stderr if `uv` genuinely is not installed.

## Tools

Expand All @@ -63,7 +71,7 @@ Read tools work with any valid `HARBOR_API_KEY`. Write tools are gated (see belo

## Write gating

Read-only is the default. Every write tool refuses unless `HARBOR_MCP_ENABLE_WRITES=true` in the server environment, which you set the same way as your key (shell export or `.env`), then restart the server. `delete_job` additionally requires `confirm=true` per call, which the agent should pass only after you explicitly approve deleting a specific job. This keeps read-only use the safe default.
Read-only is the default. Every write tool refuses unless `HARBOR_MCP_ENABLE_WRITES=true` in the server environment, so export it in your shell and restart the server. It is a local toggle, not a credential; the plugin passes it through explicitly. `delete_job` additionally requires `confirm=true` per call, which the agent should pass only after you explicitly approve deleting a specific job. This keeps read-only use the safe default.

## Testing

Expand Down
47 changes: 47 additions & 0 deletions harbor-mcp/scripts/start-server.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# Launch the harbor-hub MCP server.
#
# Exists because "command": "uv" in .mcp.json assumes uv is on whatever PATH the
# MCP client spawns with. It often is not: a Homebrew uv lives in
# /opt/homebrew/bin, which is missing from the minimal PATH some launch contexts
# provide, and the failure surfaces only as an opaque JSON-RPC -32000.
#
# Diagnostics go to stderr. stdout is the JSON-RPC channel and must stay clean.
set -euo pipefail

# Resolve the plugin root from this script's own location rather than
# CLAUDE_PLUGIN_ROOT, so it works however the server is launched.
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

find_uv() {
if command -v uv >/dev/null 2>&1; then
command -v uv
return
fi
# ponytail: fixed candidate list beats parsing shell profiles. Covers Homebrew
# on Apple Silicon and Intel, the standalone installer, and cargo install.
local candidate
for candidate in \
/opt/homebrew/bin/uv \
/usr/local/bin/uv \
"$HOME/.local/bin/uv" \
"$HOME/.cargo/bin/uv"
do
if [ -x "$candidate" ]; then
printf '%s\n' "$candidate"
return
fi
done
return 1
}

if ! UV="$(find_uv)"; then
echo "harbor-mcp: cannot find the 'uv' executable." >&2
echo " Searched PATH plus /opt/homebrew/bin, /usr/local/bin, ~/.local/bin, ~/.cargo/bin." >&2
echo " Install it (https://docs.astral.sh/uv/) or symlink it into /usr/local/bin." >&2
exit 127
fi

# First launch builds the environment from the checked-in uv.lock, which takes a
# few seconds; later launches reuse it.
exec "$UV" run --project "$ROOT" harbor-mcp
111 changes: 59 additions & 52 deletions harbor-mcp/skills/setup/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,82 +1,89 @@
---
name: setup
description: Diagnose and fix Harbor hub credentials for the harbor-mcp server. Use when the user runs /harbor-mcp:setup, when they ask how to authenticate or connect harbor-mcp, when they want to enable Harbor write tools, or when a harbor-hub tool has just failed with an authentication or "not authenticated" error.
description: Check Harbor hub authentication for the harbor-mcp server and summarize the account's current state. Use when the user runs /harbor-mcp:setup, when they ask how to authenticate or connect harbor-mcp, when they want to enable Harbor write tools, or when a harbor-hub tool has just failed with an authentication error.
---

# Harbor hub setup

Verify the user's Harbor credentials, and if they are missing, guide them to
whichever of the two credential paths suits them. Never write, echo, or read
back the value of an API key.
`harbor auth login` is the only supported way to authenticate: it mints a key
scoped to the logged-in user and stores it in `~/.harbor/credentials.json`, which
harbor reads on its own. Never write, echo, or read back a key value, and never
run `harbor auth login` on the user's behalf; it is an interactive OAuth flow
they must complete themselves.

## Step 1: check whether credentials already resolve
## Step 1: check stored credentials

Call the harbor-hub server's `whoami` tool.
```bash
command -v harbor >/dev/null && harbor auth status || uv run --project "${CLAUDE_PLUGIN_ROOT}" harbor auth status
```

On success it returns `user_id`, `key_source` (`env` or `file`), and `key_id`.
Report the identity and which source was used, then go to Step 3. Nothing else
is needed.
The fallback matters: harbor ships inside this plugin's own environment, so the
user does not need a global harbor install. Whichever path runs, use that same
form for any `harbor` command you tell the user to run, so it matches their setup.

If the tool errors with an authentication failure, or the harbor-hub server is
not connected at all, continue to Step 2.
**This command exits 0 whether or not the user is authenticated**, so branch on
its output, not its exit code:

## Step 2: pick a credential path
- Contains `Not authenticated` → go to Step 2.
- Reports `Logged in as <user> (API key sk-harbor-...)` → go to Step 3. The
printed key is already truncated to a public prefix, so it is safe to repeat.

Credential precedence, highest first:
## Step 2: not authenticated

1. `HARBOR_API_KEY` already in the environment
2. `KEY=value` in the nearest `.env` (the server searches the working directory
and up to three parents)
3. `~/.harbor/credentials.json`, written by `harbor auth login`
Tell the user to run this themselves and say when it is done:

Work out which paths are already partly set up before recommending one:
```bash
harbor auth login
```

If `harbor` was not on their PATH in Step 1, give them the plugin form instead:

```bash
command -v harbor >/dev/null && echo "harbor CLI: present" || echo "harbor CLI: absent"
test -f ~/.harbor/credentials.json && echo "credentials.json: present" || echo "credentials.json: absent"
test -f .env && echo ".env: present" || echo ".env: absent"
uv run --project "${CLAUDE_PLUGIN_ROOT}" harbor auth login
```

Then recommend:
Once they confirm, note that the harbor-hub server must be restarted to pick up
new credentials, then return to Step 1.

- **`harbor CLI: present`**: recommend the CLI path. Tell the user to run
`harbor auth login` themselves in their terminal. It is interactive and it
writes the key to `~/.harbor/credentials.json`, which harbor-mcp picks up with
no further configuration. Do not run it on their behalf.
- **`harbor CLI: absent`**: recommend the `.env` path. Tell the user to create
a `.env` in their project root containing `HARBOR_API_KEY=sk-harbor-...`, and
to get that key from `harbor auth login` on a machine that has the CLI, or
from the Harbor hub web UI. Ask *them* to paste it into the file; never write
a key value into any file yourself, and never ask them to paste it into chat.
- **`credentials.json: present` but `whoami` still failed**: the stored key is
likely revoked or expired. Tell them to re-run `harbor auth login`.
## Step 3: confirm the key actually works

If a `.env` already exists, you may check *whether* it contains a
`HARBOR_API_KEY` line without printing the value:
Call the harbor-hub `whoami` tool.

```bash
grep -q '^[[:space:]]*\(export[[:space:]]\+\)\?HARBOR_API_KEY=' .env && echo "HARBOR_API_KEY: set" || echo "HARBOR_API_KEY: missing"
```
Step 1 only reads a local file, so it reports success for a key that has since
been revoked or expired. `whoami` is what proves the credential is live. It
returns `user_id`, `key_source`, and `key_id`, never the key.

After the user completes either path, tell them the harbor-hub server must be
restarted to pick up new credentials, then call `whoami` again to confirm.
If `whoami` fails while Step 1 said the user is logged in, the stored key is
revoked or expired. Tell them to run `harbor auth logout` and then
`harbor auth login` again.

## Step 3: report write-tool status
## Step 4: summarize the account

Call `list_jobs` once with default arguments, then report a brief snapshot so the
rest of the session has context:

- How many jobs exist in total
- The two or three most recent, with name and status
- Anything still running, or finished with errors

Keep it to a few lines. Do not page through the whole account, and do not write
this to a file: it goes stale as jobs run, and re-calling `list_jobs` costs the
same as reading a cache. Call it again whenever fresh data matters.

`check_task_published` and `resolve_dataset` both require an explicit org and
name, so there is nothing to enumerate for the registry. Skip them here.

## Step 5: report write-tool status

Read tools work with any valid key. The seven write tools (`upload_job`,
`publish_task`, `publish_dataset`, `download_job`, `set_job_visibility`,
`share_job`, `delete_job`) refuse unless `HARBOR_MCP_ENABLE_WRITES=true` is set
in the server's environment. Read-only is the intended default.

State the current status plainly. Only if the user asks to enable writes,
explain that they add `HARBOR_MCP_ENABLE_WRITES=true` to the same `.env` (or
export it in their shell) and restart the server. Note that `delete_job` is
permanent and additionally requires `confirm=true` per call, which you will pass
only after they explicitly approve deleting a specific job.

Do not enable writes proactively, and do not suggest it when the user only asked
to get connected.

## Step 4: suggest a first call
State the current status in one line. Only if the user asks to enable writes,
tell them to export `HARBOR_MCP_ENABLE_WRITES=true` and restart the server. Note
that `delete_job` is permanent and additionally requires `confirm=true` per call,
which you will pass only after they explicitly approve deleting a specific job.

Close by suggesting `list_jobs` to confirm the connection returns real data.
Do not enable writes proactively, and do not raise it when the user only asked to
get connected.
55 changes: 3 additions & 52 deletions harbor-mcp/src/harbor_mcp/server.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
"""Harbor hub MCP server: stdio entry point."""

import logging
import os
from pathlib import Path

from mcp.server.fastmcp import FastMCP

from harbor_mcp.tools import register_all

logger = logging.getLogger(__name__)

# ponytail: cwd plus three parents, not a walk to the filesystem root. Covers a
# .env beside the project the agent is working in, or one level up in a
# monorepo. Deepen if someone nests deeper than that.
DOTENV_SEARCH_DEPTH = 3

INSTRUCTIONS = """Tools for the Harbor hub (harborframework.com): inspect evaluation jobs and
trials, verify uploads, resolve published tasks/datasets, and (when enabled)
publish and manage hub data.

Authenticate with `harbor auth login`, which stores a key scoped to the logged-in
user in ~/.harbor/credentials.json; harbor reads it directly.

Start with whoami to verify credentials. Find job ids with list_jobs, then
drill down with get_job_overview, get_job_trials, get_trial_detail, or
check_job_upload. Write tools (upload_job, publish_task, publish_dataset,
Expand All @@ -30,48 +22,7 @@
register_all(mcp)


def load_dotenv(start: Path | None = None) -> Path | None:
"""Load the nearest `.env` into os.environ; return the file used, if any.

Credential precedence, highest first:

1. A variable already in the environment (the MCP client's config, or the
user's shell). Never overwritten here.
2. `KEY=value` lines in the nearest `.env`.
3. `~/.harbor/credentials.json` from `harbor auth login`, which harbor
falls back to on its own when HARBOR_API_KEY is unset or blank.

A missing `.env` is therefore the normal case for anyone who authenticated
with the CLI, not an error.
"""
base = (start or Path.cwd()).resolve()
for directory in (base, *base.parents[:DOTENV_SEARCH_DEPTH]):
env_file = directory / ".env"
if not env_file.is_file():
continue
try:
text = env_file.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
# An unreadable .env must not take the server down: harbor can still
# authenticate from the env var or the credentials file.
logger.warning("could not read %s; ignoring it", env_file, exc_info=True)
return None
for line in text.splitlines():
entry = line.strip().removeprefix("export ").strip()
if not entry or entry.startswith("#") or "=" not in entry:
continue
key, _, value = entry.partition("=")
key = key.strip()
if key and key not in os.environ:
os.environ[key] = value.strip().strip("\"'")
return env_file
return None


def main() -> None:
# Before mcp.run, and never at import time: importing this module must not
# mutate the environment out from under the test suite.
load_dotenv()
mcp.run(transport="stdio")


Expand Down
Loading
Loading