5
5
* SPDX-License-Identifier: Apache-2.0
6
6
*/
7
7
8
+ import express , { Request , Response } from "express" ;
9
+ import morgan from "morgan" ;
8
10
import { z } from "zod" ;
9
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" ;
10
- import { Client , estypes , ClientOptions } from "@elastic/elasticsearch" ;
12
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js" ;
13
+ import { Client , ClientOptions , estypes } from "@elastic/elasticsearch" ;
11
14
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" ;
12
15
import fs from "fs" ;
13
16
14
17
// Configuration schema with auth options
15
18
const ConfigSchema = z
16
19
. object ( {
20
+ sse_addr : z
21
+ . string ( )
22
+ . trim ( )
23
+ . optional ( )
24
+ . describe ( "Address for SSE server (optional)" ) ,
25
+
17
26
url : z
18
27
. string ( )
19
28
. trim ( )
@@ -65,13 +74,13 @@ const ConfigSchema = z
65
74
message :
66
75
"Either ES_API_KEY or both ES_USERNAME and ES_PASSWORD must be provided, or no auth for local development" ,
67
76
path : [ "username" , "password" ] ,
68
- }
77
+ } ,
69
78
) ;
70
79
71
80
type ElasticsearchConfig = z . infer < typeof ConfigSchema > ;
72
81
73
82
export async function createElasticsearchMcpServer (
74
- config : ElasticsearchConfig
83
+ config : ElasticsearchConfig ,
75
84
) {
76
85
const validatedConfig = ConfigSchema . parse ( config ) ;
77
86
const { url, apiKey, username, password, caCert } = validatedConfig ;
@@ -96,7 +105,7 @@ export async function createElasticsearchMcpServer(
96
105
console . error (
97
106
`Failed to read certificate file: ${
98
107
error instanceof Error ? error . message : String ( error )
99
- } `
108
+ } `,
100
109
) ;
101
110
}
102
111
}
@@ -140,7 +149,7 @@ export async function createElasticsearchMcpServer(
140
149
console . error (
141
150
`Failed to list indices: ${
142
151
error instanceof Error ? error . message : String ( error )
143
- } `
152
+ } `,
144
153
) ;
145
154
return {
146
155
content : [
@@ -153,7 +162,7 @@ export async function createElasticsearchMcpServer(
153
162
] ,
154
163
} ;
155
164
}
156
- }
165
+ } ,
157
166
) ;
158
167
159
168
// Tool 2: Get mappings for an index
@@ -181,19 +190,21 @@ export async function createElasticsearchMcpServer(
181
190
} ,
182
191
{
183
192
type : "text" as const ,
184
- text : `Mappings for index ${ index } : ${ JSON . stringify (
185
- mappingResponse [ index ] ?. mappings || { } ,
186
- null ,
187
- 2
188
- ) } `,
193
+ text : `Mappings for index ${ index } : ${
194
+ JSON . stringify (
195
+ mappingResponse [ index ] ?. mappings || { } ,
196
+ null ,
197
+ 2 ,
198
+ )
199
+ } `,
189
200
} ,
190
201
] ,
191
202
} ;
192
203
} catch ( error ) {
193
204
console . error (
194
205
`Failed to get mappings: ${
195
206
error instanceof Error ? error . message : String ( error )
196
- } `
207
+ } `,
197
208
) ;
198
209
return {
199
210
content : [
@@ -206,7 +217,7 @@ export async function createElasticsearchMcpServer(
206
217
] ,
207
218
} ;
208
219
}
209
- }
220
+ } ,
210
221
) ;
211
222
212
223
// Tool 3: Search an index with simplified parameters
@@ -233,10 +244,10 @@ export async function createElasticsearchMcpServer(
233
244
} ,
234
245
{
235
246
message : "queryBody must be a valid Elasticsearch query DSL object" ,
236
- }
247
+ } ,
237
248
)
238
249
. describe (
239
- "Complete Elasticsearch query DSL object that can include query, size, from, sort, etc."
250
+ "Complete Elasticsearch query DSL object that can include query, size, from, sort, etc." ,
240
251
) ,
241
252
} ,
242
253
async ( { index, queryBody } ) => {
@@ -257,9 +268,11 @@ export async function createElasticsearchMcpServer(
257
268
if ( indexMappings . properties ) {
258
269
const textFields : Record < string , estypes . SearchHighlightField > = { } ;
259
270
260
- for ( const [ fieldName , fieldData ] of Object . entries (
261
- indexMappings . properties
262
- ) ) {
271
+ for (
272
+ const [ fieldName , fieldData ] of Object . entries (
273
+ indexMappings . properties ,
274
+ )
275
+ ) {
263
276
if ( fieldData . type === "text" || "dense_vector" in fieldData ) {
264
277
textFields [ fieldName ] = { } ;
265
278
}
@@ -285,9 +298,11 @@ export async function createElasticsearchMcpServer(
285
298
286
299
for ( const [ field , highlights ] of Object . entries ( highlightedFields ) ) {
287
300
if ( highlights && highlights . length > 0 ) {
288
- content += `${ field } (highlighted): ${ highlights . join (
289
- " ... "
290
- ) } \n`;
301
+ content += `${ field } (highlighted): ${
302
+ highlights . join (
303
+ " ... " ,
304
+ )
305
+ } \n`;
291
306
}
292
307
}
293
308
@@ -319,7 +334,7 @@ export async function createElasticsearchMcpServer(
319
334
console . error (
320
335
`Search failed: ${
321
336
error instanceof Error ? error . message : String ( error )
322
- } `
337
+ } `,
323
338
) ;
324
339
return {
325
340
content : [
@@ -332,7 +347,7 @@ export async function createElasticsearchMcpServer(
332
347
] ,
333
348
} ;
334
349
}
335
- }
350
+ } ,
336
351
) ;
337
352
338
353
// Tool 4: Get shard information
@@ -383,7 +398,7 @@ export async function createElasticsearchMcpServer(
383
398
console . error (
384
399
`Failed to get shard information: ${
385
400
error instanceof Error ? error . message : String ( error )
386
- } `
401
+ } `,
387
402
) ;
388
403
return {
389
404
content : [
@@ -396,36 +411,81 @@ export async function createElasticsearchMcpServer(
396
411
] ,
397
412
} ;
398
413
}
399
- }
414
+ } ,
400
415
) ;
401
416
402
417
return server ;
403
418
}
404
419
405
420
const config : ElasticsearchConfig = {
421
+ sse_addr : process . env . SSE_ADDR || "" ,
406
422
url : process . env . ES_URL || "" ,
407
423
apiKey : process . env . ES_API_KEY || "" ,
408
424
username : process . env . ES_USERNAME || "" ,
409
425
password : process . env . ES_PASSWORD || "" ,
410
426
caCert : process . env . ES_CA_CERT || "" ,
411
427
} ;
412
428
429
+ async function create_sse_server ( server : any ) {
430
+ const app = express ( ) ;
431
+
432
+ // Use morgan to log every request
433
+ app . use ( morgan ( "combined" ) ) ;
434
+
435
+ const transports : { [ sessionId : string ] : SSEServerTransport } = { } ;
436
+
437
+ app . get ( "/sse" , async ( _ : Request , res : Response ) => {
438
+ const transport = new SSEServerTransport ( "/messages" , res ) ;
439
+ transports [ transport . sessionId ] = transport ;
440
+ res . on ( "close" , ( ) => {
441
+ delete transports [ transport . sessionId ] ;
442
+ } ) ;
443
+ await server . connect ( transport ) ;
444
+ } ) ;
445
+
446
+ app . post ( "/messages" , async ( req : Request , res : Response ) => {
447
+ const sessionId = req . query . sessionId as string ;
448
+ const transport = transports [ sessionId ] ;
449
+ if ( transport ) {
450
+ await transport . handlePostMessage ( req , res ) ;
451
+ } else {
452
+ res . status ( 400 ) . send ( "No transport found for sessionId" ) ;
453
+ }
454
+ } ) ;
455
+
456
+ const sseAddr = process . env . SSE_ADDR || "127.0.0.1:3000" ;
457
+ const [ host , port ] = sseAddr . split ( ":" ) ;
458
+ if ( ! port ) {
459
+ console . error ( "Invalid SSE_ADDR format. Expected 'host:port'." ) ;
460
+ process . exit ( 1 ) ;
461
+ }
462
+
463
+ app . listen ( Number ( port ) , host , ( ) => {
464
+ console . info ( `SSE server started on: ${ host } :${ port } ` ) ;
465
+ } ) ;
466
+ }
467
+
413
468
async function main ( ) {
414
- const transport = new StdioServerTransport ( ) ;
469
+ console . info ( "Starting Elasticsearch MCP server..." ) ;
415
470
const server = await createElasticsearchMcpServer ( config ) ;
416
471
417
- await server . connect ( transport ) ;
472
+ if ( process . env . SSE_ADDR ) {
473
+ await create_sse_server ( server ) ;
474
+ } else {
475
+ const transport = new StdioServerTransport ( ) ;
476
+ await server . connect ( transport ) ;
418
477
419
- process . on ( "SIGINT" , async ( ) => {
420
- await server . close ( ) ;
421
- process . exit ( 0 ) ;
422
- } ) ;
478
+ process . on ( "SIGINT" , async ( ) => {
479
+ await server . close ( ) ;
480
+ process . exit ( 0 ) ;
481
+ } ) ;
482
+ }
423
483
}
424
484
425
485
main ( ) . catch ( ( error ) => {
426
486
console . error (
427
487
"Server error:" ,
428
- error instanceof Error ? error . message : String ( error )
488
+ error instanceof Error ? error . message : String ( error ) ,
429
489
) ;
430
490
process . exit ( 1 ) ;
431
491
} ) ;
0 commit comments