serper-toolkit 0.0.4__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.
@@ -0,0 +1,4 @@
1
+ # serper/__init__.py
2
+ from .server import main
3
+
4
+ __all__ = ["main"]
@@ -0,0 +1,10 @@
1
+ # serper_toolkit/__main__.py
2
+
3
+ from . import server
4
+
5
+ def main():
6
+ """Package entry point."""
7
+ server.main()
8
+
9
+ if __name__ == "__main__":
10
+ main()
@@ -0,0 +1,248 @@
1
+ {
2
+ "AF": ["AF", "Afghanistan", "Islamic Republic of Afghanistan", "AFG", "阿富汗", "阿富汗伊斯兰共和国"],
3
+ "AL": ["AL", "Albania", "Republic of Albania", "ALB", "阿尔巴尼亚"],
4
+ "DZ": ["DZ", "Algeria", "People's Democratic Republic of Algeria", "DZA", "阿尔及利亚"],
5
+ "AS": ["AS", "American Samoa", "AS", "美属萨摩亚"],
6
+ "AD": ["AD", "Andorra", "Principality of Andorra", "AND", "安道尔"],
7
+ "AO": ["AO", "Angola", "Republic of Angola", "AGO", "安哥拉"],
8
+ "AI": ["AI", "Anguilla", "AI", "安圭拉"],
9
+ "AQ": ["AQ", "Antarctica", "AQ", "南极洲"],
10
+ "AG": ["AG", "Antigua and Barbuda", "Antigua & Barbuda", "ATG", "安提瓜和巴布达"],
11
+ "AR": ["AR", "Argentina", "Argentine Republic", "ARG", "阿根廷"],
12
+ "AM": ["AM", "Armenia", "Republic of Armenia", "ARM", "亚美尼亚"],
13
+ "AW": ["AW", "Aruba", "AW", "阿鲁巴"],
14
+ "AU": ["AU", "Australia", "Commonwealth of Australia", "AUS", "澳大利亚", "Australia", "Down Under", "Oz"],
15
+ "AT": ["AT", "Austria", "Republic of Austria", "AUT", "奥地利"],
16
+ "AZ": ["AZ", "Azerbaijan", "Republic of Azerbaijan", "AZE", "阿塞拜疆"],
17
+ "BS": ["BS", "Bahamas", "Commonwealth of The Bahamas", "BHS", "巴哈马", "The Bahamas"],
18
+ "BH": ["BH", "Bahrain", "Kingdom of Bahrain", "BHR", "巴林"],
19
+ "BD": ["BD", "Bangladesh", "People's Republic of Bangladesh", "BGD", "孟加拉国", "孟加拉"],
20
+ "BB": ["BB", "Barbados", "BB", "巴巴多斯"],
21
+ "BY": ["BY", "Belarus", "Republic of Belarus", "BLR", "白俄罗斯"],
22
+ "BE": ["BE", "Belgium", "Kingdom of Belgium", "BEL", "比利时"],
23
+ "BZ": ["BZ", "Belize", "BZE", "伯利兹"],
24
+ "BJ": ["BJ", "Benin", "Republic of Benin", "BEN", "贝宁"],
25
+ "BM": ["BM", "Bermuda", "BM", "百慕大"],
26
+ "BT": ["BT", "Bhutan", "Kingdom of Bhutan", "BTN", "不丹"],
27
+ "BO": ["BO", "Bolivia", "Plurinational State of Bolivia", "BOL", "玻利维亚"],
28
+ "BA": ["BA", "Bosnia and Herzegovina", "BiH", "BA", "波黑", "波斯尼亚和黑塞哥维那"],
29
+ "BW": ["BW", "Botswana", "Republic of Botswana", "BWA", "博茨瓦纳"],
30
+ "BV": ["BV", "Bouvet Island", "Bouvetøya", "BV", "布韦岛"],
31
+ "BR": ["BR", "Brazil", "Federative Republic of Brazil", "BRA", "巴西"],
32
+ "IO": ["IO", "British Indian Ocean Territory", "BIOT", "英属印度洋领地"],
33
+ "BN": ["BN", "Brunei", "Brunei Darussalam", "BRN", "文莱", "文莱达鲁萨兰国"],
34
+ "BG": ["BG", "Bulgaria", "Republic of Bulgaria", "BGR", "保加利亚"],
35
+ "BF": ["BF", "Burkina Faso", "BFA", "布基纳法索"],
36
+ "BI": ["BI", "Burundi", "Republic of Burundi", "BDI", "布隆迪"],
37
+ "KH": ["KH", "Cambodia", "Kingdom of Cambodia", "KHM", "柬埔寨"],
38
+ "CM": ["CM", "Cameroon", "Republic of Cameroon", "CMR", "喀麦隆"],
39
+ "CA": ["CA", "Canada", "CA", "加拿大"],
40
+ "CV": ["CV", "Cape Verde", "Cabo Verde", "CPV", "佛得角", "开普绿洲"],
41
+ "KY": ["KY", "Cayman Islands", "KY", "开曼群岛"],
42
+ "CF": ["CF", "Central African Republic", "CAR", "中非共和国", "中非"],
43
+ "TD": ["TD", "Chad", "TCD", "乍得"],
44
+ "CL": ["CL", "Chile", "Republic of Chile", "CHL", "智利"],
45
+ "CN": ["CN", "China", "People's Republic of China", "PRC", "CHN", "中国", "中华人民共和国", "Mainland China", "大陆"],
46
+ "CX": ["CX", "Christmas Island", "CX", "圣诞岛"],
47
+ "CC": ["CC", "Cocos (Keeling) Islands", "CC", "科科斯(基林)群岛"],
48
+ "CO": ["CO", "Colombia", "Republic of Colombia", "COL", "哥伦比亚"],
49
+ "KM": ["KM", "Comoros", "Union of the Comoros", "COM", "科摩罗"],
50
+ "CG": ["CG", "Congo", "Republic of the Congo", "COG", "刚果(布)", "Congo-Brazzaville"],
51
+ "CD": ["CD", "Congo (Democratic Republic)", "DRC", "COD", "刚果(金)", "Democratic Republic of the Congo"],
52
+ "CK": ["CK", "Cook Islands", "CK", "库克群岛"],
53
+ "CR": ["CR", "Costa Rica", "Republic of Costa Rica", "CRI", "哥斯达黎加"],
54
+ "CI": ["CI", "Ivory Coast", "Côte d'Ivoire", "CIV", "科特迪瓦", "象牙海岸"],
55
+ "HR": ["HR", "Croatia", "Republic of Croatia", "HRV", "克罗地亚"],
56
+ "CU": ["CU", "Cuba", "Republic of Cuba", "CUB", "古巴"],
57
+ "CY": ["CY", "Cyprus", "Republic of Cyprus", "CYP", "塞浦路斯"],
58
+ "CZ": ["CZ", "Czechia", "Czech Republic", "CZE", "捷克", "捷克共和国"],
59
+ "DK": ["DK", "Denmark", "Kingdom of Denmark", "DNK", "丹麦"],
60
+ "DJ": ["DJ", "Djibouti", "Republic of Djibouti", "DJI", "吉布提"],
61
+ "DM": ["DM", "Dominica", "DM", "多米尼克"],
62
+ "DO": ["DO", "Dominican Republic", "DOM", "多米尼加共和国"],
63
+ "EC": ["EC", "Ecuador", "ECU", "厄瓜多尔"],
64
+ "EG": ["EG", "Egypt", "Arab Republic of Egypt", "EGY", "埃及"],
65
+ "SV": ["SV", "El Salvador", "SLV", "萨尔瓦多"],
66
+ "GQ": ["GQ", "Equatorial Guinea", "GNQ", "赤道几内亚"],
67
+ "ER": ["ER", "Eritrea", "ERI", "厄立特里亚"],
68
+ "EE": ["EE", "Estonia", "EST", "爱沙尼亚"],
69
+ "SZ": ["SZ", "Eswatini", "Kingdom of Eswatini", "SWZ", "斯威士兰", "埃斯瓦蒂尼"],
70
+ "ET": ["ET", "Ethiopia", "Federal Democratic Republic of Ethiopia", "ETH", "埃塞俄比亚"],
71
+ "FK": ["FK", "Falkland Islands", "FK", "福克兰群岛", "马尔维纳斯"],
72
+ "FO": ["FO", "Faroe Islands", "FO", "法罗群岛"],
73
+ "FJ": ["FJ", "Fiji", "FJI", "斐济"],
74
+ "FI": ["FI", "Finland", "Republic of Finland", "FIN", "芬兰"],
75
+ "FR": ["FR", "France", "French Republic", "FRA", "法国"],
76
+ "GF": ["GF", "French Guiana", "FG", "法属圭亚那"],
77
+ "PF": ["PF", "French Polynesia", "PF", "法属玻利尼西亚"],
78
+ "TF": ["TF", "French Southern Territories", "TF", "法属南部领地"],
79
+ "GA": ["GA", "Gabon", "Gabonese Republic", "GAB", "加蓬"],
80
+ "GM": ["GM", "Gambia", "The Gambia", "GMB", "冈比亚"],
81
+ "GE": ["GE", "Georgia", "GE", "格鲁吉亚"],
82
+ "DE": ["DE", "Germany", "Federal Republic of Germany", "DEU", "德国", "Deutschland"],
83
+ "GH": ["GH", "Ghana", "GHA", "加纳"],
84
+ "GI": ["GI", "Gibraltar", "GI", "直布罗陀"],
85
+ "GR": ["GR", "Greece", "Hellenic Republic", "GRC", "希腊"],
86
+ "GL": ["GL", "Greenland", "GL", "格陵兰"],
87
+ "GD": ["GD", "Grenada", "GRD", "格林纳达"],
88
+ "GP": ["GP", "Guadeloupe", "GP", "瓜德罗普"],
89
+ "GU": ["GU", "Guam", "GU", "关岛"],
90
+ "GT": ["GT", "Guatemala", "GTM", "危地马拉"],
91
+ "GG": ["GG", "Guernsey", "GG", "根西岛"],
92
+ "GN": ["GN", "Guinea", "GIN", "几内亚"],
93
+ "GW": ["GW", "Guinea-Bissau", "GNB", "几内亚比绍"],
94
+ "GY": ["GY", "Guyana", "GUY", "圭亚那"],
95
+ "HT": ["HT", "Haiti", "HTI", "海地"],
96
+ "HM": ["HM", "Heard Island and McDonald Islands", "HM", "赫德岛和麦克唐纳群岛"],
97
+ "VA": ["VA", "Holy See", "Vatican City", "VAT", "梵蒂冈", "教廷"],
98
+ "HN": ["HN", "Honduras", "HND", "洪都拉斯"],
99
+ "HK": ["HK", "Hong Kong", "Hong Kong SAR", "HK", "香港", "香港特别行政区"],
100
+ "HU": ["HU", "Hungary", "HUN", "匈牙利"],
101
+ "IS": ["IS", "Iceland", "ISL", "冰岛"],
102
+ "IN": ["IN", "India", "Republic of India", "IND", "印度"],
103
+ "ID": ["ID", "Indonesia", "Republic of Indonesia", "IDN", "印尼", "印度尼西亚"],
104
+ "IR": ["IR", "Iran", "Islamic Republic of Iran", "IRN", "伊朗"],
105
+ "IQ": ["IQ", "Iraq", "IRQ", "伊拉克"],
106
+ "IE": ["IE", "Ireland", "Republic of Ireland", "IRL", "爱尔兰"],
107
+ "IM": ["IM", "Isle of Man", "IM", "马恩岛"],
108
+ "IL": ["IL", "Israel", "State of Israel", "ISR", "以色列"],
109
+ "IT": ["IT", "Italy", "Italian Republic", "ITA", "意大利"],
110
+ "JM": ["JM", "Jamaica", "JAM", "牙买加"],
111
+ "JP": ["JP", "Japan", "JPN", "日本", "Nippon"],
112
+ "JE": ["JE", "Jersey", "JE", "泽西岛"],
113
+ "JO": ["JO", "Jordan", "JOR", "约旦"],
114
+ "KZ": ["KZ", "Kazakhstan", "Republic of Kazakhstan", "KAZ", "哈萨克斯坦"],
115
+ "KE": ["KE", "Kenya", "KEN", "肯尼亚"],
116
+ "KI": ["KI", "Kiribati", "KIR", "基里巴斯"],
117
+ "KP": ["KP", "North Korea", "Democratic People's Republic of Korea", "PRK", "朝鲜", "朝鲜民主主义人民共和国"],
118
+ "KR": ["KR", "South Korea", "Republic of Korea", "KOR", "韩国", "大韩民国", "Republic of Korea", "Korea, Republic of"],
119
+ "KW": ["KW", "Kuwait", "KWT", "科威特"],
120
+ "KG": ["KG", "Kyrgyzstan", "Kyrgyz Republic", "KGZ", "吉尔吉斯斯坦", "吉尔吉斯"],
121
+ "LA": ["LA", "Laos", "Lao People's Democratic Republic", "LAO", "老挝", "老挝人民民主共和国"],
122
+ "LV": ["LV", "Latvia", "LVA", "拉脱维亚"],
123
+ "LB": ["LB", "Lebanon", "LBN", "黎巴嫩"],
124
+ "LS": ["LS", "Lesotho", "LSO", "莱索托"],
125
+ "LR": ["LR", "Liberia", "LBR", "利比里亚"],
126
+ "LY": ["LY", "Libya", "Libyan Arab Jamahiriya", "LBY", "利比亚"],
127
+ "LI": ["LI", "Liechtenstein", "LIE", "列支敦士登"],
128
+ "LT": ["LT", "Lithuania", "LTU", "立陶宛"],
129
+ "LU": ["LU", "Luxembourg", "LUX", "卢森堡"],
130
+ "MO": ["MO", "Macau", "Macau SAR", "MO", "澳门", "澳门特别行政区"],
131
+ "MG": ["MG", "Madagascar", "MDG", "马达加斯加"],
132
+ "MW": ["MW", "Malawi", "MWI", "马拉维"],
133
+ "MY": ["MY", "Malaysia", "MYS", "马来西亚"],
134
+ "MV": ["MV", "Maldives", "MDV", "马尔代夫"],
135
+ "ML": ["ML", "Mali", "MLI", "马里"],
136
+ "MT": ["MT", "Malta", "MLT", "马耳他"],
137
+ "MH": ["MH", "Marshall Islands", "MHL", "马绍尔群岛"],
138
+ "MQ": ["MQ", "Martinique", "MQ", "马提尼克"],
139
+ "MR": ["MR", "Mauritania", "MRT", "毛里塔尼亚"],
140
+ "MU": ["MU", "Mauritius", "MUS", "毛里求斯"],
141
+ "YT": ["YT", "Mayotte", "YT", "马约特"],
142
+ "MX": ["MX", "Mexico", "United Mexican States", "MEX", "墨西哥"],
143
+ "FM": ["FM", "Micronesia", "Federated States of Micronesia", "FSM", "密克罗尼西亚"],
144
+ "MD": ["MD", "Moldova", "Republic of Moldova", "MDA", "摩尔多瓦"],
145
+ "MC": ["MC", "Monaco", "Principality of Monaco", "MCO", "摩纳哥"],
146
+ "MN": ["MN", "Mongolia", "MNG", "蒙古", "蒙古国"],
147
+ "ME": ["ME", "Montenegro", "MNE", "黑山"],
148
+ "MS": ["MS", "Montserrat", "MSR", "蒙特塞拉特"],
149
+ "MA": ["MA", "Morocco", "Kingdom of Morocco", "MAR", "摩洛哥"],
150
+ "MZ": ["MZ", "Mozambique", "MOZ", "莫桑比克"],
151
+ "MM": ["MM", "Myanmar", "Burma", "MMR", "缅甸", "缅甸联邦"],
152
+ "NA": ["NA", "Namibia", "NAM", "纳米比亚"],
153
+ "NR": ["NR", "Nauru", "NRU", "瑙鲁"],
154
+ "NP": ["NP", "Nepal", "NPL", "尼泊尔"],
155
+ "NL": ["NL", "Netherlands", "Holland", "NLD", "荷兰", "尼德兰"],
156
+ "NC": ["NC", "New Caledonia", "NC", "新喀里多尼亚"],
157
+ "NZ": ["NZ", "New Zealand", "NZ", "新西兰"],
158
+ "NI": ["NI", "Nicaragua", "NIC", "尼加拉瓜"],
159
+ "NE": ["NE", "Niger", "Niger Republic", "NER", "尼日尔"],
160
+ "NG": ["NG", "Nigeria", "NGA", "尼日利亚"],
161
+ "NU": ["NU", "Niue", "NIU", "纽埃"],
162
+ "NF": ["NF", "Norfolk Island", "NF", "诺福克岛"],
163
+ "MK": ["MK", "North Macedonia", "Republic of North Macedonia", "MKD", "北马其顿", "马其顿"],
164
+ "MP": ["MP", "Northern Mariana Islands", "MP", "北马里亚纳群岛"],
165
+ "NO": ["NO", "Norway", "NOR", "挪威"],
166
+ "OM": ["OM", "Oman", "OMN", "阿曼"],
167
+ "PK": ["PK", "Pakistan", "Islamic Republic of Pakistan", "PAK", "巴基斯坦"],
168
+ "PW": ["PW", "Palau", "PLW", "帕劳"],
169
+ "PS": ["PS", "Palestine", "State of Palestine", "PSE", "巴勒斯坦"],
170
+ "PA": ["PA", "Panama", "PAN", "巴拿马"],
171
+ "PG": ["PG", "Papua New Guinea", "PNG", "巴布亚新几内亚"],
172
+ "PY": ["PY", "Paraguay", "PRY", "巴拉圭"],
173
+ "PE": ["PE", "Peru", "PER", "秘鲁"],
174
+ "PH": ["PH", "Philippines", "PHL", "菲律宾"],
175
+ "PN": ["PN", "Pitcairn", "PN", "皮特凯恩群岛"],
176
+ "PL": ["PL", "Poland", "POL", "波兰"],
177
+ "PT": ["PT", "Portugal", "PRT", "葡萄牙"],
178
+ "PR": ["PR", "Puerto Rico", "PR", "波多黎各"],
179
+ "QA": ["QA", "Qatar", "QAT", "卡塔尔"],
180
+ "RE": ["RE", "Réunion", "REU", "留尼汪"],
181
+ "RO": ["RO", "Romania", "ROU", "罗马尼亚"],
182
+ "RU": ["RU", "Russia", "Russian Federation", "RUS", "俄罗斯", "俄国"],
183
+ "RW": ["RW", "Rwanda", "RWA", "卢旺达"],
184
+ "BL": ["BL", "Saint Barthélemy", "BL", "圣巴泰勒米"],
185
+ "SH": ["SH", "Saint Helena", "Saint Helena, Ascension and Tristan da Cunha", "SH", "圣赫勒拿"],
186
+ "KN": ["KN", "Saint Kitts and Nevis", "KNA", "圣基茨和尼维斯"],
187
+ "LC": ["LC", "Saint Lucia", "LCA", "圣卢西亚"],
188
+ "MF": ["MF", "Saint Martin", "MF", "圣马丁"],
189
+ "PM": ["PM", "Saint Pierre and Miquelon", "PM", "圣皮埃尔和密克隆"],
190
+ "VC": ["VC", "Saint Vincent and the Grenadines", "VCT", "圣文森特和格林纳丁斯"],
191
+ "WS": ["WS", "Samoa", "WSM", "萨摩亚"],
192
+ "SM": ["SM", "San Marino", "SMR", "圣马力诺"],
193
+ "ST": ["ST", "Sao Tome and Principe", "STP", "圣多美和普林西比"],
194
+ "SA": ["SA", "Saudi Arabia", "Kingdom of Saudi Arabia", "SAU", "沙特阿拉伯"],
195
+ "SN": ["SN", "Senegal", "SEN", "塞内加尔"],
196
+ "RS": ["RS", "Serbia", "SRB", "塞尔维亚"],
197
+ "SC": ["SC", "Seychelles", "SYC", "塞舌尔"],
198
+ "SL": ["SL", "Sierra Leone", "SLE", "塞拉利昂"],
199
+ "SG": ["SG", "Singapore", "SGP", "新加坡"],
200
+ "SX": ["SX", "Sint Maarten", "SX", "荷属圣马丁"],
201
+ "SK": ["SK", "Slovakia", "Slovak Republic", "SVK", "斯洛伐克"],
202
+ "SI": ["SI", "Slovenia", "SVN", "斯洛文尼亚"],
203
+ "SB": ["SB", "Solomon Islands", "SLB", "所罗门群岛"],
204
+ "SO": ["SO", "Somalia", "SOM", "索马里"],
205
+ "ZA": ["ZA", "South Africa", "Republic of South Africa", "ZAF", "南非"],
206
+ "GS": ["GS", "South Georgia and the South Sandwich Islands", "GS", "南乔治亚和南桑威奇群岛"],
207
+ "SS": ["SS", "South Sudan", "SSD", "南苏丹"],
208
+ "ES": ["ES", "Spain", "Kingdom of Spain", "ESP", "西班牙"],
209
+ "LK": ["LK", "Sri Lanka", "LKA", "斯里兰卡"],
210
+ "SD": ["SD", "Sudan", "SDN", "苏丹"],
211
+ "SR": ["SR", "Suriname", "SUR", "苏里南"],
212
+ "SJ": ["SJ", "Svalbard and Jan Mayen", "SJ", "斯瓦尔巴和扬马延"],
213
+ "SE": ["SE", "Sweden", "SWE", "瑞典"],
214
+ "CH": ["CH", "Switzerland", "Swiss Confederation", "CHE", "瑞士", "Swiss"],
215
+ "SY": ["SY", "Syria", "Syrian Arab Republic", "SYR", "叙利亚"],
216
+ "TW": ["TW", "Taiwan", "Taiwan, Province of China", "TWN", "台湾", "中华民国", "ROC"],
217
+ "TJ": ["TJ", "Tajikistan", "TJK", "塔吉克斯坦"],
218
+ "TZ": ["TZ", "Tanzania", "United Republic of Tanzania", "TZA", "坦桑尼亚"],
219
+ "TH": ["TH", "Thailand", "THA", "泰国"],
220
+ "TL": ["TL", "Timor-Leste", "East Timor", "TLS", "东帝汶", "蒂莫尔"],
221
+ "TG": ["TG", "Togo", "TGO", "多哥"],
222
+ "TK": ["TK", "Tokelau", "TKL", "托克劳"],
223
+ "TO": ["TO", "Tonga", "TON", "汤加"],
224
+ "TT": ["TT", "Trinidad and Tobago", "TTO", "特立尼达和多巴哥"],
225
+ "TN": ["TN", "Tunisia", "TUN", "突尼斯"],
226
+ "TR": ["TR", "Turkey", "Republic of Turkey", "TUR", "土耳其"],
227
+ "TM": ["TM", "Turkmenistan", "TKM", "土库曼斯坦"],
228
+ "TC": ["TC", "Turks and Caicos Islands", "TCA", "特克斯和凯科斯群岛"],
229
+ "TV": ["TV", "Tuvalu", "TUV", "图瓦卢"],
230
+ "UG": ["UG", "Uganda", "UGA", "乌干达"],
231
+ "UA": ["UA", "Ukraine", "UKR", "乌克兰"],
232
+ "AE": ["AE", "United Arab Emirates", "UAE", "阿拉伯联合酋长国", "阿联酋"],
233
+ "GB": ["GB", "United Kingdom", "United Kingdom of Great Britain and Northern Ireland", "UK", "GBR", "英国", "Britain", "Great Britain", "England", "UK"],
234
+ "US": ["US", "United States", "United States of America", "USA", "US", "America", "美利坚合众国", "美国"],
235
+ "UM": ["UM", "United States Minor Outlying Islands", "UM", "美国本土外小岛屿"],
236
+ "UY": ["UY", "Uruguay", "URY", "乌拉圭"],
237
+ "UZ": ["UZ", "Uzbekistan", "UZB", "乌兹别克斯坦"],
238
+ "VU": ["VU", "Vanuatu", "VUT", "瓦努阿图"],
239
+ "VE": ["VE", "Venezuela", "Bolivarian Republic of Venezuela", "VEN", "委内瑞拉"],
240
+ "VN": ["VN", "Vietnam", "Socialist Republic of Vietnam", "VNM", "越南"],
241
+ "VG": ["VG", "British Virgin Islands", "VG", "英属维尔京群岛"],
242
+ "VI": ["VI", "United States Virgin Islands", "VI", "美属维尔京群岛"],
243
+ "WF": ["WF", "Wallis and Futuna", "WF", "瓦利斯和富图纳"],
244
+ "EH": ["EH", "Western Sahara", "ESH", "西撒哈拉"],
245
+ "YE": ["YE", "Yemen", "YEM", "也门"],
246
+ "ZM": ["ZM", "Zambia", "ZMB", "赞比亚"],
247
+ "ZW": ["ZW", "Zimbabwe", "ZWE", "津巴布韦"]
248
+ }
@@ -0,0 +1,668 @@
1
+ import os
2
+ import httpx
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ import re
7
+ import unicodedata
8
+ import random
9
+ from typing import Optional, Dict, Any, Union
10
+ from dotenv import load_dotenv
11
+ from mcp.server.fastmcp import FastMCP
12
+ from concurrent.futures import ThreadPoolExecutor
13
+
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
+
17
+ load_dotenv()
18
+
19
+ API_KEY: Optional[str] = os.getenv("SERPER_API_KEY")
20
+ mcp = FastMCP("serper-mcp")
21
+
22
+ # 加载本地国家别名字典 (data/country_aliases.json)
23
+ # 简化索引:仅使用别名字典 ALIAS_MAP,并生成按字母序排列的别名列表 ALIAS_KEYS_SORTED。
24
+ # 使用自实现的 Quick Sort 对别名键进行排序,并用二分查找(binary search)做匹配查找。
25
+ ALIAS_MAP: Dict[str, str] = {}
26
+ ALIAS_KEYS_SORTED: list = []
27
+
28
+ _aliases_path = os.path.join(os.path.dirname(__file__), "data", "country_aliases.json")
29
+
30
+ # Quick Sort 实现(用于对别名字典键排序)
31
+ def quick_sort(arr: list) -> list:
32
+ if len(arr) <= 1:
33
+ return arr
34
+ pivot = arr[len(arr) // 2]
35
+ left = [x for x in arr if x < pivot]
36
+ middle = [x for x in arr if x == pivot]
37
+ right = [x for x in arr if x > pivot]
38
+ return quick_sort(left) + middle + quick_sort(right)
39
+
40
+ # 二分查找(在已排序的列表中查找精确匹配)
41
+ def binary_search(arr: list, target: str) -> Optional[int]:
42
+ lo, hi = 0, len(arr) - 1
43
+ while lo <= hi:
44
+ mid = (lo + hi) // 2
45
+ if arr[mid] == target:
46
+ return mid
47
+ if arr[mid] < target:
48
+ lo = mid + 1
49
+ else:
50
+ hi = mid - 1
51
+ return None
52
+
53
+ def normalize(text: str) -> str:
54
+ """归一化国家/地区名称:NFKD、去重音、转小写、去标点、折叠空白"""
55
+ if not text:
56
+ return ""
57
+ # Unicode normalize
58
+ s = unicodedata.normalize("NFKD", text)
59
+ # remove diacritics
60
+ s = "".join(ch for ch in s if not unicodedata.combining(ch))
61
+ # convert full-width to half-width and normalize spaces
62
+ s = s.replace("\u3000", " ")
63
+ s = s.strip().lower()
64
+ # remove punctuation except spaces
65
+ s = re.sub(r"[^\w\s'-]", " ", s, flags=re.UNICODE)
66
+ # replace underscores and multiple spaces with single space
67
+ s = re.sub(r"[_\s]+", " ", s).strip()
68
+ return s
69
+
70
+ def _generate_variants(alias: str) -> set:
71
+ """为 alias 生成若干变体以提高命中率(去标点、逗号重排等)"""
72
+ variants = set()
73
+ base = alias.strip()
74
+ variants.add(base)
75
+ # normalized base
76
+ n = normalize(base)
77
+ variants.add(n)
78
+ # remove punctuation version
79
+ variants.add(re.sub(r"[^\w\s]", "", n))
80
+ # if contains comma, try reorder segments: "Korea, South" -> "south korea"
81
+ if "," in base:
82
+ parts = [p.strip() for p in base.split(",") if p.strip()]
83
+ if len(parts) >= 2:
84
+ reordered = " ".join(reversed(parts))
85
+ variants.add(reordered)
86
+ variants.add(normalize(reordered))
87
+ # also add word-reordered variants for simple two-word names
88
+ parts = n.split()
89
+ if len(parts) == 2:
90
+ variants.add(" ".join(reversed(parts)))
91
+ return {v for v in variants if v}
92
+
93
+ try:
94
+ with open(_aliases_path, "r", encoding="utf-8") as f:
95
+ _forward = json.load(f)
96
+
97
+ # 仅基于别名字典构建 ALIAS_MAP(normalized alias -> alpha2)
98
+ for code, names in _forward.items():
99
+ code_up = code.upper()
100
+ if isinstance(names, list):
101
+ iter_names = names
102
+ else:
103
+ iter_names = [names]
104
+ for name in iter_names:
105
+ if not isinstance(name, str):
106
+ continue
107
+ for variant in _generate_variants(name):
108
+ key = normalize(variant)
109
+ if not key:
110
+ continue
111
+ # 后来的同名别名以最后一个为准(覆盖),保持简单明了
112
+ ALIAS_MAP[key] = code_up
113
+
114
+ # 使用 Quick Sort 对别名字典的键进行排序,供二分查找使用
115
+ ALIAS_KEYS_SORTED = quick_sort(list(ALIAS_MAP.keys()))
116
+
117
+ except FileNotFoundError:
118
+ logger.warning("国家别名字典未找到: %s", _aliases_path)
119
+ except Exception as e:
120
+ logger.warning("加载国家别名字典失败: %s", e)
121
+
122
+ USER_AGENT = "serper_client/1.0"
123
+ API_ENDPOINTS = {
124
+ "search": "https://google.serper.dev/search",
125
+ "image_search": "https://google.serper.dev/images",
126
+ "video_search": "https://google.serper.dev/videos",
127
+ "place_search": "https://google.serper.dev/places",
128
+ "news_search": "https://google.serper.dev/news",
129
+ "lens_search": "https://google.serper.dev/lens",
130
+ "scholar_search": "https://google.serper.dev/scholar",
131
+ "scrape": "https://scrape.serper.dev",
132
+ }
133
+ HTTP_TIMEOUT = 30.0
134
+
135
+ # 并发与重试相关配置(可通过环境变量调整)
136
+ SERPER_MAX_CONNECTIONS = int(os.getenv("SERPER_MAX_CONNECTIONS", "200"))
137
+ SERPER_KEEPALIVE = int(os.getenv("SERPER_KEEPALIVE", "20"))
138
+ SERPER_HTTP2 = os.getenv("SERPER_HTTP2", "0") == "1"
139
+ # 如果启用了 HTTP/2,确保环境中安装了 h2;否则回退为 False,避免 httpx 抛出 ImportError
140
+ if SERPER_HTTP2:
141
+ try:
142
+ import h2 # noqa: F401
143
+ except Exception:
144
+ logger.warning("SERPER_HTTP2 设置为启用,但未检测到 'h2' 包。将自动禁用 HTTP/2(请安装 httpx[http2] 以启用)。")
145
+ SERPER_HTTP2 = False
146
+
147
+ # Serper 后端总体并发上限为每秒 300 请求;将默认全局并发上限适当调高为 200(可通过环境变量调整)
148
+ SERPER_MAX_CONCURRENT_REQUESTS = int(os.getenv("SERPER_MAX_CONCURRENT_REQUESTS", "200"))
149
+ SERPER_MAX_WORKERS = int(os.getenv("SERPER_MAX_WORKERS", "10"))
150
+ SERPER_RETRY_COUNT = int(os.getenv("SERPER_RETRY_COUNT", "3"))
151
+ SERPER_RETRY_BASE_DELAY = float(os.getenv("SERPER_RETRY_BASE_DELAY", "0.5"))
152
+
153
+ # per-endpoint 配置(通过环境变量传入 JSON 字符串,示例: '{"search":10,"scrape":2}')
154
+ try:
155
+ PER_ENDPOINT_MAX_CONCURRENT = json.loads(os.getenv("SERPER_ENDPOINT_CONCURRENCY", "{}"))
156
+ except Exception:
157
+ PER_ENDPOINT_MAX_CONCURRENT = {}
158
+
159
+ # per-endpoint 是否允许重试,默认允许(用于避免对非幂等接口重试)
160
+ try:
161
+ PER_ENDPOINT_ALLOW_RETRY = json.loads(os.getenv("SERPER_ENDPOINT_RETRYABLE", '{"search": true, "scrape": false}'))
162
+ except Exception:
163
+ PER_ENDPOINT_ALLOW_RETRY = {}
164
+
165
+ # 请求并发信号量(在 startup_all 时初始化)
166
+ REQUEST_SEMAPHORE = None
167
+
168
+ # endpoint -> asyncio.Semaphore 映射(在 startup_all 中根据 PER_ENDPOINT_MAX_CONCURRENT 初始化)
169
+ ENDPOINT_SEMAPHORES: Dict[str, asyncio.Semaphore] = {}
170
+
171
+ # 全局 httpx AsyncClient 管理类
172
+ class AsyncHttpClientManager:
173
+ _client: Optional[httpx.AsyncClient] = None
174
+ _lock = asyncio.Lock()
175
+
176
+ @classmethod
177
+ async def startup(cls):
178
+ async with cls._lock:
179
+ if cls._client is None:
180
+ limits = httpx.Limits(
181
+ max_connections=SERPER_MAX_CONNECTIONS,
182
+ max_keepalive_connections=SERPER_KEEPALIVE
183
+ )
184
+ timeout_obj = httpx.Timeout(
185
+ connect=5.0,
186
+ read=20.0,
187
+ write=10.0,
188
+ pool=30.0,
189
+ timeout=HTTP_TIMEOUT
190
+ )
191
+ cls._client = httpx.AsyncClient(
192
+ timeout=timeout_obj,
193
+ headers={"User-Agent": USER_AGENT},
194
+ limits=limits,
195
+ http2=SERPER_HTTP2
196
+ )
197
+ logger.info("httpx AsyncClient 已启动 (max_connections=%d, keepalive=%d, http2=%s, timeout=%s)",
198
+ SERPER_MAX_CONNECTIONS, SERPER_KEEPALIVE, SERPER_HTTP2, HTTP_TIMEOUT)
199
+
200
+ @classmethod
201
+ def get_client(cls) -> httpx.AsyncClient:
202
+ if cls._client is None:
203
+ raise RuntimeError("AsyncHttpClientManager 未启动,请先调用startup()")
204
+ return cls._client
205
+
206
+ @classmethod
207
+ async def shutdown(cls):
208
+ async with cls._lock:
209
+ if cls._client:
210
+ await cls._client.aclose()
211
+ logger.info("httpx AsyncClient 已关闭")
212
+ cls._client = None
213
+
214
+
215
+ # 全局线程池执行器管理
216
+ class ThreadPoolManager:
217
+ _executor: Optional[ThreadPoolExecutor] = None
218
+ _max_workers = SERPER_MAX_WORKERS
219
+
220
+ @classmethod
221
+ def startup(cls, max_workers: int = 10):
222
+ if cls._executor is None:
223
+ cls._executor = ThreadPoolExecutor(max_workers=cls._max_workers)
224
+ logger.info(f"线程池启动,最大工作线程数: {cls._max_workers}")
225
+
226
+ @classmethod
227
+ def get_executor(cls) -> ThreadPoolExecutor:
228
+ if cls._executor is None:
229
+ raise RuntimeError("ThreadPoolManager 未启动,请先调用startup()")
230
+ return cls._executor
231
+
232
+ @classmethod
233
+ def shutdown(cls):
234
+ if cls._executor:
235
+ cls._executor.shutdown(wait=True)
236
+ logger.info("线程池已关闭")
237
+ cls._executor = None
238
+
239
+
240
+ def error_response(message: str, status_code: Optional[int] = None, extra: Optional[Dict[str, Any]] = None) -> str:
241
+ result: Dict[str, Any] = {
242
+ "success": False,
243
+ "error": True,
244
+ "message": message,
245
+ }
246
+ if status_code is not None:
247
+ result["status_code"] = status_code
248
+ if extra:
249
+ result.update(extra)
250
+ return json.dumps(result, ensure_ascii=False, indent=4)
251
+
252
+
253
+ def success_response(query_details: Dict[str, Any], results: Dict[str, Any]) -> str:
254
+ return json.dumps({
255
+ "success": True,
256
+ "query_details": query_details,
257
+ "results": results,
258
+ }, ensure_ascii=False, indent=4)
259
+
260
+
261
+ def get_country_code_alpha2(country_name: Optional[str]) -> str:
262
+ """
263
+ 国家代码解析(已简化):
264
+ - 仅基于别名字典 ALIAS_MAP 进行查找,使用 ALIAS_KEYS_SORTED + 二分查找匹配别名键。
265
+ - 优先在别名字典中查找传入参数(归一化后);若命中则返回对应 alpha2。
266
+ - 如果传入为两字母 ISO2 则作为后备直接返回大写。
267
+ - 未找到则默认返回 'US'。
268
+ """
269
+ # 处理空输入:直接使用默认 US
270
+ if not country_name:
271
+ return "US"
272
+
273
+ name = country_name.strip()
274
+ if not name:
275
+ return "US"
276
+
277
+ # 归一化并首先在 ALIAS_MAP 中查找(O(1))
278
+ norm = normalize(name)
279
+ if norm in ALIAS_MAP:
280
+ return ALIAS_MAP[norm]
281
+
282
+ # 对大规模别名列表,使用已排序键列表 + 二分查找
283
+ if ALIAS_KEYS_SORTED:
284
+ idx = binary_search(ALIAS_KEYS_SORTED, norm)
285
+ if idx is not None:
286
+ return ALIAS_MAP.get(ALIAS_KEYS_SORTED[idx], "US")
287
+
288
+ # 如果已经是两字母 ISO2,作为后备直接返回大写
289
+ if len(name) == 2 and name.isalpha():
290
+ return name.upper()
291
+
292
+ # 再尝试归一化的大写形式(例如传入 'USA')
293
+ u_norm = normalize(name.upper())
294
+ if u_norm in ALIAS_MAP:
295
+ return ALIAS_MAP[u_norm]
296
+ if ALIAS_KEYS_SORTED:
297
+ idx = binary_search(ALIAS_KEYS_SORTED, u_norm)
298
+ if idx is not None:
299
+ return ALIAS_MAP.get(ALIAS_KEYS_SORTED[idx], "US")
300
+
301
+ logger.info("未找到国家名称 '%s',使用默认国家码 US。", country_name)
302
+ return "US"
303
+
304
+
305
+ def validate_search_num(num_val: int) -> int:
306
+ if 1 <= num_val <= 100:
307
+ return num_val
308
+ logger.warning("搜索数量 %d 超出范围(1-100),使用默认值10。", num_val)
309
+ return 10
310
+
311
+
312
+ def map_search_time_to_tbs_param(time_period_str: Optional[str]) -> Optional[str]:
313
+ if not time_period_str:
314
+ return None
315
+ s = time_period_str.strip().lower()
316
+ mapping = {
317
+ "小时": "qdr:h", "hour": "qdr:h",
318
+ "天": "qdr:d", "day": "qdr:d",
319
+ "周": "qdr:w", "week": "qdr:w",
320
+ "月": "qdr:m", "month": "qdr:m",
321
+ "年": "qdr:y", "year": "qdr:y",
322
+ }
323
+ for k, v in mapping.items():
324
+ if k in s:
325
+ return v
326
+ logger.info("未识别的时间偏好 '%s',忽略时间过滤。", time_period_str)
327
+ return None
328
+
329
+
330
+ async def execute_serper_request(
331
+ api_url: str,
332
+ payload: Dict[str, Any],
333
+ api_name: str
334
+ ) -> Union[Dict[str, Any], None]:
335
+ """
336
+ 执行对 Serper API 的异步请求,包含并发控制与重试策略。
337
+ 返回解析后的 JSON 或错误描述字典,或 None(当缺少 API_KEY 时)。
338
+ """
339
+ if not API_KEY:
340
+ logger.error("未配置SERPER_API_KEY,无法调用 %s 接口。", api_name)
341
+ return None
342
+
343
+ headers = {
344
+ "X-API-KEY": API_KEY,
345
+ "Content-Type": "application/json",
346
+ }
347
+
348
+ try:
349
+ client = AsyncHttpClientManager.get_client()
350
+ except RuntimeError as e:
351
+ logger.error("HTTP客户端未启动: %s", e)
352
+ return {"error": True, "message": f"{api_name}接口请求失败,HTTP客户端未启动。"}
353
+
354
+ logger.info("准备调用 %s 接口,payload: %s", api_name, payload)
355
+
356
+ # 选择用于该端点的 semaphore(优先 per-endpoint,其次全局 REQUEST_SEMAPHORE)
357
+ sem = ENDPOINT_SEMAPHORES.get(api_name, REQUEST_SEMAPHORE)
358
+ # 该端点是否允许重试(默认 True)
359
+ retry_allowed = PER_ENDPOINT_ALLOW_RETRY.get(api_name, True)
360
+
361
+ attempt = 0
362
+ while True:
363
+ try:
364
+ # 并发控制(如果已在 startup_all 中初始化)
365
+ if REQUEST_SEMAPHORE:
366
+ async with REQUEST_SEMAPHORE:
367
+ response = await client.post(api_url, json=payload, headers=headers)
368
+ else:
369
+ response = await client.post(api_url, json=payload, headers=headers)
370
+
371
+ # 检查 HTTP 状态
372
+ response.raise_for_status()
373
+
374
+ # 解析返回 JSON
375
+ try:
376
+ result = response.json()
377
+ except Exception as e:
378
+ logger.error("%s接口JSON解析错误: %s", api_name, e)
379
+ return {"error": True, "message": f"{api_name}接口响应解析失败: {e}"}
380
+
381
+ return result
382
+
383
+ except httpx.HTTPStatusError as e:
384
+ status = e.response.status_code if e.response is not None else None
385
+ logger.warning("%s 接口返回 HTTP 错误 %s (尝试 %d/%d)", api_name, status, attempt + 1, SERPER_RETRY_COUNT)
386
+ # 对 5xx 做重试(仅在端点允许重试时)
387
+ if retry_allowed and status and 500 <= status < 600 and attempt < SERPER_RETRY_COUNT:
388
+ attempt += 1
389
+ delay = SERPER_RETRY_BASE_DELAY * (2 ** (attempt - 1)) + random.uniform(0, 0.1)
390
+ logger.info("对 %s 接口在 %s 秒后重试 (HTTP %s)", api_name, round(delay, 2), status)
391
+ await asyncio.sleep(delay)
392
+ continue
393
+ logger.error("%s接口HTTP错误 %s %s: %s", api_name, status, e.request.url if e.request else "", e)
394
+ return {
395
+ "error": True,
396
+ "message": f"{api_name}接口HTTP状态错误: {status}",
397
+ "status_code": status
398
+ }
399
+
400
+ except httpx.RequestError as e:
401
+ logger.warning("%s 接口请求错误 (尝试 %d/%d): %s", api_name, attempt + 1, SERPER_RETRY_COUNT, e)
402
+ if retry_allowed and attempt < SERPER_RETRY_COUNT:
403
+ attempt += 1
404
+ delay = SERPER_RETRY_BASE_DELAY * (2 ** (attempt - 1)) + random.uniform(0, 0.1)
405
+ logger.info("对 %s 接口在 %s 秒后重试 (请求错误)", api_name, round(delay, 2))
406
+ await asyncio.sleep(delay)
407
+ continue
408
+ logger.error("%s接口请求错误: %s", api_name, e)
409
+ return {"error": True, "message": f"{api_name}接口请求错误: {e}"}
410
+
411
+ except Exception as e:
412
+ logger.exception("%s接口未知错误: %s", api_name, e)
413
+ return {"error": True, "message": f"{api_name}接口未知错误: {e}"}
414
+
415
+
416
+ async def generic_serper_search(
417
+ api_name_key: str,
418
+ q: Optional[str] = None,
419
+ url: Optional[str] = None,
420
+ num: Optional[int] = None,
421
+ country: Optional[str] = None,
422
+ time: Optional[str] = None,
423
+ include_markdown: Optional[bool] = None,
424
+ extra_params: Optional[Dict[str, Any]] = None
425
+ ) -> str:
426
+ if not API_KEY:
427
+ return error_response("环境变量SERPER_API_KEY未设置,接口无法调用。")
428
+ api_url = API_ENDPOINTS.get(api_name_key)
429
+ if not api_url:
430
+ return error_response(f"未知API_KEY '{api_name_key}',无法处理请求。")
431
+ payload: Dict[str, Any] = {}
432
+ if q is not None:
433
+ payload["q"] = q
434
+ if url is not None:
435
+ if api_name_key in {"lens_search", "scrape"}:
436
+ payload["url"] = url
437
+ else:
438
+ logger.warning("url参数在接口%s中未使用", api_name_key)
439
+ if num is not None:
440
+ payload["num"] = validate_search_num(num)
441
+ country_code = get_country_code_alpha2(country)
442
+ if country_code:
443
+ payload["gl"] = country_code
444
+ logger.info("country param '%s' -> resolved country_code: %s", country, country_code)
445
+
446
+ tbs_val = map_search_time_to_tbs_param(time)
447
+ if tbs_val:
448
+ payload["tbs"] = tbs_val
449
+ if include_markdown is not None and api_name_key == "scrape":
450
+ if include_markdown:
451
+ payload["includeMarkdown"] = True
452
+ if extra_params:
453
+ # 不允许 extra_params 覆盖或设置 gl 参数(防止外部传入非 ISO2 值覆盖解析结果)
454
+ sanitized = {k: v for k, v in extra_params.items() if k.lower() != "gl"}
455
+ payload.update(sanitized)
456
+
457
+ # 最终验证 gl,确保为合法的 ISO2 大写字符串;使用 get_country_code_alpha2 进行规范化或移除
458
+ gl_val = payload.get("gl")
459
+ if gl_val is not None:
460
+ resolved = None
461
+ # 如果已经是 ASCII 两字母代码,直接规范为大写
462
+ if isinstance(gl_val, str) and re.fullmatch(r"[A-Za-z]{2}", gl_val):
463
+ resolved = gl_val.upper()
464
+ else:
465
+ # 否则尝试用别名字典解析(支持中文/英文/ISO3等)
466
+ resolved = get_country_code_alpha2(str(gl_val))
467
+ if resolved:
468
+ payload["gl"] = resolved
469
+ else:
470
+ logger.warning("无效的 gl 值 '%s',已移除(无法解析为 ISO2)", gl_val)
471
+ payload.pop("gl", None)
472
+
473
+ if api_name_key == "scrape" and ("url" not in payload or not payload["url"].strip()):
474
+ return error_response("参数 url 必填且不能为空字符串。")
475
+ result = await execute_serper_request(api_url, payload, api_name_key)
476
+ if result is None:
477
+ return error_response(f"{api_name_key}请求失败,接口响应为空。")
478
+ if isinstance(result, dict) and result.get("error"):
479
+ return json.dumps({
480
+ "success": False,
481
+ "query_details": payload,
482
+ "error": result.get("error"),
483
+ "message": result.get("message", "未知错误"),
484
+ "status_code": result.get("status_code", None),
485
+ }, ensure_ascii=False, indent=4)
486
+ return success_response(payload, result)
487
+
488
+
489
+ @mcp.tool(name="serper-general-search")
490
+ async def serper_general_search(
491
+ search_key_words: str,
492
+ search_country: Optional[str] = None,
493
+ search_num: int = 10,
494
+ search_time: Optional[str] = None,
495
+ ) -> str:
496
+ """
497
+ 通用搜索接口。
498
+ 参数:
499
+ search_key_words: 搜索关键词(必填)
500
+ search_country: 国家名称(可选),支持中文或英文国家名
501
+ search_num: 返回结果数量,1~100,默认10
502
+ search_time: 时间过滤,如“小时”,“天”,“周”,“月”,“年”,可选
503
+ 返回:
504
+ JSON格式字符串,包含查询参数和搜索结果。
505
+ """
506
+ return await generic_serper_search("search", q=search_key_words, country=search_country, num=search_num, time=search_time)
507
+
508
+
509
+ @mcp.tool(name="serper-image-search")
510
+ async def serper_image_search(
511
+ search_key_words: str,
512
+ search_country: Optional[str] = None,
513
+ search_num: int = 10,
514
+ search_time: Optional[str] = None,
515
+ ) -> str:
516
+ """
517
+ 图片搜索接口。
518
+ 参数含义与通用搜索接口相同。
519
+ """
520
+ return await generic_serper_search("image_search", q=search_key_words, country=search_country, num=search_num, time=search_time)
521
+
522
+
523
+ @mcp.tool(name="serper-video-search")
524
+ async def serper_video_search(
525
+ search_key_words: str,
526
+ search_country: Optional[str] = None,
527
+ search_num: int = 10,
528
+ search_time: Optional[str] = None,
529
+ ) -> str:
530
+ """
531
+ 视频搜索接口。
532
+ 参数含义与通用搜索接口相同。
533
+ """
534
+ return await generic_serper_search("video_search", q=search_key_words, country=search_country, num=search_num, time=search_time)
535
+
536
+
537
+ @mcp.tool(name="serper-place-search")
538
+ async def serper_place_search(
539
+ search_key_words: str,
540
+ search_country: Optional[str] = None,
541
+ ) -> str:
542
+ """
543
+ 地点搜索接口。
544
+ 参数:
545
+ search_key_words: 搜索关键词
546
+ search_country: 国家名称(可选)
547
+ """
548
+ return await generic_serper_search("place_search", q=search_key_words, country=search_country)
549
+
550
+
551
+ @mcp.tool(name="serper-news-search")
552
+ async def serper_news_search(
553
+ search_key_words: str,
554
+ search_country: Optional[str] = None,
555
+ search_num: int = 10,
556
+ search_time: Optional[str] = None,
557
+ ) -> str:
558
+ """
559
+ 新闻搜索接口。
560
+ 参数同通用搜索接口。
561
+ """
562
+ return await generic_serper_search("news_search", q=search_key_words, country=search_country, num=search_num, time=search_time)
563
+
564
+
565
+ @mcp.tool(name="serper-lens-search")
566
+ async def serper_lens_search(
567
+ image_url: str,
568
+ search_country: Optional[str] = None,
569
+ ) -> str:
570
+ """
571
+ Lens图片搜索接口。
572
+ 参数:
573
+ image_url: 图片URL(必填)
574
+ search_country: 国家名称(可选)
575
+ """
576
+ return await generic_serper_search("lens_search", url=image_url, country=search_country)
577
+
578
+
579
+ @mcp.tool(name="serper-scholar-search")
580
+ async def serper_scholar_search(
581
+ search_key_words: str,
582
+ search_country: Optional[str] = None,
583
+ ) -> str:
584
+ """
585
+ 学术搜索接口。
586
+ 参数:
587
+ search_key_words: 查询关键词
588
+ search_country: 国家名称(可选)
589
+ """
590
+ return await generic_serper_search("scholar_search", q=search_key_words, country=search_country)
591
+
592
+
593
+ @mcp.tool(name="serper-scrape")
594
+ async def serper_scrape(
595
+ url: str,
596
+ include_markdown: bool = False,
597
+ ) -> str:
598
+ """
599
+ 网页内容抓取接口。
600
+ 参数:
601
+ url: 目标网页URL(必填)
602
+ include_markdown: 是否返回Markdown格式内容(默认为False)
603
+ 返回:
604
+ JSON字符串,包含网页内容和(可选)Markdown文本。
605
+ """
606
+ return await generic_serper_search("scrape", url=url, include_markdown=include_markdown)
607
+
608
+
609
+ # 示例:同步阻塞函数,通过线程池异步调用
610
+ async def run_blocking_task_in_threadpool(blocking_func, *args, **kwargs):
611
+ loop = asyncio.get_running_loop()
612
+ executor = ThreadPoolManager.get_executor()
613
+ return await loop.run_in_executor(executor, lambda: blocking_func(*args, **kwargs))
614
+
615
+
616
+ async def startup_all():
617
+ global REQUEST_SEMAPHORE
618
+ await AsyncHttpClientManager.startup()
619
+ ThreadPoolManager.startup(max_workers=SERPER_MAX_WORKERS)
620
+ # 初始化请求并发信号量
621
+ REQUEST_SEMAPHORE = asyncio.Semaphore(SERPER_MAX_CONCURRENT_REQUESTS)
622
+ logger.info("已初始化请求并发控制,最大并发请求数: %d,线程池最大工作线程: %d", SERPER_MAX_CONCURRENT_REQUESTS, SERPER_MAX_WORKERS)
623
+ # 可以扩展这里做更多初始化
624
+
625
+
626
+ async def shutdown_all():
627
+ await AsyncHttpClientManager.shutdown()
628
+ ThreadPoolManager.shutdown()
629
+ # 可以扩展这里做更多清理
630
+
631
+
632
+ def main():
633
+ """
634
+ The main synchronous entry point for the MCP server.
635
+ Manages the asyncio event loop to run async setup/teardown around the blocking mcp.run() call.
636
+ """
637
+ if not API_KEY:
638
+ logger.error(
639
+ "警告:环境变量SERPER_API_KEY未设置,启动后所有接口调用均不可用。"
640
+ "请在.env文件或环境变量中配置。"
641
+ )
642
+ else:
643
+ logger.info("加载到SERPER_API_KEY,准备启动Serper MCP工具接口服务。")
644
+
645
+ # Manually manage the event loop
646
+ loop = asyncio.new_event_loop()
647
+ asyncio.set_event_loop(loop)
648
+
649
+ try:
650
+ # Run the async startup tasks
651
+ loop.run_until_complete(startup_all())
652
+ logger.info("Serper MCP 工具服务已启动。")
653
+
654
+ # Now, run the blocking MCP server loop
655
+ mcp.run(transport="stdio")
656
+
657
+ except KeyboardInterrupt:
658
+ logger.info("接收到中断信号,正在关闭服务...")
659
+ finally:
660
+ logger.info("开始关闭异步资源...")
661
+ # Run the async shutdown tasks
662
+ loop.run_until_complete(shutdown_all())
663
+ loop.close()
664
+ logger.info("Serper MCP工具接口服务已安全关闭。")
665
+
666
+ # This block is for direct execution via 'python -m serper_toolkit.server'
667
+ if __name__ == "__main__":
668
+ main()
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.4
2
+ Name: serper-toolkit
3
+ Version: 0.0.4
4
+ Summary: A high-performance, asynchronous MCP server for Serper Google Search, featuring connection pooling, request retries, and intelligent input parsing.
5
+ Author-email: "Joey.Kot" <joey.kot.x@gmail.com>
6
+ Keywords: serper,mcp,server,google,search
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: httpx>=0.28.1
10
+ Requires-Dist: h2>=4.2.0
11
+ Requires-Dist: dotenv>=0.9.9
12
+ Requires-Dist: mcp>=1.9.4
13
+
14
+ # Serper MCP Toolkit
15
+
16
+ A high-performance, asynchronous MCP server that provides comprehensive Google search and web content scraping capabilities through the Serper API (excluding some rarely used interfaces).
17
+
18
+ This project is built on `httpx`, utilizing asynchronous clients and connection pool management to offer LLMs a stable and efficient external information retrieval tool.
19
+
20
+ ## Key Features
21
+
22
+ - **Asynchronous Architecture**: Fully based on `asyncio` and `httpx`, ensuring high throughput and non-blocking I/O operations.
23
+ - **HTTP Connection Pool**: Manages and reuses TCP connections through a global `httpx.AsyncClient` instance, significantly improving performance under high concurrency.
24
+ - **Concurrency Control**: Built-in global and per-API endpoint concurrency semaphores effectively manage API request rates to prevent exceeding rate limits.
25
+ - **Automatic Retry Mechanism**: Integrated request retry functionality with exponential backoff strategy automatically handles temporary network fluctuations or server errors, enhancing service stability.
26
+ - **Intelligent Country Code Parsing**: Includes a comprehensive country name dictionary supporting inputs in Chinese, English, ISO Alpha-2/3, and other formats, with automatic normalization.
27
+ - **Flexible Environment Variable Configuration**: Supports fine-tuned service configuration via environment variables.
28
+
29
+ ## Available Tools
30
+
31
+ This service provides the following tools:
32
+
33
+ | Tool Name | Description |
34
+ | ------------------------ | -------------------------------------------- |
35
+ | `serper-general-search` | Performs general Google web searches. |
36
+ | `serper-image-search` | Performs Google image searches. |
37
+ | `serper-video-search` | Performs Google video searches. |
38
+ | `serper-place-search` | Performs Google place searches. |
39
+ | `serper-news-search` | Performs Google news searches. |
40
+ | `serper-lens-search` | Performs Google Lens reverse image searches via image URL. |
41
+ | `serper-scholar-search` | Performs Google Scholar searches. |
42
+ | `serper-scrape` | Scrapes and returns the content of a specified URL. |
43
+
44
+ ## Installation Guide
45
+
46
+ It is recommended to install using `pip` or `uv`.
47
+
48
+ ```bash
49
+ # Using pip
50
+ pip install serper-toolkit
51
+
52
+ # Or using uv
53
+ uv pip install serper-toolkit
54
+ ```
55
+
56
+ ## Quick Start
57
+
58
+ ### Set Environment Variables
59
+
60
+ Create a `.env` file in the project root directory and enter your Serper API key:
61
+
62
+ ```bash
63
+ SERPER_API_KEY="your-serper-api-key-here"
64
+ ```
65
+
66
+ ### Configure MCP Client
67
+
68
+ Add the following server configuration in the MCP client configuration file:
69
+
70
+ ```json
71
+ {
72
+ "mcpServers": {
73
+ "serper": {
74
+ "command": "python3",
75
+ "args": ["-m", "serper-toolkit"],
76
+ "env": {
77
+ "SERPER_API_KEY": "<Your Serper API key>"
78
+ }
79
+ }
80
+ }
81
+ }
82
+ ```
83
+
84
+ ```json
85
+ {
86
+ "mcpServers": {
87
+ "serper-toolkit": {
88
+ "command": "uvx",
89
+ "args": ["serper-toolkit"],
90
+ "env": {
91
+ "SERPER_API_KEY": "<Your Serper API key>"
92
+ }
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ ### Environment Variables
99
+
100
+ - `SERPER_MAX_CONNECTIONS`: Maximum number of HTTP client connections (default: 200).
101
+ - `SERPER_KEEPALIVE`: Maximum number of keep-alive HTTP client connections (default: 20).
102
+ - `SERPER_HTTP2`: Enable HTTP/2 (default: "0", set to "1" to enable).
103
+ - `SERPER_MAX_CONCURRENT_REQUESTS`: Global maximum concurrent requests (default: 200).
104
+ - `SERPER_RETRY_COUNT`: Maximum retry attempts for failed requests (default: 3).
105
+ - `SERPER_RETRY_BASE_DELAY`: Base delay time for retries in seconds (default: 0.5).
106
+ - `SERPER_ENDPOINT_CONCURRENCY`: Set concurrency per endpoint (JSON format), e.g., {"search":10,"scrape":2}.
107
+ - `SERPER_ENDPOINT_RETRYABLE`: Set retry allowance per endpoint (JSON format), e.g., {"scrape": false}.
108
+
109
+ ## Tool Parameters and Usage Examples
110
+
111
+ ### serper-general-search: Perform general web search
112
+
113
+ Parameters:
114
+
115
+ - `search_key_words` (str, required): Keywords to search.
116
+ - `search_country` (str, optional): Specify the country/region for search results. Supports Chinese names (e.g., "China"), English names (e.g., "United States"), or ISO codes (e.g., "US"). Default is "US".
117
+ - `search_num` (int, optional): Number of results to return, range 1-100. Default is 10.
118
+ - `search_time` (str, optional): Filter results by time range. Available values: "hour", "day", "week", "month", "year".
119
+
120
+ Example:
121
+
122
+ ```Python
123
+ result_json = serper_general_search(
124
+ search_key_words="AI advancements 2024",
125
+ search_country="United States",
126
+ search_num=5,
127
+ search_time="month"
128
+ )
129
+ ```
130
+
131
+ ### serper-lens-search: Perform reverse image search via image URL
132
+
133
+ Parameters:
134
+
135
+ - `image_url` (str, required): Public URL of the image to search.
136
+ - `search_country` (str, optional): Specify the country/region for search results. Default is "US".
137
+
138
+ Example:
139
+
140
+ ```Python
141
+ result_json = serper_lens_search(
142
+ image_url="https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png",
143
+ search_country="JP"
144
+ )
145
+ ```
146
+
147
+ ### serper-scrape: Scrape webpage content
148
+
149
+ Parameters:
150
+
151
+ - `url` (str, required): URL of the target webpage.
152
+ - `include_markdown` (bool, optional): Whether to include Markdown-formatted content in the returned results. Default is False.
153
+
154
+ Example:
155
+
156
+ ```Python
157
+ result_json = serper_scrape(
158
+ url="https://www.example.com",
159
+ include_markdown=True
160
+ )
161
+ ```
162
+
163
+ ## License Agreement
164
+
165
+ This project is licensed under the MIT License.
@@ -0,0 +1,9 @@
1
+ serper_toolkit/__init__.py,sha256=wKcw1Il2H4tTA8QSrgkRTBKqdtU5wJdo5KxpnKl7EOE,68
2
+ serper_toolkit/__main__.py,sha256=Gfly6uBEry5qVs6ulzz6YHuKd_aKSMdoQoolzjXBB2U,160
3
+ serper_toolkit/server.py,sha256=AbQlC29bgQJud583-o10_IJArlnSABGakpTx-XT31vY,24834
4
+ serper_toolkit/data/country_aliases.json,sha256=c2ScVZ4jyKocn_F7PYo-2xdmoOyb1fHn-qfLeMSqG94,15845
5
+ serper_toolkit-0.0.4.dist-info/METADATA,sha256=w3OKNjX7hviK9w1yq38Pwm3XEZLjHfhytjzLl5EULCY,5932
6
+ serper_toolkit-0.0.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ serper_toolkit-0.0.4.dist-info/entry_points.txt,sha256=2EvO9ttJ6jg2vKMxT3wxOS2DvHB7ot8kPe_3XihMh3U,64
8
+ serper_toolkit-0.0.4.dist-info/top_level.txt,sha256=tAeMIdGmGMJWIyzk2OwNp_vwYOENKaPMhIVqJdzObSs,15
9
+ serper_toolkit-0.0.4.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ serper-toolkit = serper_toolkit.__main__:main
@@ -0,0 +1 @@
1
+ serper_toolkit