├── .github ├── CODEOWNERS ├── renovate.json └── workflows │ └── build-release.yml ├── smithery.yaml ├── .gitignore ├── src ├── constants.ts ├── protocols │ ├── index.ts │ ├── stdio.ts │ └── http.ts ├── tools │ ├── index.ts │ ├── local │ │ ├── params.ts │ │ ├── index.ts │ │ └── types.ts │ ├── images │ │ ├── schemas │ │ │ ├── output.ts │ │ │ ├── input.ts │ │ │ └── response.ts │ │ ├── index.ts │ │ └── types.ts │ ├── videos │ │ ├── index.ts │ │ ├── params.ts │ │ └── types.ts │ ├── news │ │ ├── types.ts │ │ ├── index.ts │ │ └── params.ts │ ├── summarizer │ │ ├── params.ts │ │ ├── index.ts │ │ └── types.ts │ └── web │ │ ├── index.ts │ │ ├── params.ts │ │ └── types.ts ├── helpers.ts ├── index.ts ├── utils.ts ├── server.ts ├── BraveAPI │ ├── types.ts │ └── index.ts └── config.ts ├── glama.json ├── .prettierrc ├── .prettierignore ├── tsconfig.json ├── docker-compose.yml ├── Dockerfile ├── server.json ├── LICENSE ├── package.json ├── marketplace-revision-release.json └── README.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jonathansampson 2 | -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | runtime: "typescript" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | .env 4 | .smithery 5 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const RATE_LIMIT = { 2 | perSecond: 1, 3 | perMonth: 15000, 4 | } as const; 5 | -------------------------------------------------------------------------------- /src/protocols/index.ts: -------------------------------------------------------------------------------- 1 | export { default as stdioServer } from './stdio.js'; 2 | export { default as httpServer } from './http.js'; 3 | -------------------------------------------------------------------------------- /glama.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://glama.ai/mcp/schemas/server.json", 3 | "maintainers": [ 4 | "jonathansampson" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>brave/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "es5", 6 | "printWidth": 100, 7 | "useTabs": false, 8 | "bracketSpacing": true, 9 | "arrowParens": "always" 10 | } 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Build outputs 5 | dist/ 6 | build/ 7 | *.d.ts 8 | 9 | # Logs 10 | *.log 11 | npm-debug.log* 12 | 13 | # OS generated files 14 | .DS_Store 15 | Thumbs.db 16 | 17 | # Editor directories and files 18 | .vscode/ 19 | .idea/ 20 | 21 | # Package files 22 | package-lock.json 23 | yarn.lock 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "rootDir": "src", 5 | "target": "ES2022", 6 | "module": "NodeNext", 7 | "moduleResolution": "NodeNext", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules"] 16 | } -------------------------------------------------------------------------------- /src/tools/index.ts: -------------------------------------------------------------------------------- 1 | import WebSearchTool from './web/index.js'; 2 | import LocalSearchTool from './local/index.js'; 3 | import VideoSearchTool from './videos/index.js'; 4 | import ImageSearchTool from './images/index.js'; 5 | import NewsSearchTool from './news/index.js'; 6 | import SummarizerTool from './summarizer/index.js'; 7 | 8 | export default { 9 | WebSearchTool, 10 | LocalSearchTool, 11 | VideoSearchTool, 12 | ImageSearchTool, 13 | NewsSearchTool, 14 | SummarizerTool, 15 | }; 16 | -------------------------------------------------------------------------------- /src/protocols/stdio.ts: -------------------------------------------------------------------------------- 1 | import newMcpServer from '../server.js'; 2 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 3 | 4 | const createTransport = (): StdioServerTransport => { 5 | return new StdioServerTransport(); 6 | }; 7 | 8 | const start = async (): Promise => { 9 | const transport = createTransport(); 10 | const mcpServer = newMcpServer(); 11 | await mcpServer.connect(transport); 12 | }; 13 | 14 | export default { start, createTransport }; 15 | -------------------------------------------------------------------------------- /src/tools/local/params.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const LocalPoisParams = z.object({ 4 | ids: z.array(z.string()).describe('List of location IDs for which to fetch POIs'), 5 | }); 6 | 7 | export const LocalDescriptionsParams = z.object({ 8 | ids: z.array(z.string()).describe('List of location IDs for which to fetch descriptions'), 9 | }); 10 | 11 | export type LocalPoisParams = z.infer; 12 | export type LocalDescriptionsParams = z.infer; 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | mcp-server: 3 | build: . 4 | cap_drop: 5 | - all 6 | deploy: 7 | restart_policy: 8 | condition: any 9 | delay: 5s 10 | environment: 11 | - BRAVE_API_KEY 12 | - BRAVE_MCP_TRANSPORT=http 13 | - BRAVE_MCP_PORT=8080 14 | - BRAVE_MCP_HOST=0.0.0.0 15 | - BRAVE_MCP_LOG_LEVEL=info 16 | - BRAVE_MCP_ENABLED_TOOLS 17 | - BRAVE_MCP_DISABLED_TOOLS 18 | init: true 19 | ports: 20 | - 8080:8080 21 | security_opt: 22 | - no-new-privileges:true 23 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; 2 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 3 | 4 | export function registerSigIntHandler( 5 | transports: Map 6 | ) { 7 | process.on('SIGINT', async () => { 8 | for (const sessionID of transports.keys()) { 9 | await transports.get(sessionID)?.close(); 10 | transports.delete(sessionID); 11 | } 12 | 13 | console.error('Server shut down.'); 14 | process.exit(0); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { getOptions } from './config.js'; 3 | import { stdioServer, httpServer } from './protocols/index.js'; 4 | 5 | async function main() { 6 | const options = getOptions(); 7 | 8 | if (!options) { 9 | console.error('Invalid configuration'); 10 | process.exit(1); 11 | } 12 | 13 | // default to stdio server unless http is explicitly requested 14 | if (options.transport === 'http') { 15 | httpServer.start(); 16 | return; 17 | } 18 | 19 | await stdioServer.start(); 20 | } 21 | 22 | main().catch((error) => { 23 | console.error(error); 24 | process.exit(1); 25 | }); 26 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { RATE_LIMIT } from './constants.js'; 2 | 3 | let requestCount = { 4 | second: 0, 5 | month: 0, 6 | lastReset: Date.now(), 7 | }; 8 | 9 | export function checkRateLimit() { 10 | const now = Date.now(); 11 | if (now - requestCount.lastReset > 1000) { 12 | requestCount.second = 0; 13 | requestCount.lastReset = now; 14 | } 15 | if (requestCount.second >= RATE_LIMIT.perSecond || requestCount.month >= RATE_LIMIT.perMonth) { 16 | throw new Error('Rate limit exceeded'); 17 | } 18 | requestCount.second++; 19 | requestCount.month++; 20 | } 21 | 22 | export function stringify(data: any, pretty = false) { 23 | return pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data); 24 | } 25 | -------------------------------------------------------------------------------- /src/tools/images/schemas/output.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { ConfidenceSchema, ExtraSchema } from './response.js'; 3 | 4 | export const SimplifiedImageResultSchema = z.object({ 5 | title: z.string(), 6 | url: z.string().url(), 7 | page_fetched: z.string().datetime(), 8 | confidence: ConfidenceSchema, 9 | properties: z.object({ 10 | url: z.string().url(), 11 | width: z.number().int().positive(), 12 | height: z.number().int().positive(), 13 | }), 14 | }); 15 | 16 | const OutputSchema = z.object({ 17 | type: z.literal('object'), 18 | items: z.array(SimplifiedImageResultSchema), 19 | count: z.number().int().nonnegative(), 20 | might_be_offensive: ExtraSchema.shape.might_be_offensive, 21 | }); 22 | 23 | export default OutputSchema; 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine@sha256:fd164609b5ab0c6d49bac138ae06c347e72261ec6ae1de32b6aa9f5ee2271110 AS builder 2 | 3 | RUN apk add --no-cache openssl=3.5.4-r0 4 | 5 | WORKDIR /app 6 | 7 | COPY ./package.json ./package.json 8 | COPY ./package-lock.json ./package-lock.json 9 | 10 | RUN npm ci --ignore-scripts 11 | 12 | COPY ./src ./src 13 | COPY ./tsconfig.json ./tsconfig.json 14 | 15 | RUN npm run build 16 | 17 | FROM node:alpine@sha256:fd164609b5ab0c6d49bac138ae06c347e72261ec6ae1de32b6aa9f5ee2271110 AS release 18 | 19 | RUN apk add --no-cache openssl=3.5.4-r0 20 | 21 | WORKDIR /app 22 | 23 | COPY --from=builder /app/dist /app/dist 24 | COPY --from=builder /app/package.json /app/package.json 25 | COPY --from=builder /app/package-lock.json /app/package-lock.json 26 | 27 | ENV NODE_ENV=production 28 | 29 | RUN npm ci --ignore-scripts --omit-dev 30 | 31 | USER node 32 | 33 | CMD ["node", "dist/index.js"] 34 | -------------------------------------------------------------------------------- /server.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json", 3 | "name": "io.github.brave/brave-search-mcp-server", 4 | "description": "Brave Search MCP Server: web results, images, videos, rich results, AI summaries, and more.", 5 | "version": "2.0.63", 6 | "repository": { 7 | "url": "https://github.com/brave/brave-search-mcp-server", 8 | "source": "github" 9 | }, 10 | "packages": [ 11 | { 12 | "registryType": "npm", 13 | "registryBaseUrl": "https://registry.npmjs.org", 14 | "identifier": "@brave/brave-search-mcp-server", 15 | "version": "2.0.63", 16 | "transport": { 17 | "type": "stdio" 18 | }, 19 | "environmentVariables": [ 20 | { 21 | "name": "BRAVE_API_KEY", 22 | "description": "Your API key for the service", 23 | "format": "string", 24 | "isRequired": true, 25 | "isSecret": true 26 | } 27 | ] 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Anthropic, PBC 4 | Copyright (c) 2025 Brave Software, Inc 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import tools from './tools/index.js'; 2 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 3 | import pkg from '../package.json' with { type: 'json' }; 4 | import { isToolPermittedByUser } from './config.js'; 5 | import { type SmitheryConfig, setOptions } from './config.js'; 6 | export { configSchema } from './config.js'; 7 | 8 | type CreateMcpServerOptions = { 9 | config: SmitheryConfig; 10 | }; 11 | 12 | export default function createMcpServer(options?: CreateMcpServerOptions): McpServer { 13 | if (options?.config) setOptions(options.config); 14 | 15 | const mcpServer = new McpServer( 16 | { 17 | version: pkg.version, 18 | name: 'brave-search-mcp-server', 19 | title: 'Brave Search MCP Server', 20 | }, 21 | { 22 | capabilities: { 23 | logging: {}, 24 | tools: { listChanged: false }, 25 | }, 26 | instructions: `Use this server to search the Web for various types of data via the Brave Search API.`, 27 | } 28 | ); 29 | 30 | for (const tool of Object.values(tools)) { 31 | // The user may have enabled/disabled this tool at runtime 32 | if (!isToolPermittedByUser(tool.name)) continue; 33 | tool.register(mcpServer); 34 | } 35 | 36 | return mcpServer; 37 | } 38 | -------------------------------------------------------------------------------- /src/tools/images/schemas/input.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const params = z.object({ 4 | query: z 5 | .string() 6 | .min(1) 7 | .max(400) 8 | .refine((str) => str.split(/\s+/).length <= 50, 'Query cannot exceed 50 words') 9 | .describe( 10 | "The user's search query. Query cannot be empty. Limited to 400 characters and 50 words." 11 | ), 12 | country: z 13 | .string() 14 | .default('US') 15 | .describe( 16 | 'Search query country, where the results come from. The country string is limited to 2 character country codes of supported countries.' 17 | ) 18 | .optional(), 19 | search_lang: z 20 | .string() 21 | .default('en') 22 | .describe( 23 | 'Search language preference. The 2 or more character language code for which the search results are provided.' 24 | ) 25 | .optional(), 26 | count: z 27 | .number() 28 | .int() 29 | .min(1) 30 | .max(200) 31 | .default(50) 32 | .describe( 33 | 'Number of results (1-200, default 50). Combine this parameter with `offset` to paginate search results.' 34 | ) 35 | .optional(), 36 | safesearch: z 37 | .union([z.literal('off'), z.literal('strict')]) 38 | .default('strict') 39 | .describe( 40 | "Filters search results for adult content. The following values are supported: 'off' - No filtering. 'strict' - Drops all adult content from search results." 41 | ) 42 | .optional(), 43 | spellcheck: z 44 | .boolean() 45 | .default(true) 46 | .describe('Whether to spellcheck provided query.') 47 | .optional(), 48 | }); 49 | 50 | export type QueryParams = z.infer; 51 | 52 | export default params; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@brave/brave-search-mcp-server", 3 | "mcpName": "io.github.brave/brave-search-mcp-server", 4 | "version": "2.0.63", 5 | "description": "Brave Search MCP Server: web results, images, videos, rich results, AI summaries, and more.", 6 | "keywords": [ 7 | "api", 8 | "brave", 9 | "mcp", 10 | "search" 11 | ], 12 | "license": "MIT", 13 | "author": "Brave Software, Inc. (https://brave.com)", 14 | "homepage": "https://github.com/brave/brave-search-mcp-server", 15 | "bugs": "https://github.com/brave/brave-search-mcp-server/issues", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/brave/brave-search-mcp-server.git" 19 | }, 20 | "type": "module", 21 | "module": "dist/server.js", 22 | "bin": { 23 | "brave-search-mcp-server": "dist/index.js" 24 | }, 25 | "files": [ 26 | "dist" 27 | ], 28 | "scripts": { 29 | "build": "tsc && shx chmod +x dist/*.js", 30 | "smithery:build": "smithery build", 31 | "smithery:dev": "smithery dev", 32 | "prepare": "npm run format && npm run build", 33 | "watch": "tsc --watch", 34 | "format": "prettier --write \"src/**/*.ts\"", 35 | "format:check": "prettier --check \"src/**/*.ts\"", 36 | "inspector": "npx @modelcontextprotocol/inspector", 37 | "inspector:http": "npx @modelcontextprotocol/inspector --transport http" 38 | }, 39 | "dependencies": { 40 | "@modelcontextprotocol/sdk": "1.24.2", 41 | "commander": "14.0.2", 42 | "dotenv": "17.2.3", 43 | "express": "5.2.1", 44 | "zod": "3.25.76" 45 | }, 46 | "devDependencies": { 47 | "@types/express": "5.0.5", 48 | "@smithery/cli": "1.6.4", 49 | "@types/node": "24.10.1", 50 | "prettier": "3.7.1", 51 | "shx": "0.4.0", 52 | "tsx": "4.20.6", 53 | "typescript": "5.9.3" 54 | }, 55 | "overrides": { 56 | "formdata-node": "6.0.3", 57 | "tmp": "0.2.5" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/tools/videos/index.ts: -------------------------------------------------------------------------------- 1 | import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'; 2 | import params, { type QueryParams } from './params.js'; 3 | import API from '../../BraveAPI/index.js'; 4 | import { stringify } from '../../utils.js'; 5 | import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 6 | 7 | export const name = 'brave_video_search'; 8 | 9 | export const annotations: ToolAnnotations = { 10 | title: 'Brave Video Search', 11 | openWorldHint: true, 12 | }; 13 | 14 | export const description = ` 15 | Searches for videos using Brave's Video Search API and returns structured video results with metadata. 16 | 17 | When to use: 18 | - When you need to find videos related to a specific topic, keyword, or query. 19 | - Useful for discovering video content, getting video metadata, or finding videos from specific creators/publishers. 20 | 21 | Returns a JSON list of video-related results with title, url, description, duration, and thumbnail_url. 22 | `; 23 | 24 | export const execute = async (params: QueryParams) => { 25 | const response = await API.issueRequest<'videos'>('videos', params); 26 | 27 | return { 28 | content: response.results.map(({ url, title, description, video, thumbnail }) => { 29 | const duration = video?.duration; 30 | const thumbnail_url = thumbnail?.src; 31 | 32 | return { 33 | type: 'text' as const, 34 | text: stringify({ url, title, description, duration, thumbnail_url }), 35 | }; 36 | }), 37 | }; 38 | }; 39 | 40 | export const register = (mcpServer: McpServer) => { 41 | mcpServer.registerTool( 42 | name, 43 | { 44 | title: name, 45 | description: description, 46 | inputSchema: params.shape, 47 | annotations: annotations, 48 | }, 49 | execute 50 | ); 51 | }; 52 | 53 | export default { 54 | name, 55 | description, 56 | annotations, 57 | inputSchema: params.shape, 58 | execute, 59 | register, 60 | }; 61 | -------------------------------------------------------------------------------- /src/BraveAPI/types.ts: -------------------------------------------------------------------------------- 1 | import type { QueryParams as WebQueryParams } from '../tools/web/params.js'; 2 | import type { QueryParams as ImageQueryParams } from '../tools/images/schemas/input.js'; 3 | import type { QueryParams as VideoQueryParams } from '../tools/videos/params.js'; 4 | import type { QueryParams as NewsQueryParams } from '../tools/news/params.js'; 5 | import type { LocalPoisParams, LocalDescriptionsParams } from '../tools/local/params.js'; 6 | import type { SummarizerQueryParams } from '../tools/summarizer/params.js'; 7 | import type { WebSearchApiResponse } from '../tools/web/types.js'; 8 | import type { SummarizerSearchApiResponse } from '../tools/summarizer/types.js'; 9 | import type { ImageSearchApiResponse } from '../tools/images/types.js'; 10 | import type { VideoSearchApiResponse } from '../tools/videos/types.js'; 11 | import type { NewsSearchApiResponse } from '../tools/news/types.js'; 12 | import type { 13 | LocalPoiSearchApiResponse, 14 | LocalDescriptionsSearchApiResponse, 15 | } from '../tools/local/types.js'; 16 | 17 | export interface RateLimitErrorResponse { 18 | type: 'ErrorResponse'; 19 | error: { 20 | id: string; 21 | status: number; 22 | code: 'RATE_LIMITED'; 23 | detail: string; 24 | meta: { 25 | plan: string; 26 | rate_limit: number; 27 | rate_current: number; 28 | quota_limit: number; 29 | quota_current: number; 30 | component: 'rate_limiter'; 31 | }; 32 | }; 33 | time: number; 34 | } 35 | 36 | export type Endpoints = { 37 | web: { 38 | params: WebQueryParams; 39 | response: WebSearchApiResponse; 40 | requestHeaders: Headers; 41 | }; 42 | images: { 43 | params: ImageQueryParams; 44 | response: ImageSearchApiResponse; 45 | requestHeaders: Headers; 46 | }; 47 | videos: { 48 | params: VideoQueryParams; 49 | response: VideoSearchApiResponse; 50 | requestHeaders: Headers; 51 | }; 52 | news: { 53 | params: NewsQueryParams; 54 | response: NewsSearchApiResponse; 55 | requestHeaders: Headers; 56 | }; 57 | localPois: { 58 | params: LocalPoisParams; 59 | response: LocalPoiSearchApiResponse; 60 | requestHeaders: Headers; 61 | }; 62 | localDescriptions: { 63 | params: LocalDescriptionsParams; 64 | response: LocalDescriptionsSearchApiResponse; 65 | requestHeaders: Headers; 66 | }; 67 | summarizer: { 68 | params: SummarizerQueryParams; 69 | response: SummarizerSearchApiResponse; 70 | requestHeaders: Headers; 71 | }; 72 | }; 73 | -------------------------------------------------------------------------------- /src/tools/news/types.ts: -------------------------------------------------------------------------------- 1 | export interface NewsSearchApiResponse { 2 | /** The type of search API result. The value is always news. */ 3 | type: 'news'; 4 | /** News search query string. */ 5 | query: Query; 6 | /** The list of news results for the given query. */ 7 | results: NewsResult[]; 8 | } 9 | 10 | interface Query { 11 | /** The original query that was requested. */ 12 | original: string; 13 | /** The altered query by the spellchecker. This is the query that is used to search if any. */ 14 | altered?: string; 15 | /** The cleaned normalized query by the spellchecker. This is the query that is used to search if any. */ 16 | cleaned?: string; 17 | /** Whether the spellchecker is enabled or disabled. */ 18 | spellcheck_off?: boolean; 19 | /** The value is true if the lack of results is due to a strict safesearch setting. Adult content relevant to the query was found, but was blocked by safesearch. */ 20 | show_strict_warning?: boolean; 21 | } 22 | 23 | export interface NewsResult { 24 | /** The type of news search API result. The value is always news_result. */ 25 | type: 'news_result'; 26 | /** The source URL of the news article. */ 27 | url: string; 28 | /** The title of the news article. */ 29 | title: string; 30 | /** The description for the news article. */ 31 | description?: string; 32 | /** A human readable representation of the page age. */ 33 | age?: string; 34 | /** The page age found from the source web page. */ 35 | page_age?: string; 36 | /** The ISO date time when the page was last fetched. The format is YYYY-MM-DDTHH:MM:SSZ. */ 37 | page_fetched?: string; 38 | /** Whether the result includes breaking news. */ 39 | breaking?: boolean; 40 | /** The thumbnail for the news article. */ 41 | thumbnail?: Thumbnail; 42 | /** Aggregated information on the URL associated with the news search result. */ 43 | meta_url?: MetaUrl; 44 | /** A list of extra alternate snippets for the news search result. */ 45 | extra_snippets?: string[]; 46 | } 47 | 48 | interface Thumbnail { 49 | /** The served URL of the thumbnail associated with the news article. */ 50 | src: string; 51 | /** The original URL of the thumbnail associated with the news article. */ 52 | original?: string; 53 | } 54 | 55 | interface MetaUrl { 56 | /** The protocol scheme extracted from the URL. */ 57 | scheme?: string; 58 | /** The network location part extracted from the URL. */ 59 | netloc?: string; 60 | /** The lowercased domain name extracted from the URL. */ 61 | hostname?: string; 62 | /** The favicon used for the URL. */ 63 | favicon?: string; 64 | /** The hierarchical path of the URL useful as a display string. */ 65 | path?: string; 66 | } 67 | -------------------------------------------------------------------------------- /src/tools/images/index.ts: -------------------------------------------------------------------------------- 1 | import type { TextContent, ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'; 2 | import params, { type QueryParams } from './schemas/input.js'; 3 | import API from '../../BraveAPI/index.js'; 4 | import type { ImageResult } from './types.js'; 5 | import OutputSchema, { SimplifiedImageResultSchema } from './schemas/output.js'; 6 | import { z } from 'zod'; 7 | import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 8 | 9 | export const name = 'brave_image_search'; 10 | 11 | export const annotations: ToolAnnotations = { 12 | title: 'Brave Image Search', 13 | openWorldHint: true, 14 | }; 15 | 16 | export const description = ` 17 | Performs an image search using the Brave Search API. Helpful for when you need pictures of people, places, things, graphic design ideas, art inspiration, and more. When relaying results in a markdown environment, it may be helpful to include images in the results (e.g., ![image.title](image.properties.url)). 18 | `; 19 | 20 | export const execute = async (params: QueryParams) => { 21 | const response = await API.issueRequest<'images'>('images', params); 22 | const items = response.results.map(simplifySchemaForLLM).filter((o) => o !== null); 23 | 24 | const structuredContent = OutputSchema.safeParse({ 25 | type: 'object', 26 | items, 27 | count: items.length, 28 | might_be_offensive: response.extra.might_be_offensive, 29 | }); 30 | 31 | const payload = structuredContent.success 32 | ? structuredContent.data 33 | : structuredContent.error.flatten(); 34 | 35 | return { 36 | content: [{ type: 'text', text: JSON.stringify(payload) } as TextContent], 37 | isError: !structuredContent.success, 38 | structuredContent: payload, 39 | }; 40 | }; 41 | 42 | export const register = (mcpServer: McpServer) => { 43 | mcpServer.registerTool( 44 | name, 45 | { 46 | title: name, 47 | description: description, 48 | inputSchema: params.shape, 49 | outputSchema: OutputSchema.shape, 50 | annotations: annotations, 51 | }, 52 | execute 53 | ); 54 | }; 55 | 56 | function simplifySchemaForLLM( 57 | result: ImageResult 58 | ): z.infer | null { 59 | const parsed = SimplifiedImageResultSchema.safeParse({ 60 | title: result.title, 61 | url: result.url, 62 | page_fetched: result.page_fetched, 63 | confidence: result.confidence, 64 | properties: { 65 | url: result.properties?.url, 66 | width: result.properties?.width, 67 | height: result.properties?.height, 68 | }, 69 | }); 70 | 71 | return parsed.success ? parsed.data : null; 72 | } 73 | 74 | export default { 75 | name, 76 | description, 77 | annotations, 78 | inputSchema: params.shape, 79 | outputSchema: OutputSchema.shape, 80 | execute, 81 | register, 82 | }; 83 | -------------------------------------------------------------------------------- /src/tools/videos/params.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const params = z.object({ 4 | query: z 5 | .string() 6 | .min(1) 7 | .max(400) 8 | .refine((str) => str.split(/\s+/).length <= 50, 'Query cannot exceed 50 words') 9 | .describe( 10 | "The user's search query. Query cannot be empty. Limited to 400 characters and 50 words." 11 | ), 12 | country: z 13 | .string() 14 | .default('US') 15 | .describe( 16 | 'Search query country, where the results come from. The country string is limited to 2 character country codes of supported countries.' 17 | ) 18 | .optional(), 19 | search_lang: z 20 | .string() 21 | .default('en') 22 | .describe( 23 | 'Search language preference. The 2 or more character language code for which the search results are provided.' 24 | ) 25 | .optional(), 26 | ui_lang: z 27 | .string() 28 | .default('en-US') 29 | .describe( 30 | 'User interface language preferred in response. Usually of the format -. For more, see RFC 9110.' 31 | ) 32 | .optional(), 33 | count: z 34 | .number() 35 | .int() 36 | .min(1) 37 | .max(50) 38 | .default(20) 39 | .describe( 40 | 'Number of results (1-50, default 20). Combine this parameter with `offset` to paginate search results.' 41 | ) 42 | .optional(), 43 | offset: z 44 | .number() 45 | .int() 46 | .min(0) 47 | .max(9) 48 | .default(0) 49 | .describe( 50 | 'Pagination offset (max 9, default 0). Combine this parameter with `count` to paginate search results.' 51 | ) 52 | .optional(), 53 | spellcheck: z 54 | .boolean() 55 | .describe('Whether to spellcheck provided query.') 56 | .default(true) 57 | .optional(), 58 | safesearch: z 59 | .union([z.literal('off'), z.literal('moderate'), z.literal('strict')]) 60 | .default('moderate') 61 | .describe( 62 | "Filters search results for adult content. The following values are supported: 'off' - No filtering. 'moderate' - Filter out explicit content. 'strict' - Filter out explicit and suggestive content. The default value is 'moderate'." 63 | ) 64 | .optional(), 65 | freshness: z 66 | .union([z.literal('pd'), z.literal('pw'), z.literal('pm'), z.literal('py'), z.string()]) 67 | .describe( 68 | "Filters search results by when they were discovered. The following values are supported: 'pd' - Discovered within the last 24 hours. 'pw' - Discovered within the last 7 days. 'pm' - Discovered within the last 31 days. 'py' - Discovered within the last 365 days. 'YYYY-MM-DDtoYYYY-MM-DD' - timeframe is also supported by specifying the date range (e.g. '2022-04-01to2022-07-30')." 69 | ) 70 | .optional(), 71 | }); 72 | 73 | export type QueryParams = z.infer; 74 | 75 | export default params; 76 | -------------------------------------------------------------------------------- /src/tools/images/types.ts: -------------------------------------------------------------------------------- 1 | export interface ImageSearchApiResponse { 2 | /** The type of search API result. The value is always images. */ 3 | type: 'images'; 4 | /** Image search query string. */ 5 | query: Query; 6 | /** The list of image results for the given query. */ 7 | results: ImageResult[]; 8 | /** Additional information about the image search results. */ 9 | extra: Extra; 10 | } 11 | 12 | interface Query { 13 | /** The original query that was requested. */ 14 | original: string; 15 | /** The altered query by the spellchecker. This is the query that is used to search. */ 16 | altered?: string; 17 | /** Whether the spellchecker is enabled or disabled. */ 18 | spellcheck_off?: boolean; 19 | /** The value is true if the lack of results is due to a strict safesearch setting. Adult content relevant to the query was found, but was blocked by safesearch. */ 20 | show_strict_warning?: string; 21 | } 22 | 23 | export interface ImageResult { 24 | /** The type of image search API result. The value is always image_result. */ 25 | type: 'image_result'; 26 | /** The title of the image. */ 27 | title?: string; 28 | /** The original page URL where the image was found. */ 29 | url?: string; 30 | /** The source domain where the image was found. */ 31 | source?: string; 32 | /** The ISO date time when the page was last fetched. The format is YYYY-MM-DDTHH:MM:SSZ. */ 33 | page_fetched?: string; 34 | /** The thumbnail for the image. */ 35 | thumbnail?: Thumbnail; 36 | /** Metadata for the image. */ 37 | properties?: Properties; 38 | /** Aggregated information on the URL associated with the image search result. */ 39 | meta_url?: MetaUrl; 40 | /** The confidence level for the image result. */ 41 | confidence?: 'low' | 'medium' | 'high'; 42 | } 43 | 44 | interface Thumbnail { 45 | /** The served URL of the image. */ 46 | src?: string; 47 | /** The width of the thumbnail. */ 48 | width?: number; 49 | /** The height of the thumbnail. */ 50 | height?: number; 51 | } 52 | 53 | export interface Properties { 54 | /** The image URL. */ 55 | url?: string; 56 | /** The lower resolution placeholder image URL. */ 57 | placeholder?: string; 58 | /** The width of the image. */ 59 | width?: number; 60 | /** The height of the image. */ 61 | height?: number; 62 | } 63 | 64 | interface MetaUrl { 65 | /** The protocol scheme extracted from the URL. */ 66 | scheme?: string; 67 | /** The network location part extracted from the URL. */ 68 | netloc?: string; 69 | /** The lowercased domain name extracted from the URL. */ 70 | hostname?: string; 71 | /** The favicon used for the URL. */ 72 | favicon?: string; 73 | /** The hierarchical path of the URL useful as a display string. */ 74 | path?: string; 75 | } 76 | 77 | interface Extra { 78 | /** Indicates whether the image search results might contain offensive content. */ 79 | might_be_offensive: boolean; 80 | } 81 | -------------------------------------------------------------------------------- /src/tools/news/index.ts: -------------------------------------------------------------------------------- 1 | import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'; 2 | import params, { type QueryParams } from './params.js'; 3 | import API from '../../BraveAPI/index.js'; 4 | import { stringify } from '../../utils.js'; 5 | import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 6 | 7 | export const name = 'brave_news_search'; 8 | 9 | export const annotations: ToolAnnotations = { 10 | title: 'Brave News Search', 11 | openWorldHint: true, 12 | }; 13 | 14 | export const description = ` 15 | This tool searches for news articles using Brave's News Search API based on the user's query. Use it when you need current news information, breaking news updates, or articles about specific topics, events, or entities. 16 | 17 | When to use: 18 | - Finding recent news articles on specific topics 19 | - Getting breaking news updates 20 | - Researching current events or trending stories 21 | - Gathering news sources and headlines for analysis 22 | 23 | Returns a JSON list of news-related results with title, url, and description. Some results may contain snippets of text from the article. 24 | 25 | When relaying results in markdown-supporting environments, always cite sources with hyperlinks. 26 | 27 | Examples: 28 | - "According to [Reuters](https://www.reuters.com/technology/china-bans/), China bans uncertified and recalled power banks on planes". 29 | - "The [New York Times](https://www.nytimes.com/2025/06/27/us/technology/ev-sales.html) reports that Tesla's EV sales have increased by 20%". 30 | - "According to [BBC News](https://www.bbc.com/news/world-europe-65910000), the UK government has announced a new policy to support renewable energy". 31 | `; 32 | 33 | export const execute = async (params: QueryParams) => { 34 | const response = await API.issueRequest<'news'>('news', params); 35 | 36 | return { 37 | content: response.results.map((newsResult) => { 38 | return { 39 | type: 'text' as const, 40 | text: stringify({ 41 | url: newsResult.url, 42 | title: newsResult.title, 43 | age: newsResult.age, 44 | page_age: newsResult.page_age, 45 | breaking: newsResult.breaking ?? false, 46 | description: newsResult.description, 47 | extra_snippets: newsResult.extra_snippets, 48 | thumbnail: newsResult.thumbnail?.src, 49 | }), 50 | }; 51 | }), 52 | }; 53 | }; 54 | 55 | export const register = (mcpServer: McpServer) => { 56 | mcpServer.registerTool( 57 | name, 58 | { 59 | title: name, 60 | description: description, 61 | inputSchema: params.shape, 62 | annotations: annotations, 63 | }, 64 | execute 65 | ); 66 | }; 67 | 68 | export default { 69 | name, 70 | description, 71 | annotations, 72 | inputSchema: params.shape, 73 | execute, 74 | register, 75 | }; 76 | -------------------------------------------------------------------------------- /src/protocols/http.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'node:crypto'; 2 | import express, { type Request, type Response } from 'express'; 3 | import config from '../config.js'; 4 | import createMcpServer from '../server.js'; 5 | import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; 6 | import { ListToolsRequest, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; 7 | 8 | const yieldGenericServerError = (res: Response) => { 9 | res.status(500).json({ 10 | id: null, 11 | jsonrpc: '2.0', 12 | error: { code: -32603, message: 'Internal server error' }, 13 | }); 14 | }; 15 | 16 | const transports = new Map(); 17 | 18 | const isListToolsRequest = (value: unknown): value is ListToolsRequest => 19 | ListToolsRequestSchema.safeParse(value).success; 20 | 21 | const getTransport = async (request: Request): Promise => { 22 | // Check for an existing session 23 | const sessionId = request.headers['mcp-session-id'] as string; 24 | 25 | if (sessionId && transports.has(sessionId)) { 26 | return transports.get(sessionId)!; 27 | } 28 | 29 | // We have a special case where we'll permit ListToolsRequest w/o a session ID 30 | if (!sessionId && isListToolsRequest(request.body)) { 31 | const transport = new StreamableHTTPServerTransport({ 32 | sessionIdGenerator: undefined, 33 | }); 34 | 35 | const mcpServer = createMcpServer(); 36 | await mcpServer.connect(transport); 37 | return transport; 38 | } 39 | 40 | // Otherwise, start a new transport/session 41 | const transport = new StreamableHTTPServerTransport({ 42 | sessionIdGenerator: () => randomUUID(), 43 | onsessioninitialized: (sessionId) => { 44 | transports.set(sessionId, transport); 45 | }, 46 | }); 47 | 48 | const mcpServer = createMcpServer(); 49 | await mcpServer.connect(transport); 50 | return transport; 51 | }; 52 | 53 | const createApp = () => { 54 | const app = express(); 55 | 56 | app.use(express.json()); 57 | 58 | app.all('/mcp', async (req: Request, res: Response) => { 59 | try { 60 | const transport = await getTransport(req); 61 | await transport.handleRequest(req, res, req.body); 62 | } catch (error) { 63 | console.error(error); 64 | if (!res.headersSent) { 65 | yieldGenericServerError(res); 66 | } 67 | } 68 | }); 69 | 70 | app.all('/ping', (req: Request, res: Response) => { 71 | res.status(200).json({ message: 'pong' }); 72 | }); 73 | 74 | return app; 75 | }; 76 | 77 | const start = () => { 78 | if (!config.ready) { 79 | console.error('Invalid configuration'); 80 | process.exit(1); 81 | } 82 | 83 | const app = createApp(); 84 | 85 | app.listen(config.port, config.host, () => { 86 | console.log(`Server is running on http://${config.host}:${config.port}/mcp`); 87 | }); 88 | }; 89 | 90 | export default { start, createApp }; 91 | -------------------------------------------------------------------------------- /src/tools/summarizer/params.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const summarizerQueryParams = z.object({ 4 | key: z 5 | .string() 6 | .describe('The key is equal to value of field key as part of the Summarizer response model.'), 7 | entity_info: z 8 | .boolean() 9 | .default(false) 10 | .describe('Returns extra entities info with the summary response.') 11 | .optional(), 12 | inline_references: z 13 | .boolean() 14 | .default(false) 15 | .describe('Adds inline references to the summary response.') 16 | .optional(), 17 | }); 18 | 19 | export type SummarizerQueryParams = z.infer; 20 | 21 | export const chatCompletionsMessage = z.object({ 22 | role: z 23 | .enum(['user']) 24 | .default('user') 25 | .describe('The role of the message. Only "user" is supported for now.'), 26 | content: z 27 | .string() 28 | .describe('The content of the message. The value is the question to be answered.'), 29 | }); 30 | 31 | export type ChatCompletionsMessage = z.infer; 32 | 33 | export const chatCompletionParams = z.object({ 34 | messages: z 35 | .array(chatCompletionsMessage) 36 | .describe( 37 | 'The messages to use for the chat completion. The value is a list of ChatCompletionsMessage response models.' 38 | ), 39 | model: z 40 | .enum(['brave-pro', 'brave']) 41 | .default('brave-pro') 42 | .optional() 43 | .describe( 44 | 'The model to use for the chat completion. The value can be "brave-pro" (default) or "brave".' 45 | ), 46 | stream: z 47 | .boolean() 48 | .default(true) 49 | .optional() 50 | .describe( 51 | 'Whether to stream the response. The value is `true` by default. When using the OpenAI CLI, use `openai.AsyncOpenAI` for streaming and `openai.OpenAI` for blocking mode.' 52 | ), 53 | country: z 54 | .string() 55 | .default('us') 56 | .optional() 57 | .describe( 58 | 'The country backend to use for the chat completion. The value is "us" by default. Note: This parameter is passed in extra_body field when using the OpenAI CLI.' 59 | ), 60 | language: z 61 | .string() 62 | .default('en') 63 | .optional() 64 | .describe( 65 | 'The language to use for the chat completion. The value is "en" by default. Note: This parameter is passed in extra_body field when using the OpenAI CLI.' 66 | ), 67 | enable_entities: z 68 | .boolean() 69 | .default(false) 70 | .optional() 71 | .describe( 72 | 'Whether to enable entities in the chat completion. The value is `false` by default. Note: This parameter is passed in extra_body field when using the OpenAI CLI.' 73 | ), 74 | enable_citations: z 75 | .boolean() 76 | .default(false) 77 | .optional() 78 | .describe( 79 | 'Whether to enable citations in the chat completion. The value is `false` by default. Note: This parameter is passed in extra_body field when using the OpenAI CLI.' 80 | ), 81 | }); 82 | 83 | export type ChatCompletionParams = z.infer; 84 | -------------------------------------------------------------------------------- /src/tools/images/schemas/response.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | /** 4 | * https://api-dashboard.search.brave.com/app/documentation/image-search/responses 5 | */ 6 | 7 | const QuerySchema = z.object({ 8 | original: z.string().describe('The original query string.'), 9 | altered: z.string().optional().describe('The altered query string.'), 10 | spellcheck_off: z.boolean().optional().describe('Whether spellcheck was disabled.'), 11 | show_strict_warning: z 12 | .boolean() 13 | .optional() 14 | .describe('When true, some results were blocked by safesearch.'), 15 | }); 16 | 17 | const ThumbnailSchema = z.object({ 18 | src: z.string().url().optional().describe('The URL of the thumbnail.'), 19 | width: z.number().int().positive().optional().describe('The width of the thumbnail.'), 20 | height: z.number().int().positive().optional().describe('The height of the thumbnail.'), 21 | }); 22 | 23 | const PropertiesSchema = z.object({ 24 | url: z.string().url().optional().describe('The URL of the image.'), 25 | placeholder: z.string().url().optional().describe('The lower resolution placeholder image.'), 26 | width: z.number().int().positive().optional().describe('The width of the image.'), 27 | height: z.number().int().positive().optional().describe('The height of the image.'), 28 | }); 29 | 30 | const MetaUrlSchema = z.object({ 31 | scheme: z.enum(['https', 'http']).optional().describe('The scheme of the URL.'), 32 | netloc: z.string().optional().describe('The network location of the URL.'), 33 | hostname: z.string().optional().describe('The lowercased hostname of the URL.'), 34 | favicon: z.string().url().optional().describe('The URL of the favicon of the URL.'), 35 | path: z.string().optional().describe('The path of the URL (useful as a display string).'), 36 | }); 37 | 38 | export const ConfidenceSchema = z 39 | .enum(['low', 'medium', 'high']) 40 | .describe('The confidence level of the result.'); 41 | 42 | const ImageResultSchema = z.object({ 43 | type: z.literal('image_result').describe('The type of result.'), 44 | title: z.string().optional().describe('The title of the image.'), 45 | url: z.string().url().optional().describe('The URL of the image.'), 46 | source: z.string().url().optional().describe('The source URL of the image.'), 47 | page_fetched: z 48 | .string() 49 | .datetime() 50 | .optional() 51 | .describe('The date and time the page was fetched.'), 52 | thumbnail: ThumbnailSchema.optional().describe('The thumbnail of the image.'), 53 | properties: PropertiesSchema.optional().describe('The metadata for the image.'), 54 | meta_url: MetaUrlSchema.optional().describe( 55 | 'Information about the URL associated with the image.' 56 | ), 57 | confidence: ConfidenceSchema.optional(), 58 | }); 59 | 60 | export const ExtraSchema = z.object({ 61 | might_be_offensive: z.boolean().describe('Whether the image might be offensive.'), 62 | }); 63 | 64 | export const ImageSearchApiResponseSchema = z.object({ 65 | type: z.literal('images').describe('The type of API response.'), 66 | query: QuerySchema.describe('The query used to generate the results.'), 67 | results: z.array(ImageResultSchema).describe('The results of the image search.'), 68 | extra: ExtraSchema.describe('Extra information about the search.'), 69 | }); 70 | -------------------------------------------------------------------------------- /src/tools/news/params.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const params = z.object({ 4 | query: z 5 | .string() 6 | .max(400) 7 | .refine((str) => str.split(/\s+/).length <= 50, 'Query cannot exceed 50 words') 8 | .describe('Search query (max 400 chars, 50 words)'), 9 | country: z 10 | .string() 11 | .default('US') 12 | .describe( 13 | 'Search query country, where the results come from. The country string is limited to 2 character country codes of supported countries.' 14 | ) 15 | .optional(), 16 | search_lang: z 17 | .string() 18 | .default('en') 19 | .describe( 20 | 'Search language preference. The 2 or more character language code for which the search results are provided.' 21 | ) 22 | .optional(), 23 | ui_lang: z 24 | .string() 25 | .default('en-US') 26 | .describe( 27 | 'User interface language preferred in response. Usually of the format -. For more, see RFC 9110.' 28 | ) 29 | .optional(), 30 | count: z 31 | .number() 32 | .int() 33 | .min(1) 34 | .max(50) 35 | .default(20) 36 | .describe('Number of results (1-50, default 20)') 37 | .optional(), 38 | offset: z 39 | .number() 40 | .int() 41 | .min(0) 42 | .max(9) 43 | .default(0) 44 | .describe('Pagination offset (max 9, default 0)') 45 | .optional(), 46 | spellcheck: z 47 | .boolean() 48 | .default(true) 49 | .describe('Whether to spellcheck provided query.') 50 | .optional(), 51 | safesearch: z 52 | .union([z.literal('off'), z.literal('moderate'), z.literal('strict')]) 53 | .default('moderate') 54 | .describe( 55 | "Filters search results for adult content. The following values are supported: 'off' - No filtering. 'moderate' - Filter out explicit content. 'strict' - Filter out explicit and suggestive content. The default value is 'moderate'." 56 | ) 57 | .optional(), 58 | freshness: z 59 | .union([z.literal('pd'), z.literal('pw'), z.literal('pm'), z.literal('py'), z.string()]) 60 | .default('pd') 61 | .describe( 62 | "Filters search results by when they were discovered. The following values are supported: 'pd' - Discovered within the last 24 hours. 'pw' - Discovered within the last 7 Days. 'pm' - Discovered within the last 31 Days. 'py' - Discovered within the last 365 Days. 'YYYY-MM-DDtoYYYY-MM-DD' - Timeframe is also supported by specifying the date range e.g. 2022-04-01to2022-07-30." 63 | ) 64 | .optional(), 65 | extra_snippets: z 66 | .boolean() 67 | .default(false) 68 | .describe( 69 | 'A snippet is an excerpt from a page you get as a result of the query, and extra_snippets allow you to get up to 5 additional, alternative excerpts. Only available under Free AI, Base AI, Pro AI, Base Data, Pro Data and Custom plans.' 70 | ) 71 | .optional(), 72 | goggles: z 73 | .array(z.string()) 74 | .describe( 75 | "Goggles act as a custom re-ranking on top of Brave's search index. The parameter supports both a url where the Goggle is hosted or the definition of the Goggle. For more details, refer to the Goggles repository (i.e., https://github.com/brave/goggles-quickstart)." 76 | ) 77 | .optional(), 78 | }); 79 | 80 | export type QueryParams = z.infer; 81 | 82 | export default params; 83 | -------------------------------------------------------------------------------- /marketplace-revision-release.json: -------------------------------------------------------------------------------- 1 | { 2 | "Catalog": "AWSMarketplace", 3 | "ChangeSet": [ 4 | { 5 | "ChangeType": "AddDeliveryOptions", 6 | "Entity": { 7 | "Type": "ContainerProduct@1.0", 8 | "Identifier": "prod-ixigokshzjrpw" 9 | }, 10 | "DetailsDocument": { 11 | "Version": { 12 | "ReleaseNotes": "", 13 | "VersionTitle": "" 14 | }, 15 | "DeliveryOptions": [ 16 | { 17 | "DeliveryOptionTitle": "Brave Search MCP Server", 18 | "Details": { 19 | "EcrDeliveryOptionDetails": { 20 | "UsageInstructions": "To obtain a Brave Search API key, go to https://aws.amazon.com/marketplace/pp/prodview-qjlabherxghtq and purchase a subscription. \n\nPlease refer to Brave Search MCP Server usage documentation (https://brave-marketplace-artifacts-prod.s3.us-west-2.amazonaws.com/brave-search-mcp-server/MCP_Server_Usage_Instructions_md.md) for more details about configuration and usage of Brave Search MCP Server", 21 | "AgenticType": [ 22 | "MCP_SERVER" 23 | ], 24 | "CompatibleServices": [ 25 | "Bedrock-AgentCore" 26 | ], 27 | "Description": "", 28 | "DeploymentResources": [ 29 | { 30 | "Url": "https://brave-marketplace-artifacts-prod.s3.us-west-2.amazonaws.com/brave-search-mcp-server/MCP_Server_Usage_Instructions_md.md", 31 | "Name": "Brave Search MCP Usage Instructions" 32 | } 33 | ], 34 | "ContainerImages": "" 35 | } 36 | } 37 | }, 38 | { 39 | "DeliveryOptionTitle": "Docker Image", 40 | "Details": { 41 | "EcrDeliveryOptionDetails": { 42 | "UsageInstructions": "", 43 | "CompatibleServices": [ 44 | "ECS", 45 | "ECS-Anywhere", 46 | "EKS", 47 | "EKS-Anywhere" 48 | ], 49 | "Description": "", 50 | "DeploymentResources": [], 51 | "ContainerImages": "" 52 | } 53 | } 54 | } 55 | ] 56 | } 57 | } 58 | ], 59 | "ChangeSetName": "", 60 | "ClientRequestToken": "", 61 | "ChangeSetTags": [ 62 | { 63 | "Key": "Release", 64 | "Value": "" 65 | }, 66 | { 67 | "Key": "CostEntity", 68 | "Value": "SEZC" 69 | }, 70 | { 71 | "Key": "IPGroup", 72 | "Value": "KY BAT Ledger Service" 73 | } 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /src/tools/videos/types.ts: -------------------------------------------------------------------------------- 1 | export interface VideoSearchApiResponse { 2 | /** The type of search API result. The value is always video. */ 3 | type: 'videos'; 4 | /** Video search query string. */ 5 | query: Query; 6 | /** The list of video results for the given query. */ 7 | results: VideoResult[]; 8 | /** Additional information about the video search results. */ 9 | extra: Extra; 10 | } 11 | 12 | interface Query { 13 | /** The original query that was requested. */ 14 | original: string; 15 | /** The altered query by the spellchecker. This is the query that is used to search if any. */ 16 | altered?: string; 17 | /** The cleaned noramlized query by the spellchecker. This is the query that is used to search if any. */ 18 | cleaned?: string; 19 | /** Whether the spellchecker is enabled or disabled. */ 20 | spellcheck_off?: boolean; 21 | /** The value is true if the lack of results is due to a strict safesearch setting. Adult content relevant to the query was found, but was blocked by safesearch. */ 22 | show_strict_warning?: string; 23 | } 24 | 25 | interface Thumbnail { 26 | /** The served URL of the thumbnail associated with the video. */ 27 | src: string; 28 | /** The original URL of the thumbnail associated with the video. */ 29 | original?: string; 30 | } 31 | 32 | interface Profile { 33 | /** The name of the profile. */ 34 | name: string; 35 | /** The long name of the profile. */ 36 | long_name?: string; 37 | /** The original URL where the profile is available. */ 38 | url: string; 39 | /** The served image URL representing the profile. */ 40 | img?: string; 41 | } 42 | 43 | interface VideoData { 44 | /** A time string representing the duration of the video. */ 45 | duration?: string; 46 | /** The number of views of the video. */ 47 | views?: number; 48 | /** The creator of the video. */ 49 | creator?: string; 50 | /** The publisher of the video. */ 51 | publisher?: string; 52 | /** Whether the video requires a subscription. */ 53 | requires_subscription?: boolean; 54 | /** A list of tags relevant to the video. */ 55 | tags?: string[]; 56 | /** A list of profiles associated with the video. */ 57 | author?: Profile; 58 | } 59 | 60 | interface MetaUrl { 61 | /** The protocol scheme extracted from the URL. */ 62 | scheme?: string; 63 | /** The network location part extracted from the URL. */ 64 | netloc?: string; 65 | /** The lowercased domain name extracted from the URL. */ 66 | hostname?: string; 67 | /** The favicon used for the URL. */ 68 | favicon?: string; 69 | /** The hierarchical path of the URL useful as a display string. */ 70 | path?: string; 71 | } 72 | 73 | export interface VideoResult { 74 | /** The type of video search API result. The value is always video_result. */ 75 | type: 'video_result'; 76 | /** The source URL of the video. */ 77 | url: string; 78 | /** The title of the video. */ 79 | title: string; 80 | /** The description for the video. */ 81 | description?: string; 82 | /** A human readable representation of the page age. */ 83 | age?: string; 84 | /** The page age found from the source web page. */ 85 | page_age?: string; 86 | /** The ISO date time when the page was last fetched. The format is YYYY-MM-DDTHH:MM:SSZ. */ 87 | page_fetched?: string; 88 | /** The thumbnail for the video. */ 89 | thumbnail?: Thumbnail; 90 | /** Metadata for the video. */ 91 | video?: VideoData; 92 | /** Aggregated information on the URL associated with the video search result. */ 93 | meta_url?: MetaUrl; 94 | } 95 | 96 | interface Extra { 97 | /** Indicates whether the video search results might contain offensive content. */ 98 | might_be_offensive: boolean; 99 | } 100 | -------------------------------------------------------------------------------- /src/tools/summarizer/index.ts: -------------------------------------------------------------------------------- 1 | import type { CallToolResult, ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'; 2 | import { summarizerQueryParams, type SummarizerQueryParams } from './params.js'; 3 | import API from '../../BraveAPI/index.js'; 4 | import { type SummarizerSearchApiResponse } from './types.js'; 5 | import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 6 | 7 | export const name = 'brave_summarizer'; 8 | 9 | export const annotations: ToolAnnotations = { 10 | title: 'Brave Summarizer', 11 | openWorldHint: true, 12 | }; 13 | 14 | export const description = ` 15 | Retrieves AI-generated summaries of web search results using Brave's Summarizer API. This tool processes search results to create concise, coherent summaries of information gathered from multiple sources. 16 | 17 | When to use: 18 | 19 | - When you need a concise overview of complex topics from multiple sources 20 | - For quick fact-checking or getting key points without reading full articles 21 | - When providing users with summarized information that synthesizes various perspectives 22 | - For research tasks requiring distilled information from web searches 23 | 24 | Returns a text summary that consolidates information from the search results. Optional features include inline references to source URLs and additional entity information. 25 | 26 | Requirements: Must first perform a web search using brave_web_search with summary=true parameter. Requires a Pro AI subscription to access the summarizer functionality. 27 | `; 28 | 29 | export const execute = async (params: SummarizerQueryParams) => { 30 | const response: CallToolResult = { content: [], isError: false }; 31 | 32 | try { 33 | const { summary } = await pollForSummary(params); 34 | 35 | if (!summary || summary.length === 0) { 36 | response.isError = true; 37 | response.content.push({ 38 | type: 'text' as const, 39 | text: 'Unable to retrieve a Summarizer summary.', 40 | }); 41 | } else { 42 | const summaryText = summary 43 | .map((summary_part) => { 44 | if (summary_part.type === 'token') { 45 | return summary_part.data; 46 | } else if (summary_part.type === 'inline_reference') { 47 | return ` (${summary_part.data?.url})`; 48 | } else { 49 | return ''; 50 | } 51 | }) 52 | .join(''); 53 | 54 | response.content.push({ 55 | type: 'text' as const, 56 | text: summaryText, 57 | }); 58 | } 59 | } catch (error) { 60 | response.isError = true; 61 | response.content.push({ 62 | type: 'text' as const, 63 | text: 'Unable to retrieve a Summarizer summary.', 64 | }); 65 | } 66 | 67 | return response; 68 | }; 69 | 70 | export const register = (mcpServer: McpServer) => { 71 | mcpServer.registerTool( 72 | name, 73 | { 74 | title: name, 75 | description: description, 76 | inputSchema: summarizerQueryParams.shape, 77 | annotations: annotations, 78 | }, 79 | execute 80 | ); 81 | }; 82 | 83 | const pollForSummary = async ( 84 | params: SummarizerQueryParams, 85 | pollInterval: number = 50, 86 | attempts: number = 20 87 | ): Promise => { 88 | let result: SummarizerSearchApiResponse | null = null; 89 | 90 | while (!result && attempts > 0) { 91 | try { 92 | const response = await API.issueRequest<'summarizer'>('summarizer', params); 93 | if (response.status === 'complete') { 94 | result = response; 95 | } 96 | } catch (error) { 97 | await new Promise((resolve) => setTimeout(resolve, pollInterval)); 98 | } 99 | 100 | attempts--; 101 | } 102 | 103 | if (!result) { 104 | throw new Error('Summarizer summary could not be retrieved after multiple attempts.'); 105 | } 106 | 107 | return result; 108 | }; 109 | 110 | export default { 111 | name, 112 | description, 113 | annotations, 114 | inputSchema: summarizerQueryParams.shape, 115 | execute, 116 | register, 117 | }; 118 | -------------------------------------------------------------------------------- /src/BraveAPI/index.ts: -------------------------------------------------------------------------------- 1 | import type { Endpoints } from './types.js'; 2 | import config from '../config.js'; 3 | import { stringify } from '../utils.js'; 4 | 5 | const typeToPathMap: Record = { 6 | images: '/res/v1/images/search', 7 | localPois: '/res/v1/local/pois', 8 | localDescriptions: '/res/v1/local/descriptions', 9 | news: '/res/v1/news/search', 10 | videos: '/res/v1/videos/search', 11 | web: '/res/v1/web/search', 12 | summarizer: '/res/v1/summarizer/search', 13 | }; 14 | 15 | const getDefaultRequestHeaders = (): Record => { 16 | return { 17 | Accept: 'application/json', 18 | 'Accept-Encoding': 'gzip', 19 | 'X-Subscription-Token': config.braveApiKey, 20 | }; 21 | }; 22 | 23 | const isValidGoggleURL = (url: string) => { 24 | try { 25 | // Only allow HTTPS URLs 26 | return new URL(url).protocol === 'https:'; 27 | } catch { 28 | return false; 29 | } 30 | }; 31 | 32 | async function issueRequest( 33 | endpoint: T, 34 | parameters: Endpoints[T]['params'], 35 | // TODO (Sampson): Implement support for custom request headers (helpful for POIs, etc.) 36 | requestHeaders: Endpoints[T]['requestHeaders'] = {} as Endpoints[T]['requestHeaders'] 37 | ): Promise { 38 | // TODO (Sampson): Improve rate-limit logic to support self-throttling and n-keys 39 | // checkRateLimit(); 40 | 41 | // Determine URL, and setup parameters 42 | const url = new URL(`https://api.search.brave.com${typeToPathMap[endpoint]}`); 43 | const queryParams = new URLSearchParams(); 44 | 45 | // TODO (Sampson): Move param-construction/validation to modules 46 | for (const [key, value] of Object.entries(parameters)) { 47 | // The 'ids' parameter is expected to appear multiple times for multiple IDs 48 | if (['localPois', 'localDescriptions'].includes(endpoint)) { 49 | if (key === 'ids') { 50 | if (Array.isArray(value) && value.length > 0) { 51 | value.forEach((id) => queryParams.append(key, id)); 52 | } else if (typeof value === 'string') { 53 | queryParams.set(key, value); 54 | } 55 | 56 | continue; 57 | } 58 | } 59 | 60 | // Handle `result_filter` parameter 61 | if (key === 'result_filter') { 62 | // Handle special behavior of 'summary' parameter: 63 | // Requires `result_filter` to be empty, or only contain 'summarizer' 64 | // see: https://bravesoftware.slack.com/archives/C01NNFM9XMM/p1751654841090929 65 | if ('summary' in parameters && parameters.summary === true) { 66 | queryParams.set(key, 'summarizer'); 67 | } else if (Array.isArray(value) && value.length > 0) { 68 | queryParams.set(key, value.join(',')); 69 | } 70 | 71 | continue; 72 | } 73 | 74 | // Handle `goggles` parameter(s) 75 | if (key === 'goggles') { 76 | if (typeof value === 'string') { 77 | queryParams.set(key, value); 78 | } else if (Array.isArray(value)) { 79 | for (const url of value.filter(isValidGoggleURL)) { 80 | queryParams.append(key, url); 81 | } 82 | } 83 | continue; 84 | } 85 | 86 | if (value !== undefined) { 87 | queryParams.set(key === 'query' ? 'q' : key, value.toString()); 88 | } 89 | } 90 | 91 | // Issue Request 92 | const urlWithParams = url.toString() + '?' + queryParams.toString(); 93 | const headers = { ...getDefaultRequestHeaders(), ...requestHeaders } as Headers; 94 | const response = await fetch(urlWithParams, { headers }); 95 | 96 | // Handle Error 97 | if (!response.ok) { 98 | let errorMessage = `${response.status} ${response.statusText}`; 99 | 100 | try { 101 | const responseBody = await response.json(); 102 | errorMessage += `\n${stringify(responseBody, true)}`; 103 | } catch (error) { 104 | errorMessage += `\n${await response.text()}`; 105 | } 106 | 107 | // TODO (Sampson): Setup proper error handling, updating state, etc. 108 | throw new Error(errorMessage); 109 | } 110 | 111 | // Return Response 112 | const responseBody = await response.json(); 113 | 114 | return responseBody as Endpoints[T]['response']; 115 | } 116 | 117 | export default { 118 | issueRequest, 119 | }; 120 | -------------------------------------------------------------------------------- /src/tools/summarizer/types.ts: -------------------------------------------------------------------------------- 1 | export interface SummarizerSearchApiResponse { 2 | /** The type of summarizer search API result. The value is always summarizer. */ 3 | type: 'summarizer'; 4 | /** The current status of summarizer for the given key. The value can be either failed or complete. */ 5 | status: string; 6 | /** The title for the summary. */ 7 | title?: string; 8 | /** Details for the summary message. */ 9 | summary?: SummaryMessage[]; 10 | /** Enrichments that can be added to the summary message. */ 11 | enrichments?: SummaryEnrichments; 12 | /** Followup queries relevant to the current query. */ 13 | followups?: string[]; 14 | /** Details on the entities in the summary message. */ 15 | entities_infos?: Record; 16 | } 17 | 18 | interface SummaryMessage { 19 | /** The type of subset of a summary message. The value can be token (a text excerpt from the summary), enum_item (a summary entity), enum_start (describes the beginning of summary entities, which means the following item(s) in the summary list will be entities), enum_end (the end of summary entities) or inline_reference (an inline reference to the summary, requires inline_references query param to be set). */ 20 | type: string; 21 | /** The summary entity or the explanation for the type field. For type enum_start the value can be ol or ul, which means an ordered list or an unordered list of entities follows respectively. For type enum_end there is no value. For type token the value is a text excerpt. For type enum_item the value is the SummaryEntity response model. For type inline_reference the value is the SummaryInlineReference response model. */ 22 | data?: SummaryEntity | SummaryInlineReference; 23 | } 24 | 25 | interface TextLocation { 26 | /** The 0-based index, where the important part of the text starts. */ 27 | start: number; 28 | /** The 0-based index, where the important part of the text ends. */ 29 | end: number; 30 | } 31 | 32 | interface SummaryEntity { 33 | /** A unique identifier for the entity. */ 34 | uuid: string; 35 | /** The name of the entity. */ 36 | name: string; 37 | /** The URL where further details on the entity can be found. */ 38 | url?: string; 39 | /** A text message describing the entity. */ 40 | text?: string; 41 | /** The image associated with the entity. */ 42 | images?: SummaryImage[]; 43 | /** The location of the entity in the summary message. */ 44 | highlight?: TextLocation[]; 45 | } 46 | 47 | interface SummaryInlineReference { 48 | /** The type of inline reference. The value is always inline_reference. */ 49 | type: string; 50 | /** The URL that is being referenced. */ 51 | url: string; 52 | /** The start index of the inline reference in the summary message. */ 53 | start_index: number; 54 | /** The end index of the inline reference in the summary message. */ 55 | end_index: number; 56 | /** The ordinal number of the inline reference. */ 57 | number: number; 58 | /** The favicon of the URL that is being referenced. */ 59 | favicon?: string; 60 | /** The reference text snippet. */ 61 | snippet?: string; 62 | } 63 | 64 | interface Thumbnail { 65 | /** The served URL of the picture thumbnail. */ 66 | src: string; 67 | /** The original URL of the image. */ 68 | original?: string; 69 | } 70 | 71 | interface ImageProperties { 72 | /** The image URL. */ 73 | url: string; 74 | } 75 | 76 | interface Image { 77 | /** The thumbnail associated with the image. */ 78 | thumbnail: Thumbnail; 79 | /** The URL of the image. */ 80 | url?: string; 81 | /** Metadata on the image. */ 82 | properties?: ImageProperties; 83 | } 84 | 85 | interface SummaryImage extends Image { 86 | /** Text associated with the image. */ 87 | text?: string; 88 | } 89 | 90 | interface SummaryEnrichments { 91 | /** The raw summary message. */ 92 | raw: string; 93 | /** The images associated with the summary. */ 94 | images?: SummaryImage[]; 95 | /** The answers in the summary message. */ 96 | qa?: SummaryAnswer[]; 97 | /** The entities in the summary message. */ 98 | entities?: SummaryEntity[]; 99 | /** References based on which the summary was built. */ 100 | context?: SummaryContext[]; 101 | } 102 | 103 | interface SummaryAnswer { 104 | /** The answer text. */ 105 | answer: string; 106 | /** A score associated with the answer. */ 107 | score?: number; 108 | /** The location of the answer in the summary message. */ 109 | highlight?: TextLocation; 110 | } 111 | 112 | interface MetaUrl { 113 | /** The protocol scheme extracted from the URL. */ 114 | scheme?: string; 115 | /** The network location part extracted from the URL. */ 116 | netloc?: string; 117 | /** The lowercased domain name extracted from the URL. */ 118 | hostname?: string; 119 | /** The favicon used for the URL. */ 120 | favicon?: string; 121 | /** The hierarchical path of the URL useful as a display string. */ 122 | path?: string; 123 | } 124 | 125 | interface SummaryContext { 126 | /** The title of the reference. */ 127 | title: string; 128 | /** The URL where the reference can be found. */ 129 | url: string; 130 | /** Details on the URL associated with the reference. */ 131 | meta_url?: MetaUrl; 132 | } 133 | 134 | interface SummaryEntityInfo { 135 | /** The name of the provider. */ 136 | provider?: string; 137 | /** Description of the entity. */ 138 | description?: string; 139 | } 140 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { LoggingLevel, LoggingLevelSchema } from '@modelcontextprotocol/sdk/types.js'; 2 | import { Command } from 'commander'; 3 | import dotenv from 'dotenv'; 4 | import { z } from 'zod'; 5 | import tools from './tools/index.js'; 6 | 7 | dotenv.config({ debug: false, quiet: true }); 8 | 9 | // Config schema for Smithery.ai 10 | export const configSchema = z.object({ 11 | braveApiKey: z.string().describe('Your API key'), 12 | enabledTools: z 13 | .array(z.string()) 14 | .describe('Enforces a tool whitelist (cannot be used with disabledTools)') 15 | .optional(), 16 | disabledTools: z 17 | .array(z.string()) 18 | .describe('Enforces a tool blacklist (cannot be used with enabledTools)') 19 | .optional(), 20 | loggingLevel: z 21 | .enum([ 22 | 'debug', 23 | 'error', 24 | 'info', 25 | 'notice', 26 | 'warning', 27 | 'critical', 28 | 'alert', 29 | 'emergency', 30 | ] as const) 31 | .default('info') 32 | .describe('Desired logging level') 33 | .optional(), 34 | }); 35 | 36 | export type SmitheryConfig = z.infer; 37 | 38 | type Configuration = { 39 | transport: 'stdio' | 'http'; 40 | port: number; 41 | host: string; 42 | braveApiKey: string; 43 | loggingLevel: LoggingLevel; 44 | enabledTools: string[]; 45 | disabledTools: string[]; 46 | }; 47 | 48 | const state: Configuration & { ready: boolean } = { 49 | transport: 'stdio', 50 | port: 8080, 51 | host: '0.0.0.0', 52 | braveApiKey: process.env.BRAVE_API_KEY ?? '', 53 | loggingLevel: 'info', 54 | ready: false, 55 | enabledTools: [], 56 | disabledTools: [], 57 | }; 58 | 59 | export function isToolPermittedByUser(toolName: string): boolean { 60 | return state.enabledTools.length > 0 61 | ? state.enabledTools.includes(toolName) 62 | : state.disabledTools.includes(toolName) === false; 63 | } 64 | 65 | export function getOptions(): Configuration | false { 66 | const program = new Command() 67 | .option('--brave-api-key ', 'Brave API key', process.env.BRAVE_API_KEY ?? '') 68 | .option('--logging-level ', 'Logging level', process.env.BRAVE_MCP_LOG_LEVEL ?? 'info') 69 | .option( 70 | '--transport ', 71 | 'transport type', 72 | process.env.BRAVE_MCP_TRANSPORT ?? 'stdio' 73 | ) 74 | .option( 75 | '--enabled-tools ', 76 | 'tools to enable', 77 | process.env.BRAVE_MCP_ENABLED_TOOLS?.split(' ') ?? [] 78 | ) 79 | .option( 80 | '--disabled-tools ', 81 | 'tools to disable', 82 | process.env.BRAVE_MCP_DISABLED_TOOLS?.split(' ') ?? [] 83 | ) 84 | .option( 85 | '--port ', 86 | 'desired port for HTTP transport', 87 | process.env.BRAVE_MCP_PORT ?? '8080' 88 | ) 89 | .option( 90 | '--host ', 91 | 'desired host for HTTP transport', 92 | process.env.BRAVE_MCP_HOST ?? '0.0.0.0' 93 | ) 94 | .allowUnknownOption() 95 | .parse(process.argv); 96 | 97 | const options = program.opts(); 98 | const toolNames = Object.values(tools).map((tool) => tool.name); 99 | 100 | // Validate tool inclusion configuration 101 | const { enabledTools, disabledTools } = options; 102 | 103 | if (enabledTools.length > 0 && disabledTools.length > 0) { 104 | console.error('Error: --enabled-tools and --disabled-tools cannot be used together'); 105 | return false; 106 | } 107 | 108 | if ([...enabledTools, ...disabledTools].some((t) => !toolNames.includes(t))) { 109 | console.error(`Invalid tool name used. Must be one of: ${toolNames.join(', ')}`); 110 | return false; 111 | } 112 | 113 | // Validate all other options 114 | if (!['stdio', 'http'].includes(options.transport)) { 115 | console.error( 116 | `Invalid --transport value: '${options.transport}'. Must be one of: stdio, http.` 117 | ); 118 | return false; 119 | } 120 | 121 | if (!LoggingLevelSchema.options.includes(options.loggingLevel)) { 122 | console.error( 123 | `Invalid --logging-level value: '${options.loggingLevel}'. Must be one of: ${LoggingLevelSchema.options.join(', ')}` 124 | ); 125 | return false; 126 | } 127 | 128 | if (!options.braveApiKey) { 129 | console.error( 130 | 'Error: --brave-api-key is required. You can get one at https://brave.com/search/api/.' 131 | ); 132 | return false; 133 | } 134 | 135 | if (options.transport === 'http') { 136 | if (options.port < 1 || options.port > 65535) { 137 | console.error( 138 | `Invalid --port value: '${options.port}'. Must be a valid port number between 1 and 65535.` 139 | ); 140 | return false; 141 | } 142 | 143 | if (!options.host) { 144 | console.error('Error: --host is required'); 145 | return false; 146 | } 147 | } 148 | 149 | // Update state 150 | state.braveApiKey = options.braveApiKey; 151 | state.transport = options.transport; 152 | state.port = options.port; 153 | state.host = options.host; 154 | state.loggingLevel = options.loggingLevel; 155 | state.enabledTools = options.enabledTools; 156 | state.disabledTools = options.disabledTools; 157 | state.ready = true; 158 | 159 | return options as Configuration; 160 | } 161 | 162 | export function setOptions(options: SmitheryConfig) { 163 | return Object.assign(state, options); 164 | } 165 | 166 | export default state; 167 | -------------------------------------------------------------------------------- /src/tools/web/index.ts: -------------------------------------------------------------------------------- 1 | import type { TextContent, ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'; 2 | import params, { type QueryParams } from './params.js'; 3 | import API from '../../BraveAPI/index.js'; 4 | import type { 5 | Discussions, 6 | FAQ, 7 | News, 8 | Search, 9 | Videos, 10 | FormattedFAQResults, 11 | FormattedDiscussionsResults, 12 | FormattedNewsResults, 13 | FormattedVideoResults, 14 | FormattedWebResults, 15 | } from './types.js'; 16 | import { stringify } from '../../utils.js'; 17 | import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 18 | 19 | export const name = 'brave_web_search'; 20 | 21 | export const annotations: ToolAnnotations = { 22 | title: 'Brave Web Search', 23 | openWorldHint: true, 24 | }; 25 | 26 | export const description = ` 27 | Performs web searches using the Brave Search API and returns comprehensive search results with rich metadata. 28 | 29 | When to use: 30 | - General web searches for information, facts, or current topics 31 | - Location-based queries (restaurants, businesses, points of interest) 32 | - News searches for recent events or breaking stories 33 | - Finding videos, discussions, or FAQ content 34 | - Research requiring diverse result types (web pages, images, reviews, etc.) 35 | 36 | Returns a JSON list of web results with title, description, and URL. 37 | 38 | When the "results_filter" parameter is empty, JSON results may also contain FAQ, Discussions, News, and Video results. 39 | `; 40 | 41 | export const execute = async (params: QueryParams) => { 42 | const response = { content: [] as TextContent[], isError: false }; 43 | const { web, faq, discussions, news, videos, summarizer } = await API.issueRequest<'web'>( 44 | 'web', 45 | params 46 | ); 47 | 48 | if (summarizer) { 49 | response.content.push({ 50 | type: 'text' as const, 51 | text: `Summarizer key: ${summarizer.key}`, 52 | }); 53 | } 54 | 55 | if (!web || !Array.isArray(web.results) || web.results.length < 1) { 56 | response.isError = true; 57 | response.content.push({ 58 | type: 'text' as const, 59 | text: 'No web results found', 60 | }); 61 | 62 | return response; 63 | } 64 | 65 | // TODO (Sampson): The following is unnecessarily repetitive. 66 | if (web && web.results?.length > 0) { 67 | for (const entry of formatWebResults(web)) { 68 | response.content.push({ 69 | type: 'text' as const, 70 | text: stringify(entry), 71 | }); 72 | } 73 | } 74 | 75 | if (faq && faq.results?.length > 0) { 76 | for (const entry of formatFAQResults(faq)) { 77 | response.content.push({ 78 | type: 'text' as const, 79 | text: stringify(entry), 80 | }); 81 | } 82 | } 83 | 84 | if (discussions && discussions.results?.length > 0) { 85 | for (const entry of formatDiscussionsResults(discussions)) { 86 | response.content.push({ 87 | type: 'text' as const, 88 | text: stringify(entry), 89 | }); 90 | } 91 | } 92 | 93 | if (news && news.results?.length > 0) { 94 | for (const entry of formatNewsResults(news)) { 95 | response.content.push({ 96 | type: 'text' as const, 97 | text: stringify(entry), 98 | }); 99 | } 100 | } 101 | 102 | if (videos && videos.results?.length > 0) { 103 | for (const entry of formatVideoResults(videos)) { 104 | response.content.push({ 105 | type: 'text' as const, 106 | text: stringify(entry), 107 | }); 108 | } 109 | } 110 | 111 | return response; 112 | }; 113 | 114 | export const formatWebResults = (web: Search): FormattedWebResults => { 115 | return (web.results || []).map(({ url, title, description, extra_snippets }) => ({ 116 | url, 117 | title, 118 | description, 119 | extra_snippets, 120 | })); 121 | }; 122 | 123 | export const register = (mcpServer: McpServer) => { 124 | mcpServer.registerTool( 125 | name, 126 | { 127 | title: name, 128 | description: description, 129 | inputSchema: params.shape, 130 | annotations: annotations, 131 | }, 132 | execute 133 | ); 134 | }; 135 | 136 | const formatFAQResults = (faq: FAQ): FormattedFAQResults => { 137 | return (faq.results || []).map(({ question, answer, title, url }) => ({ 138 | question, 139 | answer, 140 | title, 141 | url, 142 | })); 143 | }; 144 | 145 | const formatDiscussionsResults = (discussions: Discussions): FormattedDiscussionsResults => { 146 | return (discussions.results || []).map(({ url, data }) => ({ 147 | mutated_by_goggles: discussions.mutated_by_goggles, 148 | url, 149 | data, 150 | })); 151 | }; 152 | 153 | const formatNewsResults = (news: News): FormattedNewsResults => { 154 | return (news.results || []).map( 155 | ({ source, breaking, is_live, age, url, title, description, extra_snippets }) => ({ 156 | mutated_by_goggles: news.mutated_by_goggles, 157 | source, 158 | breaking, 159 | is_live, 160 | age, 161 | url, 162 | title, 163 | description, 164 | extra_snippets, 165 | }) 166 | ); 167 | }; 168 | 169 | const formatVideoResults = (videos: Videos): FormattedVideoResults => { 170 | return (videos.results || []).map(({ url, age, title, description, video, thumbnail }) => ({ 171 | mutated_by_goggles: videos.mutated_by_goggles, 172 | url, 173 | title, 174 | description, 175 | age, 176 | thumbnail_url: thumbnail?.src, 177 | duration: video.duration, 178 | view_count: video.views, 179 | creator: video.creator, 180 | publisher: video.publisher, 181 | tags: video.tags, 182 | })); 183 | }; 184 | 185 | export default { 186 | name, 187 | description, 188 | annotations, 189 | inputSchema: params.shape, 190 | execute, 191 | register, 192 | }; 193 | -------------------------------------------------------------------------------- /src/tools/web/params.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const params = z.object({ 4 | query: z 5 | .string() 6 | .max(400) 7 | .refine((str) => str.split(/\s+/).length <= 50, 'Query cannot exceed 50 words') 8 | .describe('Search query (max 400 chars, 50 words)'), 9 | country: z 10 | .enum([ 11 | 'ALL', 12 | 'AR', 13 | 'AU', 14 | 'AT', 15 | 'BE', 16 | 'BR', 17 | 'CA', 18 | 'CL', 19 | 'DK', 20 | 'FI', 21 | 'FR', 22 | 'DE', 23 | 'HK', 24 | 'IN', 25 | 'ID', 26 | 'IT', 27 | 'JP', 28 | 'KR', 29 | 'MY', 30 | 'MX', 31 | 'NL', 32 | 'NZ', 33 | 'NO', 34 | 'CN', 35 | 'PL', 36 | 'PT', 37 | 'PH', 38 | 'RU', 39 | 'SA', 40 | 'ZA', 41 | 'ES', 42 | 'SE', 43 | 'CH', 44 | 'TW', 45 | 'TR', 46 | 'GB', 47 | 'US', 48 | ]) 49 | .default('US') 50 | .describe( 51 | 'Search query country, where the results come from. The country string is limited to 2 character country codes of supported countries.' 52 | ) 53 | .optional(), 54 | search_lang: z 55 | .enum([ 56 | 'ar', 57 | 'eu', 58 | 'bn', 59 | 'bg', 60 | 'ca', 61 | 'zh-hans', 62 | 'zh-hant', 63 | 'hr', 64 | 'cs', 65 | 'da', 66 | 'nl', 67 | 'en', 68 | 'en-gb', 69 | 'et', 70 | 'fi', 71 | 'fr', 72 | 'gl', 73 | 'de', 74 | 'gu', 75 | 'he', 76 | 'hi', 77 | 'hu', 78 | 'is', 79 | 'it', 80 | 'jp', 81 | 'kn', 82 | 'ko', 83 | 'lv', 84 | 'lt', 85 | 'ms', 86 | 'ml', 87 | 'mr', 88 | 'nb', 89 | 'pl', 90 | 'pt-br', 91 | 'pt-pt', 92 | 'pa', 93 | 'ro', 94 | 'ru', 95 | 'sr', 96 | 'sk', 97 | 'sl', 98 | 'es', 99 | 'sv', 100 | 'ta', 101 | 'te', 102 | 'th', 103 | 'tr', 104 | 'uk', 105 | 'vi', 106 | ]) 107 | .default('en') 108 | .describe( 109 | 'Search language preference. The 2 or more character language code for which the search results are provided.' 110 | ) 111 | .optional(), 112 | ui_lang: z 113 | .enum([ 114 | 'es-AR', 115 | 'en-AU', 116 | 'de-AT', 117 | 'nl-BE', 118 | 'fr-BE', 119 | 'pt-BR', 120 | 'en-CA', 121 | 'fr-CA', 122 | 'es-CL', 123 | 'da-DK', 124 | 'fi-FI', 125 | 'fr-FR', 126 | 'de-DE', 127 | 'zh-HK', 128 | 'en-IN', 129 | 'en-ID', 130 | 'it-IT', 131 | 'ja-JP', 132 | 'ko-KR', 133 | 'en-MY', 134 | 'es-MX', 135 | 'nl-NL', 136 | 'en-NZ', 137 | 'no-NO', 138 | 'zh-CN', 139 | 'pl-PL', 140 | 'en-PH', 141 | 'ru-RU', 142 | 'en-ZA', 143 | 'es-ES', 144 | 'sv-SE', 145 | 'fr-CH', 146 | 'de-CH', 147 | 'zh-TW', 148 | 'tr-TR', 149 | 'en-GB', 150 | 'en-US', 151 | 'es-US', 152 | ]) 153 | .default('en-US') 154 | .describe( 155 | 'The language of the UI. The 2 or more character language code for which the search results are provided.' 156 | ) 157 | .optional(), 158 | count: z 159 | .number() 160 | .int() 161 | .min(1) 162 | .max(20) 163 | .default(10) 164 | .describe( 165 | 'Number of results (1-20, default 10). Applies only to web search results (i.e., has no effect on locations, news, videos, etc.)' 166 | ) 167 | .optional(), 168 | offset: z 169 | .number() 170 | .int() 171 | .min(0) 172 | .max(9) 173 | .default(0) 174 | .describe('Pagination offset (max 9, default 0)') 175 | .optional(), 176 | safesearch: z 177 | .enum(['off', 'moderate', 'strict']) 178 | .default('moderate') 179 | .describe( 180 | "Filters search results for adult content. The following values are supported: 'off' - No filtering. 'moderate' - Filters explicit content (e.g., images and videos), but allows adult domains in search results. 'strict' - Drops all adult content from search results. The default value is 'moderate'." 181 | ) 182 | .optional(), 183 | freshness: z 184 | .enum(['pd', 'pw', 'pm', 'py', 'YYYY-MM-DDtoYYYY-MM-DD']) 185 | .describe( 186 | "Filters search results by when they were discovered. The following values are supported: 'pd' - Discovered within the last 24 hours. 'pw' - Discovered within the last 7 days. 'pm' - Discovered within the last 31 days. 'py' - Discovered within the last 365 days. 'YYYY-MM-DDtoYYYY-MM-DD' - Timeframe is also supported by specifying the date range e.g. 2022-04-01to2022-07-30." 187 | ) 188 | .optional(), 189 | text_decorations: z 190 | .boolean() 191 | .default(true) 192 | .describe( 193 | 'Whether display strings (e.g. result snippets) should include decoration markers (e.g. highlighting characters).' 194 | ) 195 | .optional(), 196 | spellcheck: z 197 | .boolean() 198 | .default(true) 199 | .describe('Whether to spellcheck the provided query.') 200 | .optional(), 201 | result_filter: z 202 | .array( 203 | z.enum([ 204 | 'discussions', 205 | 'faq', 206 | 'infobox', 207 | 'news', 208 | 'query', 209 | 'summarizer', 210 | 'videos', 211 | 'web', 212 | 'locations', 213 | 'rich', 214 | ]) 215 | ) 216 | .default(['web', 'query']) 217 | .describe("Result filter (default ['web', 'query'])") 218 | .optional(), 219 | goggles: z 220 | .array(z.string()) 221 | .describe( 222 | "Goggles act as a custom re-ranking on top of Brave's search index. The parameter supports both a url where the Goggle is hosted or the definition of the Goggle. For more details, refer to the Goggles repository (i.e., https://github.com/brave/goggles-quickstart)." 223 | ) 224 | .optional(), 225 | units: z 226 | .union([z.literal('metric'), z.literal('imperial')]) 227 | .describe('The measurement units. If not provided, units are derived from search country.') 228 | .optional(), 229 | extra_snippets: z 230 | .boolean() 231 | .describe( 232 | 'A snippet is an excerpt from a page you get as a result of the query, and extra_snippets allow you to get up to 5 additional, alternative excerpts. Only available under Free AI, Base AI, Pro AI, Base Data, Pro Data and Custom plans.' 233 | ) 234 | .optional(), 235 | summary: z 236 | .boolean() 237 | .describe( 238 | 'This parameter enables summary key generation in web search results. This is required for summarizer to be enabled.' 239 | ) 240 | .optional(), 241 | }); 242 | 243 | export type QueryParams = z.infer; 244 | 245 | export default params; 246 | -------------------------------------------------------------------------------- /src/tools/local/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | LocationResult, 3 | LocationDescription, 4 | OpeningHours, 5 | DayOpeningHours, 6 | } from './types.js'; 7 | import webParams, { type QueryParams as WebQueryParams } from '../web/params.js'; 8 | import type { CallToolResult, ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'; 9 | import API from '../../BraveAPI/index.js'; 10 | import { formatWebResults } from '../web/index.js'; 11 | import { stringify } from '../../utils.js'; 12 | import { type WebSearchApiResponse } from '../web/types.js'; 13 | import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 14 | 15 | export const name = 'brave_local_search'; 16 | 17 | export const annotations: ToolAnnotations = { 18 | title: 'Brave Local Search', 19 | openWorldHint: true, 20 | }; 21 | 22 | export const description = ` 23 | Brave Local Search API provides enrichments for location search results. Access to this API is available only through the Brave Search API Pro plans; confirm the user's plan before using this tool (if the user does not have a Pro plan, use the brave_web_search tool). Searches for local businesses and places using Brave's Local Search API. Best for queries related to physical locations, businesses, restaurants, services, etc. 24 | 25 | Returns detailed information including: 26 | - Business names and addresses 27 | - Ratings and review counts 28 | - Phone numbers and opening hours 29 | 30 | Use this when the query implies 'near me', 'in my area', or mentions specific locations (e.g., 'in San Francisco'). This tool automatically falls back to brave_web_search if no local results are found. 31 | `; 32 | 33 | // Access to Local API is available through the Pro plans. 34 | export const execute = async (params: WebQueryParams) => { 35 | // Make sure both 'web' and 'locations' are in the result_filter 36 | params = { ...params, result_filter: [...(params.result_filter || []), 'web', 'locations'] }; 37 | 38 | // Starts with a web search to retrieve potential location IDs 39 | const { locations, web: web_fallback } = await API.issueRequest<'web'>('web', params); 40 | 41 | // We can send up to 20 location IDs at a time to the Local API 42 | // TODO (Sampson): Add support for multiple requests 43 | const locationIDs = (locations?.results || []).map((poi) => poi.id as string).slice(0, 20); 44 | 45 | // No locations were found - user's plan may not include access to the Local API 46 | if (!locations || locationIDs.length === 0) { 47 | // If we have web results, but no locations, we'll fall back to the web results 48 | if (web_fallback && web_fallback.results.length > 0) { 49 | return buildFallbackWebResponse(web_fallback); 50 | } 51 | 52 | // If we have no web results, we'll send a message to the user 53 | return { 54 | content: [ 55 | { 56 | type: 'text' as const, 57 | text: "No location data was returned. User's plan does not support local search, or the query may be unclear.", 58 | }, 59 | ], 60 | }; 61 | } 62 | 63 | // Fetch AI-generated descriptions 64 | const descriptions = await API.issueRequest<'localDescriptions'>('localDescriptions', { 65 | ids: locationIDs, 66 | }); 67 | 68 | return { 69 | content: formatLocalResults(locations.results, descriptions.results).map((formattedPOI) => ({ 70 | type: 'text' as const, 71 | text: formattedPOI, 72 | })), 73 | }; 74 | }; 75 | 76 | export const register = (mcpServer: McpServer) => { 77 | mcpServer.registerTool( 78 | name, 79 | { 80 | title: name, 81 | description: description, 82 | inputSchema: webParams.shape, 83 | annotations: annotations, 84 | }, 85 | execute 86 | ); 87 | }; 88 | 89 | const buildFallbackWebResponse = (web_fallback: WebSearchApiResponse['web']): CallToolResult => { 90 | if (!web_fallback || web_fallback.results.length === 0) throw new Error('No web results found'); 91 | 92 | const fallback = { 93 | content: [ 94 | { 95 | type: 'text' as const, 96 | text: "No location data was returned. Either the user's plan does not support local search, or the API was unable to find locations for the provided query. Falling back to general web search.", 97 | }, 98 | ], 99 | }; 100 | 101 | for (const web_result of formatWebResults(web_fallback)) { 102 | fallback.content.push({ 103 | type: 'text' as const, 104 | text: stringify(web_result), 105 | }); 106 | } 107 | 108 | return fallback; 109 | }; 110 | 111 | const formatLocalResults = ( 112 | poisData: LocationResult[], 113 | descData: LocationDescription[] = [] 114 | ): string[] => { 115 | return poisData.map((poi) => { 116 | return stringify({ 117 | name: poi.title, 118 | price_range: poi.price_range, 119 | phone: poi.contact?.telephone, 120 | rating: poi.rating?.ratingValue, 121 | hours: formatOpeningHours(poi.opening_hours), 122 | rating_count: poi.rating?.reviewCount, 123 | description: descData.find(({ id }) => id === poi.id)?.description, 124 | address: poi.postal_address?.displayAddress, 125 | }); 126 | }); 127 | }; 128 | 129 | const formatOpeningHours = (openingHours?: OpeningHours): Record | undefined => { 130 | if (!openingHours) return undefined; 131 | /** 132 | * Response will be something like { 133 | * 'sunday': '10:00-18:00', 134 | * 'monday': '10:00-18:00', 135 | * 'tuesday': '10:00-18:00', 136 | * 'wednesday': '10:00-18:00, 19:00-22:00', 137 | * 'thursday': '10:00-18:00', 138 | * 'friday': '10:00-18:00', 139 | * 'saturday': '12:00-18:00', 140 | * } 141 | */ 142 | const today: DayOpeningHours[] = openingHours.current_day || []; 143 | const response = {} as Record; 144 | 145 | const dayHours: [string, string[]][] = [ 146 | [ 147 | `today (${today[0].full_name.toLowerCase()})`, 148 | today.map(({ opens, closes }) => `${opens}-${closes}`), 149 | ], 150 | ]; 151 | 152 | // Add the rest of the days to the response 153 | for (let parts of openingHours.days || []) { 154 | // Not all days have arrays of hours, so normalize to an array 155 | if (!Array.isArray(parts)) parts = [parts]; 156 | 157 | // Add the hours for each day to the response 158 | for (const { full_name, opens, closes } of parts) { 159 | const dayName = full_name.toLowerCase(); 160 | const existingEntry = dayHours.find(([name]) => name === dayName); 161 | 162 | existingEntry 163 | ? existingEntry[1].push(`${opens}-${closes}`) 164 | : dayHours.push([dayName, [`${opens}-${closes}`]]); 165 | } 166 | } 167 | 168 | for (const [name, hours] of dayHours) { 169 | response[name] = hours.join(', '); 170 | } 171 | 172 | return response; 173 | }; 174 | 175 | export default { 176 | name, 177 | description, 178 | annotations, 179 | inputSchema: webParams.shape, 180 | execute, 181 | register, 182 | }; 183 | -------------------------------------------------------------------------------- /src/tools/local/types.ts: -------------------------------------------------------------------------------- 1 | export interface LocalPoiSearchApiResponse { 2 | /** The type of local POI search API result. The value is always local_pois. */ 3 | type: 'local_pois'; 4 | /** Location results matching the ids in the request. */ 5 | results?: LocationResult[]; 6 | } 7 | 8 | export interface LocalDescriptionsSearchApiResponse { 9 | /** The type of local description search API result. The value is always local_descriptions. */ 10 | type: 'local_descriptions'; 11 | /** Location descriptions matching the ids in the request. */ 12 | results?: LocationDescription[]; 13 | } 14 | 15 | export interface LocationDescription { 16 | /** The type of a location description. The value is always local_description. */ 17 | type: 'local_description'; 18 | /** A Temporary id of the location with this description. */ 19 | id: string; 20 | /** AI generated description of the location with the given id. */ 21 | description?: string; 22 | } 23 | 24 | interface Result { 25 | /** The title of the web page. */ 26 | title: string; 27 | /** The URL where the page is served. */ 28 | url: string; 29 | is_source_local: boolean; 30 | is_source_both: boolean; 31 | /** A description for the web page. */ 32 | description?: string; 33 | /** A date representing the age of the web page. */ 34 | page_age?: string; 35 | /** A date representing when the web page was last fetched. */ 36 | page_fetched?: string; 37 | /** A profile associated with the web page. */ 38 | profile?: Profile; 39 | /** A language classification for the web page. */ 40 | language?: string; 41 | /** Whether the web page is family friendly. */ 42 | family_friendly: boolean; 43 | } 44 | 45 | interface Profile { 46 | /** The name of the profile. */ 47 | name: string; 48 | /** The long name of the profile. */ 49 | long_name: string; 50 | /** The original URL where the profile is available. */ 51 | url?: string; 52 | /** The served image URL representing the profile. */ 53 | img?: string; 54 | } 55 | 56 | export interface LocationResult extends Result { 57 | /** Location result type identifier. The value is always location_result. */ 58 | type: 'location_result'; 59 | /** A Temporary id associated with this result, which can be used to retrieve extra information about the location. It remains valid for 8 hours… */ 60 | id?: string; 61 | /** The complete URL of the provider. */ 62 | provider_url: string; 63 | /** A list of coordinates associated with the location. This is a lat long represented as a floating point. */ 64 | coordinates?: number[]; 65 | /** The zoom level on the map. */ 66 | zoom_level: number; 67 | /** The thumbnail associated with the location. */ 68 | thumbnail?: Thumbnail; 69 | /** The postal address associated with the location. */ 70 | postal_address?: PostalAddress; 71 | /** The opening hours, if it is a business, associated with the location . */ 72 | opening_hours?: OpeningHours; 73 | /** The contact of the business associated with the location. */ 74 | contact?: Contact; 75 | /** A display string used to show the price classification for the business. */ 76 | price_range?: string; 77 | /** The ratings of the business. */ 78 | rating?: Rating; 79 | /** The distance of the location from the client. */ 80 | distance?: Unit; 81 | /** Profiles associated with the business. */ 82 | profiles?: DataProvider[]; 83 | /** Aggregated reviews from various sources relevant to the business. */ 84 | reviews?: Reviews; 85 | /** A bunch of pictures associated with the business. */ 86 | pictures?: PictureResults; 87 | /** An action to be taken. */ 88 | action?: Action; 89 | /** A list of cuisine categories served. */ 90 | serves_cuisine?: string[]; 91 | /** A list of categories. */ 92 | categories?: string[]; 93 | /** An icon category. */ 94 | icon_category?: string; 95 | /** Web results related to this location. */ 96 | results?: LocationWebResult; 97 | /** IANA timezone identifier. */ 98 | timezone?: string; 99 | /** The utc offset of the timezone. */ 100 | timezone_offset?: string; 101 | } 102 | 103 | interface Thumbnail { 104 | /** The served URL of the picture thumbnail. */ 105 | src: string; 106 | /** The original URL of the image. */ 107 | original?: string; 108 | } 109 | 110 | interface PostalAddress { 111 | /** The type identifying a postal address. The value is always PostalAddress. */ 112 | type: 'PostalAddress'; 113 | /** The country associated with the location. */ 114 | country?: string; 115 | /** The postal code associated with the location. */ 116 | postalCode?: string; 117 | /** The street address associated with the location. */ 118 | streetAddress?: string; 119 | /** The region associated with the location. This is usually a state. */ 120 | addressRegion?: string; 121 | /** The address locality or subregion associated with the location. */ 122 | addressLocality?: string; 123 | /** The displayed address string. */ 124 | displayAddress: string; 125 | } 126 | 127 | export interface OpeningHours { 128 | /** The current day opening hours. Can have two sets of opening hours. */ 129 | current_day?: DayOpeningHours[]; 130 | /** The opening hours for the whole week. */ 131 | days?: DayOpeningHours[] | [DayOpeningHours][]; 132 | } 133 | 134 | interface Contact { 135 | /** The email address. */ 136 | email?: string; 137 | /** The telephone number. */ 138 | telephone?: string; 139 | } 140 | 141 | interface Rating { 142 | /** The current value of the rating. */ 143 | ratingValue: number; 144 | /** Best rating received. */ 145 | bestRating: number; 146 | /** The number of reviews associated with the rating. */ 147 | reviewCount?: number; 148 | /** The profile associated with the rating. */ 149 | profile?: Profile; 150 | /** Whether the rating is coming from Tripadvisor. */ 151 | is_tripadvisor: boolean; 152 | } 153 | 154 | interface Unit { 155 | /** The quantity of the unit. */ 156 | value: number; 157 | /** The name of the unit associated with the quantity. */ 158 | units: string; 159 | } 160 | 161 | interface DataProvider { 162 | /** The type representing the source of data. This is usually external. */ 163 | type: 'external'; 164 | /** The name of the data provider. This can be a domain. */ 165 | name: string; 166 | /** The URL where the information is coming from. */ 167 | url: string; 168 | /** The long name for the data provider. */ 169 | long_name?: string; 170 | /** The served URL for the image data. */ 171 | img?: string; 172 | } 173 | 174 | interface Reviews { 175 | /** A list of trip advisor reviews for the entity. */ 176 | results: TripAdvisorReview[]; 177 | /** A URL to a web page where more information on the result can be seen. */ 178 | viewMoreUrl: string; 179 | /** Any reviews available in a foreign language. */ 180 | reviews_in_foreign_language: boolean; 181 | } 182 | 183 | interface PictureResults { 184 | /** A URL to view more pictures. */ 185 | viewMoreUrl?: string; 186 | /** A list of thumbnail results. */ 187 | results: Thumbnail[]; 188 | } 189 | 190 | interface Action { 191 | /** The type representing the action. */ 192 | type: string; 193 | /** A URL representing the action to be taken. */ 194 | url: string; 195 | } 196 | 197 | interface LocationWebResult extends Result { 198 | /** Aggregated information about the URL. */ 199 | meta_url: MetaUrl; 200 | } 201 | 202 | interface MetaUrl { 203 | /** The protocol scheme extracted from the URL. */ 204 | scheme: string; 205 | /** The network location part extracted from the URL. */ 206 | netloc: string; 207 | /** The lowercased domain name extracted from the URL. */ 208 | hostname?: string; 209 | /** The favicon used for the URL. */ 210 | favicon: string; 211 | /** The hierarchical path of the URL useful as a display string. */ 212 | path: string; 213 | } 214 | 215 | export interface DayOpeningHours { 216 | /** A short string representing the day of the week. */ 217 | abbr_name: string; 218 | /** A full string representing the day of the week. */ 219 | full_name: string; 220 | /** A 24 hr clock time string for the opening time of the business on a particular day. */ 221 | opens: string; 222 | /** A 24 hr clock time string for the closing time of the business on a particular day. */ 223 | closes: string; 224 | } 225 | 226 | interface TripAdvisorReview { 227 | /** The title of the review. */ 228 | title: string; 229 | /** A description seen in the review. */ 230 | description: string; 231 | /** The date when the review was published. */ 232 | date: string; 233 | /** A rating given by the reviewer. */ 234 | rating: Rating; 235 | /** The author of the review. */ 236 | author: Person; 237 | /** A URL link to the page where the review can be found. */ 238 | review_url: string; 239 | /** The language of the review. */ 240 | language: string; 241 | } 242 | 243 | interface Person extends Omit { 244 | /** A type identifying a person. The value is always person. */ 245 | type: 'person'; 246 | /** Email address of the person. */ 247 | email?: string; 248 | } 249 | 250 | interface Thing { 251 | /** A type identifying a thing. The value is always thing. */ 252 | type: 'thing'; 253 | /** The name of the thing. */ 254 | name: string; 255 | /** A URL for the thing. */ 256 | url?: string; 257 | /** Thumbnail associated with the thing. */ 258 | thumbnail?: Thumbnail; 259 | } 260 | -------------------------------------------------------------------------------- /.github/workflows/build-release.yml: -------------------------------------------------------------------------------- 1 | name: Build Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | 10 | permissions: 11 | # Give the default GITHUB_TOKEN write permission to commit and push the 12 | # added or changed files to the repository. 13 | contents: write 14 | # Allow the job to send a JWT token request to the OIDC provider 15 | id-token: write 16 | 17 | steps: 18 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 19 | with: 20 | ref: ${{ github.head_ref }} 21 | 22 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 23 | with: 24 | node-version: '24.x' 25 | registry-url: 'https://registry.npmjs.org' 26 | 27 | - name: Configure Git 28 | run: | 29 | git config --global user.name "github-actions[bot]" 30 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 31 | 32 | - name: Install dependencies 33 | run: npm ci 34 | 35 | # Install latest version of jq 36 | - name: Install latest version of jq to populate Marketplace Catalog StartChangeSet to publish new revision 37 | uses: vegardit/gha-setup-jq@491c577e0d5e6512cf02b06cf439b1fc4165aad1 # v1.0.0 38 | with: 39 | version: "latest" 40 | 41 | - name: Bump version 42 | id: bump-version 43 | run: | 44 | TAG=$(npm version patch --no-git-tag-version) 45 | VERSION=${TAG#v} 46 | jq --arg v "$VERSION" '(.version,.packages[].version) = $v' server.json > tmp && mv tmp server.json 47 | echo "tag=${TAG}" >> $GITHUB_OUTPUT 48 | echo "version=${VERSION}" >> $GITHUB_OUTPUT 49 | 50 | - name: Build assets 51 | run: npm run build 52 | 53 | - name: Commit build assets 54 | run: | 55 | git add package.json package-lock.json server.json 56 | git commit -m "ci: update build assets (${{ steps.bump-version.outputs.tag }})" 57 | git push 58 | 59 | - name: Generate Changelog 60 | id: changelog 61 | run: | 62 | echo "# Changelog for ${{ steps.bump-version.outputs.tag }}" > ${{ github.workspace }}/RELEASE_NOTES.md 63 | echo "* Updated version" >> ${{ github.workspace }}/RELEASE_NOTES.md 64 | echo "release_notes<> $GITHUB_OUTPUT 65 | cat ${{ github.workspace }}/RELEASE_NOTES.md >> $GITHUB_OUTPUT 66 | echo "EOF" >> $GITHUB_OUTPUT 67 | 68 | - name: Create release 69 | uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 70 | id: create_release 71 | with: 72 | body_path: ${{ github.workspace }}/RELEASE_NOTES.md 73 | tag_name: ${{ steps.bump-version.outputs.tag }} 74 | generate_release_notes: true 75 | make_latest: true 76 | 77 | - name: Publish NPM package 78 | run: npm publish --no-git-checks --access public 79 | env: 80 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 81 | 82 | # DockerHub authentication 83 | - name: Login to Docker Hub 84 | id: login-docker 85 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 86 | with: 87 | username: ${{ vars.DOCKERHUB_USERNAME }} 88 | password: ${{ secrets.DOCKERHUB_TOKEN }} 89 | 90 | # AWS Marketplace ECR authentication 91 | - name: Configure AWS Credentials to deploy to AWS Marketplace ECR 92 | uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1 93 | with: 94 | role-to-assume: ${{ secrets.MARKETPL_AWS_IAM_ROLE_ARN }} 95 | aws-region: us-east-1 96 | 97 | - name: Login to Amazon ECR 98 | id: login-ecr 99 | uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1 100 | with: 101 | registries: ${{ secrets.MARKETPL_ECR_AWS_ACCOUNT_ID }} 102 | 103 | # Build docker image to support multi-architecture (https://aws.amazon.com/blogs/containers/introducing-multi-architecture-container-images-for-amazon-ecr/) 104 | # Extend Docker build capabilities by using Buildx (https://github.com/docker/buildx) 105 | - name: Set up QEMU to support multi-architecute builds 106 | uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 107 | 108 | - name: Set up Docker Buildx 109 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 110 | 111 | - name: Set GitHub short SHA Tag 112 | id: vars 113 | run: echo "short_sha=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" 114 | 115 | - name: Validate Docker Build Configuration 116 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 117 | with: 118 | call: check 119 | 120 | # Push to DockerHub (per production best practice, remove 'latest' tag) 121 | # Push to AWS Marketplace-owned ECR Repository (https://docs.aws.amazon.com/marketplace/latest/userguide/container-product-policies.html) 122 | - name: Build, tag and push to Docker Hub and AWS Marketplace ECR 123 | env: 124 | DOCKER_REGISTRY: ${{ github.repository }} 125 | MARKETPL_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 126 | MARKETPL_REPOSITORY: brave/brave-search-mcp 127 | IMAGE_RELEASE_TAG: ${{ steps.bump-version.outputs.tag }} 128 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 129 | with: 130 | platforms: linux/amd64,linux/aarch64 131 | push: true 132 | sbom: false 133 | provenance: false 134 | tags: | 135 | ${{ env.DOCKER_REGISTRY }}:${{ env.IMAGE_RELEASE_TAG }} 136 | ${{ env.DOCKER_REGISTRY }}:${{ github.sha }} 137 | ${{ env.DOCKER_REGISTRY }}:${{ steps.vars.outputs.short_sha }} 138 | ${{ env.MARKETPL_REGISTRY }}/${{ env.MARKETPL_REPOSITORY }}:${{ env.IMAGE_RELEASE_TAG }} 139 | ${{ env.MARKETPL_REGISTRY }}/${{ env.MARKETPL_REPOSITORY }}:${{ github.sha }} 140 | ${{ env.MARKETPL_REGISTRY }}/${{ env.MARKETPL_REPOSITORY }}:${{ steps.vars.outputs.short_sha }} 141 | 142 | # Publish to MCP Registry 143 | # Reference: https://github.com/modelcontextprotocol/registry/blob/main/docs/guides/publishing/publish-server.md 144 | - name: Install MCP Publisher 145 | run: | 146 | curl -L "https://github.com/modelcontextprotocol/registry/releases/download/v1.4.0/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher 147 | 148 | - name: Login to MCP Registry 149 | run: ./mcp-publisher login github-oidc 150 | 151 | - name: Publish to MCP Registry 152 | run: ./mcp-publisher publish 153 | 154 | # Update JSON template to deploy Brave Search MCP Server to AWS Marketplace Management Portal 155 | # Update Release Version 156 | - name: Update Release Version Title in marketplace-revision-release.json 157 | env: 158 | IMAGE_RELEASE_TAG: ${{ steps.bump-version.outputs.tag }} 159 | run: | 160 | jq --arg tag "$IMAGE_RELEASE_TAG" '.ChangeSet[].DetailsDocument.Version.VersionTitle = "Release Version \($tag)' marketplace-revision-release.json > tmp.json && mv tmp.json marketplace-revision-release.json 161 | jq --arg tag "$IMAGE_RELEASE_TAG" '.ChangeSetTags[] |= (if .Key == "Release" then .Value = $tag else . end)' marketplace-revision-release.json > tmp.json && mv tmp.json marketplace-revision-release.json 162 | # Update Release Notes 163 | - name: Update Release Notes in marketplace-revision-release.json 164 | env: 165 | RELEASE_NOTES: ${{ steps.changelog.outputs.release_notes }} 166 | run: | 167 | jq --arg notes "$RELEASE_NOTES" '.ChangeSet[].DetailsDocument.Version.ReleaseNotes = $notes' marketplace-revision-release.json > tmp.json && mv tmp.json marketplace-revision-release.json 168 | 169 | # Update Version Title 170 | - name: Update Version Title in marketplace-revision-release.json 171 | env: 172 | TITLE: 'Release ${{ steps.bump-version.outputs.tag }}' 173 | run: | 174 | jq --arg title "$TITLE" '.ChangeSet[].DetailsDocument.Version.VersionTitle = $title' marketplace-revision-release.json > tmp.json && mv tmp.json marketplace-revision-release.json 175 | 176 | # Generate ClientRequestToken 177 | # See https://docs.aws.amazon.com/marketplace/latest/APIReference/API_StartChangeSet.html 178 | - name: Generate UUID for ClientRequestToken used with StartChangeSet 179 | id: custom_token 180 | run: echo "client_request_token=$(uuidgen)" >> "$GITHUB_OUTPUT" 181 | 182 | - name: Update ClientRequestToken in marketplace-revision-release.json 183 | env: 184 | TOKEN: ${{ steps.custom_token.outputs.client_request_token }} 185 | run: | 186 | jq --arg token "$TOKEN" '.ClientRequestToken = $token' marketplace-revision-release.json > tmp.json && mv tmp.json marketplace-revision-release.json 187 | 188 | # Update DeliveryOptions 189 | - name: Update DeliveryOptions images in marketplace-revision-release.json 190 | env: 191 | IMAGES: '["${{ steps.login-ecr.outputs.registry }}/brave/brave-search-mcp:${{ steps.bump-version.outputs.tag }}"]' 192 | run: | 193 | jq --argjson imgs "$IMAGES" '.ChangeSet[].DetailsDocument.DeliveryOptions[].Details.EcrDeliveryOptionDetails.ContainerImages = $imgs' marketplace-revision-release.json > tmp.json && mv tmp.json marketplace-revision-release.json 194 | 195 | - name: Update DeliveryOptions Description for Brave Search MCP Server option in marketplace-revision-release.json 196 | env: 197 | DESCRIPTION: "docker pull ${{ steps.login-ecr.outputs.registry }}/brave/brave-search-mcp:${{ steps.bump-version.outputs.tag }}" 198 | run: | 199 | jq --arg desc "$DESCRIPTION" '.ChangeSet[].DetailsDocument.DeliveryOptions[] |= (if .DeliveryOptionTitle == "Brave Search MCP Server" then .Details.EcrDeliveryOptionDetails.Description = $desc else . end)' marketplace-revision-release.json > tmp.json && mv tmp.json marketplace-revision-release.json 200 | 201 | - name: Update DeliveryOptions Usage Instructions for Docker Image option in marketplace-revision-release.json 202 | env: 203 | USAGE: 'docker run --rm -ti -p 8080:8080 --cap-drop all --read-only -e BRAVE_API_KEY=\"XXXX\" -e BRAVE_MCP_TRANSPORT=http -e BRAVE_MCP_PORT=8080 -e BRAVE_MCP_HOST=0.0.0.0 ${{ steps.login-ecr.outputs.registry }}/brave/brave-search-mcp:${{ steps.bump-version.outputs.tag }}' 204 | run: | 205 | jq --arg usage "$USAGE" '.ChangeSet[].DetailsDocument.DeliveryOptions[] |= (if .DeliveryOptionTitle == "Docker Image" then .Details.EcrDeliveryOptionDetails.UsageInstructions = $usage else . end)' marketplace-revision-release.json > tmp.json && mv tmp.json marketplace-revision-release.json 206 | 207 | - name: Update DeliveryOptions Description for Docker Image option in marketplace-revision-release.json 208 | env: 209 | DESCRIPTION: 'To obtain a Brave Search API key, go to https://aws.amazon.com/marketplace/pp/prodview-qjlabherxghtq and purchase a subscription within the AWS account where Brave Search MCP Server will be used. \n\nPlease refer to AWS ECS or EKS documentation for configuring workloads to provide the environment variables referenced in the Usage Instructions.' 210 | run: | 211 | jq --arg desc "$DESCRIPTION" '.ChangeSet[].DetailsDocument.DeliveryOptions[] |= (if .DeliveryOptionTitle == "Docker Image" then .Details.EcrDeliveryOptionDetails.Description = $desc else . end)' marketplace-revision-release.json > tmp.json && mv tmp.json marketplace-revision-release.json 212 | 213 | - name: Update ChangeSetName in marketplace-revision-release.json 214 | env: 215 | NAME: 'Add Version ${{ steps.bump-version.outputs.tag }} Release' 216 | run: | 217 | jq --arg name "$NAME" '.ChangeSetName = $name' marketplace-revision-release.json > tmp.json && mv tmp.json marketplace-revision-release.json 218 | 219 | - name: Validate marketplace-revision-release.json contents 220 | run: 221 | jq . marketplace-revision-release.json 222 | 223 | - name: Publish New Release to AWS Marketplace Catalog Management Portal 224 | run: | 225 | aws marketplace-catalog start-change-set \ 226 | --cli-input-json file://marketplace-revision-release.json 227 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Brave Search MCP Server 2 | 3 | An MCP server implementation that integrates the Brave Search API, providing comprehensive search capabilities including web search, local business search, image search, video search, news search, and AI-powered summarization. This project supports both STDIO and HTTP transports, with STDIO as the default mode. 4 | 5 | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/brave/brave-search-mcp-server) 6 | 7 | ## Migration 8 | 9 | ### 1.x to 2.x 10 | 11 | #### Default transport now STDIO 12 | 13 | To follow established MCP conventions, the server now defaults to STDIO. If you would like to continue using HTTP, you will need to set the `BRAVE_MCP_TRANSPORT` environment variable to `http`, or provide the runtime argument `--transport http` when launching the server. 14 | 15 | #### Response structure of `brave_image_search` 16 | 17 | Version 1.x of the MCP server would return base64-encoded image data along with image URLs. This dramatically slowed down the response, as well as consumed unnecessarily context in the session. Version 2.x removes the base64-encoded data, and returns a response object that more closely reflects the original Brave Search API response. The updated output schema is defined in [`src/tools/images/schemas/output.ts`](https://github.com/brave/brave-search-mcp-server/blob/main/src/tools/images/schemas/output.ts). 18 | 19 | ## Tools 20 | 21 | ### Web Search (`brave_web_search`) 22 | Performs comprehensive web searches with rich result types and advanced filtering options. 23 | 24 | **Parameters:** 25 | - `query` (string, required): Search terms (max 400 chars, 50 words) 26 | - `country` (string, optional): Country code (default: "US") 27 | - `search_lang` (string, optional): Search language (default: "en") 28 | - `ui_lang` (string, optional): UI language (default: "en-US") 29 | - `count` (number, optional): Results per page (1-20, default: 10) 30 | - `offset` (number, optional): Pagination offset (max 9, default: 0) 31 | - `safesearch` (string, optional): Content filtering ("off", "moderate", "strict", default: "moderate") 32 | - `freshness` (string, optional): Time filter ("pd", "pw", "pm", "py", or date range) 33 | - `text_decorations` (boolean, optional): Include highlighting markers (default: true) 34 | - `spellcheck` (boolean, optional): Enable spell checking (default: true) 35 | - `result_filter` (array, optional): Filter result types (default: ["web", "query"]) 36 | - `goggles` (array, optional): Custom re-ranking definitions 37 | - `units` (string, optional): Measurement units ("metric" or "imperial") 38 | - `extra_snippets` (boolean, optional): Get additional excerpts (Pro plans only) 39 | - `summary` (boolean, optional): Enable summary key generation for AI summarization 40 | 41 | ### Local Search (`brave_local_search`) 42 | Searches for local businesses and places with detailed information including ratings, hours, and AI-generated descriptions. 43 | 44 | **Parameters:** 45 | - Same as `brave_web_search` with automatic location filtering 46 | - Automatically includes "web" and "locations" in result_filter 47 | 48 | **Note:** Requires Pro plan for full local search capabilities. Falls back to web search otherwise. 49 | 50 | ### Video Search (`brave_video_search`) 51 | Searches for videos with comprehensive metadata and thumbnail information. 52 | 53 | **Parameters:** 54 | - `query` (string, required): Search terms (max 400 chars, 50 words) 55 | - `country` (string, optional): Country code (default: "US") 56 | - `search_lang` (string, optional): Search language (default: "en") 57 | - `ui_lang` (string, optional): UI language (default: "en-US") 58 | - `count` (number, optional): Results per page (1-50, default: 20) 59 | - `offset` (number, optional): Pagination offset (max 9, default: 0) 60 | - `spellcheck` (boolean, optional): Enable spell checking (default: true) 61 | - `safesearch` (string, optional): Content filtering ("off", "moderate", "strict", default: "moderate") 62 | - `freshness` (string, optional): Time filter ("pd", "pw", "pm", "py", or date range) 63 | 64 | ### Image Search (`brave_image_search`) 65 | Searches for images with automatic fetching and base64 encoding for direct display. 66 | 67 | **Parameters:** 68 | - `query` (string, required): Search terms (max 400 chars, 50 words) 69 | - `country` (string, optional): Country code (default: "US") 70 | - `search_lang` (string, optional): Search language (default: "en") 71 | - `count` (number, optional): Results per page (1-200, default: 50) 72 | - `safesearch` (string, optional): Content filtering ("off", "strict", default: "strict") 73 | - `spellcheck` (boolean, optional): Enable spell checking (default: true) 74 | 75 | ### News Search (`brave_news_search`) 76 | Searches for current news articles with freshness controls and breaking news indicators. 77 | 78 | **Parameters:** 79 | - `query` (string, required): Search terms (max 400 chars, 50 words) 80 | - `country` (string, optional): Country code (default: "US") 81 | - `search_lang` (string, optional): Search language (default: "en") 82 | - `ui_lang` (string, optional): UI language (default: "en-US") 83 | - `count` (number, optional): Results per page (1-50, default: 20) 84 | - `offset` (number, optional): Pagination offset (max 9, default: 0) 85 | - `spellcheck` (boolean, optional): Enable spell checking (default: true) 86 | - `safesearch` (string, optional): Content filtering ("off", "moderate", "strict", default: "moderate") 87 | - `freshness` (string, optional): Time filter (default: "pd" for last 24 hours) 88 | - `extra_snippets` (boolean, optional): Get additional excerpts (Pro plans only) 89 | - `goggles` (array, optional): Custom re-ranking definitions 90 | 91 | ### Summarizer Search (`brave_summarizer`) 92 | Generates AI-powered summaries from web search results using Brave's summarization API. 93 | 94 | **Parameters:** 95 | - `key` (string, required): Summary key from web search results (use `summary: true` in web search) 96 | - `entity_info` (boolean, optional): Include entity information (default: false) 97 | - `inline_references` (boolean, optional): Add source URL references (default: false) 98 | 99 | **Usage:** First perform a web search with `summary: true`, then use the returned summary key with this tool. 100 | 101 | ## Configuration 102 | 103 | ### Getting an API Key 104 | 105 | 1. Sign up for a [Brave Search API account](https://brave.com/search/api/) 106 | 2. Choose a plan: 107 | - **Free**: 2,000 queries/month, basic web search 108 | - **Pro**: Enhanced features including local search, AI summaries, extra snippets 109 | 3. Generate your API key from the [developer dashboard](https://api-dashboard.search.brave.com/app/keys) 110 | 111 | ### Environment Variables 112 | 113 | The server supports the following environment variables: 114 | 115 | - `BRAVE_API_KEY`: Your Brave Search API key (required) 116 | - `BRAVE_MCP_TRANSPORT`: Transport mode ("http" or "stdio", default: "stdio") 117 | - `BRAVE_MCP_PORT`: HTTP server port (default: 8080) 118 | - `BRAVE_MCP_HOST`: HTTP server host (default: "0.0.0.0") 119 | - `BRAVE_MCP_LOG_LEVEL`: Desired logging level("debug", "info", "notice", "warning", "error", "critical", "alert", or "emergency", default: "info") 120 | - `BRAVE_MCP_ENABLED_TOOLS`: When used, specifies a whitelist for supported tools 121 | - `BRAVE_MCP_DISABLED_TOOLS`: When used, specifies a blacklist for supported tools 122 | 123 | ### Command Line Options 124 | 125 | ```bash 126 | node dist/index.js [options] 127 | 128 | Options: 129 | --brave-api-key Brave API key 130 | --transport Transport type (default: stdio) 131 | --port HTTP server port (default: 8080) 132 | --host HTTP server host (default: 0.0.0.0) 133 | --logging-level Desired logging level (one of _debug_, _info_, _notice_, _warning_, _error_, _critical_, _alert_, or _emergency_) 134 | --enabled-tools Tools whitelist (only the specified tools will be enabled) 135 | --disabled-tools Tools blacklist (included tools will be disabled) 136 | ``` 137 | 138 | ## Installation 139 | 140 | ### Installing via Smithery 141 | 142 | To install Brave Search automatically via [Smithery](https://smithery.ai/server/brave): 143 | 144 | ```bash 145 | npx -y @smithery/cli install brave 146 | ``` 147 | 148 | ### Usage with Claude Desktop 149 | 150 | Add this to your `claude_desktop_config.json`: 151 | 152 | #### Docker 153 | 154 | ```json 155 | { 156 | "mcpServers": { 157 | "brave-search": { 158 | "command": "docker", 159 | "args": ["run", "-i", "--rm", "-e", "BRAVE_API_KEY", "docker.io/mcp/brave-search"], 160 | "env": { 161 | "BRAVE_API_KEY": "YOUR_API_KEY_HERE" 162 | } 163 | } 164 | } 165 | } 166 | ``` 167 | 168 | #### NPX 169 | 170 | ```json 171 | { 172 | "mcpServers": { 173 | "brave-search": { 174 | "command": "npx", 175 | "args": ["-y", "@brave/brave-search-mcp-server", "--transport", "http"], 176 | "env": { 177 | "BRAVE_API_KEY": "YOUR_API_KEY_HERE" 178 | } 179 | } 180 | } 181 | } 182 | ``` 183 | 184 | ### Usage with VS Code 185 | 186 | For quick installation, use the one-click installation buttons below: 187 | 188 | [![Install with NPX in VS Code](https://img.shields.io/badge/VS_Code-NPM-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=brave-search&inputs=%5B%7B%22password%22%3Atrue%2C%22id%22%3A%22brave-api-key%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22Brave+Search+API+Key%22%7D%5D&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40brave%2Fbrave-search-mcp-server%22%2C%22--transport%22%2C%22stdio%22%5D%2C%22env%22%3A%7B%22BRAVE_API_KEY%22%3A%22%24%7Binput%3Abrave-api-key%7D%22%7D%7D) [![Install with NPX in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-NPM-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=brave-search&inputs=%5B%7B%22password%22%3Atrue%2C%22id%22%3A%22brave-api-key%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22Brave+Search+API+Key%22%7D%5D&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40brave%2Fbrave-search-mcp-server%22%2C%22--transport%22%2C%22stdio%22%5D%2C%22env%22%3A%7B%22BRAVE_API_KEY%22%3A%22%24%7Binput%3Abrave-api-key%7D%22%7D%7D&quality=insiders) 189 | [![Install with Docker in VS Code](https://img.shields.io/badge/VS_Code-Docker-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=brave-search&inputs=%5B%7B%22password%22%3Atrue%2C%22id%22%3A%22brave-api-key%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22Brave+Search+API+Key%22%7D%5D&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22-e%22%2C%22BRAVE_API_KEY%22%2C%22mcp%2Fbrave-search%22%5D%2C%22env%22%3A%7B%22BRAVE_API_KEY%22%3A%22%24%7Binput%3Abrave-api-key%7D%22%7D%7D) [![Install with Docker in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Docker-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=brave-search&inputs=%5B%7B%22password%22%3Atrue%2C%22id%22%3A%22brave-api-key%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22Brave+Search+API+Key%22%7D%5D&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22-e%22%2C%22BRAVE_API_KEY%22%2C%22mcp%2Fbrave-search%22%5D%2C%22env%22%3A%7B%22BRAVE_API_KEY%22%3A%22%24%7Binput%3Abrave-api-key%7D%22%7D%7D&quality=insiders) 190 | 191 | For manual installation, add the following to your User Settings (JSON) or `.vscode/mcp.json`: 192 | 193 | #### Docker 194 | 195 | ```json 196 | { 197 | "inputs": [ 198 | { 199 | "password": true, 200 | "id": "brave-api-key", 201 | "type": "promptString", 202 | "description": "Brave Search API Key", 203 | } 204 | ], 205 | "servers": { 206 | "brave-search": { 207 | "command": "docker", 208 | "args": ["run", "-i", "--rm", "-e", "BRAVE_API_KEY", "mcp/brave-search"], 209 | "env": { 210 | "BRAVE_API_KEY": "${input:brave-api-key}" 211 | } 212 | } 213 | } 214 | } 215 | ``` 216 | 217 | #### NPX 218 | 219 | ```json 220 | { 221 | "inputs": [ 222 | { 223 | "password": true, 224 | "id": "brave-api-key", 225 | "type": "promptString", 226 | "description": "Brave Search API Key", 227 | } 228 | ], 229 | "servers": { 230 | "brave-search-mcp-server": { 231 | "command": "npx", 232 | "args": ["-y", "@brave/brave-search-mcp-server", "--transport", "stdio"], 233 | "env": { 234 | "BRAVE_API_KEY": "${input:brave-api-key}" 235 | } 236 | } 237 | } 238 | } 239 | ``` 240 | 241 | ## Build 242 | 243 | ### Docker 244 | 245 | ```bash 246 | docker build -t mcp/brave-search:latest . 247 | ``` 248 | 249 | ### Local Build 250 | 251 | ```bash 252 | npm install 253 | npm run build 254 | ``` 255 | 256 | ## Development 257 | 258 | ### Prerequisites 259 | 260 | - Node.js 22.x or higher 261 | - npm 262 | - Brave Search API key 263 | 264 | ### Setup 265 | 266 | 1. Clone the repository: 267 | ```bash 268 | git clone https://github.com/brave/brave-search-mcp-server.git 269 | cd brave-search-mcp-server 270 | ``` 271 | 272 | 2. Install dependencies: 273 | ```bash 274 | npm install 275 | ``` 276 | 277 | 3. Build the project: 278 | ```bash 279 | npm run build 280 | ``` 281 | 282 | ### Testing via Claude Desktop 283 | 284 | Add a reference to your local build in `claude_desktop_config.json`: 285 | 286 | ```json 287 | { 288 | "mcpServers": { 289 | "brave-search-dev": { 290 | "command": "node", 291 | "args": ["C:\\GitHub\\brave-search-mcp-server\\dist\\index.js"], // Verify your path 292 | "env": { 293 | "BRAVE_API_KEY": "YOUR_API_KEY_HERE" 294 | } 295 | } 296 | } 297 | } 298 | ``` 299 | 300 | ### Testing via MCP Inspector 301 | 302 | 1. Build and start the server: 303 | ```bash 304 | npm run build 305 | node dist/index.js 306 | ``` 307 | 308 | 2. In another terminal, start the MCP Inspector: 309 | ```bash 310 | npx @modelcontextprotocol/inspector node dist/index.js 311 | ``` 312 | 313 | STDIO is the default mode. For HTTP mode testing, add `--transport http` to the arguments in the Inspector UI. 314 | 315 | ### Testing via Smithery.AI 316 | 317 | 1. Establish and acquire a smithery.ai account and API key 318 | 2. Run `npm run install`, `npm run smithery:build`, and lastly `npm run smithery:dev` to begin testing 319 | 320 | ### Available Scripts 321 | 322 | - `npm run build`: Build the TypeScript project 323 | - `npm run watch`: Watch for changes and rebuild 324 | - `npm run format`: Format code with Prettier 325 | - `npm run format:check`: Check code formatting 326 | - `npm run prepare`: Format and build (runs automatically on npm install) 327 | 328 | - `npm run inspector`: Launch an instance of MCP Inspector 329 | - `npm run inspector:stdio`: Launch a instance of MCP Inspector, configured for STDIO 330 | - `npm run smithery:build`: Build the project for smithery.ai 331 | - `npm run smithery:dev`: Launch the development environment for smithery.ai 332 | 333 | ### Docker Compose 334 | 335 | For local development with Docker: 336 | 337 | ```bash 338 | docker-compose up --build 339 | ``` 340 | 341 | ## License 342 | 343 | This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository. 344 | -------------------------------------------------------------------------------- /src/tools/web/types.ts: -------------------------------------------------------------------------------- 1 | export type FormattedWebResults = { 2 | url: string; 3 | title: string; 4 | description?: string; 5 | extra_snippets?: string[]; 6 | }[]; 7 | 8 | export type FormattedFAQResults = { 9 | question: string; 10 | answer: string; 11 | title: string; 12 | url: string; 13 | }[]; 14 | 15 | export type FormattedDiscussionsResults = { 16 | mutated_by_goggles?: boolean; 17 | url: string; 18 | data?: ForumData; 19 | }[]; 20 | 21 | export type FormattedNewsResults = { 22 | mutated_by_goggles?: boolean; 23 | source?: string; 24 | breaking: boolean; 25 | is_live: boolean; 26 | age?: string; 27 | url: string; 28 | title: string; 29 | description?: string; 30 | extra_snippets?: string[]; 31 | }[]; 32 | 33 | export type FormattedVideoResults = { 34 | mutated_by_goggles?: boolean; 35 | url: string; 36 | title: string; 37 | description?: string; 38 | age?: string; 39 | thumbnail_url?: string; 40 | duration?: string; 41 | view_count?: string; 42 | creator?: string; 43 | publisher?: string; 44 | tags?: string[]; 45 | }[]; 46 | 47 | export interface WebSearchApiResponse { 48 | /** The type of web search API result. The value is always search. */ 49 | type: 'search'; 50 | /** Discussions clusters aggregated from forum posts that are relevant to the query. */ 51 | discussions?: Discussions; 52 | /** Frequently asked questions that are relevant to the search query. */ 53 | faq?: FAQ; 54 | /** Aggregated information on an entity showable as an infobox. */ 55 | infobox?: GraphInfobox; 56 | /** Places of interest (POIs) relevant to location sensitive queries. */ 57 | locations?: Locations; 58 | /** Preferred ranked order of search results. */ 59 | mixed?: MixedResponse; 60 | /** News results relevant to the query. */ 61 | news?: News; 62 | /** Search query string and its modifications that are used for search. */ 63 | query?: Query; 64 | /** Videos relevant to the query. */ 65 | videos?: Videos; 66 | /** Web search results relevant to the query. */ 67 | web?: Search; 68 | /** Summary key to get summary results for the query. */ 69 | summarizer?: Summarizer; 70 | /** Callback information for rich results. */ 71 | rich?: RichCallbackInfo; 72 | } 73 | 74 | export interface LocalPoiSearchApiResponse { 75 | /** The type of local POI search API result. The value is always local_pois. */ 76 | type: 'local_pois'; 77 | /** Location results matching the ids in the request. */ 78 | results?: LocationResult[]; 79 | } 80 | 81 | export interface LocalDescriptionsSearchApiResponse { 82 | /** The type of local description search API result. The value is always local_descriptions. */ 83 | type: 'local_descriptions'; 84 | /** Location descriptions matching the ids in the request. */ 85 | results?: LocationDescription[]; 86 | } 87 | 88 | interface Query { 89 | /** The original query that was requested. */ 90 | original: string; 91 | /** Whether there is more content available for query, but the response was restricted due to safesearch. */ 92 | show_strict_warning?: boolean; 93 | /** The altered query for which the search was performed. */ 94 | altered?: string; 95 | /** Whether safesearch was enabled. */ 96 | safesearch?: boolean; 97 | /** Whether the query is a navigational query to a domain. */ 98 | is_navigational?: boolean; 99 | /** Whether the query has location relevance. */ 100 | is_geolocal?: boolean; 101 | /** Whether the query was decided to be location sensitive. */ 102 | local_decision?: string; 103 | /** The index of the location. */ 104 | local_locations_idx?: number; 105 | /** Whether the query is trending. */ 106 | is_trending?: boolean; 107 | /** Whether the query has news breaking articles relevant to it. */ 108 | is_news_breaking?: boolean; 109 | /** Whether the query requires location information for better results. */ 110 | ask_for_location?: boolean; 111 | /** The language information gathered from the query. */ 112 | language?: Language; 113 | /** Whether the spellchecker was off. */ 114 | spellcheck_off?: boolean; 115 | /** The country that was used. */ 116 | country?: string; 117 | /** Whether there are bad results for the query. */ 118 | bad_results?: boolean; 119 | /** Whether the query should use a fallback. */ 120 | should_fallback?: boolean; 121 | /** The gathered location latitutde associated with the query. */ 122 | lat?: string; 123 | /** The gathered location longitude associated with the query. */ 124 | long?: string; 125 | /** The gathered postal code associated with the query. */ 126 | postal_code?: string; 127 | /** The gathered city associated with the query. */ 128 | city?: string; 129 | /** The gathered state associated with the query. */ 130 | state?: string; 131 | /** The country for the request origination. */ 132 | header_country?: string; 133 | /** Whether more results are available for the given query. */ 134 | more_results_available?: boolean; 135 | /** Any custom location labels attached to the query. */ 136 | custom_location_label?: string; 137 | /** Any reddit cluster associated with the query. */ 138 | reddit_cluster?: string; 139 | } 140 | 141 | export interface Discussions { 142 | /** The type identifying a discussion cluster. Currently the value is always search. */ 143 | type: 'search'; 144 | /** A list of discussion results. */ 145 | results: DiscussionResult[]; 146 | /** Whether the discussion results are changed by a Goggle. The value is false by default. */ 147 | mutated_by_goggles: boolean; 148 | } 149 | 150 | interface DiscussionResult extends Omit { 151 | /** The discussion result type identifier. The value is always discussion. */ 152 | type: 'discussion'; 153 | /** The enriched aggregated data for the relevant forum post. */ 154 | data?: ForumData; 155 | } 156 | 157 | export interface ForumData { 158 | /** The name of the forum. */ 159 | forum_name: string; 160 | /** The number of answers to the post. */ 161 | num_answers?: number; 162 | /** The score of the post on the forum. */ 163 | score?: string; 164 | /** The title of the post on the forum. */ 165 | title?: string; 166 | /** The question asked in the forum post. */ 167 | question?: string; 168 | /** The top-rated comment under the forum post. */ 169 | top_comment?: string; 170 | } 171 | 172 | export interface FAQ { 173 | /** The FAQ result type identifier. The value is always faq. */ 174 | type: 'faq'; 175 | /** A list of aggregated question answer results relevant to the query. */ 176 | results: QA[]; 177 | } 178 | 179 | interface QA { 180 | /** The question being asked. */ 181 | question: string; 182 | /** The answer to the question. */ 183 | answer: string; 184 | /** The title of the post. */ 185 | title: string; 186 | /** The URL pointing to the post. */ 187 | url: string; 188 | /** Aggregated information about the URL. */ 189 | meta_url?: MetaUrl; 190 | } 191 | 192 | interface MetaUrl { 193 | /** The protocol scheme extracted from the URL. */ 194 | scheme: string; 195 | /** The network location part extracted from the URL. */ 196 | netloc: string; 197 | /** The lowercased domain name extracted from the URL. */ 198 | hostname?: string; 199 | /** The favicon used for the URL. */ 200 | favicon: string; 201 | /** The hierarchical path of the URL useful as a display string. */ 202 | path: string; 203 | } 204 | 205 | export interface Search { 206 | /** A type identifying web search results. The value is always search. */ 207 | type: 'search'; 208 | /** A list of search results. */ 209 | results: SearchResult[]; 210 | /** Whether the results are family friendly. */ 211 | family_friendly: boolean; 212 | } 213 | 214 | export interface SearchResult extends Result { 215 | /** A type identifying a web search result. The value is always search_result. */ 216 | type: 'search_result'; 217 | /** A sub type identifying the web search result type. */ 218 | subtype: 'generic'; 219 | /** Whether the web search result is currently live. Default value is false. */ 220 | is_live: boolean; 221 | /** Gathered information on a web search result. */ 222 | deep_results?: DeepResult; 223 | /** A list of schemas (structured data) extracted from the page. The schemas try to follow schema.org and will return anything we can extract from the HTML that can fit into these models. */ 224 | schemas?: [][]; 225 | /** Aggregated information on the URL associated with the web search result. */ 226 | meta_url?: MetaUrl; 227 | /** The thumbnail of the web search result. */ 228 | thumbnail?: Thumbnail; 229 | /** A string representing the age of the web search result. */ 230 | age?: string; 231 | /** The main language on the web search result. */ 232 | language: string; 233 | /** The location details if the query relates to a restaurant. */ 234 | location?: LocationResult; 235 | /** The video associated with the web search result. */ 236 | video?: VideoData; 237 | /** The movie associated with the web search result. */ 238 | movie?: MovieData; 239 | /** Any frequently asked questions associated with the web search result. */ 240 | faq?: FAQ; 241 | /** Any question answer information associated with the web search result page. */ 242 | qa?: QAPage; 243 | /** Any book information associated with the web search result page. */ 244 | book?: Book; 245 | /** Rating found for the web search result page. */ 246 | rating?: Rating; 247 | /** An article found for the web search result page. */ 248 | article?: Article; 249 | /** The main product and a review that is found on the web search result page. */ 250 | product?: Product | Review; 251 | /** A list of products and reviews that are found on the web search result page. */ 252 | product_cluster?: Product | Review[]; 253 | /** A type representing a cluster. The value can be product_cluster. */ 254 | cluster_type?: string; 255 | /** A list of web search results. */ 256 | cluster?: Result[]; 257 | /** Aggregated information on the creative work found on the web search result. */ 258 | creative_work?: CreativeWork; 259 | /** Aggregated information on music recording found on the web search result. */ 260 | music_recording?: MusicRecording; 261 | /** Aggregated information on the review found on the web search result. */ 262 | review?: Review; 263 | /** Aggregated information on a software product found on the web search result page. */ 264 | software?: Software; 265 | /** Aggregated information on a recipe found on the web search result page. */ 266 | recipe?: Recipe; 267 | /** Aggregated information on a organization found on the web search result page. */ 268 | organization?: Organization; 269 | /** The content type associated with the search result page. */ 270 | content_type?: string; 271 | /** A list of extra alternate snippets for the web search result. */ 272 | extra_snippets?: string[]; 273 | } 274 | 275 | interface Result { 276 | /** The title of the web page. */ 277 | title: string; 278 | /** The URL where the page is served. */ 279 | url: string; 280 | is_source_local: boolean; 281 | is_source_both: boolean; 282 | /** A description for the web page. */ 283 | description?: string; 284 | /** A date representing the age of the web page. */ 285 | page_age?: string; 286 | /** A date representing when the web page was last fetched. */ 287 | page_fetched?: string; 288 | /** A profile associated with the web page. */ 289 | profile?: Profile; 290 | /** A language classification for the web page. */ 291 | language?: string; 292 | /** Whether the web page is family friendly. */ 293 | family_friendly: boolean; 294 | } 295 | 296 | interface AbstractGraphInfobox extends Result { 297 | /** The infobox result type identifier. The value is always infobox. */ 298 | type: 'infobox'; 299 | /** The position on a search result page. */ 300 | position: number; 301 | /** Any label associated with the entity. */ 302 | label?: string; 303 | /** Category classification for the entity. */ 304 | category?: string; 305 | /** A longer description for the entity. */ 306 | long_desc?: string; 307 | /** The thumbnail associated with the entity. */ 308 | thumbnail?: Thumbnail; 309 | /** A list of attributes about the entity. */ 310 | attributes?: string[][]; 311 | /** The profiles associated with the entity. */ 312 | profiles?: Profile[] | DataProvider[]; 313 | /** The official website pertaining to the entity. */ 314 | website_url?: string; 315 | /** Any ratings given to the entity. */ 316 | ratings?: Rating[]; 317 | /** A list of data sources for the entity. */ 318 | providers?: DataProvider[]; 319 | /** A unit representing quantity relevant to the entity. */ 320 | distance?: Unit; 321 | /** A list of images relevant to the entity. */ 322 | images?: Thumbnail[]; 323 | /** Any movie data relevant to the entity. Appears only when the result is a movie. */ 324 | movie?: MovieData; 325 | } 326 | 327 | interface GenericInfobox extends AbstractGraphInfobox { 328 | /** The infobox subtype identifier. The value is always generic. */ 329 | subtype: 'generic'; 330 | /** List of URLs where the entity was found. */ 331 | found_in_urls?: string[]; 332 | } 333 | 334 | interface EntityInfobox extends AbstractGraphInfobox { 335 | /** The infobox subtype identifier. The value is always entity. */ 336 | subtype: 'entity'; 337 | } 338 | 339 | interface QAInfobox extends AbstractGraphInfobox { 340 | /** The infobox subtype identifier. The value is always code. */ 341 | subtype: 'code'; 342 | /** The question and relevant answer. */ 343 | data: QAPage; 344 | /** Detailed information on the page containing the question and relevant answer. */ 345 | meta_url?: MetaUrl; 346 | } 347 | 348 | interface InfoboxWithLocation extends AbstractGraphInfobox { 349 | /** The infobox subtype identifier. The value is always location. */ 350 | subtype: 'location'; 351 | /** Whether the entity a location. */ 352 | is_location: boolean; 353 | /** The coordinates of the location. */ 354 | coordinates?: number[]; 355 | /** The map zoom level. */ 356 | zoom_level: number; 357 | /** The location result. */ 358 | location?: LocationResult; 359 | } 360 | 361 | interface InfoboxPlace extends AbstractGraphInfobox { 362 | /** The infobox subtype identifier. The value is always place. */ 363 | subtype: 'place'; 364 | /** The location result. */ 365 | location: LocationResult; 366 | } 367 | 368 | interface GraphInfobox { 369 | /** The type identifier for infoboxes. The value is always graph. */ 370 | type: 'graph'; 371 | /** A list of infoboxes associated with the query. */ 372 | results: GenericInfobox | QAInfobox | InfoboxPlace | InfoboxWithLocation | EntityInfobox; 373 | } 374 | 375 | interface QAPage { 376 | /** The question that is being asked. */ 377 | question: string; 378 | /** An answer to the question. */ 379 | answer: Answer; 380 | } 381 | 382 | interface Answer { 383 | /** The main content of the answer. */ 384 | text: string; 385 | /** The name of the author of the answer. */ 386 | author?: string; 387 | /** Number of upvotes on the answer. */ 388 | upvoteCount?: number; 389 | /** The number of downvotes on the answer. */ 390 | downvoteCount?: number; 391 | } 392 | 393 | interface Thumbnail { 394 | /** The served URL of the picture thumbnail. */ 395 | src: string; 396 | /** The original URL of the image. */ 397 | original?: string; 398 | } 399 | 400 | interface LocationWebResult extends Result { 401 | /** Aggregated information about the URL. */ 402 | meta_url: MetaUrl; 403 | } 404 | 405 | interface LocationResult extends Result { 406 | /** Location result type identifier. The value is always location_result. */ 407 | type: 'location_result'; 408 | /** A Temporary id associated with this result, which can be used to retrieve extra information about the location. It remains valid for 8 hours… */ 409 | id?: string; 410 | /** The complete URL of the provider. */ 411 | provider_url: string; 412 | /** A list of coordinates associated with the location. This is a lat long represented as a floating point. */ 413 | coordinates?: number[]; 414 | /** The zoom level on the map. */ 415 | zoom_level: number; 416 | /** The thumbnail associated with the location. */ 417 | thumbnail?: Thumbnail; 418 | /** The postal address associated with the location. */ 419 | postal_address?: PostalAddress; 420 | /** The opening hours, if it is a business, associated with the location . */ 421 | opening_hours?: OpeningHours; 422 | /** The contact of the business associated with the location. */ 423 | contact?: Contact; 424 | /** A display string used to show the price classification for the business. */ 425 | price_range?: string; 426 | /** The ratings of the business. */ 427 | rating?: Rating; 428 | /** The distance of the location from the client. */ 429 | distance?: Unit; 430 | /** Profiles associated with the business. */ 431 | profiles?: DataProvider[]; 432 | /** Aggregated reviews from various sources relevant to the business. */ 433 | reviews?: Reviews; 434 | /** A bunch of pictures associated with the business. */ 435 | pictures?: PictureResults; 436 | /** An action to be taken. */ 437 | action?: Action; 438 | /** A list of cuisine categories served. */ 439 | serves_cuisine?: string[]; 440 | /** A list of categories. */ 441 | categories?: string[]; 442 | /** An icon category. */ 443 | icon_category?: string; 444 | /** Web results related to this location. */ 445 | results?: LocationWebResult; 446 | /** IANA timezone identifier. */ 447 | timezone?: string; 448 | /** The utc offset of the timezone. */ 449 | timezone_offset?: string; 450 | } 451 | 452 | interface LocationDescription { 453 | /** The type of a location description. The value is always local_description. */ 454 | type: 'local_description'; 455 | /** A Temporary id of the location with this description. */ 456 | id: string; 457 | /** AI generated description of the location with the given id. */ 458 | description?: string; 459 | } 460 | 461 | interface Locations { 462 | /** Location type identifier. The value is always locations. */ 463 | type: 'locations'; 464 | /** An aggregated list of location sensitive results. */ 465 | results: LocationResult[]; 466 | } 467 | 468 | interface MixedResponse { 469 | /** The type representing the model mixed. The value is always mixed. */ 470 | type: 'mixed'; 471 | /** The ranking order for the main section of the search result page. */ 472 | main?: ResultReference[]; 473 | /** The ranking order for the top section of the search result page. */ 474 | top?: ResultReference[]; 475 | /** The ranking order for the side section of the search result page. */ 476 | side?: ResultReference[]; 477 | } 478 | 479 | interface ResultReference { 480 | /** The type of the result. */ 481 | type: string; 482 | /** The 0th based index where the result should be placed. */ 483 | index?: number; 484 | /** Whether to put all the results from the type at specific position. */ 485 | all: boolean; 486 | } 487 | 488 | export interface Videos { 489 | /** The type representing the videos. The value is always videos. */ 490 | type: 'videos'; 491 | /** A list of video results. */ 492 | results: VideoResult[]; 493 | /** Whether the video results are changed by a Goggle. The value is false by default. */ 494 | mutated_by_goggles?: boolean; 495 | } 496 | 497 | export interface News { 498 | /** The type representing the news. The value is always news. */ 499 | type: 'news'; 500 | /** A list of news results. */ 501 | results: NewsResult[]; 502 | /** Whether the news results are changed by a Goggle. The value is false by default. */ 503 | mutated_by_goggles?: boolean; 504 | } 505 | 506 | interface NewsResult extends Result { 507 | /** The aggregated information on the URL representing a news result. */ 508 | meta_url?: MetaUrl; 509 | /** The source of the news. */ 510 | source?: string; 511 | /** Whether the news result is currently a breaking news. */ 512 | breaking: boolean; 513 | /** Whether the news result is currently live. */ 514 | is_live: boolean; 515 | /** The thumbnail associated with the news result. */ 516 | thumbnail?: Thumbnail; 517 | /** A string representing the age of the news article. */ 518 | age?: string; 519 | /** A list of extra alternate snippets for the news search result. */ 520 | extra_snippets?: string[]; 521 | } 522 | 523 | interface PictureResults { 524 | /** A URL to view more pictures. */ 525 | viewMoreUrl?: string; 526 | /** A list of thumbnail results. */ 527 | results: Thumbnail[]; 528 | } 529 | 530 | interface Action { 531 | /** The type representing the action. */ 532 | type: string; 533 | /** A URL representing the action to be taken. */ 534 | url: string; 535 | } 536 | 537 | interface PostalAddress { 538 | /** The type identifying a postal address. The value is always PostalAddress. */ 539 | type: 'PostalAddress'; 540 | /** The country associated with the location. */ 541 | country?: string; 542 | /** The postal code associated with the location. */ 543 | postalCode?: string; 544 | /** The street address associated with the location. */ 545 | streetAddress?: string; 546 | /** The region associated with the location. This is usually a state. */ 547 | addressRegion?: string; 548 | /** The address locality or subregion associated with the location. */ 549 | addressLocality?: string; 550 | /** The displayed address string. */ 551 | displayAddress: string; 552 | } 553 | 554 | interface OpeningHours { 555 | /** The current day opening hours. Can have two sets of opening hours. */ 556 | current_day?: DayOpeningHours[]; 557 | /** The opening hours for the whole week. */ 558 | days?: DayOpeningHours[] | [DayOpeningHours][]; 559 | } 560 | 561 | interface DayOpeningHours { 562 | /** A short string representing the day of the week. */ 563 | abbr_name: string; 564 | /** A full string representing the day of the week. */ 565 | full_name: string; 566 | /** A 24 hr clock time string for the opening time of the business on a particular day. */ 567 | opens: string; 568 | /** A 24 hr clock time string for the closing time of the business on a particular day. */ 569 | closes: string; 570 | } 571 | 572 | interface Contact { 573 | /** The email address. */ 574 | email?: string; 575 | /** The telephone number. */ 576 | telephone?: string; 577 | } 578 | 579 | interface DataProvider { 580 | /** The type representing the source of data. This is usually external. */ 581 | type: 'external'; 582 | /** The name of the data provider. This can be a domain. */ 583 | name: string; 584 | /** The URL where the information is coming from. */ 585 | url: string; 586 | /** The long name for the data provider. */ 587 | long_name?: string; 588 | /** The served URL for the image data. */ 589 | img?: string; 590 | } 591 | 592 | interface Profile { 593 | /** The name of the profile. */ 594 | name: string; 595 | /** The long name of the profile. */ 596 | long_name: string; 597 | /** The original URL where the profile is available. */ 598 | url?: string; 599 | /** The served image URL representing the profile. */ 600 | img?: string; 601 | } 602 | 603 | interface Unit { 604 | /** The quantity of the unit. */ 605 | value: number; 606 | /** The name of the unit associated with the quantity. */ 607 | units: string; 608 | } 609 | 610 | interface MovieData { 611 | /** Name of the movie. */ 612 | name?: string; 613 | /** A short plot summary for the movie. */ 614 | description?: string; 615 | /** A URL serving a movie profile page. */ 616 | url?: string; 617 | /** A thumbnail for a movie poster. */ 618 | thumbnail?: Thumbnail; 619 | /** The release date for the movie. */ 620 | release?: string; 621 | /** A list of people responsible for directing the movie. */ 622 | directors?: Person[]; 623 | /** A list of actors in the movie. */ 624 | actors?: Person[]; 625 | /** Rating provided to the movie from various sources. */ 626 | rating?: Rating; 627 | /** The runtime of the movie. The format is HH:MM:SS. */ 628 | duration?: string; 629 | /** List of genres in which the movie can be classified. */ 630 | genre?: string[]; 631 | /** The query that resulted in the movie result. */ 632 | query?: string; 633 | } 634 | 635 | interface Thing { 636 | /** A type identifying a thing. The value is always thing. */ 637 | type: 'thing'; 638 | /** The name of the thing. */ 639 | name: string; 640 | /** A URL for the thing. */ 641 | url?: string; 642 | /** Thumbnail associated with the thing. */ 643 | thumbnail?: Thumbnail; 644 | } 645 | 646 | interface Person extends Omit { 647 | /** A type identifying a person. The value is always person. */ 648 | type: 'person'; 649 | /** Email address of the person. */ 650 | email?: string; 651 | } 652 | 653 | interface Rating { 654 | /** The current value of the rating. */ 655 | ratingValue: number; 656 | /** Best rating received. */ 657 | bestRating: number; 658 | /** The number of reviews associated with the rating. */ 659 | reviewCount?: number; 660 | /** The profile associated with the rating. */ 661 | profile?: Profile; 662 | /** Whether the rating is coming from Tripadvisor. */ 663 | is_tripadvisor: boolean; 664 | } 665 | 666 | interface Book { 667 | /** The title of the book. */ 668 | title: string; 669 | /** The author of the book. */ 670 | author: Person[]; 671 | /** The publishing date of the book. */ 672 | date?: string; 673 | /** The price of the book. */ 674 | price?: Price; 675 | /** The number of pages in the book. */ 676 | pages?: number; 677 | /** The publisher of the book. */ 678 | publisher?: Person; 679 | /** A gathered rating from different sources associated with the book. */ 680 | rating?: Rating; 681 | } 682 | 683 | interface Price { 684 | /** The price value in a given currency. */ 685 | price: string; 686 | /** The current of the price value. */ 687 | price_currency: string; 688 | } 689 | 690 | interface Article { 691 | /** The author of the article. */ 692 | author?: Person[]; 693 | /** The date when the article was published. */ 694 | date?: string; 695 | /** The name of the publisher for the article. */ 696 | publisher?: Organization; 697 | /** A thumbnail associated with the article. */ 698 | thumbnail?: Thumbnail; 699 | /** Whether the article is free to read or is behind a paywall. */ 700 | isAccessibleForFree?: boolean; 701 | } 702 | 703 | interface ContactPoint extends Omit { 704 | /** A type string identifying a contact point. The value is always contact_point. */ 705 | type: 'contact_point'; 706 | /** The telephone number of the entity. */ 707 | telephone?: string; 708 | /** The email address of the entity. */ 709 | email?: string; 710 | } 711 | 712 | interface Organization extends Omit { 713 | /** A type string identifying an organization. The value is always organization. */ 714 | type: 'organization'; 715 | /** A list of contact points for the organization. */ 716 | contact_points?: ContactPoint[]; 717 | } 718 | 719 | interface HowTo { 720 | /** The how to text. */ 721 | text: string; 722 | /** A name for the how to. */ 723 | name?: string; 724 | /** A URL associated with the how to. */ 725 | url?: string; 726 | /** A list of image URLs associated with the how to. */ 727 | image?: string[]; 728 | } 729 | 730 | interface Recipe { 731 | /** The title of the recipe. */ 732 | title: string; 733 | /** The description of the recipe. */ 734 | description: string; 735 | /** A thumbnail associated with the recipe. */ 736 | thumbnail: Thumbnail; 737 | /** The URL of the web page where the recipe was found. */ 738 | url: string; 739 | /** The domain of the web page where the recipe was found. */ 740 | domain: string; 741 | /** The URL for the favicon of the web page where the recipe was found. */ 742 | favicon: string; 743 | /** The total time required to cook the recipe. */ 744 | time?: string; 745 | /** The preparation time for the recipe. */ 746 | prep_time?: string; 747 | /** The cooking time for the recipe. */ 748 | cook_time?: string; 749 | /** Ingredients required for the recipe. */ 750 | ingredients?: string; 751 | /** List of instructions for the recipe. */ 752 | instructions?: HowTo[]; 753 | /** How many people the recipe serves. */ 754 | servings?: number; 755 | /** Calorie count for the recipe. */ 756 | calories?: number; 757 | /** Aggregated information on the ratings associated with the recipe. */ 758 | rating?: Rating; 759 | /** The category of the recipe. */ 760 | recipeCategory?: string; 761 | /** The cuisine classification for the recipe. */ 762 | recipeCuisine?: string; 763 | /** Aggregated information on the cooking video associated with the recipe. */ 764 | video?: VideoData; 765 | } 766 | 767 | interface Product { 768 | /** A string representing a product type. The value is always product. */ 769 | type: 'Product'; 770 | /** The name of the product. */ 771 | name: string; 772 | /** The category of the product. */ 773 | category?: string; 774 | /** The price of the product. */ 775 | price: string; 776 | /** A thumbnail associated with the product. */ 777 | thumbnail: Thumbnail; 778 | /** The description of the product. */ 779 | description?: string; 780 | /** A list of offers available on the product. */ 781 | offers?: Offer[]; 782 | /** A rating associated with the product. */ 783 | rating?: Rating; 784 | } 785 | 786 | interface Offer { 787 | /** The URL where the offer can be found. */ 788 | url: string; 789 | /** The currency in which the offer is made. */ 790 | priceCurrency: string; 791 | /** The price of the product currently on offer. */ 792 | price: string; 793 | } 794 | 795 | interface Review { 796 | /** A string representing review type. This is always review. */ 797 | type: 'review'; 798 | /** The review title for the review. */ 799 | name: string; 800 | /** The thumbnail associated with the reviewer. */ 801 | thumbnail: Thumbnail; 802 | /** A description of the review (the text of the review itself). */ 803 | description: string; 804 | /** The ratings associated with the review. */ 805 | rating: Rating; 806 | } 807 | 808 | interface Reviews { 809 | /** A list of trip advisor reviews for the entity. */ 810 | results: TripAdvisorReview[]; 811 | /** A URL to a web page where more information on the result can be seen. */ 812 | viewMoreUrl: string; 813 | /** Any reviews available in a foreign language. */ 814 | reviews_in_foreign_language: boolean; 815 | } 816 | 817 | interface TripAdvisorReview { 818 | /** The title of the review. */ 819 | title: string; 820 | /** A description seen in the review. */ 821 | description: string; 822 | /** The date when the review was published. */ 823 | date: string; 824 | /** A rating given by the reviewer. */ 825 | rating: Rating; 826 | /** The author of the review. */ 827 | author: Person; 828 | /** A URL link to the page where the review can be found. */ 829 | review_url: string; 830 | /** The language of the review. */ 831 | language: string; 832 | } 833 | 834 | interface CreativeWork { 835 | /** The name of the creative work. */ 836 | name: string; 837 | /** A thumbnail associated with the creative work. */ 838 | thumbnail: Thumbnail; 839 | /** A rating that is given to the creative work. */ 840 | rating?: Rating; 841 | } 842 | 843 | interface MusicRecording { 844 | /** The name of the song or album. */ 845 | name: string; 846 | /** A thumbnail associated with the music. */ 847 | thumbnail?: Thumbnail; 848 | /** The rating of the music. */ 849 | rating?: Rating; 850 | } 851 | 852 | interface Software { 853 | /** The name of the software product. */ 854 | name?: string; 855 | /** The author of software product. */ 856 | author?: string; 857 | /** The latest version of the software product. */ 858 | version?: string; 859 | /** The code repository where the software product is currently available or maintained. */ 860 | codeRepository?: string; 861 | /** The home page of the software product. */ 862 | homepage?: string; 863 | /** The date when the software product was published. */ 864 | datePublisher?: string; 865 | /** Whether the software product is available on npm. */ 866 | is_npm?: boolean; 867 | /** Whether the software product is available on pypi. */ 868 | is_pypi?: boolean; 869 | /** The number of stars on the repository. */ 870 | stars?: number; 871 | /** The numbers of forks of the repository. */ 872 | forks?: number; 873 | /** The programming language spread on the software product. */ 874 | ProgrammingLanguage?: string; 875 | } 876 | 877 | interface DeepResult { 878 | /** A list of news results associated with the result. */ 879 | news?: NewsResult[]; 880 | /** A list of buttoned results associated with the result. */ 881 | buttons?: ButtonResult[]; 882 | /** Videos associated with the result. */ 883 | videos?: VideoResult[]; 884 | /** Images associated with the result. */ 885 | images?: Image[]; 886 | } 887 | 888 | interface VideoResult extends Result { 889 | /** The type identifying the video result. The value is always video_result. */ 890 | type: 'video_result'; 891 | /** Meta data for the video. */ 892 | video: VideoData; 893 | /** Aggregated information on the URL. */ 894 | meta_url?: MetaUrl; 895 | /** The thumbnail of the video. */ 896 | thumbnail?: Thumbnail; 897 | /** A string representing the age of the video. */ 898 | age?: string; 899 | } 900 | 901 | interface VideoData { 902 | /** A time string representing the duration of the video. The format can be HH:MM:SS or MM:SS. */ 903 | duration?: string; 904 | /** The number of views of the video. */ 905 | views?: string; 906 | /** The creator of the video. */ 907 | creator?: string; 908 | /** The publisher of the video. */ 909 | publisher?: string; 910 | /** A thumbnail associated with the video. */ 911 | thumbnail?: Thumbnail; 912 | /** A list of tags associated with the video. */ 913 | tags?: string[]; 914 | /** Author of the video. */ 915 | author?: Profile; 916 | /** Whether the video requires a subscription to watch. */ 917 | requires_subscription?: boolean; 918 | } 919 | 920 | interface ButtonResult { 921 | /** A type identifying button result. The value is always button_result. */ 922 | type: 'button_result'; 923 | /** The title of the result. */ 924 | title: string; 925 | /** The URL for the button result. */ 926 | url: string; 927 | } 928 | 929 | interface Image { 930 | /** The thumbnail associated with the image. */ 931 | thumbnail: Thumbnail; 932 | /** The URL of the image. */ 933 | url?: string; 934 | /** Metadata on the image. */ 935 | properties?: ImageProperties; 936 | } 937 | 938 | interface Language { 939 | /** The main language seen in the string. */ 940 | main: string; 941 | } 942 | 943 | interface ImageProperties { 944 | /** The original image URL. */ 945 | url: string; 946 | /** The URL for a better quality resized image. */ 947 | resized: string; 948 | /** The placeholder image URL. */ 949 | placeholder: string; 950 | /** The image height. */ 951 | height?: number; 952 | /** The image width. */ 953 | width?: number; 954 | /** The image format. */ 955 | format?: string; 956 | /** The image size. */ 957 | content_size?: string; 958 | } 959 | 960 | interface Summarizer { 961 | /** The value is always summarizer. */ 962 | type: 'summarizer'; 963 | /** The key for the summarizer API. */ 964 | key: string; 965 | } 966 | 967 | interface RichCallbackInfo { 968 | /** The value is always rich. */ 969 | type: 'rich'; 970 | /** The hint for the rich result. */ 971 | hint?: RichCallbackHint; 972 | } 973 | 974 | interface RichCallbackHint { 975 | /** The name of the vertical of the rich result. */ 976 | vertical: string; 977 | /** The callback key for the rich result. */ 978 | callback_key: string; 979 | } 980 | --------------------------------------------------------------------------------