@@ -5,65 +5,12 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
5
5
import {
6
6
CallToolRequestSchema ,
7
7
ListToolsRequestSchema ,
8
- Tool ,
9
8
} from "@modelcontextprotocol/sdk/types.js" ;
10
-
11
- const WEB_SEARCH_TOOL : Tool = {
12
- name : "brave_web_search" ,
13
- description :
14
- "Performs a web search using the Brave Search API, ideal for general queries, news, articles, and online content. " +
15
- "Use this for broad information gathering, recent events, or when you need diverse web sources. " +
16
- "Supports pagination, content filtering, and freshness controls. " +
17
- "Maximum 20 results per request, with offset for pagination. " ,
18
- inputSchema : {
19
- type : "object" ,
20
- properties : {
21
- query : {
22
- type : "string" ,
23
- description : "Search query (max 400 chars, 50 words)"
24
- } ,
25
- count : {
26
- type : "number" ,
27
- description : "Number of results (1-20, default 10)" ,
28
- default : 10
29
- } ,
30
- offset : {
31
- type : "number" ,
32
- description : "Pagination offset (max 9, default 0)" ,
33
- default : 0
34
- } ,
35
- } ,
36
- required : [ "query" ] ,
37
- } ,
38
- } ;
39
-
40
- const LOCAL_SEARCH_TOOL : Tool = {
41
- name : "brave_local_search" ,
42
- description :
43
- "Searches for local businesses and places using Brave's Local Search API. " +
44
- "Best for queries related to physical locations, businesses, restaurants, services, etc. " +
45
- "Returns detailed information including:\n" +
46
- "- Business names and addresses\n" +
47
- "- Ratings and review counts\n" +
48
- "- Phone numbers and opening hours\n" +
49
- "Use this when the query implies 'near me' or mentions specific locations. " +
50
- "Automatically falls back to web search if no local results are found." ,
51
- inputSchema : {
52
- type : "object" ,
53
- properties : {
54
- query : {
55
- type : "string" ,
56
- description : "Local search query (e.g. 'pizza near Central Park')"
57
- } ,
58
- count : {
59
- type : "number" ,
60
- description : "Number of results (1-20, default 5)" ,
61
- default : 5
62
- } ,
63
- } ,
64
- required : [ "query" ]
65
- }
66
- } ;
9
+ import tools from "./tools.js" ;
10
+ import { handleRequest as handleImageSearchRequest } from "./tools/imageSearch.js" ;
11
+ import { handleRequest as handleWebSearchRequest } from "./tools/webSearch.js" ;
12
+ import { handleRequest as handleLocalSearchRequest } from "./tools/localSearch.js" ;
13
+ import { checkEnvVariables } from "./env.js" ;
67
14
68
15
// Server implementation
69
16
const server = new Server (
@@ -79,238 +26,10 @@ const server = new Server(
79
26
) ;
80
27
81
28
// Check for API key
82
- const BRAVE_API_KEY = process . env . BRAVE_API_KEY ! ;
83
- if ( ! BRAVE_API_KEY ) {
84
- console . error ( "Error: BRAVE_API_KEY environment variable is required" ) ;
85
- process . exit ( 1 ) ;
86
- }
87
-
88
- const RATE_LIMIT = {
89
- perSecond : 1 ,
90
- perMonth : 15000
91
- } ;
92
-
93
- let requestCount = {
94
- second : 0 ,
95
- month : 0 ,
96
- lastReset : Date . now ( )
97
- } ;
98
-
99
- function checkRateLimit ( ) {
100
- const now = Date . now ( ) ;
101
- if ( now - requestCount . lastReset > 1000 ) {
102
- requestCount . second = 0 ;
103
- requestCount . lastReset = now ;
104
- }
105
- if ( requestCount . second >= RATE_LIMIT . perSecond ||
106
- requestCount . month >= RATE_LIMIT . perMonth ) {
107
- throw new Error ( 'Rate limit exceeded' ) ;
108
- }
109
- requestCount . second ++ ;
110
- requestCount . month ++ ;
111
- }
112
-
113
- interface BraveWeb {
114
- web ?: {
115
- results ?: Array < {
116
- title : string ;
117
- description : string ;
118
- url : string ;
119
- language ?: string ;
120
- published ?: string ;
121
- rank ?: number ;
122
- } > ;
123
- } ;
124
- locations ?: {
125
- results ?: Array < {
126
- id : string ; // Required by API
127
- title ?: string ;
128
- } > ;
129
- } ;
130
- }
131
-
132
- interface BraveLocation {
133
- id : string ;
134
- name : string ;
135
- address : {
136
- streetAddress ?: string ;
137
- addressLocality ?: string ;
138
- addressRegion ?: string ;
139
- postalCode ?: string ;
140
- } ;
141
- coordinates ?: {
142
- latitude : number ;
143
- longitude : number ;
144
- } ;
145
- phone ?: string ;
146
- rating ?: {
147
- ratingValue ?: number ;
148
- ratingCount ?: number ;
149
- } ;
150
- openingHours ?: string [ ] ;
151
- priceRange ?: string ;
152
- }
153
-
154
- interface BravePoiResponse {
155
- results : BraveLocation [ ] ;
156
- }
157
-
158
- interface BraveDescription {
159
- descriptions : { [ id : string ] : string } ;
160
- }
161
-
162
- function isBraveWebSearchArgs ( args : unknown ) : args is { query: string ; count ? : number } {
163
- return (
164
- typeof args === "object" &&
165
- args !== null &&
166
- "query" in args &&
167
- typeof ( args as { query : string } ) . query === "string"
168
- ) ;
169
- }
170
-
171
- function isBraveLocalSearchArgs ( args : unknown ) : args is { query: string ; count ? : number } {
172
- return (
173
- typeof args === "object" &&
174
- args !== null &&
175
- "query" in args &&
176
- typeof ( args as { query : string } ) . query === "string"
177
- ) ;
178
- }
179
-
180
- async function performWebSearch ( query : string , count : number = 10 , offset : number = 0 ) {
181
- checkRateLimit ( ) ;
182
- const url = new URL ( 'https://api.search.brave.com/res/v1/web/search' ) ;
183
- url . searchParams . set ( 'q' , query ) ;
184
- url . searchParams . set ( 'count' , Math . min ( count , 20 ) . toString ( ) ) ; // API limit
185
- url . searchParams . set ( 'offset' , offset . toString ( ) ) ;
186
-
187
- const response = await fetch ( url , {
188
- headers : {
189
- 'Accept' : 'application/json' ,
190
- 'Accept-Encoding' : 'gzip' ,
191
- 'X-Subscription-Token' : BRAVE_API_KEY
192
- }
193
- } ) ;
194
-
195
- if ( ! response . ok ) {
196
- throw new Error ( `Brave API error: ${ response . status } ${ response . statusText } \n${ await response . text ( ) } ` ) ;
197
- }
198
-
199
- const data = await response . json ( ) as BraveWeb ;
200
-
201
- // Extract just web results
202
- const results = ( data . web ?. results || [ ] ) . map ( result => ( {
203
- title : result . title || '' ,
204
- description : result . description || '' ,
205
- url : result . url || ''
206
- } ) ) ;
207
-
208
- return results . map ( r =>
209
- `Title: ${ r . title } \nDescription: ${ r . description } \nURL: ${ r . url } `
210
- ) . join ( '\n\n' ) ;
211
- }
212
-
213
- async function performLocalSearch ( query : string , count : number = 5 ) {
214
- checkRateLimit ( ) ;
215
- // Initial search to get location IDs
216
- const webUrl = new URL ( 'https://api.search.brave.com/res/v1/web/search' ) ;
217
- webUrl . searchParams . set ( 'q' , query ) ;
218
- webUrl . searchParams . set ( 'search_lang' , 'en' ) ;
219
- webUrl . searchParams . set ( 'result_filter' , 'locations' ) ;
220
- webUrl . searchParams . set ( 'count' , Math . min ( count , 20 ) . toString ( ) ) ;
221
-
222
- const webResponse = await fetch ( webUrl , {
223
- headers : {
224
- 'Accept' : 'application/json' ,
225
- 'Accept-Encoding' : 'gzip' ,
226
- 'X-Subscription-Token' : BRAVE_API_KEY
227
- }
228
- } ) ;
229
-
230
- if ( ! webResponse . ok ) {
231
- throw new Error ( `Brave API error: ${ webResponse . status } ${ webResponse . statusText } \n${ await webResponse . text ( ) } ` ) ;
232
- }
233
-
234
- const webData = await webResponse . json ( ) as BraveWeb ;
235
- const locationIds = webData . locations ?. results ?. filter ( ( r ) : r is { id : string ; title ? : string } = > r . id != null ) . map ( r => r . id ) || [ ] ;
236
-
237
- if ( locationIds . length === 0 ) {
238
- return performWebSearch ( query , count ) ; // Fallback to web search
239
- }
240
-
241
- // Get POI details and descriptions in parallel
242
- const [ poisData , descriptionsData ] = await Promise . all ( [
243
- getPoisData ( locationIds ) ,
244
- getDescriptionsData ( locationIds )
245
- ] ) ;
246
-
247
- return formatLocalResults ( poisData , descriptionsData ) ;
248
- }
249
-
250
- async function getPoisData ( ids : string [ ] ) : Promise < BravePoiResponse > {
251
- checkRateLimit ( ) ;
252
- const url = new URL ( 'https://api.search.brave.com/res/v1/local/pois' ) ;
253
- ids . filter ( Boolean ) . forEach ( id => url . searchParams . append ( 'ids' , id ) ) ;
254
- const response = await fetch ( url , {
255
- headers : {
256
- 'Accept' : 'application/json' ,
257
- 'Accept-Encoding' : 'gzip' ,
258
- 'X-Subscription-Token' : BRAVE_API_KEY
259
- }
260
- } ) ;
261
-
262
- if ( ! response . ok ) {
263
- throw new Error ( `Brave API error: ${ response . status } ${ response . statusText } \n${ await response . text ( ) } ` ) ;
264
- }
265
-
266
- const poisResponse = await response . json ( ) as BravePoiResponse ;
267
- return poisResponse ;
268
- }
269
-
270
- async function getDescriptionsData ( ids : string [ ] ) : Promise < BraveDescription > {
271
- checkRateLimit ( ) ;
272
- const url = new URL ( 'https://api.search.brave.com/res/v1/local/descriptions' ) ;
273
- ids . filter ( Boolean ) . forEach ( id => url . searchParams . append ( 'ids' , id ) ) ;
274
- const response = await fetch ( url , {
275
- headers : {
276
- 'Accept' : 'application/json' ,
277
- 'Accept-Encoding' : 'gzip' ,
278
- 'X-Subscription-Token' : BRAVE_API_KEY
279
- }
280
- } ) ;
281
-
282
- if ( ! response . ok ) {
283
- throw new Error ( `Brave API error: ${ response . status } ${ response . statusText } \n${ await response . text ( ) } ` ) ;
284
- }
285
-
286
- const descriptionsData = await response . json ( ) as BraveDescription ;
287
- return descriptionsData ;
288
- }
289
-
290
- function formatLocalResults ( poisData : BravePoiResponse , descData : BraveDescription ) : string {
291
- return ( poisData . results || [ ] ) . map ( poi => {
292
- const address = [
293
- poi . address ?. streetAddress ?? '' ,
294
- poi . address ?. addressLocality ?? '' ,
295
- poi . address ?. addressRegion ?? '' ,
296
- poi . address ?. postalCode ?? ''
297
- ] . filter ( part => part !== '' ) . join ( ', ' ) || 'N/A' ;
298
-
299
- return `Name: ${ poi . name }
300
- Address: ${ address }
301
- Phone: ${ poi . phone || 'N/A' }
302
- Rating: ${ poi . rating ?. ratingValue ?? 'N/A' } (${ poi . rating ?. ratingCount ?? 0 } reviews)
303
- Price Range: ${ poi . priceRange || 'N/A' }
304
- Hours: ${ ( poi . openingHours || [ ] ) . join ( ', ' ) || 'N/A' }
305
- Description: ${ descData . descriptions [ poi . id ] || 'No description available' }
306
- ` ;
307
- } ) . join ( '\n---\n' ) || 'No local results found' ;
308
- }
29
+ checkEnvVariables ( ) ;
309
30
310
31
// Tool handlers
311
- server . setRequestHandler ( ListToolsRequestSchema , async ( ) => ( {
312
- tools : [ WEB_SEARCH_TOOL , LOCAL_SEARCH_TOOL ] ,
313
- } ) ) ;
32
+ server . setRequestHandler ( ListToolsRequestSchema , async ( ) => ( { tools } ) ) ;
314
33
315
34
server . setRequestHandler ( CallToolRequestSchema , async ( request ) => {
316
35
try {
@@ -321,30 +40,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
321
40
}
322
41
323
42
switch ( name ) {
324
- case "brave_web_search" : {
325
- if ( ! isBraveWebSearchArgs ( args ) ) {
326
- throw new Error ( "Invalid arguments for brave_web_search" ) ;
327
- }
328
- const { query, count = 10 } = args ;
329
- const results = await performWebSearch ( query , count ) ;
330
- return {
331
- content : [ { type : "text" , text : results } ] ,
332
- isError : false ,
333
- } ;
334
- }
335
-
336
- case "brave_local_search" : {
337
- if ( ! isBraveLocalSearchArgs ( args ) ) {
338
- throw new Error ( "Invalid arguments for brave_local_search" ) ;
339
- }
340
- const { query, count = 5 } = args ;
341
- const results = await performLocalSearch ( query , count ) ;
342
- return {
343
- content : [ { type : "text" , text : results } ] ,
344
- isError : false ,
345
- } ;
346
- }
347
-
43
+ case "brave_web_search" :
44
+ return handleWebSearchRequest ( args ) ;
45
+ case "brave_local_search" :
46
+ return handleLocalSearchRequest ( args ) ;
47
+ case "brave_image_search" :
48
+ return handleImageSearchRequest ( args ) ;
348
49
default :
349
50
return {
350
51
content : [ { type : "text" , text : `Unknown tool: ${ name } ` } ] ,
0 commit comments