Skip to content

Group/Tag Filtering Implementation #840

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
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
253 changes: 253 additions & 0 deletions src/client/filtering.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
import { Client } from "./index.js";
import { MockTransport } from "./mockTransport.js";
import {
Group,
Tag,
Tool,
ToolsFilter,
} from "../types.js";

describe("Client filtering capabilities", () => {
let client: Client;
let transport: MockTransport;

beforeEach(() => {
transport = new MockTransport();
client = new Client({ name: "test-client", version: "1.0.0" });
client.connect(transport);

// Mock server capabilities to include filtering
transport.mockServerCapabilities({
filtering: {
groups: {
listChanged: true
},
tags: {
listChanged: true
}
}
});

// Client is already connected and ready to use
});

describe("listGroups", () => {
test("should request groups from the server", async () => {
// Mock server response
const mockGroups: Group[] = [
{
name: "user",
title: "User Management Tools",
description: "Tools used for managing user accounts within the system."
},
{
name: "mapping",
title: "Geospatial Mapping Tools",
description: "Tools used for map rendering, geocoding, and spatial analysis."
}
];

transport.mockResponse(
{ method: "groups/list" },
{ groups: mockGroups }
);

// Call the method
const result = await client.listGroups();

// Verify the request was made correctly
expect(transport.lastRequest).toEqual({
jsonrpc: "2.0",
method: "groups/list",
params: {},
id: expect.anything()
});

// Verify the response was parsed correctly
expect(result).toEqual({ groups: mockGroups });
});

test("should throw an error if filtering capability is not available", async () => {
// Create a new client without filtering capability
const newTransport = new MockTransport();
const newClient = new Client({ name: "test-client", version: "1.0.0" });
newClient.connect(newTransport);

// Mock server capabilities without filtering
newTransport.mockServerCapabilities({});

// Expect the method to throw an error
await expect(newClient.listGroups()).rejects.toThrow(
"Server does not support method: groups/list"
);
}, 10000); // Increase timeout to 10 seconds
});

describe("listTags", () => {
test("should request tags from the server", async () => {
// Mock server response
const mockTags: Tag[] = [
{
name: "beta",
description: "Experimental or in-testing tools"
},
{
name: "stable",
description: "Production-ready tools."
}
];

transport.mockResponse(
{ method: "tags/list" },
{ tags: mockTags }
);

// Call the method
const result = await client.listTags();

// Verify the request was made correctly
expect(transport.lastRequest).toEqual({
jsonrpc: "2.0",
method: "tags/list",
params: {},
id: expect.anything()
});

// Verify the response was parsed correctly
expect(result).toEqual({ tags: mockTags });
});

test("should throw an error if filtering capability is not available", async () => {
// Create a new client without filtering capability
const newTransport = new MockTransport();
const newClient = new Client({ name: "test-client", version: "1.0.0" });
newClient.connect(newTransport);

// Mock server capabilities without filtering
newTransport.mockServerCapabilities({});

// Expect the method to throw an error
await expect(newClient.listTags()).rejects.toThrow(
"Server does not support method: tags/list"
);
}, 10000); // Increase timeout to 10 seconds
});

describe("listTools with filtering", () => {
test("should request tools with group filter", async () => {
// Mock tools
const mockTools: Tool[] = [
{
name: "user_create",
title: "Create User",
description: "Creates a new user account",
inputSchema: { type: "object", properties: {} },
groups: ["user"]
}
];

// Set up the mock response
transport.mockResponse(
{ method: "tools/list" },
{ tools: mockTools }
);

// Create filter
const filter: ToolsFilter = {
groups: ["user"]
};

// Call the method with filter
const result = await client.listTools({ filter });

// Verify the request was made correctly
expect(transport.lastRequest).toEqual({
jsonrpc: "2.0",
method: "tools/list",
params: { filter },
id: expect.anything()
});

// Verify the response was parsed correctly
expect(result).toEqual({ tools: mockTools });
});

test("should request tools with tag filter", async () => {
// Mock tools
const mockTools: Tool[] = [
{
name: "map_render",
title: "Render Map",
description: "Renders a map with the given parameters",
inputSchema: { type: "object", properties: {} },
tags: ["stable"]
}
];

// Set up the mock response
transport.mockResponse(
{ method: "tools/list" },
{ tools: mockTools }
);

// Create filter
const filter: ToolsFilter = {
tags: ["stable"]
};

// Call the method with filter
const result = await client.listTools({ filter });

// Verify the request was made correctly
expect(transport.lastRequest).toEqual({
jsonrpc: "2.0",
method: "tools/list",
params: { filter },
id: expect.anything()
});

// Verify the response was parsed correctly
expect(result).toEqual({ tools: mockTools });
});

test("should request tools with both group and tag filters", async () => {
// Mock tools
const mockTools: Tool[] = [
{
name: "user_delete",
title: "Delete User",
description: "Deletes a user account",
inputSchema: { type: "object", properties: {} },
groups: ["user"],
tags: ["destructive"]
}
];

// Set up the mock response
transport.mockResponse(
{ method: "tools/list" },
{ tools: mockTools }
);

// Create filter
const filter: ToolsFilter = {
groups: ["user"],
tags: ["destructive"]
};

// Call the method with filter
const result = await client.listTools({ filter });

// Verify the request was made correctly
expect(transport.lastRequest).toEqual({
jsonrpc: "2.0",
method: "tools/list",
params: { filter },
id: expect.anything()
});

// Verify the response was parsed correctly
expect(result).toEqual({ tools: mockTools });
});
});
});
37 changes: 36 additions & 1 deletion src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@ import {
Implementation,
InitializeResultSchema,
LATEST_PROTOCOL_VERSION,
ListGroupsRequest,
ListGroupsResultSchema,
ListPromptsRequest,
ListPromptsResultSchema,
ListResourcesRequest,
ListResourcesResultSchema,
ListResourceTemplatesRequest,
ListResourceTemplatesResultSchema,
ListTagsRequest,
ListTagsResultSchema,
ListToolsRequest,
ListToolsResultSchema,
LoggingLevel,
Expand Down Expand Up @@ -501,8 +505,13 @@ export class Client<
params?: ListToolsRequest["params"],
options?: RequestOptions,
) {
const request: ListToolsRequest = {
method: "tools/list",
params: params || {},
};

const result = await this.request(
{ method: "tools/list", params },
request,
ListToolsResultSchema,
options,
);
Expand All @@ -513,6 +522,32 @@ export class Client<
return result;
}

async listGroups(
params?: ListGroupsRequest["params"],
options?: RequestOptions,
) {
this.assertCapabilityForMethod("groups/list");
// Use type assertion with unknown as intermediate step
return this.request(
{ method: "groups/list", params: params || {} } as unknown as ClientRequest,
ListGroupsResultSchema,
options,
);
}

async listTags(
params?: ListTagsRequest["params"],
options?: RequestOptions,
) {
this.assertCapabilityForMethod("tags/list");
// Use type assertion with unknown as intermediate step
return this.request(
{ method: "tags/list", params: params || {} } as unknown as ClientRequest,
ListTagsResultSchema,
options,
);
}

async sendRootsListChanged() {
return this.notification({ method: "notifications/roots/list_changed" });
}
Expand Down
Loading