search-mcp-rotator 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -36,21 +36,14 @@ async function main() {
36
36
  if (args.setup) {
37
37
  const { runSetup } = await import('./setup.js');
38
38
  await runSetup(args.config);
39
- // Auto-warmup cache after setup so first MCP start is instant
40
- try {
41
- const config = await loadConfig(configPath);
42
- process.stdout.write('\nWarming tools cache for faster MCP startup...\n');
43
- const { warmupCache } = await import('./proxy.js');
44
- await warmupCache(config.providers);
45
- process.stdout.write('Cache ready.\n');
46
- }
47
- catch { }
48
39
  return;
49
40
  }
50
- // ── Warmup ─────────────────────────────────────────────────────────────
41
+ // ── Warmup ─ probe all providers in parallel & report tool counts ──────
42
+ // No disk cache is written; this exists purely to verify connectivity and
43
+ // surface schema info to the user during onboarding/troubleshooting.
51
44
  if (args.warmup) {
52
45
  const config = await loadConfig(configPath);
53
- process.stdout.write('Warming tools cache...\n');
46
+ process.stdout.write('Probing all configured providers...\n');
54
47
  const { warmupCache } = await import('./proxy.js');
55
48
  await warmupCache(config.providers);
56
49
  process.stdout.write('Done.\n');
@@ -3,30 +3,10 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
3
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
4
4
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
5
5
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
6
- import * as fs from 'fs';
7
- import * as path from 'path';
8
- import * as os from 'os';
9
6
  import { KeyPool } from './key-pool.js';
10
7
  import { ExhaustionDetector, extractCooldown } from './detector.js';
11
8
  import { AuthInjector } from './auth-injector.js';
12
9
  import { logger } from './logger.js';
13
- const CACHE_DIR = path.join(os.homedir(), '.config', 'search-mcp-rotator');
14
- const CACHE_FILE = path.join(CACHE_DIR, 'tools-cache.json');
15
- function loadToolsCache() {
16
- try {
17
- if (fs.existsSync(CACHE_FILE))
18
- return JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8'));
19
- }
20
- catch { }
21
- return {};
22
- }
23
- function saveToolsCache(cache) {
24
- try {
25
- fs.mkdirSync(CACHE_DIR, { recursive: true });
26
- fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
27
- }
28
- catch { }
29
- }
30
10
  // ── Tool name helpers ─────────────────────────────────────────────────────────
31
11
  // Prefix: "{provider}__{toolname}" — double underscore as separator
32
12
  // All provider IDs are single words without underscores, making parsing unambiguous.
@@ -65,14 +45,6 @@ export class MultiProxy {
65
45
  }
66
46
  }
67
47
  async start() {
68
- // Load cached tools for all providers — instant, no network
69
- const cache = loadToolsCache();
70
- for (const [name, state] of this.providers) {
71
- if (cache[name]?.length) {
72
- state.tools = cache[name];
73
- logger.info(`Loaded ${state.tools.length} cached tools for ${name}`);
74
- }
75
- }
76
48
  this.registerHandlers();
77
49
  const transport = new StdioServerTransport();
78
50
  await this.server.connect(transport);
@@ -94,12 +66,11 @@ export class MultiProxy {
94
66
  state.transport = new StreamableHTTPClientTransport(new URL(url), { requestInit: { headers } });
95
67
  await state.client.connect(state.transport);
96
68
  state.connected = true;
97
- // Refresh tools from upstream and update cache
69
+ // Always fetch fresh tools from upstream no disk cache. Providers can
70
+ // change/add/remove tools at any time; a stale cache would expose
71
+ // wrong schemas and missing tools to the LLM client.
98
72
  const { tools } = await state.client.listTools();
99
73
  state.tools = tools;
100
- const cache = loadToolsCache();
101
- cache[state.name] = tools;
102
- saveToolsCache(cache);
103
74
  logger.info(`Connected to upstream for ${state.name}`, {
104
75
  toolCount: tools.length,
105
76
  currentKey: state.currentKey.slice(0, 8) + '...',
@@ -142,19 +113,24 @@ export class MultiProxy {
142
113
  }
143
114
  // ── Register MCP handlers ────────────────────────────────────────────────
144
115
  registerHandlers() {
145
- // tools/list — serve all providers' tools from cache instantly
116
+ // tools/list — fetch fresh from all providers in parallel.
117
+ // Tools are kept in memory for the lifetime of this process (per-call
118
+ // refresh would be wasteful), but NEVER persisted to disk so we always
119
+ // pick up upstream tool changes on next process start.
146
120
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
121
+ // Connect all providers that haven't been connected yet — in parallel.
122
+ await Promise.allSettled([...this.providers.entries()]
123
+ .filter(([, state]) => state.tools.length === 0)
124
+ .map(async ([name, state]) => {
125
+ try {
126
+ await this.ensureConnected(state);
127
+ }
128
+ catch (e) {
129
+ logger.warn(`Could not fetch tools for ${name}: ${e.message}`);
130
+ }
131
+ }));
147
132
  const allTools = [];
148
133
  for (const [name, state] of this.providers) {
149
- // If no cache yet, connect upstream now (first-ever run)
150
- if (state.tools.length === 0) {
151
- try {
152
- await this.ensureConnected(state);
153
- }
154
- catch (e) {
155
- logger.warn(`Could not fetch tools for ${name}: ${e.message}`);
156
- }
157
- }
158
134
  for (const tool of state.tools) {
159
135
  allTools.push({
160
136
  ...tool,
package/dist/proxy.js CHANGED
@@ -3,33 +3,15 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
3
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
4
4
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
5
5
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
6
- import * as fs from 'fs';
7
- import * as path from 'path';
8
- import * as os from 'os';
9
6
  import { KeyPool } from './key-pool.js';
10
7
  import { extractCooldown } from './detector.js';
11
8
  import { AuthInjector } from './auth-injector.js';
12
9
  import { logger } from './logger.js';
13
- const CACHE_DIR = path.join(os.homedir(), '.config', 'search-mcp-rotator');
14
- const CACHE_FILE = path.join(CACHE_DIR, 'tools-cache.json');
15
- function loadToolsCache() {
16
- try {
17
- if (fs.existsSync(CACHE_FILE)) {
18
- return JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8'));
19
- }
20
- }
21
- catch { }
22
- return {};
23
- }
24
- function saveToolsCache(cache) {
25
- try {
26
- fs.mkdirSync(CACHE_DIR, { recursive: true });
27
- fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
28
- }
29
- catch { }
30
- }
10
+ // Probe upstream providers in parallel and report tool counts.
11
+ // No disk caching — providers may change tools at any time, so we always
12
+ // fetch fresh on each process start. This is a no-op for runtime behavior,
13
+ // only used by the `--warmup` CLI flag for diagnostic reporting.
31
14
  export async function warmupCache(providers) {
32
- const cache = loadToolsCache();
33
15
  const results = await Promise.allSettled(Object.entries(providers)
34
16
  .filter(([, cfg]) => cfg.enabled)
35
17
  .map(async ([name, cfg]) => {
@@ -41,14 +23,12 @@ export async function warmupCache(providers) {
41
23
  const client = new Client({ name: `${name}-warmup`, version: '1.0.0' }, { capabilities: {} });
42
24
  await client.connect(transport);
43
25
  const { tools } = await client.listTools();
44
- cache[name] = tools;
45
26
  await client.close();
46
27
  process.stdout.write(` ✓ ${name} — ${tools.length} tools\n`);
47
28
  }));
48
- saveToolsCache(cache);
49
29
  const failed = results.filter(r => r.status === 'rejected');
50
30
  if (failed.length) {
51
- failed.forEach((r, i) => process.stdout.write(` ✗ failed: ${r.reason}\n`));
31
+ failed.forEach(r => process.stdout.write(` ✗ failed: ${r.reason}\n`));
52
32
  }
53
33
  }
54
34
  export class MCPProxy {
@@ -72,13 +52,7 @@ export class MCPProxy {
72
52
  this.upstreamClient = new Client({ name: `${providerName}-rotator-client`, version: '1.0.0' }, { capabilities: {} });
73
53
  }
74
54
  async start() {
75
- // Load cached tools so tools/list responds instantly without upstream call
76
- const cache = loadToolsCache();
77
- if (cache[this.providerName]?.length) {
78
- this.tools = cache[this.providerName];
79
- logger.info(`Loaded ${this.tools.length} cached tools for ${this.providerName}`);
80
- }
81
- // Register handlers — upstream connects lazily on first actual tool call
55
+ // Register handlers upstream connects lazily on first tools/list or tools/call
82
56
  this.registerHandlers();
83
57
  const transport = new StdioServerTransport();
84
58
  await this.server.connect(transport);
@@ -107,21 +81,17 @@ export class MCPProxy {
107
81
  return;
108
82
  this.currentKey = this.keyPool.next();
109
83
  await this.connectUpstream(this.currentKey);
110
- // Refresh tools from upstream and update cache
111
- const fresh = await this.discoverTools();
112
- this.tools = fresh;
113
- const cache = loadToolsCache();
114
- cache[this.providerName] = fresh;
115
- saveToolsCache(cache);
84
+ // Always fetch fresh tools from upstream no disk cache. Providers can
85
+ // change/add/remove tools at any time.
86
+ this.tools = await this.discoverTools();
116
87
  logger.info(`Connected to upstream for ${this.providerName}`, {
117
88
  toolCount: this.tools.length,
118
89
  currentKey: this.maskKey(this.currentKey)
119
90
  });
120
91
  }
121
92
  registerHandlers() {
122
- // tools/list — serve from cache instantly, connect upstream lazily on tool call
93
+ // tools/list — fetch fresh from upstream. In-memory only, not persisted.
123
94
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
124
- // If no tools yet (first ever run), do a full upstream connect now
125
95
  if (this.tools.length === 0) {
126
96
  await this.ensureConnected();
127
97
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "search-mcp-rotator",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Transparent API key rotation proxy for MCP search providers — Exa, Firecrawl, Tavily, Linkup, Bright Data, Olostep, Dappier, Parallel",
5
5
  "type": "module",
6
6
  "bin": {