@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 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
- constructor(apiKeys, initialQuota = FREE_TIER_MONTHLY_QUOTA) {
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
- // Sort by remaining quota (descending)
97
- availableKeys.sort((a, b) => b.remainingQuota - a.remainingQuota);
98
- // Get keys with max quota
99
- const maxQuota = availableKeys[0].remainingQuota;
100
- const topKeys = availableKeys.filter((k) => k.remainingQuota === maxQuota);
101
- // Random selection if tied
102
- const selected = topKeys.length > 1 ? topKeys[Math.floor(Math.random() * topKeys.length)] : topKeys[0];
103
- // Mark as in-use
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.0.70",
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",