mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2024-12-01 18:58:34 +03:00
Merge pull request #15 from modelcontextprotocol/bravesearch-server
Brave search server
This commit is contained in:
160
package-lock.json
generated
160
package-lock.json
generated
@@ -12,6 +12,7 @@
|
||||
"src/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/server-brave-search": "*",
|
||||
"@modelcontextprotocol/server-everything": "*",
|
||||
"@modelcontextprotocol/server-gdrive": "*",
|
||||
"@modelcontextprotocol/server-memory": "*",
|
||||
@@ -65,6 +66,10 @@
|
||||
"zod": "^3.23.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/server-brave-search": {
|
||||
"resolved": "src/brave-search",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/server-everything": {
|
||||
"resolved": "src/everything",
|
||||
"link": true
|
||||
@@ -1001,6 +1006,29 @@
|
||||
"pend": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fetch-blob": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||
@@ -1032,6 +1060,18 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -1727,6 +1767,25 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
@@ -2774,6 +2833,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
@@ -2895,6 +2963,80 @@
|
||||
"zod": "^3.23.3"
|
||||
}
|
||||
},
|
||||
"src/brave-search": {
|
||||
"name": "@modelcontextprotocol/server-brave-search",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "0.5.0",
|
||||
"node-fetch": "^3.3.2"
|
||||
},
|
||||
"bin": {
|
||||
"mcp-server-brave-search": "dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
},
|
||||
"src/brave-search/node_modules/@types/node": {
|
||||
"version": "20.17.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.6.tgz",
|
||||
"integrity": "sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"src/brave-search/node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"src/brave-search/node_modules/node-fetch": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"src/duckduckgo": {
|
||||
"name": "@modelcontextprotocol/server-duckduckgo",
|
||||
"version": "0.1.0",
|
||||
"extraneous": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "0.5.0",
|
||||
"jsdom": "^24.1.3",
|
||||
"node-fetch": "^3.3.2"
|
||||
},
|
||||
"bin": {
|
||||
"mcp-server-duckduckgo": "dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsdom": "^21.1.6",
|
||||
"@types/node": "^20.10.0",
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
},
|
||||
"src/everything": {
|
||||
"name": "@modelcontextprotocol/server-everything",
|
||||
"version": "0.1.0",
|
||||
@@ -2914,6 +3056,24 @@
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
},
|
||||
"src/filesystem": {
|
||||
"name": "@modelcontextprotocol/server-filesystem",
|
||||
"version": "0.1.0",
|
||||
"extraneous": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "0.5.0",
|
||||
"glob": "^10.3.10"
|
||||
},
|
||||
"bin": {
|
||||
"mcp-server-filesystem": "dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
"src/gdrive": {
|
||||
"name": "@modelcontextprotocol/server-gdrive",
|
||||
"version": "0.1.0",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"@modelcontextprotocol/server-postgres": "*",
|
||||
"@modelcontextprotocol/server-puppeteer": "*",
|
||||
"@modelcontextprotocol/server-slack": "*",
|
||||
"@modelcontextprotocol/server-brave-search": "*",
|
||||
"@modelcontextprotocol/server-memory": "*"
|
||||
}
|
||||
}
|
||||
|
||||
46
src/brave-search/README.md
Normal file
46
src/brave-search/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Brave Search MCP Server
|
||||
|
||||
An MCP server implementation that integrates the Brave Search API, providing both web and local search capabilities.
|
||||
|
||||
## Features
|
||||
|
||||
- **Web Search**: General queries, news, articles, with pagination and freshness controls
|
||||
- **Local Search**: Find businesses, restaurants, and services with detailed information
|
||||
- **Flexible Filtering**: Control result types, safety levels, and content freshness
|
||||
- **Smart Fallbacks**: Local search automatically falls back to web when no results are found
|
||||
|
||||
## Tools
|
||||
|
||||
- **brave_web_search**
|
||||
- Execute web searches with pagination and filtering
|
||||
- Inputs:
|
||||
- `query` (string): Search terms
|
||||
- `count` (number, optional): Results per page (max 20)
|
||||
- `offset` (number, optional): Pagination offset (max 9)
|
||||
|
||||
- **brave_local_search**
|
||||
- Search for local businesses and services
|
||||
- Inputs:
|
||||
- `query` (string): Local search terms
|
||||
- `count` (number, optional): Number of results (max 20)
|
||||
- Automatically falls back to web search if no local results found
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
### Getting an API Key
|
||||
1. Sign up for a [Brave Search API account](https://brave.com/search/api/)
|
||||
2. Choose a plan (Free tier available with 2,000 queries/month)
|
||||
3. Generate your API key [from the developer dashboard](https://api.search.brave.com/app/keys)
|
||||
|
||||
### Usage with Claude Desktop
|
||||
Add this to your `claude_desktop_config.json`:
|
||||
|
||||
```json
|
||||
"mcp-server-brave-search": {
|
||||
"command": "mcp-server-brave-search",
|
||||
"env": {
|
||||
"BRAVE_API_KEY": "YOUR_API_KEY_HERE"
|
||||
}
|
||||
}
|
||||
```
|
||||
377
src/brave-search/index.ts
Normal file
377
src/brave-search/index.ts
Normal file
@@ -0,0 +1,377 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
Tool,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import fetch from "node-fetch";
|
||||
|
||||
const WEB_SEARCH_TOOL: Tool = {
|
||||
name: "brave_web_search",
|
||||
description:
|
||||
"Performs a web search using the Brave Search API, ideal for general queries, news, articles, and online content. " +
|
||||
"Use this for broad information gathering, recent events, or when you need diverse web sources. " +
|
||||
"Supports pagination, content filtering, and freshness controls. " +
|
||||
"Maximum 20 results per request, with offset for pagination. ",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
description: "Search query (max 400 chars, 50 words)"
|
||||
},
|
||||
count: {
|
||||
type: "number",
|
||||
description: "Number of results (1-20, default 10)",
|
||||
default: 10
|
||||
},
|
||||
offset: {
|
||||
type: "number",
|
||||
description: "Pagination offset (max 9, default 0)",
|
||||
default: 0
|
||||
},
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
};
|
||||
|
||||
const LOCAL_SEARCH_TOOL: Tool = {
|
||||
name: "brave_local_search",
|
||||
description:
|
||||
"Searches for local businesses and places using Brave's Local Search API. " +
|
||||
"Best for queries related to physical locations, businesses, restaurants, services, etc. " +
|
||||
"Returns detailed information including:\n" +
|
||||
"- Business names and addresses\n" +
|
||||
"- Ratings and review counts\n" +
|
||||
"- Phone numbers and opening hours\n" +
|
||||
"Use this when the query implies 'near me' or mentions specific locations. " +
|
||||
"Automatically falls back to web search if no local results are found.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
description: "Local search query (e.g. 'pizza near Central Park')"
|
||||
},
|
||||
count: {
|
||||
type: "number",
|
||||
description: "Number of results (1-20, default 5)",
|
||||
default: 5
|
||||
},
|
||||
},
|
||||
required: ["query"]
|
||||
}
|
||||
};
|
||||
|
||||
// Server implementation
|
||||
const server = new Server(
|
||||
{
|
||||
name: "example-servers/brave-search",
|
||||
version: "0.1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Check for API key
|
||||
const BRAVE_API_KEY = process.env.BRAVE_API_KEY!;
|
||||
if (!BRAVE_API_KEY) {
|
||||
console.error("Error: BRAVE_API_KEY environment variable is required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const RATE_LIMIT = {
|
||||
perSecond: 1,
|
||||
perMonth: 15000
|
||||
};
|
||||
|
||||
let requestCount = {
|
||||
second: 0,
|
||||
month: 0,
|
||||
lastReset: Date.now()
|
||||
};
|
||||
|
||||
function checkRateLimit() {
|
||||
const now = Date.now();
|
||||
if (now - requestCount.lastReset > 1000) {
|
||||
requestCount.second = 0;
|
||||
requestCount.lastReset = now;
|
||||
}
|
||||
if (requestCount.second >= RATE_LIMIT.perSecond ||
|
||||
requestCount.month >= RATE_LIMIT.perMonth) {
|
||||
throw new Error('Rate limit exceeded');
|
||||
}
|
||||
requestCount.second++;
|
||||
requestCount.month++;
|
||||
}
|
||||
|
||||
interface BraveWeb {
|
||||
web?: {
|
||||
results?: Array<{
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
language?: string;
|
||||
published?: string;
|
||||
rank?: number;
|
||||
}>;
|
||||
};
|
||||
locations?: {
|
||||
results?: Array<{
|
||||
id: string; // Required by API
|
||||
title?: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
interface BraveLocation {
|
||||
id: string;
|
||||
name: string;
|
||||
address: {
|
||||
streetAddress?: string;
|
||||
addressLocality?: string;
|
||||
addressRegion?: string;
|
||||
postalCode?: string;
|
||||
};
|
||||
coordinates?: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
};
|
||||
phone?: string;
|
||||
rating?: {
|
||||
ratingValue?: number;
|
||||
ratingCount?: number;
|
||||
};
|
||||
openingHours?: string[];
|
||||
priceRange?: string;
|
||||
}
|
||||
|
||||
interface BravePoiResponse {
|
||||
results: BraveLocation[];
|
||||
}
|
||||
|
||||
interface BraveDescription {
|
||||
descriptions: {[id: string]: string};
|
||||
}
|
||||
|
||||
function isBraveWebSearchArgs(args: unknown): args is { query: string; count?: number } {
|
||||
return (
|
||||
typeof args === "object" &&
|
||||
args !== null &&
|
||||
"query" in args &&
|
||||
typeof (args as { query: string }).query === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function isBraveLocalSearchArgs(args: unknown): args is { query: string; count?: number } {
|
||||
return (
|
||||
typeof args === "object" &&
|
||||
args !== null &&
|
||||
"query" in args &&
|
||||
typeof (args as { query: string }).query === "string"
|
||||
);
|
||||
}
|
||||
|
||||
async function performWebSearch(query: string, count: number = 10, offset: number = 0) {
|
||||
checkRateLimit();
|
||||
const url = new URL('https://api.search.brave.com/res/v1/web/search');
|
||||
url.searchParams.set('q', query);
|
||||
url.searchParams.set('count', Math.min(count, 20).toString()); // API limit
|
||||
url.searchParams.set('offset', offset.toString());
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Accept-Encoding': 'gzip',
|
||||
'X-Subscription-Token': BRAVE_API_KEY
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as BraveWeb;
|
||||
|
||||
// Extract just web results
|
||||
const results = (data.web?.results || []).map(result => ({
|
||||
title: result.title || '',
|
||||
description: result.description || '',
|
||||
url: result.url || ''
|
||||
}));
|
||||
|
||||
return results.map(r =>
|
||||
`Title: ${r.title}\nDescription: ${r.description}\nURL: ${r.url}`
|
||||
).join('\n\n');
|
||||
}
|
||||
|
||||
async function performLocalSearch(query: string, count: number = 5) {
|
||||
checkRateLimit();
|
||||
// Initial search to get location IDs
|
||||
const webUrl = new URL('https://api.search.brave.com/res/v1/web/search');
|
||||
webUrl.searchParams.set('q', query);
|
||||
webUrl.searchParams.set('search_lang', 'en');
|
||||
webUrl.searchParams.set('result_filter', 'locations');
|
||||
webUrl.searchParams.set('count', Math.min(count, 20).toString());
|
||||
|
||||
const webResponse = await fetch(webUrl, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Accept-Encoding': 'gzip',
|
||||
'X-Subscription-Token': BRAVE_API_KEY
|
||||
}
|
||||
});
|
||||
|
||||
if (!webResponse.ok) {
|
||||
throw new Error(`Brave API error: ${webResponse.status} ${webResponse.statusText}\n${await webResponse.text()}`);
|
||||
}
|
||||
|
||||
const webData = await webResponse.json() as BraveWeb;
|
||||
const locationIds = webData.locations?.results?.filter((r): r is {id: string; title?: string} => r.id != null).map(r => r.id) || [];
|
||||
|
||||
if (locationIds.length === 0) {
|
||||
return performWebSearch(query, count); // Fallback to web search
|
||||
}
|
||||
|
||||
// Get POI details and descriptions in parallel
|
||||
const [poisData, descriptionsData] = await Promise.all([
|
||||
getPoisData(locationIds),
|
||||
getDescriptionsData(locationIds)
|
||||
]);
|
||||
|
||||
return formatLocalResults(poisData, descriptionsData);
|
||||
}
|
||||
|
||||
async function getPoisData(ids: string[]): Promise<BravePoiResponse> {
|
||||
checkRateLimit();
|
||||
const url = new URL('https://api.search.brave.com/res/v1/local/pois');
|
||||
ids.filter(Boolean).forEach(id => url.searchParams.append('ids', id));
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Accept-Encoding': 'gzip',
|
||||
'X-Subscription-Token': BRAVE_API_KEY
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`);
|
||||
}
|
||||
|
||||
const poisResponse = await response.json() as BravePoiResponse;
|
||||
return poisResponse;
|
||||
}
|
||||
|
||||
async function getDescriptionsData(ids: string[]): Promise<BraveDescription> {
|
||||
checkRateLimit();
|
||||
const url = new URL('https://api.search.brave.com/res/v1/local/descriptions');
|
||||
ids.filter(Boolean).forEach(id => url.searchParams.append('ids', id));
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Accept-Encoding': 'gzip',
|
||||
'X-Subscription-Token': BRAVE_API_KEY
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`);
|
||||
}
|
||||
|
||||
const descriptionsData = await response.json() as BraveDescription;
|
||||
return descriptionsData;
|
||||
}
|
||||
|
||||
function formatLocalResults(poisData: BravePoiResponse, descData: BraveDescription): string {
|
||||
return (poisData.results || []).map(poi => {
|
||||
const address = [
|
||||
poi.address?.streetAddress ?? '',
|
||||
poi.address?.addressLocality ?? '',
|
||||
poi.address?.addressRegion ?? '',
|
||||
poi.address?.postalCode ?? ''
|
||||
].filter(part => part !== '').join(', ') || 'N/A';
|
||||
|
||||
return `Name: ${poi.name}
|
||||
Address: ${address}
|
||||
Phone: ${poi.phone || 'N/A'}
|
||||
Rating: ${poi.rating?.ratingValue ?? 'N/A'} (${poi.rating?.ratingCount ?? 0} reviews)
|
||||
Price Range: ${poi.priceRange || 'N/A'}
|
||||
Hours: ${(poi.openingHours || []).join(', ') || 'N/A'}
|
||||
Description: ${descData.descriptions[poi.id] || 'No description available'}
|
||||
`;
|
||||
}).join('\n---\n') || 'No local results found';
|
||||
}
|
||||
|
||||
// Tool handlers
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [WEB_SEARCH_TOOL, LOCAL_SEARCH_TOOL],
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
try {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
if (!args) {
|
||||
throw new Error("No arguments provided");
|
||||
}
|
||||
|
||||
switch (name) {
|
||||
case "brave_web_search": {
|
||||
if (!isBraveWebSearchArgs(args)) {
|
||||
throw new Error("Invalid arguments for brave_web_search");
|
||||
}
|
||||
const { query, count = 10 } = args;
|
||||
const results = await performWebSearch(query, count);
|
||||
return {
|
||||
content: [{ type: "text", text: results }],
|
||||
isError: false,
|
||||
};
|
||||
}
|
||||
|
||||
case "brave_local_search": {
|
||||
if (!isBraveLocalSearchArgs(args)) {
|
||||
throw new Error("Invalid arguments for brave_local_search");
|
||||
}
|
||||
const { query, count = 5 } = args;
|
||||
const results = await performLocalSearch(query, count);
|
||||
return {
|
||||
content: [{ type: "text", text: results }],
|
||||
isError: false,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function runServer() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("Brave Search MCP Server running on stdio");
|
||||
}
|
||||
|
||||
runServer().catch((error) => {
|
||||
console.error("Fatal error running server:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
30
src/brave-search/package.json
Normal file
30
src/brave-search/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@modelcontextprotocol/server-brave-search",
|
||||
"version": "0.1.0",
|
||||
"description": "MCP server for Brave Search API integration",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
"homepage": "https://modelcontextprotocol.io",
|
||||
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"mcp-server-brave-search": "dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && shx chmod +x dist/*.js",
|
||||
"prepare": "npm run build",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "0.5.0",
|
||||
"node-fetch": "^3.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
10
src/brave-search/tsconfig.json
Normal file
10
src/brave-search/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user