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
10 changes: 8 additions & 2 deletions harbor-mcp/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
{
"name": "harbor-mcp",
"version": "0.2.0",
"version": "0.3.0",
"description": "MCP server for the Harbor hub: inspect evaluation jobs, trials, and uploads; publish tasks and datasets when writes are enabled.",
"author": {
"name": "Walker Hughes"
},
"license": "MIT",
"homepage": "https://github.com/walkerhughes/mcps/tree/main/harbor-mcp",
"repository": "https://github.com/walkerhughes/mcps",
"keywords": ["harbor", "evals", "mcp", "evaluation", "llm"]
"keywords": [
"harbor",
"evals",
"mcp",
"evaluation",
"llm"
]
}
2 changes: 1 addition & 1 deletion harbor-mcp/.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"harbor-hub": {
"type": "stdio",
"command": "bash",
"args": ["${CLAUDE_PLUGIN_ROOT:-.}/scripts/start-server.sh"]
"args": ["${CLAUDE_PLUGIN_ROOT}/scripts/start-server.sh"]
}
}
}
10 changes: 8 additions & 2 deletions harbor-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,15 @@ You do not need a global harbor install: harbor ships inside the plugin's enviro
uv sync
```

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.
Run the server directly:

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.
```bash
./scripts/start-server.sh
```

[`.mcp.json`](.mcp.json) is the **plugin** config and only works when loaded as a plugin, because `${CLAUDE_PLUGIN_ROOT}` is substituted by the plugin loader. Do not add a `:-default` to it to make local discovery work, which is what broke it for three releases: the `:-default` form is handled by environment-variable expansion, which does not know `CLAUDE_PLUGIN_ROOT`, so the default silently won and the server was launched against whatever project the user happened to be in. It failed as an opaque JSON-RPC `-32000`, visible only in `~/Library/Caches/claude-cli-nodejs/<project>/mcp-logs-plugin-harbor-mcp-harbor-hub/`. Two tests in [tests/unit/test_plugin_config.py](tests/unit/test_plugin_config.py) now guard this.

[`scripts/start-server.sh`](scripts/start-server.sh) resolves the plugin root from its own location, finds `uv` on PATH or in the common install directories, drops an inherited `VIRTUAL_ENV`, and writes diagnostics to stderr so stdout stays a clean JSON-RPC channel.

## Tools

Expand Down
55 changes: 55 additions & 0 deletions harbor-mcp/tests/unit/test_plugin_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Guards on the shipped plugin config.

These exist because a bad .mcp.json fails at MCP connect time with an opaque
-32000 that no other test can see, and the first three attempts at this file all
shipped broken.
"""

import json
from pathlib import Path

REPO = Path(__file__).resolve().parents[2]
MCP_CONFIG = REPO / ".mcp.json"
PLUGIN_MANIFEST = REPO / ".claude-plugin" / "plugin.json"


def server_config() -> dict:
return json.loads(MCP_CONFIG.read_text())["mcpServers"]["harbor-hub"]


def test_plugin_root_has_no_default_fallback():
"""`${CLAUDE_PLUGIN_ROOT:-.}` silently expands to `.`, not the plugin root.

The `:-default` form is handled by env-var expansion, which does not know
CLAUDE_PLUGIN_ROOT, so the default always won and the server was launched
against the user's own project directory. Only the bare form is substituted
by the plugin loader.
"""
for arg in server_config()["args"]:
assert ":-" not in arg, (
f"{arg!r} uses a :-default; CLAUDE_PLUGIN_ROOT must be bare or it "
"expands to the default and the server launches from the wrong "
"directory"
)


def test_launcher_path_exists():
"""The referenced script must actually ship, or bash exits before the server runs."""
args = server_config()["args"]
assert len(args) == 1, f"expected a single script argument, got {args}"
relative = args[0].replace("${CLAUDE_PLUGIN_ROOT}/", "")
script = REPO / relative
assert script.is_file(), f"{script} is referenced by .mcp.json but does not exist"
assert script.stat().st_mode & 0o111, f"{script} is not executable"


def test_manifest_version_matches_nothing_stale():
"""Version is the plugin cache key, so it must be a real semver triple.

A malformed version would land users in an unexpected cache directory.
"""
version = json.loads(PLUGIN_MANIFEST.read_text())["version"]
parts = version.split(".")
assert len(parts) == 3 and all(p.isdigit() for p in parts), (
f"version {version!r} must be MAJOR.MINOR.PATCH"
)
Loading