mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-04-28 00:35:16 +02:00
Updated Github to Zod
This commit is contained in:
101
package-lock.json
generated
101
package-lock.json
generated
@@ -12,6 +12,7 @@
|
|||||||
"src/*"
|
"src/*"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/server-brave-search": "*",
|
||||||
"@modelcontextprotocol/server-everything": "*",
|
"@modelcontextprotocol/server-everything": "*",
|
||||||
"@modelcontextprotocol/server-gdrive": "*",
|
"@modelcontextprotocol/server-gdrive": "*",
|
||||||
"@modelcontextprotocol/server-memory": "*",
|
"@modelcontextprotocol/server-memory": "*",
|
||||||
@@ -65,6 +66,10 @@
|
|||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/server-brave-search": {
|
||||||
|
"resolved": "src/brave-search",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@modelcontextprotocol/server-everything": {
|
"node_modules/@modelcontextprotocol/server-everything": {
|
||||||
"resolved": "src/everything",
|
"resolved": "src/everything",
|
||||||
"link": true
|
"link": true
|
||||||
@@ -1019,6 +1024,7 @@
|
|||||||
"url": "https://paypal.me/jimmywarting"
|
"url": "https://paypal.me/jimmywarting"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"node-domexception": "^1.0.0",
|
"node-domexception": "^1.0.0",
|
||||||
"web-streams-polyfill": "^3.0.3"
|
"web-streams-polyfill": "^3.0.3"
|
||||||
@@ -1062,6 +1068,7 @@
|
|||||||
"version": "4.0.10",
|
"version": "4.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fetch-blob": "^3.1.2"
|
"fetch-blob": "^3.1.2"
|
||||||
},
|
},
|
||||||
@@ -1778,6 +1785,7 @@
|
|||||||
"url": "https://paypal.me/jimmywarting"
|
"url": "https://paypal.me/jimmywarting"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.5.0"
|
"node": ">=10.5.0"
|
||||||
}
|
}
|
||||||
@@ -2833,6 +2841,7 @@
|
|||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||||
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
@@ -2958,6 +2967,80 @@
|
|||||||
"zod": "^3.23.3"
|
"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": {
|
"src/everything": {
|
||||||
"name": "@modelcontextprotocol/server-everything",
|
"name": "@modelcontextprotocol/server-everything",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
@@ -2977,6 +3060,24 @@
|
|||||||
"typescript": "^5.6.2"
|
"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": {
|
"src/gdrive": {
|
||||||
"name": "@modelcontextprotocol/server-gdrive",
|
"name": "@modelcontextprotocol/server-gdrive",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"@modelcontextprotocol/server-postgres": "*",
|
"@modelcontextprotocol/server-postgres": "*",
|
||||||
"@modelcontextprotocol/server-puppeteer": "*",
|
"@modelcontextprotocol/server-puppeteer": "*",
|
||||||
"@modelcontextprotocol/server-slack": "*",
|
"@modelcontextprotocol/server-slack": "*",
|
||||||
|
"@modelcontextprotocol/server-brave-search": "*",
|
||||||
"@modelcontextprotocol/server-memory": "*"
|
"@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