|  | 
| 33 | 33 | 	import AuditLogsPageContent from './audit-logs/AuditLogsPageContent.svelte'; | 
| 34 | 34 | 	import { page } from '$app/state'; | 
| 35 | 35 | 	import { getRegistryLabel, openUrl } from '$lib/utils'; | 
| 36 |  | -	import CatalogConfigureForm, { type LaunchFormData } from '../mcp/CatalogConfigureForm.svelte'; | 
|  | 36 | +	import CatalogConfigureForm, { | 
|  | 37 | +		type LaunchFormData, | 
|  | 38 | +		type CompositeLaunchFormData, | 
|  | 39 | +		type ComponentLaunchFormData | 
|  | 40 | +	} from '../mcp/CatalogConfigureForm.svelte'; | 
| 37 | 41 | 	import ResponsiveDialog from '../ResponsiveDialog.svelte'; | 
| 38 | 42 | 	import { setVirtualPageDisabled } from '../ui/virtual-page/context'; | 
| 39 | 43 | 	import { profile } from '$lib/stores'; | 
| 40 | 44 | 	import OverflowContainer from '../OverflowContainer.svelte'; | 
| 41 | 45 | 
 | 
| 42 |  | -	type MCPType = 'single' | 'multi' | 'remote'; | 
|  | 46 | +	type MCPType = 'single' | 'multi' | 'remote' | 'composite'; | 
| 43 | 47 | 
 | 
| 44 | 48 | 	interface Props { | 
| 45 | 49 | 		id?: string; | 
|  | 
| 114 | 118 | 	let oauthURL = $state<string>(); | 
| 115 | 119 | 
 | 
| 116 | 120 | 	let configDialog = $state<ReturnType<typeof CatalogConfigureForm>>(); | 
| 117 |  | -	let configureForm = $state<LaunchFormData>(); | 
|  | 121 | +	let configureForm = $state<LaunchFormData | CompositeLaunchFormData>(); | 
| 118 | 122 | 	let saving = $state(false); | 
| 119 | 123 | 	let error = $state<string>(); | 
| 120 | 124 | 	let showButtonInlineError = $state(false); | 
|  | 
| 219 | 223 | 	} | 
| 220 | 224 | 
 | 
