Skip to content

Commit fde6005

Browse files
committed
Testing the group/tag changes
In src/client/filtering.test.ts * Tests client-side filtering capabilities * Verifies listGroups and listTags methods work correctly * Tests filtering tools by groups, tags, and combinations * Verifies error handling when filtering capability is not available In src/server/filtering.test.ts * Tests server-side filtering capabilities * Verifies group and tag registration, updating, and removal * Tests filtering tools by groups, tags, and combinations * Verifies that tools match ANY of the specified groups * Verifies that tools match ALL of the specified tags In src/integration-tests/filtering.test.ts * End-to-end tests for filtering capabilities * Tests listing groups and tags * Tests filtering tools by groups, tags, and combinations * Verifies correct behavior with real HTTP server and transports
1 parent 1e88579 commit fde6005

File tree

4 files changed

+1162
-0
lines changed

4 files changed

+1162
-0
lines changed

src/client/filtering.test.ts

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import { Client } from "./index.js";
2+
import { MockTransport } from "./mockTransport.js";
3+
import {
4+
Group,
5+
Tag,
6+
Tool,
7+
ToolsFilter,
8+
} from "../types.js";
9+
10+
describe("Client filtering capabilities", () => {
11+
let client: Client;
12+
let transport: MockTransport;
13+
14+
beforeEach(() => {
15+
transport = new MockTransport();
16+
client = new Client({ name: "test-client", version: "1.0.0" });
17+
client.connect(transport);
18+
19+
// Mock server capabilities to include filtering
20+
transport.mockServerCapabilities({
21+
filtering: {
22+
groups: {
23+
listChanged: true
24+
},
25+
tags: {
26+
listChanged: true
27+
}
28+
}
29+
});
30+
31+
// Client is already connected and ready to use
32+
});
33+
34+
describe("listGroups", () => {
35+
test("should request groups from the server", async () => {
36+
// Mock server response
37+
const mockGroups: Group[] = [
38+
{
39+
name: "user",
40+
title: "User Management Tools",
41+
description: "Tools used for managing user accounts within the system."
42+
},
43+
{
44+
name: "mapping",
45+
title: "Geospatial Mapping Tools",
46+
description: "Tools used for map rendering, geocoding, and spatial analysis."
47+
}
48+
];
49+
50+
transport.mockResponse(
51+
{ method: "groups/list" },
52+
{ groups: mockGroups }
53+
);
54+
55+
// Call the method
56+
const result = await client.listGroups();
57+
58+
// Verify the request was made correctly
59+
expect(transport.lastRequest).toEqual({
60+
jsonrpc: "2.0",
61+
method: "groups/list",
62+
params: {},
63+
id: expect.anything()
64+
});
65+
66+
// Verify the response was parsed correctly
67+
expect(result).toEqual({ groups: mockGroups });
68+
});
69+
70+
test("should throw an error if filtering capability is not available", async () => {
71+
// Create a new client without filtering capability
72+
const newTransport = new MockTransport();
73+
const newClient = new Client({ name: "test-client", version: "1.0.0" });
74+
newClient.connect(newTransport);
75+
76+
// Mock server capabilities without filtering
77+
newTransport.mockServerCapabilities({});
78+
79+
// Expect the method to throw an error
80+
await expect(newClient.listGroups()).rejects.toThrow(
81+
"Server does not support method: groups/list"
82+
);
83+
}, 10000); // Increase timeout to 10 seconds
84+
});
85+
86+
describe("listTags", () => {
87+
test("should request tags from the server", async () => {
88+
// Mock server response
89+
const mockTags: Tag[] = [
90+
{
91+
name: "beta",
92+
description: "Experimental or in-testing tools"
93+
},
94+
{
95+
name: "stable",
96+
description: "Production-ready tools."
97+
}
98+
];
99+
100+
transport.mockResponse(
101+
{ method: "tags/list" },
102+
{ tags: mockTags }
103+
);
104+
105+
// Call the method
106+
const result = await client.listTags();
107+
108+
// Verify the request was made correctly
109+
expect(transport.lastRequest).toEqual({
110+
jsonrpc: "2.0",
111+
method: "tags/list",
112+
params: {},
113+
id: expect.anything()
114+
});
115+
116+
// Verify the response was parsed correctly
117+
expect(result).toEqual({ tags: mockTags });
118+
});
119+
120+
test("should throw an error if filtering capability is not available", async () => {
121+
// Create a new client without filtering capability
122+
const newTransport = new MockTransport();
123+
const newClient = new Client({ name: "test-client", version: "1.0.0" });
124+
newClient.connect(newTransport);
125+
126+
// Mock server capabilities without filtering
127+
newTransport.mockServerCapabilities({});
128+
129+
// Expect the method to throw an error
130+
await expect(newClient.listTags()).rejects.toThrow(
131+
"Server does not support method: tags/list"
132+
);
133+
}, 10000); // Increase timeout to 10 seconds
134+
});
135+
136+
describe("listTools with filtering", () => {
137+
test("should request tools with group filter", async () => {
138+
// Mock tools
139+
const mockTools: Tool[] = [
140+
{
141+
name: "user_create",
142+
title: "Create User",
143+
description: "Creates a new user account",
144+
inputSchema: { type: "object", properties: {} },
145+
groups: ["user"]
146+
}
147+
];
148+
149+
// Set up the mock response
150+
transport.mockResponse(
151+
{ method: "tools/list" },
152+
{ tools: mockTools }
153+
);
154+
155+
// Create filter
156+
const filter: ToolsFilter = {
157+
groups: ["user"]
158+
};
159+
160+
// Call the method with filter
161+
const result = await client.listTools({ filter });
162+
163+
// Verify the request was made correctly
164+
expect(transport.lastRequest).toEqual({
165+
jsonrpc: "2.0",
166+
method: "tools/list",
167+
params: { filter },
168+
id: expect.anything()
169+
});
170+
171+
// Verify the response was parsed correctly
172+
expect(result).toEqual({ tools: mockTools });
173+
});
174+
175+
test("should request tools with tag filter", async () => {
176+
// Mock tools
177+
const mockTools: Tool[] = [
178+
{
179+
name: "map_render",
180+
title: "Render Map",
181+
description: "Renders a map with the given parameters",
182+
inputSchema: { type: "object", properties: {} },
183+
tags: ["stable"]
184+
}
185+
];
186+
187+
// Set up the mock response
188+
transport.mockResponse(
189+
{ method: "tools/list" },
190+
{ tools: mockTools }
191+
);
192+
193+
// Create filter
194+
const filter: ToolsFilter = {
195+
tags: ["stable"]
196+
};
197+
198+
// Call the method with filter
199+
const result = await client.listTools({ filter });
200+
201+
// Verify the request was made correctly
202+
expect(transport.lastRequest).toEqual({
203+
jsonrpc: "2.0",
204+
method: "tools/list",
205+
params: { filter },
206+
id: expect.anything()
207+
});
208+
209+
// Verify the response was parsed correctly
210+
expect(result).toEqual({ tools: mockTools });
211+
});
212+
213+
test("should request tools with both group and tag filters", async () => {
214+
// Mock tools
215+
const mockTools: Tool[] = [
216+
{
217+
name: "user_delete",
218+
title: "Delete User",
219+
description: "Deletes a user account",
220+
inputSchema: { type: "object", properties: {} },
221+
groups: ["user"],
222+
tags: ["destructive"]
223+
}
224+
];
225+
226+
// Set up the mock response
227+
transport.mockResponse(
228+
{ method: "tools/list" },
229+
{ tools: mockTools }
230+
);
231+
232+
// Create filter
233+
const filter: ToolsFilter = {
234+
groups: ["user"],
235+
tags: ["destructive"]
236+
};
237+
238+
// Call the method with filter
239+
const result = await client.listTools({ filter });
240+
241+
// Verify the request was made correctly
242+
expect(transport.lastRequest).toEqual({
243+
jsonrpc: "2.0",
244+
method: "tools/list",
245+
params: { filter },
246+
id: expect.anything()
247+
});
248+
249+
// Verify the response was parsed correctly
250+
expect(result).toEqual({ tools: mockTools });
251+
});
252+
});
253+
});

