Skip to content

Commit 49ac701

Browse files
author
Lasim
committed
feat: Enhance MCP Server Catalog with GitHub integration and pagination
- Added GitHub step in the server creation process to fetch repository data and auto-populate form fields. - Implemented error handling for GitHub data fetching with user feedback. - Introduced pagination controls for the server catalog view, allowing users to navigate through server listings. - Updated server editing view to display detailed information, including tags, tools, resources, and installation methods. - Added delete confirmation dialog for server deletion with warning messages about consequences. - Improved overall user experience with loading states and error handling across components.
1 parent 0dcc861 commit 49ac701

39 files changed

+9612
-1926
lines changed

services/backend/api-spec.json

Lines changed: 1768 additions & 195 deletions
Large diffs are not rendered by default.

services/backend/api-spec.yaml

Lines changed: 1300 additions & 162 deletions
Large diffs are not rendered by default.

services/backend/drizzle/migrations_sqlite/0014_concerned_trish_tilby.sql

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,6 @@ CREATE UNIQUE INDEX `mcpServers_slug_unique` ON `mcpServers` (`slug`);--> statem
6363
CREATE INDEX `mcp_servers_visibility_idx` ON `mcpServers` (`visibility`);--> statement-breakpoint
6464
CREATE INDEX `mcp_servers_category_idx` ON `mcpServers` (`category_id`);--> statement-breakpoint
6565
CREATE INDEX `mcp_servers_status_idx` ON `mcpServers` (`status`);--> statement-breakpoint
66-
CREATE INDEX `mcp_servers_owner_team_idx` ON `mcpServers` (`owner_team_id`);
66+
CREATE INDEX `mcp_servers_owner_team_idx` ON `mcpServers` (`owner_team_id`);--> statement-breakpoint
67+
INSERT INTO `mcpCategories` (`id`, `name`, `description`, `icon`, `sort_order`, `created_at`)
68+
VALUES ('default-category-id', 'Default', 'Default category', 'Tags', 0, strftime('%s', 'now') * 1000);
Lines changed: 303 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,313 @@
11
import { type FastifyInstance } from 'fastify';
2+
import { z } from 'zod';
3+
import { zodToJsonSchema } from 'zod-to-json-schema';
4+
import { requireGlobalAdmin } from '../../../middleware/roleMiddleware';
5+
import { McpCatalogService } from '../../../services/mcpCatalogService';
6+
import { getDb } from '../../../db';
7+
8+
// Request schema for creating global MCP servers
9+
const createGlobalServerRequestSchema = z.object({
10+
// Required fields
11+
name: z.string().min(1, 'Name is required').max(255, 'Name must be 255 characters or less'),
12+
description: z.string().min(1, 'Description is required'),
13+
language: z.string().min(1, 'Language is required'),
14+
runtime: z.string().min(1, 'Runtime is required'),
15+
installation_methods: z.array(z.object({
16+
type: z.string().min(1, 'Installation method type is required'),
17+
command: z.string().optional(),
18+
image: z.string().optional(),
19+
description: z.string().optional()
20+
})).min(1, 'At least one installation method is required'),
21+
tools: z.array(z.object({
22+
name: z.string().min(1, 'Tool name is required'),
23+
description: z.string().min(1, 'Tool description is required')
24+
})).min(1, 'At least one tool is required'),
25+
26+
// Optional fields
27+
long_description: z.string().optional(),
28+
github_url: z.string().url('Invalid GitHub URL').optional(),
29+
git_branch: z.string().default('main'),
30+
homepage_url: z.string().url('Invalid homepage URL').optional(),
31+
runtime_min_version: z.string().optional(),
32+
resources: z.array(z.object({
33+
type: z.string().min(1, 'Resource type is required'),
34+
description: z.string().min(1, 'Resource description is required')
35+
})).optional(),
36+
prompts: z.array(z.object({
37+
name: z.string().min(1, 'Prompt name is required'),
38+
description: z.string().min(1, 'Prompt description is required')
39+
})).optional(),
40+
author_name: z.string().optional(),
41+
author_contact: z.string().optional(),
42+
organization: z.string().optional(),
43+
license: z.string().optional(),
44+
default_config: z.record(z.any()).optional(),
45+
environment_variables: z.array(z.object({
46+
name: z.string().min(1, 'Environment variable name is required'),
47+
description: z.string().min(1, 'Environment variable description is required'),
48+
required: z.boolean().default(false),
49+
default_value: z.string().optional()
50+
})).optional(),
51+
dependencies: z.record(z.any()).optional(),
52+
category_id: z.string().optional(),
53+
tags: z.array(z.string()).optional(),
54+
featured: z.boolean().default(false)
55+
});
56+
57+
// Response schema for successful creation
58+
const createGlobalServerResponseSchema = z.object({
59+
success: z.boolean(),
60+
data: z.object({
61+
id: z.string(),
62+
name: z.string(),
63+
slug: z.string(),
64+
description: z.string(),
65+
long_description: z.string().nullable(),
66+
github_url: z.string().nullable(),
67+
git_branch: z.string().nullable(),
68+
homepage_url: z.string().nullable(),
69+
language: z.string(),
70+
runtime: z.string(),
71+
runtime_min_version: z.string().nullable(),
72+
installation_methods: z.array(z.any()),
73+
tools: z.array(z.any()),
74+
resources: z.array(z.any()).nullable(),
75+
prompts: z.array(z.any()).nullable(),
76+
visibility: z.enum(['global', 'team']),
77+
owner_team_id: z.string().nullable(),
78+
created_by: z.string(),
79+
author_name: z.string().nullable(),
80+
author_contact: z.string().nullable(),
81+
organization: z.string().nullable(),
82+
license: z.string().nullable(),
83+
default_config: z.record(z.any()).nullable(),
84+
environment_variables: z.array(z.any()).nullable(),
85+
dependencies: z.record(z.any()).nullable(),
86+
category_id: z.string().nullable(),
87+
tags: z.array(z.string()).nullable(),
88+
status: z.enum(['active', 'deprecated', 'maintenance']),
89+
featured: z.boolean(),
90+
created_at: z.string(),
91+
updated_at: z.string(),
92+
last_sync_at: z.string().nullable()
93+
})
94+
});
95+
96+
// Error response schema
97+
const errorResponseSchema = z.object({
98+
success: z.boolean().default(false),
99+
error: z.string(),
100+
details: z.any().optional()
101+
});
2102

