@endday/search-mcp 1.0.0 → 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.
Files changed (85) hide show
  1. package/dist/index.js +4724 -0
  2. package/dist/search-mcp.js +4715 -0
  3. package/package.json +14 -14
  4. package/data/blocklist.generated.js +0 -2
  5. package/envs.js +0 -129
  6. package/index.js +0 -6
  7. package/mcp/search-mcp.js +0 -8
  8. package/src/content/extract.impl.js +0 -228
  9. package/src/content/extract.js +0 -1
  10. package/src/content/fetch.impl.js +0 -400
  11. package/src/content/fetch.js +0 -1
  12. package/src/core/crypto.js +0 -7
  13. package/src/core/errors.impl.js +0 -52
  14. package/src/core/errors.js +0 -1
  15. package/src/core/html.impl.js +0 -69
  16. package/src/core/html.js +0 -1
  17. package/src/mcp/config.js +0 -75
  18. package/src/mcp/format.js +0 -44
  19. package/src/mcp/index.js +0 -10
  20. package/src/mcp/local/content.js +0 -26
  21. package/src/mcp/local/search.js +0 -233
  22. package/src/mcp/schemas.js +0 -132
  23. package/src/mcp/server.js +0 -97
  24. package/src/mcp/tools/content.js +0 -31
  25. package/src/mcp/tools/jinaContent.js +0 -38
  26. package/src/mcp/tools/newsSearch.js +0 -22
  27. package/src/mcp/tools/webSearch.js +0 -57
  28. package/src/platform/auth.impl.js +0 -166
  29. package/src/platform/auth.js +0 -1
  30. package/src/platform/cache.impl.js +0 -166
  31. package/src/platform/cache.js +0 -1
  32. package/src/platform/health.impl.js +0 -133
  33. package/src/platform/health.js +0 -1
  34. package/src/platform/http.impl.js +0 -108
  35. package/src/platform/http.js +0 -1
  36. package/src/platform/logger.impl.js +0 -51
  37. package/src/platform/logger.js +0 -1
  38. package/src/platform/metrics.impl.js +0 -43
  39. package/src/platform/metrics.js +0 -1
  40. package/src/platform/nodeHttpClient.js +0 -104
  41. package/src/platform/rateLimit.impl.js +0 -141
  42. package/src/platform/rateLimit.js +0 -1
  43. package/src/platform/requestContext.impl.js +0 -10
  44. package/src/platform/requestContext.js +0 -1
  45. package/src/platform/session.impl.js +0 -198
  46. package/src/platform/session.js +0 -1
  47. package/src/platform/stateKv.impl.js +0 -18
  48. package/src/platform/stateKv.js +0 -1
  49. package/src/platform/tasks.impl.js +0 -17
  50. package/src/platform/tasks.js +0 -1
  51. package/src/routes/requestParams.impl.js +0 -12
  52. package/src/routes/requestParams.js +0 -1
  53. package/src/search/engineRegistry.impl.js +0 -117
  54. package/src/search/engineRegistry.js +0 -1
  55. package/src/search/engineRequest.impl.js +0 -377
  56. package/src/search/engineRequest.js +0 -1
  57. package/src/search/engineUtils.impl.js +0 -227
  58. package/src/search/engineUtils.js +0 -1
  59. package/src/search/engines/baidu.impl.js +0 -145
  60. package/src/search/engines/baidu.js +0 -2
  61. package/src/search/engines/bing.impl.js +0 -509
  62. package/src/search/engines/bing.js +0 -2
  63. package/src/search/engines/brave.impl.js +0 -223
  64. package/src/search/engines/brave.js +0 -2
  65. package/src/search/engines/duckduckgo.impl.js +0 -164
  66. package/src/search/engines/duckduckgo.js +0 -2
  67. package/src/search/engines/mojeek.impl.js +0 -115
  68. package/src/search/engines/mojeek.js +0 -2
  69. package/src/search/engines/qwant.impl.js +0 -188
  70. package/src/search/engines/qwant.js +0 -2
  71. package/src/search/engines/startpage.impl.js +0 -237
  72. package/src/search/engines/startpage.js +0 -2
  73. package/src/search/engines/toutiao.impl.js +0 -265
  74. package/src/search/engines/toutiao.js +0 -2
  75. package/src/search/engines/yahoo.impl.js +0 -379
  76. package/src/search/engines/yahoo.js +0 -2
  77. package/src/search/gateway.impl.js +0 -423
  78. package/src/search/gateway.js +0 -1
  79. package/src/search/ranking.impl.js +0 -381
  80. package/src/search/ranking.js +0 -1
  81. package/src/search/requestPolicy.impl.js +0 -137
  82. package/src/search/requestPolicy.js +0 -1
  83. package/src/search/upstreamSession.impl.js +0 -148
  84. package/src/search/upstreamSession.js +0 -1
  85. /package/{index.d.ts → dist/index.d.ts} +0 -0
