feat: non-editable bundled extensions (#2114)

This commit is contained in:
Alex Hancock
2025-04-09 17:27:40 -04:00
committed by GitHub
parent 9610406065
commit 1d74f538ef
12 changed files with 127 additions and 62 deletions
+20
View File
@@ -390,6 +390,11 @@
"type"
],
"properties": {
"bundled": {
"type": "boolean",
"description": "Whether this extension is bundled with Goose",
"nullable": true
},
"description": {
"type": "string",
"nullable": true
@@ -434,6 +439,11 @@
"type": "string"
}
},
"bundled": {
"type": "boolean",
"description": "Whether this extension is bundled with Goose",
"nullable": true
},
"cmd": {
"type": "string"
},
@@ -470,6 +480,11 @@
"type"
],
"properties": {
"bundled": {
"type": "boolean",
"description": "Whether this extension is bundled with Goose",
"nullable": true
},
"display_name": {
"type": "string",
"nullable": true
@@ -501,6 +516,11 @@
"type"
],
"properties": {
"bundled": {
"type": "boolean",
"description": "Whether this extension is bundled with Goose",
"nullable": true
},
"instructions": {
"type": "string",
"description": "Instructions for how to use these tools",
+16
View File
@@ -24,6 +24,10 @@ export type Envs = {
* Represents the different types of MCP extensions that can be added to the manager
*/
export type ExtensionConfig = {
/**
* Whether this extension is bundled with Goose
*/
bundled?: boolean | null;
description?: string | null;
envs?: Envs;
/**
@@ -35,6 +39,10 @@ export type ExtensionConfig = {
uri: string;
} | {
args: Array<string>;
/**
* Whether this extension is bundled with Goose
*/
bundled?: boolean | null;
cmd: string;
description?: string | null;
envs?: Envs;
@@ -45,6 +53,10 @@ export type ExtensionConfig = {
timeout?: number | null;
type: 'stdio';
} | {
/**
* Whether this extension is bundled with Goose
*/
bundled?: boolean | null;
display_name?: string | null;
/**
* The name used to identify this extension
@@ -53,6 +65,10 @@ export type ExtensionConfig = {
timeout?: number | null;
type: 'builtin';
} | {
/**
* Whether this extension is bundled with Goose
*/
bundled?: boolean | null;
/**
* Instructions for how to use these tools
*/
@@ -7,7 +7,8 @@
"enabled": true,
"type": "builtin",
"env_keys": [],
"timeout": 300
"timeout": 300,
"bundled": true
},
{
"id": "computercontroller",
@@ -17,7 +18,8 @@
"enabled": false,
"type": "builtin",
"env_keys": [],
"timeout": 300
"timeout": 300,
"bundled": true
},
{
"id": "memory",
@@ -27,7 +29,8 @@
"enabled": false,
"type": "builtin",
"env_keys": [],
"timeout": 300
"timeout": 300,
"bundled": true
},
{
"id": "jetbrains",
@@ -37,7 +40,8 @@
"enabled": false,
"type": "builtin",
"env_keys": [],
"timeout": 300
"timeout": 300,
"bundled": true
},
{
"id": "tutorial",
@@ -46,6 +50,7 @@
"description": "Access interactive tutorials and guides",
"enabled": false,
"type": "builtin",
"env_keys": []
"env_keys": [],
"bundled": true
}
]
@@ -33,68 +33,58 @@ export async function syncBundledExtensions(
addExtensionFn: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>
): Promise<void> {
try {
// Create a set of existing extension IDs for quick lookup
const existingExtensionKeys = new Set(existingExtensions.map((ext) => nameToKey(ext.name)));
// Cast the imported JSON data to the expected type
const bundledExtensions = bundledExtensionsData as BundledExtension[];
// Track how many extensions were added
let addedCount = 0;
// Check each built-in extension
// Process each bundled extension
for (const bundledExt of bundledExtensions) {
// Only add if the extension doesn't already exist -- use the id
if (!existingExtensionKeys.has(bundledExt.id)) {
console.log(`Adding built-in extension: ${bundledExt.id}`);
let extConfig: ExtensionConfig;
switch (bundledExt.type) {
case 'builtin':
extConfig = {
name: bundledExt.name,
display_name: bundledExt.display_name,
type: bundledExt.type,
timeout: bundledExt.timeout ?? 300,
};
break;
case 'stdio':
extConfig = {
name: bundledExt.name,
description: bundledExt.description,
type: bundledExt.type,
timeout: bundledExt.timeout,
cmd: bundledExt.cmd,
args: bundledExt.args,
envs: bundledExt.envs,
};
break;
case 'sse':
extConfig = {
name: bundledExt.name,
description: bundledExt.description,
type: bundledExt.type,
timeout: bundledExt.timeout,
uri: bundledExt.uri,
};
}
// Add the extension with its default enabled state
try {
await addExtensionFn(bundledExt.name, extConfig, bundledExt.enabled);
addedCount++;
} catch (error) {
console.error(`Failed to add built-in extension ${bundledExt.name}:`, error);
// Continue with other extensions even if one fails
}
}
}
// Find if this extension already exists
const existingExt = existingExtensions.find((ext) => nameToKey(ext.name) === bundledExt.id);
if (addedCount > 0) {
console.log(`Added ${addedCount} built-in extensions.`);
} else {
console.log('All built-in extensions already present.');
// Skip if extension exists and is already marked as bundled
if (existingExt?.bundled) continue;
// Create the config for this extension
let extConfig: ExtensionConfig;
switch (bundledExt.type) {
case 'builtin':
extConfig = {
name: bundledExt.name,
display_name: bundledExt.display_name,
type: bundledExt.type,
timeout: bundledExt.timeout ?? 300,
bundled: true,
};
break;
case 'stdio':
extConfig = {
name: bundledExt.name,
description: bundledExt.description,
type: bundledExt.type,
timeout: bundledExt.timeout,
cmd: bundledExt.cmd,
args: bundledExt.args,
envs: bundledExt.envs,
bundled: true,
};
break;
case 'sse':
extConfig = {
name: bundledExt.name,
description: bundledExt.description,
type: bundledExt.type,
timeout: bundledExt.timeout,
uri: bundledExt.uri,
bundled: true,
};
}
// Add or update the extension, preserving enabled state if it exists
const enabled = existingExt ? existingExt.enabled : bundledExt.enabled;
await addExtensionFn(bundledExt.name, extConfig, enabled);
}
} catch (error) {
console.error('Failed to add built-in extensions:', error);
console.error('Failed to sync built-in extensions:', error);
throw error;
}
}
@@ -56,6 +56,10 @@ export default function ExtensionItem({ extension, onToggle, onConfigure }: Exte
));
};
// Bundled extensions and builtins are not editable
// Over time we can take the first part of the conditional away as people have bundled: true in their config.yaml entries
const editable = !(extension.type === 'builtin' || extension.bundled);
return (
<div
className="flex justify-between rounded-lg transition-colors border border-borderSubtle p-4 pt-3 hover:border-borderProminent hover:cursor-pointer"
@@ -70,8 +74,7 @@ export default function ExtensionItem({ extension, onToggle, onConfigure }: Exte
className="flex items-center justify-end gap-2 w-max-[10%]"
onClick={(e) => e.stopPropagation()}
>
{/* Only show config button for non-builtin extensions */}
{extension.type !== 'builtin' && (
{editable && (
<button
className="text-textSubtle hover:text-textStandard"
onClick={() => onConfigure(extension)}