deepseek-web-retrieval 0.0.0-stage → 0.2.0
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/CHANGELOG.md +55 -0
- package/LICENSE +21 -0
- package/README.md +96 -2
- package/dist/cli.js +865 -0
- package/docs/hosts.jsonc +129 -0
- package/docs/integration.md +159 -0
- package/docs/mcp-compatibility.md +252 -0
- package/docs/opencode-v2.md +246 -0
- package/docs/pi.md +181 -0
- package/examples/opencode.global.jsonc +25 -0
- package/examples/opencode.project.jsonc +24 -0
- package/examples/pi.mcp.jsonc +18 -0
- package/index.ts +2 -0
- package/package.json +67 -4
- package/src/core/auth.ts +8 -0
- package/src/core/client.ts +336 -0
- package/src/core/constants.ts +25 -0
- package/src/core/domains.ts +153 -0
- package/src/core/format.ts +97 -0
- package/src/core/index.ts +9 -0
- package/src/core/options.ts +109 -0
- package/src/core/search.ts +86 -0
- package/src/core/types.ts +83 -0
- package/src/core/url.ts +42 -0
- package/src/mcp/auth.ts +12 -0
- package/src/mcp/cli.ts +23 -0
- package/src/mcp/config.ts +81 -0
- package/src/mcp/format.ts +31 -0
- package/src/mcp/server.ts +139 -0
- package/src/opencode/auth.ts +69 -0
- package/src/opencode/plugin.ts +150 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/mcp/cli.ts
|
|
4
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
5
|
+
|
|
6
|
+
// src/core/auth.ts
|
|
7
|
+
var API_KEY_ENV = "DEEPSEEK_API_KEY";
|
|
8
|
+
function apiKeyFromEnv(env = process.env) {
|
|
9
|
+
const key = env[API_KEY_ENV]?.trim();
|
|
10
|
+
return key ? key : undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/mcp/auth.ts
|
|
14
|
+
var MISSING_API_KEY_MESSAGE = "DeepSeek API key not found. Set DEEPSEEK_API_KEY in the MCP server's env configuration.";
|
|
15
|
+
function requireApiKey(env) {
|
|
16
|
+
const apiKey = apiKeyFromEnv(env);
|
|
17
|
+
if (!apiKey)
|
|
18
|
+
throw new Error(MISSING_API_KEY_MESSAGE);
|
|
19
|
+
return apiKey;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/core/constants.ts
|
|
23
|
+
var SEARCH_ENDPOINT = "https://api.deepseek.com/v1/tools/search";
|
|
24
|
+
var FETCH_ENDPOINT = "https://api.deepseek.com/v1/tools/fetch";
|
|
25
|
+
var DEFAULT_SEARCH_COUNT = 10;
|
|
26
|
+
var DEFAULT_MAX_RESULTS = 5;
|
|
27
|
+
var DEFAULT_MIN_RERANK = 1.5;
|
|
28
|
+
var DEFAULT_MIN_AUTHORITY = 2;
|
|
29
|
+
var DEFAULT_MAX_SUMMARY_CHARS = 1200;
|
|
30
|
+
var DEFAULT_FETCH_MAX_CHARS = 16000;
|
|
31
|
+
var DEFAULT_TIMEOUT_MS = 30000;
|
|
32
|
+
var MIN_TIMEOUT_MS = 1000;
|
|
33
|
+
var MAX_TIMEOUT_MS = 120000;
|
|
34
|
+
var MAX_QUERY_CHARS = 128;
|
|
35
|
+
var MAX_URL_CHARS = 8192;
|
|
36
|
+
var MAX_ERROR_BODY_CHARS = 2000;
|
|
37
|
+
var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
38
|
+
var FETCH_TRUNCATION_MARKER = `
|
|
39
|
+
|
|
40
|
+
[content truncated]`;
|
|
41
|
+
var DEFAULT_API_KEY_SOURCE = "auto";
|
|
42
|
+
var DEFAULT_MAX_RETRIES = 0;
|
|
43
|
+
var MAX_RETRIES = 5;
|
|
44
|
+
var DEFAULT_RETRY_BASE_DELAY_MS = 250;
|
|
45
|
+
var MAX_RETRY_DELAY_MS = 1e4;
|
|
46
|
+
|
|
47
|
+
// src/core/domains.ts
|
|
48
|
+
import { isIP } from "node:net";
|
|
49
|
+
function normalizeDomain(value) {
|
|
50
|
+
const trimmed = value.trim();
|
|
51
|
+
if (!trimmed || /\s/.test(trimmed))
|
|
52
|
+
return;
|
|
53
|
+
const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed);
|
|
54
|
+
if (hasScheme && !/^https?:\/\//i.test(trimmed))
|
|
55
|
+
return;
|
|
56
|
+
let hostname;
|
|
57
|
+
try {
|
|
58
|
+
hostname = new URL(hasScheme ? trimmed : `http://${trimmed}`).hostname;
|
|
59
|
+
} catch {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
return normalizeHostname(hostname);
|
|
63
|
+
}
|
|
64
|
+
function normalizeHostname(value) {
|
|
65
|
+
return value.trim().toLowerCase().replace(/^\[/, "").replace(/\]$/, "").replace(/\.+$/, "");
|
|
66
|
+
}
|
|
67
|
+
function domainMatches(hostname, domain) {
|
|
68
|
+
const host = normalizeHostname(hostname);
|
|
69
|
+
const target = normalizeHostname(domain);
|
|
70
|
+
if (!host || !target)
|
|
71
|
+
return false;
|
|
72
|
+
return host === target || host.endsWith(`.${target}`);
|
|
73
|
+
}
|
|
74
|
+
function normalizeDomains(values) {
|
|
75
|
+
if (!Array.isArray(values))
|
|
76
|
+
return [];
|
|
77
|
+
const domains = new Set;
|
|
78
|
+
for (const value of values) {
|
|
79
|
+
if (typeof value !== "string")
|
|
80
|
+
continue;
|
|
81
|
+
const domain = normalizeDomain(value);
|
|
82
|
+
if (domain)
|
|
83
|
+
domains.add(domain);
|
|
84
|
+
}
|
|
85
|
+
return [...domains];
|
|
86
|
+
}
|
|
87
|
+
function isPrivateHostname(value) {
|
|
88
|
+
const host = normalizeHostname(value);
|
|
89
|
+
if (!host)
|
|
90
|
+
return true;
|
|
91
|
+
if (host === "localhost" || host === "local")
|
|
92
|
+
return true;
|
|
93
|
+
if (host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal") || host.endsWith(".lan") || host.endsWith(".home.arpa")) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
const version = isIP(host);
|
|
97
|
+
if (version === 4)
|
|
98
|
+
return isPrivateIpv4(host);
|
|
99
|
+
if (version === 6)
|
|
100
|
+
return isPrivateIpv6(host);
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
function assertFetchHostPolicy(url, options) {
|
|
104
|
+
const host = normalizeHostname(url.hostname);
|
|
105
|
+
if (options.blockPrivateHosts && isPrivateHostname(host)) {
|
|
106
|
+
throw new Error(`Fetch blocked for ${host}: local/private addresses are not reachable by DeepSeek. ` + "Set fetchBlockPrivateHosts=false only if you really need to send the URL.");
|
|
107
|
+
}
|
|
108
|
+
const blocked = options.blockedDomains.find((domain) => domainMatches(host, domain));
|
|
109
|
+
if (blocked) {
|
|
110
|
+
throw new Error(`Fetch blocked for ${host}: matched fetchBlockedDomains entry "${blocked}"`);
|
|
111
|
+
}
|
|
112
|
+
if (options.allowedDomains.length > 0 && !options.allowedDomains.some((domain) => domainMatches(host, domain))) {
|
|
113
|
+
throw new Error(`Fetch not allowed for ${host}: add it to fetchAllowedDomains (exact host or subdomain)`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function isPrivateIpv4(host) {
|
|
117
|
+
const parts = host.split(".").map((part) => Number(part));
|
|
118
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
const [a, b] = parts;
|
|
122
|
+
return a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 198 && (b === 18 || b === 19) || a >= 224;
|
|
123
|
+
}
|
|
124
|
+
function isPrivateIpv6(host) {
|
|
125
|
+
const value = host.toLowerCase();
|
|
126
|
+
if (value === "::" || value === "::1")
|
|
127
|
+
return true;
|
|
128
|
+
if (value.startsWith("fc") || value.startsWith("fd"))
|
|
129
|
+
return true;
|
|
130
|
+
if (/^fe[89ab]/.test(value))
|
|
131
|
+
return true;
|
|
132
|
+
const dottedMapped = value.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/)?.[1];
|
|
133
|
+
if (dottedMapped)
|
|
134
|
+
return isPrivateIpv4(dottedMapped);
|
|
135
|
+
const mapped = ipv4FromIpv6Hextets(value, /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
136
|
+
if (mapped)
|
|
137
|
+
return isPrivateIpv4(mapped);
|
|
138
|
+
const compatible = ipv4FromIpv6Hextets(value, /^::([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
139
|
+
if (compatible)
|
|
140
|
+
return isPrivateIpv4(compatible);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
function ipv4FromIpv6Hextets(value, pattern) {
|
|
144
|
+
const match = value.match(pattern);
|
|
145
|
+
if (!match)
|
|
146
|
+
return;
|
|
147
|
+
const high = Number.parseInt(match[1], 16);
|
|
148
|
+
const low = Number.parseInt(match[2], 16);
|
|
149
|
+
if (!Number.isFinite(high) || !Number.isFinite(low))
|
|
150
|
+
return;
|
|
151
|
+
return `${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/core/options.ts
|
|
155
|
+
function resolvePluginOptions(input) {
|
|
156
|
+
return {
|
|
157
|
+
search: resolveSearchOptions(input),
|
|
158
|
+
fetch: resolveFetchOptions(input),
|
|
159
|
+
auth: resolveAuthOptions(input),
|
|
160
|
+
retry: resolveRetryOptions(input)
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function resolveSearchOptions(input) {
|
|
164
|
+
return {
|
|
165
|
+
count: clampInt(input?.count, DEFAULT_SEARCH_COUNT, 1, 50),
|
|
166
|
+
maxResults: clampInt(input?.maxResults, DEFAULT_MAX_RESULTS, 0, 50),
|
|
167
|
+
minRerank: finiteNumber(input?.minRerank, DEFAULT_MIN_RERANK),
|
|
168
|
+
minAuthority: finiteNumber(input?.minAuthority, DEFAULT_MIN_AUTHORITY),
|
|
169
|
+
allowUnknownAuthority: input?.allowUnknownAuthority === true,
|
|
170
|
+
blockedDomains: normalizeDomains(input?.blockedDomains),
|
|
171
|
+
includeDomains: normalizeDomains(input?.includeDomains),
|
|
172
|
+
language: optionalText(input?.language),
|
|
173
|
+
maxSummaryChars: clampInt(input?.maxSummaryChars, DEFAULT_MAX_SUMMARY_CHARS, 0, 1e6),
|
|
174
|
+
timeoutMs: clampInt(input?.searchTimeoutMs, DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS),
|
|
175
|
+
setDefault: input?.setDefaultWebsearch === true
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function resolveFetchOptions(input) {
|
|
179
|
+
return {
|
|
180
|
+
maxChars: clampInt(input?.fetchMaxChars, DEFAULT_FETCH_MAX_CHARS, 0, 1e6),
|
|
181
|
+
allowedDomains: normalizeDomains(input?.fetchAllowedDomains),
|
|
182
|
+
blockedDomains: normalizeDomains(input?.fetchBlockedDomains),
|
|
183
|
+
timeoutMs: clampInt(input?.fetchTimeoutMs, DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS),
|
|
184
|
+
overrideWebfetch: input?.fetchOverrideWebfetch === true,
|
|
185
|
+
registerTool: input?.fetchRegisterTool !== false,
|
|
186
|
+
blockPrivateHosts: input?.fetchBlockPrivateHosts !== false
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function resolveAuthOptions(input) {
|
|
190
|
+
const value = input?.apiKeySource;
|
|
191
|
+
return {
|
|
192
|
+
source: value === "opencode" || value === "env" ? value : DEFAULT_API_KEY_SOURCE
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function resolveRetryOptions(input) {
|
|
196
|
+
return {
|
|
197
|
+
maxRetries: clampInt(input?.maxRetries, DEFAULT_MAX_RETRIES, 0, MAX_RETRIES),
|
|
198
|
+
baseDelayMs: clampInt(input?.retryBaseDelayMs, DEFAULT_RETRY_BASE_DELAY_MS, 0, MAX_RETRY_DELAY_MS)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function optionalText(value) {
|
|
202
|
+
if (typeof value !== "string")
|
|
203
|
+
return;
|
|
204
|
+
const trimmed = value.trim();
|
|
205
|
+
return trimmed ? trimmed : undefined;
|
|
206
|
+
}
|
|
207
|
+
function finiteNumber(value, fallback) {
|
|
208
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
209
|
+
}
|
|
210
|
+
function clampInt(value, fallback, min, max) {
|
|
211
|
+
const number = typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : fallback;
|
|
212
|
+
return Math.min(max, Math.max(min, number));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/mcp/config.ts
|
|
216
|
+
function resolveEnvOptions(env = process.env) {
|
|
217
|
+
return resolvePluginOptions(pluginOptionsFromEnv(env));
|
|
218
|
+
}
|
|
219
|
+
function pluginOptionsFromEnv(env) {
|
|
220
|
+
const options = {};
|
|
221
|
+
putNumber(options, "count", env.DEEPSEEK_SEARCH_COUNT);
|
|
222
|
+
putNumber(options, "maxResults", env.DEEPSEEK_SEARCH_MAX_RESULTS);
|
|
223
|
+
putNumber(options, "minRerank", env.DEEPSEEK_SEARCH_MIN_RERANK);
|
|
224
|
+
putNumber(options, "minAuthority", env.DEEPSEEK_SEARCH_MIN_AUTHORITY);
|
|
225
|
+
putBoolean(options, "allowUnknownAuthority", env.DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY);
|
|
226
|
+
putDomains(options, "blockedDomains", env.DEEPSEEK_SEARCH_BLOCKED_DOMAINS);
|
|
227
|
+
putDomains(options, "includeDomains", env.DEEPSEEK_SEARCH_INCLUDE_DOMAINS);
|
|
228
|
+
putText(options, "language", env.DEEPSEEK_SEARCH_LANGUAGE);
|
|
229
|
+
putNumber(options, "maxSummaryChars", env.DEEPSEEK_SEARCH_MAX_SUMMARY_CHARS);
|
|
230
|
+
putNumber(options, "searchTimeoutMs", env.DEEPSEEK_SEARCH_TIMEOUT_MS);
|
|
231
|
+
putNumber(options, "fetchMaxChars", env.DEEPSEEK_FETCH_MAX_CHARS);
|
|
232
|
+
putDomains(options, "fetchAllowedDomains", env.DEEPSEEK_FETCH_ALLOWED_DOMAINS);
|
|
233
|
+
putDomains(options, "fetchBlockedDomains", env.DEEPSEEK_FETCH_BLOCKED_DOMAINS);
|
|
234
|
+
putNumber(options, "fetchTimeoutMs", env.DEEPSEEK_FETCH_TIMEOUT_MS);
|
|
235
|
+
if (isFalseValue(env.DEEPSEEK_FETCH_BLOCK_PRIVATE_HOSTS)) {
|
|
236
|
+
options.fetchBlockPrivateHosts = false;
|
|
237
|
+
}
|
|
238
|
+
putNumber(options, "maxRetries", env.DEEPSEEK_MAX_RETRIES);
|
|
239
|
+
putNumber(options, "retryBaseDelayMs", env.DEEPSEEK_RETRY_BASE_DELAY_MS);
|
|
240
|
+
return options;
|
|
241
|
+
}
|
|
242
|
+
function putNumber(target, key, value) {
|
|
243
|
+
if (value === undefined)
|
|
244
|
+
return;
|
|
245
|
+
const trimmed = value.trim();
|
|
246
|
+
if (!trimmed)
|
|
247
|
+
return;
|
|
248
|
+
const parsed = Number(trimmed);
|
|
249
|
+
if (Number.isFinite(parsed))
|
|
250
|
+
target[key] = parsed;
|
|
251
|
+
}
|
|
252
|
+
function putText(target, key, value) {
|
|
253
|
+
if (value === undefined)
|
|
254
|
+
return;
|
|
255
|
+
const trimmed = value.trim();
|
|
256
|
+
if (trimmed)
|
|
257
|
+
target[key] = trimmed;
|
|
258
|
+
}
|
|
259
|
+
function putBoolean(target, key, value) {
|
|
260
|
+
if (isTrueValue(value))
|
|
261
|
+
target[key] = true;
|
|
262
|
+
}
|
|
263
|
+
function putDomains(target, key, value) {
|
|
264
|
+
if (value === undefined || !value.trim())
|
|
265
|
+
return;
|
|
266
|
+
const domains = value.split(",").map((domain) => domain.trim()).filter((domain) => domain.length > 0);
|
|
267
|
+
if (domains.length > 0)
|
|
268
|
+
target[key] = domains;
|
|
269
|
+
}
|
|
270
|
+
function isTrueValue(value) {
|
|
271
|
+
const trimmed = value?.trim().toLowerCase();
|
|
272
|
+
return trimmed === "true" || trimmed === "1" || trimmed === "yes" || trimmed === "on";
|
|
273
|
+
}
|
|
274
|
+
function isFalseValue(value) {
|
|
275
|
+
const trimmed = value?.trim().toLowerCase();
|
|
276
|
+
return trimmed === "false" || trimmed === "0" || trimmed === "no" || trimmed === "off";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/mcp/server.ts
|
|
280
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
281
|
+
import * as z from "zod/v4";
|
|
282
|
+
|
|
283
|
+
// src/core/format.ts
|
|
284
|
+
function truncate(value, maxChars) {
|
|
285
|
+
if (maxChars <= 0)
|
|
286
|
+
return "";
|
|
287
|
+
if (value.length <= maxChars)
|
|
288
|
+
return value;
|
|
289
|
+
return Array.from(value).slice(0, maxChars).join("");
|
|
290
|
+
}
|
|
291
|
+
function formatFetchedContent(title, content, maxChars) {
|
|
292
|
+
const document = withTitle(title, content);
|
|
293
|
+
const length = Array.from(document).length;
|
|
294
|
+
if (maxChars <= 0)
|
|
295
|
+
return { content: "", truncated: length > 0 };
|
|
296
|
+
if (length <= maxChars)
|
|
297
|
+
return { content: document, truncated: false };
|
|
298
|
+
const markerLength = Array.from(FETCH_TRUNCATION_MARKER).length;
|
|
299
|
+
if (maxChars <= markerLength) {
|
|
300
|
+
return { content: truncate(document, maxChars), truncated: true };
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
content: `${truncate(document, maxChars - markerLength)}${FETCH_TRUNCATION_MARKER}`,
|
|
304
|
+
truncated: true
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function withTitle(title, content) {
|
|
308
|
+
const heading = title.trim();
|
|
309
|
+
if (!heading)
|
|
310
|
+
return content;
|
|
311
|
+
const body = content.trimStart();
|
|
312
|
+
const lines = body.split(/\r?\n/);
|
|
313
|
+
const firstHeading = lines[0]?.match(/^\\?#\s+(.+?)\s*$/)?.[1];
|
|
314
|
+
if (firstHeading && normalizeHeading(firstHeading) === normalizeHeading(heading)) {
|
|
315
|
+
lines[0] = `# ${heading}`;
|
|
316
|
+
return lines.join(`
|
|
317
|
+
`);
|
|
318
|
+
}
|
|
319
|
+
return `# ${heading}
|
|
320
|
+
|
|
321
|
+
${content}`;
|
|
322
|
+
}
|
|
323
|
+
function normalizeHeading(value) {
|
|
324
|
+
return value.trim().replace(/\s+/g, " ").replace(/\s+#+\s*$/, "").toLowerCase();
|
|
325
|
+
}
|
|
326
|
+
function redactSecrets(value) {
|
|
327
|
+
return value.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "sk-***").replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, "Bearer ***");
|
|
328
|
+
}
|
|
329
|
+
function formatApiError(operation, status, body) {
|
|
330
|
+
const detail = redactSecrets(body.trim());
|
|
331
|
+
const detailChars = Array.from(detail);
|
|
332
|
+
const text = detail ? detailChars.length > MAX_ERROR_BODY_CHARS ? `${detailChars.slice(0, MAX_ERROR_BODY_CHARS).join("")}…` : detail : "empty response body";
|
|
333
|
+
const hint = status === 401 ? "invalid API key" : status === 402 ? "insufficient balance" : status === 403 ? "permission denied" : status === 429 ? "rate limited" : undefined;
|
|
334
|
+
const label = hint ? `${operation} failed (HTTP ${status}, ${hint})` : `${operation} failed (HTTP ${status})`;
|
|
335
|
+
return `${label}: ${text}`;
|
|
336
|
+
}
|
|
337
|
+
function formatTimeoutError(operation, timeoutMs) {
|
|
338
|
+
return `${operation} timed out after ${timeoutMs} ms`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/core/url.ts
|
|
342
|
+
function parseFetchUrl(value) {
|
|
343
|
+
const input = value.trim();
|
|
344
|
+
if (!input)
|
|
345
|
+
throw new Error("URL must not be empty");
|
|
346
|
+
if (input.length > MAX_URL_CHARS) {
|
|
347
|
+
throw new Error(`URL must be ${MAX_URL_CHARS} characters or fewer`);
|
|
348
|
+
}
|
|
349
|
+
let url;
|
|
350
|
+
try {
|
|
351
|
+
url = new URL(input);
|
|
352
|
+
} catch {
|
|
353
|
+
throw new Error("URL must be an absolute http:// or https:// URL");
|
|
354
|
+
}
|
|
355
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
356
|
+
throw new Error("URL must use http:// or https://");
|
|
357
|
+
}
|
|
358
|
+
if (!url.hostname || !normalizeHostname(url.hostname)) {
|
|
359
|
+
throw new Error("URL must include a host");
|
|
360
|
+
}
|
|
361
|
+
if (url.username || url.password) {
|
|
362
|
+
throw new Error("URL must not contain credentials");
|
|
363
|
+
}
|
|
364
|
+
return url;
|
|
365
|
+
}
|
|
366
|
+
function normalizeResultUrl(value) {
|
|
367
|
+
if (typeof value !== "string")
|
|
368
|
+
return;
|
|
369
|
+
const input = value.trim();
|
|
370
|
+
if (!input || input.length > MAX_URL_CHARS)
|
|
371
|
+
return;
|
|
372
|
+
try {
|
|
373
|
+
const url = new URL(input);
|
|
374
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
375
|
+
return;
|
|
376
|
+
if (!url.hostname || url.username || url.password)
|
|
377
|
+
return;
|
|
378
|
+
return url.href;
|
|
379
|
+
} catch {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/core/search.ts
|
|
385
|
+
function buildQuery(query, options) {
|
|
386
|
+
const parts = [query.trim()];
|
|
387
|
+
for (const domain of normalizeDomains(options.blockedDomains))
|
|
388
|
+
parts.push(`-site:${domain}`);
|
|
389
|
+
const include = normalizeDomains(options.includeDomains);
|
|
390
|
+
if (include.length)
|
|
391
|
+
parts.push(include.map((domain) => `site:${domain}`).join(" OR "));
|
|
392
|
+
const language = options.language?.trim().replace(/^lang:/i, "");
|
|
393
|
+
if (language)
|
|
394
|
+
parts.push(`lang:${language}`);
|
|
395
|
+
return parts.filter((part) => part.length > 0).join(" ");
|
|
396
|
+
}
|
|
397
|
+
function parsePublished(value) {
|
|
398
|
+
if (typeof value !== "string" || !value.trim())
|
|
399
|
+
return;
|
|
400
|
+
const timestamp = Date.parse(value);
|
|
401
|
+
return Number.isFinite(timestamp) ? timestamp : undefined;
|
|
402
|
+
}
|
|
403
|
+
function processResults(raw, options) {
|
|
404
|
+
const results = [];
|
|
405
|
+
const seen = new Set;
|
|
406
|
+
for (const item of raw) {
|
|
407
|
+
if (results.length >= options.maxResults)
|
|
408
|
+
break;
|
|
409
|
+
if (!item || typeof item !== "object")
|
|
410
|
+
continue;
|
|
411
|
+
const url = normalizeResultUrl(item.url);
|
|
412
|
+
if (!url || seen.has(url))
|
|
413
|
+
continue;
|
|
414
|
+
const rerank = item.rerank_score;
|
|
415
|
+
if (typeof rerank !== "number" || !Number.isFinite(rerank) || rerank < options.minRerank)
|
|
416
|
+
continue;
|
|
417
|
+
const authority = item.authority;
|
|
418
|
+
const knownAuthority = typeof authority === "number" && Number.isFinite(authority) && authority >= 0;
|
|
419
|
+
if (knownAuthority) {
|
|
420
|
+
if (authority < options.minAuthority)
|
|
421
|
+
continue;
|
|
422
|
+
} else if (!options.allowUnknownAuthority) {
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
seen.add(url);
|
|
426
|
+
const content = truncate(optionalText2(item.summary) ?? optionalText2(item.snippet) ?? "", options.maxSummaryChars);
|
|
427
|
+
const time = {};
|
|
428
|
+
const published = parsePublished(item.date);
|
|
429
|
+
if (published !== undefined)
|
|
430
|
+
time.published = published;
|
|
431
|
+
const title = optionalText2(item.title);
|
|
432
|
+
results.push({
|
|
433
|
+
url,
|
|
434
|
+
...title ? { title } : {},
|
|
435
|
+
...content ? { content } : {},
|
|
436
|
+
time
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
return results;
|
|
440
|
+
}
|
|
441
|
+
function optionalText2(value) {
|
|
442
|
+
if (typeof value !== "string")
|
|
443
|
+
return;
|
|
444
|
+
const trimmed = value.trim();
|
|
445
|
+
return trimmed ? trimmed : undefined;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/core/client.ts
|
|
449
|
+
class DeepSeekApiError extends Error {
|
|
450
|
+
operation;
|
|
451
|
+
status;
|
|
452
|
+
body;
|
|
453
|
+
retryable;
|
|
454
|
+
retryAfterMs;
|
|
455
|
+
constructor(input) {
|
|
456
|
+
const body = redactSecrets(input.body);
|
|
457
|
+
super(formatApiError(input.operation, input.status, body));
|
|
458
|
+
this.name = "DeepSeekApiError";
|
|
459
|
+
this.operation = input.operation;
|
|
460
|
+
this.status = input.status;
|
|
461
|
+
this.body = body;
|
|
462
|
+
this.retryable = input.status === 408 || input.status === 425 || input.status === 429 || input.status >= 500 && input.status <= 599;
|
|
463
|
+
this.retryAfterMs = input.retryAfterMs;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
async function searchWeb(query, options, request) {
|
|
467
|
+
const trimmed = query.trim();
|
|
468
|
+
if (!trimmed)
|
|
469
|
+
throw new Error("DeepSeek search query must not be empty");
|
|
470
|
+
if (Array.from(trimmed).length > MAX_QUERY_CHARS) {
|
|
471
|
+
throw new Error(`DeepSeek search query must be ${MAX_QUERY_CHARS} characters or fewer`);
|
|
472
|
+
}
|
|
473
|
+
const builtQuery = buildQuery(trimmed, options);
|
|
474
|
+
const builtQueryLength = Array.from(builtQuery).length;
|
|
475
|
+
if (builtQueryLength > MAX_QUERY_CHARS) {
|
|
476
|
+
throw new Error(`DeepSeek search query must be ${MAX_QUERY_CHARS} characters or fewer after applying ` + `blockedDomains, includeDomains, and language ` + `(built query is ${builtQueryLength} characters)`);
|
|
477
|
+
}
|
|
478
|
+
const payload = await postJson(SEARCH_ENDPOINT, {
|
|
479
|
+
query: builtQuery,
|
|
480
|
+
count: options.count,
|
|
481
|
+
need_content: false
|
|
482
|
+
}, "DeepSeek Search", request, options.timeoutMs);
|
|
483
|
+
if (!Array.isArray(payload.results)) {
|
|
484
|
+
throw new Error("DeepSeek Search returned a response without a results array");
|
|
485
|
+
}
|
|
486
|
+
return processResults(payload.results, options);
|
|
487
|
+
}
|
|
488
|
+
async function fetchPage(url, options, request) {
|
|
489
|
+
assertFetchHostPolicy(url, options);
|
|
490
|
+
const payload = await postJson(FETCH_ENDPOINT, { url: url.href }, "DeepSeek Fetch", request, options.timeoutMs);
|
|
491
|
+
const title = typeof payload.title === "string" ? payload.title.trim() : "";
|
|
492
|
+
const content = typeof payload.content === "string" ? payload.content : "";
|
|
493
|
+
if (!content.trim()) {
|
|
494
|
+
throw new Error("DeepSeek Fetch returned no page content (the page may be empty or unavailable)");
|
|
495
|
+
}
|
|
496
|
+
return { url: url.href, title, content };
|
|
497
|
+
}
|
|
498
|
+
async function postJson(endpoint, body, operation, options, timeoutMs) {
|
|
499
|
+
const attempts = options.retry.maxRetries + 1;
|
|
500
|
+
let lastError;
|
|
501
|
+
for (let attempt = 0;attempt < attempts; attempt++) {
|
|
502
|
+
try {
|
|
503
|
+
return await requestOnce(endpoint, body, operation, options, timeoutMs);
|
|
504
|
+
} catch (error) {
|
|
505
|
+
lastError = error;
|
|
506
|
+
if (!(error instanceof DeepSeekApiError) || !error.retryable || attempt === attempts - 1) {
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
await sleep(retryDelay(error.retryAfterMs, options.retry.baseDelayMs, attempt), options.signal);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
throw lastError;
|
|
513
|
+
}
|
|
514
|
+
async function requestOnce(endpoint, body, operation, options, timeoutMs) {
|
|
515
|
+
return fetchWithTimeout(endpoint, {
|
|
516
|
+
method: "POST",
|
|
517
|
+
headers: {
|
|
518
|
+
Authorization: `Bearer ${options.auth.apiKey}`,
|
|
519
|
+
"Content-Type": "application/json"
|
|
520
|
+
},
|
|
521
|
+
body: JSON.stringify(body)
|
|
522
|
+
}, options.signal, timeoutMs, operation, async (response) => {
|
|
523
|
+
if (!response.ok) {
|
|
524
|
+
const text2 = await readBoundedText(response, MAX_ERROR_BODY_CHARS);
|
|
525
|
+
throw new DeepSeekApiError({
|
|
526
|
+
operation,
|
|
527
|
+
status: response.status,
|
|
528
|
+
body: text2,
|
|
529
|
+
retryAfterMs: parseRetryAfter(response.headers.get("retry-after"))
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
const text = await readResponseText(response, MAX_RESPONSE_BYTES, operation);
|
|
533
|
+
if (!text.trim())
|
|
534
|
+
throw new Error(`${operation} returned an empty response body`);
|
|
535
|
+
try {
|
|
536
|
+
return JSON.parse(text);
|
|
537
|
+
} catch {
|
|
538
|
+
throw new Error(`${operation} returned invalid JSON: ${truncate(redactSecrets(text), 500)}`);
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
async function fetchWithTimeout(endpoint, init, signal, timeoutMs, operation, handleResponse) {
|
|
543
|
+
const controller = new AbortController;
|
|
544
|
+
let timedOut = false;
|
|
545
|
+
const forwardAbort = () => controller.abort(signal.reason);
|
|
546
|
+
if (signal.aborted)
|
|
547
|
+
forwardAbort();
|
|
548
|
+
else
|
|
549
|
+
signal.addEventListener("abort", forwardAbort, { once: true });
|
|
550
|
+
const timer = signal.aborted ? undefined : setTimeout(() => {
|
|
551
|
+
timedOut = true;
|
|
552
|
+
controller.abort(new Error("timeout"));
|
|
553
|
+
}, timeoutMs);
|
|
554
|
+
try {
|
|
555
|
+
const response = await fetch(endpoint, { ...init, signal: controller.signal });
|
|
556
|
+
return await handleResponse(response);
|
|
557
|
+
} catch (error) {
|
|
558
|
+
if (timedOut)
|
|
559
|
+
throw new Error(formatTimeoutError(operation, timeoutMs));
|
|
560
|
+
if (signal.aborted)
|
|
561
|
+
throw abortReason(signal, operation);
|
|
562
|
+
throw error;
|
|
563
|
+
} finally {
|
|
564
|
+
if (timer !== undefined)
|
|
565
|
+
clearTimeout(timer);
|
|
566
|
+
signal.removeEventListener("abort", forwardAbort);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function parseRetryAfter(value) {
|
|
570
|
+
if (!value)
|
|
571
|
+
return;
|
|
572
|
+
const trimmed = value.trim();
|
|
573
|
+
if (!trimmed)
|
|
574
|
+
return;
|
|
575
|
+
if (/^\d+$/.test(trimmed)) {
|
|
576
|
+
return Math.min(MAX_RETRY_DELAY_MS, Number(trimmed) * 1000);
|
|
577
|
+
}
|
|
578
|
+
const timestamp = Date.parse(trimmed);
|
|
579
|
+
if (!Number.isFinite(timestamp))
|
|
580
|
+
return;
|
|
581
|
+
return Math.min(MAX_RETRY_DELAY_MS, Math.max(0, timestamp - Date.now()));
|
|
582
|
+
}
|
|
583
|
+
function retryDelay(retryAfterMs, baseDelayMs, attempt) {
|
|
584
|
+
if (retryAfterMs !== undefined)
|
|
585
|
+
return retryAfterMs;
|
|
586
|
+
return Math.min(MAX_RETRY_DELAY_MS, baseDelayMs * 2 ** attempt);
|
|
587
|
+
}
|
|
588
|
+
function sleep(ms, signal) {
|
|
589
|
+
if (ms <= 0)
|
|
590
|
+
return Promise.resolve();
|
|
591
|
+
return new Promise((resolve, reject) => {
|
|
592
|
+
const timer = setTimeout(() => {
|
|
593
|
+
signal.removeEventListener("abort", onAbort);
|
|
594
|
+
resolve();
|
|
595
|
+
}, ms);
|
|
596
|
+
const onAbort = () => {
|
|
597
|
+
clearTimeout(timer);
|
|
598
|
+
reject(abortReason(signal, "DeepSeek request"));
|
|
599
|
+
};
|
|
600
|
+
if (signal.aborted) {
|
|
601
|
+
clearTimeout(timer);
|
|
602
|
+
reject(abortReason(signal, "DeepSeek request"));
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
function abortReason(signal, operation) {
|
|
609
|
+
return signal.reason instanceof Error ? signal.reason : new Error(`${operation} request aborted`);
|
|
610
|
+
}
|
|
611
|
+
async function readBoundedText(response, maxChars) {
|
|
612
|
+
if (!response.body)
|
|
613
|
+
return truncate(await response.text(), maxChars + 1);
|
|
614
|
+
const reader = response.body.getReader();
|
|
615
|
+
const decoder = new TextDecoder;
|
|
616
|
+
const limit = maxChars + 1;
|
|
617
|
+
let text = "";
|
|
618
|
+
let chars = 0;
|
|
619
|
+
try {
|
|
620
|
+
while (chars < limit) {
|
|
621
|
+
const { done, value } = await reader.read();
|
|
622
|
+
if (done)
|
|
623
|
+
break;
|
|
624
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
625
|
+
text += chunk;
|
|
626
|
+
chars += Array.from(chunk).length;
|
|
627
|
+
}
|
|
628
|
+
text += decoder.decode();
|
|
629
|
+
} finally {
|
|
630
|
+
await reader.cancel().catch(() => {
|
|
631
|
+
return;
|
|
632
|
+
});
|
|
633
|
+
reader.releaseLock();
|
|
634
|
+
}
|
|
635
|
+
return truncate(text, limit);
|
|
636
|
+
}
|
|
637
|
+
async function readResponseText(response, maxBytes, operation) {
|
|
638
|
+
if (!response.body) {
|
|
639
|
+
const text2 = await response.text();
|
|
640
|
+
if (new TextEncoder().encode(text2).byteLength > maxBytes) {
|
|
641
|
+
throw new Error(`${operation} response exceeded ${maxBytes} bytes`);
|
|
642
|
+
}
|
|
643
|
+
return text2;
|
|
644
|
+
}
|
|
645
|
+
const reader = response.body.getReader();
|
|
646
|
+
const decoder = new TextDecoder;
|
|
647
|
+
let text = "";
|
|
648
|
+
let bytes = 0;
|
|
649
|
+
try {
|
|
650
|
+
while (true) {
|
|
651
|
+
const { done, value } = await reader.read();
|
|
652
|
+
if (done)
|
|
653
|
+
break;
|
|
654
|
+
bytes += value.byteLength;
|
|
655
|
+
if (bytes > maxBytes) {
|
|
656
|
+
throw new Error(`${operation} response exceeded ${maxBytes} bytes`);
|
|
657
|
+
}
|
|
658
|
+
text += decoder.decode(value, { stream: true });
|
|
659
|
+
}
|
|
660
|
+
text += decoder.decode();
|
|
661
|
+
} finally {
|
|
662
|
+
await reader.cancel().catch(() => {
|
|
663
|
+
return;
|
|
664
|
+
});
|
|
665
|
+
reader.releaseLock();
|
|
666
|
+
}
|
|
667
|
+
return text;
|
|
668
|
+
}
|
|
669
|
+
// package.json
|
|
670
|
+
var package_default = {
|
|
671
|
+
$schema: "https://json.schemastore.org/package.json",
|
|
672
|
+
name: "deepseek-web-retrieval",
|
|
673
|
+
version: "0.2.0",
|
|
674
|
+
description: "Token-efficient DeepSeek Search and Fetch for coding agents, starting with OpenCode v2.",
|
|
675
|
+
type: "module",
|
|
676
|
+
license: "MIT",
|
|
677
|
+
repository: {
|
|
678
|
+
type: "git",
|
|
679
|
+
url: "git+https://github.com/gwokhou/deepseek-web-retrieval.git"
|
|
680
|
+
},
|
|
681
|
+
homepage: "https://github.com/gwokhou/deepseek-web-retrieval#readme",
|
|
682
|
+
bugs: "https://github.com/gwokhou/deepseek-web-retrieval/issues",
|
|
683
|
+
keywords: [
|
|
684
|
+
"opencode",
|
|
685
|
+
"opencode-plugin",
|
|
686
|
+
"deepseek",
|
|
687
|
+
"websearch",
|
|
688
|
+
"webfetch",
|
|
689
|
+
"coding-agent",
|
|
690
|
+
"mcp"
|
|
691
|
+
],
|
|
692
|
+
exports: {
|
|
693
|
+
".": "./index.ts",
|
|
694
|
+
"./core": "./src/core/index.ts",
|
|
695
|
+
"./mcp": "./src/mcp/server.ts"
|
|
696
|
+
},
|
|
697
|
+
bin: {
|
|
698
|
+
"deepseek-web-retrieval": "./dist/cli.js"
|
|
699
|
+
},
|
|
700
|
+
files: [
|
|
701
|
+
"index.ts",
|
|
702
|
+
"src",
|
|
703
|
+
"dist",
|
|
704
|
+
"docs",
|
|
705
|
+
"README.md",
|
|
706
|
+
"LICENSE",
|
|
707
|
+
"CHANGELOG.md",
|
|
708
|
+
"examples"
|
|
709
|
+
],
|
|
710
|
+
scripts: {
|
|
711
|
+
typecheck: "tsc --noEmit",
|
|
712
|
+
build: "bun build src/mcp/cli.ts --target=node --format=esm --outdir=dist --external @modelcontextprotocol/server --external @modelcontextprotocol/server/stdio --external zod --external zod/v4",
|
|
713
|
+
prepublishOnly: "bun run typecheck && bun run build",
|
|
714
|
+
test: "bun test",
|
|
715
|
+
"test:e2e": "bun run test/e2e/run.ts",
|
|
716
|
+
"docs:matrix": "bun run scripts/render-matrix.ts",
|
|
717
|
+
"test:coverage": "bun test --coverage",
|
|
718
|
+
check: "bun run typecheck && bun run build && bun test"
|
|
719
|
+
},
|
|
720
|
+
peerDependencies: {
|
|
721
|
+
"@opencode/plugin": "^2.0.32"
|
|
722
|
+
},
|
|
723
|
+
devDependencies: {
|
|
724
|
+
"@modelcontextprotocol/client": "^2.3.1",
|
|
725
|
+
"@opencode/plugin": "2.0.32",
|
|
726
|
+
"@types/bun": "1.4.0",
|
|
727
|
+
"@types/node": "^26.6.5",
|
|
728
|
+
typescript: "5.9.3"
|
|
729
|
+
},
|
|
730
|
+
engines: {
|
|
731
|
+
bun: ">=1.3.0",
|
|
732
|
+
node: ">=20"
|
|
733
|
+
},
|
|
734
|
+
dependencies: {
|
|
735
|
+
"@modelcontextprotocol/server": "^2.3.1",
|
|
736
|
+
zod: "^4"
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
// src/mcp/format.ts
|
|
741
|
+
function formatSearchResults(results) {
|
|
742
|
+
if (results.length === 0)
|
|
743
|
+
return "No results found.";
|
|
744
|
+
return results.map((result, index) => {
|
|
745
|
+
const title = result.title?.trim() || result.url;
|
|
746
|
+
const lines = [`${index + 1}. [${escapeBrackets(title)}](${result.url})`];
|
|
747
|
+
if (result.content) {
|
|
748
|
+
lines.push(` ${result.content.replaceAll(`
|
|
749
|
+
`, " ")}`);
|
|
750
|
+
}
|
|
751
|
+
if (result.time.published !== undefined) {
|
|
752
|
+
lines.push(` Published: ${formatPublished(result.time.published)}`);
|
|
753
|
+
}
|
|
754
|
+
return lines.join(`
|
|
755
|
+
`);
|
|
756
|
+
}).join(`
|
|
757
|
+
|
|
758
|
+
`);
|
|
759
|
+
}
|
|
760
|
+
function escapeBrackets(value) {
|
|
761
|
+
return value.replaceAll("[", "\\[").replaceAll("]", "\\]");
|
|
762
|
+
}
|
|
763
|
+
function formatPublished(epochMs) {
|
|
764
|
+
return new Date(epochMs).toISOString().slice(0, 10);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// src/mcp/server.ts
|
|
768
|
+
var MCP_SERVER_NAME = "deepseek-web-retrieval";
|
|
769
|
+
var SEARCH_INPUT = z.object({
|
|
770
|
+
query: z.string().min(1).describe("Search query, 128 characters or fewer including operators (site:, -site:, quoted phrases, lang:).")
|
|
771
|
+
});
|
|
772
|
+
var SEARCH_OUTPUT = z.object({
|
|
773
|
+
results: z.array(z.object({
|
|
774
|
+
url: z.string(),
|
|
775
|
+
title: z.string().optional(),
|
|
776
|
+
content: z.string().optional(),
|
|
777
|
+
published: z.number().optional()
|
|
778
|
+
}))
|
|
779
|
+
});
|
|
780
|
+
var FETCH_INPUT = z.object({
|
|
781
|
+
url: z.string().min(1).describe("Absolute http:// or https:// URL to fetch. Credentials are rejected.")
|
|
782
|
+
});
|
|
783
|
+
var FETCH_OUTPUT = z.object({
|
|
784
|
+
url: z.string(),
|
|
785
|
+
title: z.string(),
|
|
786
|
+
content: z.string(),
|
|
787
|
+
truncated: z.boolean()
|
|
788
|
+
});
|
|
789
|
+
var SEARCH_DESCRIPTION = "Search the web with DeepSeek's in-house search engine and return a short, filtered result " + "list (title, URL, summary). Results are ranked by relevance and source authority, deduplicated " + "by URL, and capped at the configured maximum. An empty result list is normal, not an error. " + "Use site:/-site: operators in the query to include or exclude domains; the full query must stay " + "within 128 characters. To read a full page, pass a result URL to the deepseek_fetch tool.";
|
|
790
|
+
var FETCH_DESCRIPTION = "Fetch one web page as clean Markdown through DeepSeek's Fetch API. DeepSeek does not render " + "JavaScript and cannot reach private networks; PDFs, images, and other binary files are " + "unsupported. Output is truncated to the configured size.";
|
|
791
|
+
function createDeepSeekServer(config, env = process.env) {
|
|
792
|
+
const server = new McpServer({ name: MCP_SERVER_NAME, version: package_default.version });
|
|
793
|
+
server.registerTool("deepseek_web_search", {
|
|
794
|
+
title: "DeepSeek Web Search",
|
|
795
|
+
description: SEARCH_DESCRIPTION,
|
|
796
|
+
inputSchema: SEARCH_INPUT,
|
|
797
|
+
outputSchema: SEARCH_OUTPUT,
|
|
798
|
+
annotations: { readOnlyHint: true }
|
|
799
|
+
}, async ({ query }, ctx) => {
|
|
800
|
+
try {
|
|
801
|
+
const request = requestContext(config, env, ctx.mcpReq.signal);
|
|
802
|
+
const results = await searchWeb(query, config.search, request);
|
|
803
|
+
return {
|
|
804
|
+
content: [{ type: "text", text: formatSearchResults(results) }],
|
|
805
|
+
structuredContent: { results: results.map(toStructuredResult) }
|
|
806
|
+
};
|
|
807
|
+
} catch (error) {
|
|
808
|
+
return toErrorResult(error);
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
server.registerTool("deepseek_fetch", {
|
|
812
|
+
title: "DeepSeek Fetch",
|
|
813
|
+
description: FETCH_DESCRIPTION,
|
|
814
|
+
inputSchema: FETCH_INPUT,
|
|
815
|
+
outputSchema: FETCH_OUTPUT,
|
|
816
|
+
annotations: { readOnlyHint: true }
|
|
817
|
+
}, async ({ url }, ctx) => {
|
|
818
|
+
try {
|
|
819
|
+
const request = requestContext(config, env, ctx.mcpReq.signal);
|
|
820
|
+
const target = parseFetchUrl(url);
|
|
821
|
+
assertFetchHostPolicy(target, config.fetch);
|
|
822
|
+
const page = await fetchPage(target, config.fetch, request);
|
|
823
|
+
const formatted = formatFetchedContent(page.title, page.content, config.fetch.maxChars);
|
|
824
|
+
return {
|
|
825
|
+
content: [{ type: "text", text: formatted.content }],
|
|
826
|
+
structuredContent: {
|
|
827
|
+
url: page.url,
|
|
828
|
+
title: page.title,
|
|
829
|
+
content: formatted.content,
|
|
830
|
+
truncated: formatted.truncated
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
} catch (error) {
|
|
834
|
+
return toErrorResult(error);
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
return server;
|
|
838
|
+
}
|
|
839
|
+
function requestContext(config, env, signal) {
|
|
840
|
+
return { signal, auth: { apiKey: requireApiKey(env) }, retry: config.retry };
|
|
841
|
+
}
|
|
842
|
+
function toStructuredResult(result) {
|
|
843
|
+
return {
|
|
844
|
+
url: result.url,
|
|
845
|
+
...result.title ? { title: result.title } : {},
|
|
846
|
+
...result.content ? { content: result.content } : {},
|
|
847
|
+
...result.time.published !== undefined ? { published: result.time.published } : {}
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
function toErrorResult(error) {
|
|
851
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
852
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// src/mcp/cli.ts
|
|
856
|
+
var env = process.env;
|
|
857
|
+
if (!apiKeyFromEnv(env)) {
|
|
858
|
+
console.error(MISSING_API_KEY_MESSAGE);
|
|
859
|
+
process.exit(1);
|
|
860
|
+
}
|
|
861
|
+
var config = resolveEnvOptions(env);
|
|
862
|
+
var handle = serveStdio(() => createDeepSeekServer(config, env));
|
|
863
|
+
console.error(`deepseek-web-retrieval MCP server on stdio ` + `(search: max ${config.search.maxResults} results, fetch: max ${config.fetch.maxChars} chars)`);
|
|
864
|
+
process.on("SIGINT", () => void handle.close());
|
|
865
|
+
process.on("SIGTERM", () => void handle.close());
|