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 +4 -11
- package/dist/multi-proxy.js +18 -42
- package/dist/proxy.js +10 -40
- package/package.json +1 -1
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('
|
|
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');
|
package/dist/multi-proxy.js
CHANGED
|
@@ -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
|
-
//
|
|
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 —
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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(
|
|
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
|
-
//
|
|
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
|
-
//
|
|
111
|
-
|
|
112
|
-
this.tools =
|
|
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 —
|
|
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