#!/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,
ListToolsResultSchema,
CallToolRequestSchema,
CallToolResultSchema,
} 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,
} as const;
const idsArraySchema = {
type: 'object',
properties: {
ids: {
type: 'array',
items: { type: 'string' },
minItems: 1,
maxItems: 20,
},
},
required: ['ids'],
additionalProperties: false,
} as const;
const richCallbackSchema = {
type: 'object',
properties: { q: { type: 'string', minLength: 1 } },
required: ['q'],
additionalProperties: false,
} as const;
// Utility fetch wrapper
async function braveGet(url: string, params: Record<string, string | number | boolean | undefined>) {
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: Record<string, string> = {
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();
}
// Tool registry
type ToolDef = {
name: string;
description?: string;
inputSchema: Record<string, unknown>;
handler: (args: any) => Promise<{ content: Array<{ type: 'text'; text: string }> }>;
};
const toolDefs: ToolDef[] = [
{
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 as string[]) 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 as string[]) 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 as any)?.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 as any)?.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 } as unknown as typeof ListToolsResultSchema._type;
});
// 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}` }] } as unknown as typeof CallToolResultSchema._type;
}
try {
const result = await tool.handler(args);
return result as unknown as typeof CallToolResultSchema._type;
} catch (err: any) {
const message = err?.message ?? String(err);
return { content: [{ type: 'text', text: `Error: ${message}` }] } as unknown as typeof CallToolResultSchema._type;
}
});
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);
});