8000 Add PlaygroundClient client. Add tests for RunPipeline tool. · mongodb-js/mongodb-mcp-server@e74e7d2 · GitHub
[go: up one dir, main page]

Skip to content

Commit e74e7d2

Browse files
committed
Add PlaygroundClient client. Add tests for RunPipeline tool.
1 parent 298adf4 commit e74e7d2

File tree

3 files changed

+151
-88
lines changed

3 files changed

+151
-88
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
const PLAYGROUND_SEARCH_URL = "https://search-playground.mongodb.com/api/tools/code-playground/search";
2+
3+
/**
4+
* Payload for the Playground endpoint.
5+
*/
6+
export interface PlaygroundRunRequest {
7+
documents: string;
8+
aggregationPipeline: string;
9+
indexDefinition: string;
10+
synonyms: string;
11+
}
12+
13+
/**
14+
* Successful response from Playground server.
15+
*/
16+
export interface PlaygroundRunResponse {
17+
documents: Array<Record<string, unknown>>;
18+
}
19+
20+
/**
21+
* Error response from Playground server.
22+
*/
23+
interface PlaygroundRunErrorResponse {
24+
code: string;
25+
message: string;
26+
}
27+
28+
/**
29+
* MCP specific Playground error public for tools.
30+
*/
31+
export class PlaygroundRunError extends Error implements PlaygroundRunErrorResponse {
32+
constructor(
33+
public message: string,
34+
public code: string
35+
) {
36+
super(message);
37+
}
38+
}
39+
40+
export enum RunErrorCode {
41+
NETWORK_ERROR = "NETWORK_ERROR",
42+
UNKNOWN = "UNKNOWN",
43+
}
44+
45+
/**
46+
* Handles Search Playground requests, abstracting low-level details from MCP tools.
47+
* https://search-playground.mongodb.com
48+
*/
49+
export class PlaygroundClient {
50+
async run(request: PlaygroundRunRequest): Promise<PlaygroundRunResponse> {
51+
const options: RequestInit = {
52+
method: "POST",
53+
headers: {
54+
"Content-Type": "application/json",
55+
},
56+
body: JSON.stringify(request),
57+
};
58+
59+
let response: Response;
60+
try {
61+
response = await fetch(PLAYGROUND_SEARCH_URL, options);
62+
} catch {
63+
throw new PlaygroundRunError("Cannot run pipeline.", RunErrorCode.NETWORK_ERROR);
64+
}
65+
66+
if (!response.ok) {
67+
const runErrorResponse = await this.getRunErrorResponse(response);
68+
throw new PlaygroundRunError(runErrorResponse.message, runErrorResponse.code);
69+
}
70+
71+
try {
72+
return (await response.json()) as PlaygroundRunResponse;
73+
} catch {
74+
throw new PlaygroundRunError("Response is not valid JSON.", RunErrorCode.UNKNOWN);
75+
}
76+
}
77+
78+
private async getRunErrorResponse(response: Response): Promise<PlaygroundRunErrorResponse> {
79+
try {
80+
return (await response.json()) as PlaygroundRunErrorResponse;
81+
} catch {
82+
return {
83+
message: `HTTP ${response.status} ${response.statusText}.`,
84+
code: RunErrorCode.UNKNOWN,
85+
};
86+
}
87+
}
88+
}

src/tools/playground/runPipeline.ts