src/client/mockTransport.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { Transport } from "../shared/transport.js";
2+
import { JSONRPCMessage, RequestId, ServerCapabilities } from "../types.js";
3+
import { AuthInfo } from "../server/auth/types.js";
4+
5+
/**
6+
* Mock transport for testing client functionality.
7+
* This implements the Transport interface and adds methods for mocking server responses.
8+
*/
9+
export class MockTransport implements Transport {
10+
onclose?: () => void;
11+
onerror?: (error: Error) => void;
12+
onmessage?: (message: JSONRPCMessage, extra?: { authInfo?: AuthInfo }) => void;
13+
sessionId?: string;
14+
15+
private _serverCapabilities: ServerCapabilities = {};
16+
lastRequest?: JSONRPCMessage;
17+
18+
/**
19+
* Mocks the server capabilities that would be returned during initialization.
20+
*/
21+
mockServerCapabilities(capabilities: ServerCapabilities): void {
22+
this._serverCapabilities = capabilities;
23+
}
24+
25+
/**
26+
* Mocks a response from the server for a specific request.
27+
* @param request Object containing method and optional params to match
28+
* @param response The response to return when the request matches
29+
*/
30+
mockResponse(
31+
request: { method: string; params?: Record<string, unknown> },
32+
response: Record<string, unknown>
33+
): void {
34+
const key = this.getRequestKey(request);
35+
this._responseMap.set(key, response);
36+
}
37+
38+
private getRequestKey(request: { method: string; params?: Record<string, unknown> }): string {
39+
if (!request.params) {
40+
return request.method;
41+
}
42+
// Create a unique key based on method and params
43+
return `${request.method}:${JSON.stringify(request.params)}`;
44+
}
45+
46+
private _responseMap = new Map<string, Record<string, unknown>>();
47+
48+
async start(): Promise<void> {
49+
// No-op for mock
50+
}
51+
52+
async close(): Promise<void> {
53+
this.onclose?.();
54+
}
55+
56+
async send(message: JSONRPCMessage, _options?: { relatedRequestId?: RequestId, authInfo?: AuthInfo }): Promise<void> {
57+
// Store the last request for assertions
58+
this.lastRequest = message;
59+
60+
// Check if this is a request message (has method and id)
61+
if ('method' in message && 'id' in message) {
62+
// Handle initialize request specially
63+
if (message.method === "initialize") {
64+
this.onmessage?.({
65+
jsonrpc: "2.0",
66+
id: message.id,
67+
result: {
68+
protocolVersion: "2024-10-07",
69+
capabilities: this._serverCapabilities,
70+
serverInfo: {
71+
name: "mock-server",
72+
version: "1.0.0",
73+
},
74+
},
75+
});
76+
return;
77+
}
78+
79+
// Check if the method requires a capability that's not available
80+
if ((message.method === "groups/list" || message.method === "tags/list") &&
81+
(!this._serverCapabilities.filtering)) {
82+
// Return an error for unsupported method
83+
this.onmessage?.({
84+
jsonrpc: "2.0",
85+
id: message.id,
86+
error: {
87+
code: -32601,
88+
message: `Server does not support method: ${message.method}`
89+
}
90+
});
91+
return;
92+
}
93+
94+
// For other requests, check if we have a mocked response
95+
// First try to match with params
96+
const requestWithParams = {
97+
method: message.method,
98+
params: 'params' in message ? message.params as Record<string, unknown> : undefined
99+
};
100+
const key = this.getRequestKey(requestWithParams);
101+
102+
if (this._responseMap.has(key)) {
103+
const response = this._responseMap.get(key);
104+
this.onmessage?.({
105+
jsonrpc: "2.0",
106+
id: message.id,
107+
result: response || {},
108+
});
109+
}
110+
// Fall back to method-only match if no match with params
111+
else if (this._responseMap.has(message.method)) {
112+
const response = this._responseMap.get(message.method);
113+
this.onmessage?.({
114+
jsonrpc: "2.0",
115+
id: message.id,
116+
result: response || {},
117+
});
118+
}
119+
}
120+
}
121+
}

0 commit comments

Comments
 (0)