Skip to content

Commit 2683ffb

Browse files
author
Lasim
committed
feat(mcp-catalog): implement edit functionality for MCP server with form wizard and JSON configuration support
refactor(mcp-catalog): update server data handling and improve error management in edit view fix(mcp-catalog): enhance JSON parsing for environment variables, default config, and dependencies in server view
1 parent 1e7dd6f commit 2683ffb

File tree

13 files changed

+688
-195
lines changed

13 files changed

+688
-195
lines changed

services/backend/api-spec.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,34 @@
1616
"schemas": {}
1717
},
1818
"paths": {
19+
"/api/plugin/example-plugin/examples": {
20+
"get": {
21+
"responses": {
22+
"200": {
23+
"description": "Default Response"
24+
}
25+
}
26+
}
27+
},
28+
"/api/plugin/example-plugin/examples/{id}": {
29+
"get": {
30+
"parameters": [
31+
{
32+
"schema": {
33+
"type": "string"
34+
},
35+
"in": "path",
36+
"name": "id",
37+
"required": true
38+
}
39+
],
40+
"responses": {
41+
"200": {
42+
"description": "Default Response"
43+
}
44+
}
45+
}
46+
},
1947
"/": {
2048
"get": {
2149
"summary": "API health check",

services/backend/api-spec.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,22 @@ components:
1111
name: auth_session
1212
schemas: {}
1313
paths:
14+
/api/plugin/example-plugin/examples:
15+
get:
16+
responses:
17+
"200":
18+
description: Default Response
19+
/api/plugin/example-plugin/examples/{id}:
20+
get:
21+
parameters:
22+
- schema:
23+
type: string
24+
in: path
25+
name: id
26+
required: true
27+
responses:
28+
"200":
29+
description: Default Response
1430
/:
1531
get:
1632
summary: API health check

services/backend/src/db/index.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,7 @@ async function createDatabaseInstance(config: DatabaseConfig, schema: AnySchema,
151151
* Apply migrations for any database type
152152
*/
153153
async function applyMigrations(db: AnyDatabase, config: DatabaseConfig, logger: FastifyBaseLogger) {
154-
// Skip migrations in test mode
155-
if (isTestMode()) {
156-
return;
157-
}
154+
// Note: Migrations now run in test mode to ensure plugin tables are created
158155

159156
const projectRootMigrationsDir = path.join(process.cwd(), 'drizzle');
160157
const migrationsPath = path.join(projectRootMigrationsDir, 'migrations_sqlite');

services/backend/src/plugins/example-plugin/index.ts

Lines changed: 43 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
type PluginRouteManager
88
} from '../../plugin-system/types';
99

10-
import { type AnyDatabase, getSchema } from '../../db'; // Import getSchema
10+
import { type AnyDatabase, type AnySchema, getSchema } from '../../db'; // Import getSchema and AnySchema
1111
import { type BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'; // For type guard
1212
import { type NodePgDatabase } from 'drizzle-orm/node-postgres'; // For casting db
1313
import { type SQLiteTable } from 'drizzle-orm/sqlite-core'; // For casting table from schema
@@ -97,65 +97,54 @@ class ExamplePlugin implements Plugin {
9797
tableDefinitions: examplePluginTableDefinitions, // Use tableDefinitions
9898

9999
// Optional initialization function
100-
// Use arrow function to correctly capture 'this' for access to this.meta.id
101-
onDatabaseInit: async (db: AnyDatabase, logger: FastifyBaseLogger) => {
102-
logger.info({
103-
operation: 'plugin_database_init',
104-
pluginId: this.meta.id
105-
}, 'Initializing example plugin database...');
100+
onDatabaseInit: async (db: AnyDatabase, schema: AnySchema) => {
101+
// Note: The function signature expects (db, schema) not (db, logger)
102+
// We'll use console.log for now since logger is not available
103+
console.log('Initializing example plugin database...');
106104

107-
const currentSchema = getSchema();
108-
// 'this' here refers to the ExamplePlugin instance because of the arrow function
109-
const tableNameInSchema = `${this.meta.id}_example_entities`;
110-
const table = currentSchema[tableNameInSchema];
105+
// Use hardcoded plugin ID since 'this' is not available in arrow function
106+
const tableNameInSchema = `example-plugin_example_entities`;
107+
const table = schema[tableNameInSchema];
111108

112-
if (!table) {
113-
logger.error({
114-
operation: 'plugin_database_init',
115-
pluginId: this.meta.id,
116-
tableNameInSchema,
117-
error: 'Table not found in global schema'
118-
}, 'Critical: Table not found in global schema! Cannot initialize database for plugin.');
119-
return;
120-
}
121-
122-
let currentCount = 0;
123-
if (isSQLiteDB(db)) {
124-
const result = await db
125-
.select({ count: sql<number>`count(*)` })
126-
.from(table as SQLiteTable)
127-
.get();
128-
currentCount = result?.count ?? 0;
129-
} else {
130-
// Assume NodePgDatabase-like behavior
131-
const rows = await (db as NodePgDatabase)
132-
.select({ count: sql<number>`count(*)` })
133-
.from(table as PgTable);
134-
currentCount = rows[0]?.count ?? 0;
135-
}
136-
137-
if (currentCount === 0) {
138-
logger.info({
139-
operation: 'plugin_database_seed',
140-
pluginId: this.meta.id
141-
}, 'Seeding initial data...');
142-
const dataToSeed = {
143-
id: 'example1',
144-
name: 'Example Entity',
145-
description: 'This is an example entity created by the plugin',
146-
};
109+
if (!table) {
110+
console.error('Critical: Table not found in global schema! Cannot initialize database for plugin.', {
111+
tableNameInSchema,
112+
availableTables: Object.keys(schema)
113+
});
114+
return;
115+
}
116+
117+
let currentCount = 0;
147118
if (isSQLiteDB(db)) {
148-
await db.insert(table as SQLiteTable).values(dataToSeed).run();
119+
const result = await db
120+
.select({ count: sql<number>`count(*)` })
121+
.from(table as SQLiteTable)
122+
.get();
123+
currentCount = result?.count ?? 0;
149124
} else {
150125
// Assume NodePgDatabase-like behavior
151-
await (db as NodePgDatabase).insert(table as PgTable).values(dataToSeed);
126+
const rows = await (db as NodePgDatabase)
127+
.select({ count: sql<number>`count(*)` })
128+
.from(table as PgTable);
129+
currentCount = rows[0]?.count ?? 0;
130+
}
131+
132+
if (currentCount === 0) {
133+
console.log('Seeding initial data for example plugin...');
134+
const dataToSeed = {
135+
id: 'example1',
136+
name: 'Example Entity',
137+
description: 'This is an example entity created by the plugin',
138+
};
139+
if (isSQLiteDB(db)) {
140+
await db.insert(table as SQLiteTable).values(dataToSeed).run();
141+
} else {
142+
// Assume NodePgDatabase-like behavior
143+
await (db as NodePgDatabase).insert(table as PgTable).values(dataToSeed);
144+
}
145+
console.log('Seeded initial data for example plugin');
152146
}
153-
logger.info({
154-
operation: 'plugin_database_seed',
155-
pluginId: this.meta.id
156-
}, 'Seeded initial data');
157-
}
158-
},
147+
},
159148
};
160149

161150
// Initialize the plugin (non-route initialization only)

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

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -187,17 +187,9 @@ export default async function getServer(server: FastifyInstance) {
187187
teamCount: teamIds.length
188188
}, 'MCP server access granted');
189189

190-
// Parse JSON fields for response with proper null checks
190+
// Format dates for response - JSON fields are already parsed by the service
191191
const responseServer = {
192192
...server,
193-
installation_methods: server.installation_methods ? JSON.parse(server.installation_methods) : [],
194-
tools: server.tools ? JSON.parse(server.tools) : [],
195-
resources: server.resources ? JSON.parse(server.resources) : null,
196-
prompts: server.prompts ? JSON.parse(server.prompts) : null,
197-
default_config: server.default_config ? JSON.parse(server.default_config) : null,
198-
environment_variables: server.environment_variables ? JSON.parse(server.environment_variables) : null,
199-
dependencies: server.dependencies ? JSON.parse(server.dependencies) : null,
200-
tags: server.tags ? JSON.parse(server.tags) : null,
201193
created_at: server.created_at.toISOString(),
202194
updated_at: server.updated_at.toISOString(),
203195
last_sync_at: server.last_sync_at?.toISOString() || null

services/backend/src/services/mcpCatalogService.ts

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,8 @@ export class McpCatalogService {
227227
serversFound: servers.length
228228
}, 'Retrieved MCP servers for user');
229229

230-
return servers as McpServer[];
230+
// Parse JSON fields for all servers
231+
return servers.map((server: any) => this.parseServerJsonFields(server)) as McpServer[];
231232
}
232233

233234
async getServerById(serverId: string): Promise<McpServer | null> {
@@ -246,7 +247,7 @@ export class McpCatalogService {
246247
return null;
247248
}
248249

249-
return servers[0] as McpServer;
250+
return this.parseServerJsonFields(servers[0]) as McpServer;
250251
}
251252

252253
async createServer(
@@ -451,6 +452,86 @@ export class McpCatalogService {
451452
return true;
452453
}
453454

455+
private parseServerJsonFields(server: any): any {
456+
// Parse JSON string fields back to objects/arrays
457+
const parsed = { ...server };
458+
459+
// Parse JSON fields that should be arrays/objects
460+
try {
461+
if (parsed.installation_methods && typeof parsed.installation_methods === 'string') {
462+
parsed.installation_methods = JSON.parse(parsed.installation_methods);
463+
}
464+
} catch (e) {
465+
this.logger.warn({ field: 'installation_methods', error: e }, 'Failed to parse JSON field');
466+
parsed.installation_methods = [];
467+
}
468+
469+
try {
470+
if (parsed.tools && typeof parsed.tools === 'string') {
471+
parsed.tools = JSON.parse(parsed.tools);
472+
}
473+
} catch (e) {
474+
this.logger.warn({ field: 'tools', error: e }, 'Failed to parse JSON field');
475+
parsed.tools = [];
476+
}
477+
478+
try {
479+
if (parsed.resources && typeof parsed.resources === 'string') {
480+
parsed.resources = JSON.parse(parsed.resources);
481+
}
482+
} catch (e) {
483+
this.logger.warn({ field: 'resources', error: e }, 'Failed to parse JSON field');
484+
parsed.resources = [];
485+
}
486+
487+
try {
488+
if (parsed.prompts && typeof parsed.prompts === 'string') {
489+
parsed.prompts = JSON.parse(parsed.prompts);
490+
}
491+
} catch (e) {
492+
this.logger.warn({ field: 'prompts', error: e }, 'Failed to parse JSON field');
493+
parsed.prompts = [];
494+
}
495+
496+
try {
497+
if (parsed.environment_variables && typeof parsed.environment_variables === 'string') {
498+
parsed.environment_variables = JSON.parse(parsed.environment_variables);
499+
}
500+
} catch (e) {
501+
this.logger.warn({ field: 'environment_variables', error: e }, 'Failed to parse JSON field');
502+
parsed.environment_variables = {};
503+
}
504+
505+
try {
506+
if (parsed.default_config && typeof parsed.default_config === 'string') {
507+
parsed.default_config = JSON.parse(parsed.default_config);
508+
}
509+
} catch (e) {
510+
this.logger.warn({ field: 'default_config', error: e }, 'Failed to parse JSON field');
511+
parsed.default_config = {};
512+
}
513+
514+
try {
515+
if (parsed.dependencies && typeof parsed.dependencies === 'string') {
516+
parsed.dependencies = JSON.parse(parsed.dependencies);
517+
}
518+
} catch (e) {
519+
this.logger.warn({ field: 'dependencies', error: e }, 'Failed to parse JSON field');
520+
parsed.dependencies = {};
521+
}
522+
523+
try {
524+
if (parsed.tags && typeof parsed.tags === 'string') {
525+
parsed.tags = JSON.parse(parsed.tags);
526+
}
527+
} catch (e) {
528+
this.logger.warn({ field: 'tags', error: e }, 'Failed to parse JSON field');
529+
parsed.tags = [];
530+
}
531+
532+
return parsed;
533+
}
534+
454535
private canUserManageServer(server: McpServer, userId: string, userRole: string): boolean {
455536
// Global admin can manage any server
456537
if (userRole === 'global_admin') {

services/frontend/src/components/admin/mcp-catalog/ReviewStep.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,14 @@ const formatJson = (jsonString: string) => {
147147
:key="index"
148148
class="flex items-center gap-2 text-sm"
149149
>
150-
<Badge variant="outline" class="text-xs">{{ method.type }}</Badge>
150+
<Badge variant="outline" class="text-xs">{{ method.client || 'Unknown' }}</Badge>
151151
<code class="text-xs bg-muted px-2 py-1 rounded">{{ method.command }}</code>
152+
<span v-if="method.args && method.args.length > 0" class="text-xs text-muted-foreground">
153+
+ {{ method.args.length }} arg{{ method.args.length > 1 ? 's' : '' }}
154+
</span>
155+
<span v-if="method.env && Object.keys(method.env).length > 0" class="text-xs text-muted-foreground">
156+
+ {{ Object.keys(method.env).length }} env var{{ Object.keys(method.env).length > 1 ? 's' : '' }}
157+
</span>
152158
</div>
153159
</div>
154160
</div>

0 commit comments

Comments
 (0)