3103
export default async function createGlobalServer(server: FastifyInstance) {
4104
server.post('/mcp/servers/global', {
105+
preValidation: requireGlobalAdmin(),
5106
schema: {
6107
tags: ['MCP Servers'],
7-
summary: 'Create global MCP server (Admin only)',
8-
description: 'Create a new global MCP server - requires global admin permissions. Will require Content-Type: application/json header when sending request body once implemented.'
108+
summary: 'Create global MCP server (Global Admin only)',
109+
description: 'Create a new global MCP server - requires global admin permissions. Global servers are visible to all users. Requires Content-Type: application/json header when sending request body.',
110+
security: [{ cookieAuth: [] }],
111+
requestBody: {
112+
required: true,
113+
content: {
114+
'application/json': {
115+
schema: zodToJsonSchema(createGlobalServerRequestSchema, {
116+
$refStrategy: 'none',
117+
target: 'openApi3'
118+
})
119+
}
120+
}
121+
},
122+
response: {
123+
201: zodToJsonSchema(createGlobalServerResponseSchema, {
124+
$refStrategy: 'none',
125+
target: 'openApi3'
126+
}),
127+
400: zodToJsonSchema(errorResponseSchema.describe('Bad Request - Invalid input or missing Content-Type header'), {
128+
$refStrategy: 'none',
129+
target: 'openApi3'
130+
}),
131+
401: zodToJsonSchema(errorResponseSchema.describe('Unauthorized - Authentication required'), {
132+
$refStrategy: 'none',
133+
target: 'openApi3'
134+
}),
135+
403: zodToJsonSchema(errorResponseSchema.describe('Forbidden - Global admin permissions required'), {
136+
$refStrategy: 'none',
137+
target: 'openApi3'
138+
}),
139+
409: zodToJsonSchema(errorResponseSchema.describe('Conflict - Server name already exists'), {
140+
$refStrategy: 'none',
141+
target: 'openApi3'
142+
}),
143+
500: zodToJsonSchema(errorResponseSchema.describe('Internal Server Error'), {
144+
$refStrategy: 'none',
145+
target: 'openApi3'
146+
})
147+
}
9148
}
10149
}, async (request, reply) => {
11-
return reply.status(501).send({
12-
success: false,
13-
error: 'Not implemented yet'
14-
});
150+
const requestData = request.body as z.infer<typeof createGlobalServerRequestSchema>;
151+
152+
request.log.info({
153+
operation: 'create_global_mcp_server',
154+
userId: request.user?.id,
155+
serverName: requestData.name,
156+
language: requestData.language,
157+
runtime: requestData.runtime,
158+
featured: requestData.featured
159+
}, 'Creating global MCP server');
160+
161+
try {
162+
const db = getDb();
163+
const mcpService = new McpCatalogService(db, request.log);
164+
165+
// Force global visibility and no team ownership for global servers
166+
const serverData = {
167+
...requestData,
168+
visibility: 'global' as const
169+
};
170+
171+
const newServer = await mcpService.createServer(
172+
request.user!.id,
173+
'global_admin', // We know user is global admin due to middleware
174+
null, // No team for global servers
175+
serverData
176+
);
177+
178+
request.log.info({
179+
operation: 'create_global_mcp_server',
180+
userId: request.user?.id,
181+
serverId: newServer.id,
182+
serverSlug: newServer.slug,
183+
serverName: newServer.name,
184+
featured: newServer.featured
185+
}, 'Global MCP server created successfully');
186+
187+
// Parse JSON fields for response with proper null checks and error handling
188+
try {
189+
request.log.debug({
190+
operation: 'create_global_mcp_server',
191+
userId: request.user?.id,
192+
serverId: newServer.id,
193+
rawServerData: {
194+
installation_methods: newServer.installation_methods,
195+
tools: newServer.tools,
196+
resources: newServer.resources,
197+
prompts: newServer.prompts,
198+
default_config: newServer.default_config,
199+
environment_variables: newServer.environment_variables,
200+
dependencies: newServer.dependencies,
201+
tags: newServer.tags
202+
}
203+
}, 'About to parse JSON fields for response');
204+
205+
const responseData = {
206+
id: newServer.id,
207+
name: newServer.name,
208+
slug: newServer.slug,
209+
description: newServer.description,
210+
long_description: newServer.long_description || null,
211+
github_url: newServer.github_url || null,
212+
git_branch: newServer.git_branch || null,
213+
homepage_url: newServer.homepage_url || null,
214+
language: newServer.language,
215+
runtime: newServer.runtime,
216+
runtime_min_version: newServer.runtime_min_version || null,
217+
installation_methods: newServer.installation_methods ? JSON.parse(newServer.installation_methods) : [],
218+
tools: newServer.tools ? JSON.parse(newServer.tools) : [],
219+
resources: newServer.resources ? JSON.parse(newServer.resources) : null,
220+
prompts: newServer.prompts ? JSON.parse(newServer.prompts) : null,
221+
visibility: newServer.visibility,
222+
owner_team_id: newServer.owner_team_id || null,
223+
created_by: newServer.created_by,
224+
author_name: newServer.author_name || null,
225+
author_contact: newServer.author_contact || null,
226+
organization: newServer.organization || null,
227+
license: newServer.license || null,
228+
default_config: newServer.default_config ? JSON.parse(newServer.default_config) : null,
229+
environment_variables: newServer.environment_variables ? JSON.parse(newServer.environment_variables) : null,
230+
dependencies: newServer.dependencies ? JSON.parse(newServer.dependencies) : null,
231+
category_id: newServer.category_id || null,
232+
tags: newServer.tags ? JSON.parse(newServer.tags) : null,
233+
status: newServer.status,
234+
featured: newServer.featured,
235+
created_at: newServer.created_at.toISOString(),
236+
updated_at: newServer.updated_at.toISOString(),
237+
last_sync_at: newServer.last_sync_at?.toISOString() || null
238+
};
239+
240+
request.log.debug({
241+
operation: 'create_global_mcp_server',
242+
userId: request.user?.id,
243+
serverId: newServer.id
244+
}, 'JSON parsing completed, about to send response');
245+
246+
const response = {
247+
success: true,
248+
data: responseData
249+
};
250+
251+
request.log.debug({
252+
operation: 'create_global_mcp_server',
253+
userId: request.user?.id,
254+
serverId: newServer.id
255+
}, 'Sending 201 response');
256+
257+
return reply.status(201).send(response);
258+
} catch (jsonError) {
259+
request.log.error({
260+
operation: 'create_global_mcp_server',
261+
userId: request.user?.id,
262+
serverId: newServer.id,
263+
jsonError,
264+
serverData: {
265+
installation_methods: newServer.installation_methods,
266+
tools: newServer.tools,
267+
resources: newServer.resources,
268+
prompts: newServer.prompts,
269+
default_config: newServer.default_config,
270+
environment_variables: newServer.environment_variables,
271+
dependencies: newServer.dependencies,
272+
tags: newServer.tags
273+
}
274+
}, 'Failed to parse JSON fields in response');
275+
276+
return reply.status(500).send({
277+
success: false,
278+
error: 'Failed to format server response'
279+
});
280+
}
281+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
282+
} catch (error: any) {
283+
request.log.error({
284+
operation: 'create_global_mcp_server',
285+
userId: request.user?.id,
286+
serverName: requestData.name,
287+
error
288+
}, 'Failed to create global MCP server');
289+
290+
// Handle specific error cases
291+
if (error.message?.includes('UNIQUE constraint failed') ||
292+
error.message?.includes('already exists') ||
293+
error.message?.includes('duplicate')) {
294+
return reply.status(409).send({
295+
success: false,
296+
error: 'Server name already exists'
297+
});
298+
}
299+
300+
if (error.message?.includes('Only global administrators')) {
301+
return reply.status(403).send({
302+
success: false,
303+
error: 'Global admin permissions required'
304+
});
305+
}
306+
307+
return reply.status(500).send({
308+
success: false,
309+
error: 'Failed to create global MCP server'
310+
});
311+
}
15312
});
16313
}

0 commit comments

Comments
 (0)