@dyingc/brave-search-mcp-server 2.0.70 → 2.1.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/dist/config.js CHANGED
@@ -37,6 +37,18 @@ 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(),
40
52
  });
41
53
  const state = {
42
54
  transport: 'stdio',
@@ -51,6 +63,8 @@ const state = {
51
63
  retryMaxAttempts: 5,
52
64
  retryBaseDelay: 1000,
53
65
  retryMaxDelay: 30000,
66
+ apiKeySelectionStrategy: 'soft-max',
67
+ apiKeyTemperature: 1.0,
54
68
  };
55
69
  export function isToolPermittedByUser(toolName) {
56
70
  return state.enabledTools.length > 0
@@ -101,6 +115,8 @@ export function getOptions() {
101
115
  .option('--retry-max-attempts <number>', 'Maximum retry attempts for rate-limited requests', process.env.BRAVE_MCP_MAX_RETRIES ?? '5')
102
116
  .option('--retry-base-delay <number>', 'Base delay for retry in milliseconds', process.env.BRAVE_MCP_RETRY_BASE_DELAY ?? '1000')
103
117
  .option('--retry-max-delay <number>', 'Maximum delay for retry in milliseconds', process.env.BRAVE_MCP_RETRY_MAX_DELAY ?? '30000')
118
+ .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')
119
+ .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')
104
120
  .allowUnknownOption()
105
121
  .parse(process.argv);
106
122
  const options = program.opts();
@@ -150,6 +166,20 @@ export function getOptions() {
150
166
  return false;
151
167
  }
152
168
  options.retryMaxDelay = retryMaxDelay;
169
+ // Validate API key selection strategy
170
+ const validStrategies = ['greedy-max', 'greedy-min', 'soft-max'];
171
+ if (!validStrategies.includes(options.apiKeySelectionStrategy)) {
172
+ console.error(`Invalid --api-key-selection-strategy value: '${options.apiKeySelectionStrategy}'. Must be one of: ${validStrategies.join(', ')}`);
173
+ return false;
174
+ }
175
+ // Validate temperature (range 0-5.0)
176
+ const temperature = parseFloat(options.apiKeyTemperature);
177
+ if (isNaN(temperature) || temperature < 0 || temperature > 5.0) {
178
+ console.error(`Invalid --api-key-temperature value: '${options.apiKeyTemperature}'. Must be a number between 0 and 5.0`);
179
+ return false;
180
+ }
181
+ options.apiKeyTemperature = temperature;
182
+ options.apiKeySelectionStrategy = options.apiKeySelectionStrategy;
153
183
  if (options.transport === 'http') {
154
184
  if (options.port < 1 || options.port > 65535) {
155
185
  console.error(`Invalid --port value: '${options.port}'. Must be a valid port number between 1 and 65535.`);
@@ -174,6 +204,8 @@ export function getOptions() {
174
204
  state.retryMaxAttempts = options.retryMaxAttempts;
175
205
  state.retryBaseDelay = options.retryBaseDelay;
176
206
  state.retryMaxDelay = options.retryMaxDelay;
207
+ state.apiKeySelectionStrategy = options.apiKeySelectionStrategy;
208
+ state.apiKeyTemperature = options.apiKeyTemperature;
177
209
  state.ready = true;
178
210
  return options;
179
211
  }
package/dist/index.js CHANGED
@@ -8,8 +8,11 @@ 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
+ });
13
16
  // default to stdio server unless http is explicitly requested
14
17
  if (options.transport === 'http') {
15
18
  httpServer.start();
@@ -12,9 +12,14 @@ 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
+ constructor(apiKeys, options = {}) {
16
18
  this.keys = new Map();
17
19
  const savedStates = this.loadState();
20
+ const { initialQuota = FREE_TIER_MONTHLY_QUOTA, selectionStrategy = 'soft-max', temperature = 1.0, } = options;
21
+ this.selectionStrategy = selectionStrategy;
22
+ this.temperature = temperature;
18
23
  for (const key of apiKeys) {
19
24
  const keyHash = key.slice(0, 8);
20
25
  const savedRemaining = savedStates.get(keyHash);
@@ -49,7 +54,6 @@ class ApiKeyManager {
49
54
  result.set(keyHash, info.remaining);
50
55
  }
51
56
  }
52
- console.log(`[ApiKeyManager] Loaded state for ${result.size} keys from ${STATE_FILE}`);
53
57
  return result;
54
58
  }
55
59
  catch (err) {
@@ -90,20 +94,104 @@ class ApiKeyManager {
90
94
  selectBestKey() {
91
95
  // Filter valid keys with remaining quota and not in use
92
96
  const availableKeys = Array.from(this.keys.values()).filter((k) => k.isValid && k.remainingQuota > 0 && !k.inUse);
97
+ // No available keys
93
98
  if (availableKeys.length === 0) {
94
99
  return null;
95
100
  }
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
101
+ // Single available key - return directly
102
+ if (availableKeys.length === 1) {
103
+ availableKeys[0].inUse = true;
104
+ return availableKeys[0].key;
105
+ }
106
+ let selected;
107
+ switch (this.selectionStrategy) {
108
+ case 'greedy-min':
109
+ selected = this.selectGreedyMin(availableKeys);
110
+ break;
111
+ case 'soft-max':
112
+ selected = this.selectKeyWithSoftMax(availableKeys, this.temperature);
113
+ break;
114
+ case 'greedy-max':
115
+ default:
116
+ selected = this.selectGreedyMax(availableKeys);
117
+ break;
118
+ }
119
+ // selected should never be null here since we checked availableKeys.length >= 2
120
+ if (!selected) {
121
+ return null;
122
+ }
104
123
  selected.inUse = true;
105
124
  return selected.key;
106
125
  }
126
+ /**
127
+ * Select key with maximum remaining quota (greedy-max)
128
+ */
129
+ selectGreedyMax(keys) {
130
+ keys.sort((a, b) => b.remainingQuota - a.remainingQuota);
131
+ const maxQuota = keys[0].remainingQuota;
132
+ const topKeys = keys.filter((k) => k.remainingQuota === maxQuota);
133
+ return topKeys.length > 1 ? topKeys[Math.floor(Math.random() * topKeys.length)] : topKeys[0];
134
+ }
135
+ /**
136
+ * Select key with minimum remaining quota (greedy-min)
137
+ */
138
+ selectGreedyMin(keys) {
139
+ keys.sort((a, b) => a.remainingQuota - b.remainingQuota);
140
+ const minQuota = keys[0].remainingQuota;
141
+ const bottomKeys = keys.filter((k) => k.remainingQuota === minQuota);
142
+ return bottomKeys.length > 1
143
+ ? bottomKeys[Math.floor(Math.random() * bottomKeys.length)]
144
+ : bottomKeys[0];
145
+ }
146
+ /**
147
+ * Calculate soft max probabilities for key selection
148
+ * Uses total normalization + standard soft max formula
149
+ */
150
+ calculateSoftMaxProbabilities(keys, temperature) {
151
+ // Calculate total quota for normalization
152
+ const totalQuota = keys.reduce((sum, k) => sum + k.remainingQuota, 0);
153
+ // Normalize by total (sum of normalized values = 1), then invert (lower quota = higher score)
154
+ // normalized_quota = quota / total_quota ∈ [0, 1]
155
+ // score = -normalized_quota ∈ [-1, 0], lower quota gives score closer to 0
156
+ const scores = keys.map((k) => -k.remainingQuota / totalQuota);
157
+ // Standard soft max: scale by temperature
158
+ const scaledScores = scores.map((s) => s / temperature);
159
+ // Numerical stability: subtract max value
160
+ const maxScore = Math.max(...scaledScores);
161
+ const expScores = scaledScores.map((s) => Math.exp(s - maxScore));
162
+ const sumExp = expScores.reduce((sum, exp) => sum + exp, 0);
163
+ const probabilities = expScores.map((exp) => exp / sumExp);
164
+ return keys.map((key, index) => ({
165
+ key,
166
+ probability: probabilities[index],
167
+ }));
168
+ }
169
+ /**
170
+ * Select key using soft max probability distribution
171
+ * temperature = 0: hard limit (equivalent to greedy-min)
172
+ */
173
+ selectKeyWithSoftMax(keys, temperature) {
174
+ // Empty array
175
+ if (keys.length === 0) {
176
+ return null;
177
+ }
178
+ // temperature = 0: hard limit, equivalent to greedy-min
179
+ if (temperature === 0) {
180
+ return this.selectGreedyMin(keys);
181
+ }
182
+ const keyProbabilities = this.calculateSoftMaxProbabilities(keys, temperature);
183
+ // Weighted random sampling (roulette wheel selection)
184
+ const random = Math.random();
185
+ let cumulativeProbability = 0;
186
+ for (const { key, probability } of keyProbabilities) {
187
+ cumulativeProbability += probability;
188
+ if (random <= cumulativeProbability) {
189
+ return key;
190
+ }
191
+ }
192
+ // Fallback to last key
193
+ return keyProbabilities[keyProbabilities.length - 1].key;
194
+ }
107
195
  /**
108
196
  * Release a key after request completion
109
197
  */
@@ -142,7 +230,6 @@ class ApiKeyManager {
142
230
  state.inUse = false;
143
231
  // Save state after update
144
232
  this.saveState();
145
- console.debug(`[ApiKeyManager] Key ${apiKey.slice(0, 8)}... quota: ${state.remainingQuota}/${state.monthLimit}`);
146
233
  }
147
234
  /**
148
235
  * Mark a key as invalid (e.g., 401 Unauthorized)
@@ -180,8 +267,8 @@ class ApiKeyManager {
180
267
  }
181
268
  // Singleton instance
182
269
  let apiKeyManagerInstance = null;
183
- export function initApiKeyManager(apiKeys) {
184
- apiKeyManagerInstance = new ApiKeyManager(apiKeys);
270
+ export function initApiKeyManager(apiKeys, options) {
271
+ apiKeyManagerInstance = new ApiKeyManager(apiKeys, options);
185
272
  }
186
273
  export function getApiKeyManager() {
187
274
  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.0",
6
6
  "description": "Brave Search MCP Server: web results, images, videos, rich results, AI summaries, and more.",
7
7
  "keywords": [
8
8
  "api",