feat(mongo): add dropIndex and dropIndexes support across full stack

This commit is contained in:
onenewcode authored and GitHub committed 2026-07-01 17:53:09 +08:00
1 parent bb72a58e54
commit 93eeeaa9c6
29 files changed
+1142 -24

No files matched your search

+95 -1
View File
@@ -35,7 +35,9 @@ export type MongoWriteCommand =
| { kind: "insert"; collection: string; docsJson: string }
| { kind: "update"; collection: string; filter: string; update: string; many: boolean }
| { kind: "delete"; collection: string; filter: string; many: boolean }
| { kind: "createIndex"; collection: string; keys: string; options?: string };
| { kind: "createIndex"; collection: string; keys: string; options?: string }
| { kind: "dropIndex"; collection: string; index: string }
| { kind: "dropIndexes"; collection: string; indexes?: string };
export interface MongoAggregateSafetyOptions {
allowWrites?: boolean;
@@ -220,9 +222,47 @@ export function parseMongoWriteCommand(input: string): MongoWriteCommand | null
return { kind: "createIndex", collection: createIndex.collection, keys, ...(options ? { options } : {}) };
}
const dropIndex = parseCollectionMethodTarget(source, "dropIndex");
if (dropIndex) {
const args = parseMethodArgs(source, dropIndex.methodCallIndex);
if (!args) return null;
const index = parseMongoDropIndexArgument(args);
return index ? { kind: "dropIndex", collection: dropIndex.collection, index } : null;
}
const dropIndexes = parseCollectionMethodTarget(source, "dropIndexes");
if (dropIndexes) {
const args = parseMethodArgs(source, dropIndexes.methodCallIndex);
if (!args) return null;
const indexes = parseMongoDropIndexesArgument(args);
return indexes !== null ? { kind: "dropIndexes", collection: dropIndexes.collection, ...(indexes ? { indexes } : {}) } : null;
}
return null;
}
export function evaluateMongoWriteSafety(command: MongoWriteCommand, options: MongoAggregateSafetyOptions): { allowed: boolean; reason?: string } {
if (!options.allowWrites) {
return {
allowed: false,
reason: "MCP MongoDB execution is read-only by default. Set DBX_MCP_ALLOW_WRITES=1 to allow write commands.",
};
}
if (!options.allowDangerous && (command.kind === "update" || command.kind === "delete") && isEmptyJsonObject(command.filter)) {
return {
allowed: false,
reason: "MongoDB update/delete commands must include a non-empty filter unless DBX_MCP_ALLOW_DANGEROUS_SQL=1 is set.",
};
}
if (!options.allowDangerous && mongoDropIndexesRequiresDangerous(command)) {
return {
allowed: false,
reason: "MongoDB dropIndexes() without a specific single index requires DBX_MCP_ALLOW_DANGEROUS_SQL=1.",
};
}
return { allowed: true };
}
export function mongoAggregateWriteStage(pipelineJson: string): "$out" | "$merge" | null {
try {
const pipeline = JSON.parse(pipelineJson);
@@ -310,6 +350,15 @@ export function mongoCreateIndexToQueryResult(name: string, executionTimeMs: num
};
}
export function mongoDroppedIndexesToQueryResult(names: string[], executionTimeMs: number): QueryResult {
return {
columns: ["name"],
rows: names.map((name) => [name]),
affected_rows: names.length,
execution_time_ms: Math.max(0, Math.round(executionTimeMs)),
};
}
export function mongoUseToQueryResult(database: string, executionTimeMs: number): QueryResult {
return {
columns: ["message"],
@@ -444,6 +493,26 @@ function convertSingleQuotedStrings(source: string): string {
return quote === "'" ? source : result + source.slice(copiedUntil);
}
function parseMongoDropIndexArgument(args: string[]): string | null {
if (args.length !== 1 || !args[0]?.trim()) return null;
const normalized = normalizeJsonArgument(args[0]);
if (!normalized) return null;
const parsed = parseNormalizedJson(normalized);
if (typeof parsed === "string") return parsed === "*" ? null : normalized;
return isNonEmptyRecord(parsed) ? normalized : null;
}
function parseMongoDropIndexesArgument(args: string[]): string | undefined | null {
if (args.length !== 1) return null;
if (!args[0]?.trim()) return undefined;
const normalized = normalizeJsonArgument(args[0]);
if (!normalized) return null;
const parsed = parseNormalizedJson(normalized);
if (typeof parsed === "string") return normalized;
if (isNonEmptyRecord(parsed)) return normalized;
return Array.isArray(parsed) && parsed.length > 0 && parsed.every((item) => typeof item === "string") ? normalized : null;
}
export function quoteUnquotedObjectKeys(source: string): string {
let result = "";
let quote: string | null = null;
@@ -577,6 +646,31 @@ function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function parseNormalizedJson(json: string): unknown {
try {
return JSON.parse(json);
} catch {
return undefined;
}
}
function isNonEmptyRecord(value: unknown): value is Record<string, unknown> {
return isRecord(value) && Object.keys(value).length > 0;
}
function isEmptyJsonObject(json: string): boolean {
const parsed = parseNormalizedJson(json);
return isRecord(parsed) && Object.keys(parsed).length === 0;
}
function mongoDropIndexesRequiresDangerous(command: MongoWriteCommand): boolean {
if (command.kind !== "dropIndexes") return false;
if (!command.indexes) return true;
const parsed = parseNormalizedJson(command.indexes);
if (parsed === "*") return true;
return Array.isArray(parsed) && parsed.length > 1;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === "object" && !Array.isArray(value);
}