wayforth-mcp 0.1.0__py3-none-any.whl

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.
ranker.py ADDED
@@ -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)
server.py ADDED
@@ -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()
@@ -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,6 @@
1
+ ranker.py,sha256=BXmNibw6IgXqoI8Fdf71D2TNOIUZewo3sYPdKUxE8ZE,2916
2
+ server.py,sha256=VEER4AZESkayl2hTHjmt9jQNtjDhLqdKHZr4qniBFD4,5066
3
+ wayforth_mcp-0.1.0.dist-info/METADATA,sha256=GP4ZN_3ExWJRs10FVjL_Tm7zX9VYJSnzohMUvUJTnr8,2025
4
+ wayforth_mcp-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
5
+ wayforth_mcp-0.1.0.dist-info/entry_points.txt,sha256=K9DKtcVCAs62vn3pPk5l6Baq0Es1wa6c_VB2iFSasJg,45
6
+ wayforth_mcp-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ wayforth-mcp = server:main