Skip to content

Commit 119dd1e

Browse files
author
Lasim
committed
feat(backend): add sorting functionality for mcp server search results
1 parent 5206466 commit 119dd1e

File tree

6 files changed

+118
-11
lines changed

6 files changed

+118
-11
lines changed

services/backend/api-spec.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14337,6 +14337,19 @@
1433714337
"required": false,
1433814338
"description": "Filter by auto-install flag"
1433914339
},
14340+
{
14341+
"schema": {
14342+
"type": "string",
14343+
"enum": [
14344+
"name",
14345+
"github_stars"
14346+
]
14347+
},
14348+
"in": "query",
14349+
"name": "sort_by",
14350+
"required": false,
14351+
"description": "Sort results by name (default) or GitHub stars (descending, nulls last)"
14352+
},
1434014353
{
1434114354
"schema": {
1434214355
"type": "string",

services/backend/api-spec.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10043,6 +10043,16 @@ paths:
1004310043
name: auto_install_new_default_team
1004410044
required: false
1004510045
description: Filter by auto-install flag
10046+
- schema:
10047+
type: string
10048+
enum:
10049+
- name
10050+
- github_stars
10051+
in: query
10052+
name: sort_by
10053+
required: false
10054+
description: Sort results by name (default) or GitHub stars (descending, nulls
10055+
last)
1004610056
- schema:
1004710057
type: string
1004810058
pattern: ^\d+$

services/backend/src/routes/mcp/servers/schemas.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ export const SEARCH_SERVERS_QUERY_SCHEMA = {
109109
type: 'boolean',
110110
description: 'Filter by auto-install flag'
111111
},
112+
sort_by: {
113+
type: 'string',
114+
enum: ['name', 'github_stars'],
115+
description: 'Sort results by name (default) or GitHub stars (descending, nulls last)'
116+
},
112117
limit: {
113118
type: 'string',
114119
pattern: '^\\d+$',

services/backend/src/routes/mcp/servers/search.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
// TypeScript interface for search query params
1717
interface SearchServersQueryParams extends Omit<ListServersQueryParams, 'search'> {
1818
q: string; // Search query is required for search endpoint
19+
sort_by?: 'name' | 'github_stars'; // Optional sort parameter
1920
}
2021

2122
export default async function searchServers(server: FastifyInstance) {
@@ -62,6 +63,7 @@ export default async function searchServers(server: FastifyInstance) {
6263
status: queryParams.status,
6364
featured: queryParams.featured
6465
},
66+
sortBy: queryParams.sort_by,
6567
pagination: {
6668
limit: queryParams.limit,
6769
offset: queryParams.offset
@@ -96,6 +98,7 @@ export default async function searchServers(server: FastifyInstance) {
9698
const offset = parseInt(queryParams.offset || '0') || 0;
9799
const featured = queryParams.featured === 'true' ? true : queryParams.featured === 'false' ? false : undefined;
98100
const status = queryParams.status as 'active' | 'deprecated' | 'maintenance' | undefined;
101+
const sortBy = (queryParams.sort_by as 'name' | 'github_stars') || 'name';
99102

100103
// Build filters object - use 'search' internally (service expects 'search', not 'q')
101104
const filters = {
@@ -112,7 +115,8 @@ export default async function searchServers(server: FastifyInstance) {
112115
request.user!.id,
113116
userRole,
114117
teamIds,
115-
filters
118+
filters,
119+
sortBy
116120
);
117121

118122
// Apply pagination
@@ -123,6 +127,7 @@ export default async function searchServers(server: FastifyInstance) {
123127
operation: 'search_mcp_servers',
124128
userId: request.user!.id,
125129
query: queryParams.q,
130+
sortBy: sortBy,
126131
totalResults: total,
127132
returnedResults: paginatedServers.length,
128133
userRole,

services/backend/src/services/mcpCatalogService.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,8 @@ export class McpCatalogService {
223223
userId: string,
224224
userRole: string,
225225
teamIds: string[],
226-
filters?: McpServerFilters
226+
filters?: McpServerFilters,
227+
sortBy: 'name' | 'github_stars' = 'name'
227228
): Promise<McpServer[]> {
228229
this.logger.debug({
229230
operation: 'get_servers_for_user',
@@ -288,8 +289,14 @@ export class McpCatalogService {
288289
query = query.where(and(...whereConditions));
289290
}
290291

291-
// Order by featured first, then by name
292-
query = query.orderBy(desc(mcpServers.featured), asc(mcpServers.name));
292+
// Apply sorting based on sortBy parameter
293+
if (sortBy === 'github_stars') {
294+
// Sort by GitHub stars (descending, nulls last), then by name
295+
query = query.orderBy(desc(mcpServers.github_stars), asc(mcpServers.name));
296+
} else {
297+
// Default: Sort by featured first, then by name
298+
query = query.orderBy(desc(mcpServers.featured), asc(mcpServers.name));
299+
}
293300

294301
const servers = await query;
295302

services/backend/tests/unit/routes/mcp/servers/search.test.ts

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,8 @@ describe('MCP Servers - Search Servers', () => {
219219
[],
220220
expect.objectContaining({
221221
search: 'test'
222-
})
222+
}),
223+
'name'
223224
);
224225

225226
expect(mockReply.status).toHaveBeenCalledWith(200);
@@ -276,7 +277,8 @@ describe('MCP Servers - Search Servers', () => {
276277
runtime: 'node',
277278
status: 'active',
278279
featured: true
279-
}
280+
},
281+
'name'
280282
);
281283

282284
expect(mockReply.status).toHaveBeenCalledWith(200);
@@ -386,7 +388,8 @@ describe('MCP Servers - Search Servers', () => {
386388
'test-user-id',
387389
'global_admin',
388390
[],
389-
expect.any(Object)
391+
expect.any(Object),
392+
'name'
390393
);
391394
});
392395

@@ -421,7 +424,8 @@ describe('MCP Servers - Search Servers', () => {
421424
'test-user-id',
422425
'global_user',
423426
['team-1', 'team-2'],
424-
expect.any(Object)
427+
expect.any(Object),
428+
'name'
425429
);
426430
});
427431

@@ -445,7 +449,8 @@ describe('MCP Servers - Search Servers', () => {
445449
'test-user-id',
446450
'global_user',
447451
[],
448-
expect.any(Object)
452+
expect.any(Object),
453+
'name'
449454
);
450455
});
451456

@@ -621,6 +626,66 @@ describe('MCP Servers - Search Servers', () => {
621626
});
622627
});
623628

629+
describe('Sorting', () => {
630+
beforeEach(async () => {
631+
await searchServers(mockFastify as FastifyInstance);
632+
});
633+
634+
it('should sort by name by default', async () => {
635+
mockMcpService.getServersForUser.mockResolvedValue([]);
636+
637+
const handler = routeHandlers['GET /mcp/servers/search'];
638+
await handler({
639+
...mockRequest,
640+
query: { q: 'test', limit: '20', offset: '0' }
641+
}, mockReply);
642+
643+
expect(mockMcpService.getServersForUser).toHaveBeenCalledWith(
644+
'test-user-id',
645+
'global_user',
646+
[],
647+
expect.any(Object),
648+
'name'
649+
);
650+
});
651+
652+
it('should sort by github_stars when specified', async () => {
653+
mockMcpService.getServersForUser.mockResolvedValue([]);
654+
655+
const handler = routeHandlers['GET /mcp/servers/search'];
656+
await handler({
657+
...mockRequest,
658+
query: { q: 'test', sort_by: 'github_stars', limit: '20', offset: '0' }
659+
}, mockReply);
660+
661+
expect(mockMcpService.getServersForUser).toHaveBeenCalledWith(
662+
'test-user-id',
663+
'global_user',
664+
[],
665+
expect.any(Object),
666+
'github_stars'
667+
);
668+
});
669+
670+
it('should sort by name when explicitly specified', async () => {
671+
mockMcpService.getServersForUser.mockResolvedValue([]);
672+
673+
const handler = routeHandlers['GET /mcp/servers/search'];
674+
await handler({
675+
...mockRequest,
676+
query: { q: 'test', sort_by: 'name', limit: '20', offset: '0' }
677+
}, mockReply);
678+
679+
expect(mockMcpService.getServersForUser).toHaveBeenCalledWith(
680+
'test-user-id',
681+
'global_user',
682+
[],
683+
expect.any(Object),
684+
'name'
685+
);
686+
});
687+
});
688+
624689
describe('Query Parameter Validation', () => {
625690
beforeEach(async () => {
626691
await searchServers(mockFastify as FastifyInstance);
@@ -659,7 +724,8 @@ describe('MCP Servers - Search Servers', () => {
659724
'test-user-id',
660725
'global_user',
661726
[],
662-
expect.objectContaining({ featured: true })
727+
expect.objectContaining({ featured: true }),
728+
'name'
663729
);
664730

665731
// Test featured: 'false' (as string, which gets converted to boolean)
@@ -672,7 +738,8 @@ describe('MCP Servers - Search Servers', () => {
672738
'test-user-id',
673739
'global_user',
674740
[],
675-
expect.objectContaining({ featured: false })
741+
expect.objectContaining({ featured: false }),
742+
'name'
676743
);
677744
});
678745
});

0 commit comments

Comments
 (0)