search-rotation 0.4.11 → 0.5.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/README.md +8 -6
- package/dist/index.js +8 -6
- package/dist/mcp/engine-config.js +179 -0
- package/dist/mcp/engine-status.js +30 -10
- package/dist/mcp/server.js +68 -9
- package/docs/assets/dashboard-dark.png +0 -0
- package/docs/assets/dashboard-light.png +0 -0
- package/docs/assets/mcp-tools-dark.png +0 -0
- package/docs/assets/mcp-tools-light.png +0 -0
- package/docs/assets/search-rotation.svg +1 -1
- package/docs/clients.md +2 -2
- package/docs/distribution.md +6 -6
- package/docs/operations.md +2 -2
- package/docs/release-notes.md +27 -0
- package/package.json +1 -1
- package/static/app.js +3 -2
- package/static/i18n.js +22 -10
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.
|
|
46
|
+
npx -y --allow-git=all github:robin-bially/search-rotation-mcp#v0.5.0
|
|
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
|
-
`
|
|
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
|
|
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
|
-
`
|
|
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":"
|
|
93
|
+
{"name":"search_web","arguments":{"query":"AI inference research","timeRange":"week","numResults":5}}
|
|
92
94
|
```
|
|
93
95
|
|
|
94
96
|
```json
|
|
95
|
-
{"name":"
|
|
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
|
|
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
|
|
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,
|
|
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: {
|
package/dist/mcp/server.js
CHANGED
|
@@ -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("
|
|
29
|
+
server.registerTool("search_web", {
|
|
29
30
|
title: "Web Search",
|
|
30
|
-
|
|
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
|
|
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
|
-
|
|
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("
|
|
75
|
+
server.registerTool("get_engine_status", {
|
|
73
76
|
title: "Engine Status",
|
|
74
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
75
|
-
description: "Read engine configuration, per-capability
|
|
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
|
-
|
|
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">
|
|
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.
|
|
80
|
+
npm install -g ./search-rotation-0.5.0.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
|
|
package/docs/distribution.md
CHANGED
|
@@ -7,11 +7,11 @@ what still needs attention. Snapshot: 2026-09-26, after the account rename from
|
|
|
7
7
|
|
|
8
8
|
| Channel | Entry | Kept current by | State |
|
|
9
9
|
| --- | --- | --- | --- |
|
|
10
|
-
| npm | `search-rotation` | release workflow, trusted publishing | 0.4.
|
|
10
|
+
| npm | `search-rotation` | release workflow, trusted publishing | 0.4.11 |
|
|
11
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 |
|
|
12
|
+
| Official MCP Registry | `io.github.robin-bially/search-rotation` | release workflow, GitHub OIDC | 0.4.11; the old `io.github.localfoundry/search-rotation` stays frozen at 0.4.10 because the registry has no unpublish |
|
|
13
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
|
|
14
|
+
| 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
15
|
| mcpservers.org | [submission](https://mcpservers.org/de/submit) from 2026-09-26 | reviewed by the site | in review, up to two weeks |
|
|
16
16
|
| PulseMCP | — | ingests the official registry | submissions paused, not listed yet |
|
|
17
17
|
| [punkpeye/awesome-mcp-servers](https://github.com/punkpeye/awesome-mcp-servers/pull/15171) | Search & Data Extraction | maintainers | pull request open, listing checks done |
|
|
@@ -37,14 +37,14 @@ Four details are easy to forget:
|
|
|
37
37
|
- `server.json` and `mcpName` must name the same server; `test/registry-manifest.test.ts` fails when they drift apart.
|
|
38
38
|
- The registry limits `description` to 100 characters.
|
|
39
39
|
- 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
|
|
40
|
+
- 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
41
|
|
|
42
42
|
## Recurring checks
|
|
43
43
|
|
|
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
|
|
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. 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
45
|
- Dependency and tap maintenance runs outside this repository on a biweekly schedule.
|
|
46
46
|
- 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
|
|
47
|
+
- Open externally: mcpservers.org reviews the submission, PulseMCP is paused, and the three list pull requests wait for their maintainers. Glama has re-crawled the repository after the rename.
|
|
48
48
|
|
|
49
49
|
## Deliberately not used
|
|
50
50
|
|
package/docs/operations.md
CHANGED
|
@@ -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 `
|
|
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
|
|
package/docs/release-notes.md
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.5.0",
|
|
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: "
|
|
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: "
|
|
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": "
|
|
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.
|
|
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.
|
|
38
|
-
"tools.
|
|
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": "
|
|
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.
|
|
196
|
-
"tools.fetch_url": "Fetches a specific webpage and returns its content as Markdown. Uses a separate provider rotation for text extraction.",
|
|
197
|
-
"tools.
|
|
198
|
-
"tools.
|
|
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",
|