search-rotation 0.4.10 → 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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Robin Bially (LocalFoundry)
3
+ Copyright (c) 2026 Robin Bially
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  **One MCP server for web search and page extraction across multiple providers.**
7
7
 
8
- Built by [Robin Bially](https://github.com/localfoundry) under the LocalFoundry label.
8
+ Built by [Robin Bially](https://github.com/robin-bially).
9
9
 
10
10
  Rotate across available quotas, automatically fail over when a provider is unavailable, and return consistent results to your AI assistant. A local dashboard lets you manage API keys, reorder engines, check quotas, and inspect request history.
11
11
 
@@ -25,23 +25,25 @@ npx -y search-rotation --http --open
25
25
  ```
26
26
 
27
27
  Requires **Node.js 20.3+**. For MCP over stdio, configure your client to run
28
- `npx -y search-rotation` with no arguments.
28
+ `npx -y search-rotation` with no arguments. The server is also listed in the
29
+ [official MCP Registry](https://registry.modelcontextprotocol.io/?q=io.github.robin-bially%2Fsearch-rotation)
30
+ as `io.github.robin-bially/search-rotation`.
29
31
 
30
32
  ### Homebrew
31
33
 
32
34
  ```sh
33
- brew install localfoundry/tap/search-rotation
35
+ brew install robin-bially/tap/search-rotation
34
36
  search-rotation --http --open
35
37
  ```
36
38
 
37
39
  Homebrew installs the required Node.js runtime. For MCP over stdio, configure your
38
40
  client to run `search-rotation` with no arguments. See the
39
- [LocalFoundry tap](https://github.com/localfoundry/homebrew-tap) for upgrades and details.
41
+ [Homebrew tap](https://github.com/robin-bially/homebrew-tap) for upgrades and details.
40
42
 
41
43
  ### Pinned release from GitHub
42
44
 
43
45
  ```sh
44
- npx -y --allow-git=all github:localfoundry/search-rotation-mcp#v0.4.10
46
+ npx -y --allow-git=all github:robin-bially/search-rotation-mcp#v0.5.0
45
47
  ```
46
48
 
47
49
  Use this when you want a fixed version instead of the current npm release.
@@ -59,20 +61,22 @@ Add your provider keys in the dashboard, then connect your assistant using the *
59
61
 
60
62
  ## MCP tools
61
63
 
62
- `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.
63
67
 
64
68
  The dashboard's **MCP Tools** tab explains each tool, its parameters, and copyable example calls.
65
69
 
66
70
  <picture>
67
71
  <source media="(prefers-color-scheme: dark)" srcset="docs/assets/mcp-tools-dark.png">
68
- <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">
69
73
  </picture>
70
74
 
71
75
  *Actual dashboard UI, shown in English.*
72
76
 
73
77
  ## Search time filters
74
78
 
75
- `web_search` accepts the following arguments:
79
+ `search_web` accepts the following arguments:
76
80
 
77
81
  | Parameter | Meaning |
78
82
  | --- | --- |
@@ -86,11 +90,11 @@ The dashboard's **MCP Tools** tab explains each tool, its parameters, and copyab
86
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.
87
91
 
88
92
  ```json
89
- {"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}}
90
94
  ```
91
95
 
92
96
  ```json
93
- {"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"}}
94
98
  ```
95
99
 
96
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.
@@ -117,13 +121,15 @@ After updating, reconnect your MCP client to load the new tool schema.
117
121
  ## Releasing
118
122
 
119
123
  ```sh
120
- VERSION=0.4.9 ./scripts/release.sh --publish
124
+ VERSION=0.4.10 ./scripts/release.sh --publish
121
125
  ```
122
126
 
123
- The script checks the package (`npm ci`, build, tests, `npm run smoke:package`), bumps the version, points the document pins at it, commits and tags, packs the tarball with its checksum, creates the GitHub release, updates the formula in `localfoundry/homebrew-tap` and finally waits until the npm registry serves the new version. Without `--publish` it only prepares the artifacts in `.build/releases`; `--dry-run` checks the prerequisites, `--draft` creates a draft release and `--force` tolerates a dirty tree. The tap is cloned temporarily when `TAP_DIR` is not set, so a fresh checkout is enough.
127
+ The script checks the package (`npm ci`, build, tests, `npm run smoke:package`), bumps the version, points the document pins at it, commits and tags, packs the tarball with its checksum, creates the GitHub release, updates the formula in `robin-bially/homebrew-tap` and finally waits until the npm registry serves the new version. Without `--publish` it only prepares the artifacts in `.build/releases`; `--dry-run` checks the prerequisites, `--draft` creates a draft release and `--force` tolerates a dirty tree. The tap is cloned temporarily when `TAP_DIR` is not set, so a fresh checkout is enough.
124
128
 
125
129
  Publishing to npm happens in [`.github/workflows/publish.yml`](.github/workflows/publish.yml) through trusted publishing once the GitHub release is published, so the script itself needs no npm credentials. The workflow requires a trusted publisher for `search-rotation` on npmjs.com that points at this repository and `publish.yml`.
126
130
 
131
+ The same workflow registers the version in the official MCP Registry from `server.json`. [Distribution & listings](docs/distribution.md) tracks every channel, who keeps it current, and what each one still needs.
132
+
127
133
  ## Learn more
128
134
 
129
- [Client setup](docs/clients.md) · [Operations & configuration (DE)](docs/operations.md) · [Releases](https://github.com/localfoundry/search-rotation-mcp/releases) · [CI](https://github.com/localfoundry/search-rotation-mcp/actions/workflows/ci.yml) · [MIT license](LICENSE)
135
+ [Client setup](docs/clients.md) · [Operations & configuration (DE)](docs/operations.md) · [Distribution & listings](docs/distribution.md) · [Releases](https://github.com/robin-bially/search-rotation-mcp/releases) · [CI](https://github.com/robin-bially/search-rotation-mcp/actions/workflows/ci.yml) · [MIT license](LICENSE)
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
@@ -5,7 +5,7 @@
5
5
  ## Homebrew installation
6
6
 
7
7
  ```sh
8
- brew install localfoundry/tap/search-rotation
8
+ brew install robin-bially/tap/search-rotation
9
9
  ```
10
10
 
11
11
  Homebrew installs Node.js automatically. Set your MCP client's command to
@@ -74,21 +74,21 @@ Add this entry under `mcp.servers`:
74
74
 
75
75
  ## Local install or release archive
76
76
 
77
- If installed locally, use `search-rotation` as the command. A prebuilt archive is also available under [GitHub Releases](https://github.com/localfoundry/search-rotation-mcp/releases/latest):
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.10.tgz
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
84
- `github:localfoundry/search-rotation-mcp#vX` form from the README needs `--allow-git=all`
84
+ `github:robin-bially/search-rotation-mcp#vX` form from the README needs `--allow-git=all`
85
85
  instead, because npm 12 resolves Git dependencies only when explicitly allowed.
86
86
 
87
87
  For remote HTTP access, authentication and advanced settings, see the [operations guide (German)](operations.md).
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
 
@@ -0,0 +1,53 @@
1
+ # Distribution and listings
2
+
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.
5
+
6
+ ## Channels
7
+
8
+ | Channel | Entry | Kept current by | State |
9
+ | --- | --- | --- | --- |
10
+ | npm | `search-rotation` | release workflow, trusted publishing | 0.4.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 | 0.4.11; the old `io.github.localfoundry/search-rotation` stays frozen 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 under the current account name; Glama picked up the rename by itself, the previous `RobinBially` path still resolves |
15
+ | mcpservers.org | [submission](https://mcpservers.org/de/submit) from 2026-09-26 | reviewed by the site | in review, up to two weeks |
16
+ | 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
+
21
+ Channels that build on the official registry follow a release by themselves, so a
22
+ release needs no manual work for them.
23
+
24
+ ## What a release updates
25
+
26
+ `VERSION=x.y.z ./scripts/release.sh --publish` runs the checks, bumps the version,
27
+ commits and tags, creates the GitHub release, packs the tarball with its checksum,
28
+ writes the formula in `robin-bially/homebrew-tap`, and waits until npm serves the
29
+ new version. The published release triggers
30
+ [`.github/workflows/publish.yml`](../.github/workflows/publish.yml), which pushes
31
+ the package to npm with provenance and then registers the same version in the
32
+ official MCP Registry from `server.json` and the `mcpName` field of
33
+ `package.json`.
34
+
35
+ Four details are easy to forget:
36
+
37
+ - `server.json` and `mcpName` must name the same server; `test/registry-manifest.test.ts` fails when they drift apart.
38
+ - The registry limits `description` to 100 characters.
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, 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
+ ## Recurring checks
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. 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
+ - Dependency and tap maintenance runs outside this repository on a biweekly schedule.
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, and the three list pull requests wait for their maintainers. Glama has re-crawled the repository after the rename.
48
+
49
+ ## Deliberately not used
50
+
51
+ Directories that require a new account before a submission (Glama's own form,
52
+ mcp.so) are skipped. The Glama listing exists because Glama indexes public GitHub
53
+ repositories without a submission; it picked up the repository rename by itself.
@@ -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,49 @@
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
+
28
+ ## v0.4.11 — Distribution under the robin-bially account
29
+
30
+ The GitHub account moved from `localfoundry` to `robin-bially`, so every distribution
31
+ path follows: the Homebrew tap is `robin-bially/tap`, the official MCP Registry
32
+ namespace becomes `io.github.robin-bially/search-rotation`, and the npm metadata now
33
+ points at the renamed repository. The package itself is unchanged, so there is no
34
+ behaviour change and no configuration migration.
35
+
36
+ ```sh
37
+ brew update && brew upgrade robin-bially/tap/search-rotation
38
+ ```
39
+
40
+ Or run the pinned GitHub version:
41
+
42
+ ```sh
43
+ npx -y --allow-git=all github:robin-bially/search-rotation-mcp#v0.4.11
44
+ ```
45
+
46
+
1
47
  ## v0.4.10 — npm distribution and MCP Registry metadata
2
48
 
3
49
  The package now ships through npm as well, so `npx -y search-rotation` installs the
@@ -6,13 +52,13 @@ trusted publishing, and `mcpName` plus `server.json` carry the metadata for the
6
52
  official MCP Registry.
7
53
 
8
54
  ```sh
9
- brew update && brew upgrade localfoundry/tap/search-rotation
55
+ brew update && brew upgrade robin-bially/tap/search-rotation
10
56
  ```
11
57
 
12
58
  Or run the pinned GitHub version:
13
59
 
14
60
  ```sh
15
- npx -y --allow-git=all github:localfoundry/search-rotation-mcp#v0.4.10
61
+ npx -y --allow-git=all github:robin-bially/search-rotation-mcp#v0.4.10
16
62
  ```
17
63
 
18
64
 
@@ -21,13 +67,13 @@ npx -y --allow-git=all github:localfoundry/search-rotation-mcp#v0.4.10
21
67
  Routine maintenance release: the Node type definitions move to 26.6.3. No behaviour change and no configuration migration. The GitHub release page now shows only the notes for the current version instead of the whole changelog.
22
68
 
23
69
  ```sh
24
- brew update && brew upgrade localfoundry/tap/search-rotation
70
+ brew update && brew upgrade robin-bially/tap/search-rotation
25
71
  ```
26
72
 
27
73
  Or run the pinned GitHub version:
28
74
 
29
75
  ```sh
30
- npx -y --allow-git=all github:localfoundry/search-rotation-mcp#v0.4.9
76
+ npx -y --allow-git=all github:robin-bially/search-rotation-mcp#v0.4.9
31
77
  ```
32
78
 
33
79
 
@@ -36,13 +82,13 @@ npx -y --allow-git=all github:localfoundry/search-rotation-mcp#v0.4.9
36
82
  Routine maintenance release: the MCP SDK moves to 1.30.1, Hono to 4.13.9, Zod to 4.6.5, tsx to 4.23.15 and the Node type definitions to 26.6.2, together with the matching transitive updates (jose, proxy-addr, ip-address, fast-uri, undici-types). No behaviour change and no configuration migration.
37
83
 
38
84
  ```sh
39
- brew update && brew upgrade localfoundry/tap/search-rotation
85
+ brew update && brew upgrade robin-bially/tap/search-rotation
40
86
  ```
41
87
 
42
88
  Or run the pinned GitHub version:
43
89
 
44
90
  ```sh
45
- npx -y --allow-git=all github:localfoundry/search-rotation-mcp#v0.4.8
91
+ npx -y --allow-git=all github:robin-bially/search-rotation-mcp#v0.4.8
46
92
  ```
47
93
 
48
94
 
@@ -60,11 +106,11 @@ Engine call counts are now visible with or without an API key. Search, fetch, an
60
106
  No configuration migration is required. Reconnect MCP clients after updating.
61
107
 
62
108
  ```sh
63
- brew update && brew upgrade localfoundry/tap/search-rotation
109
+ brew update && brew upgrade robin-bially/tap/search-rotation
64
110
  ```
65
111
 
66
112
  Or run the pinned GitHub version:
67
113
 
68
114
  ```sh
69
- npx -y --allow-git=all github:localfoundry/search-rotation-mcp#v0.4.7
115
+ npx -y --allow-git=all github:robin-bially/search-rotation-mcp#v0.4.7
70
116
  ```
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "search-rotation",
3
- "version": "0.4.10",
4
- "mcpName": "io.github.localfoundry/search-rotation",
3
+ "version": "0.5.0",
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",
7
7
  "license": "MIT",
8
8
  "author": {
9
9
  "name": "Robin Bially",
10
- "url": "https://github.com/localfoundry"
10
+ "url": "https://github.com/robin-bially"
11
11
  },
12
12
  "bin": {
13
13
  "search-rotation": "dist/index.js"
@@ -57,10 +57,10 @@
57
57
  },
58
58
  "repository": {
59
59
  "type": "git",
60
- "url": "git+https://github.com/localfoundry/search-rotation-mcp.git"
60
+ "url": "git+https://github.com/robin-bially/search-rotation-mcp.git"
61
61
  },
62
- "homepage": "https://github.com/localfoundry/search-rotation-mcp#readme",
62
+ "homepage": "https://github.com/robin-bially/search-rotation-mcp#readme",
63
63
  "bugs": {
64
- "url": "https://github.com/localfoundry/search-rotation-mcp/issues"
64
+ "url": "https://github.com/robin-bially/search-rotation-mcp/issues"
65
65
  }
66
66
  }
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",
package/static/index.html CHANGED
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>search-rotation · LocalFoundry</title>
6
+ <title>search-rotation</title>
7
7
  <meta name="theme-color" content="#0a0c12">
8
8
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%238b7cf7'/%3E%3Cstop offset='1' stop-color='%234dd8e6'/%3E%3C/linearGradient%3E%3C/defs%3E%3Ccircle cx='16' cy='16' r='14' fill='none' stroke='url(%23g)' stroke-width='3.5' stroke-dasharray='66 22' stroke-linecap='round'/%3E%3Ccircle cx='16' cy='16' r='4.5' fill='url(%23g)'/%3E%3C/svg%3E">
9
9
  <link rel="stylesheet" href="/style.css">
@@ -83,7 +83,7 @@
83
83
  <main class="view" id="view" aria-live="polite"></main>
84
84
 
85
85
  <footer class="footer">
86
- <span><strong>LocalFoundry</strong> · <span data-i18n="footer.text"></span></span>
86
+ <span><strong>Robin Bially</strong> · <span data-i18n="footer.text"></span></span>
87
87
  <span class="footer-meta"><code id="configpath"></code></span>
88
88
  </footer>
89
89