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.
- package/LICENSE +21 -0
- package/README.md +410 -0
- package/backends/domain_profiles.json +364 -0
- package/backends/engine_registry.yaml +505 -0
- package/backends/quota_profiles.json +395 -0
- package/bin/argo.js +54 -0
- package/config.yaml +554 -0
- package/package.json +43 -0
- package/scripts/adaptive.py +179 -0
- package/scripts/benchmark.py +124 -0
- package/scripts/cache.py +374 -0
- package/scripts/clarify.py +689 -0
- package/scripts/config.py +262 -0
- package/scripts/crawl.py +73 -0
- package/scripts/engines.py +386 -0
- package/scripts/evidence.py +381 -0
- package/scripts/extract.py +69 -0
- package/scripts/fetch.py +118 -0
- package/scripts/health_check.py +437 -0
- package/scripts/health_probe.py +218 -0
- package/scripts/mcp_diag.py +81 -0
- package/scripts/mcp_server.py +488 -0
- package/scripts/query_rewriter.py +278 -0
- package/scripts/quota.py +196 -0
- package/scripts/research.py +499 -0
- package/scripts/route.py +341 -0
- package/scripts/search.py +508 -0
- package/scripts/search_types.py +72 -0
- package/scripts/tfidf_router.py +312 -0
- package/sub-skills/local-search/SKILL.md +104 -0
- package/sub-skills/local-search/config.yaml +328 -0
- package/sub-skills/local-search/engine_registry.py +298 -0
- package/sub-skills/local-search/health_check.py +347 -0
- package/sub-skills/local-search/local_search_adapter.py +56 -0
- package/sub-skills/local-search/parse_maps.yaml +184 -0
- package/sub-skills/local-search/search_v3.py +558 -0
- package/sub-skills/local-search/smart_router.py +215 -0
|
@@ -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()
|