Lines changed: 20 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,28 @@ import { OperationType, TelemetryToolMetadata, ToolArgs, ToolBase, ToolCategory
22
import { z } from "zod";
33
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
44
import { EJSON } from "bson";
5-
6-
const PLAYGROUND_SEARCH_URL = "https://search-playground.mongodb.com/api/tools/code-playground/search";
7-
8-
const DEFAULT_DOCUMENTS = [
9-
{
10-
name: "First document",
11-
},
12-
{
13-
name: "Second document",
14-
},
15-
];
5+
import {
6+
PlaygroundRunError,
7+
PlaygroundRunRequest,
8+
PlaygroundRunResponse,
9+
} from "../../common/playground/playgroundClient.js";
1610

1711
const DEFAULT_SEARCH_INDEX_DEFINITION = {
1812
mappings: {
1913
dynamic: true,
2014
},
2115
};
2216

23-
const DEFAULT_PIPELINE = [
24-
{
25-
$search: {
26-
index: "default",
27-
text: {
28-
query: "first",
29-
path: {
30-
wildcard: "*",
31-
},
32-
},
33-
},
34-
},
35-
];
36-
3717
const DEFAULT_SYNONYMS: Array<Record<string, unknown>> = [];
3818

3919
export const RunPipelineOperationArgs = {
4020
documents: z
4121
.array(z.record(z.string(), z.unknown()))
4222
.max(500)
43-
.describe("Documents to run the pipeline against. 500 is maxim 10000 um.")
44-
.default(DEFAULT_DOCUMENTS),
23+
.describe("Documents to run the pipeline against. 500 is maximum."),
4524
aggregationPipeline: z
4625
.array(z.record(z.string(), z.unknown()))
47-
.describe("MongoDB aggregation pipeline to run on the provided documents.")
48-
.default(DEFAULT_PIPELINE),
26+
.describe("MongoDB aggregation pipeline to run on the provided documents."),
4927
searchIndexDefinition: z
5028
.record(z.string(), z.unknown())
5129
.describe("MongoDB search index definition to create before running the pipeline.")
@@ -58,22 +36,6 @@ export const RunPipelineOperationArgs = {
5836
.default(DEFAULT_SYNONYMS),
5937
};
6038

61-
interface RunRequest {
62-
documents: string;
63-
aggregationPipeline: string;
64-
indexDefinition: string;
65-
synonyms: string;
66-
}
67-
68-
interface RunResponse {
69-
documents: Array<Record<string, unknown>>;
70-
}
71-
72-
interface RunErrorResponse {
73-
code: string;
74-
message: string;
75-
}
76-
7739
export class RunPipeline extends ToolBase {
7840
protected name = "run-pipeline";
7941
protected description =
@@ -93,47 +55,24 @@ export class RunPipeline extends ToolBase {
9355
return {};
9456
}
9557

96-
private async runPipeline(runRequest: RunRequest): Promise<RunResponse> {
97-
const options: RequestInit = {
98-
method: "POST",
99-
headers: {
100-
"Content-Type": "application/json",
101-
},
102-
body: JSON.stringify(runRequest),
103-
};
104-
105-
let response: Response;
58+
private async runPipeline(runRequest: PlaygroundRunRequest): Promise<PlaygroundRunResponse> {
59+
// import PlaygroundClient dynamically so we can mock it properly in the tests
60+
const { PlaygroundClient } = await import("../../common/playground/playgroundClient.js");
61+
const client = new PlaygroundClient();
10662
try {
107-
response = await fetch(PLAYGROUND_SEARCH_URL, options);
108-
} catch {
109-
throw new Error("Cannot run pipeline: network error.");
110-
}
63+
return await client.run(runRequest);
64+
} catch (error: unknown) {
65+
let message: string | undefined;
11166

112-
if (!response.ok) {
113-
const errorMessage = await this.getPlaygroundResponseError(response);
114-
throw new Error(`Pipeline run failed: ${errorMessage}`);
115-
}
67+
if (error instanceof PlaygroundRunError) {
68+
message = `Error code: ${error.code}. Error message: ${error.message}.`;
69+
}
11670

117-
try {
118-
return (await response.json()) as RunResponse;
119-
} catch {
120-
throw new Error("Pipeline run failed: response is not valid JSON.");
71+
throw new Error(message || "Cannot run pipeline.");
12172
}
12273
}
12374

124-
private async getPlaygroundResponseError(response: Response): Promise<string> {
125-
let errorMessage = `HTTP ${response.status} ${response.statusText}.`;
126-
try {
127-
const errorResponse = (await response.json()) as RunErrorResponse;
128-
errorMessage += ` Error code: ${errorResponse.code}. Error message: ${errorResponse.message}`;
129-
} catch {
130-
// Ignore JSON parse errors
131-
}
132-
133-
return errorMessage;
134-
}
135-
136-
private convertToRunRequest(toolArgs: ToolArgs<typeof this.argsShape>): RunRequest {
75+
private convertToRunRequest(toolArgs: ToolArgs<typeof this.argsShape>): PlaygroundRunRequest {
13776
try {
13877
return {
13978
documents: JSON.stringify(toolArgs.documents),
@@ -146,7 +85,7 @@ export class RunPipeline extends ToolBase {
14685
}
14786
}
14887

149-
private convertToToolResult(runResponse: RunResponse): CallToolResult {
88+
private convertToToolResult(runResponse: PlaygroundRunResponse): CallToolResult {
15089
const content: Array<{ text: string; type: "text" }> = [
15190
{
15291
text: `Found ${runResponse.documents.length} documents":`,

tests/integration/tools/playground/runPipeline.test.ts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
1+
import { jest } from "@jest/globals";
12
import { describeWithMongoDB } from "../mongodb/mongodbHelpers.js";
23
import { getResponseElements } from "../../helpers.js";
4+
import { PlaygroundRunError } from "../../../../src/common/playground/playgroundClient.js";
5+
6+
const setupMockPlaygroundClient = (implementation: unknown) => {
7+
// mock ESM modules https://jestjs.io/docs/ecmascript-modules#module-mocking-in-esm
8+
jest.unstable_mockModule("../../../../src/common/playground/playgroundClient.js", () => ({
9+
PlaygroundClient: implementation,
10+
}));
11+
};
312

413
describeWithMongoDB("runPipeline tool", (integration) => {
14+
beforeEach(() => {
15+
jest.resetModules();
16+
});
17+
518
it("should return results", async () => {
6-
await integration.connectMcpClient();
19+
class PlaygroundClientMock {
20+
run = () => ({
21+
documents: [{ name: "First document" }],
22+
});
23+
}
24+
setupMockPlaygroundClient(PlaygroundClientMock);
25+
726
const response = await integration.mcpClient().callTool({
827
name: "run-pipeline",
928
arguments: {
@@ -20,12 +39,6 @@ describeWithMongoDB("runPipeline tool", (integration) => {
2039
},
2140
},
2241
},
23-
{
24-
$project: {
25-
_id: 0,
26-
name: 1,
27-
},
28-
},
2942
],
3043
},
3144
});
@@ -41,4 +54,27 @@ describeWithMongoDB("runPipeline tool", (integration) => {
4154
},
4255
]);
4356
});
57+
58+
it("should return error", async () => {
59+
class PlaygroundClientMock {
60+
run = () => {
61+
throw new PlaygroundRunError("Test error message", "TEST_CODE");
62+
};
63+
}
64+
setupMockPlaygroundClient(PlaygroundClientMock);
65+
66+
const response = await integration.mcpClient().callTool({
67+
name: "run-pipeline",
68+
arguments: {
69+
documents: [],
70+
aggregationPipeline: [],
71+
},
72+
});
73+
expect(response.content).toEqual([
74+
{
75+
type: "text",
76+
text: "Error running run-pipeline: Error code: TEST_CODE. Error message: Test error message.",
77+
},
78+
]);
79+
});
4480
});

0 commit comments

Comments
 (0)
0