Skip to content

Commit fd0e4b6

Browse files
chore: add telemetry properties for vector search and mcp auto-embed usage (#796)
1 parent 0e1acff commit fd0e4b6

File tree

6 files changed

+201
-6
lines changed

6 files changed

+201
-6
lines changed

src/server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,8 @@ export class Server {
236236
event.properties.read_only_mode = this.userConfig.readOnly ? "true" : "false";
237237
event.properties.disabled_tools = this.userConfig.disabledTools || [];
238238
event.properties.confirmation_required_tools = this.userConfig.confirmationRequiredTools || [];
239+
event.properties.previewFeatures = this.userConfig.previewFeatures;
240+
event.properties.embeddingProviderConfigured = !!this.userConfig.voyageApiKey;
239241
}
240242
if (command === "stop") {
241243
event.properties.runtime_duration_ms = Date.now() - this.startTime;

src/telemetry/types.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export type ServerEventProperties = {
4545
read_only_mode?: TelemetryBoolSet;
4646
disabled_tools?: string[];
4747
confirmation_required_tools?: string[];
48+
previewFeatures?: string[];
49+
embeddingProviderConfigured?: boolean;
4850
};
4951

5052
export type ServerEvent = TelemetryEvent<ServerEventProperties>;
@@ -141,22 +143,35 @@ export type CommonProperties = {
141143
* For MongoDB tools, this is typically empty, while for Atlas tools, this should include
142144
* the project and organization IDs if available.
143145
*/
144-
export type TelemetryToolMetadata = AtlasMetadata | PerfAdvisorToolMetadata | ConnectionMetadata;
146+
export type TelemetryToolMetadata =
147+
| AtlasMetadata
148+
| ConnectionMetadata
149+
| PerfAdvisorToolMetadata
150+
| AutoEmbeddingsUsageMetadata;
145151

146152
export type AtlasMetadata = {
147153
project_id?: string;
148154
org_id?: string;
149155
};
150156

151-
export type PerfAdvisorToolMetadata = AtlasMetadata & {
152-
operations: string[];
157+
type AtlasLocalToolMetadata = {
158+
atlas_local_deployment_id?: string;
153159
};
154160

155161
export type ConnectionMetadata = AtlasMetadata &
156162
AtlasLocalToolMetadata & {
157163
connection_auth_type?: string;
158164
};
159165

160-
type AtlasLocalToolMetadata = {
161-
atlas_local_deployment_id?: string;
166+
export type PerfAdvisorToolMetadata = AtlasMetadata & {
167+
operations: string[];
168+
};
169+
170+
export type AutoEmbeddingsUsageMetadata = ConnectionMetadata & {
171+
/**
172+
* Indicates which component generated the embeddings.
173+
* "mcp" is used when embeddings are generated by the MCP server.
174+
* "mongot" is reserved for future use, when embeddings may be generated by MongoDB's mongot process.
175+
*/
176+
embeddingsGeneratedBy: "mcp" | "mongot";
162177
};

src/tools/mongodb/create/insertMany.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { zEJSON } from "../../args.js";
66
import { type Document } from "bson";
77
import { zSupportedEmbeddingParameters } from "../mongodbSchemas.js";
88
import { ErrorCodes, MongoDBError } from "../../../common/errors.js";
9+
import type { ConnectionMetadata, AutoEmbeddingsUsageMetadata } from "../../../telemetry/types.js";
910

1011
const zSupportedEmbeddingParametersWithInput = zSupportedEmbeddingParameters.extend({
1112
input: z
@@ -155,4 +156,18 @@ export class InsertManyTool extends MongoDBToolBase {
155156
}
156157
}
157158
}
159+
160+
protected resolveTelemetryMetadata(
161+
args: ToolArgs<typeof this.argsShape>,
162+
{ result }: { result: CallToolResult }
163+
): ConnectionMetadata | AutoEmbeddingsUsageMetadata {
164+
if ("embeddingParameters" in args && this.config.voyageApiKey) {
165+
return {
166+
...super.resolveTelemetryMetadata(args, { result }),
167+
embeddingsGeneratedBy: "mcp",
168+
};
169+
} else {
170+
return super.resolveTelemetryMetadata(args, { result });
171+
}
172+
}
158173
}

src/tools/mongodb/read/aggregate.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
assertVectorSearchFilterFieldsAreIndexed,
1818
type SearchIndex,
1919
} from "../../../helpers/assertVectorSearchFilterFieldsAreIndexed.js";
20+
import type { AutoEmbeddingsUsageMetadata, ConnectionMetadata } from "../../../telemetry/types.js";
2021

2122
const pipelineDescriptionWithVectorSearch = `\
2223
An array of aggregation stages to execute.
@@ -344,4 +345,25 @@ The aggregation resulted in ${aggResultsCount === undefined ? "indeterminable nu
344345
Returning ${documents.length} documents${appliedLimitText ? ` ${appliedLimitText}` : "."}\
345346
`;
346347
}
348+
349+
protected resolveTelemetryMetadata(
350+
args: ToolArgs<typeof this.argsShape>,
351+
{ result }: { result: CallToolResult }
352+
): ConnectionMetadata | AutoEmbeddingsUsageMetadata {
353+
const [maybeVectorStage] = args.pipeline;
354+
if (
355+
maybeVectorStage !== null &&
356+
maybeVectorStage instanceof Object &&
357+
"$vectorSearch" in maybeVectorStage &&
358+
"embeddingParameters" in maybeVectorStage["$vectorSearch"] &&
359+
this.config.voyageApiKey
360+
) {
361+
return {
362+
...super.resolveTelemetryMetadata(args, { result }),
363+
embeddingsGeneratedBy: "mcp",
364+
};
365+
} else {
366+
return super.resolveTelemetryMetadata(args, { result });
367+
}
368+
}
347369
}

tests/integration/tools/mongodb/create/insertMany.test.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ import {
1414
getDataFromUntrustedContent,
1515
defaultTestConfig,
1616
} from "../../../helpers.js";
17-
import { beforeEach, afterEach, expect, it, describe } from "vitest";
17+
import { beforeEach, afterEach, expect, it, describe, vi } from "vitest";
1818
import { ObjectId } from "bson";
1919
import type { Collection } from "mongodb";
20+
import type { ToolEvent } from "../../../../../src/telemetry/types.js";
2021

2122
describeWithMongoDB("insertMany tool when search is disabled", (integration) => {
2223
validateToolMetadata(
@@ -101,6 +102,29 @@ describeWithMongoDB("insertMany tool when search is disabled", (integration) =>
101102
expect(content).toContain(insertedIds[0]?.toString());
102103
});
103104

105+
it("should emit tool event without auto-embedding usage metadata", async () => {
106+
const mockEmitEvents = vi.spyOn(integration.mcpServer()["telemetry"], "emitEvents");
107+
vi.spyOn(integration.mcpServer()["telemetry"], "isTelemetryEnabled").mockReturnValue(true);
108+
await integration.connectMcpClient();
109+
110+
const response = await integration.mcpClient().callTool({
111+
name: "insert-many",
112+
arguments: {
113+
database: integration.randomDbName(),
114+
collection: "test",
115+
documents: [{ title: "The Matrix" }],
116+
},
117+
});
118+
119+
const content = getResponseContent(response.content);
120+
expect(content).toContain("Documents were inserted successfully.");
121+
122+
expect(mockEmitEvents).toHaveBeenCalled();
123+
const emittedEvent = mockEmitEvents.mock.lastCall?.[0][0] as ToolEvent;
124+
expectDefined(emittedEvent);
125+
expect(emittedEvent.properties.embeddingsGeneratedBy).toBeUndefined();
126+
});
127+
104128
validateAutoConnectBehavior(integration, "insert-many", () => {
105129
return {
106130
args: {
@@ -237,6 +261,7 @@ describeWithMongoDB(
237261

238262
afterEach(async () => {
239263
await collection.drop();
264+
vi.clearAllMocks();
240265
});
241266

242267
it("generates embeddings for a single document with one field", async () => {
@@ -627,6 +652,42 @@ describeWithMongoDB(
627652
// Verify embeddings are different for different text
628653
expect(doc?.titleEmbeddings).not.toEqual(doc?.plotEmbeddings);
629654
});
655+
656+
it("should emit tool event with auto-embedding usage metadata", async () => {
657+
const mockEmitEvents = vi.spyOn(integration.mcpServer()["telemetry"], "emitEvents");
658+
vi.spyOn(integration.mcpServer()["telemetry"], "isTelemetryEnabled").mockReturnValue(true);
659+
660+
await createVectorSearchIndexAndWait(integration.mongoClient(), database, "test", [
661+
{
662+
type: "vector",
663+
path: "titleEmbeddings",
664+
numDimensions: 1024,
665+
similarity: "cosine",
666+
quantization: "scalar",
667+
},
668+
]);
669+
670+
const response = await integration.mcpClient().callTool({
671+
name: "insert-many",
672+
arguments: {
673+
database,
674+
collection: "test",
675+
documents: [{ title: "The Matrix" }],
676+
embeddingParameters: {
677+
model: "voyage-3.5-lite",
678+
input: [{ titleEmbeddings: "The Matrix" }],
679+
},
680+
},
681+
});
682+
683+
const content = getResponseContent(response.content);
684+
expect(content).toContain("Documents were inserted successfully.");
685+
686+
expect(mockEmitEvents).toHaveBeenCalled();
687+
const emittedEvent = mockEmitEvents.mock.lastCall?.[0][0] as ToolEvent;
688+
expectDefined(emittedEvent);
689+
expect(emittedEvent.properties.embeddingsGeneratedBy).toBe("mcp");
690+
});
630691
});
631692
},
632693
{

tests/integration/tools/mongodb/read/aggregate.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
validateThrowsForInvalidArguments,
55
getResponseContent,
66
defaultTestConfig,
7+
expectDefined,
78
} from "../../../helpers.js";
89
import { beforeEach, describe, expect, it, vi, afterEach } from "vitest";
910
import {
@@ -17,6 +18,7 @@ import * as constants from "../../../../../src/helpers/constants.js";
1718
import { freshInsertDocuments } from "./find.test.js";
1819
import { BSON } from "bson";
1920
import { DOCUMENT_EMBEDDINGS } from "./vyai/embeddings.js";
21+
import type { ToolEvent } from "../../../../../src/telemetry/types.js";
2022

2123
describeWithMongoDB("aggregate tool", (integration) => {
2224
afterEach(() => {
@@ -151,6 +153,36 @@ describeWithMongoDB("aggregate tool", (integration) => {
151153
);
152154
});
153155

156+
it("should emit tool event without auto-embedding usage metadata", async () => {
157+
const mockEmitEvents = vi.spyOn(integration.mcpServer()["telemetry"], "emitEvents");
158+
vi.spyOn(integration.mcpServer()["telemetry"], "isTelemetryEnabled").mockReturnValue(true);
159+
160+
const mongoClient = integration.mongoClient();
161+
await mongoClient
162+
.db(integration.randomDbName())
163+
.collection("people")
164+
.insertMany([
165+
{ name: "Peter", age: 5 },
166+
{ name: "Laura", age: 10 },
167+
{ name: "Søren", age: 15 },
168+
]);
169+
170+
await integration.connectMcpClient();
171+
await integration.mcpClient().callTool({
172+
name: "aggregate",
173+
arguments: {
174+
database: integration.randomDbName(),
175+
collection: "people",
176+
pipeline: [{ $match: { age: { $gt: 8 } } }, { $sort: { name: -1 } }],
177+
},
178+
});
179+
180+
expect(mockEmitEvents).toHaveBeenCalled();
181+
const emittedEvent = mockEmitEvents.mock.lastCall?.[0][0] as ToolEvent;
182+
expectDefined(emittedEvent);
183+
expect(emittedEvent.properties.embeddingsGeneratedBy).toBeUndefined();
184+
});
185+
154186
for (const disabledOpType of ["create", "update", "delete"] as const) {
155187
it(`can not run $out stages when ${disabledOpType} operation is disabled`, async () => {
156188
await integration.connectMcpClient();
@@ -393,6 +425,10 @@ describeWithMongoDB(
393425
await integration.mongoClient().db(integration.randomDbName()).collection("databases").drop();
394426
});
395427

428+
afterEach(() => {
429+
vi.clearAllMocks();
430+
});
431+
396432
validateToolMetadata(integration, "aggregate", "Run an aggregation against a MongoDB collection", "read", [
397433
...databaseCollectionParameters,
398434
{
@@ -462,6 +498,50 @@ If the user requests additional filtering, include filters in \`$vectorSearch.fi
462498
);
463499
});
464500

501+
it("should emit tool event with auto-embedding usage metadata", async () => {
502+
const mockEmitEvents = vi.spyOn(integration.mcpServer()["telemetry"], "emitEvents");
503+
vi.spyOn(integration.mcpServer()["telemetry"], "isTelemetryEnabled").mockReturnValue(true);
504+
505+
await waitUntilSearchIsReady(integration.mongoClient());
506+
await createVectorSearchIndexAndWait(integration.mongoClient(), integration.randomDbName(), "databases", [
507+
{
508+
type: "vector",
509+
path: "description_embedding",
510+
numDimensions: 256,
511+
similarity: "cosine",
512+
quantization: "none",
513+
},
514+
]);
515+
516+
await integration.mcpClient().callTool({
517+
name: "aggregate",
518+
arguments: {
519+
database: integration.randomDbName(),
520+
collection: "databases",
521+
pipeline: [
522+
{
523+
$vectorSearch: {
524+
index: "default",
525+
path: "description_embedding",
526+
queryVector: "some data",
527+
numCandidates: 10,
528+
limit: 10,
529+
embeddingParameters: {
530+
model: "voyage-3-large",
531+
outputDimension: "256",
532+
},
533+
},
534+
},
535+
],
536+
},
537+
});
538+
539+
expect(mockEmitEvents).toHaveBeenCalled();
540+
const emittedEvent = mockEmitEvents.mock.lastCall?.[0][0] as ToolEvent;
541+
expectDefined(emittedEvent);
542+
expect(emittedEvent.properties.embeddingsGeneratedBy).toBe("mcp");
543+
});
544+
465545
for (const [dataType, embedding] of Object.entries(DOCUMENT_EMBEDDINGS)) {
466546
for (const similarity of ["euclidean", "cosine", "dotProduct"]) {
467547
describe(`querying with dataType ${dataType} and similarity ${similarity}`, () => {

0 commit comments

Comments
 (0)