Skip to content

Commit ad287e4

Browse files
author
Lasim
committed
feat(all): add real-time MCP server config updates with automatic stdio restart
1 parent cee81fb commit ad287e4

File tree

18 files changed

+793
-104
lines changed

18 files changed

+793
-104
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

services/backend/api-spec.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17843,6 +17843,10 @@
1784317843
"description": {
1784417844
"type": "string",
1784517845
"description": "Optional description of the template argument"
17846+
},
17847+
"order": {
17848+
"type": "number",
17849+
"description": "Order position for STDIO argument ordering"
1784617850
}
1784717851
},
1784817852
"required": [
@@ -17995,6 +17999,10 @@
1799517999
"max_items": {
1799618000
"type": "number",
1799718001
"description": "Maximum number of items (for arrays)"
18002+
},
18003+
"order": {
18004+
"type": "number",
18005+
"description": "Order position for STDIO argument ordering"
1799818006
}
1799918007
},
1800018008
"required": [
@@ -18206,6 +18214,10 @@
1820618214
"max_items": {
1820718215
"type": "number",
1820818216
"description": "Maximum number of items (for arrays)"
18217+
},
18218+
"order": {
18219+
"type": "number",
18220+
"description": "Order position for STDIO argument ordering"
1820918221
}
1821018222
},
1821118223
"required": [
@@ -19296,6 +19308,10 @@
1929619308
"description": {
1929719309
"type": "string",
1929819310
"description": "Optional description of the template argument"
19311+
},
19312+
"order": {
19313+
"type": "number",
19314+
"description": "Order position for STDIO argument ordering"
1929919315
}
1930019316
},
1930119317
"required": [
@@ -19448,6 +19464,10 @@
1944819464
"max_items": {
1944919465
"type": "number",
1945019466
"description": "Maximum number of items (for arrays)"
19467+
},
19468+
"order": {
19469+
"type": "number",
19470+
"description": "Order position for STDIO argument ordering"
1945119471
}
1945219472
},
1945319473
"required": [
@@ -19659,6 +19679,10 @@
1965919679
"max_items": {
1966019680
"type": "number",
1966119681
"description": "Maximum number of items (for arrays)"
19682+
},
19683+
"order": {
19684+
"type": "number",
19685+
"description": "Order position for STDIO argument ordering"
1966219686
}
1966319687
},
1966419688
"required": [

services/backend/api-spec.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12486,6 +12486,9 @@ paths:
1248612486
description:
1248712487
type: string
1248812488
description: Optional description of the template argument
12489+
order:
12490+
type: number
12491+
description: Order position for STDIO argument ordering
1248912492
required:
1249012493
- value
1249112494
- locked
@@ -12600,6 +12603,9 @@ paths:
1260012603
max_items:
1260112604
type: number
1260212605
description: Maximum number of items (for arrays)
12606+
order:
12607+
type: number
12608+
description: Order position for STDIO argument ordering
1260312609
required:
1260412610
- name
1260512611
- type
@@ -12763,6 +12769,9 @@ paths:
1276312769
max_items:
1276412770
type: number
1276512771
description: Maximum number of items (for arrays)
12772+
order:
12773+
type: number
12774+
description: Order position for STDIO argument ordering
1276612775
required:
1276712776
- name
1276812777
- type
@@ -13574,6 +13583,9 @@ paths:
1357413583
description:
1357513584
type: string
1357613585
description: Optional description of the template argument
13586+
order:
13587+
type: number
13588+
description: Order position for STDIO argument ordering
1357713589
required:
1357813590
- value
1357913591
- locked
@@ -13688,6 +13700,9 @@ paths:
1368813700
max_items:
1368913701
type: number
1369013702
description: Maximum number of items (for arrays)
13703+
order:
13704+
type: number
13705+
description: Order position for STDIO argument ordering
1369113706
required:
1369213707
- name
1369313708
- type
@@ -13851,6 +13866,9 @@ paths:
1385113866
max_items:
1385213867
type: number
1385313868
description: Maximum number of items (for arrays)
13869+
order:
13870+
type: number
13871+
description: Order position for STDIO argument ordering
1385413872
required:
1385513873
- name
1385613874
- type

services/backend/src/routes/gateway/me-mcp-configurations.ts

Lines changed: 61 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ export default async function gatewayMeMcpConfigurationsRoute(server: FastifyIns
200200
try {
201201
// Parse base configuration from packages
202202
const packages = JSON.parse(server.packages || '[]');
203-
203+
204204
if (!packages || packages.length === 0) {
205205
request.log.warn({
206206
serverId: server.id,
@@ -220,10 +220,37 @@ export default async function gatewayMeMcpConfigurationsRoute(server: FastifyIns
220220

221221
// Start with base configuration from package transport
222222
let finalCommand = packageConfig.transport.command || 'npx';
223-
let finalArgs = [...(packageConfig.transport.args || [])];
224223
let finalEnv = { ...(packageConfig.transport.env || {}) };
225224

226-
// Apply team configuration with proper decryption
225+
// Build args from three-tier configuration with proper order
226+
interface ArgItem {
227+
value: string;
228+
order: number;
229+
source: 'template' | 'team' | 'user';
230+
}
231+
const orderedArgs: ArgItem[] = [];
232+
233+
// 1. Parse template_args (fixed values with order)
234+
try {
235+
const templateArgs = JSON.parse(server.template_args || '[]');
236+
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
237+
templateArgs.forEach((arg: any, index: number) => {
238+
if (arg.value !== undefined && arg.value !== '') {
239+
orderedArgs.push({
240+
value: arg.value,
241+
order: arg.order ?? index,
242+
source: 'template'
243+
});
244+
}
245+
});
246+
} catch (error) {
247+
request.log.warn({
248+
serverId: server.id,
249+
error: error instanceof Error ? error.message : String(error)
250+
}, 'Failed to parse template_args');
251+
}
252+
253+
// 2. Parse team_args_schema and get values from installation.team_args
227254
if (installation.team_args) {
228255
try {
229256
const teamArgsSchema = JSON.parse(server.team_args_schema || '[]');
@@ -233,29 +260,29 @@ export default async function gatewayMeMcpConfigurationsRoute(server: FastifyIns
233260
{ maskSecrets: false }, // Decrypt secrets for gateway
234261
request.log
235262
);
236-
237-
// Apply decrypted team arguments
238-
if (Array.isArray(decryptedTeamArgs) && decryptedTeamArgs.length > 0) {
239-
// Replace args based on team_args_schema
240-
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
241-
teamArgsSchema.forEach((schema: any, index: number) => {
242-
if (decryptedTeamArgs[index] !== undefined) {
243-
// Find the argument in finalArgs and replace it
244-
const argIndex = finalArgs.findIndex(arg => arg === schema.name);
245-
if (argIndex !== -1) {
246-
finalArgs[argIndex + 1] = decryptedTeamArgs[index]; // Replace the value after the argument name
247-
}
248-
}
249-
});
250-
}
263+
264+
// Map decrypted values back with their schema order
265+
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
266+
teamArgsSchema.forEach((schema: any, index: number) => {
267+
const value = decryptedTeamArgs[index];
268+
if (value !== undefined && value !== '') {
269+
orderedArgs.push({
270+
value: value,
271+
order: schema.order ?? (100 + index), // Team args after template args
272+
source: 'team'
273+
});
274+
}
275+
});
251276
} catch (error) {
252277
request.log.warn({
253278
serverId: server.id,
254279
error: error instanceof Error ? error.message : String(error)
255-
}, 'Failed to decrypt and parse team_args');
280+
}, 'Failed to decrypt and parse team_args for ordering');
256281
}
257282
}
258283

284+
// NOTE: Don't sort yet - user args will be added below with their order values
285+
259286
// Apply team environment variables with proper decryption
260287
if (installation.team_env) {
261288
try {
@@ -296,7 +323,7 @@ export default async function gatewayMeMcpConfigurationsRoute(server: FastifyIns
296323
let configStatus: 'ready' | 'invalid' = 'ready';
297324

298325
if (userConfig) {
299-
// Apply user args with proper decryption
326+
// Apply user args with proper decryption and ordering
300327
if (userConfig.user_args) {
301328
try {
302329
const userArgsSchema = JSON.parse(server.user_args_schema || '[]');
@@ -306,18 +333,17 @@ export default async function gatewayMeMcpConfigurationsRoute(server: FastifyIns
306333
{ maskSecrets: false }, // Decrypt secrets for gateway
307334
request.log
308335
);
309-
310-
// Replace user-specific arguments
336+
337+
// Add user args to orderedArgs with their schema order
311338
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
312-
userArgsSchema.forEach((schema: any) => {
313-
if (decryptedUserArgs[schema.name] !== undefined) {
314-
const argIndex = finalArgs.findIndex(arg => arg === schema.name);
315-
if (argIndex !== -1) {
316-
finalArgs[argIndex] = decryptedUserArgs[schema.name];
317-
} else {
318-
// Add new argument if not found
319-
finalArgs.push(decryptedUserArgs[schema.name]);
320-
}
339+
userArgsSchema.forEach((schema: any, index: number) => {
340+
const value = decryptedUserArgs[schema.name];
341+
if (value !== undefined && value !== '') {
342+
orderedArgs.push({
343+
value: value,
344+
order: schema.order ?? (200 + index), // User args after template and team
345+
source: 'user' as const
346+
});
321347
} else if (schema.required) {
322348
configStatus = 'invalid';
323349
request.log.warn({
@@ -372,6 +398,10 @@ export default async function gatewayMeMcpConfigurationsRoute(server: FastifyIns
372398
}
373399
}
374400

401+
// Sort all args by order and extract values
402+
orderedArgs.sort((a, b) => a.order - b.order);
403+
const finalArgs = orderedArgs.map(arg => arg.value);
404+
375405
servers.push({
376406
id: server.id,
377407
name: server.name,

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

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ const COMMON_FIELDS = {
2323
default_team_locked: { type: 'boolean', description: 'Whether this configuration is locked at team level by default' },
2424
visible_to_users: { type: 'boolean', description: 'Whether this configuration is visible to team users' },
2525
min_items: { type: 'number', description: 'Minimum number of items (for arrays)' },
26-
max_items: { type: 'number', description: 'Maximum number of items (for arrays)' }
26+
max_items: { type: 'number', description: 'Maximum number of items (for arrays)' },
27+
order: { type: 'number', description: 'Order position for STDIO argument ordering' }
2728
} as const;
2829

2930
// Schema factory functions to reduce duplication
@@ -214,17 +215,21 @@ export const LIST_SERVERS_QUERY_SCHEMA = {
214215
export const TEMPLATE_ARG_SCHEMA = {
215216
type: 'object',
216217
properties: {
217-
value: {
218+
value: {
218219
type: 'string',
219220
description: 'Template argument value'
220221
},
221-
locked: {
222+
locked: {
222223
type: 'boolean',
223224
description: 'Whether this template argument is locked'
224225
},
225-
description: {
226+
description: {
226227
type: 'string',
227228
description: 'Optional description of the template argument'
229+
},
230+
order: {
231+
type: 'number',
232+
description: 'Order position for STDIO argument ordering'
228233
}
229234
},
230235
required: ['value', 'locked'],
@@ -315,7 +320,8 @@ const TEAM_ADDITIONAL_FIELDS = {
315320
export const TEAM_ARG_SCHEMA = createConfigSchema('team argument', {
316321
...TEAM_ADDITIONAL_FIELDS,
317322
min_items: COMMON_FIELDS.min_items,
318-
max_items: COMMON_FIELDS.max_items
323+
max_items: COMMON_FIELDS.max_items,
324+
order: COMMON_FIELDS.order
319325
}, ['default_team_locked']);
320326

321327
export const TEAM_ENV_SCHEMA = createConfigSchema('team environment variable', TEAM_ADDITIONAL_FIELDS, ['default_team_locked', 'visible_to_users']);
@@ -327,7 +333,8 @@ export const TEAM_URL_QUERY_PARAM_SCHEMA = createConfigSchema('team URL query pa
327333
// User level - basic structure
328334
export const USER_ARG_SCHEMA = createConfigSchema('user argument', {
329335
min_items: COMMON_FIELDS.min_items,
330-
max_items: COMMON_FIELDS.max_items
336+
max_items: COMMON_FIELDS.max_items,
337+
order: COMMON_FIELDS.order
331338
});
332339

333340
export const USER_ENV_SCHEMA = createConfigSchema('user environment variable');
@@ -1417,6 +1424,7 @@ export interface TemplateArg {
14171424
value: string;
14181425
locked: boolean;
14191426
description?: string;
1427+
order?: number;
14201428
}
14211429

14221430
export interface TemplateEnv extends BaseConfig {
@@ -1439,6 +1447,7 @@ export interface TeamConfig extends BaseConfig {
14391447
export interface TeamArg extends TeamConfig {
14401448
min_items?: number;
14411449
max_items?: number;
1450+
order?: number;
14421451
}
14431452

14441453
// TeamEnv, TeamHeader, and TeamUrlQueryParam are just aliases for TeamConfig
@@ -1449,14 +1458,15 @@ export type TeamUrlQueryParam = TeamConfig;
14491458
export interface UserConfig extends BaseConfig {
14501459
min_items?: number;
14511460
max_items?: number;
1461+
order?: number;
14521462
}
14531463

14541464
// UserArg is just an alias for UserConfig
14551465
export type UserArg = UserConfig;
1456-
// UserEnv, UserHeader, and UserUrlQueryParam omit the array-specific fields
1457-
export type UserEnv = Omit<UserConfig, 'min_items' | 'max_items'>;
1458-
export type UserHeader = Omit<UserConfig, 'min_items' | 'max_items'>;
1459-
export type UserUrlQueryParam = Omit<UserConfig, 'min_items' | 'max_items'>;
1466+
// UserEnv, UserHeader, and UserUrlQueryParam omit the array-specific fields and order (order is for args only)
1467+
export type UserEnv = Omit<UserConfig, 'min_items' | 'max_items' | 'order'>;
1468+
export type UserHeader = Omit<UserConfig, 'min_items' | 'max_items' | 'order'>;
1469+
export type UserUrlQueryParam = Omit<UserConfig, 'min_items' | 'max_items' | 'order'>;
14601470

14611471
export interface ConfigurationSchema {
14621472
template_args?: TemplateArg[];

0 commit comments

Comments
 (0)