Skip to content

Commit bf97cd6

Browse files
author
Lasim
committed
feat(backend): implement minimal server response format for list endpoints
1 parent 79c6a53 commit bf97cd6

File tree

8 files changed

+291
-681
lines changed

8 files changed

+291
-681
lines changed

services/backend/api-spec.json

Lines changed: 64 additions & 336 deletions
Large diffs are not rendered by default.

services/backend/api-spec.yaml

Lines changed: 56 additions & 274 deletions
Large diffs are not rendered by default.

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
type ListServersQueryParams,
1111
type ListServersSuccessResponse,
1212
type ErrorResponse,
13-
formatServerResponse
13+
formatServerListResponse
1414
} from './schemas';
1515

1616
export default async function listServers(server: FastifyInstance) {
@@ -121,8 +121,8 @@ export default async function listServers(server: FastifyInstance) {
121121
appliedFilters: filters
122122
}, 'MCP server list completed');
123123

124-
// Format dates for response using the shared utility function
125-
const responseServers = paginatedServers.map(server => formatServerResponse(server));
124+
// Format response using minimal list formatter (excludes config schemas, packages, etc.)
125+
const responseServers = paginatedServers.map(server => formatServerListResponse(server));
126126

127127
// Manual JSON serialization to ensure consistent JSON output
128128
const successResponse: ListServersSuccessResponse = {

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

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,10 +1001,40 @@ export const COMMON_ERROR_RESPONSES = {
10011001
500: { ...ERROR_RESPONSE_SCHEMA, description: 'Internal Server Error' }
10021002
} as const;
10031003

1004+
// Minimal server entity for list endpoints - excludes configuration schemas and heavy fields
1005+
export const SERVER_LIST_ENTITY_SCHEMA = {
1006+
type: 'object',
1007+
properties: {
1008+
id: { type: 'string', description: 'Unique server identifier' },
1009+
name: { type: 'string', description: 'Server name' },
1010+
slug: { type: 'string', description: 'URL-friendly server slug' },
1011+
description: { type: 'string', description: 'Server description' },
1012+
icon_url: { type: 'string', nullable: true, description: 'Icon/logo URL' },
1013+
website_url: { type: 'string', nullable: true, description: 'Website URL' },
1014+
language: { type: 'string', description: 'Programming language' },
1015+
runtime: { type: 'string', description: 'Runtime environment' },
1016+
transport_type: { type: 'string', enum: ['stdio', 'http', 'sse'], description: 'Transport type' },
1017+
visibility: { type: 'string', enum: ['global', 'team'], description: 'Server visibility' },
1018+
owner_team_id: { type: 'string', nullable: true, description: 'Owning team ID' },
1019+
category_id: { type: 'string', nullable: true, description: 'Category ID' },
1020+
tags: { type: 'array', items: { type: 'string' }, nullable: true, description: 'Server tags' },
1021+
status: { type: 'string', enum: ['active', 'deprecated', 'maintenance'], description: 'Server status' },
1022+
featured: { type: 'boolean', description: 'Whether server is featured' },
1023+
requires_oauth: { type: 'boolean', description: 'Whether server requires OAuth' },
1024+
github_stars: { type: 'number', nullable: true, description: 'GitHub stars count' },
1025+
author_name: { type: 'string', nullable: true, description: 'Author name' },
1026+
organization: { type: 'string', nullable: true, description: 'Organization' },
1027+
official_name: { type: 'string', nullable: true, description: 'Official registry name' },
1028+
created_at: { type: 'string', format: 'date-time', description: 'Creation timestamp' },
1029+
updated_at: { type: 'string', format: 'date-time', description: 'Last update timestamp' }
1030+
},
1031+
required: ['id', 'name', 'slug', 'description', 'language', 'runtime', 'transport_type', 'visibility', 'status', 'featured', 'requires_oauth', 'created_at', 'updated_at']
1032+
} as const;
1033+
10041034
export const LIST_SERVERS_SUCCESS_RESPONSE_SCHEMA = {
10051035
type: 'object',
10061036
properties: {
1007-
success: {
1037+
success: {
10081038
type: 'boolean',
10091039
description: 'Indicates successful server list retrieval'
10101040
},
@@ -1013,8 +1043,8 @@ export const LIST_SERVERS_SUCCESS_RESPONSE_SCHEMA = {
10131043
properties: {
10141044
servers: {
10151045
type: 'array',
1016-
items: SERVER_ENTITY_SCHEMA,
1017-
description: 'Array of server objects'
1046+
items: SERVER_LIST_ENTITY_SCHEMA,
1047+
description: 'Array of server objects (minimal fields for listing)'
10181048
},
10191049
pagination: {
10201050
...PAGINATION_SCHEMA,
@@ -1519,10 +1549,36 @@ export interface DeleteGlobalServerSuccessResponse {
15191549
};
15201550
}
15211551

1552+
// Minimal server entity for list responses
1553+
export interface ServerListEntity {
1554+
id: string;
1555+
name: string;
1556+
slug: string;
1557+
description: string;
1558+
icon_url: string | null;
1559+
website_url: string | null;
1560+
language: string;
1561+
runtime: string;
1562+
transport_type: 'stdio' | 'http' | 'sse';
1563+
visibility: 'global' | 'team';
1564+
owner_team_id: string | null;
1565+
category_id: string | null;
1566+
tags: string[] | null;
1567+
status: 'active' | 'deprecated' | 'maintenance';
1568+
featured: boolean;
1569+
requires_oauth: boolean;
1570+
github_stars: number | null;
1571+
author_name: string | null;
1572+
organization: string | null;
1573+
official_name: string | null;
1574+
created_at: string;
1575+
updated_at: string;
1576+
}
1577+
15221578
export interface ListServersSuccessResponse {
15231579
success: boolean;
15241580
data: {
1525-
servers: ServerEntity[];
1581+
servers: ServerListEntity[];
15261582
pagination: PaginationInfo;
15271583
};
15281584
}
@@ -1647,3 +1703,46 @@ export function formatServerResponse(server: any): ServerEntity {
16471703
last_sync_at: server.last_sync_at?.toISOString() || null
16481704
};
16491705
}
1706+
1707+
/**
1708+
* Converts McpServer to minimal API response format for list endpoints
1709+
* Excludes configuration schemas, packages, remotes, and other heavy fields
1710+
*/
1711+
export function formatServerListResponse(server: any): ServerListEntity {
1712+
const safeJsonParse = (field: any, defaultValue: any) => {
1713+
if (!field) return defaultValue;
1714+
if (typeof field === 'string') {
1715+
try {
1716+
return JSON.parse(field);
1717+
} catch {
1718+
return defaultValue;
1719+
}
1720+
}
1721+
return field;
1722+
};
1723+
1724+
return {
1725+
id: server.id,
1726+
name: server.name,
1727+
slug: server.slug,
1728+
description: server.description,
1729+
icon_url: server.icon_url || null,
1730+
website_url: server.website_url || null,
1731+
language: server.language,
1732+
runtime: server.runtime,
1733+
transport_type: server.transport_type,
1734+
visibility: server.visibility,
1735+
owner_team_id: server.owner_team_id || null,
1736+
category_id: server.category_id || null,
1737+
tags: safeJsonParse(server.tags, null),
1738+
status: server.status,
1739+
featured: server.featured,
1740+
requires_oauth: server.requires_oauth || false,
1741+
github_stars: server.github_stars || null,
1742+
author_name: server.author_name || null,
1743+
organization: server.organization || null,
1744+
official_name: server.official_name || null,
1745+
created_at: server.created_at.toISOString(),
1746+
updated_at: server.updated_at.toISOString()
1747+
};
1748+
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
type ListServersQueryParams,
1111
type ListServersSuccessResponse,
1212
type ErrorResponse,
13-
formatServerResponse
13+
formatServerListResponse
1414
} from './schemas';
1515

1616
// TypeScript interface for search query params
@@ -138,8 +138,8 @@ export default async function searchServers(server: FastifyInstance) {
138138
teamCount: teamIds.length
139139
}, 'MCP server search completed');
140140

141-
// Format servers using the shared utility function
142-
const responseServers = paginatedServers.map(server => formatServerResponse(server));
141+
// Format response using minimal list formatter (excludes config schemas, packages, etc.)
142+
const responseServers = paginatedServers.map(server => formatServerListResponse(server));
143143

144144
// Use the same response structure as list endpoint
145145
const response: ListServersSuccessResponse = {

services/backend/tests/unit/routes/mcp/index.test.ts

Lines changed: 48 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import mcpRoutes from '../../../../src/routes/mcp/index';
44

55
// Mock all the route modules
66
vi.mock('../../../../src/routes/mcp/categories/list');
7+
vi.mock('../../../../src/routes/mcp/categories/list-featured');
78
vi.mock('../../../../src/routes/mcp/categories/create');
89
vi.mock('../../../../src/routes/mcp/categories/update');
910
vi.mock('../../../../src/routes/mcp/categories/delete');
@@ -35,6 +36,7 @@ vi.mock('../../../../src/routes/mcp/user-configurations');
3536

3637
// Import mocked modules
3738
import listCategories from '../../../../src/routes/mcp/categories/list';
39+
import listFeaturedCategories from '../../../../src/routes/mcp/categories/list-featured';
3840
import createCategory from '../../../../src/routes/mcp/categories/create';
3941
import updateCategory from '../../../../src/routes/mcp/categories/update';
4042
import deleteCategory from '../../../../src/routes/mcp/categories/delete';
@@ -66,6 +68,7 @@ import userConfigurationsRoutes from '../../../../src/routes/mcp/user-configurat
6668

6769
// Type the mocked functions
6870
const mockListCategories = listCategories as MockedFunction<typeof listCategories>;
71+
const mockListFeaturedCategories = listFeaturedCategories as MockedFunction<typeof listFeaturedCategories>;
6972
const mockCreateCategory = createCategory as MockedFunction<typeof createCategory>;
7073
const mockUpdateCategory = updateCategory as MockedFunction<typeof updateCategory>;
7174
const mockDeleteCategory = deleteCategory as MockedFunction<typeof deleteCategory>;
@@ -115,6 +118,7 @@ describe('MCP Routes Registration', () => {
115118

116119
// Mock all route modules to return resolved promises
117120
mockListCategories.mockResolvedValue(undefined);
121+
mockListFeaturedCategories.mockResolvedValue(undefined);
118122
mockCreateCategory.mockResolvedValue(undefined);
119123
mockUpdateCategory.mockResolvedValue(undefined);
120124
mockDeleteCategory.mockResolvedValue(undefined);
@@ -148,14 +152,15 @@ describe('MCP Routes Registration', () => {
148152
it('should register all MCP route modules', async () => {
149153
await mcpRoutes(mockFastify as FastifyInstance);
150154

151-
// Verify that all 24 routes are registered
152-
expect(mockFastify.register).toHaveBeenCalledTimes(24);
155+
// Verify that all 25 routes are registered
156+
expect(mockFastify.register).toHaveBeenCalledTimes(25);
153157
});
154158

155159
it('should register all category routes', async () => {
156160
await mcpRoutes(mockFastify as FastifyInstance);
157161

158162
expect(mockFastify.register).toHaveBeenCalledWith(listCategories);
163+
expect(mockFastify.register).toHaveBeenCalledWith(listFeaturedCategories);
159164
expect(mockFastify.register).toHaveBeenCalledWith(createCategory);
160165
expect(mockFastify.register).toHaveBeenCalledWith(updateCategory);
161166
expect(mockFastify.register).toHaveBeenCalledWith(deleteCategory);
@@ -222,88 +227,89 @@ describe('MCP Routes Registration', () => {
222227
await mcpRoutes(mockFastify as FastifyInstance);
223228

224229
const registerCalls = (mockFastify.register as any).mock.calls;
225-
226-
// Check that category routes are registered first (positions 0-3)
230+
231+
// Check that category routes are registered first (positions 0-4)
227232
expect(registerCalls[0][0]).toBe(listCategories);
228-
expect(registerCalls[1][0]).toBe(createCategory);
229-
expect(registerCalls[2][0]).toBe(updateCategory);
230-
expect(registerCalls[3][0]).toBe(deleteCategory);
233+
expect(registerCalls[1][0]).toBe(listFeaturedCategories);
234+
expect(registerCalls[2][0]).toBe(createCategory);
235+
expect(registerCalls[3][0]).toBe(updateCategory);
236+
expect(registerCalls[4][0]).toBe(deleteCategory);
231237
});
232238

233239
it('should register server routes after categories', async () => {
234240
await mcpRoutes(mockFastify as FastifyInstance);
235241

236242
const registerCalls = (mockFastify.register as any).mock.calls;
237-
238-
// Check that server routes are in positions 4-10
239-
expect(registerCalls[4][0]).toBe(listServers);
240-
expect(registerCalls[5][0]).toBe(getServer);
241-
expect(registerCalls[6][0]).toBe(getServerReadme);
242-
expect(registerCalls[7][0]).toBe(searchServers);
243-
expect(registerCalls[8][0]).toBe(getTags);
244-
expect(registerCalls[9][0]).toBe(getLanguages);
245-
expect(registerCalls[10][0]).toBe(getRuntimes);
243+
244+
// Check that server routes are in positions 5-11
245+
expect(registerCalls[5][0]).toBe(listServers);
246+
expect(registerCalls[6][0]).toBe(getServer);
247+
expect(registerCalls[7][0]).toBe(getServerReadme);
248+
expect(registerCalls[8][0]).toBe(searchServers);
249+
expect(registerCalls[9][0]).toBe(getTags);
250+
expect(registerCalls[10][0]).toBe(getLanguages);
251+
expect(registerCalls[11][0]).toBe(getRuntimes);
246252
});
247253

248254
it('should register global server management routes correctly', async () => {
249255
await mcpRoutes(mockFastify as FastifyInstance);
250256

251257
const registerCalls = (mockFastify.register as any).mock.calls;
252-
253-
// Check that global server management routes are in positions 11-13
254-
expect(registerCalls[11][0]).toBe(createGlobalServer);
255-
expect(registerCalls[12][0]).toBe(updateGlobalServer);
256-
expect(registerCalls[13][0]).toBe(deleteGlobalServer);
258+
259+
// Check that global server management routes are in positions 12-14
260+
expect(registerCalls[12][0]).toBe(createGlobalServer);
261+
expect(registerCalls[13][0]).toBe(updateGlobalServer);
262+
expect(registerCalls[14][0]).toBe(deleteGlobalServer);
257263
});
258264

259265
it('should register team server management routes correctly', async () => {
260266
await mcpRoutes(mockFastify as FastifyInstance);
261267

262268
const registerCalls = (mockFastify.register as any).mock.calls;
263-
264-
// Check that team server management routes are in positions 14-17
265-
expect(registerCalls[14][0]).toBe(listTeamServers);
266-
expect(registerCalls[15][0]).toBe(createTeamServer);
267-
expect(registerCalls[16][0]).toBe(updateTeamServer);
268-
expect(registerCalls[17][0]).toBe(deleteTeamServer);
269+
270+
// Check that team server management routes are in positions 15-18
271+
expect(registerCalls[15][0]).toBe(listTeamServers);
272+
expect(registerCalls[16][0]).toBe(createTeamServer);
273+
expect(registerCalls[17][0]).toBe(updateTeamServer);
274+
expect(registerCalls[18][0]).toBe(deleteTeamServer);
269275
});
270276

271277
it('should register version management routes correctly', async () => {
272278
await mcpRoutes(mockFastify as FastifyInstance);
273279

274280
const registerCalls = (mockFastify.register as any).mock.calls;
275-
276-
// Check that version management routes are in positions 18-20
277-
expect(registerCalls[18][0]).toBe(listVersions);
278-
expect(registerCalls[19][0]).toBe(createVersion);
279-
expect(registerCalls[20][0]).toBe(updateVersion);
281+
282+
// Check that version management routes are in positions 19-21
283+
expect(registerCalls[19][0]).toBe(listVersions);
284+
expect(registerCalls[20][0]).toBe(createVersion);
285+
expect(registerCalls[21][0]).toBe(updateVersion);
280286
});
281287

282288
it('should register GitHub integration routes correctly', async () => {
283289
await mcpRoutes(mockFastify as FastifyInstance);
284290

285291
const registerCalls = (mockFastify.register as any).mock.calls;
286-
287-
// Check that GitHub route is at position 21
288-
expect(registerCalls[21][0]).toBe(getRepoInfo);
292+
293+
// Check that GitHub route is at position 22
294+
expect(registerCalls[22][0]).toBe(getRepoInfo);
289295
});
290296

291297
it('should register installations routes correctly', async () => {
292298
await mcpRoutes(mockFastify as FastifyInstance);
293299

294300
const registerCalls = (mockFastify.register as any).mock.calls;
295-
296-
// Check that installations route is at position 22
297-
expect(registerCalls[22][0]).toBe(installationsRoutes);
301+
302+
// Check that installations route is at position 23
303+
expect(registerCalls[23][0]).toBe(installationsRoutes);
298304
});
299305

300306
it('should register user configurations routes last', async () => {
301307
await mcpRoutes(mockFastify as FastifyInstance);
302308

303309
const registerCalls = (mockFastify.register as any).mock.calls;
304-
305-
// Check that user configurations route is at position 23 (last)
306-
expect(registerCalls[23][0]).toBe(userConfigurationsRoutes);
310+
311+
// Check that user configurations route is at position 24 (last)
312+
expect(registerCalls[24][0]).toBe(userConfigurationsRoutes);
307313
});
308314
});
309315

@@ -333,6 +339,7 @@ describe('MCP Routes Registration', () => {
333339
it('should properly import and use all route modules', () => {
334340
// Verify that all modules are properly imported and mocked
335341
expect(listCategories).toBeDefined();
342+
expect(listFeaturedCategories).toBeDefined();
336343
expect(createCategory).toBeDefined();
337344
expect(updateCategory).toBeDefined();
338345
expect(deleteCategory).toBeDefined();

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,8 @@ describe('MCP Servers - List Servers', () => {
192192
id: 'server-1',
193193
name: 'Test Server 1',
194194
created_at: '2024-01-01T00:00:00.000Z',
195-
updated_at: '2024-01-01T00:00:00.000Z',
196-
last_sync_at: null
195+
updated_at: '2024-01-01T00:00:00.000Z'
196+
// Minimal list response excludes last_sync_at
197197
})
198198
]),
199199
pagination: {

0 commit comments

Comments
 (0)