@dyingc/brave-search-mcp-server 2.1.0 → 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
@@ -49,6 +49,13 @@ export const configSchema = z.object({
49
49
  .default(1.0)
50
50
  .describe('Temperature for soft-max strategy (0-5.0, 0 = hard limit)')
51
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(),
52
59
  });
53
60
  const state = {
54
61
  transport: 'stdio',
@@ -65,6 +72,7 @@ const state = {
65
72
  retryMaxDelay: 30000,
66
73
  apiKeySelectionStrategy: 'soft-max',
67
74
  apiKeyTemperature: 1.0,
75
+ apiKeyExplorationRate: 0.1,
68
76
  };
69
77
  export function isToolPermittedByUser(toolName) {
70
78
  return state.enabledTools.length > 0
@@ -117,6 +125,7 @@ export function getOptions() {
117
125
  .option('--retry-max-delay <number>', 'Maximum delay for retry in milliseconds', process.env.BRAVE_MCP_RETRY_MAX_DELAY ?? '30000')
118
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')
119
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')
120
129
  .allowUnknownOption()
121
130
  .parse(process.argv);
122
131
  const options = program.opts();
@@ -180,6 +189,13 @@ export function getOptions() {
180
189
  }
181
190
  options.apiKeyTemperature = temperature;
182
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;
183
199
  if (options.transport === 'http') {
184
200
  if (options.port < 1 || options.port > 65535) {
185
201
  console.error(`Invalid --port value: '${options.port}'. Must be a valid port number between 1 and 65535.`);
@@ -206,6 +222,7 @@ export function getOptions() {
206
222
  state.retryMaxDelay = options.retryMaxDelay;
207
223
  state.apiKeySelectionStrategy = options.apiKeySelectionStrategy;
208
224
  state.apiKeyTemperature = options.apiKeyTemperature;
225
+ state.apiKeyExplorationRate = options.apiKeyExplorationRate;
209
226
  state.ready = true;
210
227
  return options;
211
228
  }
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ async function main() {
12
12
  initApiKeyManager(options.braveApiKeys, {
13
13
  selectionStrategy: options.apiKeySelectionStrategy,
14
14
  temperature: options.apiKeyTemperature,
15
+ explorationRate: options.apiKeyExplorationRate,
15
16
  });
16
17
  // default to stdio server unless http is explicitly requested
17
18
  if (options.transport === 'http') {
@@ -14,12 +14,15 @@ class ApiKeyManager {
14
14
  keys;
15
15
  selectionStrategy;
16
16
  temperature;
17
+ explorationRate; // Probability of random exploration
17
18
  constructor(apiKeys, options = {}) {
18
19
  this.keys = new Map();
19
20
  const savedStates = this.loadState();
20
- const { initialQuota = FREE_TIER_MONTHLY_QUOTA, selectionStrategy = 'soft-max', temperature = 1.0, } = options;
21
+ const { initialQuota = FREE_TIER_MONTHLY_QUOTA, selectionStrategy = 'soft-max', temperature = 1.0, explorationRate = 0.1, // Default 10% exploration rate
22
+ } = options;
21
23
  this.selectionStrategy = selectionStrategy;
22
24
  this.temperature = temperature;
25
+ this.explorationRate = explorationRate;
23
26
  for (const key of apiKeys) {
24
27
  const keyHash = key.slice(0, 8);
25
28
  const savedRemaining = savedStates.get(keyHash);
@@ -89,6 +92,8 @@ class ApiKeyManager {
89
92
  }
90
93
  /**
91
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
92
97
  * Returns null if no valid keys available
93
98
  */
94
99
  selectBestKey() {
@@ -104,17 +109,23 @@ class ApiKeyManager {
104
109
  return availableKeys[0].key;
105
110
  }
106
111
  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;
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
+ }
118
129
  }
119
130
  // selected should never be null here since we checked availableKeys.length >= 2
120
131
  if (!selected) {
@@ -145,15 +156,19 @@ class ApiKeyManager {
145
156
  }
146
157
  /**
147
158
  * Calculate soft max probabilities for key selection
148
- * Uses total normalization + standard soft max formula
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
149
168
  */
150
169
  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);
170
+ // Use negative remaining quota as score: lower remaining = higher score
171
+ const scores = keys.map((k) => -k.remainingQuota);
157
172
  // Standard soft max: scale by temperature
158
173
  const scaledScores = scores.map((s) => s / temperature);
159
174
  // Numerical stability: subtract max value
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.1.0",
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",