Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env node
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
- import {
- CallToolRequestSchema,
- ListToolsRequestSchema,
- ListResourcesRequestSchema,
- ListPromptsRequestSchema
- } from "@modelcontextprotocol/sdk/types.js";
- import https from 'node:https';
- import { URL } from 'node:url';
- // Uncomment if needed for corporate proxies
- process.env.HTTPS_PROXY = 'http://your-zscaler-proxy:port';
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // Only for debugging
- const WEB_SEARCH_TOOL = {
- name: "brave_web_search",
- description: "Performs a web search using the Brave Search API, ideal for general queries, news, articles, and online content.",
- 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 = {
- name: "brave_local_search",
- description: "Searches for local businesses and places using Brave's Local Search API.",
- 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: {},
- resources: {},
- prompts: {}
- },
- });
- // 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++;
- }
- function isBraveWebSearchArgs(args) {
- return (typeof args === "object" &&
- args !== null &&
- "query" in args &&
- typeof args.query === "string");
- }
- function isBraveLocalSearchArgs(args) {
- return (typeof args === "object" &&
- args !== null &&
- "query" in args &&
- typeof args.query === "string");
- }
- // Helper function for HTTP requests
- function httpsRequest(url, options = {}, postData = null) {
- return new Promise((resolve, reject) => {
- console.error(`Making HTTPS request to: ${url}`);
- // Set default headers if not provided
- if (!options.headers) {
- options.headers = {};
- }
- // Use proper content negotiation
- options.headers['Accept'] = 'application/json';
- // Do not use compression for debugging
- delete options.headers['Accept-Encoding'];
- const req = https.request(url, options, (res) => {
- const chunks = [];
- res.on('data', (chunk) => {
- chunks.push(chunk);
- });
- res.on('end', () => {
- const body = Buffer.concat(chunks).toString();
- console.error(`Response status: ${res.statusCode}`);
- if (res.statusCode >= 200 && res.statusCode < 300) {
- try {
- const data = JSON.parse(body);
- resolve({ statusCode: res.statusCode, data });
- } catch (e) {
- console.error(`Error parsing JSON: ${e.message}`);
- console.error(`Raw response body: ${body.substring(0, 200)}...`);
- reject(new Error(`Invalid JSON response: ${e.message}`));
- }
- } else {
- console.error(`Error response: ${body}`);
- reject(new Error(`HTTP Error: ${res.statusCode} ${res.statusMessage}`));
- }
- });
- });
- req.on('error', (error) => {
- console.error(`Request error: ${error.message}`);
- reject(error);
- });
- if (postData) {
- req.write(postData);
- }
- req.end();
- });
- }
- async function performWebSearch(query, count = 10, offset = 0) {
- checkRateLimit();
- console.error(`Performing web search for query: ${query}`);
- const searchUrl = new URL('https://api.search.brave.com/res/v1/web/search');
- searchUrl.searchParams.set('q', query);
- searchUrl.searchParams.set('count', Math.min(count, 20).toString());
- searchUrl.searchParams.set('offset', offset.toString());
- searchUrl.searchParams.set('mcp', 'true');
- console.error(`Request URL: ${searchUrl.toString()}`);
- try {
- const options = {
- method: 'GET',
- headers: {
- 'X-Subscription-Token': BRAVE_API_KEY,
- 'User-Agent': 'Mozilla/5.0 MCP Client'
- }
- };
- const { statusCode, data } = await httpsRequest(searchUrl, options);
- console.error(`Response data structure: ${Object.keys(data).join(', ')}`);
- // Just return the raw results for debugging
- return JSON.stringify(data, null, 2);
- } catch (error) {
- console.error(`Search error: ${error.message}`);
- throw error;
- }
- }
- async function performLocalSearch(query, count = 5) {
- checkRateLimit();
- console.error(`Performing local search for query: ${query}`);
- // 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());
- try {
- const options = {
- method: 'GET',
- headers: {
- 'Accept': 'application/json',
- 'Accept-Encoding': 'gzip',
- 'X-Subscription-Token': BRAVE_API_KEY
- }
- };
- const { data: webData } = await httpsRequest(webUrl, options);
- const locationIds = webData.locations?.results?.filter((r) => r.id != null).map(r => r.id) || [];
- if (locationIds.length === 0) {
- console.error('No location IDs found, falling back to web search');
- 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);
- } catch (error) {
- console.error(`Local search error: ${error.message}`);
- throw error;
- }
- }
- async function getPoisData(ids) {
- 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 options = {
- method: 'GET',
- headers: {
- 'Accept': 'application/json',
- 'Accept-Encoding': 'gzip',
- 'X-Subscription-Token': BRAVE_API_KEY
- }
- };
- const { data } = await httpsRequest(url, options);
- return data;
- }
- async function getDescriptionsData(ids) {
- 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 options = {
- method: 'GET',
- headers: {
- 'Accept': 'application/json',
- 'Accept-Encoding': 'gzip',
- 'X-Subscription-Token': BRAVE_API_KEY
- }
- };
- const { data } = await httpsRequest(url, options);
- return data;
- }
- function formatLocalResults(poisData, descData) {
- 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],
- }));
- // Add handlers for the missing methods
- server.setRequestHandler(ListResourcesRequestSchema, async () => ({
- resources: [] // Empty list as this isn't implemented
- }));
- server.setRequestHandler(ListPromptsRequestSchema, async () => ({
- prompts: [] // Empty list as this isn't implemented
- }));
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
- try {
- console.error(`Received tool call: ${JSON.stringify(request.params)}`);
- 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");
- }
- console.error(`Processing brave_web_search with args: ${JSON.stringify(args)}`);
- const { query, count = 10 } = args;
- const results = await performWebSearch(query, count);
- console.error(`Search completed successfully`);
- 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);
- });
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement