#!/usr/bin/env node
import 'dotenv/config';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
// Constants derived from MCP_NAME or default
const CANONICAL_ID = (process.env.MCP_NAME ?? 'earch-mcp').trim();
// JSON Schemas for tool inputs
const webSearchSchema = {
type: 'object',
properties: {
q: { type: 'string', minLength: 1 },
count: { type: 'integer', minimum: 1, maximum: 50 },
country: { type: 'string' },
safesearch: { enum: ['off', 'moderate', 'strict'] },
freshness: { enum: ['pd', 'pw', 'pm', 'py'] },
},
required: ['q'],
additionalProperties: false,
};
const idsArraySchema = {
type: 'object',
properties: {
ids: {
type: 'array',
items: { type: 'string' },
minItems: 1,
maxItems: 20,
},
},
required: ['ids'],
additionalProperties: false,
};
const richCallbackSchema = {
type: 'object',
properties: { q: { type: 'string', minLength: 1 } },
required: ['q'],
additionalProperties: false,
};
// Utility fetch wrapper
async function braveGet(url, params) {
const apiKey = process.env.EARCH_MCP_API_KEY || process.env.BRAVE_API_KEY || process.env.BRAVE_SEARCH_API_KEY;
if (!apiKey) {
throw new Error('Missing API key. Set EARCH_MCP_API_KEY or BRAVE_API_KEY.');
}
const headers = {
Accept: 'application/json',
'Accept-Encoding': 'gzip',
'X-Subscription-Token': apiKey,
};
const usp = new URLSearchParams();
for (const [k, v] of Object.entries(params)) {
if (v === undefined)
continue;
usp.set(k, String(v));
}
const endpoint = `${url}?${usp.toString()}`;
const res = await fetch(endpoint, { headers });
if (!res.ok) {
const body = await res.text().catch(() => '');
throw new Error(`Brave API error ${res.status}: ${body}`);
}
return res.json();
}
const toolDefs = [
{
name: 'web.search',
description: 'Brave Web Search: returns results for query q',
inputSchema: webSearchSchema,
async handler(args) {
const data = await braveGet('https://api.search.brave.com/res/v1/web/search', {
q: args.q,
count: args.count,
country: args.country,
safesearch: args.safesearch,
freshness: args.freshness,
});
return { content: [{ type: 'text', text: JSON.stringify(data) }] };
},
},
{
name: 'local.pois',
description: 'Brave Local Search POIs: fetch extra info for up to 20 location ids',
inputSchema: idsArraySchema,
async handler(args) {
const usp = new URLSearchParams();
for (const id of args.ids)
usp.append('ids', id);
const data = await braveGet('https://api.search.brave.com/res/v1/local/pois', Object.fromEntries(usp.entries()));
return { content: [{ type: 'text', text: JSON.stringify(data) }] };
},
},
{
name: 'local.descriptions',
description: 'Brave Local Search AI descriptions: fetch descriptions for up to 20 location ids',
inputSchema: idsArraySchema,
async handler(args) {
const usp = new URLSearchParams();
for (const id of args.ids)
usp.append('ids', id);
const data = await braveGet('https://api.search.brave.com/res/v1/local/descriptions', Object.fromEntries(usp.entries()));
return { content: [{ type: 'text', text: JSON.stringify(data) }] };
},
},
{
name: 'web.rich',
description: 'Brave Rich Search via callback flow. First enables rich callback, then fetches rich data',
inputSchema: richCallbackSchema,
async handler(args) {
const first = await braveGet('https://api.search.brave.com/res/v1/web/search', {
q: args.q,
enable_rich_callback: 1,
});
const callbackKey = first?.rich?.hint?.callback_key;
if (!callbackKey) {
return { content: [{ type: 'text', text: 'No rich results available for this query.' }] };
}
const rich = await braveGet('https://api.search.brave.com/res/v1/web/rich', {
callback_key: callbackKey,
});
return { content: [{ type: 'text', text: JSON.stringify({ hint: first?.rich?.hint, rich }) }] };
},
},
];
const toolList = toolDefs.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }));
const toolMap = new Map(toolDefs.map((t) => [t.name, t]));
async function main() {
const server = new Server({ name: CANONICAL_ID, version: '0.1.0' }, { capabilities: { tools: { listChanged: false } } });
// tools/list
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools: toolList };
});
// tools/call
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const name = request.params.name;
const args = request.params.arguments ?? {};
const tool = toolMap.get(name);
if (!tool) {
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }] };
}
try {
const result = await tool.handler(args);
return result;
}
catch (err) {
const message = err?.message ?? String(err);
return { content: [{ type: 'text', text: `Error: ${message}` }] };
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`[${CANONICAL_ID}] MCP server started on STDIO`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});