elasticsearch-mcp
Version:
Secure MCP server for Elasticsearch integration with comprehensive tools and Elastic Cloud support
163 lines • 7.14 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.UpdateDocumentTool = void 0;
const schemas_js_1 = require("../validation/schemas.js");
const handlers_js_1 = require("../errors/handlers.js");
class UpdateDocumentTool {
elasticsearch;
logger;
constructor(elasticsearch, logger) {
this.elasticsearch = elasticsearch;
this.logger = logger.child({ tool: 'update-document' });
}
async execute(args) {
try {
const validatedArgs = schemas_js_1.UpdateDocumentArgsSchema.parse(args);
this.logger.info('Updating document', {
index: validatedArgs.index,
id: validatedArgs.id,
hasDocument: !!validatedArgs.document,
hasScript: !!validatedArgs.script,
upsert: validatedArgs.upsert,
refresh: validatedArgs.refresh,
});
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`);
}
// Prepare update body
const updateBody = {};
if (validatedArgs.document) {
this.validateDocument(validatedArgs.document);
updateBody.doc = validatedArgs.document;
if (validatedArgs.upsert) {
updateBody.doc_as_upsert = true;
}
}
if (validatedArgs.script) {
updateBody.script = this.validateAndPrepareScript(validatedArgs.script);
if (validatedArgs.upsert && validatedArgs.document) {
updateBody.upsert = validatedArgs.document;
}
}
// Execute update
const response = await client.update({
index: validatedArgs.index,
id: validatedArgs.id,
body: updateBody,
refresh: this.normalizeRefreshParameter(validatedArgs.refresh),
retry_on_conflict: 3, // Retry on version conflicts
});
this.logger.info('Successfully updated document', {
id: response._id,
index: response._index,
version: response._version,
result: response.result,
});
return {
_id: response._id,
_index: response._index,
_version: response._version,
result: response.result,
};
}
catch (error) {
if (error instanceof Error && error.name === 'ZodError') {
throw new handlers_js_1.ValidationError('Invalid arguments for update_document', {
details: error.message,
});
}
// Handle Elasticsearch specific errors
if (error instanceof Error) {
const errorMessage = error.message.toLowerCase();
if (errorMessage.includes('document_missing_exception') || errorMessage.includes('not_found')) {
throw new handlers_js_1.NotFoundError(`Document with ID '${args?.id}' not found in index '${args?.index}'`);
}
if (errorMessage.includes('version_conflict')) {
throw new handlers_js_1.ValidationError('Document was modified by another process, please retry');
}
}
this.logger.error('Failed to update document', {}, error);
throw new handlers_js_1.ElasticsearchError('Failed to update document in Elasticsearch', error, { args });
}
}
validateDocument(document) {
// Check if document is empty
if (Object.keys(document).length === 0) {
throw new handlers_js_1.ValidationError('Update document cannot be empty');
}
// Check for invalid field names
for (const key of Object.keys(document)) {
if (key.startsWith('_')) {
throw new handlers_js_1.ValidationError(`Field name '${key}' cannot start with underscore (reserved)`);
}
if (key.includes('.') && !this.isValidDottedField(key)) {
throw new handlers_js_1.ValidationError(`Invalid field name '${key}': improper dot notation`);
}
}
// Validate document size
const documentSize = JSON.stringify(document).length;
if (documentSize > 100 * 1024 * 1024) { // 100MB limit
throw new handlers_js_1.ValidationError('Document size exceeds 100MB limit');
}
}
validateAndPrepareScript(script) {
if (!script.source || script.source.trim().length === 0) {
throw new handlers_js_1.ValidationError('Script source cannot be empty');
}
// Sanitize script source for security
const sanitizedSource = (0, schemas_js_1.sanitizeScriptSource)(script.source);
// Validate script parameters
if (script.params) {
this.validateScriptParams(script.params);
}
return {
source: sanitizedSource,
...(script.params && { params: script.params }),
lang: 'painless', // Default to Painless scripting language
};
}
validateScriptParams(params) {
const maxParams = 50;
const paramKeys = Object.keys(params);
if (paramKeys.length > maxParams) {
throw new handlers_js_1.ValidationError(`Too many script parameters (max ${maxParams})`);
}
for (const [key, value] of Object.entries(params)) {
// Check parameter name
if (key.length > 128) {
throw new handlers_js_1.ValidationError(`Parameter name '${key}' too long (max 128 characters)`);
}
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
throw new handlers_js_1.ValidationError(`Invalid parameter name '${key}': must be a valid identifier`);
}
// Check parameter value size
const valueSize = JSON.stringify(value).length;
if (valueSize > 1024 * 1024) { // 1MB limit per parameter
throw new handlers_js_1.ValidationError(`Parameter '${key}' value exceeds 1MB limit`);
}
}
}
isValidDottedField(fieldName) {
// Check for valid dot notation (no consecutive dots, no leading/trailing dots)
return !/^\.|\.$|\.\./.test(fieldName);
}
normalizeRefreshParameter(refresh) {
if (refresh === undefined || refresh === false || refresh === 'false') {
return false;
}
if (refresh === true || refresh === 'true') {
return true;
}
if (refresh === 'wait_for') {
return 'wait_for';
}
return false;
}
}
exports.UpdateDocumentTool = UpdateDocumentTool;
//# sourceMappingURL=update-document.js.map