| 221 | 225 | 	function compileTemporaryInstanceBody() { | 
|  | 226 | +		if (configureForm && 'componentConfigs' in (configureForm as any)) { | 
|  | 227 | +			const body: { | 
|  | 228 | +				componentConfigs: Record< | 
|  | 229 | +					string, | 
|  | 230 | +					{ config: Record<string, string>; url: string; enabled: boolean } | 
|  | 231 | +				>; | 
|  | 232 | +			} = { componentConfigs: {} }; | 
|  | 233 | +			const composite = configureForm as CompositeLaunchFormData; | 
|  | 234 | +			for (const [compId, comp] of Object.entries(composite.componentConfigs)) { | 
|  | 235 | +				const cfg: Record<string, string> = {}; | 
|  | 236 | +				for (const f of comp.envs || []) if (f.value) cfg[f.key] = f.value; | 
|  | 237 | +				for (const f of comp.headers || []) if (f.value) cfg[f.key] = f.value; | 
|  | 238 | +				body.componentConfigs[compId] = { | 
|  | 239 | +					config: cfg, | 
|  | 240 | +					url: comp.url || '', | 
|  | 241 | +					enabled: !!comp.enabled | 
|  | 242 | +				}; | 
|  | 243 | +			} | 
|  | 244 | +			return body; | 
|  | 245 | +		} | 
| 222 | 246 | 		return { | 
| 223 |  | -			url: configureForm?.url, | 
| 224 |  | -			config: [...(configureForm?.headers ?? []), ...(configureForm?.envs ?? [])].reduce< | 
| 225 |  | -				Record<string, string> | 
| 226 |  | -			>((acc, curr) => { | 
|  | 247 | +			url: (configureForm as LaunchFormData)?.url, | 
|  | 248 | +			config: [ | 
|  | 249 | +				...((configureForm as LaunchFormData)?.headers ?? []), | 
|  | 250 | +				...((configureForm as LaunchFormData)?.envs ?? []) | 
|  | 251 | +			].reduce<Record<string, string>>((acc, curr) => { | 
| 227 | 252 | 				acc[curr.key] = curr.value; | 
| 228 | 253 | 				return acc; | 
| 229 | 254 | 			}, {}) | 
|  | 
| 257 | 282 | 				entity === 'workspace' | 
| 258 | 283 | 					? ChatService.generateWorkspaceMCPCatalogEntryToolPreviews | 
| 259 | 284 | 					: AdminService.generateMcpCatalogEntryToolPreviews; | 
| 260 |  | -			await generateToolsFn(id, entry.id, body); | 
|  | 285 | +			await (generateToolsFn as any)(id, entry.id, body as any); | 
| 261 | 286 | 			window.location.reload(); | 
| 262 | 287 | 		} catch (err) { | 
| 263 | 288 | 			const errMessage = err instanceof Error ? err.message : 'An unknown error occurred'; | 
|  | 
| 266 | 291 | 					entity === 'workspace' | 
| 267 | 292 | 						? ChatService.getWorkspaceMCPCatalogEntryToolPreviewsOauth | 
| 268 | 293 | 						: AdminService.getMcpCatalogToolPreviewsOauth; | 
| 269 |  | -				const oauthResponse = await getOauthFn(id, entry.id, body); | 
|  | 294 | +				const oauthResponse = await (getOauthFn as any)(id, entry.id, body as any); | 
| 270 | 295 | 				if (oauthResponse) { | 
| 271 | 296 | 					configDialog?.close(); | 
| 272 | 297 | 					handleTemporaryInstanceOauth(oauthResponse); | 
|  | 
| 283 | 308 | 	function handleInitTemporaryInstance() { | 
| 284 | 309 | 		if (!entry) return; | 
| 285 | 310 | 
 | 
|  | 311 | +		if (entry.manifest?.runtime === 'composite') { | 
|  | 312 | +			const comps = entry.manifest?.compositeConfig?.componentServers || []; | 
|  | 313 | +			const componentConfigs: Record<string, ComponentLaunchFormData> = {}; | 
|  | 314 | +			for (const c of comps) { | 
|  | 315 | +				const rc: any = c.manifest?.remoteConfig; | 
|  | 316 | +				const hasHostname = rc && 'hostname' in rc && rc.hostname; | 
|  | 317 | +				componentConfigs[c.catalogEntryID] = { | 
|  | 318 | +					envs: (c.manifest?.env || []).map((e) => ({ ...e, value: '' })), | 
|  | 319 | +					headers: (c.manifest?.remoteConfig?.headers || []).map((h) => ({ ...h, value: '' })), | 
|  | 320 | +					...(hasHostname ? { hostname: rc.hostname as string, url: '' } : {}), | 
|  | 321 | +					name: c.manifest?.name || c.catalogEntryID, | 
|  | 322 | +					icon: c.manifest?.icon, | 
|  | 323 | +					enabled: true | 
|  | 324 | +				}; | 
|  | 325 | +			} | 
|  | 326 | +			configureForm = { componentConfigs } as CompositeLaunchFormData; | 
|  | 327 | +
 | 
|  | 328 | +			const needsCompositeConfig = comps.some((c) => { | 
|  | 329 | +				const envs = c.manifest?.env || []; | 
|  | 330 | +				const headers = c.manifest?.remoteConfig?.headers || []; | 
|  | 331 | +				const hasReqEnv = envs.some((e) => e.required); | 
|  | 332 | +				const hasReqHeader = headers.some((h) => h.required); | 
|  | 333 | +				const rc: any = c.manifest?.remoteConfig; | 
|  | 334 | +				const hasHostname = rc && 'hostname' in rc && rc.hostname; | 
|  | 335 | +				const hasFixed = rc && 'fixedURL' in rc && rc.fixedURL; | 
|  | 336 | +				const tmpl = rc && 'urlTemplate' in rc && rc.urlTemplate ? (rc.urlTemplate as string) : ''; | 
|  | 337 | +				const needsUrl = hasHostname && !hasFixed && !tmpl; | 
|  | 338 | +				const needsTemplateVars = /\$\{[^}]+\}/.test(tmpl); | 
|  | 339 | +				return hasReqEnv || hasReqHeader || needsUrl || needsTemplateVars; | 
|  | 340 | +			}); | 
|  | 341 | +
 | 
|  | 342 | +			if (needsCompositeConfig) { | 
|  | 343 | +				configDialog?.open(); | 
|  | 344 | +			} else { | 
|  | 345 | +				handleLaunchTemporaryInstance(true); | 
|  | 346 | +			} | 
|  | 347 | +			return; | 
|  | 348 | +		} | 
|  | 349 | +
 | 
| 286 | 350 | 		const hostname = | 
| 287 | 351 | 			entry?.manifest?.remoteConfig && | 
| 288 | 352 | 			'hostname' in entry.manifest.remoteConfig && | 
|  | 
| 304 | 368 | 		const needsEnvValue = configureForm.envs?.some((env) => !env.value); | 
| 305 | 369 | 		const needsHeaderValue = configureForm.headers?.some((header) => !header.value); | 
| 306 | 370 | 		const hasConfigFields = | 
| 307 |  | -			type !== 'multi' && (needsEnvValue || needsHeaderValue || configureForm.hostname); | 
|  | 371 | +			type !== 'multi' && | 
|  | 372 | +			(needsEnvValue || needsHeaderValue || (configureForm as LaunchFormData).hostname); | 
| 308 | 373 | 		if (hasConfigFields) { | 
| 309 | 374 | 			configDialog?.open(); | 
| 310 | 375 | 		} else { | 
|  | 
0 commit comments