argo-search 1.0.1

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,81 @@
1
+ #!/usr/bin/env python3
2
+ """诊断版 MCP 服务器——记录所有 stdin 到日志"""
3
+ import sys, os, time, json
4
+
5
+ LOG = os.path.expanduser("~/.kimi/argo_diag.log")
6
+ PID = os.getpid()
7
+
8
+ with open(LOG, "a") as log:
9
+ log.write(f"\n=== PID={PID} START {time.strftime('%H:%M:%S')} ===\n")
10
+ log.write(f"args: {sys.argv}\n")
11
+ log.write(f"python: {sys.executable}\n")
12
+ log.write(f"cwd: {os.getcwd()}\n")
13
+ log.write(f"PATH: {os.environ.get('PATH','')}\n")
14
+ log.flush()
15
+
16
+ def _send(data):
17
+ encoded = json.dumps(data, ensure_ascii=False).encode()
18
+ sys.stdout.buffer.write(f"Content-Length: {len(encoded)}\r\n\r\n".encode())
19
+ sys.stdout.buffer.write(encoded)
20
+ sys.stdout.buffer.flush()
21
+ log.write(f" -> SEND {len(encoded)}B\n")
22
+ log.flush()
23
+
24
+ while True:
25
+ header = sys.stdin.buffer.readline()
26
+ if not header:
27
+ log.write(" EOF — stdin closed\n")
28
+ log.flush()
29
+ break
30
+
31
+ header_str = header.decode("utf-8", errors="replace").strip()
32
+ log.write(f" <- HEADER: {repr(header_str)}\n")
33
+ log.flush()
34
+
35
+ if not header_str:
36
+ log.write(" (empty header, skip)\n")
37
+ log.flush()
38
+ continue
39
+
40
+ if header_str.startswith("Content-Length:"):
41
+ length = int(header_str.split(":")[1].strip())
42
+ sys.stdin.buffer.readline() # blank line
43
+ body = sys.stdin.buffer.read(length).decode("utf-8")
44
+ log.write(f" <- BODY({length}B): {body[:200]}\n")
45
+ log.flush()
46
+ try:
47
+ req = json.loads(body)
48
+ method = req.get("method", "?")
49
+ reqid = req.get("id", "?")
50
+ log.write(f" <- METHOD: {method} id={reqid}\n")
51
+ log.flush()
52
+
53
+ if method == "initialize":
54
+ _send({"jsonrpc":"2.0","id":reqid,"result":{
55
+ "protocolVersion":"2024-11-05",
56
+ "capabilities":{"tools":{"listChanged":False}},
57
+ "serverInfo":{"name":"argo-diag","version":"0"}
58
+ }})
59
+ elif method == "tools/list":
60
+ _send({"jsonrpc":"2.0","id":reqid,"result":{"tools":[]}})
61
+ elif method.startswith("notifications/"):
62
+ log.write(f" notification, no reply\n")
63
+ log.flush()
64
+ else:
65
+ log.write(f" unknown method: {method}\n")
66
+ log.flush()
67
+ except Exception as e:
68
+ log.write(f" PARSE ERROR: {e}\n")
69
+ log.flush()
70
+ else:
71
+ # 行模式
72
+ log.write(f" <- LINE MODE: {header_str[:100]}\n")
73
+ log.flush()
74
+ try:
75
+ req = json.loads(header_str)
76
+ log.write(f" <- METHOD(line): {req.get('method','?')}\n")
77
+ log.flush()
78
+ except:
79
+ log.write(f" not JSON: {header_str[:50]}\n")
80
+ log.flush()
81
+
@@ -0,0 +1,488 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ mcp_server.py — Argo MCP 服务层
4
+
5
+ 将 research/evidence/clarify 三个工具暴露为 MCP tool,
6
+ 通过 JSON-RPC over stdio 与 Grok/Claude 等客户端通信。
7
+
8
+ 用法:
9
+ python3 mcp_server.py # 启动 MCP stdio 服务
10
+ python3 mcp_server.py --test # 本地测试模式
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import os
17
+ import sys
18
+ import traceback
19
+ from typing import Any
20
+
21
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
22
+ sys.path.insert(0, SCRIPT_DIR)
23
+
24
+ # 进程级共享缓存:同一 MCP 会话中 search/evidence/research 共用
25
+ _cache_instance = None
26
+
27
+ def _get_cache():
28
+ global _cache_instance
29
+ if _cache_instance is None:
30
+ from cache import SearchCache
31
+ _cache_instance = SearchCache()
32
+ return _cache_instance
33
+
34
+
35
+ # ── 工具定义(MCP schema) ────────────────────────────────────────────────────
36
+
37
+ TOOLS = [
38
+ {
39
+ "name": "argo_search",
40
+ "description": "统一搜索引擎:47 个引擎(22 远程 + 25 本地)统一搜索,支持 TF-IDF 语义路由 + RRF 多引擎融合 + Bocha 语义精排 + 双层缓存。适用于所有通用搜索场景:查资料、找答案、搜新闻、学术检索、代码搜索、中文内容搜索等。",
41
+ "inputSchema": {
42
+ "type": "object",
43
+ "properties": {
44
+ "query": {
45
+ "type": "string",
46
+ "description": "搜索查询词(支持中英文)"
47
+ },
48
+ "engine": {
49
+ "type": "string",
50
+ "description": "指定搜索引擎(默认 auto,可选 anysearch/zhihu/eastmoney/arxiv/wigolo/duckduckgo/byted/bocha/tavily/github/wikipedia/semantic_scholar/local_search 等)",
51
+ "default": "auto"
52
+ },
53
+ "max_results": {
54
+ "type": "integer",
55
+ "description": "最大结果数(默认 5)",
56
+ "default": 5,
57
+ "minimum": 1,
58
+ "maximum": 20
59
+ },
60
+ "depth": {
61
+ "type": "string",
62
+ "enum": ["fast", "balanced", "deep"],
63
+ "description": "搜索深度(默认 fast)",
64
+ "default": "fast"
65
+ },
66
+ "mode": {
67
+ "type": "string",
68
+ "enum": ["fast", "auto", "deep", "budget"],
69
+ "description": "预算模式:fast=免费优先, auto=成本感知, deep=质量优先, budget=配额控制(默认 auto)",
70
+ "default": "auto"
71
+ },
72
+ "skip_cache": {
73
+ "type": "boolean",
74
+ "description": "跳过缓存(默认 false)",
75
+ "default": False
76
+ },
77
+ "summary": {
78
+ "type": "boolean",
79
+ "description": "精简模式:snippet 截断到 80 字符,节省 LLM token(默认 false)",
80
+ "default": False
81
+ }
82
+ },
83
+ "required": ["query"]
84
+ }
85
+ },
86
+ {
87
+ "name": "argo_research",
88
+ "description": "深度研究:将复杂查询分解为子问题,多源并行采集,输出综合报告+引用+知识缺口。适用于学术综述、事实核查、竞品分析、技术选型等需要多步搜索的场景。",
89
+ "inputSchema": {
90
+ "type": "object",
91
+ "properties": {
92
+ "query": {
93
+ "type": "string",
94
+ "description": "研究查询(可以是复杂的、多步骤的问题)"
95
+ },
96
+ "num_sub_queries": {
97
+ "type": "integer",
98
+ "description": "子查询数量(默认4,最大8)",
99
+ "default": 4,
100
+ "minimum": 2,
101
+ "maximum": 8
102
+ },
103
+ "max_results": {
104
+ "type": "integer",
105
+ "description": "每个子查询最大结果数(默认5)",
106
+ "default": 5
107
+ },
108
+ "depth": {
109
+ "type": "string",
110
+ "enum": ["fast", "balanced", "deep"],
111
+ "description": "搜索深度(默认balanced)",
112
+ "default": "balanced"
113
+ },
114
+ "mode": {
115
+ "type": "string",
116
+ "enum": ["fast", "auto", "deep", "budget"],
117
+ "description": "预算模式(默认auto)",
118
+ "default": "auto"
119
+ }
120
+ },
121
+ "required": ["query"]
122
+ }
123
+ },
124
+ {
125
+ "name": "argo_evidence",
126
+ "description": "来源可信度评估:对搜索结果进行权威性+时效性+交叉验证的综合评分,输出每个结果的可信度分解。适用于事实核查、高后果决策、学术引用等需要评估来源可靠性的场景。",
127
+ "inputSchema": {
128
+ "type": "object",
129
+ "properties": {
130
+ "query": {
131
+ "type": "string",
132
+ "description": "搜索查询词(用于交叉验证)"
133
+ },
134
+ "results_json": {
135
+ "type": "string",
136
+ "description": "搜索结果 JSON 字符串(可选;为空时自动调用 super_search 搜索)。格式:{\"results\": [{\"title\": \"...\", \"url\": \"...\", \"snippet\": \"...\", \"source\": \"...\", \"score\": 0.8}]}"
137
+ },
138
+ "max_results": {
139
+ "type": "integer",
140
+ "description": "自动搜索时的最大结果数(默认 10,仅在 results_json 为空时有效)",
141
+ "default": 10
142
+ }
143
+ },
144
+ "required": ["query"]
145
+ }
146
+ },
147
+ {
148
+ "name": "argo_clarify",
149
+ "description": "意图消歧:分析查询中的歧义词、多义实体,给出意图分类和推荐搜索策略。适用于查询含歧义词(如「苹果」=公司/水果、「Python」=语言/蛇)或意图不明确的场景。",
150
+ "inputSchema": {
151
+ "type": "object",
152
+ "properties": {
153
+ "query": {
154
+ "type": "string",
155
+ "description": "需要消歧的搜索查询"
156
+ }
157
+ },
158
+ "required": ["query"]
159
+ }
160
+ },
161
+ {
162
+ "name": "argo_crawl",
163
+ "description": "站点级爬取:通过 sitemap.xml 或 BFS 策略批量抓取站点页面,输出页面 URL、正文片段和深度。适用于整站内容审计、站内多页对比、批量抓取等场景。",
164
+ "inputSchema": {
165
+ "type": "object",
166
+ "properties": {
167
+ "url": {
168
+ "type": "string",
169
+ "description": "目标站点根 URL(如 https://docs.python.org/)"
170
+ },
171
+ "strategy": {
172
+ "type": "string",
173
+ "enum": ["sitemap", "bfs"],
174
+ "description": "爬取策略(默认 bfs)",
175
+ "default": "bfs"
176
+ },
177
+ "max_pages": {
178
+ "type": "integer",
179
+ "description": "最大抓取页面数(默认 10,sitemap 策略默认 20)",
180
+ "default": 10,
181
+ "minimum": 1,
182
+ "maximum": 50
183
+ },
184
+ "max_depth": {
185
+ "type": "integer",
186
+ "description": "BFS 最大深度(默认 2)",
187
+ "default": 2,
188
+ "minimum": 1,
189
+ "maximum": 5
190
+ },
191
+ "timeout": {
192
+ "type": "integer",
193
+ "description": "单页超时秒数(默认 8)",
194
+ "default": 8,
195
+ "minimum": 3,
196
+ "maximum": 30
197
+ }
198
+ },
199
+ "required": ["url"]
200
+ }
201
+ },
202
+ {
203
+ "name": "argo_extract",
204
+ "description": "结构化数据提取:从页面 HTML 中抽取表格、<meta> 元数据、OpenGraph、JSON-LD 等结构化信息。适用于价格表抽取、SEO 元数据分析、富媒体结构化数据解析等场景。",
205
+ "inputSchema": {
206
+ "type": "object",
207
+ "properties": {
208
+ "url": {
209
+ "type": "string",
210
+ "description": "目标页面 URL"
211
+ },
212
+ "mode": {
213
+ "type": "string",
214
+ "enum": ["tables", "metadata", "jsonld", "all"],
215
+ "description": "提取模式(默认 all)",
216
+ "default": "all"
217
+ }
218
+ },
219
+ "required": ["url"]
220
+ }
221
+ },
222
+ ]
223
+
224
+
225
+ # ── 工具执行 ──────────────────────────────────────────────────────────────────
226
+
227
+ def execute_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
228
+ """执行 MCP 工具。"""
229
+ try:
230
+ if name == "argo_search":
231
+ from search import super_search
232
+ result = super_search(
233
+ query=arguments["query"],
234
+ engine=arguments.get("engine", "auto"),
235
+ n=arguments.get("max_results", 5),
236
+ skip_cache=arguments.get("skip_cache", False),
237
+ timeout=arguments.get("timeout", 10),
238
+ depth=arguments.get("depth", "fast"),
239
+ mode=arguments.get("mode", "auto"),
240
+ cache=_get_cache(),
241
+ )
242
+ # 精简模式:截断 snippet
243
+ if arguments.get("summary", False):
244
+ for r in result.get("results", []):
245
+ if r.get("snippet"):
246
+ r["snippet"] = r["snippet"][:80]
247
+ return {"content": [{"type": "text", "text": json.dumps(result, ensure_ascii=False, indent=2)}]}
248
+
249
+ elif name == "argo_research":
250
+ from research import deep_research
251
+ result = deep_research(
252
+ query=arguments["query"],
253
+ num_sub_queries=arguments.get("num_sub_queries", 4),
254
+ max_results=arguments.get("max_results", 5),
255
+ depth=arguments.get("depth", "balanced"),
256
+ mode=arguments.get("mode", "auto"),
257
+ )
258
+ return {"content": [{"type": "text", "text": json.dumps(result, ensure_ascii=False, indent=2)}]}
259
+
260
+ elif name == "argo_evidence":
261
+ from evidence import compute_credibility
262
+ results_json_str = arguments.get("results_json", "")
263
+ # results_json 为空时自动调用 super_search 获取结果
264
+ if not results_json_str or not results_json_str.strip():
265
+ from search import super_search
266
+ search_result = super_search(
267
+ query=arguments["query"],
268
+ n=arguments.get("max_results", 10),
269
+ depth="fast",
270
+ mode="auto",
271
+ cache=_get_cache(),
272
+ )
273
+ results = search_result.get("results", [])
274
+ else:
275
+ results_data = json.loads(results_json_str)
276
+ results = results_data.get("results", [])
277
+ result = compute_credibility(results, arguments["query"])
278
+ return {"content": [{"type": "text", "text": json.dumps(result, ensure_ascii=False, indent=2)}]}
279
+
280
+ elif name == "argo_clarify":
281
+ from clarify import analyze_query, recommend_routing
282
+ analysis = analyze_query(arguments["query"])
283
+ routing = recommend_routing(analysis)
284
+ analysis["routing"] = routing
285
+ return {"content": [{"type": "text", "text": json.dumps(analysis, ensure_ascii=False, indent=2)}]}
286
+
287
+ elif name == "argo_crawl":
288
+ from crawl import crawl_bfs, crawl_sitemap
289
+ strategy = arguments.get("strategy", "bfs")
290
+ max_pages = arguments.get("max_pages", 10)
291
+ max_depth = arguments.get("max_depth", 2)
292
+ timeout = arguments.get("timeout", 8)
293
+ if strategy == "sitemap":
294
+ result = crawl_sitemap(arguments["url"], max_pages=max_pages, timeout=timeout)
295
+ else:
296
+ result = crawl_bfs(arguments["url"], max_pages=max_pages, max_depth=max_depth, timeout=timeout)
297
+ return {"content": [{"type": "text", "text": json.dumps(result, ensure_ascii=False, indent=2)}]}
298
+
299
+ elif name == "argo_extract":
300
+ from extract import extract_tables, extract_metadata, extract_jsonld
301
+ from fetch import fetch_page
302
+ mode = arguments.get("mode", "all")
303
+ fetch_result = fetch_page(arguments["url"], max_chars=50000, timeout=15, raw=True)
304
+ if not fetch_result["success"]:
305
+ return {"content": [{"type": "text", "text": json.dumps({"error": fetch_result.get("error", "fetch failed")}, ensure_ascii=False)}], "isError": True}
306
+ html = fetch_result["html"]
307
+ output = {}
308
+ if mode in ("tables", "all"):
309
+ output["tables"] = extract_tables(html)
310
+ if mode in ("metadata", "all"):
311
+ output["metadata"] = extract_metadata(html)
312
+ if mode in ("jsonld", "all"):
313
+ output["jsonld"] = extract_jsonld(html)
314
+ output["url"] = fetch_result["url"]
315
+ return {"content": [{"type": "text", "text": json.dumps(output, ensure_ascii=False, indent=2)}]}
316
+
317
+ else:
318
+ return {"error": {"code": -32601, "message": f"Unknown tool: {name}"}}
319
+
320
+ except Exception as e:
321
+ return {
322
+ "content": [{"type": "text", "text": json.dumps({"error": {"code": -32000, "message": f"{type(e).__name__}: {e}"}}, ensure_ascii=False)}],
323
+ "isError": True
324
+ }
325
+
326
+
327
+ # ── MCP JSON-RPC 处理 ────────────────────────────────────────────────────────
328
+
329
+ def handle_rpc(method: str, params: dict[str, Any]) -> dict[str, Any]:
330
+ """处理 JSON-RPC 请求。"""
331
+ if method == "initialize":
332
+ return {
333
+ "protocolVersion": "2024-11-05",
334
+ "capabilities": {"tools": {"listChanged": False}},
335
+ "serverInfo": {
336
+ "name": "argo",
337
+ "version": "1.0.1"
338
+ },
339
+ "instructions": "Argo MCP 提供 6 个工具:argo_search(47 引擎统一搜索)、argo_research(深度研究)、argo_evidence(可信度评估)、argo_clarify(意图消歧)、argo_crawl(站点爬取)、argo_extract(结构化数据提取)。底层使用 47 个搜索引擎的统一搜索基础设施,支持 TF-IDF 语义路由、RRF 多引擎融合、Bocha 语义精排、双层缓存和成本感知预算控制。"
340
+ }
341
+
342
+ elif method == "tools/list":
343
+ return {"tools": TOOLS}
344
+
345
+ elif method == "tools/call":
346
+ tool_name = params.get("name", "")
347
+ arguments = params.get("arguments", {})
348
+ return execute_tool(tool_name, arguments)
349
+
350
+ elif method == "ping":
351
+ return {}
352
+
353
+ elif method.startswith("notifications/"):
354
+ # 通知消息无需回复
355
+ return None
356
+
357
+ else:
358
+ return {"error": {"code": -32601, "message": f"Method not found: {method}"}}
359
+
360
+
361
+ def run_stdio():
362
+ """运行 MCP stdio 服务。MCP 帧协议:Content-Length: N\\r\\n\\r\\n{json}"""
363
+ import sys, os, time as _time
364
+ try:
365
+ with open(os.path.expanduser("~/.kimi/argo_diag.log"), "a") as _log:
366
+ _log.write(f"=== PID={os.getpid()} ENTER run_stdio {_time.strftime('%H:%M:%S')} ===\n")
367
+ _log.write(f"python={sys.executable} cwd={os.getcwd()}\n")
368
+ _log.flush()
369
+ except: pass
370
+
371
+ while True:
372
+ try:
373
+ # 读取 Content-Length 头
374
+ header = sys.stdin.buffer.readline()
375
+ if not header:
376
+ break # EOF
377
+ header_str = header.decode("utf-8", errors="replace").strip()
378
+ if not header_str:
379
+ continue
380
+ if not header_str.startswith("Content-Length:"):
381
+ # 兼容行模式(某些客户端不发 Content-Length)
382
+ try:
383
+ request = json.loads(header_str)
384
+ except json.JSONDecodeError:
385
+ _send_error(None, -32700, "Parse error")
386
+ continue
387
+ else:
388
+ length = int(header_str.split(":")[1].strip())
389
+ sys.stdin.buffer.readline() # skip blank line
390
+ body = sys.stdin.buffer.read(length).decode("utf-8")
391
+ request = json.loads(body)
392
+
393
+ method = request.get("method", "")
394
+ params = request.get("params", {})
395
+ request_id = request.get("id")
396
+
397
+ response = handle_rpc(method, params)
398
+
399
+ # 通知消息无需回复
400
+ if response is None:
401
+ continue
402
+
403
+ if request_id is not None:
404
+ response["jsonrpc"] = "2.0"
405
+ response["id"] = request_id
406
+ _send_response(response)
407
+
408
+ except json.JSONDecodeError:
409
+ _send_error(None, -32700, "Parse error")
410
+ except Exception as e:
411
+ _send_error(None, -32000, f"Internal error: {e}")
412
+
413
+
414
+ def _send_response(response: dict):
415
+ """发送 MCP 帧响应。"""
416
+ data = json.dumps(response, ensure_ascii=False)
417
+ encoded = data.encode("utf-8")
418
+ sys.stdout.buffer.write(f"Content-Length: {len(encoded)}\r\n\r\n".encode())
419
+ sys.stdout.buffer.write(encoded)
420
+ sys.stdout.buffer.flush()
421
+
422
+
423
+ def _send_error(request_id, code: int, message: str):
424
+ """发送 JSON-RPC 错误响应。"""
425
+ resp = {
426
+ "jsonrpc": "2.0",
427
+ "id": request_id,
428
+ "error": {"code": code, "message": message}
429
+ }
430
+ _send_response(resp)
431
+
432
+
433
+ # ── 测试模式 ──────────────────────────────────────────────────────────────────
434
+
435
+ def test_mode():
436
+ """本地测试。"""
437
+ print("=== Argo MCP 工具测试 ===\n")
438
+
439
+ # 测试 search
440
+ print("--- argo_search 测试(fast模式)---")
441
+ result = execute_tool("argo_search", {
442
+ "query": "Python async best practices",
443
+ "max_results": 3,
444
+ "depth": "fast",
445
+ "mode": "fast",
446
+ })
447
+ print(result["content"][0]["text"][:500])
448
+ print()
449
+
450
+ # 测试 clarify
451
+ print("--- clarify 测试 ---")
452
+ result = execute_tool("argo_clarify", {"query": "Python 吞苹果 兼容吗"})
453
+ print(result["content"][0]["text"][:500])
454
+ print()
455
+
456
+ # 测试 research(快速模式)
457
+ print("--- research 测试(fast模式)---")
458
+ result = execute_tool("argo_research", {
459
+ "query": "React Server Components 2025 生产环境案例",
460
+ "num_sub_queries": 2,
461
+ "max_results": 3,
462
+ "depth": "fast",
463
+ "mode": "fast",
464
+ })
465
+ text = result["content"][0]["text"]
466
+ # 只打印前 500 字符
467
+ print(text[:500])
468
+ print()
469
+
470
+ # 测试 evidence
471
+ print("--- evidence 测试 ---")
472
+ sample_results = json.dumps({
473
+ "results": [
474
+ {"title": "Python docs", "url": "https://docs.python.org", "snippet": "Official Python documentation", "source": "wikipedia", "score": 0.9},
475
+ {"title": "Some blog", "url": "https://random-blog.com/python", "snippet": "Python tutorial", "source": "duckduckgo", "score": 0.6},
476
+ ]
477
+ })
478
+ result = execute_tool("argo_evidence", {"query": "Python tutorial", "results_json": sample_results})
479
+ print(result["content"][0]["text"][:500])
480
+
481
+
482
+ # ── 入口 ──────────────────────────────────────────────────────────────────────
483
+
484
+ if __name__ == "__main__":
485
+ if "--test" in sys.argv:
486
+ test_mode()
487
+ else:
488
+ run_stdio()