search-rotation 0.4.11 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,7 +43,7 @@ client to run `search-rotation` with no arguments. See the
43
43
  ### Pinned release from GitHub
44
44
 
45
45
  ```sh
46
- npx -y --allow-git=all github:robin-bially/search-rotation-mcp#v0.4.11
46
+ npx -y --allow-git=all github:robin-bially/search-rotation-mcp#v0.5.1
47
47
  ```
48
48
 
49
49
  Use this when you want a fixed version instead of the current npm release.
@@ -61,20 +61,22 @@ Add your provider keys in the dashboard, then connect your assistant using the *
61
61
 
62
62
  ## MCP tools
63
63
 
64
- `web_search` · `fetch_url` · `engine_status` · `open_dashboard`
64
+ `search_web` · `fetch_url` · `get_engine_status` · `update_engine_config` · `open_dashboard`
65
+
66
+ Every name pairs an action with its target. `update_engine_config` covers what the dashboard's engine list does: enable and disable engines, set the search and fetch rotation order, and set monthly or daily limits per engine. API keys stay in the dashboard. If your client keeps an allowlist of tool names, replace the earlier `web_search` and `engine_status` entries there.
65
67
 
66
68
  The dashboard's **MCP Tools** tab explains each tool, its parameters, and copyable example calls.
67
69
 
68
70
  <picture>
69
71
  <source media="(prefers-color-scheme: dark)" srcset="docs/assets/mcp-tools-dark.png">
70
- <img src="docs/assets/mcp-tools-light.png" alt="MCP Tools overview with parameters and example calls for all four tools">
72
+ <img src="docs/assets/mcp-tools-light.png" alt="MCP Tools overview with parameters and example calls for all five tools">
71
73
  </picture>
72
74
 
73
75
  *Actual dashboard UI, shown in English.*
74
76
 
75
77
  ## Search time filters
76
78
 
77
- `web_search` accepts the following arguments:
79
+ `search_web` accepts the following arguments:
78
80
 
79
81
  | Parameter | Meaning |
80
82
  | --- | --- |
@@ -88,11 +90,11 @@ The dashboard's **MCP Tools** tab explains each tool, its parameters, and copyab
88
90
  Use either `timeRange` or explicit dates. One-sided bounds and equal start/end dates are allowed; invalid calendar dates, reversed bounds, and mixing relative and explicit filters are rejected before any provider request.
89
91
 
90
92
  ```json
91
- {"name":"web_search","arguments":{"query":"AI inference research","timeRange":"week","numResults":5}}
93
+ {"name":"search_web","arguments":{"query":"AI inference research","timeRange":"week","numResults":5}}
92
94
  ```
93
95
 
94
96
  ```json
95
- {"name":"web_search","arguments":{"query":"AI inference research","startDate":"2026-08-01","endDate":"2026-08-31"}}
97
+ {"name":"search_web","arguments":{"query":"AI inference research","startDate":"2026-08-01","endDate":"2026-08-31"}}
96
98
  ```
97
99
 
98
100
  Relative windows are resolved **once per request** into UTC dates, including across failover. These are date filters, not exact rolling 24-hour windows; `month` and `year` mean 30 and 365 days, not calendar arithmetic. Exa receives the start of the first UTC day and the end of the last UTC day.
