deepseek-web-retrieval 0.7.0 → 0.8.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/dist/cli.js CHANGED
@@ -3619,7 +3619,7 @@ var init_state = __esm(() => {
3619
3619
  recordSchema = z.object({
3620
3620
  version: z.literal(1),
3621
3621
  installerVersion: z.string().optional(),
3622
- host: z.enum(["opencode", "pi", "codex"]),
3622
+ host: z.enum(["opencode", "pi", "codex", "claude-code"]),
3623
3623
  scope: z.enum(["global", "project"]),
3624
3624
  tiers: z.object({ search: tier, fetch: tier }),
3625
3625
  createdAt: z.string().refine((value) => Number.isFinite(Date.parse(value))),
@@ -4656,7 +4656,7 @@ function isFalseValue(value) {
4656
4656
  const trimmed = value?.trim().toLowerCase();
4657
4657
  return trimmed === "false" || trimmed === "0" || trimmed === "no" || trimmed === "off";
4658
4658
  }
4659
- var NUMBER_ENV, TEXT_ENV, DOMAINS_ENV, FALSE_ONLY_ENV, OPTION_ENV_VARS;
4659
+ var NUMBER_ENV, TEXT_ENV, DOMAINS_ENV, FALSE_ONLY_ENV, OPTION_ENV_BY_KEY, OPTION_ENV_VARS;
4660
4660
  var init_env = __esm(() => {
4661
4661
  init_constants();
4662
4662
  init_domains();
@@ -4689,12 +4689,13 @@ var init_env = __esm(() => {
4689
4689
  searchEnabled: ENABLE_SEARCH_ENV,
4690
4690
  fetchEnabled: ENABLE_FETCH_ENV
4691
4691
  };
4692
- OPTION_ENV_VARS = [
4693
- ...Object.values(NUMBER_ENV),
4694
- ...Object.values(TEXT_ENV),
4695
- ...Object.values(DOMAINS_ENV),
4696
- ...Object.values(FALSE_ONLY_ENV)
4697
- ];
4692
+ OPTION_ENV_BY_KEY = {
4693
+ ...NUMBER_ENV,
4694
+ ...TEXT_ENV,
4695
+ ...DOMAINS_ENV,
4696
+ ...FALSE_ONLY_ENV
4697
+ };
4698
+ OPTION_ENV_VARS = Object.values(OPTION_ENV_BY_KEY);
4698
4699
  });
4699
4700
 
4700
4701
  // src/installer/config-command.ts
@@ -4870,10 +4871,227 @@ var init_shared = __esm(() => {
4870
4871
  init_formats();
4871
4872
  });
4872
4873
 
4873
- // src/installer/hosts/codex.ts
4874
+ // src/installer/hosts/claude-code.ts
4874
4875
  import { spawnSync } from "node:child_process";
4876
+ import { existsSync as existsSync9 } from "node:fs";
4877
+ function valueKind(value) {
4878
+ return Array.isArray(value) ? "an array" : `a ${typeof value}`;
4879
+ }
4880
+ function probeClaudeCodeVersion(err) {
4881
+ const result = spawnSync("claude", ["--version"], { timeout: 5000, encoding: "utf8", env: process.env, shell: true });
4882
+ const version = result.status === 0 ? parseClaudeCodeVersion(result.stdout ?? "") : undefined;
4883
+ if (version === undefined) {
4884
+ err("note: claude was not found on PATH or printed no version; the version-drift check was skipped");
4885
+ }
4886
+ return version;
4887
+ }
4888
+ function parseClaudeCodeVersion(output) {
4889
+ return [...output.matchAll(/\b\d+\.\d+\.\d+\b/g)].map((match) => match[0]).at(-1);
4890
+ }
4891
+ function claudeCodePaths(input) {
4892
+ return input.scope === "global" ? {
4893
+ config: join2(input.home, ".claude.json"),
4894
+ settings: join2(input.home, ".claude", "settings.json"),
4895
+ rules: join2(input.home, ".claude", "CLAUDE.md")
4896
+ } : {
4897
+ config: join2(input.cwd, ".mcp.json"),
4898
+ settings: join2(input.cwd, ".claude", "settings.json"),
4899
+ rules: join2(input.cwd, "CLAUDE.md")
4900
+ };
4901
+ }
4902
+ function managedSettingsPath(input) {
4903
+ const platform = input.platform ?? process.platform;
4904
+ if (platform === "darwin")
4905
+ return "/Library/Application Support/ClaudeCode/managed-settings.json";
4906
+ if (platform === "win32")
4907
+ return "C:\\Program Files\\ClaudeCode\\managed-settings.json";
4908
+ return "/etc/claude-code/managed-settings.json";
4909
+ }
4910
+ function planClaudeCode(input, _previous) {
4911
+ const paths = claudeCodePaths(input);
4912
+ const loaded = codecFor("json", input.host).load(paths.config);
4913
+ const tiers = { search: input.search, fetch: input.fetch };
4914
+ const header = planHeader(input, paths, loaded, "JSON");
4915
+ if (header.abort) {
4916
+ return { host: "claude-code", scope: input.scope, tiers, edits: [], notes: header.notes, errors: header.errors };
4917
+ }
4918
+ const notes = [...header.notes];
4919
+ const errors = [...header.errors];
4920
+ const edits = [];
4921
+ if (input.hostVersion !== undefined && input.hostVersion !== CLAUDE_CODE_TESTED_VERSION) {
4922
+ notes.push(`Claude Code ${input.hostVersion} is installed, but this installer's Claude Code facts are verified against ` + `${CLAUDE_CODE_TESTED_VERSION} (the docs/hosts.jsonc lock). Deny-rule and tool-visibility behavior may have ` + "drifted between the two — re-verify the tiers if routing misbehaves.");
4923
+ }
4924
+ const managed = managedSettingsPath(input);
4925
+ if (existsSync9(managed)) {
4926
+ notes.push(`${managed} exists: the managed policy layer outranks every settings file this plan writes, so a deny or allow ` + "there overrides these changes. Check with your administrator if routing does not take effect.");
4927
+ }
4928
+ const servers = loaded.value.mcpServers;
4929
+ if (servers !== undefined && !isPlainObject(servers)) {
4930
+ errors.push({
4931
+ message: `${paths.config}: mcpServers is ${valueKind(servers)}, not an object; fix it before installing.`
4932
+ });
4933
+ }
4934
+ const existingEntry = isPlainObject(servers) ? servers.deepseek : undefined;
4935
+ const entry = {
4936
+ command: "npx",
4937
+ args: ["-y", input.packageRef],
4938
+ env: { [API_KEY_ENV]: `\${${API_KEY_ENV}}` }
4939
+ };
4940
+ const ops = [{ op: "set", path: ["mcpServers", "deepseek"], value: entry, previous: existingEntry }];
4941
+ edits.push({
4942
+ file: paths.config,
4943
+ format: "json",
4944
+ ...input.scope === "global" ? { shared: true } : {},
4945
+ ops
4946
+ });
4947
+ if (isPlainObject(servers)) {
4948
+ if ("deepseek" in servers)
4949
+ notes.push(replacingExistingEntryNote("mcpServers.deepseek"));
4950
+ }
4951
+ notes.push(`\`${API_KEY_ENV}\` is written as "\${${API_KEY_ENV}}" in the server's env block, so the key expands from the ` + "environment that launches Claude Code and stays out of the file.");
4952
+ if (input.scope === "project") {
4953
+ notes.push("Claude Code asks once per project whether to use servers from .mcp.json; approve the deepseek server when " + "prompted (or via the /mcp menu), or its tools will not load in that project.");
4954
+ } else {
4955
+ notes.push(`User-scope servers live in the mcpServers key of ${paths.config} — Claude Code's own user-level store, where ` + "`claude mcp add --scope user` writes too; every other key in the file is left untouched.");
4956
+ }
4957
+ const forced = input.search === "forced" || input.fetch === "forced";
4958
+ const scanned = {};
4959
+ if (isPlainObject(servers))
4960
+ Object.assign(scanned, servers);
4961
+ const otherFile = input.scope === "project" ? join2(input.home, ".claude.json") : join2(input.cwd, ".mcp.json");
4962
+ if (forced && existsSync9(otherFile)) {
4963
+ let otherLoaded;
4964
+ try {
4965
+ otherLoaded = codecFor("json", input.host).load(otherFile);
4966
+ } catch (error) {
4967
+ errors.push({
4968
+ message: `${otherFile} exists but cannot be read (${error instanceof Error ? error.message : String(error)}); the forced-tier competitor scan requires it. Fix the file or its permissions, or choose a lower tier.`
4969
+ });
4970
+ }
4971
+ if (otherLoaded?.parseError) {
4972
+ errors.push({ message: `${otherFile} exists but does not parse as JSON: ${otherLoaded.parseError}` });
4973
+ } else if (otherLoaded !== undefined && otherLoaded.value.mcpServers !== undefined && !isPlainObject(otherLoaded.value.mcpServers)) {
4974
+ errors.push({
4975
+ message: `${otherFile}: mcpServers is ${valueKind(otherLoaded.value.mcpServers)}, not an object; the forced-tier competitor scan cannot read it. Fix the file or choose a lower tier.`
4976
+ });
4977
+ } else if (otherLoaded !== undefined && isPlainObject(otherLoaded.value.mcpServers)) {
4978
+ const otherServers = otherLoaded.value.mcpServers;
4979
+ Object.assign(scanned, otherServers);
4980
+ if ("deepseek" in otherServers) {
4981
+ notes.push(`A deepseek server is also registered in ${otherFile}; Claude Code resolves the name local first, then ` + "project, then user scope, so consider uninstalling the other scope to keep one registration.");
4982
+ }
4983
+ }
4984
+ }
4985
+ const denyEntries = [];
4986
+ const { competitors, bystanders } = splitCompetitors(scanned);
4987
+ if (forced && competitors.length > 0) {
4988
+ const rules = competitors.map((name) => `mcp__${name}`);
4989
+ denyEntries.push(...rules);
4990
+ notes.push(`Forced tier: web-capable MCP servers are denied by server-level rules (${rules.join(", ")}), so DeepSeek ` + "routing is exclusive while the entries stay." + (input.scope === "global" ? " These denies apply in every project on this machine and match by server name, so a same-named server in another project is denied too." : ""));
4991
+ }
4992
+ const stayRegistered = forced ? bystanders : [...competitors, ...bystanders];
4993
+ if (stayRegistered.length > 0)
4994
+ notes.push(othersStayRegisteredNote(stayRegistered));
4995
+ if (input.search === "forced")
4996
+ denyEntries.push(BUILTIN_TOOL_IDS.search);
4997
+ if (input.fetch === "forced")
4998
+ denyEntries.push(BUILTIN_TOOL_IDS.fetch);
4999
+ if (input.search === "off")
5000
+ denyEntries.push(MCP_TOOL_IDS.search);
5001
+ if (input.fetch === "off")
5002
+ denyEntries.push(MCP_TOOL_IDS.fetch);
5003
+ if (denyEntries.length > 0) {
5004
+ const settingsLoaded = codecFor("json", input.host).load(paths.settings);
5005
+ if (settingsLoaded.parseError) {
5006
+ errors.push({ message: `${paths.settings} exists but does not parse as JSON: ${settingsLoaded.parseError}` });
5007
+ } else {
5008
+ const permissions = settingsLoaded.value.permissions;
5009
+ const deny = isPlainObject(permissions) ? permissions.deny : undefined;
5010
+ if (permissions !== undefined && !isPlainObject(permissions)) {
5011
+ errors.push({
5012
+ message: `${paths.settings}: the permissions key is ${valueKind(permissions)}, not an object; fix it before installing.`
5013
+ });
5014
+ } else if (deny !== undefined && !Array.isArray(deny)) {
5015
+ errors.push({
5016
+ message: `${paths.settings}: permissions.deny is ${valueKind(deny)}, not an array; fix it before installing.`
5017
+ });
5018
+ } else {
5019
+ const existingCount = Array.isArray(deny) ? deny.length : 0;
5020
+ const existingDeny = Array.isArray(deny) ? deny.filter((item) => typeof item === "string") : [];
5021
+ const denyOps = [
5022
+ {
5023
+ op: "append-array",
5024
+ path: ["permissions", "deny"],
5025
+ entries: denyEntries,
5026
+ existingCount
5027
+ }
5028
+ ];
5029
+ edits.push({
5030
+ file: paths.settings,
5031
+ format: "json",
5032
+ ...input.scope === "global" ? { shared: true } : {},
5033
+ ops: denyOps
5034
+ });
5035
+ if (existingCount > 0) {
5036
+ notes.push(`permissions.deny already holds ${existingCount} ${existingCount === 1 ? "entry" : "entries"}; ` + "the plan appends only its own and never removes yours.");
5037
+ }
5038
+ const preexisting = denyEntries.filter((name) => existingDeny.includes(name));
5039
+ if (preexisting.length > 0) {
5040
+ notes.push(`${preexisting.join(", ")} ${preexisting.length === 1 ? "is" : "are"} already present in permissions.deny — present entries are not duplicated.`);
5041
+ }
5042
+ }
5043
+ }
5044
+ }
5045
+ if (errors.length > 0) {
5046
+ return { host: "claude-code", scope: input.scope, tiers, edits: [], notes, errors };
5047
+ }
5048
+ const forcedIds = [
5049
+ ...input.search === "forced" ? [BUILTIN_TOOL_IDS.search] : [],
5050
+ ...input.fetch === "forced" ? [BUILTIN_TOOL_IDS.fetch] : []
5051
+ ];
5052
+ if (forcedIds.length > 0) {
5053
+ notes.push(`Forced is exclusion: bare deny rules (${forcedIds.join(", ")}) in permissions.deny remove the built-in tool ` + "from the model's context entirely — deny outranks allow at every settings level and cannot be carved out, so " + `the exclusion also applies to every other workflow that relied on the tool ${input.scope === "global" ? "in every project on this machine" : "in this project"}. ` + FORCED_COST_NOTE);
5054
+ }
5055
+ const offIds = [
5056
+ ...input.search === "off" ? [MCP_TOOL_IDS.search] : [],
5057
+ ...input.fetch === "off" ? [MCP_TOOL_IDS.fetch] : []
5058
+ ];
5059
+ if (offIds.length > 0) {
5060
+ notes.push(`Off capabilities are written as their full tool ids (${offIds.join(", ")}) in permissions.deny: the tool ` + "leaves the model's context while the server keeps running; delete the entry to re-enable it.");
5061
+ }
5062
+ if (input.search === "optional" || input.fetch === "optional") {
5063
+ notes.push("Claude Code ships built-in WebSearch and WebFetch tools; under the optional tier they stay available " + "alongside the DeepSeek tools.");
5064
+ }
5065
+ if (input.search === "preferred" || input.fetch === "preferred") {
5066
+ if (input.scope === "project" && !existsSync9(paths.rules) && existsSync9(join2(input.cwd, "AGENTS.md"))) {
5067
+ notes.push(`This plan creates ${paths.rules} where none exists, and ${join2(input.cwd, "AGENTS.md")} is present: Claude ` + "Code reads AGENTS.md only when no CLAUDE.md exists above the cwd, so the new CLAUDE.md takes precedence — " + "merge anything you rely on from AGENTS.md before applying.");
5068
+ }
5069
+ edits.push({
5070
+ file: paths.rules,
5071
+ format: "rules",
5072
+ block: buildRulesBlock(input.host, rulesLines(MCP_TOOL_IDS, input.search, input.fetch))
5073
+ });
5074
+ notes.push(input.scope === "global" ? `Rules block upserted in ${paths.rules}; Claude Code loads the user CLAUDE.md plus every CLAUDE.md from the cwd up the directory tree, concatenating the levels.` : `Rules block upserted in ${paths.rules}; Claude Code loads every CLAUDE.md from the cwd up the directory tree, concatenating the levels.`);
5075
+ }
5076
+ return { host: "claude-code", scope: input.scope, tiers, edits, notes, errors };
5077
+ }
5078
+ var CLAUDE_CODE_TESTED_VERSION = "2.1.303", BUILTIN_TOOL_IDS;
5079
+ var init_claude_code = __esm(() => {
5080
+ init_constants();
5081
+ init_detect();
5082
+ init_formats();
5083
+ init_rules();
5084
+ init_value();
5085
+ init_paths();
5086
+ init_rulesContent();
5087
+ init_shared();
5088
+ BUILTIN_TOOL_IDS = { search: "WebSearch", fetch: "WebFetch" };
5089
+ });
5090
+
5091
+ // src/installer/hosts/codex.ts
5092
+ import { spawnSync as spawnSync2 } from "node:child_process";
4875
5093
  function probeCodexVersion(err) {
4876
- const result = spawnSync("codex", ["--version"], { timeout: 5000, encoding: "utf8", env: process.env });
5094
+ const result = spawnSync2("codex", ["--version"], { timeout: 5000, encoding: "utf8", env: process.env, shell: true });
4877
5095
  const version = result.status === 0 ? parseCodexVersion(result.stdout ?? "") : undefined;
4878
5096
  if (version === undefined) {
4879
5097
  err("note: codex was not found on PATH or printed no version; the version-drift check was skipped");
@@ -5163,7 +5381,12 @@ function planPiExtension(input, previous) {
5163
5381
  previous: typeof namespace.enableFetch === "boolean" ? namespace.enableFetch : undefined
5164
5382
  });
5165
5383
  }
5166
- edits.push({ file: paths.config, format: "json", ops });
5384
+ edits.push({
5385
+ file: paths.config,
5386
+ format: "json",
5387
+ ...input.scope === "global" ? { shared: true } : {},
5388
+ ops
5389
+ });
5167
5390
  notes.push("Pi resolves the packages source at session start; the first launch after apply fetches the package from npm " + "(pi update --extensions refreshes it later).");
5168
5391
  notes.push(`The extension reads ${API_KEY_ENV} from the Pi process environment; no env forwarding is needed. ` + "Set it in the shell that launches Pi.");
5169
5392
  if (input.search === "off" || input.fetch === "off") {
@@ -5227,8 +5450,14 @@ function planMcpCompetitorDisables(input, notes, errors) {
5227
5450
  const edits = [];
5228
5451
  if (competitors.length > 0) {
5229
5452
  const ops = planCompetitorDisables(servers, competitors, notes, errors);
5230
- if (ops.length > 0)
5231
- edits.push({ file: mcpFile, format: "json", ops });
5453
+ if (ops.length > 0) {
5454
+ edits.push({
5455
+ file: mcpFile,
5456
+ format: "json",
5457
+ ...input.scope === "global" ? { shared: true } : {},
5458
+ ops
5459
+ });
5460
+ }
5232
5461
  }
5233
5462
  if (bystanders.length > 0)
5234
5463
  notes.push(othersStayRegisteredNote(bystanders));
@@ -5260,7 +5489,12 @@ function planPiMcp(input) {
5260
5489
  const servers = loaded.value.mcpServers;
5261
5490
  const existingEntry = isPlainObject(servers) ? servers.deepseek : undefined;
5262
5491
  const ops = [{ op: "set", path: ["mcpServers", "deepseek"], value: entry, previous: existingEntry }];
5263
- edits.push({ file: paths.config, format: "json", ops });
5492
+ edits.push({
5493
+ file: paths.config,
5494
+ format: "json",
5495
+ ...input.scope === "global" ? { shared: true } : {},
5496
+ ops
5497
+ });
5264
5498
  if (isPlainObject(servers)) {
5265
5499
  if (servers.deepseek !== undefined)
5266
5500
  notes.push(replacingExistingEntryNote("mcpServers.deepseek"));
@@ -5447,16 +5681,19 @@ var init_hosts = __esm(() => {
5447
5681
  init_value();
5448
5682
  init_ownership();
5449
5683
  init_state();
5684
+ init_claude_code();
5450
5685
  init_codex();
5451
5686
  init_opencode();
5452
5687
  init_pi();
5453
5688
  HOST_PLANNERS = {
5454
5689
  opencode: planOpenCode,
5455
5690
  pi: planPi,
5456
- codex: planCodex
5691
+ codex: planCodex,
5692
+ "claude-code": planClaudeCode
5457
5693
  };
5458
5694
  HOST_VERSION_PROBES = {
5459
- codex: probeCodexVersion
5695
+ codex: probeCodexVersion,
5696
+ "claude-code": probeClaudeCodeVersion
5460
5697
  };
5461
5698
  });
5462
5699
 
@@ -5547,8 +5784,8 @@ function uninstallLocked(input, record) {
5547
5784
  for (const file of [...record.files].reverse()) {
5548
5785
  lines.push(...revertRecordedFile(file, record.host, { label: "", whole: true }));
5549
5786
  }
5550
- deleteRecord(input);
5551
5787
  releaseFileOwnership(input, record.files);
5788
+ deleteRecord(input);
5552
5789
  pruneEmptyDir(stateDir(input, input.scope));
5553
5790
  lines.push(`removed install record (${input.host}, ${input.scope})`);
5554
5791
  return lines;
@@ -5871,7 +6108,7 @@ var init_package = __esm(() => {
5871
6108
  package_default = {
5872
6109
  $schema: "https://json.schemastore.org/package.json",
5873
6110
  name: "deepseek-web-retrieval",
5874
- version: "0.7.0",
6111
+ version: "0.8.0",
5875
6112
  description: "Token-efficient DeepSeek Search and Fetch for coding agents, starting with OpenCode v2.",
5876
6113
  type: "module",
5877
6114
  license: "MIT",
@@ -0,0 +1,273 @@
1
+ # Claude Code
2
+
3
+ Claude Code adapter for [DeepSeek Web Retrieval](../../README.md).
4
+
5
+ Integration mechanism: MCP server over stdio, registered in Claude Code's own MCP config
6
+ files — `.mcp.json` at the project root (project scope) or the top-level `mcpServers` key
7
+ of `~/.claude.json` (user scope, where `claude mcp add --scope user` writes). Claude Code
8
+ negotiates MCP revision `2026-07-28`; the server accepts clients from `2024-11-05` through
9
+ `2026-07-28`, so the same build works for other MCP hosts (see
10
+ [the compatibility research](../research/mcp-compatibility.md)).
11
+
12
+ When registered, it adds:
13
+
14
+ - A `deepseek_web_search` tool backed by DeepSeek Search
15
+ - A `deepseek_fetch` tool backed by DeepSeek Fetch
16
+
17
+ The tools appear in Claude Code as `mcp__deepseek__deepseek_web_search` and
18
+ `mcp__deepseek__deepseek_fetch`. Claude Code's built-in `WebSearch` and `WebFetch` tools
19
+ are not replaced; see [Control tiers](#control-tiers) for routing them away.
20
+
21
+ For shared search/fetch limits, retries, and privacy behavior, see
22
+ [General behavior](../../README.md#general-behavior).
23
+ For configuration layers, the full knob mapping, and defaults, see the
24
+ [configuration guide](../configuration.md).
25
+
26
+ ## Requirements
27
+
28
+ - Claude Code at the tested version in [`docs/hosts.jsonc`](../hosts.jsonc); init reports
29
+ version drift, and newer versions may require rechecking routing behavior
30
+ - Node.js 20 or newer (`npx` runs the server)
31
+ - A DeepSeek account with access to `/v1/tools/search` and `/v1/tools/fetch`
32
+ - A funded DeepSeek account; Search and Fetch are billed per successful request
33
+ - `DEEPSEEK_API_KEY` set in the shell that launches Claude Code
34
+
35
+ ## Install
36
+
37
+ The quickest option is the installer CLI:
38
+
39
+ ```bash
40
+ npx deepseek-web-retrieval init --host claude-code --scope project \
41
+ --search <tier> --fetch <tier>
42
+ ```
43
+
44
+ It prints the full plan — every file, old value → new value, and notes about existing MCP
45
+ servers — and writes nothing until you pass `--apply`. Add `--pin` to register the exact
46
+ version of the CLI you are running instead of the always-latest package name. `uninstall`
47
+ removes recorded additions and restores snapshotted values that have not since been edited.
48
+ `--scope project` registers the server in `./.mcp.json` (shareable via VCS) and writes tier
49
+ artifacts to `./.claude/settings.json` and `./CLAUDE.md`; `--scope global` uses
50
+ `~/.claude.json`, `~/.claude/settings.json`, and `~/.claude/CLAUDE.md` instead — the two
51
+ scopes use disjoint files, so both can coexist. An `off` capability is written as the
52
+ tool's full id in the `permissions.deny` array — Claude Code's own mechanism, which removes
53
+ the tool from the model's context while the server keeps running — merging with any entries
54
+ you put there yourself. When `claude` is on PATH, `init` also probes `claude --version` and
55
+ adds a plan note when the installed release differs from the tested lock (see
56
+ [hosts.jsonc](../hosts.jsonc)) — version-anchored behavior such as the deny rules may have
57
+ drifted between the two.
58
+
59
+ For an existing installer-managed setup, follow the
60
+ [upgrade instructions](../integration.md#upgrading-existing-installs) from the original
61
+ project directory and scope.
62
+
63
+ ### Manual install
64
+
65
+ Add the server to `.mcp.json` at the project root:
66
+
67
+ ```json
68
+ {
69
+ "mcpServers": {
70
+ "deepseek": {
71
+ "command": "npx",
72
+ "args": ["-y", "deepseek-web-retrieval"],
73
+ "env": { "DEEPSEEK_API_KEY": "${DEEPSEEK_API_KEY}" }
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ > [!NOTE]
80
+ > `"${DEEPSEEK_API_KEY}"` is Claude Code's own `${VAR}` expansion: the key is read from
81
+ > the environment that launches Claude Code and never sits in the file. A literal value
82
+ > works too if you prefer it in the file. For a user-scope install, the same entry goes
83
+ > under the top-level `mcpServers` key of `~/.claude.json`, or use
84
+ > `claude mcp add --scope user --env 'DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY}' deepseek -- npx -y deepseek-web-retrieval`.
85
+
86
+ Claude Code asks once per project whether to use servers from `.mcp.json`; approve the
87
+ `deepseek` server when prompted (or via the `/mcp` menu), or its tools will not load in
88
+ that project.
89
+
90
+ To disable one capability, add the tool's full id to `permissions.deny` in
91
+ `.claude/settings.json` — Claude Code removes the tool from the model's context while the
92
+ server keeps running:
93
+
94
+ ```json
95
+ {
96
+ "permissions": {
97
+ "deny": ["mcp__deepseek__deepseek_web_search"]
98
+ }
99
+ }
100
+ ```
101
+
102
+ The ids match the tool names verbatim (`mcp__deepseek__deepseek_web_search`,
103
+ `mcp__deepseek__deepseek_fetch`). The server also honors
104
+ `DEEPSEEK_ENABLE_SEARCH`/`DEEPSEEK_ENABLE_FETCH` in the server's `env` block if you prefer
105
+ switching the tools off at the source; the installer uses `permissions.deny`.
106
+
107
+ ### Local checkout
108
+
109
+ ```bash
110
+ git clone https://github.com/gwokhou/deepseek-web-retrieval.git
111
+ cd deepseek-web-retrieval && bun install && bun run build
112
+ ```
113
+
114
+ ```json
115
+ {
116
+ "mcpServers": {
117
+ "deepseek": {
118
+ "command": "node",
119
+ "args": ["/absolute/path/to/deepseek-web-retrieval/dist/cli.js"],
120
+ "env": { "DEEPSEEK_API_KEY": "${DEEPSEEK_API_KEY}" }
121
+ }
122
+ }
123
+ }
124
+ ```
125
+
126
+ ## Control tiers
127
+
128
+ Claude Code ships built-in `WebSearch` and `WebFetch` tools; neither is replaceable, so
129
+ both capabilities use exclusion for the forced tier:
130
+
131
+ | Capability | Optional | Preferred | Forced |
132
+ | --- | --- | --- | --- |
133
+ | Search | Tools registered; the built-in `WebSearch` stays available | Plus a CLAUDE.md rules block steering the model to `mcp__deepseek__deepseek_web_search` | Exclusion: `permissions.deny: ["WebSearch"]` |
134
+ | Fetch | Tools registered; the built-in `WebFetch` stays available | Plus the rules block | Exclusion: `permissions.deny: ["WebFetch"]` |
135
+
136
+ The installer maps each tier to these artifacts. Deny entries land in
137
+ `~/.claude/settings.json` (user) or `./.claude/settings.json` (project) and merge with
138
+ your existing `permissions.deny` entries. The CLAUDE.md rules block lands in
139
+ `~/.claude/CLAUDE.md` (user) or `./CLAUDE.md` (project); Claude Code loads the user file
140
+ plus every CLAUDE.md from the cwd up the directory tree, concatenating the levels — so
141
+ the block steers wherever it is installed. Under a forced tier, other web-capable MCP
142
+ servers detected in the config are denied by server-level rules (`mcp__<server>` denies
143
+ every tool of that server), so routing stays exclusive.
144
+
145
+ > [!WARNING]
146
+ > Deny rules outrank allow rules at every settings level and **cannot be carved out** —
147
+ > denying `WebSearch` or `WebFetch` removes the tool for every workflow in the install's
148
+ > scope (every project on the machine for `--scope global`). A managed policy layer
149
+ > (`managed-settings.json`) outranks both files; the installer reports when one exists.
150
+
151
+ ## Authentication
152
+
153
+ `DEEPSEEK_API_KEY` is the only source; MCP has no host credential store. It is forwarded
154
+ through the server's `env` block as `${DEEPSEEK_API_KEY}` and read per request, sent only
155
+ to `api.deepseek.com` as a Bearer token. Without it the server exits at startup with a
156
+ message on stderr.
157
+
158
+ ## Usage
159
+
160
+ Ask naturally:
161
+
162
+ ```text
163
+ Search the web for the latest Claude Code MCP config changes and cite the sources.
164
+ Fetch https://code.claude.com/docs/en/mcp and summarize the config keys.
165
+ ```
166
+
167
+ The model calls `mcp__deepseek__deepseek_web_search` or `mcp__deepseek__deepseek_fetch`.
168
+
169
+ The search tool accepts an optional `original_question` argument — the user's question in
170
+ their own words — forwarded as DeepSeek's `ori_question` to sharpen search intent; omit it
171
+ when the query already stands alone.
172
+
173
+ Tool output is capped at 25k tokens by default (`MAX_MCP_OUTPUT_TOKENS`), and successful
174
+ text results longer than 50,000 characters are saved to a file and referenced — not
175
+ dropped silently.
176
+
177
+ ## Configuration
178
+
179
+ Set these in the server's `env` block in `.mcp.json` (or `~/.claude.json`). Values map onto
180
+ the shared option pipeline, so defaults and clamping match the other adapters.
181
+ `DEEPSEEK_API_KEY` goes in the same block as the `${VAR}` expansion.
182
+
183
+ > [!NOTE]
184
+ > A shared config file — `~/.config/deepseek-web-retrieval/config.json`
185
+ > (Windows: `%APPDATA%\deepseek-web-retrieval\config.json`), camelCase keys —
186
+ > sits under the environment layer and works with no env forwarding at all: the server
187
+ > reads the file itself, making it the way to set knobs without editing the MCP entry. The
188
+ > file carries neither the API key nor the host-integration switches. Run
189
+ > `npx deepseek-web-retrieval config` to see every knob's effective value and source
190
+ > (it replays file warnings visibly and exits 1 on file problems); `config --search "<query>"`
191
+ > verifies key, config, and network with one billed search.
192
+
193
+ ### Search
194
+
195
+ The "Config file" column names each key's camelCase twin in the shared config file —
196
+ one vocabulary pair, one pipeline. `—` marks host-integration entries the file rejects.
197
+
198
+ | Env variable | Config file | Default | Description |
199
+ | --- | --- | --- | --- |
200
+ | `DEEPSEEK_ENABLE_SEARCH` | — | `true` | Register the search tool. Only an explicit `false`/`0`/`no`/`off` disables it; at least one tool must stay enabled. |
201
+ | `DEEPSEEK_SEARCH_COUNT` | `count` | `10` | Results requested from DeepSeek, 1–50. Billing is per request, not per result. |
202
+ | `DEEPSEEK_SEARCH_MAX_RESULTS` | `maxResults` | `10` | Results returned to the model after filtering, 0–50; defaults to `DEEPSEEK_SEARCH_COUNT` when unset. `0` returns no results. |
203
+ | `DEEPSEEK_SEARCH_MIN_RERANK` | `minRerank` | `0` | Drop results below this relevance score (0–3); results without a finite score pass at zero and are dropped at a positive threshold. |
204
+ | `DEEPSEEK_SEARCH_MIN_AUTHORITY` | `minAuthority` | `0` | Drop known authorities below this tier. Authority is 0–4. |
205
+ | `DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY` | `allowUnknownAuthority` | `true` | Keep results whose authority is missing or unknown. Set `false`/`0`/`no`/`off` to drop them. |
206
+ | `DEEPSEEK_SEARCH_BLOCKED_DOMAINS` | `blockedDomains` | unset | Comma-separated; adds `-site:` operators to the query. |
207
+ | `DEEPSEEK_SEARCH_INCLUDE_DOMAINS` | `includeDomains` | unset | Comma-separated; restricts results with `site:` operators. |
208
+ | `DEEPSEEK_SEARCH_LANGUAGE` | `language` | unset | Adds a `lang:` operator, for example `zh,en`. |
209
+ | `DEEPSEEK_SEARCH_LOCALIZATION` | `searchLocalization` | unset | Valid IPv4 or IPv6 address sent as DeepSeek's `localization` to prefer geo-localized results; invalid values are ignored. Deployment-level only, never a per-call parameter. |
210
+ | `DEEPSEEK_SEARCH_MAX_SUMMARY_CHARS` | `maxSummaryChars` | `1200` | Per-result summary cap, 0–1,000,000. |
211
+ | `DEEPSEEK_SEARCH_TIMEOUT_MS` | `searchTimeoutMs` | `30000` | Search timeout, 1,000–120,000 ms. |
212
+
213
+ ### Fetch
214
+
215
+ | Env variable | Config file | Default | Description |
216
+ | --- | --- | --- | --- |
217
+ | `DEEPSEEK_ENABLE_FETCH` | — | `true` | Register the fetch tool. Same false-only semantics as `DEEPSEEK_ENABLE_SEARCH`. |
218
+ | `DEEPSEEK_FETCH_MAX_CHARS` | `fetchMaxChars` | `16000` | Maximum characters returned, including the title, 0–1,000,000. |
219
+ | `DEEPSEEK_FETCH_TIMEOUT_MS` | `fetchTimeoutMs` | `30000` | Fetch timeout, 1,000–120,000 ms. |
220
+ | `DEEPSEEK_FETCH_ALLOWED_DOMAINS` | `fetchAllowedDomains` | unset | If set, only these hosts and subdomains may be fetched. |
221
+ | `DEEPSEEK_FETCH_BLOCKED_DOMAINS` | `fetchBlockedDomains` | unset | Always block these hosts and subdomains. Blocked wins over allowed. |
222
+ | `DEEPSEEK_FETCH_BLOCK_PRIVATE_HOSTS` | `fetchBlockPrivateHosts` | `true` | Reject localhost, private IPs, `.local`, `.internal`, and similar hosts. |
223
+
224
+ ### Credentials and retries
225
+
226
+ | Env variable | Config file | Default | Description |
227
+ | --- | --- | --- | --- |
228
+ | `DEEPSEEK_API_KEY` | — | required | DeepSeek API key, expanded from the launching environment via `${DEEPSEEK_API_KEY}`. The server refuses to start without it. |
229
+ | `DEEPSEEK_MAX_RETRIES` | `maxRetries` | `2` | Retries for 408, 425, 429, and 5xx responses, 0–5. |
230
+ | `DEEPSEEK_RETRY_BASE_DELAY_MS` | `retryBaseDelayMs` | `250` | Base delay for exponential backoff. `Retry-After` is honored up to 10 seconds. |
231
+
232
+ ## Troubleshooting
233
+
234
+ | Error or symptom | What to do |
235
+ | --- | --- |
236
+ | Server fails to start or `HTTP 401` from the tools | `DEEPSEEK_API_KEY` must be set in the environment that launches Claude Code; the `${DEEPSEEK_API_KEY}` expansion in the server's `env` block reads it at launch. |
237
+ | Project tools missing after install | Claude Code asks once per project whether to use `.mcp.json` servers; approve the `deepseek` server when prompted or via the `/mcp` menu. |
238
+ | Both tools missing after startup | At least one of `DEEPSEEK_ENABLE_SEARCH`/`DEEPSEEK_ENABLE_FETCH` must stay enabled; the server exits when both are false. |
239
+ | Forced tier still shows the built-in tool | The kill switch is the bare deny rule in `permissions.deny` at the install's scope; a managed policy layer (`managed-settings.json`) outranks it — the plan reports when one exists. |
240
+ | Routing not exclusive | Other MCP servers stay registered and are noted in the plan. Under a forced tier, web-capable servers detected in the config (and the other scope's file) are denied by server-level `mcp__<server>` rules; a server added after install is not covered — re-run init. |
241
+ | Fetch results appear as a file reference instead of content | Tool output is capped at 25k tokens by default; raise `MAX_MCP_OUTPUT_TOKENS` or lower `DEEPSEEK_FETCH_MAX_CHARS`. |
242
+ | `HTTP 402`, `HTTP 429` | Top up the DeepSeek account; wait, reduce concurrency, or set `DEEPSEEK_MAX_RETRIES`. |
243
+ | Fetch fails on localhost or an internal URL | Use a local fetch tool; DeepSeek cannot reach private networks. |
244
+
245
+ ## Compatibility
246
+
247
+ | Component | Version |
248
+ | --- | --- |
249
+ | Claude Code | locked in [`docs/hosts.jsonc`](../hosts.jsonc) |
250
+ | MCP revision | `2026-07-28` negotiated; server accepts `2024-11-05`–`2026-07-28` |
251
+ | MCP SDK | `@modelcontextprotocol/server` 2.3.1 |
252
+ | Runtime | Node.js 20+ (stdio) |
253
+ | DeepSeek API | `/v1/tools/search`, `/v1/tools/fetch` |
254
+
255
+ Tested with Claude Code at the `docs/hosts.jsonc` lock on Linux over stdio: the keyless
256
+ Docker e2e verifies the installer's registration, tiers, and uninstall with a real
257
+ `claude mcp list` handshake, and the billed scenario (`bun run test:e2e claude-code`,
258
+ requires `DEEPSEEK_API_KEY`) runs headless prompts end to end with the deny kill switch
259
+ asserted for both capabilities.
260
+
261
+ ## Uninstall
262
+
263
+ ```bash
264
+ npx deepseek-web-retrieval uninstall --host claude-code --scope project
265
+ ```
266
+
267
+ This removes the recorded `mcpServers.deepseek` entry and the installer-appended
268
+ `permissions.deny` entries only when their current values still match what the installer
269
+ wrote, removes the managed rules block, and deletes installer-created config files only
270
+ when left empty. Later user edits to configuration values are kept and reported. For a
271
+ manual installation, remove the `deepseek` entry from `.mcp.json` (or `~/.claude.json`),
272
+ any `deepseek-web-retrieval` rules block in your `CLAUDE.md`, and the deny entries the
273
+ setup added to `settings.json` if they were only for this integration.
@@ -24,6 +24,8 @@ both the text and `structuredContent`.
24
24
 
25
25
  For shared search/fetch limits, retries, and privacy behavior, see
26
26
  [General behavior](../../README.md#general-behavior).
27
+ For configuration layers, the full knob mapping, and defaults, see the
28
+ [configuration guide](../configuration.md).
27
29
 
28
30
  ## Requirements
29
31
 
@@ -15,6 +15,8 @@ if you want all Markdown fetches to go through DeepSeek.
15
15
 
16
16
  For shared search/fetch limits, retries, and privacy behavior, see
17
17
  [General behavior](../../README.md#general-behavior).
18
+ For configuration layers, the full knob mapping, and defaults, see the
19
+ [configuration guide](../configuration.md).
18
20
 
19
21
  ## Requirements
20
22
 
package/docs/agents/pi.md CHANGED
@@ -22,6 +22,8 @@ When loaded, it adds:
22
22
  Pi 1.3.0 ships no built-in web tool, so the registered tools are the only web path. For
23
23
  shared search/fetch limits, retries, and privacy behavior, see
24
24
  [General behavior](../../README.md#general-behavior).
25
+ For configuration layers, the full knob mapping, and defaults, see the
26
+ [configuration guide](../configuration.md).
25
27
 
26
28
  ## Requirements
27
29