UNPKG

elasticsearch-mcp

Version:

Secure MCP server for Elasticsearch integration with comprehensive tools and Elastic Cloud support

205 lines 8.56 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SearchElasticsearchTool = void 0; const schemas_js_1 = require("../validation/schemas.js"); const handlers_js_1 = require("../errors/handlers.js"); class SearchElasticsearchTool { elasticsearch; logger; constructor(elasticsearch, logger) { this.elasticsearch = elasticsearch; this.logger = logger.child({ tool: 'search-elasticsearch' }); } async execute(args) { try { const validatedArgs = schemas_js_1.SearchArgsSchema.parse(args); this.logger.info('Executing search', { index: validatedArgs.index, hasQuery: !!validatedArgs.query, size: validatedArgs.size, from: validatedArgs.from, hasSort: !!validatedArgs.sort, hasAggregations: !!validatedArgs.aggregations, hasHighlight: !!validatedArgs.highlight, }); const client = this.elasticsearch.getClient(); // Check if index exists const indexExists = await client.indices.exists({ index: validatedArgs.index, }); if (!indexExists) { throw new handlers_js_1.NotFoundError(`Index '${validatedArgs.index}' does not exist`); } // Build search request const searchRequest = this.buildSearchRequest(validatedArgs); // Execute search const response = await client.search(searchRequest); this.logger.info('Search executed successfully', { index: validatedArgs.index, totalHits: response.hits.total, took: response.took, hasAggregations: !!response.aggregations, }); return this.formatSearchResponse(response); } catch (error) { if (error instanceof Error && error.name === 'ZodError') { throw new handlers_js_1.ValidationError('Invalid arguments for search_elasticsearch', { details: error.message, }); } if (error instanceof handlers_js_1.ValidationError || error instanceof handlers_js_1.NotFoundError) { throw error; } this.logger.error('Failed to execute search', {}, error); throw new handlers_js_1.ElasticsearchError('Failed to execute search in Elasticsearch', error, { args }); } } buildSearchRequest(args) { const request = { index: args.index, body: {}, }; // Add query if (args.query) { const sanitizedQuery = (0, schemas_js_1.sanitizeQuery)(args.query); this.validateQuery(sanitizedQuery || {}); request.body.query = sanitizedQuery || { match_all: {} }; } else { request.body.query = { match_all: {} }; } // Add pagination if (args.size !== undefined) { request.body.size = Math.min(args.size, 10000); // Elasticsearch limit } if (args.from !== undefined) { request.body.from = args.from; } // Add sorting if (args.sort && args.sort.length > 0) { this.validateSort(args.sort); request.body.sort = args.sort; } // Add aggregations if (args.aggregations) { this.validateAggregations(args.aggregations); request.body.aggs = args.aggregations; } // Add highlighting if (args.highlight) { this.validateHighlight(args.highlight); request.body.highlight = args.highlight; } // Add source filtering if (args.source !== undefined) { if (Array.isArray(args.source)) { this.validateSourceFields(args.source); } request.body._source = args.source; } // Add timeout request.timeout = '30s'; return request; } validateQuery(query) { // Check for script queries (potential security risk) const queryStr = JSON.stringify(query).toLowerCase(); if (queryStr.includes('script_score') || queryStr.includes('script_query')) { throw new handlers_js_1.ValidationError('Script-based queries require additional security validation'); } // Validate query depth this.validateQueryDepth(query, 0); // Check for wildcard queries on analyzed fields (performance concern) if (queryStr.includes('wildcard') || queryStr.includes('prefix')) { this.logger.warn('Wildcard/prefix queries detected - may impact performance', { query }); } } validateSort(sort) { if (sort.length > 20) { throw new handlers_js_1.ValidationError('Too many sort criteria (max 20)'); } for (const sortItem of sort) { if (typeof sortItem !== 'object' || Array.isArray(sortItem)) { throw new handlers_js_1.ValidationError('Each sort item must be an object'); } // Check for script-based sorting const sortStr = JSON.stringify(sortItem).toLowerCase(); if (sortStr.includes('_script')) { throw new handlers_js_1.ValidationError('Script-based sorting is not allowed'); } } } validateAggregations(aggregations) { const aggStr = JSON.stringify(aggregations).toLowerCase(); // Check for script-based aggregations if (aggStr.includes('script')) { throw new handlers_js_1.ValidationError('Script-based aggregations require additional security validation'); } // Validate aggregation depth this.validateQueryDepth(aggregations, 0); // Check number of aggregations if (Object.keys(aggregations).length > 50) { throw new handlers_js_1.ValidationError('Too many aggregations (max 50)'); } } validateHighlight(highlight) { // Validate highlight configuration if (highlight.fields && typeof highlight.fields === 'object') { const fieldCount = Object.keys(highlight.fields).length; if (fieldCount > 20) { throw new handlers_js_1.ValidationError('Too many highlight fields (max 20)'); } } // Check for script-based highlighting const highlightStr = JSON.stringify(highlight).toLowerCase(); if (highlightStr.includes('script')) { throw new handlers_js_1.ValidationError('Script-based highlighting is not allowed'); } } validateSourceFields(fields) { if (fields.length > 100) { throw new handlers_js_1.ValidationError('Too many source fields specified (max 100)'); } for (const field of fields) { if (typeof field !== 'string' || field.length === 0) { throw new handlers_js_1.ValidationError('Source fields must be non-empty strings'); } if (field.length > 256) { throw new handlers_js_1.ValidationError(`Source field name too long: '${field}' (max 256 characters)`); } } } validateQueryDepth(obj, depth) { const MAX_QUERY_DEPTH = 20; if (depth > MAX_QUERY_DEPTH) { throw new handlers_js_1.ValidationError(`Query nesting exceeds maximum depth of ${MAX_QUERY_DEPTH}`); } for (const value of Object.values(obj)) { if (typeof value === 'object' && value !== null && !Array.isArray(value)) { this.validateQueryDepth(value, depth + 1); } } } formatSearchResponse(response) { const hits = response.hits.hits.map((hit) => ({ _id: hit._id, _source: hit._source || {}, _score: hit._score || 0, ...(hit.highlight && { highlight: hit.highlight }), })); return { hits: { total: { value: response.hits.total?.value || response.hits.total || 0, relation: response.hits.total?.relation || 'eq', }, hits, }, ...(response.aggregations && { aggregations: response.aggregations }), took: response.took || 0, }; } } exports.SearchElasticsearchTool = SearchElasticsearchTool; //# sourceMappingURL=search-elasticsearch.js.map