package/dist/index.js CHANGED
@@ -74,6 +74,12 @@ async function main() {
74
74
  const router = new SearchRouter({ getConfig, usage, adapters: ADAPTERS, history });
75
75
  const month = () => usage.monthKey();
76
76
  const status = () => buildStatus(getConfig(), usage, ADAPTERS);
77
+ const saveConfig = (next) => {
78
+ store.save(next);
79
+ cfg = next;
80
+ // Neuer Key? Dann Remote-Quota sofort frisch ziehen statt 5 Min Cache.
81
+ clearRemoteQuotaCache();
82
+ };
77
83
  const testEngine = async (id, kind, arg, signal) => {
78
84
  const adapter = ADAPTERS.find(a => a.meta.id === id);
79
85
  if (!adapter)
@@ -124,6 +130,7 @@ async function main() {
124
130
  const security = mountSecurity(app, { token, origin, additionalOrigins: [localOrigin], dashboardEnabled });
125
131
  const mcpDeps = {
126
132
  router, status, month, dashboardUrl,
133
+ getConfig, saveConfig,
127
134
  openDashboard: () => {
128
135
  if (dashboardEnabled)
129
136
  openBrowser(security.browserUrl());
@@ -132,12 +139,7 @@ async function main() {
132
139
  app.route("/", buildWebApp({
133
140
  configPath: store.file,
134
141
  getConfig,
135
- saveConfig: (next) => {
136
- store.save(next);
137
- cfg = next;
138
- // Neuer Key? Dann Remote-Quota sofort frisch ziehen statt 5 Min Cache.
139
- clearRemoteQuotaCache();
140
- },
142
+ saveConfig,
141
143
  adapters: ADAPTERS,
142
144
  status,
143
145
  month,
@@ -0,0 +1,179 @@
1
+ import { capabilityAccess, capabilityRotation, missingExtraConfiguration, } from "./engine-status.js";
2
+ const CAPABILITIES = ["search", "fetch"];
3
+ function capableIds(rows, capability) {
4
+ return rows.filter(row => capabilityAccess(row, capability) !== "unsupported").map(row => row.id);
5
+ }
6
+ function checkOrder(field, value, capable) {
7
+ if (!Array.isArray(value) || value.some(id => typeof id !== "string")) {
8
+ return { error: `${field} must be an array of engine ids` };
9
+ }
10
+ const order = value;
11
+ const unknown = order.filter(id => !capable.includes(id));
12
+ if (unknown.length) {
13
+ return { error: `${field}: unknown or unsupported engine ids: ${[...new Set(unknown)].join(", ")}. Supported ids: ${capable.join(", ")}` };
14
+ }
15
+ if (new Set(order).size !== order.length) {
16
+ return { error: `${field} contains duplicate engine ids` };
17
+ }
18
+ const missing = capable.filter(id => !order.includes(id));
19
+ if (missing.length) {
20
+ return { error: `${field} must list every capable engine exactly once; missing: ${missing.join(", ")}` };
21
+ }
22
+ return { order };
23
+ }
24
+ function checkLimits(field, value, known) {
25
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
26
+ return { error: `${field} must be an object mapping engine ids to integers or null` };
27
+ }
28
+ const limits = {};
29
+ for (const [id, raw] of Object.entries(value)) {
30
+ if (!known.includes(id)) {
31
+ return { error: `${field}: unknown engine id "${id}". Known ids: ${known.join(", ")}` };
32
+ }
33
+ if (raw === null) {
34
+ limits[id] = null;
35
+ continue;
36
+ }
37
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) {
38
+ return { error: `${field}.${id} must be a non-negative integer or null (null removes the override), got ${JSON.stringify(raw)}` };
39
+ }
40
+ limits[id] = raw;
41
+ }
42
+ return { limits };
43
+ }
44
+ /**
45
+ * Wendet eine Teiländerung auf eine Kopie der Konfiguration an. Bei Fehlern wird
46
+ * nichts zurückgegeben, was geschrieben werden dürfte: die Prüfungen laufen
47
+ * vollständig, bevor der Aufrufer speichert.
48
+ */
49
+ export function applyEngineConfigPatch(cfg, rows, patch) {
50
+ const known = rows.map(row => row.id);
51
+ const byId = new Map(rows.map(row => [row.id, row]));
52
+ const searchCapable = capableIds(rows, "search");
53
+ const fetchCapable = capableIds(rows, "fetch");
54
+ const enabledEntries = Object.entries(patch.enabled ?? {});
55
+ const empty = enabledEntries.length === 0 &&
56
+ patch.searchOrder === undefined &&
57
+ patch.fetchOrder === undefined &&
58
+ Object.keys(patch.monthlyLimits ?? {}).length === 0 &&
59
+ Object.keys(patch.dailyLimits ?? {}).length === 0;
60
+ if (empty) {
61
+ return {
62
+ ok: false,
63
+ error: "Nothing to change: pass at least one of enabled, searchOrder, fetchOrder, monthlyLimits or dailyLimits.",
64
+ };
65
+ }
66
+ let engines = cfg.engines.map(engine => ({
67
+ ...engine,
68
+ extra: engine.extra ? { ...engine.extra } : undefined,
69
+ }));
70
+ const limits = {
71
+ monthlyLimits: { ...cfg.settings.monthlyLimits },
72
+ dailyLimits: { ...(cfg.settings.dailyLimits ?? {}) },
73
+ };
74
+ const changed = [];
75
+ for (const [id, enabled] of enabledEntries) {
76
+ const row = byId.get(id);
77
+ if (!row) {
78
+ return { ok: false, error: `enabled: unknown engine id "${id}". Known ids: ${known.join(", ")}` };
79
+ }
80
+ const engine = engines.find(candidate => candidate.id === id);
81
+ if (!engine) {
82
+ return { ok: false, error: `enabled: unknown engine id "${id}". Known ids: ${known.join(", ")}` };
83
+ }
84
+ // Nur ein tatsächlicher Wechsel auf "enabled" braucht Credentials; ein
85
+ // wiederholtes enabled=true auf einer laufenden Engine bleibt idempotent.
86
+ if (enabled && !engine.enabled) {
87
+ const modes = CAPABILITIES.map(capability => capabilityAccess(row, capability));
88
+ if (modes.every(mode => mode === "unsupported" || mode === "api_key_required" || mode === "extra_configuration_required")) {
89
+ const fields = missingExtraConfiguration(row);
90
+ const reason = modes.includes("extra_configuration_required")
91
+ ? `required extra configuration is missing (${fields.join(", ")})`
92
+ : "no API key is configured and this engine does not work keyless";
93
+ return {
94
+ ok: false,
95
+ error: `cannot enable "${id}": ${reason}. Set it in the dashboard (open_dashboard) first; nothing was changed.`,
96
+ };
97
+ }
98
+ }
99
+ if (engine.enabled !== enabled) {
100
+ changed.push(`${id}: enabled ${engine.enabled} -> ${enabled}`);
101
+ engine.enabled = enabled;
102
+ }
103
+ }
104
+ if (patch.searchOrder !== undefined) {
105
+ const checked = checkOrder("searchOrder", patch.searchOrder, searchCapable);
106
+ if ("error" in checked)
107
+ return { ok: false, error: checked.error };
108
+ const searchSet = new Set(searchCapable);
109
+ const byConfigId = new Map(engines.map(engine => [engine.id, engine]));
110
+ // Such-Engines in der gewünschten Reihenfolge, alles andere (z. B. reine
111
+ // Fetch-Engines wie Jina) unverändert dahinter — wie in normalizeConfig.
112
+ const others = engines.filter(engine => !searchSet.has(engine.id)).map(engine => engine.id);
113
+ const ordered = [...checked.order, ...others]
114
+ .map(id => byConfigId.get(id))
115
+ .filter((engine) => Boolean(engine));
116
+ const before = engines.map(engine => engine.id).join(", ");
117
+ const after = ordered.map(engine => engine.id).join(", ");
118
+ if (before !== after)
119
+ changed.push(`search order: ${before} -> ${after}`);
120
+ engines = ordered;
121
+ }
122
+ let fetchOrder = cfg.fetchOrder;
123
+ if (patch.fetchOrder !== undefined) {
124
+ const checked = checkOrder("fetchOrder", patch.fetchOrder, fetchCapable);
125
+ if ("error" in checked)
126
+ return { ok: false, error: checked.error };
127
+ if (fetchOrder.join(", ") !== checked.order.join(", ")) {
128
+ changed.push(`fetch order: ${fetchOrder.join(", ")} -> ${checked.order.join(", ")}`);
129
+ }
130
+ fetchOrder = checked.order;
131
+ }
132
+ for (const field of ["monthlyLimits", "dailyLimits"]) {
133
+ const raw = patch[field];
134
+ if (raw === undefined)
135
+ continue;
136
+ const checked = checkLimits(field, raw, known);
137
+ if ("error" in checked)
138
+ return { ok: false, error: checked.error };
139
+ const label = field === "monthlyLimits" ? "monthly" : "daily";
140
+ for (const [id, limit] of Object.entries(checked.limits)) {
141
+ const current = limits[field][id] ?? null;
142
+ if (limit === null)
143
+ delete limits[field][id];
144
+ else
145
+ limits[field][id] = limit;
146
+ if (current !== limit) {
147
+ changed.push(`${id}: ${label} limit ${current ?? "provider default"} -> ${limit ?? "provider default"}`);
148
+ }
149
+ }
150
+ }
151
+ return {
152
+ ok: true,
153
+ changed,
154
+ config: {
155
+ version: 1,
156
+ engines,
157
+ fetchOrder,
158
+ settings: { ...cfg.settings, monthlyLimits: limits.monthlyLimits, dailyLimits: limits.dailyLimits },
159
+ },
160
+ };
161
+ }
162
+ /** Kompakte, rosterweite Sicht auf die effektive Konfiguration für Tool-Antworten. */
163
+ export function engineRotationSummary(rows, cfg) {
164
+ const searchPos = new Map(cfg.engines.map((engine, index) => [engine.id, index]));
165
+ const fetchPos = new Map(cfg.fetchOrder.map((id, index) => [id, index]));
166
+ return rows.map(row => {
167
+ const engine = cfg.engines.find(candidate => candidate.id === row.id);
168
+ const enabled = engine?.enabled ?? false;
169
+ return {
170
+ id: row.id,
171
+ name: row.label,
172
+ enabled,
173
+ search: capabilityRotation(capabilityAccess(row, "search"), enabled, searchPos.get(row.id) ?? -1),
174
+ fetch: capabilityRotation(capabilityAccess(row, "fetch"), enabled, fetchPos.get(row.id) ?? -1),
175
+ monthlyLimitOverride: cfg.settings.monthlyLimits[row.id] ?? null,
176
+ dailyLimitOverride: cfg.settings.dailyLimits?.[row.id] ?? null,
177
+ };
178
+ });
179
+ }
@@ -1,3 +1,30 @@
1
+ /** Zusatzfelder (z. B. Google `cx`), die konfiguriert sein müssen, aber leer sind. */
2
+ export function missingExtraConfiguration(row) {
3
+ return (row.extraFields ?? []).filter(field => !row.extrasSet?.[field.key]).map(field => field.key);
4
+ }
5
+ /**
6
+ * Zugriffsmodus einer Engine für eine Fähigkeit — gemeinsame Wahrheit für
7
+ * `engine_status` und die Engine-Konfiguration, damit beide nie auseinanderlaufen.
8
+ */
9
+ export function capabilityAccess(row, capability) {
10
+ const supported = row.supportedCapabilities ?? row.capabilities ?? [];
11
+ if (!supported.includes(capability))
12
+ return "unsupported";
13
+ const keyless = row.keylessCapabilities ?? (row.keyless === "ip" ? row.capabilities ?? [] : []);
14
+ if (!row.hasKey && !keyless.includes(capability))
15
+ return "api_key_required";
16
+ if (missingExtraConfiguration(row).length)
17
+ return "extra_configuration_required";
18
+ return row.hasKey ? "configured_key" : "keyless";
19
+ }
20
+ /** Rotationsplatz einer Fähigkeit; -1 bedeutet: nicht in der konfigurierten Reihenfolge. */
21
+ export function capabilityRotation(mode, enabled, position) {
22
+ return {
23
+ mode,
24
+ includedInRotation: enabled && (mode === "keyless" || mode === "configured_key") && position >= 0,
25
+ configuredOrder: position >= 0 ? position + 1 : null,
26
+ };
27
+ }
1
28
  /** Model-facing facts. Configuration and stored diagnostics are not live health checks. */
2
29
  export function engineStatus(rows, month, version) {
3
30
  return {
@@ -11,17 +38,10 @@ export function engineStatus(rows, month, version) {
11
38
  "Local usage counts successful calls from this installation, separately from errors. Unknown provider limits and total usage are null, not zero or unlimited. Provider account balances must not be described as monthly consumption.",
12
39
  ],
13
40
  engines: rows.map(row => {
14
- const supported = row.supportedCapabilities ?? row.capabilities ?? [];
15
- const keyless = row.keylessCapabilities ?? (row.keyless === "ip" ? row.capabilities ?? [] : []);
16
- const missingExtraConfiguration = (row.extraFields ?? []).filter(field => !row.extrasSet?.[field.key]).map(field => field.key);
41
+ const missing = missingExtraConfiguration(row);
17
42
  const access = Object.fromEntries(["search", "fetch"].map(capability => {
18
- const mode = !supported.includes(capability) ? "unsupported"
19
- : !row.hasKey && !keyless.includes(capability) ? "api_key_required"
20
- : missingExtraConfiguration.length ? "extra_configuration_required"
21
- : row.hasKey ? "configured_key" : "keyless";
22
43
  const position = capability === "search" ? row.searchPosition : row.fetchPosition;
23
- return [capability, { mode, includedInRotation: row.enabled && (mode === "keyless" || mode === "configured_key") && position >= 0,
24
- configuredOrder: position >= 0 ? position + 1 : null }];
44
+ return [capability, capabilityRotation(capabilityAccess(row, capability), row.enabled, position)];
25
45
  }));
26
46
  const q = row.quota;
27
47
  const anonymous = !row.hasKey;
@@ -32,7 +52,7 @@ export function engineStatus(rows, month, version) {
32
52
  enabled: row.enabled,
33
53
  disabledReason: row.enabled ? null : "disabled_in_configuration; original reason not recorded",
34
54
  apiKeyConfigured: row.hasKey,
35
- missingExtraConfiguration,
55
+ missingExtraConfiguration: missing,
36
56
  access,
37
57
  localUsage: { successfulSearches: row.used.search, successfulFetches: row.used.fetch, failedAttempts: row.used.errors ?? 0 },
38
58
  quota: {
@@ -2,6 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  import { RouterError } from "../router.js";
4
4
  import { engineStatus } from "./engine-status.js";
5
+ import { applyEngineConfigPatch, engineRotationSummary } from "./engine-config.js";
5
6
  import { dateSchema, timeRangeSchema, describeSearchTime } from "../search-time.js";
6
7
  import { VERSION } from "../version.js";
7
8
  import { readFileSync } from "node:fs";
@@ -25,16 +26,17 @@ async function withRouterDiagnostics(operation) {
25
26
  }
26
27
  export function buildMcpServer(deps) {
27
28
  const server = new McpServer({ name: "search-rotation", version: VERSION, icons });
28
- server.registerTool("web_search", {
29
+ server.registerTool("search_web", {
29
30
  title: "Web Search",
30
- description: "Web search with automatic round-robin across multiple free search APIs (Tavily, Firecrawl, Parallel, Exa, Google PSE, DuckDuckGo) including transparent failover. Returns numbered results with title, URL, snippet and publication date when available. Time filters restrict rotation and failover to compatible providers; never silently dropped.",
31
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
32
+ description: "Search the web through a rotating pool of search providers (Tavily, Firecrawl, Parallel, Exa, Google PSE, DuckDuckGo) with automatic failover when one errors, is rate-limited or has no local quota left. Use it when the answer depends on current web pages and no URL is known yet; use fetch_url to read a page whose URL is already known. Returns numbered results (title, URL, snippet, publication date when the provider supplies one) and names the provider that answered plus every failed attempt, so failover stays visible. Time filters keep rotation inside the providers that implement them and fail explicitly instead of searching without the filter; if every eligible provider fails, the tool returns an error listing each provider's reason. The engine parameter only prefers a provider and leaves failover active.",
31
33
  inputSchema: {
32
34
  query: z.string().describe("Search query"),
33
35
  numResults: z.number().int().min(1).max(20).optional().describe("Optional result count (1–20). Omit to use the dashboard setting: a custom count or the provider default."),
34
36
  timeRange: timeRangeSchema.optional().describe("Relative UTC date window: day=1, week=7, month=30, year=365 days through today. Cannot combine with startDate/endDate; day precision, not rolling hours."),
35
37
  startDate: dateSchema.optional().describe("Start date YYYY-MM-DD. May be used alone or with endDate; cannot combine with timeRange."),
36
38
  endDate: dateSchema.optional().describe("End date YYYY-MM-DD (through this date, subject to provider boundary semantics). Cannot combine with timeRange."),
37
- engine: z.string().optional().describe("Preferred engine id; failover to the other engines stays active"),
39
+ engine: z.string().optional().describe("Preferred engine id from get_engine_status; only moves that engine to the front, failover to the others stays active. Engines without implemented support for the time filter are skipped even when preferred."),
38
40
  },
39
41
  }, async ({ query, numResults, engine, timeRange, startDate, endDate }, extra) => {
40
42
  const r = await withRouterDiagnostics(deps.router.search({ query, numResults, timeRange, startDate, endDate }, { preferEngine: engine, signal: deps.requestSignal ? AbortSignal.any([extra.signal, deps.requestSignal]) : extra.signal }));
@@ -55,9 +57,10 @@ export function buildMcpServer(deps) {
55
57
  });
56
58
  server.registerTool("fetch_url", {
57
59
  title: "Fetch URL",
58
- description: "Fetch a web page and return its content as markdown. Rotates across extraction providers (Jina Reader, Firecrawl, Parallel, Tavily, Exa) with failover.",
60
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
61
+ description: "Fetch one web page and return its main content as Markdown, rotating across extraction providers (Jina Reader, Firecrawl, Parallel, Tavily, Exa) with failover. Use it to read a URL the user or a previous search_web result already names; it does not discover links or crawl a site. Only http and https URLs are accepted. The result names the provider that succeeded, lists failed attempts and truncates content beyond 50,000 characters with a marker; if every provider fails, the tool returns an error listing each provider's reason.",
59
62
  inputSchema: {
60
- url: z.string().url().describe("URL to fetch"),
63
+ url: z.string().url().describe("Absolute http(s) URL of the page to fetch, including scheme and host"),
61
64
  },
62
65
  }, async ({ url }, extra) => {
63
66
  const r = await withRouterDiagnostics(deps.router.fetchUrl({ url }, { signal: deps.requestSignal ? AbortSignal.any([extra.signal, deps.requestSignal]) : extra.signal }));
@@ -69,10 +72,10 @@ export function buildMcpServer(deps) {
69
72
  const head = `Fetched ${url} via ${r.engine}` + (failover.length ? ` (failover after: ${failover.join("; ")})` : "");
70
73
  return { content: [{ type: "text", text: `${head}\n\n${text}` }] };
71
74
  });
72
- server.registerTool("engine_status", {
75
+ server.registerTool("get_engine_status", {
73
76
  title: "Engine Status",
74
- annotations: { readOnlyHint: true, destructiveHint: false },
75
- description: "Read engine configuration, per-capability authentication requirements, local usage and quota sources. This is not a live health check. Historical errors do not prove a current failure; disabled engines are not necessarily missing credentials.",
77
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
78
+ description: "Read this server's local engine configuration: enabled state, configured rotation order, per-capability access (keyless, configured key, or key/extra credentials still required), local usage counters and the quota source per engine. Use it before update_engine_config for valid engine ids, and as the machine-readable answer when you need to know which providers are active, how much quota is left or why a provider is skipped — the dashboard is the human-facing view of the same installation. The tool performs no search and no fetch: it is not a live health check, stored errors are historical observations, and a disabled engine is a configuration state, not proof that a key is missing. Provider balances can come from a cached lookup that the dashboard refreshes; unknown provider limits are reported as null rather than zero or unlimited.",
76
79
  inputSchema: {},
77
80
  }, async () => {
78
81
  const status = engineStatus(await deps.status(), deps.month(), VERSION);
@@ -83,7 +86,8 @@ export function buildMcpServer(deps) {
83
86
  });
84
87
  server.registerTool("open_dashboard", {
85
88
  title: "Open Dashboard",
86
- description: "Open the local dashboard (API keys, engine order and toggles, quota view) in a web browser. Use when the user wants to configure engines or check quota.",
89
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
90
+ description: "Open the local dashboard of this server in a browser on the machine running the server, for a human: it is the only place to enter API keys and provider credentials, strict-free mode, port and access token, and it shows the interactive usage, quota and request-history views. Use it for those credential and network settings and when a person should look at the data; read configuration with get_engine_status and change engines with update_engine_config instead, because their results are machine-readable and this call only launches a browser. The browser opens on the server host, not on the client's machine, and a server started with --no-dashboard returns an error.",
87
91
  inputSchema: {},
88
92
  }, async () => {
89
93
  const url = deps.dashboardUrl();
@@ -101,5 +105,60 @@ export function buildMcpServer(deps) {
101
105
  deps.openDashboard();
102
106
  return { content: [{ type: "text", text: `Dashboard: ${url}` }] };
103
107
  });
108
+ server.registerTool("update_engine_config", {
109
+ title: "Update Engine Config",
110
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
111
+ description: "Change the local engine configuration: enable or disable engines, set the search and fetch rotation order, and set monthly or daily call limits per engine. Use it when the user wants a provider switched on or off, reordered or capped; call get_engine_status first for valid ids, current order and access modes. The tool writes the shared configuration file of this installation, so the change applies to later searches of every client without a restart. API keys are not accepted here: an engine that still needs a key or extra credentials cannot be enabled until they are set in the dashboard (open_dashboard). Returns the effective per-engine configuration, or an error naming the offending ids — on error nothing is written.",
112
+ inputSchema: {
113
+ enabled: z.record(z.string(), z.boolean()).optional().describe("Per engine id (see get_engine_status): true enables, false disables. Enabling an engine whose access mode is api_key_required or extra_configuration_required fails until the credential exists in the dashboard."),
114
+ searchOrder: z.array(z.string()).optional().describe("Complete search rotation: every search-capable engine id exactly once, the first entry is queried first. Fetch-only engines are not part of this order and keep their place behind it."),
115
+ fetchOrder: z.array(z.string()).optional().describe("Complete fetch rotation: every fetch-capable engine id exactly once, the first entry is tried first."),
116
+ monthlyLimits: z.record(z.string(), z.number().int().min(0).nullable()).optional().describe("Per engine id: cap for locally counted calls per month, or null to remove the override and fall back to the provider default. The cap drives rotation and strict-free ordering; it does not change the provider account."),
117
+ dailyLimits: z.record(z.string(), z.number().int().min(0).nullable()).optional().describe("Per engine id: cap for locally counted calls per day, or null to remove the override. Applies to providers whose quota period is counted per day."),
118
+ },
119
+ }, async ({ enabled, searchOrder, fetchOrder, monthlyLimits, dailyLimits }) => {
120
+ const rows = await deps.status();
121
+ const applied = applyEngineConfigPatch(deps.getConfig(), rows, {
122
+ enabled,
123
+ searchOrder,
124
+ fetchOrder,
125
+ monthlyLimits,
126
+ dailyLimits,
127
+ });
128
+ if (!applied.ok) {
129
+ return { isError: true, content: [{ type: "text", text: applied.error }] };
130
+ }
131
+ if (applied.changed.length)
132
+ deps.saveConfig(applied.config);
133
+ const engines = engineRotationSummary(rows, deps.getConfig());
134
+ const head = applied.changed.length
135
+ ? "Local engine configuration updated (applies to later searches, no restart needed):"
136
+ : "No change: the requested values already match the local configuration.";
137
+ const lines = engines.map(engine => {
138
+ const parts = [engine.enabled ? "enabled" : "disabled"];
139
+ for (const capability of ["search", "fetch"]) {
140
+ const state = engine[capability];
141
+ if (state.mode === "unsupported")
142
+ continue;
143
+ parts.push(`${capability} ${state.configuredOrder === null ? "not in rotation" : `#${state.configuredOrder}`} (${state.mode})`);
144
+ }
145
+ if (engine.monthlyLimitOverride !== null)
146
+ parts.push(`monthly limit ${engine.monthlyLimitOverride}`);
147
+ if (engine.dailyLimitOverride !== null)
148
+ parts.push(`daily limit ${engine.dailyLimitOverride}`);
149
+ return `- ${engine.id}: ${parts.join(", ")}`;
150
+ });
151
+ const text = [
152
+ head,
153
+ ...applied.changed.map(change => `- ${change}`),
154
+ "",
155
+ "Effective configuration:",
156
+ ...lines,
157
+ ].join("\n");
158
+ return {
159
+ structuredContent: { ok: true, changed: applied.changed, engines },
160
+ content: [{ type: "text", text }],
161
+ };
162
+ });
104
163
  return server;
105
164
  }
Binary file
Binary file
Binary file
Binary file
@@ -22,7 +22,7 @@
22
22
  <text x="76" y="286" fill="#8d9bb5" font-size="12" font-weight="700" letter-spacing="1.5">DEIN MCP-CLIENT</text>
23
23
  <text x="76" y="327" fill="#f1f4fb" font-size="23" font-weight="650">Eine Anfrage</text>
24
24
  <rect x="75" y="347" width="202" height="39" rx="8" fill="#0c1220"/>
25
- <text x="89" y="372" fill="#c5baff" font-family="'SFMono-Regular', Consolas, monospace" font-size="15">web_search(query)</text>
25
+ <text x="89" y="372" fill="#c5baff" font-family="'SFMono-Regular', Consolas, monospace" font-size="15">search_web(query)</text>
26
26
  <text x="76" y="417" fill="#93a0b8" font-size="14">Codex · Claude · Cursor · …</text>
27
27
  <path d="M307 346 H368" fill="none" stroke="#78879e" stroke-width="2" marker-end="url(#arrow)"/>
28
28
  <rect x="384" y="235" width="328" height="228" rx="18" fill="url(#router)" stroke="#726ca7"/>
package/docs/clients.md CHANGED
@@ -77,7 +77,7 @@ Add this entry under `mcp.servers`:
77
77
  If installed locally, use `search-rotation` as the command. A prebuilt archive is also available under [GitHub Releases](https://github.com/robin-bially/search-rotation-mcp/releases/latest):
78
78
 
79
79
  ```sh
80
- npm install -g ./search-rotation-0.4.11.tgz
80
+ npm install -g ./search-rotation-0.5.1.tgz
81
81
  ```
82
82
 
83
83
  npx installs the package from the npm registry, so no Git access is required. The pinned
@@ -88,7 +88,7 @@ For remote HTTP access, authentication and advanced settings, see the [operation
88
88
 
89
89
  ## Multiple harnesses and updates
90
90
 
91
- Each stdio client starts its own server process. Its dashboard binds to the configured port (6277 by default), or the next free port up to 20 ports higher. `open_dashboard` opens the dashboard belonging to that process; it does not attach to another harness's process.
91
+ Each stdio client starts its own server process. Its dashboard binds to the configured port (6277 by default), or the next free port up to 20 ports higher. `open_dashboard` opens the dashboard belonging to that process; it does not attach to another harness's process. Configuration is shared, though: every process reads `~/.config/search-rotation/config.json`, so an `update_engine_config` call in one harness applies to the others as soon as they read the file for their next search.
92
92
 
93
93
  Processes using the same `SEARCH_ROTATION_HOME` share configuration and counters. Engine settings reload before requests; port and authentication changes require a restart. Rotation cursors, cooldowns and in-flight reservations are per process, so strict-free mode is not a global spending lock across harnesses.
94
94
 
@@ -1,22 +1,21 @@
1
1
  # Distribution and listings
2
2
 
3
3
  Where search-rotation is published or listed, who keeps each channel current, and
4
- what still needs attention. Snapshot: 2026-09-26, after the account rename from `localfoundry` to `robin-bially` and the 0.4.11 release.
4
+ what still needs attention. Snapshot: 2026-09-27, after the 0.5.0 release that renamed
5
+ the MCP tools to `search_web`, `fetch_url`, `get_engine_status`, `update_engine_config`
6
+ and `open_dashboard`.
5
7
 
6
8
  ## Channels
7
9
 
8
10
  | Channel | Entry | Kept current by | State |
9
11
  | --- | --- | --- | --- |
10
- | npm | `search-rotation` | release workflow, trusted publishing | 0.4.10; 0.4.11 blocked by the stale trusted publisher, see below |
11
- | Homebrew | `brew install robin-bially/tap/search-rotation` | release script writes the formula | 0.4.11 |
12
- | Official MCP Registry | `io.github.robin-bially/search-rotation` | release workflow, GitHub OIDC | blocked: the registry verifies the npm package, so it needs 0.4.11 on npm first; `io.github.localfoundry/search-rotation` stays live at 0.4.10 because the registry has no unpublish |
13
- | GitHub releases | tag, tarball and checksum | release script | v0.4.11 |
14
- | Glama | [directory entry](https://glama.ai/mcp/servers/robin-bially/search-rotation-mcp), rated A | Glama indexes the repository on its own | listed; the account rename is pending a re-crawl |
12
+ | npm | `search-rotation` | release workflow, trusted publishing | 0.5.0 |
13
+ | Homebrew | `brew install robin-bially/tap/search-rotation` | release script writes the formula | 0.5.0 |
14
+ | Official MCP Registry | `io.github.robin-bially/search-rotation` | release workflow, GitHub OIDC | 0.5.0; the old `io.github.localfoundry/search-rotation` stays frozen at 0.4.10 because the registry has no unpublish |
15
+ | GitHub releases | tag, tarball and checksum | release script | v0.5.0 |
16
+ | Glama | [directory entry](https://glama.ai/mcp/servers/robin-bially/search-rotation-mcp), rated A | Glama indexes the repository on its own | listed under the current account name; Glama picked up the rename by itself, the previous `RobinBially` path still resolves |
15
17
  | mcpservers.org | [submission](https://mcpservers.org/de/submit) from 2026-09-26 | reviewed by the site | in review, up to two weeks |
16
18
  | PulseMCP | — | ingests the official registry | submissions paused, not listed yet |
17
- | [punkpeye/awesome-mcp-servers](https://github.com/punkpeye/awesome-mcp-servers/pull/15171) | Search & Data Extraction | maintainers | pull request open, listing checks done |
18
- | [YuzeHao2023/Awesome-MCP-Servers](https://github.com/YuzeHao2023/Awesome-MCP-Servers/pull/550) | Search & Web | maintainers | pull request open |
19
- | [ever-works/awesome-mcp-servers](https://github.com/ever-works/awesome-mcp-servers/pull/188) | Web Search | maintainers | pull request open |
20
19
 
21
20
  Channels that build on the official registry follow a release by themselves, so a
22
21
  release needs no manual work for them.
@@ -37,17 +36,27 @@ Four details are easy to forget:
37
36
  - `server.json` and `mcpName` must name the same server; `test/registry-manifest.test.ts` fails when they drift apart.
38
37
  - The registry limits `description` to 100 characters.
39
38
  - The registry verifies ownership through the published npm package, so a manifest can only be registered once that version exists on npm. The workflow skips an already published version, which makes a re-run safe.
40
- - npm trusted publishing is bound to one repository. The entry for `search-rotation` still named `localfoundry/search-rotation-mcp`, so the 0.4.11 publish failed with `404 Not Found - PUT https://registry.npmjs.org/search-rotation`. Repair with `npm trust github search-rotation --file publish.yml --repo robin-bially/search-rotation-mcp --allow-publish`, then revoke the stale entry (`npm trust list search-rotation`, `npm trust revoke search-rotation --id=<id>`) and re-run the publish workflow. Both commands ask for a one-time password in the browser. The old owner name is free again, so the stale entry also lets a stranger publish to this package.
39
+ - That ownership check also races npm propagation: with 0.5.0 the registry answered `version '0.5.0' was not found (status: 404)` seconds after the publish, and `gh run rerun <id> --failed` registered the version on the second attempt without republishing (the publish step sees the existing version and skips).
40
+ - The Homebrew formula carries a `test do` block that asserts the tool names from `tools/list`; it has to follow a rename like the one in 0.5.0, and `brew audit` does not catch a stale list.
41
+ - npm trusted publishing is bound to one repository, so an account rename has to be re-pointed by hand: `npm trust github search-rotation --file publish.yml --repo robin-bially/search-rotation-mcp --allow-publish` (one-time password in the browser). That was missed for 0.4.11, whose publish failed with `404 Not Found - PUT https://registry.npmjs.org/search-rotation`; the stale entry for `localfoundry/search-rotation-mcp` was revoked on 2026-09-26, because the freed owner name would otherwise let a stranger publish to this package.
41
42
 
42
43
  ## Recurring checks
43
44
 
44
- - After a release: `npm view search-rotation version` and the [registry entry](https://registry.modelcontextprotocol.io/?q=io.github.robin-bially%2Fsearch-rotation) should both show the new version. The release script already waits for npm, but a failed publish workflow leaves the release and the tap ahead of npm, as with 0.4.11.
45
+ - After a release: `npm view search-rotation version` and the [registry entry](https://registry.modelcontextprotocol.io/?q=io.github.robin-bially%2Fsearch-rotation) should both show the new version. The release script already waits for npm. A failed publish workflow leaves the release and the tap ahead of npm, which happened with 0.4.11; `gh workflow run publish.yml` then repairs npm and the registry in one run.
45
46
  - Dependency and tap maintenance runs outside this repository on a biweekly schedule.
46
47
  - The GitHub account moved from `localfoundry` to `robin-bially` on 2026-09-26. The registry has no unpublish, so the entry `io.github.localfoundry/search-rotation` stays frozen at 0.4.10 while the next release adds `io.github.robin-bially/search-rotation`. The old handle is free again, so anything still pointing there can end up at a different account.
47
- - Open externally: mcpservers.org reviews the submission, PulseMCP is paused, the three list pull requests wait for their maintainers, and Glama re-crawls repositories at its own pace.
48
+ - Open externally: mcpservers.org reviews the submission and PulseMCP is paused. Glama has re-crawled the repository after the rename.
48
49
 
49
50
  ## Deliberately not used
50
51
 
51
52
  Directories that require a new account before a submission (Glama's own form,
52
53
  mcp.so) are skipped. The Glama listing exists because Glama indexes public GitHub
53
54
  repositories without a submission; it picked up the repository rename by itself.
55
+
56
+ Curated awesome lists are skipped too. The three merge requests opened on
57
+ 2026-09-26 (punkpeye/awesome-mcp-servers#15171, YuzeHao2023/Awesome-MCP-Servers#550,
58
+ ever-works/awesome-mcp-servers#188) were closed on 2026-09-27 without waiting for a
59
+ reviewer: the official MCP Registry and Glama already carry the server, and neither
60
+ list adds discovery that the registry does not. PulseMCP and Glama derive their
61
+ entries from the registry and the public repository, so no manual list work is
62
+ needed to stay visible.
@@ -15,7 +15,7 @@ Die Cooldowns und Rotationspositionen gelten pro Serverprozess und werden beim N
15
15
 
16
16
  ### Kontingente und Speicherung
17
17
 
18
- Dashboard und `engine_status` zeigen **Zeitraum, Einheit, Quelle und Schätzstatus**. Google zählt pro Tag in `America/Los_Angeles` einschließlich Sommerzeit; Monatszähler verwenden UTC. Keyless Firecrawl/Exa sowie Jina und DuckDuckGo werden als unbekanntes IP-Kontingent angezeigt, nicht als kostenloses Monatsguthaben.
18
+ Dashboard und `get_engine_status` zeigen **Zeitraum, Einheit, Quelle und Schätzstatus**. Google zählt pro Tag in `America/Los_Angeles` einschließlich Sommerzeit; Monatszähler verwenden UTC. Keyless Firecrawl/Exa sowie Jina und DuckDuckGo werden als unbekanntes IP-Kontingent angezeigt, nicht als kostenloses Monatsguthaben.
19
19
 
20
20
  Tavily/Firecrawl verwenden, soweit vorhanden, den Remote-Kontostand mit fünf Minuten Cache. Eigene erfolgreiche Aufrufe reduzieren den gecachten Rest sofort. Die lokalen Credit-Schätzungen sind derzeit Tavily Search 1 / Fetch 0,2 sowie Firecrawl Search 2 je angefangene 10 angefragte Treffer / Fetch 1. Die Aufrufzahl wird davon unabhängig gespeichert. Andere Engines zählen lokal Requests; ein aus Geldguthaben abgeleitetes Exa-Limit bleibt eine Schätzung.
21
21
 
@@ -46,7 +46,7 @@ Für dieses Beispiel leitet ein HTTPS-Reverse-Proxy `search.example.com` an Port
46
46
 
47
47
  `/mcp` bleibt Bearer-authentifiziert. Im Browser führt die Dashboard-URL zu **`/login`**: Token einmal im Passwortformular eingeben, anschließend gilt ein acht Stunden gültiges `HttpOnly`-/`SameSite=Strict`-Session-Cookie (bei HTTPS zusätzlich `Secure`). Geheimnisfreie Skripte und Styles sind öffentlich, die Dashboard-API ist geschützt. API-Schreibzugriffe benötigen JSON und eine erlaubte Origin, sofern der Client eine sendet.
48
48
 
49
- Langlebige Tokens erscheinen weder in Logs noch in Tool-Ergebnissen oder URLs. `open_dashboard` öffnet intern einen 60 s gültigen Einmallink; der zurückgegebene Text enthält nur die Basis-URL. Die frühere `?token=…`-Anmeldung entfällt. `--no-dashboard` schaltet Dashboard, Login und Admin-API vollständig ab.
49
+ Langlebige Tokens erscheinen weder in Logs noch in Tool-Ergebnissen oder URLs. `open_dashboard` öffnet intern einen 60 s gültigen Einmallink; der zurückgegebene Text enthält nur die Basis-URL. Die frühere `?token=…`-Anmeldung entfällt. `--no-dashboard` schaltet Dashboard, Login und Admin-API vollständig ab; `get_engine_status` und `update_engine_config` arbeiten weiterhin auf der lokalen Konfigurationsdatei.
50
50
 
51
51
  Ohne Keys verwenden Firecrawl/Exa die Server-Egress-IP; verfügbare IP-Kontingente hängen deshalb vom Hosting ab.
52
52
 
@@ -1,3 +1,30 @@
1
+ ## v0.5.0 — Consistent tool names and programmable engine configuration
2
+
3
+ The tool definitions were rewritten against the TDQS rubric, and two tools changed
4
+ name: `web_search` is now `search_web`, `engine_status` is now `get_engine_status`.
5
+ All five tools follow the same verb_noun pattern. Arguments, results and error texts
6
+ are unchanged, so only clients that keep an allowlist of tool names need an edit.
7
+
8
+ `update_engine_config` is new: it enables or disables engines, sets the search and
9
+ fetch rotation order, and sets monthly or daily limits per engine. It writes the same
10
+ local configuration file as the dashboard and applies to later searches without a
11
+ restart; API keys stay in the dashboard.
12
+
13
+ Every description now states when to use the tool and how failures surface, and all
14
+ five tools carry MCP annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`,
15
+ `openWorldHint`).
16
+
17
+ ```sh
18
+ brew update && brew upgrade robin-bially/tap/search-rotation
19
+ ```
20
+
21
+ Or run the pinned GitHub version:
22
+
23
+ ```sh
24
+ npx -y --allow-git=all github:robin-bially/search-rotation-mcp#v0.5.0
25
+ ```
26
+
27
+
1
28
  ## v0.4.11 — Distribution under the robin-bially account
2
29
 
3
30
  The GitHub account moved from `localfoundry` to `robin-bially`, so every distribution
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "search-rotation",
3
- "version": "0.4.11",
3
+ "version": "0.5.1",
4
4
  "mcpName": "io.github.robin-bially/search-rotation",
5
5
  "description": "Round-robin web search over the free tiers of multiple search APIs (Tavily, Firecrawl, Parallel, Exa, Google PSE, Jina, DuckDuckGo) as an MCP server — with a local dashboard for API keys, engine order/toggles and live quota status.",
6
6
  "type": "module",
package/static/app.js CHANGED
@@ -478,9 +478,10 @@ function renderSnippets() {
478
478
 
479
479
  function renderTools() {
480
480
  const tools = [
481
- { name: "web_search", icon: "search", params: [["query", "query"], ["numResults", "count"], ["engine", "engine"], ["timeRange", "timeRange"], ["startDate", "startDate"], ["endDate", "endDate"]], args: { query: "latest AI research", numResults: 5, timeRange: "week" } },
481
+ { name: "search_web", icon: "search", params: [["query", "query"], ["numResults", "count"], ["engine", "engine"], ["timeRange", "timeRange"], ["startDate", "startDate"], ["endDate", "endDate"]], args: { query: "latest AI research", numResults: 5, timeRange: "week" } },
482
482
  { name: "fetch_url", icon: "link", params: [["url", "url"]], args: { url: "https://example.com" } },
483
- { name: "engine_status", icon: "gauge", params: [], args: {} },
483
+ { name: "get_engine_status", icon: "gauge", params: [], args: {} },
484
+ { name: "update_engine_config", icon: "grip", params: [["enabled", "enabled"], ["searchOrder", "searchOrder"], ["fetchOrder", "fetchOrder"], ["monthlyLimits", "monthlyLimits"], ["dailyLimits", "dailyLimits"]], args: { enabled: { duckduckgo: false }, searchOrder: ["tavily", "firecrawl", "parallel", "exa", "google-cse", "duckduckgo"] } },
484
485
  { name: "open_dashboard", icon: "stack", params: [], args: {} },
485
486
  ];
486
487
  $("#view").innerHTML = '<div class="page">' + pageHead(t("tools.title"), t("tools.sub")) +
package/static/i18n.js CHANGED
@@ -30,12 +30,13 @@ window.I18N = {
30
30
  "settings.results": "Standard-Ergebnisanzahl",
31
31
  "nav.tools": "MCP Tools",
32
32
  "tools.title": "MCP Tools",
33
- "tools.sub": "Vier Werkzeuge, die dein KI-Assistent aufrufen kann",
33
+ "tools.sub": "Fünf Werkzeuge, die dein KI-Assistent aufrufen kann",
34
34
  "tools.intro": "Beschreibe deinem Assistenten einfach, was du brauchst. Er ruft das passende Tool auf. Die Beispiele zeigen Toolname und Argumente; Kopieren führt keinen Aufruf aus.",
35
- "tools.web_search": "Sucht im Web und gibt Titel, URLs und Textauszüge zurück. Rotiert automatisch zwischen Engines und wechselt bei Fehlern zum nächsten Anbieter.",
36
- "tools.fetch_url": "Ruft eine bestimmte Webseite ab und gibt ihren Inhalt als Markdown zurück. Nutzt eine eigene Anbieter-Rotation für die Textextraktion.",
37
- "tools.engine_status": "Zeigt Aktivierung, Zugang pro Funktion, lokale Nutzung und Kontingente. Fehlende Keys sind nicht automatisch ein Problem: Einige Funktionen gehen keyless. Gespeicherte Fehler sind historisch; dies ist kein Live-Funktionstest.",
38
- "tools.open_dashboard": "Öffnet dieses Dashboard im Browser des Rechners, auf dem der Server läuft. Bei einem entfernten Server öffnet es nicht automatisch deinen lokalen Browser.",
35
+ "tools.search_web": "Sucht im Web und gibt Titel, URLs und Textauszüge zurück. Rotiert automatisch zwischen Engines und wechselt bei Fehlern zum nächsten Anbieter. Richtig, solange keine URL bekannt ist; für eine bekannte Adresse ist fetch_url zuständig.",
36
+ "tools.fetch_url": "Ruft eine bestimmte Webseite ab und gibt ihren Inhalt als Markdown zurück. Nutzt eine eigene Anbieter-Rotation für die Textextraktion. Richtig für eine bekannte URL; Seiten finden kann nur search_web.",
37
+ "tools.get_engine_status": "Zeigt Aktivierung, Reihenfolge, Zugang pro Funktion, lokale Nutzung und Kontingente. Fehlende Keys sind nicht automatisch ein Problem: Einige Funktionen gehen keyless. Gespeicherte Fehler sind historisch; dies ist kein Live-Funktionstest.",
38
+ "tools.update_engine_config": "Schaltet Engines ein oder aus, setzt die Reihenfolge von Such- und Fetch-Rotation und begrenzt Aufrufe pro Engine (Monats- und Tageslimit). Schreibt die lokale Konfigurationsdatei und gilt für spätere Suchen ohne Neustart; API-Keys und Zugangsdaten bleiben im Dashboard.",
39
+ "tools.open_dashboard": "Öffnet dieses Dashboard im Browser des Rechners, auf dem der Server läuft: API-Keys, Zugangsdaten, Netzwerkeinstellungen sowie Nutzung, Kontingente und Verlauf. Bei einem entfernten Server öffnet es nicht automatisch deinen lokalen Browser.",
39
40
  "tools.parameters": "Parameter",
40
41
  "tools.none": "Keine Parameter.",
41
42
  "tools.example": "Beispielaufruf",
@@ -47,6 +48,11 @@ window.I18N = {
47
48
  "tools.param.startDate": "Optional · Startdatum YYYY-MM-DD. Allein oder mit endDate; muss vor oder auf endDate liegen.",
48
49
  "tools.param.endDate": "Optional · Enddatum YYYY-MM-DD. Datumsinterpretation hängt vom Anbieter ab; ungeeignete Anbieter werden übersprungen.",
49
50
  "tools.param.url": "Erforderlich · Vollständige URL der Webseite.",
51
+ "tools.param.enabled": "Optional · Pro Engine-ID: true aktiviert, false deaktiviert. Aktivieren scheitert, solange ein nötiger Key oder Zusatzfelder fehlen.",
52
+ "tools.param.searchOrder": "Optional · Vollständige Such-Rotation: jede suchfähige Engine-ID genau einmal, erste zuerst. Reine Fetch-Engines bleiben außen vor.",
53
+ "tools.param.fetchOrder": "Optional · Vollständige Fetch-Rotation: jede fetchfähige Engine-ID genau einmal, erste zuerst.",
54
+ "tools.param.monthlyLimits": "Optional · Pro Engine-ID: Obergrenze für lokal gezählte Aufrufe pro Monat. null entfernt den Override und stellt den Anbieterstandard her.",
55
+ "tools.param.dailyLimits": "Optional · Pro Engine-ID: Obergrenze für lokal gezählte Aufrufe pro Tag. null entfernt den Override.",
50
56
 
51
57
  "settings.title": "Rotation einstellen",
52
58
  "settings.strict": "Strikter Gratis-Modus",
@@ -190,12 +196,13 @@ window.I18N = {
190
196
  "settings.results": "Default result count",
191
197
  "nav.tools": "MCP Tools",
192
198
  "tools.title": "MCP Tools",
193
- "tools.sub": "Four tools your AI assistant can call",
199
+ "tools.sub": "Five tools your AI assistant can call",
194
200
  "tools.intro": "Tell your assistant what you need and it will call the appropriate tool. Examples show tool names and arguments; copying an example does not run it.",
195
- "tools.web_search": "Searches the web and returns titles, URLs and snippets. Automatically rotates between engines and falls back to another provider when a request fails.",
196
- "tools.fetch_url": "Fetches a specific webpage and returns its content as Markdown. Uses a separate provider rotation for text extraction.",
197
- "tools.engine_status": "Reports configuration, access per capability, local usage and quotas. A missing key is not always a problem: some capabilities work keyless. Stored errors are historical; this is not a live health check.",
198
- "tools.open_dashboard": "Opens this dashboard in a browser on the machine running the server. A remote server does not automatically open your local browser.",
201
+ "tools.search_web": "Searches the web and returns titles, URLs and snippets. Automatically rotates between engines and falls back to another provider when a request fails. Use it while no URL is known; fetch_url reads a known address.",
202
+ "tools.fetch_url": "Fetches a specific webpage and returns its content as Markdown. Uses a separate provider rotation for text extraction. Use it for a known URL; finding pages is search_web's job.",
203
+ "tools.get_engine_status": "Reports configuration, rotation order, access per capability, local usage and quotas. A missing key is not always a problem: some capabilities work keyless. Stored errors are historical; this is not a live health check.",
204
+ "tools.update_engine_config": "Enables or disables engines, sets the search and fetch rotation order and caps calls per engine (monthly and daily limits). Writes the local configuration file and applies to later searches without a restart; API keys and credentials stay in the dashboard.",
205
+ "tools.open_dashboard": "Opens this dashboard in a browser on the machine running the server: API keys, credentials, network settings, plus usage, quotas and history. A remote server does not automatically open your local browser.",
199
206
  "tools.parameters": "Parameters",
200
207
  "tools.none": "No parameters.",
201
208
  "tools.example": "Example call",
@@ -207,6 +214,11 @@ window.I18N = {
207
214
  "tools.param.startDate": "Optional · Start date YYYY-MM-DD. Alone or with endDate; must be on or before endDate.",
208
215
  "tools.param.endDate": "Optional · End date YYYY-MM-DD. Date semantics depend on the provider; incompatible providers are skipped.",
209
216
  "tools.param.url": "Required · Full webpage URL.",
217
+ "tools.param.enabled": "Optional · Per engine ID: true enables, false disables. Enabling fails while a required key or extra configuration is missing.",
218
+ "tools.param.searchOrder": "Optional · Complete search rotation: every search-capable engine ID exactly once, first entry first. Fetch-only engines are not part of this order.",
219
+ "tools.param.fetchOrder": "Optional · Complete fetch rotation: every fetch-capable engine ID exactly once, first entry first.",
220
+ "tools.param.monthlyLimits": "Optional · Per engine ID: cap for locally counted calls per month. null removes the override and restores the provider default.",
221
+ "tools.param.dailyLimits": "Optional · Per engine ID: cap for locally counted calls per day. null removes the override.",
210
222
 
211
223
  "settings.title": "Rotation settings",
212
224
  "settings.strict": "Strict free mode",