@brave/brave-search-mcp-server 1.3.3 → 1.3.5
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/README.md +1 -0
- package/dist/BraveAPI/index.js +7 -6
- package/dist/ClientLogger.js +63 -0
- package/dist/config.js +9 -1
- package/dist/protocols/http.js +42 -11
- package/dist/protocols/stdio.js +7 -4
- package/dist/server.js +7 -3
- package/dist/tools/images/index.js +4 -3
- package/dist/tools/summarizer/index.js +2 -2
- package/dist/utils.js +0 -5
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -102,6 +102,7 @@ The server supports the following environment variables:
|
|
|
102
102
|
- `BRAVE_MCP_TRANSPORT`: Transport mode ("http" or "stdio", default: "http")
|
|
103
103
|
- `BRAVE_MCP_PORT`: HTTP server port (default: 8080)
|
|
104
104
|
- `BRAVE_MCP_HOST`: HTTP server host (default: "0.0.0.0")
|
|
105
|
+
- `BRAVE_MCP_LOG_LEVEL`: Desired logging level("debug", "info", "notice", "warning", "error", "critical", "alert", or "emergency", default: "info")
|
|
105
106
|
|
|
106
107
|
### Command Line Options
|
|
107
108
|
|
package/dist/BraveAPI/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import config from '../config.js';
|
|
2
|
-
import {
|
|
2
|
+
import { stringify } from '../utils.js';
|
|
3
|
+
import ClientLogger from '../ClientLogger.js';
|
|
3
4
|
const typeToPathMap = {
|
|
4
5
|
images: '/res/v1/images/search',
|
|
5
6
|
localPois: '/res/v1/local/pois',
|
|
@@ -22,7 +23,7 @@ requestHeaders = {}) {
|
|
|
22
23
|
// Determine URL, and setup parameters
|
|
23
24
|
const url = new URL(`https://api.search.brave.com${typeToPathMap[endpoint]}`);
|
|
24
25
|
const queryParams = new URLSearchParams();
|
|
25
|
-
await log('info', `Preparing to issue request to ${url.toString()}`);
|
|
26
|
+
await ClientLogger.log('info', `Preparing to issue request to ${url.toString()}`);
|
|
26
27
|
// TODO (Sampson): Move param-construction/validation to modules
|
|
27
28
|
for (const [key, value] of Object.entries(parameters)) {
|
|
28
29
|
// The 'ids' parameter is expected to appear multiple times for multiple IDs
|
|
@@ -64,12 +65,12 @@ requestHeaders = {}) {
|
|
|
64
65
|
queryParams.set(key === 'query' ? 'q' : key, value.toString());
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
|
-
await log('debug', `Using parameters: ${queryParams.toString()}`);
|
|
68
|
+
await ClientLogger.log('debug', `Using parameters: ${queryParams.toString()}`);
|
|
68
69
|
// Issue Request
|
|
69
70
|
const urlWithParams = url.toString() + '?' + queryParams.toString();
|
|
70
71
|
const headers = { ...defaultRequestHeaders, ...requestHeaders };
|
|
71
72
|
const response = await fetch(urlWithParams, { headers });
|
|
72
|
-
await log('debug', `Received response from ${urlWithParams}`);
|
|
73
|
+
await ClientLogger.log('debug', `Received response from ${urlWithParams}`);
|
|
73
74
|
// Handle Error
|
|
74
75
|
if (!response.ok) {
|
|
75
76
|
let errorMessage = `${response.status} ${response.statusText}`;
|
|
@@ -80,13 +81,13 @@ requestHeaders = {}) {
|
|
|
80
81
|
catch (error) {
|
|
81
82
|
errorMessage += `\n${await response.text()}`;
|
|
82
83
|
}
|
|
83
|
-
await log('error', errorMessage);
|
|
84
|
+
await ClientLogger.log('error', errorMessage);
|
|
84
85
|
// TODO (Sampson): Setup proper error handling, updating state, etc.
|
|
85
86
|
throw new Error(errorMessage);
|
|
86
87
|
}
|
|
87
88
|
// Return Response
|
|
88
89
|
const responseBody = await response.json();
|
|
89
|
-
await log('debug', `Returning response: ${stringify(responseBody, true)}`);
|
|
90
|
+
await ClientLogger.log('debug', `Returning response: ${stringify(responseBody, true)}`);
|
|
90
91
|
return responseBody;
|
|
91
92
|
}
|
|
92
93
|
export default {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { LoggingLevelSchema, SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import config from './config.js';
|
|
3
|
+
let mcpServer;
|
|
4
|
+
const setServer = (server) => {
|
|
5
|
+
mcpServer = server;
|
|
6
|
+
};
|
|
7
|
+
let customLogger;
|
|
8
|
+
let currentLevel = LoggingLevelSchema.options.indexOf(config.loggingLevel);
|
|
9
|
+
/**
|
|
10
|
+
* Attempt to register a custom set level request handler.
|
|
11
|
+
* If the method is already handled by the SDK/Server, we'll ignore the error.
|
|
12
|
+
* @see https://github.com/modelcontextprotocol/typescript-sdk/issues/871
|
|
13
|
+
* @param server {Server} The server to register the handler on.
|
|
14
|
+
*/
|
|
15
|
+
export const maybeRegisterCustomSetLevelRequestHandler = (server) => {
|
|
16
|
+
try {
|
|
17
|
+
server.assertCanSetRequestHandler(SetLevelRequestSchema.shape.method.value);
|
|
18
|
+
server.setRequestHandler(SetLevelRequestSchema, async (request) => {
|
|
19
|
+
await log('info', `Setting logging level to ${request.params.level}`);
|
|
20
|
+
currentLevel = LoggingLevelSchema.options.indexOf(request.params.level);
|
|
21
|
+
return {};
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
console.error(`Failed to register custom SetLevelRequest handler. The SDK may now provide its own handler. See https://github.com/modelcontextprotocol/typescript-sdk/issues/871 for more information.`, error);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const log = async (level, message) => {
|
|
29
|
+
// If a custom logger exists, call it, and let it handle it's own log-level filtering
|
|
30
|
+
if (customLogger) {
|
|
31
|
+
await customLogger(level, message);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// If the log-level is less than the current log-level, skip it
|
|
35
|
+
if (LoggingLevelSchema.options.indexOf(level) < currentLevel) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// Fall back to default logger if no custom logger is set
|
|
39
|
+
const time = new Date().toISOString();
|
|
40
|
+
if (!mcpServer?.isConnected()) {
|
|
41
|
+
console.error(`${time} [${level}] ${message}`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
await mcpServer.server.sendLoggingMessage({ level, data: { message, time } });
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
console.error(`Error sending logging message: ${error}`);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const setLogger = (logger) => {
|
|
52
|
+
customLogger = logger;
|
|
53
|
+
};
|
|
54
|
+
const setLoggingLevel = (desiredLevel) => {
|
|
55
|
+
const desiredLevelIndex = LoggingLevelSchema.options.indexOf(desiredLevel);
|
|
56
|
+
if (desiredLevelIndex === -1) {
|
|
57
|
+
console.error(`Invalid logging level: ${desiredLevel}. Must be one of: ${LoggingLevelSchema.options.join(', ')}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
currentLevel = desiredLevelIndex;
|
|
61
|
+
};
|
|
62
|
+
const getLoggingLevel = () => LoggingLevelSchema.options[currentLevel];
|
|
63
|
+
export default { log, setServer, setLogger, setLoggingLevel, getLoggingLevel };
|
package/dist/config.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
|
+
import { LoggingLevelSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
1
2
|
import { Command } from 'commander';
|
|
2
3
|
import dotenv from 'dotenv';
|
|
3
|
-
dotenv.config();
|
|
4
|
+
dotenv.config({ debug: false, quiet: true });
|
|
4
5
|
const state = {
|
|
5
6
|
transport: 'http',
|
|
6
7
|
port: 8080,
|
|
7
8
|
host: '0.0.0.0',
|
|
8
9
|
braveApiKey: process.env.BRAVE_API_KEY ?? '',
|
|
10
|
+
loggingLevel: 'info',
|
|
9
11
|
ready: false,
|
|
10
12
|
};
|
|
11
13
|
export function getOptions() {
|
|
12
14
|
const program = new Command()
|
|
13
15
|
.option('--brave-api-key <string>', 'Brave API key', process.env.BRAVE_API_KEY ?? '')
|
|
16
|
+
.option('--logging-level <string>', 'Logging level', process.env.BRAVE_MCP_LOG_LEVEL ?? 'info')
|
|
14
17
|
.option('--transport <stdio|http>', 'transport type', process.env.BRAVE_MCP_TRANSPORT ?? 'http')
|
|
15
18
|
.option('--port <number>', 'desired port for HTTP transport', process.env.BRAVE_MCP_PORT ?? '8080')
|
|
16
19
|
.option('--host <string>', 'desired host for HTTP transport', process.env.BRAVE_MCP_HOST ?? '0.0.0.0')
|
|
@@ -21,6 +24,10 @@ export function getOptions() {
|
|
|
21
24
|
console.error(`Invalid --transport value: '${options.transport}'. Must be one of: stdio, http.`);
|
|
22
25
|
return false;
|
|
23
26
|
}
|
|
27
|
+
if (!LoggingLevelSchema.options.includes(options.loggingLevel)) {
|
|
28
|
+
console.error(`Invalid --logging-level value: '${options.loggingLevel}'. Must be one of: ${LoggingLevelSchema.options.join(', ')}`);
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
24
31
|
if (!options.braveApiKey) {
|
|
25
32
|
console.error('Error: --brave-api-key is required. You can get one at https://brave.com/search/api/.');
|
|
26
33
|
return false;
|
|
@@ -40,6 +47,7 @@ export function getOptions() {
|
|
|
40
47
|
state.transport = options.transport;
|
|
41
48
|
state.port = options.port;
|
|
42
49
|
state.host = options.host;
|
|
50
|
+
state.loggingLevel = options.loggingLevel;
|
|
43
51
|
state.ready = true;
|
|
44
52
|
return options;
|
|
45
53
|
}
|
package/dist/protocols/http.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import express from 'express';
|
|
2
3
|
import config from '../config.js';
|
|
3
|
-
import {
|
|
4
|
+
import { mcpServer } from '../server.js';
|
|
4
5
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
6
|
+
import { isInitializeRequest, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
7
|
const yieldGenericServerError = (res) => {
|
|
6
8
|
res.status(500).json({
|
|
7
9
|
id: null,
|
|
@@ -9,23 +11,44 @@ const yieldGenericServerError = (res) => {
|
|
|
9
11
|
error: { code: -32603, message: 'Internal server error' },
|
|
10
12
|
});
|
|
11
13
|
};
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const transports = new Map();
|
|
15
|
+
export const getTransport = async (request) => {
|
|
16
|
+
// Check for an existing session
|
|
17
|
+
const sessionId = request.headers['mcp-session-id'];
|
|
18
|
+
if (sessionId && transports.has(sessionId)) {
|
|
19
|
+
return transports.get(sessionId);
|
|
16
20
|
}
|
|
21
|
+
// Is the client attempting to initialize a new session?
|
|
22
|
+
if (isInitializeRequest(request.body)) {
|
|
23
|
+
const transport = new StreamableHTTPServerTransport({
|
|
24
|
+
sessionIdGenerator: () => randomUUID(),
|
|
25
|
+
onsessioninitialized: (sessionId) => {
|
|
26
|
+
transports.set(sessionId, transport);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
await mcpServer.connect(transport);
|
|
30
|
+
return transport;
|
|
31
|
+
}
|
|
32
|
+
// We have a special case where we'll permit ListToolsRequest w/o a session ID
|
|
33
|
+
if (request.body.method === ListToolsRequestSchema.shape.method.value) {
|
|
34
|
+
const transport = new StreamableHTTPServerTransport({
|
|
35
|
+
sessionIdGenerator: undefined,
|
|
36
|
+
});
|
|
37
|
+
await mcpServer.connect(transport);
|
|
38
|
+
return transport;
|
|
39
|
+
}
|
|
40
|
+
throw new Error('Invalid request: must be an initialization request, include a valid session ID, or be a ListTools method request');
|
|
41
|
+
};
|
|
42
|
+
export const createApp = () => {
|
|
17
43
|
const app = express();
|
|
18
44
|
app.use(express.json());
|
|
19
45
|
app.all('/mcp', async (req, res) => {
|
|
20
46
|
try {
|
|
21
|
-
const transport =
|
|
22
|
-
// Setting to undefined will opt-out of session-id generation
|
|
23
|
-
sessionIdGenerator: undefined,
|
|
24
|
-
});
|
|
25
|
-
await server.connect(transport);
|
|
47
|
+
const transport = await getTransport(req);
|
|
26
48
|
await transport.handleRequest(req, res, req.body);
|
|
27
49
|
}
|
|
28
50
|
catch (error) {
|
|
51
|
+
console.error(error);
|
|
29
52
|
if (!res.headersSent) {
|
|
30
53
|
yieldGenericServerError(res);
|
|
31
54
|
}
|
|
@@ -34,8 +57,16 @@ export const start = () => {
|
|
|
34
57
|
app.all('/ping', (req, res) => {
|
|
35
58
|
res.status(200).json({ message: 'pong' });
|
|
36
59
|
});
|
|
60
|
+
return app;
|
|
61
|
+
};
|
|
62
|
+
export const start = () => {
|
|
63
|
+
if (!config.ready) {
|
|
64
|
+
console.error('Invalid configuration');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
const app = createApp();
|
|
37
68
|
app.listen(config.port, config.host, () => {
|
|
38
69
|
console.error(`Server is running on http://${config.host}:${config.port}/mcp`);
|
|
39
70
|
});
|
|
40
71
|
};
|
|
41
|
-
export default { start };
|
|
72
|
+
export default { start, createApp };
|
package/dist/protocols/stdio.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mcpServer } from '../server.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
export const createTransport = () => {
|
|
4
|
+
return new StdioServerTransport();
|
|
5
|
+
};
|
|
3
6
|
export const start = async () => {
|
|
4
|
-
const transport =
|
|
5
|
-
await
|
|
7
|
+
const transport = createTransport();
|
|
8
|
+
await mcpServer.connect(transport);
|
|
6
9
|
console.error('Stdio server started');
|
|
7
10
|
};
|
|
8
|
-
export default { start };
|
|
11
|
+
export default { start, createTransport };
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import tools from './tools/index.js';
|
|
3
|
-
|
|
3
|
+
import ClientLogger, { maybeRegisterCustomSetLevelRequestHandler } from './ClientLogger.js';
|
|
4
|
+
export const mcpServer = new McpServer({
|
|
4
5
|
version: '0.1.0',
|
|
5
6
|
name: 'brave-search-mcp-server',
|
|
6
7
|
title: 'Brave Search MCP Server',
|
|
@@ -9,8 +10,11 @@ export const server = new McpServer({
|
|
|
9
10
|
logging: {},
|
|
10
11
|
tools: { listChanged: false },
|
|
11
12
|
},
|
|
12
|
-
instructions:
|
|
13
|
+
instructions: `Use this server to search the Web for various types of data via the Brave Search API.`,
|
|
13
14
|
});
|
|
15
|
+
ClientLogger.setServer(mcpServer);
|
|
16
|
+
// https://github.com/modelcontextprotocol/typescript-sdk/issues/871
|
|
17
|
+
maybeRegisterCustomSetLevelRequestHandler(mcpServer.server);
|
|
14
18
|
for (const tool of Object.values(tools)) {
|
|
15
|
-
|
|
19
|
+
mcpServer.tool(tool.name, tool.description, tool.inputSchema, tool.annotations, tool.execute);
|
|
16
20
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import params from './params.js';
|
|
2
2
|
import API from '../../BraveAPI/index.js';
|
|
3
|
-
import {
|
|
3
|
+
import { stringify } from '../../utils.js';
|
|
4
|
+
import ClientLogger from '../../ClientLogger.js';
|
|
4
5
|
export const name = 'brave_image_search';
|
|
5
6
|
export const annotations = {
|
|
6
7
|
title: 'Brave Image Search',
|
|
@@ -27,7 +28,7 @@ export const execute = async (params) => {
|
|
|
27
28
|
return { content, isError: false };
|
|
28
29
|
};
|
|
29
30
|
async function fetchImage(url) {
|
|
30
|
-
await log('info', `Fetching image data from ${url}`);
|
|
31
|
+
await ClientLogger.log('info', `Fetching image data from ${url}`);
|
|
31
32
|
try {
|
|
32
33
|
const response = await fetch(url);
|
|
33
34
|
const buffer = await response.arrayBuffer();
|
|
@@ -37,7 +38,7 @@ async function fetchImage(url) {
|
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
40
|
catch (error) {
|
|
40
|
-
await log('error', `Error fetching image data from ${url}: ${error}`);
|
|
41
|
+
await ClientLogger.log('error', `Error fetching image data from ${url}: ${error}`);
|
|
41
42
|
return null;
|
|
42
43
|
}
|
|
43
44
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { summarizerQueryParams } from './params.js';
|
|
2
2
|
import API from '../../BraveAPI/index.js';
|
|
3
|
-
import
|
|
3
|
+
import ClientLogger from '../../ClientLogger.js';
|
|
4
4
|
export const name = 'brave_summarizer';
|
|
5
5
|
export const annotations = {
|
|
6
6
|
title: 'Brave Summarizer',
|
|
@@ -70,7 +70,7 @@ const pollForSummary = async (params, pollInterval = 50, attempts = 20) => {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
catch (error) {
|
|
73
|
-
await log('error', `Error polling for summarizer results: ${error}`);
|
|
73
|
+
await ClientLogger.log('error', `Error polling for summarizer results: ${error}`);
|
|
74
74
|
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
75
75
|
}
|
|
76
76
|
attempts--;
|
package/dist/utils.js
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import { RATE_LIMIT } from './constants.js';
|
|
2
|
-
import { server } from './server.js';
|
|
3
2
|
let requestCount = {
|
|
4
3
|
second: 0,
|
|
5
4
|
month: 0,
|
|
6
5
|
lastReset: Date.now(),
|
|
7
6
|
};
|
|
8
|
-
export async function log(level, message) {
|
|
9
|
-
const time = new Date().toISOString();
|
|
10
|
-
await server.server.sendLoggingMessage({ level, data: { message, time } });
|
|
11
|
-
}
|
|
12
7
|
export function checkRateLimit() {
|
|
13
8
|
const now = Date.now();
|
|
14
9
|
if (now - requestCount.lastReset > 1000) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brave/brave-search-mcp-server",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
4
4
|
"description": "MCP server for Brave Search. Uses the Brave Search API to return results from the web, including ranked links, images, and videos, as well as AI summaries of pages, rich results, and more.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api",
|
|
@@ -29,10 +29,11 @@
|
|
|
29
29
|
"watch": "tsc --watch",
|
|
30
30
|
"format": "prettier --write \"src/**/*.ts\"",
|
|
31
31
|
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
32
|
-
"inspector": "npx @modelcontextprotocol/inspector"
|
|
32
|
+
"inspector": "npx @modelcontextprotocol/inspector",
|
|
33
|
+
"inspector:stdio": "npx @modelcontextprotocol/inspector --transport stdio"
|
|
33
34
|
},
|
|
34
35
|
"dependencies": {
|
|
35
|
-
"@modelcontextprotocol/sdk": "1.
|
|
36
|
+
"@modelcontextprotocol/sdk": "1.17.2",
|
|
36
37
|
"commander": "14.0.0",
|
|
37
38
|
"dotenv": "^17.2.1",
|
|
38
39
|
"express": "5.1.0",
|
|
@@ -40,9 +41,9 @@
|
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@types/express": "5.0.3",
|
|
43
|
-
"@types/node": "24.
|
|
44
|
+
"@types/node": "24.2.0",
|
|
44
45
|
"prettier": "3.6.2",
|
|
45
46
|
"shx": "0.4.0",
|
|
46
|
-
"typescript": "5.
|
|
47
|
+
"typescript": "5.9.2"
|
|
47
48
|
}
|
|
48
49
|
}
|