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()