@dyingc/brave-search-mcp-server 2.0.70 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.js +49 -0
- package/dist/index.js +6 -2
- package/dist/utils/apiKeyManager.js +115 -13
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -37,6 +37,25 @@ export const configSchema = z.object({
|
|
|
37
37
|
.default(false)
|
|
38
38
|
.describe('Whether the server should be stateless')
|
|
39
39
|
.optional(),
|
|
40
|
+
apiKeySelectionStrategy: z
|
|
41
|
+
.enum(['greedy-max', 'greedy-min', 'soft-max'])
|
|
42
|
+
.default('soft-max')
|
|
43
|
+
.describe('API key selection strategy')
|
|
44
|
+
.optional(),
|
|
45
|
+
apiKeyTemperature: z
|
|
46
|
+
.number()
|
|
47
|
+
.min(0)
|
|
48
|
+
.max(5.0)
|
|
49
|
+
.default(1.0)
|
|
50
|
+
.describe('Temperature for soft-max strategy (0-5.0, 0 = hard limit)')
|
|
51
|
+
.optional(),
|
|
52
|
+
apiKeyExplorationRate: z
|
|
53
|
+
.number()
|
|
54
|
+
.min(0)
|
|
55
|
+
.max(1.0)
|
|
56
|
+
.default(0.1)
|
|
57
|
+
.describe('Epsilon for epsilon-greedy exploration (0-1, 0 = no exploration, 1 = always random)')
|
|
58
|
+
.optional(),
|
|
40
59
|
});
|
|
41
60
|
const state = {
|
|
42
61
|
transport: 'stdio',
|
|
@@ -51,6 +70,9 @@ const state = {
|
|
|
51
70
|
retryMaxAttempts: 5,
|
|
52
71
|
retryBaseDelay: 1000,
|
|
53
72
|
retryMaxDelay: 30000,
|
|
73
|
+
apiKeySelectionStrategy: 'soft-max',
|
|
74
|
+
apiKeyTemperature: 1.0,
|
|
75
|
+
apiKeyExplorationRate: 0.1,
|
|
54
76
|
};
|
|
55
77
|
export function isToolPermittedByUser(toolName) {
|
|
56
78
|
return state.enabledTools.length > 0
|
|
@@ -101,6 +123,9 @@ export function getOptions() {
|
|
|
101
123
|
.option('--retry-max-attempts <number>', 'Maximum retry attempts for rate-limited requests', process.env.BRAVE_MCP_MAX_RETRIES ?? '5')
|
|
102
124
|
.option('--retry-base-delay <number>', 'Base delay for retry in milliseconds', process.env.BRAVE_MCP_RETRY_BASE_DELAY ?? '1000')
|
|
103
125
|
.option('--retry-max-delay <number>', 'Maximum delay for retry in milliseconds', process.env.BRAVE_MCP_RETRY_MAX_DELAY ?? '30000')
|
|
126
|
+
.option('--api-key-selection-strategy <strategy>', 'API key selection strategy (greedy-max, greedy-min, soft-max)', process.env.BRAVE_API_KEY_SELECTION_STRATEGY ?? 'soft-max')
|
|
127
|
+
.option('--api-key-temperature <number>', 'Temperature for soft-max strategy (0-5.0, default: 1.0, 0 = hard limit)', process.env.BRAVE_API_KEY_TEMPERATURE ?? '1.0')
|
|
128
|
+
.option('--api-key-exploration-rate <number>', 'Epsilon for epsilon-greedy exploration (0-1, default: 0.1)', process.env.BRAVE_API_KEY_EXPLORATION_RATE ?? '0.1')
|
|
104
129
|
.allowUnknownOption()
|
|
105
130
|
.parse(process.argv);
|
|
106
131
|
const options = program.opts();
|
|
@@ -150,6 +175,27 @@ export function getOptions() {
|
|
|
150
175
|
return false;
|
|
151
176
|
}
|
|
152
177
|
options.retryMaxDelay = retryMaxDelay;
|
|
178
|
+
// Validate API key selection strategy
|
|
179
|
+
const validStrategies = ['greedy-max', 'greedy-min', 'soft-max'];
|
|
180
|
+
if (!validStrategies.includes(options.apiKeySelectionStrategy)) {
|
|
181
|
+
console.error(`Invalid --api-key-selection-strategy value: '${options.apiKeySelectionStrategy}'. Must be one of: ${validStrategies.join(', ')}`);
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
// Validate temperature (range 0-5.0)
|
|
185
|
+
const temperature = parseFloat(options.apiKeyTemperature);
|
|
186
|
+
if (isNaN(temperature) || temperature < 0 || temperature > 5.0) {
|
|
187
|
+
console.error(`Invalid --api-key-temperature value: '${options.apiKeyTemperature}'. Must be a number between 0 and 5.0`);
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
options.apiKeyTemperature = temperature;
|
|
191
|
+
options.apiKeySelectionStrategy = options.apiKeySelectionStrategy;
|
|
192
|
+
// Validate exploration rate (range 0-1)
|
|
193
|
+
const explorationRate = parseFloat(options.apiKeyExplorationRate ?? '0.1');
|
|
194
|
+
if (isNaN(explorationRate) || explorationRate < 0 || explorationRate > 1) {
|
|
195
|
+
console.error(`Invalid --api-key-exploration-rate value: '${options.apiKeyExplorationRate}'. Must be a number between 0 and 1`);
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
options.apiKeyExplorationRate = explorationRate;
|
|
153
199
|
if (options.transport === 'http') {
|
|
154
200
|
if (options.port < 1 || options.port > 65535) {
|
|
155
201
|
console.error(`Invalid --port value: '${options.port}'. Must be a valid port number between 1 and 65535.`);
|
|
@@ -174,6 +220,9 @@ export function getOptions() {
|
|
|
174
220
|
state.retryMaxAttempts = options.retryMaxAttempts;
|
|
175
221
|
state.retryBaseDelay = options.retryBaseDelay;
|
|
176
222
|
state.retryMaxDelay = options.retryMaxDelay;
|
|
223
|
+
state.apiKeySelectionStrategy = options.apiKeySelectionStrategy;
|
|
224
|
+
state.apiKeyTemperature = options.apiKeyTemperature;
|
|
225
|
+
state.apiKeyExplorationRate = options.apiKeyExplorationRate;
|
|
177
226
|
state.ready = true;
|
|
178
227
|
return options;
|
|
179
228
|
}
|
package/dist/index.js
CHANGED
|
@@ -8,8 +8,12 @@ async function main() {
|
|
|
8
8
|
console.error('Invalid configuration');
|
|
9
9
|
process.exit(1);
|
|
10
10
|
}
|
|
11
|
-
// Initialize API Key Manager with parsed keys
|
|
12
|
-
initApiKeyManager(options.braveApiKeys
|
|
11
|
+
// Initialize API Key Manager with parsed keys and selection strategy
|
|
12
|
+
initApiKeyManager(options.braveApiKeys, {
|
|
13
|
+
selectionStrategy: options.apiKeySelectionStrategy,
|
|
14
|
+
temperature: options.apiKeyTemperature,
|
|
15
|
+
explorationRate: options.apiKeyExplorationRate,
|
|
16
|
+
});
|
|
13
17
|
// default to stdio server unless http is explicitly requested
|
|
14
18
|
if (options.transport === 'http') {
|
|
15
19
|
httpServer.start();
|
|
@@ -12,9 +12,17 @@ const DEFAULT_PER_SECOND_LIMIT = 1;
|
|
|
12
12
|
const STATE_FILE = path.join(os.homedir(), '.brave-mcp', 'state.json');
|
|
13
13
|
class ApiKeyManager {
|
|
14
14
|
keys;
|
|
15
|
-
|
|
15
|
+
selectionStrategy;
|
|
16
|
+
temperature;
|
|
17
|
+
explorationRate; // Probability of random exploration
|
|
18
|
+
constructor(apiKeys, options = {}) {
|
|
16
19
|
this.keys = new Map();
|
|
17
20
|
const savedStates = this.loadState();
|
|
21
|
+
const { initialQuota = FREE_TIER_MONTHLY_QUOTA, selectionStrategy = 'soft-max', temperature = 1.0, explorationRate = 0.1, // Default 10% exploration rate
|
|
22
|
+
} = options;
|
|
23
|
+
this.selectionStrategy = selectionStrategy;
|
|
24
|
+
this.temperature = temperature;
|
|
25
|
+
this.explorationRate = explorationRate;
|
|
18
26
|
for (const key of apiKeys) {
|
|
19
27
|
const keyHash = key.slice(0, 8);
|
|
20
28
|
const savedRemaining = savedStates.get(keyHash);
|
|
@@ -49,7 +57,6 @@ class ApiKeyManager {
|
|
|
49
57
|
result.set(keyHash, info.remaining);
|
|
50
58
|
}
|
|
51
59
|
}
|
|
52
|
-
console.log(`[ApiKeyManager] Loaded state for ${result.size} keys from ${STATE_FILE}`);
|
|
53
60
|
return result;
|
|
54
61
|
}
|
|
55
62
|
catch (err) {
|
|
@@ -85,25 +92,121 @@ class ApiKeyManager {
|
|
|
85
92
|
}
|
|
86
93
|
/**
|
|
87
94
|
* Select the best available key based on remaining quota
|
|
95
|
+
* Uses epsilon-greedy strategy: explore randomly with probability epsilon,
|
|
96
|
+
* otherwise exploit using the configured selection strategy
|
|
88
97
|
* Returns null if no valid keys available
|
|
89
98
|
*/
|
|
90
99
|
selectBestKey() {
|
|
91
100
|
// Filter valid keys with remaining quota and not in use
|
|
92
101
|
const availableKeys = Array.from(this.keys.values()).filter((k) => k.isValid && k.remainingQuota > 0 && !k.inUse);
|
|
102
|
+
// No available keys
|
|
93
103
|
if (availableKeys.length === 0) {
|
|
94
104
|
return null;
|
|
95
105
|
}
|
|
96
|
-
//
|
|
97
|
-
availableKeys.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
// Single available key - return directly
|
|
107
|
+
if (availableKeys.length === 1) {
|
|
108
|
+
availableKeys[0].inUse = true;
|
|
109
|
+
return availableKeys[0].key;
|
|
110
|
+
}
|
|
111
|
+
let selected;
|
|
112
|
+
// Epsilon-greedy exploration: random selection with probability epsilon
|
|
113
|
+
if (Math.random() < this.explorationRate) {
|
|
114
|
+
selected = availableKeys[Math.floor(Math.random() * availableKeys.length)];
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
switch (this.selectionStrategy) {
|
|
118
|
+
case 'greedy-min':
|
|
119
|
+
selected = this.selectGreedyMin(availableKeys);
|
|
120
|
+
break;
|
|
121
|
+
case 'soft-max':
|
|
122
|
+
selected = this.selectKeyWithSoftMax(availableKeys, this.temperature);
|
|
123
|
+
break;
|
|
124
|
+
case 'greedy-max':
|
|
125
|
+
default:
|
|
126
|
+
selected = this.selectGreedyMax(availableKeys);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// selected should never be null here since we checked availableKeys.length >= 2
|
|
131
|
+
if (!selected) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
104
134
|
selected.inUse = true;
|
|
105
135
|
return selected.key;
|
|
106
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Select key with maximum remaining quota (greedy-max)
|
|
139
|
+
*/
|
|
140
|
+
selectGreedyMax(keys) {
|
|
141
|
+
keys.sort((a, b) => b.remainingQuota - a.remainingQuota);
|
|
142
|
+
const maxQuota = keys[0].remainingQuota;
|
|
143
|
+
const topKeys = keys.filter((k) => k.remainingQuota === maxQuota);
|
|
144
|
+
return topKeys.length > 1 ? topKeys[Math.floor(Math.random() * topKeys.length)] : topKeys[0];
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Select key with minimum remaining quota (greedy-min)
|
|
148
|
+
*/
|
|
149
|
+
selectGreedyMin(keys) {
|
|
150
|
+
keys.sort((a, b) => a.remainingQuota - b.remainingQuota);
|
|
151
|
+
const minQuota = keys[0].remainingQuota;
|
|
152
|
+
const bottomKeys = keys.filter((k) => k.remainingQuota === minQuota);
|
|
153
|
+
return bottomKeys.length > 1
|
|
154
|
+
? bottomKeys[Math.floor(Math.random() * bottomKeys.length)]
|
|
155
|
+
: bottomKeys[0];
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Calculate soft max probabilities for key selection
|
|
159
|
+
* Uses standard soft max formula: softmax_i(z;T) = exp(z_i/T) / sum(exp(z_j/T))
|
|
160
|
+
* where z_i = -remainingQuota (lower remaining = higher selection probability)
|
|
161
|
+
*
|
|
162
|
+
* Example with T=1:
|
|
163
|
+
* - Key with 1 remaining: score = -1, exp(-1) ≈ 0.368
|
|
164
|
+
* - Key with 10 remaining: score = -10, exp(-10) ≈ 0.000045
|
|
165
|
+
* - Key with 1 remaining has much higher selection probability
|
|
166
|
+
*
|
|
167
|
+
* Probabilities always sum to 1 by definition of softmax formula
|
|
168
|
+
*/
|
|
169
|
+
calculateSoftMaxProbabilities(keys, temperature) {
|
|
170
|
+
// Use negative remaining quota as score: lower remaining = higher score
|
|
171
|
+
const scores = keys.map((k) => -k.remainingQuota);
|
|
172
|
+
// Standard soft max: scale by temperature
|
|
173
|
+
const scaledScores = scores.map((s) => s / temperature);
|
|
174
|
+
// Numerical stability: subtract max value
|
|
175
|
+
const maxScore = Math.max(...scaledScores);
|
|
176
|
+
const expScores = scaledScores.map((s) => Math.exp(s - maxScore));
|
|
177
|
+
const sumExp = expScores.reduce((sum, exp) => sum + exp, 0);
|
|
178
|
+
const probabilities = expScores.map((exp) => exp / sumExp);
|
|
179
|
+
return keys.map((key, index) => ({
|
|
180
|
+
key,
|
|
181
|
+
probability: probabilities[index],
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Select key using soft max probability distribution
|
|
186
|
+
* temperature = 0: hard limit (equivalent to greedy-min)
|
|
187
|
+
*/
|
|
188
|
+
selectKeyWithSoftMax(keys, temperature) {
|
|
189
|
+
// Empty array
|
|
190
|
+
if (keys.length === 0) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
// temperature = 0: hard limit, equivalent to greedy-min
|
|
194
|
+
if (temperature === 0) {
|
|
195
|
+
return this.selectGreedyMin(keys);
|
|
196
|
+
}
|
|
197
|
+
const keyProbabilities = this.calculateSoftMaxProbabilities(keys, temperature);
|
|
198
|
+
// Weighted random sampling (roulette wheel selection)
|
|
199
|
+
const random = Math.random();
|
|
200
|
+
let cumulativeProbability = 0;
|
|
201
|
+
for (const { key, probability } of keyProbabilities) {
|
|
202
|
+
cumulativeProbability += probability;
|
|
203
|
+
if (random <= cumulativeProbability) {
|
|
204
|
+
return key;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Fallback to last key
|
|
208
|
+
return keyProbabilities[keyProbabilities.length - 1].key;
|
|
209
|
+
}
|
|
107
210
|
/**
|
|
108
211
|
* Release a key after request completion
|
|
109
212
|
*/
|
|
@@ -142,7 +245,6 @@ class ApiKeyManager {
|
|
|
142
245
|
state.inUse = false;
|
|
143
246
|
// Save state after update
|
|
144
247
|
this.saveState();
|
|
145
|
-
console.debug(`[ApiKeyManager] Key ${apiKey.slice(0, 8)}... quota: ${state.remainingQuota}/${state.monthLimit}`);
|
|
146
248
|
}
|
|
147
249
|
/**
|
|
148
250
|
* Mark a key as invalid (e.g., 401 Unauthorized)
|
|
@@ -180,8 +282,8 @@ class ApiKeyManager {
|
|
|
180
282
|
}
|
|
181
283
|
// Singleton instance
|
|
182
284
|
let apiKeyManagerInstance = null;
|
|
183
|
-
export function initApiKeyManager(apiKeys) {
|
|
184
|
-
apiKeyManagerInstance = new ApiKeyManager(apiKeys);
|
|
285
|
+
export function initApiKeyManager(apiKeys, options) {
|
|
286
|
+
apiKeyManagerInstance = new ApiKeyManager(apiKeys, options);
|
|
185
287
|
}
|
|
186
288
|
export function getApiKeyManager() {
|
|
187
289
|
return apiKeyManagerInstance;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@dyingc/brave-search-mcp-server",
|
|
3
3
|
"mcpName": "io.github.brave/brave-search-mcp-server",
|
|
4
4
|
"private": false,
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.1.1",
|
|
6
6
|
"description": "Brave Search MCP Server: web results, images, videos, rich results, AI summaries, and more.",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"api",
|