agent-harnesses-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# IntelliJ
|
|
2
|
+
target/
|
|
3
|
+
.idea/
|
|
4
|
+
*.iml
|
|
5
|
+
|
|
6
|
+
# Sublime
|
|
7
|
+
*.sublime-workspace
|
|
8
|
+
|
|
9
|
+
# Eclipse
|
|
10
|
+
.settings
|
|
11
|
+
|
|
12
|
+
# VS Code
|
|
13
|
+
.project
|
|
14
|
+
.classpath
|
|
15
|
+
.vscode/*
|
|
16
|
+
# Ignore all local history of files
|
|
17
|
+
**/.history
|
|
18
|
+
|
|
19
|
+
# Java
|
|
20
|
+
*.class
|
|
21
|
+
target/
|
|
22
|
+
|
|
23
|
+
# C
|
|
24
|
+
*.so
|
|
25
|
+
|
|
26
|
+
# Python
|
|
27
|
+
*.pyc
|
|
28
|
+
*.egg-info
|
|
29
|
+
__pycache__
|
|
30
|
+
.ipynb_checkpoints
|
|
31
|
+
.Python
|
|
32
|
+
dist/
|
|
33
|
+
.python-version
|
|
34
|
+
.installed.cfg
|
|
35
|
+
*.egg
|
|
36
|
+
reqlib-metadata
|
|
37
|
+
.mypy_cache/
|
|
38
|
+
.venv
|
|
39
|
+
venv/
|
|
40
|
+
build/
|
|
41
|
+
|
|
42
|
+
# Byte-compiled / optimized / DLL files
|
|
43
|
+
*.pyc
|
|
44
|
+
__pycache__/
|
|
45
|
+
*.py[cod]
|
|
46
|
+
*$py.class
|
|
47
|
+
|
|
48
|
+
# Unit test / coverage reports
|
|
49
|
+
htmlcov/
|
|
50
|
+
.tox/
|
|
51
|
+
.nox/
|
|
52
|
+
.coverage
|
|
53
|
+
.coverage.*
|
|
54
|
+
.cache
|
|
55
|
+
nosetests.xml
|
|
56
|
+
coverage.xml
|
|
57
|
+
*,cover
|
|
58
|
+
.hypothesis/
|
|
59
|
+
.pytest_cache/
|
|
60
|
+
|
|
61
|
+
# NPM / Node / JavaScript
|
|
62
|
+
.npm
|
|
63
|
+
node_modules/
|
|
64
|
+
jspm_packages/
|
|
65
|
+
|
|
66
|
+
# Runtime data
|
|
67
|
+
pids
|
|
68
|
+
*.pid
|
|
69
|
+
*.seed
|
|
70
|
+
*.pid.lock
|
|
71
|
+
|
|
72
|
+
# Logs
|
|
73
|
+
logs
|
|
74
|
+
*.log
|
|
75
|
+
npm-debug.log*
|
|
76
|
+
yarn-debug.log*
|
|
77
|
+
yarn-error.log*
|
|
78
|
+
lerna-debug.log*
|
|
79
|
+
|
|
80
|
+
# vim temporary files
|
|
81
|
+
*~
|
|
82
|
+
.*.sw?
|
|
83
|
+
|
|
84
|
+
# Other Artifacts
|
|
85
|
+
hs_err_pid*
|
|
86
|
+
*.log
|
|
87
|
+
*.swp
|
|
88
|
+
*.swo
|
|
89
|
+
temp/*
|
|
90
|
+
.DS_Store
|
|
91
|
+
|
|
92
|
+
# Local / editor (do not publish)
|
|
93
|
+
.cursor/*
|
|
94
|
+
!.cursor/rules/
|
|
95
|
+
.cursor/rules/*
|
|
96
|
+
!.cursor/rules/git-author.mdc
|
|
97
|
+
claude.md
|
|
98
|
+
PLAN.md
|
|
99
|
+
history/
|
|
100
|
+
.env
|
|
101
|
+
.env.*
|
|
102
|
+
!.env.example
|
|
103
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-harnesses-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for best-of-Agent-Harnesses: harness recommendations, search, and head-to-head decision guides over a hand-curated, weekly-rescored list of 110 agent harnesses
|
|
5
|
+
Project-URL: Repository, https://github.com/RyanAlberts/best-of-Agent-Harnesses
|
|
6
|
+
Project-URL: Documentation, https://github.com/RyanAlberts/best-of-Agent-Harnesses/tree/main/mcp
|
|
7
|
+
Author: Ryan Alberts
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: agent-harness,agents,llm,mcp,model-context-protocol
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Requires-Dist: mcp>=1.2
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# agent-harnesses MCP server
|
|
15
|
+
|
|
16
|
+
The [best-of-Agent-Harnesses](https://github.com/RyanAlberts/best-of-Agent-Harnesses) list as an MCP server, so agents can recommend harnesses instead of you reading 101 table rows.
|
|
17
|
+
|
|
18
|
+
Single file, stdio transport, no clone needed — it fetches [harnesses.json](../harnesses.json) from this repo at startup (or reads it locally from a checkout). Requires [uv](https://docs.astral.sh/uv/).
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
Claude Code:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
claude mcp add agent-harnesses -- uv run https://raw.githubusercontent.com/RyanAlberts/best-of-Agent-Harnesses/main/mcp/server.py
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Any other MCP client (Cursor, Codex, Gemini CLI, ...):
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"agent-harnesses": {
|
|
34
|
+
"command": "uv",
|
|
35
|
+
"args": ["run", "https://raw.githubusercontent.com/RyanAlberts/best-of-Agent-Harnesses/main/mcp/server.py"]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Tools
|
|
42
|
+
|
|
43
|
+
| Tool | What it does |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `pick_harness(use_case, max_complexity?, min_autonomy?, min_recovery?, open_source_only?, limit?)` | Ranked recommendations for a use case, seeded by the list's hand-curated use-case index. `max_complexity` caps adoption surface (`super simple` → `complex`); `min_autonomy` requires a designed autonomy regime (`step-gated` → `headless`); `min_recovery` requires a failure-recovery tier (`none` → `durable`). |
|
|
46
|
+
| `search_harnesses(query, limit?)` | Keyword search across names, descriptions, tags, and categories. |
|
|
47
|
+
| `get_harness(github_id)` | Full record for one project. |
|
|
48
|
+
| `list_comparisons()` | The head-to-head decision guides (OpenClaw vs Hermes, terminal coding agents, …) with summaries. |
|
|
49
|
+
| `get_comparison(slug)` | Full markdown of one guide — architecture trade-offs, field reports, billing reality. Always current: served from the repo's `main`. |
|
|
50
|
+
| `list_categories()` | The 10 categories, use-case intents, and the complexity/autonomy/recovery scales. |
|
|
51
|
+
|
|
52
|
+
Example: *"pick_harness('sandboxed code execution for generated code', max_complexity='slightly complex', open_source_only=True)"* → E2B, smolagents, Daytona... each with stars, tier, license signal, and a one-line reason.
|
|
53
|
+
|
|
54
|
+
Data is regenerated by [`scripts/generate.py`](../scripts/generate.py); star counts carry a `stars_captured` date, and the comparisons index is rebuilt from `comparisons/*.md` on every refresh — the server always serves current `main`.
|
|
55
|
+
|
|
56
|
+
## Distribution
|
|
57
|
+
|
|
58
|
+
The server is packaged as **`agent-harnesses-mcp`** (this directory's `pyproject.toml`) and registered in the official MCP registry as **`io.github.ryanalberts/agent-harnesses`** (`server.json` at the repo root). Once a release is published, package-manager installs work everywhere:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
# any MCP client, via PyPI
|
|
62
|
+
uvx agent-harnesses-mcp
|
|
63
|
+
# Claude Code
|
|
64
|
+
claude mcp add agent-harnesses -- uvx agent-harnesses-mcp
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Until then (and forever, as the zero-install path), the raw-URL one-liner at the top of this README works from any machine with uv.
|
|
68
|
+
|
|
69
|
+
## Publishing (maintainer runbook)
|
|
70
|
+
|
|
71
|
+
Releases are automated by [`.github/workflows/publish-mcp.yml`](../.github/workflows/publish-mcp.yml) on a `mcp-v*` tag: it builds the wheel, publishes to PyPI via trusted publishing, and publishes `server.json` to the official MCP registry via GitHub OIDC.
|
|
72
|
+
|
|
73
|
+
One-time setup, then never again:
|
|
74
|
+
1. On pypi.org: create the project name `agent-harnesses-mcp` → Settings → Publishing → add a **trusted publisher**: owner `RyanAlberts`, repo `best-of-Agent-Harnesses`, workflow `publish-mcp.yml`. No API tokens.
|
|
75
|
+
2. Nothing for the MCP registry — GitHub OIDC from this repo authorizes the `io.github.ryanalberts/*` namespace automatically.
|
|
76
|
+
|
|
77
|
+
Per release: bump the version in `mcp/pyproject.toml` **and** `server.json` (the workflow fails loudly on mismatch), then `git tag mcp-v<version> && git push origin mcp-v<version>`.
|
|
78
|
+
|
|
79
|
+
Also indexed via [`smithery.yaml`](../smithery.yaml) (submit the repo once at [smithery.ai](https://smithery.ai)); other directories (Glama, PulseMCP, mcpservers.org) crawl the official registry.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# agent-harnesses MCP server
|
|
2
|
+
|
|
3
|
+
The [best-of-Agent-Harnesses](https://github.com/RyanAlberts/best-of-Agent-Harnesses) list as an MCP server, so agents can recommend harnesses instead of you reading 101 table rows.
|
|
4
|
+
|
|
5
|
+
Single file, stdio transport, no clone needed — it fetches [harnesses.json](../harnesses.json) from this repo at startup (or reads it locally from a checkout). Requires [uv](https://docs.astral.sh/uv/).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Claude Code:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
claude mcp add agent-harnesses -- uv run https://raw.githubusercontent.com/RyanAlberts/best-of-Agent-Harnesses/main/mcp/server.py
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Any other MCP client (Cursor, Codex, Gemini CLI, ...):
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"mcpServers": {
|
|
20
|
+
"agent-harnesses": {
|
|
21
|
+
"command": "uv",
|
|
22
|
+
"args": ["run", "https://raw.githubusercontent.com/RyanAlberts/best-of-Agent-Harnesses/main/mcp/server.py"]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Tools
|
|
29
|
+
|
|
30
|
+
| Tool | What it does |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `pick_harness(use_case, max_complexity?, min_autonomy?, min_recovery?, open_source_only?, limit?)` | Ranked recommendations for a use case, seeded by the list's hand-curated use-case index. `max_complexity` caps adoption surface (`super simple` → `complex`); `min_autonomy` requires a designed autonomy regime (`step-gated` → `headless`); `min_recovery` requires a failure-recovery tier (`none` → `durable`). |
|
|
33
|
+
| `search_harnesses(query, limit?)` | Keyword search across names, descriptions, tags, and categories. |
|
|
34
|
+
| `get_harness(github_id)` | Full record for one project. |
|
|
35
|
+
| `list_comparisons()` | The head-to-head decision guides (OpenClaw vs Hermes, terminal coding agents, …) with summaries. |
|
|
36
|
+
| `get_comparison(slug)` | Full markdown of one guide — architecture trade-offs, field reports, billing reality. Always current: served from the repo's `main`. |
|
|
37
|
+
| `list_categories()` | The 10 categories, use-case intents, and the complexity/autonomy/recovery scales. |
|
|
38
|
+
|
|
39
|
+
Example: *"pick_harness('sandboxed code execution for generated code', max_complexity='slightly complex', open_source_only=True)"* → E2B, smolagents, Daytona... each with stars, tier, license signal, and a one-line reason.
|
|
40
|
+
|
|
41
|
+
Data is regenerated by [`scripts/generate.py`](../scripts/generate.py); star counts carry a `stars_captured` date, and the comparisons index is rebuilt from `comparisons/*.md` on every refresh — the server always serves current `main`.
|
|
42
|
+
|
|
43
|
+
## Distribution
|
|
44
|
+
|
|
45
|
+
The server is packaged as **`agent-harnesses-mcp`** (this directory's `pyproject.toml`) and registered in the official MCP registry as **`io.github.ryanalberts/agent-harnesses`** (`server.json` at the repo root). Once a release is published, package-manager installs work everywhere:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
# any MCP client, via PyPI
|
|
49
|
+
uvx agent-harnesses-mcp
|
|
50
|
+
# Claude Code
|
|
51
|
+
claude mcp add agent-harnesses -- uvx agent-harnesses-mcp
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Until then (and forever, as the zero-install path), the raw-URL one-liner at the top of this README works from any machine with uv.
|
|
55
|
+
|
|
56
|
+
## Publishing (maintainer runbook)
|
|
57
|
+
|
|
58
|
+
Releases are automated by [`.github/workflows/publish-mcp.yml`](../.github/workflows/publish-mcp.yml) on a `mcp-v*` tag: it builds the wheel, publishes to PyPI via trusted publishing, and publishes `server.json` to the official MCP registry via GitHub OIDC.
|
|
59
|
+
|
|
60
|
+
One-time setup, then never again:
|
|
61
|
+
1. On pypi.org: create the project name `agent-harnesses-mcp` → Settings → Publishing → add a **trusted publisher**: owner `RyanAlberts`, repo `best-of-Agent-Harnesses`, workflow `publish-mcp.yml`. No API tokens.
|
|
62
|
+
2. Nothing for the MCP registry — GitHub OIDC from this repo authorizes the `io.github.ryanalberts/*` namespace automatically.
|
|
63
|
+
|
|
64
|
+
Per release: bump the version in `mcp/pyproject.toml` **and** `server.json` (the workflow fails loudly on mismatch), then `git tag mcp-v<version> && git push origin mcp-v<version>`.
|
|
65
|
+
|
|
66
|
+
Also indexed via [`smithery.yaml`](../smithery.yaml) (submit the repo once at [smithery.ai](https://smithery.ai)); other directories (Glama, PulseMCP, mcpservers.org) crawl the official registry.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agent-harnesses-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server for best-of-Agent-Harnesses: harness recommendations, search, and head-to-head decision guides over a hand-curated, weekly-rescored list of 110 agent harnesses"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Ryan Alberts" }]
|
|
13
|
+
keywords = ["mcp", "model-context-protocol", "agents", "agent-harness", "llm"]
|
|
14
|
+
dependencies = ["mcp>=1.2"]
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
agent-harnesses-mcp = "agent_harnesses_mcp:main"
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Repository = "https://github.com/RyanAlberts/best-of-Agent-Harnesses"
|
|
21
|
+
Documentation = "https://github.com/RyanAlberts/best-of-Agent-Harnesses/tree/main/mcp"
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
24
|
+
"server.py" = "agent_harnesses_mcp/__init__.py"
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# dependencies = ["mcp>=1.2"]
|
|
5
|
+
# ///
|
|
6
|
+
"""MCP server for best-of-Agent-Harnesses.
|
|
7
|
+
|
|
8
|
+
Serves the curated list (harnesses.json) as tools so agents can recommend
|
|
9
|
+
agent harnesses: pick_harness, search_harnesses, get_harness, list_categories.
|
|
10
|
+
|
|
11
|
+
Run directly from GitHub (no clone needed):
|
|
12
|
+
uv run https://raw.githubusercontent.com/RyanAlberts/best-of-Agent-Harnesses/main/mcp/server.py
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
import urllib.request
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from mcp.server.fastmcp import FastMCP
|
|
21
|
+
|
|
22
|
+
DATA_URL = "https://raw.githubusercontent.com/RyanAlberts/best-of-Agent-Harnesses/main/harnesses.json"
|
|
23
|
+
|
|
24
|
+
mcp = FastMCP("agent-harnesses")
|
|
25
|
+
|
|
26
|
+
_data: dict | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def data() -> dict:
|
|
30
|
+
global _data
|
|
31
|
+
if _data is None:
|
|
32
|
+
local = Path(__file__).resolve().parent.parent / "harnesses.json"
|
|
33
|
+
if local.exists():
|
|
34
|
+
_data = json.loads(local.read_text())
|
|
35
|
+
else:
|
|
36
|
+
with urllib.request.urlopen(DATA_URL, timeout=15) as r:
|
|
37
|
+
_data = json.loads(r.read().decode())
|
|
38
|
+
return _data
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_STOP = {"i", "a", "an", "the", "to", "for", "of", "in", "on", "with", "and",
|
|
42
|
+
"or", "my", "me", "want", "need", "agent", "agents", "ai", "llm"}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _tokens(text: str) -> set:
|
|
46
|
+
return {w for w in re.findall(r"[a-z0-9+#-]+", text.lower()) if w not in _STOP}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _overlap(q: set, hay: set) -> set:
|
|
50
|
+
"""Query tokens with a match in hay, tolerating inflections: tokens of 4+
|
|
51
|
+
chars match if either is a prefix of the other (benchmark/benchmarks,
|
|
52
|
+
evaluate/evaluates)."""
|
|
53
|
+
hits = set()
|
|
54
|
+
for w in q:
|
|
55
|
+
for h in hay:
|
|
56
|
+
if w == h or (len(w) >= 4 and len(h) >= 4 and (w.startswith(h) or h.startswith(w))):
|
|
57
|
+
hits.add(w)
|
|
58
|
+
break
|
|
59
|
+
return hits
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _brief(p: dict, reason: str = "") -> dict:
|
|
63
|
+
out = {
|
|
64
|
+
"name": p["name"],
|
|
65
|
+
"github_id": p["github_id"],
|
|
66
|
+
"url": p["url"],
|
|
67
|
+
"stars": p["stars"],
|
|
68
|
+
"tier": p["tier"],
|
|
69
|
+
"autonomy": p.get("autonomy", "n/a"),
|
|
70
|
+
"recovery": p.get("recovery", "n/a"),
|
|
71
|
+
"license_signal": p["license_signal"],
|
|
72
|
+
"category": p["category_title"],
|
|
73
|
+
"description": p["description"],
|
|
74
|
+
"tags": p["tags"],
|
|
75
|
+
}
|
|
76
|
+
if reason:
|
|
77
|
+
out["why"] = reason
|
|
78
|
+
return out
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@mcp.tool()
|
|
82
|
+
def pick_harness(use_case: str, max_complexity: str = "complex",
|
|
83
|
+
min_autonomy: str = "", min_recovery: str = "",
|
|
84
|
+
open_source_only: bool = False, limit: int = 5) -> str:
|
|
85
|
+
"""Recommend agent harnesses for a use case, ranked from a hand-curated list of 101.
|
|
86
|
+
|
|
87
|
+
use_case: what you want to do, e.g. "terminal coding agent", "drop-in memory
|
|
88
|
+
layer", "evaluate agents on coding benchmarks".
|
|
89
|
+
max_complexity: cap on adoption surface — one of "super simple",
|
|
90
|
+
"mostly simple", "slightly complex", "complex" (default: no cap).
|
|
91
|
+
min_autonomy: require at least this designed autonomy regime — one of
|
|
92
|
+
"step-gated", "checkpoint-gated", "bounded", "headless" (e.g. "bounded"
|
|
93
|
+
means "must be able to run a whole task unattended"; excludes n/a entries).
|
|
94
|
+
min_recovery: require at least this failure-recovery tier — one of "none",
|
|
95
|
+
"retry", "resumable", "durable" (excludes n/a entries).
|
|
96
|
+
open_source_only: drop projects with restricted or unknown licenses.
|
|
97
|
+
Returns JSON: ranked picks with a one-line reason each.
|
|
98
|
+
"""
|
|
99
|
+
d = data()
|
|
100
|
+
tiers: list = d["meta"]["tiers"]
|
|
101
|
+
max_rank = tiers.index(max_complexity) + 1 if max_complexity in tiers else 4
|
|
102
|
+
a_tiers: list = d["meta"].get("autonomy_tiers", [])
|
|
103
|
+
r_tiers: list = d["meta"].get("recovery_tiers", [])
|
|
104
|
+
min_a = a_tiers.index(min_autonomy) + 1 if min_autonomy in a_tiers else 0
|
|
105
|
+
min_r = r_tiers.index(min_recovery) + 1 if min_recovery in r_tiers else 0
|
|
106
|
+
q = _tokens(use_case)
|
|
107
|
+
|
|
108
|
+
# Curated use-case intents are the strongest signal: best word-overlap intent
|
|
109
|
+
# seeds its hand-picked projects to the top, in curated order.
|
|
110
|
+
seeded: dict = {}
|
|
111
|
+
best = max(d["use_cases"], key=lambda u: len(_overlap(q, _tokens(u["intent"]))), default=None)
|
|
112
|
+
if best and len(_overlap(q, _tokens(best["intent"]))) >= 2:
|
|
113
|
+
for rank, gid in enumerate(best["picks"]):
|
|
114
|
+
seeded[gid] = (100 - rank, f"curated pick for \"{best['intent']}\"")
|
|
115
|
+
|
|
116
|
+
import math
|
|
117
|
+
scored = []
|
|
118
|
+
for p in d["projects"]:
|
|
119
|
+
if p["tier_rank"] > max_rank:
|
|
120
|
+
continue
|
|
121
|
+
if min_a and p.get("autonomy_rank", 0) < min_a:
|
|
122
|
+
continue
|
|
123
|
+
if min_r and p.get("recovery_rank", 0) < min_r:
|
|
124
|
+
continue
|
|
125
|
+
if open_source_only and p["license_signal"] != "open-source":
|
|
126
|
+
continue
|
|
127
|
+
if p["github_id"] in seeded:
|
|
128
|
+
score, reason = seeded[p["github_id"]]
|
|
129
|
+
else:
|
|
130
|
+
overlap = _overlap(q, _tokens(f"{p['description']} {' '.join(p['tags'])} {p['category_title']}"))
|
|
131
|
+
if not overlap:
|
|
132
|
+
continue
|
|
133
|
+
score = len(overlap) * 3 + math.log10(max(p["stars"], 2))
|
|
134
|
+
reason = "matches: " + ", ".join(sorted(overlap))
|
|
135
|
+
scored.append((score, p, reason))
|
|
136
|
+
|
|
137
|
+
scored.sort(key=lambda t: -t[0])
|
|
138
|
+
picks = [_brief(p, reason) for _, p, reason in scored[:limit]]
|
|
139
|
+
return json.dumps({
|
|
140
|
+
"use_case": use_case,
|
|
141
|
+
"picks": picks,
|
|
142
|
+
"source": d["meta"]["url"],
|
|
143
|
+
"stars_captured": d["meta"]["stars_captured"],
|
|
144
|
+
}, indent=2, ensure_ascii=False)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@mcp.tool()
|
|
148
|
+
def search_harnesses(query: str, limit: int = 10) -> str:
|
|
149
|
+
"""Keyword search across all 101 projects (name, description, tags, category).
|
|
150
|
+
|
|
151
|
+
Returns JSON: matching projects sorted by relevance then stars.
|
|
152
|
+
"""
|
|
153
|
+
d = data()
|
|
154
|
+
q = _tokens(query)
|
|
155
|
+
ql = query.lower()
|
|
156
|
+
import math
|
|
157
|
+
scored = []
|
|
158
|
+
for p in d["projects"]:
|
|
159
|
+
hay = f"{p['name']} {p['github_id']} {p['description']} {' '.join(p['tags'])} {p['category_title']}"
|
|
160
|
+
name_hit = 50 if ql in p["name"].lower() or ql in p["github_id"].lower() else 0
|
|
161
|
+
overlap = _overlap(q, _tokens(hay))
|
|
162
|
+
if not (name_hit or overlap):
|
|
163
|
+
continue
|
|
164
|
+
scored.append((name_hit + len(overlap) * 3 + math.log10(max(p["stars"], 2)), p))
|
|
165
|
+
scored.sort(key=lambda t: -t[0])
|
|
166
|
+
return json.dumps({"query": query, "results": [_brief(p) for _, p in scored[:limit]]},
|
|
167
|
+
indent=2, ensure_ascii=False)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@mcp.tool()
|
|
171
|
+
def get_harness(github_id: str) -> str:
|
|
172
|
+
"""Full record for one project by github_id (e.g. "anomalyco/opencode")."""
|
|
173
|
+
for p in data()["projects"]:
|
|
174
|
+
if p["github_id"].lower() == github_id.lower():
|
|
175
|
+
return json.dumps(p, indent=2, ensure_ascii=False)
|
|
176
|
+
return json.dumps({"error": f"unknown github_id: {github_id}",
|
|
177
|
+
"hint": "use search_harnesses to find the right id"})
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@mcp.tool()
|
|
181
|
+
def list_comparisons() -> str:
|
|
182
|
+
"""The list's head-to-head decision guides (e.g. "OpenClaw vs Hermes",
|
|
183
|
+
"How to pick a harness") — slug, title, and summary for each. Fetch the
|
|
184
|
+
full text of one with get_comparison(slug)."""
|
|
185
|
+
return json.dumps({"comparisons": data().get("comparisons", [])},
|
|
186
|
+
indent=2, ensure_ascii=False)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@mcp.tool()
|
|
190
|
+
def get_comparison(slug: str) -> str:
|
|
191
|
+
"""Full markdown of one decision guide by slug (see list_comparisons).
|
|
192
|
+
Guides cover architecture trade-offs, field reports, and the post-June-2026
|
|
193
|
+
billing reality — use them when a user is choosing between specific
|
|
194
|
+
harnesses, not just browsing."""
|
|
195
|
+
for c in data().get("comparisons", []):
|
|
196
|
+
if c["slug"] == slug:
|
|
197
|
+
local = Path(__file__).resolve().parent.parent / "comparisons" / f"{slug}.md"
|
|
198
|
+
if local.exists():
|
|
199
|
+
return local.read_text()
|
|
200
|
+
with urllib.request.urlopen(c["raw_url"], timeout=15) as r:
|
|
201
|
+
return r.read().decode()
|
|
202
|
+
return json.dumps({"error": f"unknown slug: {slug}",
|
|
203
|
+
"available": [c["slug"] for c in data().get("comparisons", [])]})
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@mcp.tool()
|
|
207
|
+
def list_categories() -> str:
|
|
208
|
+
"""The list's 9 categories and 13 curated use-case intents, with project counts."""
|
|
209
|
+
d = data()
|
|
210
|
+
counts: dict = {}
|
|
211
|
+
for p in d["projects"]:
|
|
212
|
+
counts[p["category"]] = counts.get(p["category"], 0) + 1
|
|
213
|
+
return json.dumps({
|
|
214
|
+
"categories": [dict(c, project_count=counts.get(c["id"], 0)) for c in d["categories"]],
|
|
215
|
+
"use_cases": [u["intent"] for u in d["use_cases"]],
|
|
216
|
+
"tiers": d["meta"]["tiers"],
|
|
217
|
+
"autonomy_tiers": d["meta"].get("autonomy_tiers", []),
|
|
218
|
+
"recovery_tiers": d["meta"].get("recovery_tiers", []),
|
|
219
|
+
}, indent=2, ensure_ascii=False)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def main():
|
|
223
|
+
mcp.run()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
if __name__ == "__main__":
|
|
227
|
+
main()
|