Skip to content

Commit 92a86a1

Browse files
author
Lasim
committed
feat(backend): enhance server list and search responses with category details
1 parent bf97cd6 commit 92a86a1

File tree

7 files changed

+176
-40
lines changed

7 files changed

+176
-40
lines changed

services/backend/api-spec.json

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15967,10 +15967,28 @@
1596715967
"nullable": true,
1596815968
"description": "Owning team ID"
1596915969
},
15970-
"category_id": {
15971-
"type": "string",
15970+
"category": {
15971+
"type": "object",
1597215972
"nullable": true,
15973-
"description": "Category ID"
15973+
"properties": {
15974+
"id": {
15975+
"type": "string",
15976+
"description": "Category ID"
15977+
},
15978+
"name": {
15979+
"type": "string",
15980+
"description": "Category name"
15981+
},
15982+
"icon": {
15983+
"type": "string",
15984+
"nullable": true,
15985+
"description": "Category icon name/class"
15986+
}
15987+
},
15988+
"required": [
15989+
"id",
15990+
"name"
15991+
]
1597415992
},
1597515993
"tags": {
1597615994
"type": "array",
@@ -16918,10 +16936,28 @@
1691816936
"nullable": true,
1691916937
"description": "Owning team ID"
1692016938
},
16921-
"category_id": {
16922-
"type": "string",
16939+
"category": {
16940+
"type": "object",
1692316941
"nullable": true,
16924-
"description": "Category ID"
16942+
"properties": {
16943+
"id": {
16944+
"type": "string",
16945+
"description": "Category ID"
16946+
},
16947+
"name": {
16948+
"type": "string",
16949+
"description": "Category name"
16950+
},
16951+
"icon": {
16952+
"type": "string",
16953+
"nullable": true,
16954+
"description": "Category icon name/class"
16955+
}
16956+
},
16957+
"required": [
16958+
"id",
16959+
"name"
16960+
]
1692516961
},
1692616962
"tags": {
1692716963
"type": "array",

services/backend/api-spec.yaml

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11121,10 +11121,23 @@ paths:
1112111121
type: string
1112211122
nullable: true
1112311123
description: Owning team ID
11124-
category_id:
11125-
type: string
11124+
category:
11125+
type: object
1112611126
nullable: true
11127-
description: Category ID
11127+
properties:
11128+
id:
11129+
type: string
11130+
description: Category ID
11131+
name:
11132+
type: string
11133+
description: Category name
11134+
icon:
11135+
type: string
11136+
nullable: true
11137+
description: Category icon name/class
11138+
required:
11139+
- id
11140+
- name
1112811141
tags:
1112911142
type: array
1113011143
items:
@@ -11824,10 +11837,23 @@ paths:
1182411837
type: string
1182511838
nullable: true
1182611839
description: Owning team ID
11827-
category_id:
11828-
type: string
11840+
category:
11841+
type: object
1182911842
nullable: true
11830-
description: Category ID
11843+
properties:
11844+
id:
11845+
type: string
11846+
description: Category ID
11847+
name:
11848+
type: string
11849+
description: Category name
11850+
icon:
11851+
type: string
11852+
nullable: true
11853+
description: Category icon name/class
11854+
required:
11855+
- id
11856+
- name
1183111857
tags:
1183211858
type: array
1183311859
items:

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

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import { type FastifyInstance } from 'fastify';
22
import { McpCatalogService } from '../../../services/mcpCatalogService';
33
import { TeamService } from '../../../services/teamService';
44
import { getUserRole, requirePermission } from '../../../middleware/roleMiddleware';
5-
import { getDb } from '../../../db';
5+
import { getDb, getSchema } from '../../../db';
66
import {
77
LIST_SERVERS_QUERY_SCHEMA,
88
LIST_SERVERS_SUCCESS_RESPONSE_SCHEMA,
99
ERROR_RESPONSE_SCHEMA,
1010
type ListServersQueryParams,
1111
type ListServersSuccessResponse,
1212
type ErrorResponse,
13+
type CategoryEmbed,
1314
formatServerListResponse
1415
} from './schemas';
1516

@@ -43,12 +44,13 @@ export default async function listServers(server: FastifyInstance) {
4344
}, async (request, reply) => {
4445
try {
4546
const db = getDb();
47+
const schema = getSchema();
4648
const catalogService = new McpCatalogService(db, request.log);
47-
49+
4850
// Get user role and team memberships (same as search endpoint)
4951
const roleInfo = await getUserRole(request.user!.id);
5052
const userRole = roleInfo?.id || 'global_user';
51-
53+
5254
// Get user's team memberships
5355
let teamIds: string[] = [];
5456
try {
@@ -66,31 +68,31 @@ export default async function listServers(server: FastifyInstance) {
6668

6769
// Parse query parameters for filtering (Fastify has already validated)
6870
const query = request.query as ListServersQueryParams;
69-
71+
7072
// Build filters object
7173
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7274
const filters: any = {};
73-
75+
7476
if (query.category_id) {
7577
filters.category_id = query.category_id;
7678
}
77-
79+
7880
if (query.language) {
7981
filters.language = query.language;
8082
}
81-
83+
8284
if (query.runtime) {
8385
filters.runtime = query.runtime;
8486
}
85-
87+
8688
if (query.status) {
8789
filters.status = query.status;
8890
}
89-
91+
9092
if (query.featured) {
9193
filters.featured = query.featured === 'true';
9294
}
93-
95+
9496
if (query.search) {
9597
filters.search = query.search;
9698
}
@@ -103,10 +105,21 @@ export default async function listServers(server: FastifyInstance) {
103105
filters
104106
);
105107

108+
// Fetch all categories and build a lookup map
109+
const categories = await db.select().from(schema.mcpCategories);
110+
const categoriesMap = new Map<string, CategoryEmbed>();
111+
for (const cat of categories) {
112+
categoriesMap.set(cat.id, {
113+
id: cat.id,
114+
name: cat.name,
115+
icon: cat.icon || null
116+
});
117+
}
118+
106119
// Parse pagination parameters (already validated by Fastify)
107120
const limit = parseInt(query.limit || '20');
108121
const offset = parseInt(query.offset || '0');
109-
122+
110123
// Apply pagination
111124
const total = allServers.length;
112125
const paginatedServers = allServers.slice(offset, offset + limit);
@@ -122,7 +135,7 @@ export default async function listServers(server: FastifyInstance) {
122135
}, 'MCP server list completed');
123136

124137
// Format response using minimal list formatter (excludes config schemas, packages, etc.)
125-
const responseServers = paginatedServers.map(server => formatServerListResponse(server));
138+
const responseServers = paginatedServers.map(server => formatServerListResponse(server, categoriesMap));
126139

127140
// Manual JSON serialization to ensure consistent JSON output
128141
const successResponse: ListServersSuccessResponse = {

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

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

1004+
// Category object schema for embedding in server list responses
1005+
export const CATEGORY_EMBED_SCHEMA = {
1006+
type: 'object',
1007+
nullable: true,
1008+
properties: {
1009+
id: { type: 'string', description: 'Category ID' },
1010+
name: { type: 'string', description: 'Category name' },
1011+
icon: { type: 'string', nullable: true, description: 'Category icon name/class' }
1012+
},
1013+
required: ['id', 'name']
1014+
} as const;
1015+
10041016
// Minimal server entity for list endpoints - excludes configuration schemas and heavy fields
10051017
export const SERVER_LIST_ENTITY_SCHEMA = {
10061018
type: 'object',
@@ -1016,7 +1028,7 @@ export const SERVER_LIST_ENTITY_SCHEMA = {
10161028
transport_type: { type: 'string', enum: ['stdio', 'http', 'sse'], description: 'Transport type' },
10171029
visibility: { type: 'string', enum: ['global', 'team'], description: 'Server visibility' },
10181030
owner_team_id: { type: 'string', nullable: true, description: 'Owning team ID' },
1019-
category_id: { type: 'string', nullable: true, description: 'Category ID' },
1031+
category: CATEGORY_EMBED_SCHEMA,
10201032
tags: { type: 'array', items: { type: 'string' }, nullable: true, description: 'Server tags' },
10211033
status: { type: 'string', enum: ['active', 'deprecated', 'maintenance'], description: 'Server status' },
10221034
featured: { type: 'boolean', description: 'Whether server is featured' },
@@ -1549,6 +1561,13 @@ export interface DeleteGlobalServerSuccessResponse {
15491561
};
15501562
}
15511563

1564+
// Category embed interface for list responses
1565+
export interface CategoryEmbed {
1566+
id: string;
1567+
name: string;
1568+
icon: string | null;
1569+
}
1570+
15521571
// Minimal server entity for list responses
15531572
export interface ServerListEntity {
15541573
id: string;
@@ -1562,7 +1581,7 @@ export interface ServerListEntity {
15621581
transport_type: 'stdio' | 'http' | 'sse';
15631582
visibility: 'global' | 'team';
15641583
owner_team_id: string | null;
1565-
category_id: string | null;
1584+
category: CategoryEmbed | null;
15661585
tags: string[] | null;
15671586
status: 'active' | 'deprecated' | 'maintenance';
15681587
featured: boolean;
@@ -1707,8 +1726,14 @@ export function formatServerResponse(server: any): ServerEntity {
17071726
/**
17081727
* Converts McpServer to minimal API response format for list endpoints
17091728
* Excludes configuration schemas, packages, remotes, and other heavy fields
1729+
*
1730+
* @param server - The server entity from database
1731+
* @param categoriesMap - Optional map of category_id to category data for embedding
17101732
*/
1711-
export function formatServerListResponse(server: any): ServerListEntity {
1733+
export function formatServerListResponse(
1734+
server: any,
1735+
categoriesMap?: Map<string, CategoryEmbed>
1736+
): ServerListEntity {
17121737
const safeJsonParse = (field: any, defaultValue: any) => {
17131738
if (!field) return defaultValue;
17141739
if (typeof field === 'string') {
@@ -1721,6 +1746,10 @@ export function formatServerListResponse(server: any): ServerListEntity {
17211746
return field;
17221747
};
17231748

1749+
// Get category embed if available
1750+
const categoryId = server.category_id || null;
1751+
const category = categoryId && categoriesMap ? categoriesMap.get(categoryId) || null : null;
1752+
17241753
return {
17251754
id: server.id,
17261755
name: server.name,
@@ -1733,7 +1762,7 @@ export function formatServerListResponse(server: any): ServerListEntity {
17331762
transport_type: server.transport_type,
17341763
visibility: server.visibility,
17351764
owner_team_id: server.owner_team_id || null,
1736-
category_id: server.category_id || null,
1765+
category: category,
17371766
tags: safeJsonParse(server.tags, null),
17381767
status: server.status,
17391768
featured: server.featured,

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import { type FastifyInstance } from 'fastify';
22
import { McpCatalogService } from '../../../services/mcpCatalogService';
33
import { TeamService } from '../../../services/teamService';
44
import { getUserRole, requirePermission } from '../../../middleware/roleMiddleware';
5-
import { getDb } from '../../../db';
5+
import { getDb, getSchema } from '../../../db';
66
import {
77
SEARCH_SERVERS_QUERY_SCHEMA,
88
LIST_SERVERS_SUCCESS_RESPONSE_SCHEMA,
99
ERROR_RESPONSE_SCHEMA,
1010
type ListServersQueryParams,
1111
type ListServersSuccessResponse,
1212
type ErrorResponse,
13+
type CategoryEmbed,
1314
formatServerListResponse
1415
} from './schemas';
1516

@@ -74,12 +75,13 @@ export default async function searchServers(server: FastifyInstance) {
7475

7576
try {
7677
const db = getDb();
78+
const schema = getSchema();
7779
const mcpService = new McpCatalogService(db, request.log);
78-
80+
7981
// Get user role and team memberships
8082
const roleInfo = await getUserRole(request.user!.id);
8183
const userRole = roleInfo?.id || 'global_user';
82-
84+
8385
// Get user's team memberships
8486
let teamIds: string[] = [];
8587
try {
@@ -122,6 +124,17 @@ export default async function searchServers(server: FastifyInstance) {
122124
sortBy
123125
);
124126

127+
// Fetch all categories and build a lookup map
128+
const categories = await db.select().from(schema.mcpCategories);
129+
const categoriesMap = new Map<string, CategoryEmbed>();
130+
for (const cat of categories) {
131+
categoriesMap.set(cat.id, {
132+
id: cat.id,
133+
name: cat.name,
134+
icon: cat.icon || null
135+
});
136+
}
137+
125138
// Apply pagination
126139
const total = allServers.length;
127140
const paginatedServers = allServers.slice(offset, offset + limit);
@@ -139,7 +152,7 @@ export default async function searchServers(server: FastifyInstance) {
139152
}, 'MCP server search completed');
140153

141154
// Format response using minimal list formatter (excludes config schemas, packages, etc.)
142-
const responseServers = paginatedServers.map(server => formatServerListResponse(server));
155+
const responseServers = paginatedServers.map(server => formatServerListResponse(server, categoriesMap));
143156

144157
// Use the same response structure as list endpoint
145158
const response: ListServersSuccessResponse = {

0 commit comments

Comments
 (0)