@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 +32 -0
- package/dist/index.js +5 -2
- package/dist/utils/apiKeyManager.js +100 -13
- package/package.json +1 -1
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
|
-
|
|
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
|
-
//
|
|
97
|
-
availableKeys.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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",
|