webify-mcp 0.0.1__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.
mcp_server.py ADDED
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env python3
2
+ """Webify MCP Server — stdio transport.
3
+
4
+ Exposes Webify as MCP tools for Claude Code / any MCP client.
5
+
6
+ Setup:
7
+ claude mcp add webify -- python3 /path/to/mcp_server.py
8
+
9
+ Tools:
10
+ web_lookup(url, query) — Smart lookup with graph retrieval + confidence
11
+ web_find(query) — Search web + parallel multi-source retrieval
12
+ web_build(url) — Pre-build graph for a URL (cache it)
13
+ web_stats(url) — Show graph stats for a cached URL
14
+ """
15
+
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
20
+
21
+ from mcp.server.fastmcp import FastMCP
22
+ import webify
23
+
24
+ mcp = FastMCP(
25
+ "webify",
26
+ instructions=(
27
+ "Webify provides efficient web lookup via semantic graph retrieval. "
28
+ "Use web_find(query) to search the web and get a synthesized, multi-source answer — "
29
+ "it searches DuckDuckGo, builds semantic graphs in parallel, extracts structurally "
30
+ "relevant subtrees from multiple anchors per page, then synthesizes with Haiku. "
31
+ "Result quality matches DeepSearch at 1-5% of the token cost. "
32
+ "Use web_lookup(url, query) when you already know the URL. "
33
+ "If web_lookup returns status='fallback_needed', use WebFetch instead."
34
+ ),
35
+ )
36
+
37
+
38
+ @mcp.tool()
39
+ def web_lookup(url: str, query: str, max_results: int = 3) -> dict:
40
+ """Look up specific information from a web page using graph-based retrieval.
41
+
42
+ Fetches the page (cached after first hit), builds a semantic graph, then
43
+ retrieves only the relevant nodes via BFS traversal. Returns ~250-750 tokens
44
+ instead of the full page (5000-50000 tokens).
45
+
46
+ Args:
47
+ url: The web page URL to look up.
48
+ query: What you're looking for (e.g. "authentication setup", "API rate limits").
49
+ max_results: Max nodes to return (default 3, usually sufficient).
50
+
51
+ Returns:
52
+ status: "success" | "low_confidence" | "fallback_needed"
53
+ content: The retrieved content (relevant sections only).
54
+ confidence: Graph quality assessment.
55
+ tokens_used: How many tokens the response costs.
56
+ fallback_reason: Why to use WebFetch instead (if fallback_needed).
57
+ """
58
+ return webify.smart_lookup(url, query, max_results=max_results)
59
+
60
+
61
+ @mcp.tool()
62
+ def web_find(query: str, num_sources: int = 0, synthesize: bool = True) -> dict:
63
+ """Search the web and return a synthesized, high-quality answer from multiple sources.
64
+
65
+ Pipeline: DuckDuckGo search → parallel semantic graph builds → multi-aspect
66
+ BM25 extraction → Haiku synthesis. Adaptively scales depth based on query
67
+ complexity (more sources + broader retrieval for multi-dimensional questions).
68
+
69
+ No hard cap on sources. For exhaustive research, call web_find multiple times
70
+ with different sub-queries — each call runs its own search and graph builds.
71
+ This is how you get deep-research-level coverage through Webify.
72
+
73
+ Args:
74
+ query: What to search for (e.g. "gut microbiome mental health evidence").
75
+ num_sources: Sources to fetch per search. 0 = auto-scale by complexity (3-6).
76
+ Pass higher (8, 10, 12) for broader single-query coverage.
77
+ For exhaustive research, prefer multiple calls with focused queries.
78
+ synthesize: Set False to return raw fragments instead of synthesized answer.
79
+
80
+ Returns:
81
+ status: "success" | "partial" | "no_results"
82
+ content: Synthesized answer with source-attributed fragments appended
83
+ for complex queries (gives the caller full context).
84
+ sources: [{url, title, confidence, tokens}]
85
+ tokens_used: Tokens in response.
86
+ search_results: Raw DDG results for transparency.
87
+ """
88
+ result = webify.web_find(query, num_sources=num_sources, synthesize=synthesize)
89
+
90
+ # For complex queries: append raw fragments so the calling model has full
91
+ # material to synthesize from (Haiku synthesis is concise; the caller may
92
+ # want the underlying evidence for deeper answers)
93
+ complexity = webify._query_complexity(query)
94
+ if complexity >= 2 and synthesize and result.get("raw_fragments"):
95
+ result["content"] = (
96
+ result["content"] +
97
+ "\n\n---\n## Source Fragments (for additional detail)\n\n" +
98
+ result["raw_fragments"]
99
+ )
100
+ result["tokens_used"] = result.get("fragment_tokens", 0) + result.get("tokens_used", 0)
101
+
102
+ result.pop("raw_fragments", None)
103
+ return result
104
+
105
+
106
+ @mcp.tool()
107
+ def web_build(url: str, force_refresh: bool = False) -> dict:
108
+ """Pre-build/refresh the graph for a URL. Useful for pages you'll query multiple times.
109
+
110
+ Args:
111
+ url: The web page URL to index.
112
+ force_refresh: Re-fetch even if cached.
113
+ """
114
+ try:
115
+ graph = webify.build_graph(url, force_refresh=force_refresh)
116
+ return {
117
+ "ok": True,
118
+ "url": url,
119
+ "title": graph.get("title", "")[:100],
120
+ "stats": graph.get("stats", {}),
121
+ "confidence": graph.get("confidence", {}),
122
+ "extraction_method": graph.get("extraction_method", "unknown"),
123
+ }
124
+ except Exception as e:
125
+ return {"ok": False, "error": str(e)}
126
+
127
+
128
+ @mcp.tool()
129
+ def web_stats(url: str) -> dict:
130
+ """Show graph statistics for a previously fetched URL.
131
+
132
+ Args:
133
+ url: The URL to check stats for.
134
+ """
135
+ try:
136
+ graph = webify.build_graph(url, force_refresh=False)
137
+ return {
138
+ "ok": True,
139
+ "url": url,
140
+ "title": graph.get("title", "")[:100],
141
+ "stats": graph.get("stats", {}),
142
+ "confidence": graph.get("confidence", {}),
143
+ "cached": True,
144
+ }
145
+ except Exception as e:
146
+ return {"ok": False, "error": str(e)}
147
+
148
+
149
+ def main():
150
+ mcp.run(transport="stdio")
151
+
152
+
153
+ if __name__ == "__main__":
154
+ main()