@@ -1,233 +0,0 @@
1
- import { env } from "../../../envs.js";
2
- import { ApiError } from "../../core/errors.js";
3
- import { searchAllWithMeta } from "../../search/gateway.js";
4
-
5
- const LATIN_QUERY_RE = /^[\p{Script=Latin}\p{Number}\s'".,!?():/_+-]+$/u;
6
- const DEFAULT_NEWS_ENGINES = ["bing", "brave", "yahoo"];
7
-
8
- function normalizeSourceTypeList(value) {
9
- const items = Array.isArray(value)
10
- ? value
11
- : String(value || "")
12
- .split(",")
13
- .map((item) => item.trim());
14
-
15
- return [
16
- ...new Set(
17
- items
18
- .map((item) => String(item || "").trim().toLowerCase())
19
- .filter(Boolean)
20
- ),
21
- ];
22
- }
23
-
24
- function normalizeMinAuthorityScore(value) {
25
- if (value === undefined || value === null || value === "") {
26
- return null;
27
- }
28
-
29
- const parsed = Number.parseFloat(value);
30
- return Number.isFinite(parsed) ? parsed : null;
31
- }
32
-
33
- function normalizeSourceFilters(options) {
34
- const includeSourceTypes = normalizeSourceTypeList(options.include_source_types);
35
- const excludeSourceTypes = normalizeSourceTypeList(options.exclude_source_types);
36
- const minAuthorityScore = normalizeMinAuthorityScore(options.min_authority_score);
37
-
38
- return {
39
- include_source_types: includeSourceTypes,
40
- exclude_source_types: excludeSourceTypes,
41
- min_authority_score: minAuthorityScore,
42
- active:
43
- includeSourceTypes.length > 0 ||
44
- excludeSourceTypes.length > 0 ||
45
- minAuthorityScore !== null,
46
- };
47
- }
48
-
49
- function resultMatchesSourceFilters(result, filters) {
50
- if (!filters.active) {
51
- return true;
52
- }
53
-
54
- const sourceType = String(result.source_type || "unknown").toLowerCase();
55
- const authorityScore = Number.isFinite(result.authority_score)
56
- ? result.authority_score
57
- : 0;
58
-
59
- if (
60
- filters.include_source_types.length > 0 &&
61
- !filters.include_source_types.includes(sourceType)
62
- ) {
63
- return false;
64
- }
65
-
66
- if (filters.exclude_source_types.includes(sourceType)) {
67
- return false;
68
- }
69
-
70
- if (
71
- filters.min_authority_score !== null &&
72
- authorityScore < filters.min_authority_score
73
- ) {
74
- return false;
75
- }
76
-
77
- return true;
78
- }
79
-
80
- function applySourceFilters(response, filters) {
81
- if (!filters.active) {
82
- return response;
83
- }
84
-
85
- const results = response.results.filter((result) =>
86
- resultMatchesSourceFilters(result, filters)
87
- );
88
-
89
- return {
90
- ...response,
91
- number_of_results: results.length,
92
- source_filters: filters,
93
- results,
94
- };
95
- }
96
-
97
- function inferLanguageFromQuery(query, fallbackLanguage) {
98
- const normalizedQuery = String(query || "");
99
-
100
- if (/[\u3040-\u30ff]/u.test(normalizedQuery)) {
101
- return "ja-JP";
102
- }
103
-
104
- if (/[\uac00-\ud7af]/u.test(normalizedQuery)) {
105
- return "ko-KR";
106
- }
107
-
108
- if (/[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/u.test(normalizedQuery)) {
109
- return "zh-CN";
110
- }
111
-
112
- if (LATIN_QUERY_RE.test(normalizedQuery) && /[a-z]/i.test(normalizedQuery)) {
113
- return "en-US";
114
- }
115
-
116
- return fallbackLanguage;
117
- }
118
-
119
- function resolveLanguage(query, language) {
120
- return language || inferLanguageFromQuery(query, env.DEFAULT_LANGUAGE);
121
- }
122
-
123
- function isChineseLanguage(language) {
124
- return String(language || "").trim().toLowerCase().startsWith("zh");
125
- }
126
-
127
- function resolveRequestedEngines(engines, language, vertical = "web") {
128
- if (Array.isArray(engines) && engines.length > 0) {
129
- return engines;
130
- }
131
-
132
- if (vertical === "news") {
133
- return DEFAULT_NEWS_ENGINES;
134
- }
135
-
136
- return isChineseLanguage(language)
137
- ? env.DEFAULT_ENGINES_ZH
138
- : env.DEFAULT_ENGINES_NON_ZH;
139
- }
140
-
141
- function appendLocationToQuery(query, location) {
142
- const normalizedLocation = String(location || "").trim();
143
- if (!normalizedLocation) {
144
- return query;
145
- }
146
-
147
- if (String(query).toLowerCase().includes(normalizedLocation.toLowerCase())) {
148
- return query;
149
- }
150
-
151
- return `${query} ${normalizedLocation}`;
152
- }
153
-
154
- function createRuntimeContext(query, clientId) {
155
- return {
156
- request: new Request(
157
- `https://mcp.local/search?q=${encodeURIComponent(query)}`,
158
- {
159
- method: "GET",
160
- headers: {
161
- "x-mcp-client-id": clientId,
162
- },
163
- }
164
- ),
165
- };
166
- }
167
-
168
- function paginateResponse(response, options) {
169
- const offset = Math.max(0, Number.parseInt(options.offset ?? "0", 10) || 0);
170
- const count =
171
- options.count === undefined || options.count === null || options.count === ""
172
- ? null
173
- : Math.max(1, Number.parseInt(options.count, 10) || 0);
174
-
175
- if (offset === 0 && count === null) {
176
- return response;
177
- }
178
-
179
- const results =
180
- count === null
181
- ? response.results.slice(offset)
182
- : response.results.slice(offset, offset + count);
183
-
184
- return {
185
- ...response,
186
- number_of_results: results.length,
187
- offset,
188
- count,
189
- results,
190
- };
191
- }
192
-
193
- export async function searchLocal(query, engines = null, options = {}) {
194
- const normalizedQuery = String(query || "").trim();
195
- if (!normalizedQuery) {
196
- throw new ApiError({
197
- status: 400,
198
- code: "MISSING_QUERY",
199
- category: "validation",
200
- message: "query required",
201
- });
202
- }
203
-
204
- const vertical = String(options.vertical || "web").trim().toLowerCase() || "web";
205
- const language = resolveLanguage(
206
- normalizedQuery,
207
- options.search_lang || options.ui_lang || options.language
208
- );
209
- const requestedEngines = resolveRequestedEngines(engines, language, vertical);
210
- const effectiveQuery = appendLocationToQuery(normalizedQuery, options.location);
211
- const filters = normalizeSourceFilters(options);
212
- const clientId = String(options.clientId || "mcp-local").trim() || "mcp-local";
213
- const runtimeContext = createRuntimeContext(effectiveQuery, clientId);
214
-
215
- const { response } = await searchAllWithMeta({
216
- vertical,
217
- query: effectiveQuery,
218
- engines: requestedEngines,
219
- language,
220
- time_range: options.time_range,
221
- pageno: options.pageno,
222
- clientId,
223
- runtimeContext,
224
- });
225
-
226
- return {
227
- ...paginateResponse(applySourceFilters(response, filters), options),
228
- query: normalizedQuery,
229
- effective_query: effectiveQuery,
230
- location: options.location || null,
231
- location_source: options.location ? "explicit" : "disabled",
232
- };
233
- }
@@ -1,132 +0,0 @@
1
- import { MAX_CHARS_DEFAULT, MAX_CHARS_MAX, MAX_CHARS_MIN } from "./format.js";
2
-
3
- function createSearchProperties(allEngines, options = {}) {
4
- const properties = {
5
- query: {
6
- type: "string",
7
- description: "Search query",
8
- },
9
- engines: {
10
- type: "array",
11
- items: {
12
- type: "string",
13
- enum: allEngines,
14
- },
15
- description: `Engines to query. Default: ${allEngines.join(", ")}.`,
16
- },
17
- language: {
18
- type: "string",
19
- description: "Language hint (e.g. en, zh-CN)",
20
- },
21
- search_lang: {
22
- type: "string",
23
- description: "Search language hint (e.g. en, zh-CN)",
24
- },
25
- ui_lang: {
26
- type: "string",
27
- description: "UI locale hint (e.g. en-US, zh-CN)",
28
- },
29
- time_range: {
30
- type: "string",
31
- enum: ["day", "week", "month", "year"],
32
- description: "Time range filter",
33
- },
34
- count: {
35
- type: "integer",
36
- minimum: 1,
37
- description: "Max number of results to return after ranking",
38
- },
39
- offset: {
40
- type: "integer",
41
- minimum: 0,
42
- description: "Result offset to apply after ranking",
43
- },
44
- min_authority_score: {
45
- type: "number",
46
- description: "Min source authority score",
47
- },
48
- include_source_types: {
49
- type: "array",
50
- items: { type: "string" },
51
- description: "Source types to include",
52
- },
53
- exclude_source_types: {
54
- type: "array",
55
- items: { type: "string" },
56
- description: "Source types to exclude",
57
- },
58
- };
59
-
60
- if (options.includeLocation !== false) {
61
- properties.location = {
62
- type: "string",
63
- description: "Location hint for query enrichment",
64
- };
65
- }
66
-
67
- if (options.includePage !== false) {
68
- properties.pageno = {
69
- type: "integer",
70
- minimum: 0,
71
- description: "Page number (0-based)",
72
- };
73
- }
74
-
75
- return properties;
76
- }
77
-
78
- export function createSearchInputSchema(allEngines) {
79
- return {
80
- type: "object",
81
- properties: createSearchProperties(allEngines, {
82
- includeLocation: true,
83
- includePage: true,
84
- }),
85
- required: ["query"],
86
- };
87
- }
88
-
89
- export function createNewsSearchInputSchema(allEngines) {
90
- return {
91
- type: "object",
92
- properties: createSearchProperties(allEngines, {
93
- includeLocation: false,
94
- includePage: false,
95
- }),
96
- required: ["query"],
97
- };
98
- }
99
-
100
- export const CONTENT_INPUT_SCHEMA = {
101
- type: "object",
102
- properties: {
103
- url: {
104
- type: "string",
105
- description: "URL to extract content from",
106
- },
107
- max_chars: {
108
- type: "integer",
109
- minimum: MAX_CHARS_MIN,
110
- maximum: MAX_CHARS_MAX,
111
- description: `Max text chars to return. Default: ${MAX_CHARS_DEFAULT}.`,
112
- },
113
- },
114
- required: ["url"],
115
- };
116
-
117
- export const JINA_CONTENT_INPUT_SCHEMA = {
118
- type: "object",
119
- properties: {
120
- url: {
121
- type: "string",
122
- description: "URL to extract content from via Jina AI reader",
123
- },
124
- max_chars: {
125
- type: "integer",
126
- minimum: MAX_CHARS_MIN,
127
- maximum: MAX_CHARS_MAX,
128
- description: `Max text chars to return. Default: ${MAX_CHARS_DEFAULT}.`,
129
- },
130
- },
131
- required: ["url"],
132
- };
package/src/mcp/server.js DELETED
@@ -1,97 +0,0 @@
1
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import {
4
- CallToolRequestSchema,
5
- ListToolsRequestSchema,
6
- } from "@modelcontextprotocol/sdk/types.js";
7
-
8
- import {
9
- CONTENT_INPUT_SCHEMA,
10
- createNewsSearchInputSchema,
11
- createSearchInputSchema,
12
- JINA_CONTENT_INPUT_SCHEMA,
13
- } from "./schemas.js";
14
- import { getSupportedEnginesForVertical } from "../search/engineRegistry.js";
15
- import { handleWebSearch } from "./tools/webSearch.js";
16
- import { handleNewsSearch } from "./tools/newsSearch.js";
17
- import { handleContent } from "./tools/content.js";
18
- import { handleJinaContent } from "./tools/jinaContent.js";
19
-
20
- export function createServer(config) {
21
- const newsEngineSet = new Set(getSupportedEnginesForVertical("news"));
22
- const newsEngines =
23
- config.newsEngines ||
24
- config.allEngines.filter((engineName) => newsEngineSet.has(engineName));
25
- const server = new Server(
26
- { name: "search-mcp", version: "2.0.0" },
27
- { capabilities: { tools: {} } }
28
- );
29
-
30
- const TOOL_LIST = {
31
- tools: [
32
- {
33
- name: "web_search",
34
- description:
35
- "Search the web with local-first MCP execution. Returns deduplicated results with snippets and source authority scores.",
36
- inputSchema: createSearchInputSchema(config.allEngines),
37
- },
38
- {
39
- name: "news_search",
40
- description:
41
- "Search recent news with an explicit news-search capability. Use this instead of web_search when the caller needs news results rather than general web pages.",
42
- inputSchema: createNewsSearchInputSchema(newsEngines),
43
- },
44
- {
45
- name: "content",
46
- description:
47
- "Extract readable text from a URL with local-first MCP execution. Returns title, URL, and extracted content.",
48
- inputSchema: CONTENT_INPUT_SCHEMA,
49
- },
50
- {
51
- name: "jina_content",
52
- description:
53
- "Extract readable text from a URL using Jina AI reader. Per-user rate limit unless JINA_API_KEY is configured.",
54
- inputSchema: JINA_CONTENT_INPUT_SCHEMA,
55
- },
56
- ],
57
- };
58
-
59
- const VALID_TOOL_NAMES = new Set([
60
- "web_search",
61
- "news_search",
62
- "content",
63
- "jina_content",
64
- ]);
65
-
66
- server.setRequestHandler(ListToolsRequestSchema, async () => TOOL_LIST);
67
-
68
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
69
- const { name } = request.params;
70
- if (!VALID_TOOL_NAMES.has(name)) {
71
- throw new Error(`Unknown tool: ${name}`);
72
- }
73
-
74
- const args = request.params.arguments || {};
75
-
76
- if (name === "web_search") {
77
- return handleWebSearch(config, args);
78
- }
79
-
80
- if (name === "news_search") {
81
- return handleNewsSearch(config, args);
82
- }
83
-
84
- if (name === "content") {
85
- return handleContent(config, args);
86
- }
87
-
88
- return handleJinaContent(config, args);
89
- });
90
-
91
- return server;
92
- }
93
-
94
- export async function startServer(server) {
95
- const transport = new StdioServerTransport();
96
- await server.connect(transport);
97
- }
@@ -1,31 +0,0 @@
1
- import {
2
- formatContentResponse,
3
- normalizeMaxChars,
4
- } from "../format.js";
5
- import { contentLocal, requireUrl } from "../local/content.js";
6
-
7
- function estimateContentMaxBytes(maxChars) {
8
- return Math.max(1000000, maxChars * 16 + 200000);
9
- }
10
-
11
- async function executeContent(config, url, estimatedMaxBytes) {
12
- return contentLocal(url, { max_bytes: estimatedMaxBytes });
13
- }
14
-
15
- export async function handleContent(config, args) {
16
- const url = requireUrl(args);
17
- const maxChars = normalizeMaxChars(args.max_chars);
18
- const estimatedMaxBytes = estimateContentMaxBytes(maxChars);
19
-
20
- try {
21
- const result = await executeContent(config, url, estimatedMaxBytes);
22
- return {
23
- content: [{ type: "text", text: formatContentResponse(result, maxChars) }],
24
- };
25
- } catch (error) {
26
- return {
27
- content: [{ type: "text", text: `Content failed: ${error.message}` }],
28
- isError: true,
29
- };
30
- }
31
- }
@@ -1,38 +0,0 @@
1
- import { normalizeMaxChars, truncateText } from "../format.js";
2
- import { requireUrl } from "../local/content.js";
3
-
4
- async function jinaContentAPI(config, targetUrl) {
5
- const encodedUrl = encodeURIComponent(targetUrl);
6
- const response = await fetch(`${config.jinaBaseUrl}${encodedUrl}`, {
7
- headers: config.jinaApiKey
8
- ? { Authorization: `Bearer ${config.jinaApiKey}` }
9
- : {},
10
- });
11
-
12
- if (!response.ok) {
13
- const body = await response.text().catch(() => "");
14
- throw new Error(`Jina failed: ${response.status} ${body}`.trim());
15
- }
16
-
17
- return {
18
- url: targetUrl,
19
- text: await response.text(),
20
- };
21
- }
22
-
23
- export async function handleJinaContent(config, args) {
24
- const url = requireUrl(args);
25
- const maxChars = normalizeMaxChars(args.max_chars);
26
-
27
- try {
28
- const result = await jinaContentAPI(config, url);
29
- return {
30
- content: [{ type: "text", text: truncateText(String(result.text || ""), maxChars) }],
31
- };
32
- } catch (error) {
33
- return {
34
- content: [{ type: "text", text: error.message }],
35
- isError: true,
36
- };
37
- }
38
- }
@@ -1,22 +0,0 @@
1
- import { formatSearchResponse } from "../format.js";
2
- import { executeSearch } from "./webSearch.js";
3
-
4
- export async function handleNewsSearch(config, args) {
5
- if (!args.query || typeof args.query !== "string") {
6
- throw new Error("query required");
7
- }
8
-
9
- try {
10
- const result = await executeSearch(config, args, {
11
- vertical: "news",
12
- });
13
- return {
14
- content: [{ type: "text", text: formatSearchResponse(result) }],
15
- };
16
- } catch (error) {
17
- return {
18
- content: [{ type: "text", text: `News search failed: ${error.message}` }],
19
- isError: true,
20
- };
21
- }
22
- }
@@ -1,57 +0,0 @@
1
- import { formatSearchResponse } from "../format.js";
2
- import { searchLocal } from "../local/search.js";
3
-
4
- export function buildSearchOptions(args, overrides = {}) {
5
- const searchOptions = {
6
- vertical: overrides.vertical || "web",
7
- language: args.language,
8
- search_lang: args.search_lang,
9
- ui_lang: args.ui_lang,
10
- location: args.location,
11
- time_range: args.time_range,
12
- pageno: args.pageno,
13
- count: args.count,
14
- offset: args.offset,
15
- clientId: args.client_id,
16
- };
17
-
18
- if (Number.isFinite(args.min_authority_score)) {
19
- searchOptions.min_authority_score = args.min_authority_score;
20
- }
21
- if (Array.isArray(args.include_source_types)) {
22
- searchOptions.include_source_types = args.include_source_types;
23
- }
24
- if (Array.isArray(args.exclude_source_types)) {
25
- searchOptions.exclude_source_types = args.exclude_source_types;
26
- }
27
-
28
- return searchOptions;
29
- }
30
-
31
- export async function executeSearch(config, args, overrides = {}) {
32
- const engines = Array.isArray(args.engines) && args.engines.length > 0 ? args.engines : null;
33
- const searchOptions = {
34
- ...buildSearchOptions(args, overrides),
35
- clientId: args.client_id || config.localClientId,
36
- };
37
-
38
- return searchLocal(args.query, engines, searchOptions);
39
- }
40
-
41
- export async function handleWebSearch(config, args) {
42
- if (!args.query || typeof args.query !== "string") {
43
- throw new Error("query required");
44
- }
45
-
46
- try {
47
- const result = await executeSearch(config, args);
48
- return {
49
- content: [{ type: "text", text: formatSearchResponse(result) }],
50
- };
51
- } catch (error) {
52
- return {
53
- content: [{ type: "text", text: `Search failed: ${error.message}` }],
54
- isError: true,
55
- };
56
- }
57
- }