wayforth-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.
- wayforth_mcp-0.1.0/.gitignore +30 -0
- wayforth_mcp-0.1.0/PKG-INFO +71 -0
- wayforth_mcp-0.1.0/README.md +51 -0
- wayforth_mcp-0.1.0/pyproject.toml +38 -0
- wayforth_mcp-0.1.0/ranker.py +79 -0
- wayforth_mcp-0.1.0/server.json +20 -0
- wayforth_mcp-0.1.0/server.py +160 -0
- wayforth_mcp-0.1.0/uv.lock +734 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.pyo
|
|
5
|
+
*.pyd
|
|
6
|
+
.venv/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.mypy_cache/
|
|
12
|
+
.ruff_cache/
|
|
13
|
+
|
|
14
|
+
# Environment
|
|
15
|
+
.env
|
|
16
|
+
|
|
17
|
+
# Node
|
|
18
|
+
node_modules/
|
|
19
|
+
|
|
20
|
+
# Docker volumes
|
|
21
|
+
docker/volumes/
|
|
22
|
+
|
|
23
|
+
# IDE
|
|
24
|
+
.cursor/
|
|
25
|
+
.vscode/
|
|
26
|
+
.idea/
|
|
27
|
+
|
|
28
|
+
# OS
|
|
29
|
+
.DS_Store
|
|
30
|
+
Thumbs.db
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wayforth-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for Wayforth — search engine and payment rail for AI agents
|
|
5
|
+
Project-URL: Homepage, https://wayforth.io
|
|
6
|
+
Project-URL: Repository, https://github.com/WayforthOfficial/wayforth
|
|
7
|
+
Project-URL: Documentation, https://api-production-fd71.up.railway.app/docs
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: agents,ai,api,mcp,wayforth
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: anthropic>=0.40
|
|
16
|
+
Requires-Dist: httpx>=0.27
|
|
17
|
+
Requires-Dist: mcp>=1.0.0
|
|
18
|
+
Requires-Dist: python-dotenv>=1.0
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# wayforth-mcp
|
|
22
|
+
|
|
23
|
+
MCP server for [Wayforth](https://github.com/your-org/wayforth) — discover AI services from Claude Code, Cursor, or any MCP-compatible client.
|
|
24
|
+
|
|
25
|
+
## Install (one command)
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
claude mcp add wayforth -- uv run --directory ~/Code/wayforth/packages/mcp-server python server.py
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
That's it. Claude Code will restart and the three Wayforth tools will be available immediately.
|
|
32
|
+
|
|
33
|
+
## Tools
|
|
34
|
+
|
|
35
|
+
| Tool | Description |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `wayforth_search` | Search services by natural language intent, with optional category and tier filters |
|
|
38
|
+
| `wayforth_list` | List all services, optionally filtered by category |
|
|
39
|
+
| `wayforth_status` | Catalog stats (counts by tier and category) and API health |
|
|
40
|
+
|
|
41
|
+
## Prerequisites
|
|
42
|
+
|
|
43
|
+
The Wayforth API must be running:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
cd ~/Code/wayforth/apps/api
|
|
47
|
+
uv run uvicorn main:app --port 8000
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
By default the MCP server connects to `http://localhost:8000`. Override with:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
export WAYFORTH_API_URL=https://api.wayforth.io
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Run manually (for testing)
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
cd ~/Code/wayforth/packages/mcp-server
|
|
60
|
+
uv run python server.py
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The server starts on stdio and waits for MCP protocol messages. Press `Ctrl+C` to exit.
|
|
64
|
+
|
|
65
|
+
## Development
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
cd packages/mcp-server
|
|
69
|
+
uv sync # install deps
|
|
70
|
+
uv run python server.py
|
|
71
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# wayforth-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for [Wayforth](https://github.com/your-org/wayforth) — discover AI services from Claude Code, Cursor, or any MCP-compatible client.
|
|
4
|
+
|
|
5
|
+
## Install (one command)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
claude mcp add wayforth -- uv run --directory ~/Code/wayforth/packages/mcp-server python server.py
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
That's it. Claude Code will restart and the three Wayforth tools will be available immediately.
|
|
12
|
+
|
|
13
|
+
## Tools
|
|
14
|
+
|
|
15
|
+
| Tool | Description |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `wayforth_search` | Search services by natural language intent, with optional category and tier filters |
|
|
18
|
+
| `wayforth_list` | List all services, optionally filtered by category |
|
|
19
|
+
| `wayforth_status` | Catalog stats (counts by tier and category) and API health |
|
|
20
|
+
|
|
21
|
+
## Prerequisites
|
|
22
|
+
|
|
23
|
+
The Wayforth API must be running:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cd ~/Code/wayforth/apps/api
|
|
27
|
+
uv run uvicorn main:app --port 8000
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
By default the MCP server connects to `http://localhost:8000`. Override with:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
export WAYFORTH_API_URL=https://api.wayforth.io
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Run manually (for testing)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
cd ~/Code/wayforth/packages/mcp-server
|
|
40
|
+
uv run python server.py
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The server starts on stdio and waits for MCP protocol messages. Press `Ctrl+C` to exit.
|
|
44
|
+
|
|
45
|
+
## Development
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cd packages/mcp-server
|
|
49
|
+
uv sync # install deps
|
|
50
|
+
uv run python server.py
|
|
51
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "wayforth-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MCP server for Wayforth — search engine and payment rail for AI agents"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = {text = "MIT"}
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"mcp>=1.0.0",
|
|
10
|
+
"httpx>=0.27",
|
|
11
|
+
"python-dotenv>=1.0",
|
|
12
|
+
"anthropic>=0.40",
|
|
13
|
+
]
|
|
14
|
+
keywords = ["ai", "agents", "mcp", "wayforth", "api"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
wayforth-mcp = "server:main"
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://wayforth.io"
|
|
27
|
+
Repository = "https://github.com/WayforthOfficial/wayforth"
|
|
28
|
+
Documentation = "https://api-production-fd71.up.railway.app/docs"
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["hatchling"]
|
|
32
|
+
build-backend = "hatchling.build"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
include = ["server.py", "ranker.py"]
|
|
36
|
+
|
|
37
|
+
[tool.mcp]
|
|
38
|
+
name = "io.github.WayforthOfficial/wayforth"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from anthropic import AsyncAnthropic
|
|
4
|
+
|
|
5
|
+
_client: AsyncAnthropic | None = None
|
|
6
|
+
|
|
7
|
+
_SYSTEM = (
|
|
8
|
+
"You are a service ranker for AI agents. Given an agent's intent and a list of "
|
|
9
|
+
"services, return a JSON array of the same services ranked by relevance, each with "
|
|
10
|
+
"an added 'score' (0-100) and 'reason' (one sentence). "
|
|
11
|
+
"Return ONLY a JSON array, no explanation, no markdown, no code fences."
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_client() -> AsyncAnthropic | None:
|
|
16
|
+
key = os.getenv("ANTHROPIC_API_KEY")
|
|
17
|
+
if not key:
|
|
18
|
+
return None
|
|
19
|
+
global _client
|
|
20
|
+
if _client is None:
|
|
21
|
+
_client = AsyncAnthropic(api_key=key)
|
|
22
|
+
return _client
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _keyword_rank(intent: str, services: list[dict]) -> list[dict]:
|
|
26
|
+
tokens = [w.lower() for w in intent.split() if len(w) > 2]
|
|
27
|
+
|
|
28
|
+
def _score(s: dict) -> int:
|
|
29
|
+
haystack = f"{s.get('name', '')} {s.get('description', '')}".lower()
|
|
30
|
+
return sum(1 for t in tokens if t in haystack)
|
|
31
|
+
|
|
32
|
+
ranked = sorted(services, key=_score, reverse=True)
|
|
33
|
+
for s in ranked:
|
|
34
|
+
s["score"] = _score(s) * 10
|
|
35
|
+
s["reason"] = "keyword match"
|
|
36
|
+
return ranked
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_HAIKU_CANDIDATE_LIMIT = 20 # pre-filter before sending to Haiku
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def rank_services(intent: str, services: list[dict]) -> list[dict]:
|
|
43
|
+
"""Rank services by semantic relevance using Claude Haiku; falls back to keyword ranking."""
|
|
44
|
+
client = _get_client()
|
|
45
|
+
if not client or not services:
|
|
46
|
+
return _keyword_rank(intent, list(services))
|
|
47
|
+
|
|
48
|
+
# Pre-filter: keyword rank → top N candidates so Haiku output fits in token budget
|
|
49
|
+
candidates = _keyword_rank(intent, [dict(s) for s in services])[:_HAIKU_CANDIDATE_LIMIT]
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
slim = [
|
|
53
|
+
{"name": s.get("name"), "description": s.get("description"), "category": s.get("category")}
|
|
54
|
+
for s in candidates
|
|
55
|
+
]
|
|
56
|
+
msg = await client.messages.create(
|
|
57
|
+
model="claude-haiku-4-5",
|
|
58
|
+
max_tokens=2048,
|
|
59
|
+
system=_SYSTEM,
|
|
60
|
+
messages=[{"role": "user", "content": f"Intent: {intent}\n\nServices:\n{json.dumps(slim)}"}],
|
|
61
|
+
)
|
|
62
|
+
text = msg.content[0].text
|
|
63
|
+
print(f"[DEBUG ranker] Haiku raw: {text[:500]!r}")
|
|
64
|
+
text = text.strip().removeprefix("```json").removeprefix("```").removesuffix("```").strip()
|
|
65
|
+
ranked_slim = json.loads(text)
|
|
66
|
+
name_to_meta = {item["name"]: item for item in ranked_slim}
|
|
67
|
+
|
|
68
|
+
result = []
|
|
69
|
+
for s in candidates:
|
|
70
|
+
s_copy = dict(s)
|
|
71
|
+
meta = name_to_meta.get(s.get("name"), {})
|
|
72
|
+
s_copy["score"] = int(meta.get("score", 0))
|
|
73
|
+
s_copy["reason"] = meta.get("reason", "")
|
|
74
|
+
result.append(s_copy)
|
|
75
|
+
|
|
76
|
+
return sorted(result, key=lambda x: x["score"], reverse=True)
|
|
77
|
+
except Exception as exc:
|
|
78
|
+
print(f"[DEBUG ranker] Haiku ranking failed ({exc!r}), falling back to keyword ranking")
|
|
79
|
+
return _keyword_rank(intent, candidates)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.WayforthOfficial/wayforth",
|
|
4
|
+
"description": "Search engine and payment rail for AI agents. Discover 2,300+ agent-payable services across inference, data, and translation categories. Semantic ranking powered by Claude Haiku.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/WayforthOfficial/wayforth",
|
|
7
|
+
"source": "github"
|
|
8
|
+
},
|
|
9
|
+
"version": "0.1.0",
|
|
10
|
+
"packages": [
|
|
11
|
+
{
|
|
12
|
+
"registryType": "pypi",
|
|
13
|
+
"identifier": "wayforth-mcp",
|
|
14
|
+
"version": "0.1.0",
|
|
15
|
+
"transport": {
|
|
16
|
+
"type": "stdio"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import httpx
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
from mcp.server.fastmcp import FastMCP
|
|
5
|
+
from ranker import rank_services
|
|
6
|
+
|
|
7
|
+
load_dotenv()
|
|
8
|
+
|
|
9
|
+
API_BASE = os.getenv("WAYFORTH_API_URL", "https://api-production-fd71.up.railway.app")
|
|
10
|
+
|
|
11
|
+
mcp = FastMCP("wayforth")
|
|
12
|
+
|
|
13
|
+
TIER_LABELS = {0: "free", 1: "basic", 2: "standard", 3: "premium"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def _fetch_services() -> list[dict] | None:
|
|
17
|
+
try:
|
|
18
|
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
19
|
+
r = await client.get(f"{API_BASE}/services")
|
|
20
|
+
r.raise_for_status()
|
|
21
|
+
return r.json()
|
|
22
|
+
except Exception:
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _score(service: dict, tokens: list[str]) -> int:
|
|
27
|
+
haystack = f"{service.get('name', '')} {service.get('description', '')}".lower()
|
|
28
|
+
return sum(1 for t in tokens if t in haystack)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _format_ranked_service(idx: int, s: dict) -> str:
|
|
32
|
+
tier = TIER_LABELS.get(s.get("coverage_tier", 0), str(s.get("coverage_tier")))
|
|
33
|
+
price = s.get("pricing_usdc")
|
|
34
|
+
price_str = f"${float(price):.4f}" if price is not None else "$0"
|
|
35
|
+
score = s.get("score", 0)
|
|
36
|
+
reason = s.get("reason", "")
|
|
37
|
+
return (
|
|
38
|
+
f"{idx}. {s['name']} (score: {score}) — Reason: {reason}\n"
|
|
39
|
+
f" Tier: {tier} | Price: {price_str} | Endpoint: {s.get('endpoint_url', 'N/A')}"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _format_service(s: dict) -> str:
|
|
44
|
+
tier = TIER_LABELS.get(s.get("coverage_tier", 0), str(s.get("coverage_tier")))
|
|
45
|
+
price = s.get("pricing_usdc")
|
|
46
|
+
price_str = f"${float(price):.4f} USDC" if price is not None else "free"
|
|
47
|
+
return (
|
|
48
|
+
f"**{s['name']}** [{s.get('category', 'unknown')} / tier {tier}]\n"
|
|
49
|
+
f" {s.get('description') or 'No description'}\n"
|
|
50
|
+
f" Pricing: {price_str} | Endpoint: {s.get('endpoint_url', 'N/A')}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@mcp.tool()
|
|
55
|
+
async def wayforth_search(intent: str, category: str = None, max_tier: int = 2) -> str:
|
|
56
|
+
"""Search Wayforth for AI services matching a natural language intent.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
intent: What you're looking for, e.g. "translate Spanish documents"
|
|
60
|
+
category: Optional filter — inference, data, or translation
|
|
61
|
+
max_tier: Maximum coverage tier to include (0=free, 1=basic, 2=standard, 3=premium)
|
|
62
|
+
"""
|
|
63
|
+
services = await _fetch_services()
|
|
64
|
+
if services is None:
|
|
65
|
+
return (
|
|
66
|
+
"Wayforth API is not reachable at "
|
|
67
|
+
f"{API_BASE}. Start the API with:\n"
|
|
68
|
+
" cd apps/api && uv run uvicorn main:app --port 8000"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
candidates = [
|
|
72
|
+
s for s in services
|
|
73
|
+
if (category is None or s.get("category") == category)
|
|
74
|
+
and s.get("coverage_tier", 0) <= max_tier
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
ranked = await rank_services(intent, candidates)
|
|
78
|
+
top = ranked[:3]
|
|
79
|
+
|
|
80
|
+
if not top:
|
|
81
|
+
return f"No services found matching '{intent}'" + (
|
|
82
|
+
f" in category '{category}'" if category else ""
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
lines = [f"Top {len(top)} result(s) for \"{intent}\":\n"]
|
|
86
|
+
lines += [_format_ranked_service(i + 1, s) for i, s in enumerate(top)]
|
|
87
|
+
return "\n\n".join(lines)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@mcp.tool()
|
|
91
|
+
async def wayforth_list(category: str = None) -> str:
|
|
92
|
+
"""List all services in the Wayforth catalog.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
category: Optional filter — inference, data, or translation
|
|
96
|
+
"""
|
|
97
|
+
services = await _fetch_services()
|
|
98
|
+
if services is None:
|
|
99
|
+
return (
|
|
100
|
+
"Wayforth API is not reachable at "
|
|
101
|
+
f"{API_BASE}. Start the API with:\n"
|
|
102
|
+
" cd apps/api && uv run uvicorn main:app --port 8000"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
filtered = [
|
|
106
|
+
s for s in services
|
|
107
|
+
if category is None or s.get("category") == category
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
if not filtered:
|
|
111
|
+
return f"No services found" + (f" in category '{category}'" if category else "")
|
|
112
|
+
|
|
113
|
+
header = "All Wayforth services" + (f" — category: {category}" if category else "")
|
|
114
|
+
lines = [f"{header} ({len(filtered)} total):\n"]
|
|
115
|
+
lines += [_format_service(s) for s in filtered]
|
|
116
|
+
return "\n\n".join(lines)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@mcp.tool()
|
|
120
|
+
async def wayforth_status() -> str:
|
|
121
|
+
"""Return catalog stats: service counts by tier and category, plus API health."""
|
|
122
|
+
services = await _fetch_services()
|
|
123
|
+
if services is None:
|
|
124
|
+
return (
|
|
125
|
+
f"API health: UNREACHABLE ({API_BASE})\n"
|
|
126
|
+
"Start with: cd apps/api && uv run uvicorn main:app --port 8000"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
total = len(services)
|
|
130
|
+
by_category: dict[str, int] = {}
|
|
131
|
+
by_tier: dict[int, int] = {}
|
|
132
|
+
|
|
133
|
+
for s in services:
|
|
134
|
+
cat = s.get("category") or "unknown"
|
|
135
|
+
by_category[cat] = by_category.get(cat, 0) + 1
|
|
136
|
+
tier = s.get("coverage_tier", 0)
|
|
137
|
+
by_tier[tier] = by_tier.get(tier, 0) + 1
|
|
138
|
+
|
|
139
|
+
cat_lines = "\n".join(
|
|
140
|
+
f" {cat}: {count}" for cat, count in sorted(by_category.items())
|
|
141
|
+
)
|
|
142
|
+
tier_lines = "\n".join(
|
|
143
|
+
f" tier {t} ({TIER_LABELS.get(t, '?')}): {count}"
|
|
144
|
+
for t, count in sorted(by_tier.items())
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
f"API health: OK ({API_BASE})\n"
|
|
149
|
+
f"Total services: {total}\n\n"
|
|
150
|
+
f"By category:\n{cat_lines}\n\n"
|
|
151
|
+
f"By tier:\n{tier_lines}"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def main():
|
|
156
|
+
mcp.run()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
main()
|