Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ export {
type OperationType,
type ToolArgs,
type ToolExecutionContext,
type ToolResult,
} from "./tool.js";
75 changes: 47 additions & 28 deletions src/tools/mongodb/metadata/collectionIndexes.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,80 @@
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
import type { ToolArgs, OperationType } from "../../tool.js";
import type { ToolArgs, OperationType, ToolResult } from "../../tool.js";
import { formatUntrustedData } from "../../tool.js";
import { z } from "zod";

type SearchIndexStatus = {
name: string;
type: string;
status: string;
queryable: boolean;
latestDefinition: Document;
const CollectionIndexesOutputSchema = {
classicIndexes: z.array(
z.object({
name: z.string(),
key: z.record(z.unknown()),
})
),
searchIndexes: z.array(
z.object({
name: z.string(),
type: z.string(),
status: z.string(),
queryable: z.boolean(),
latestDefinition: z.record(z.unknown()),
})
),
classicIndexesCount: z.number(),
searchIndexesCount: z.number(),
};

type IndexStatus = {
name: string;
key: Document;
};
export type CollectionIndexesOutput = z.infer<z.ZodObject<typeof CollectionIndexesOutputSchema>>;

type SearchIndexStatus = CollectionIndexesOutput["searchIndexes"][number];
type IndexStatus = CollectionIndexesOutput["classicIndexes"][number];

export class CollectionIndexesTool extends MongoDBToolBase {
public name = "collection-indexes";
public description = "Describe the indexes for a collection";
public argsShape = DbOperationArgs;
public override outputSchema = CollectionIndexesOutputSchema;
static operationType: OperationType = "metadata";

protected async execute({ database, collection }: ToolArgs<typeof DbOperationArgs>): Promise<CallToolResult> {
protected async execute({
database,
collection,
}: ToolArgs<typeof DbOperationArgs>): Promise<ToolResult<typeof this.outputSchema>> {
const provider = await this.ensureConnected();
const indexes = await provider.getIndexes(database, collection);
const indexDefinitions: IndexStatus[] = indexes.map((index) => ({
const classicIndexes: IndexStatus[] = indexes.map((index) => ({
name: index.name as string,
key: index.key as Document,
key: index.key as Record<string, unknown>,
}));

const searchIndexDefinitions: SearchIndexStatus[] = [];
const searchIndexes: SearchIndexStatus[] = [];
if (this.isFeatureEnabled("search") && (await this.session.isSearchSupported())) {
const searchIndexes = await provider.getSearchIndexes(database, collection);
searchIndexDefinitions.push(...this.extractSearchIndexDetails(searchIndexes));
searchIndexes.push(...this.extractSearchIndexDetails(searchIndexes));
}

return {
content: [
...formatUntrustedData(
`Found ${indexDefinitions.length} classic indexes in the collection "${collection}":`,
...indexDefinitions.map((i) => JSON.stringify(i))
`Found ${classicIndexes.length} classic indexes in the collection "${collection}":`,
JSON.stringify(classicIndexes)
),
...(searchIndexDefinitions.length > 0
...(searchIndexes.length > 0
? formatUntrustedData(
`Found ${searchIndexDefinitions.length} search and vector search indexes in the collection "${collection}":`,
...searchIndexDefinitions.map((i) => JSON.stringify(i))
`Found ${searchIndexes.length} search and vector search indexes in the collection "${collection}":`,
JSON.stringify(searchIndexes)
)
: []),
],
structuredContent: {
classicIndexes,
searchIndexes,
classicIndexesCount: classicIndexes.length,
searchIndexesCount: searchIndexes.length,
},
};
}

protected handleError(
error: unknown,
args: ToolArgs<typeof this.argsShape>
): Promise<CallToolResult> | CallToolResult {
protected handleError(error: unknown, args: ToolArgs<typeof this.argsShape>): ToolResult | Promise<ToolResult> {
if (error instanceof Error && "codeName" in error && error.codeName === "NamespaceNotFound") {
return {
content: [
Expand All @@ -68,7 +87,7 @@ export class CollectionIndexesTool extends MongoDBToolBase {
};
}

return super.handleError(error, args);
return super.handleError(error, args) as ToolResult | Promise<ToolResult>;
}

/**
Expand All @@ -83,7 +102,7 @@ export class CollectionIndexesTool extends MongoDBToolBase {
type: (index["type"] ?? "UNKNOWN") as string,
status: (index["status"] ?? "UNKNOWN") as string,
queryable: (index["queryable"] ?? false) as boolean,
latestDefinition: index["latestDefinition"] as Document,
latestDefinition: index["latestDefinition"] as Record<string, unknown>,
}));
}
}
31 changes: 21 additions & 10 deletions src/tools/mongodb/metadata/collectionSchema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
import type { ToolArgs, OperationType, ToolExecutionContext } from "../../tool.js";
import type { ToolArgs, OperationType, ToolExecutionContext, ToolResult } from "../../tool.js";
import { formatUntrustedData } from "../../tool.js";
import { getSimplifiedSchema } from "mongodb-schema";
import z from "zod";
Expand All @@ -10,6 +9,13 @@ import { isObjectEmpty } from "../../../helpers/isObjectEmpty.js";

const MAXIMUM_SAMPLE_SIZE_HARD_LIMIT = 50_000;

const CollectionSchemaOutputSchema = {
schema: z.record(z.unknown()),
fieldsCount: z.number(),
};

export type CollectionSchemaOutput = z.infer<z.ZodObject<typeof CollectionSchemaOutputSchema>>;

export class CollectionSchemaTool extends MongoDBToolBase {
public name = "collection-schema";
public description = "Describe the schema for a collection";
Expand All @@ -24,18 +30,19 @@ export class CollectionSchemaTool extends MongoDBToolBase {
`The maximum number of bytes to return in the response. This value is capped by the server's configured maxBytesPerQuery and cannot be exceeded.`
),
};
public override outputSchema = CollectionSchemaOutputSchema;

static operationType: OperationType = "metadata";

protected async execute(
{ database, collection, sampleSize, responseBytesLimit }: ToolArgs<typeof this.argsShape>,
{ signal }: ToolExecutionContext
): Promise<CallToolResult> {
): Promise<ToolResult<typeof this.outputSchema>> {
const provider = await this.ensureConnected();
const cursor = provider.aggregate(database, collection, [
{ $sample: { size: Math.min(sampleSize, MAXIMUM_SAMPLE_SIZE_HARD_LIMIT) } },
]);
const { cappedBy, documents } = await collectCursorUntilMaxBytesLimit({
const { documents } = await collectCursorUntilMaxBytesLimit({
cursor,
configuredMaxBytesPerQuery: this.config.maxBytesPerQuery,
toolResponseBytesLimit: responseBytesLimit,
Expand All @@ -51,18 +58,22 @@ export class CollectionSchemaTool extends MongoDBToolBase {
type: "text",
},
],
structuredContent: {
schema: {},
fieldsCount: 0,
},
};
}

const fieldsCount = Object.keys(schema).length;
const header = `Found ${fieldsCount} fields in the schema for "${database}.${collection}"`;
const cappedWarning =
cappedBy !== undefined
? `\nThe schema was inferred from a subset of documents due to the response size limit. (${cappedBy})`
: "";
const header = `Found ${fieldsCount} fields in the schema for "${database}.${collection}". Note that this schema is inferred from a sample and may not represent the full schema of the collection.`;

return {
content: formatUntrustedData(`${header}${cappedWarning}`, JSON.stringify(schema)),
content: formatUntrustedData(`${header}`, JSON.stringify(schema)),
structuredContent: {
schema,
fieldsCount,
},
};
}
}
28 changes: 20 additions & 8 deletions src/tools/mongodb/metadata/collectionStorageSize.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
import type { ToolArgs, OperationType } from "../../tool.js";
import type { ToolArgs, OperationType, ToolResult } from "../../tool.js";
import { z } from "zod";

const CollectionStorageSizeOutputSchema = {
size: z.number(),
units: z.string(),
};

export type CollectionStorageSizeOutput = z.infer<z.ZodObject<typeof CollectionStorageSizeOutputSchema>>;

export class CollectionStorageSizeTool extends MongoDBToolBase {
public name = "collection-storage-size";
public description = "Gets the size of the collection";
public argsShape = DbOperationArgs;
public override outputSchema = CollectionStorageSizeOutputSchema;

static operationType: OperationType = "metadata";

protected async execute({ database, collection }: ToolArgs<typeof DbOperationArgs>): Promise<CallToolResult> {
protected async execute({
database,
collection,
}: ToolArgs<typeof DbOperationArgs>): Promise<ToolResult<typeof this.outputSchema>> {
const provider = await this.ensureConnected();
const [{ value }] = (await provider
.aggregate(database, collection, [
Expand All @@ -27,13 +38,14 @@ export class CollectionStorageSizeTool extends MongoDBToolBase {
type: "text",
},
],
structuredContent: {
size: scaledValue,
units,
},
};
}

protected handleError(
error: unknown,
args: ToolArgs<typeof this.argsShape>
): Promise<CallToolResult> | CallToolResult {
protected handleError(error: unknown, args: ToolArgs<typeof this.argsShape>): Promise<ToolResult> | ToolResult {
if (error instanceof Error && "codeName" in error && error.codeName === "NamespaceNotFound") {
return {
content: [
Expand All @@ -46,7 +58,7 @@ export class CollectionStorageSizeTool extends MongoDBToolBase {
};
}

return super.handleError(error, args);
return super.handleError(error, args) as ToolResult | Promise<ToolResult>;
}

private static getStats(value: number): { value: number; units: string } {
Expand Down
18 changes: 15 additions & 3 deletions src/tools/mongodb/metadata/dbStats.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
import type { ToolArgs, OperationType } from "../../tool.js";
import type { ToolArgs, OperationType, ToolResult } from "../../tool.js";
import { formatUntrustedData } from "../../tool.js";
import { EJSON } from "bson";
import { z } from "zod";

const DbStatsOutputSchema = {
stats: z.record(z.unknown()),
};

export type DbStatsOutput = z.infer<z.ZodObject<typeof DbStatsOutputSchema>>;

export class DbStatsTool extends MongoDBToolBase {
public name = "db-stats";
public description = "Returns statistics that reflect the use state of a single database";
public argsShape = {
database: DbOperationArgs.database,
};
public override outputSchema = DbStatsOutputSchema;

static operationType: OperationType = "metadata";

protected async execute({ database }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
protected async execute({
database,
}: ToolArgs<typeof this.argsShape>): Promise<ToolResult<typeof this.outputSchema>> {
const provider = await this.ensureConnected();
const result = await provider.runCommandWithCheck(database, {
dbStats: 1,
Expand All @@ -22,6 +31,9 @@ export class DbStatsTool extends MongoDBToolBase {

return {
content: formatUntrustedData(`Statistics for database ${database}`, EJSON.stringify(result)),
structuredContent: {
stats: result,
},
};
}
}
19 changes: 16 additions & 3 deletions src/tools/mongodb/metadata/explain.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
import type { ToolArgs, OperationType } from "../../tool.js";
import type { ToolArgs, OperationType, ToolResult } from "../../tool.js";
import { formatUntrustedData } from "../../tool.js";
import { z } from "zod";
import type { Document } from "mongodb";
import { getAggregateArgs } from "../read/aggregate.js";
import { FindArgs } from "../read/find.js";
import { CountArgs } from "../read/count.js";

const ExplainOutputSchema = {
explainResult: z.record(z.unknown()),
method: z.string(),
verbosity: z.string(),
};

export type ExplainOutput = z.infer<z.ZodObject<typeof ExplainOutputSchema>>;

export class ExplainTool extends MongoDBToolBase {
public name = "explain";
public description =
Expand Down Expand Up @@ -47,6 +54,7 @@ export class ExplainTool extends MongoDBToolBase {
"The verbosity of the explain plan, defaults to queryPlanner. If the user wants to know how fast is a query in execution time, use executionStats. It supports all verbosities as defined in the MongoDB Driver."
),
};
public override outputSchema = ExplainOutputSchema;

static operationType: OperationType = "metadata";

Expand All @@ -55,7 +63,7 @@ export class ExplainTool extends MongoDBToolBase {
collection,
method: methods,
verbosity,
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
}: ToolArgs<typeof this.argsShape>): Promise<ToolResult<typeof this.outputSchema>> {
const provider = await this.ensureConnected();
const method = methods[0];

Expand Down Expand Up @@ -103,6 +111,11 @@ export class ExplainTool extends MongoDBToolBase {
`Here is some information about the winning plan chosen by the query optimizer for running the given \`${method.name}\` operation in "${database}.${collection}". The execution plan was run with the following verbosity: "${verbosity}". This information can be used to understand how the query was executed and to optimize the query performance.`,
JSON.stringify(result)
),
structuredContent: {
explainResult: result,
method: method.name,
verbosity,
},
};
}
}
Loading
Loading