feat: upgrade to rmcp 0.12.0 and sacp 10.0.0 by removing SSE transport (#6304)

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Adrian Cole
2026-01-02 07:07:53 +08:00
committed by GitHub
parent 38fcf4f374
commit 5a5312a2e6
51 changed files with 508 additions and 931 deletions
+5 -9
View File
@@ -230,18 +230,10 @@ export type ErrorResponse = {
* Represents the different types of MCP extensions that can be added to the manager
*/
export type ExtensionConfig = {
available_tools?: Array<string>;
bundled?: boolean | null;
description: string;
env_keys?: Array<string>;
envs?: Envs;
/**
* The name used to identify this extension
*/
name: string;
timeout?: number | null;
type: 'sse';
uri: string;
uri?: string | null;
} | {
args: Array<string>;
available_tools?: Array<string>;
@@ -351,6 +343,7 @@ export type ExtensionQuery = {
export type ExtensionResponse = {
extensions: Array<ExtensionEntry>;
warnings?: Array<string>;
};
export type FrontendToolRequest = {
@@ -667,6 +660,9 @@ export type RawImageContent = {
};
export type RawResource = {
_meta?: {
[key: string]: unknown;
};
description?: string;
icons?: Array<Icon>;
mimeType?: string;
@@ -31,6 +31,7 @@ interface ConfigContextType {
config: ConfigResponse['config'];
providersList: ProviderDetails[];
extensionsList: FixedExtensionEntry[];
extensionWarnings: string[];
upsert: (key: string, value: unknown, is_secret: boolean) => Promise<void>;
read: (key: string, is_secret: boolean) => Promise<unknown>;
remove: (key: string, is_secret: boolean) => Promise<void>;
@@ -62,6 +63,7 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
const [config, setConfig] = useState<ConfigResponse['config']>({});
const [providersList, setProvidersList] = useState<ProviderDetails[]>([]);
const [extensionsList, setExtensionsList] = useState<FixedExtensionEntry[]>([]);
const [extensionWarnings, setExtensionWarnings] = useState<string[]>([]);
const reloadConfig = useCallback(async () => {
const response = await readAllConfig();
@@ -116,6 +118,7 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
const extensionResponse: ExtensionResponse = result.data!;
setExtensionsList(extensionResponse.extensions);
setExtensionWarnings(extensionResponse.warnings || []);
return extensionResponse.extensions;
}, [extensionsList]);
@@ -216,6 +219,7 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
try {
const extensionsResponse = await apiGetExtensions();
setExtensionsList(extensionsResponse.data?.extensions || []);
setExtensionWarnings(extensionsResponse.data?.warnings || []);
} catch (error) {
console.error('Failed to load extensions:', error);
}
@@ -244,6 +248,7 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
config,
providersList,
extensionsList,
extensionWarnings,
upsert,
read,
remove,
@@ -260,6 +265,7 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
config,
providersList,
extensionsList,
extensionWarnings,
upsert,
read,
remove,
@@ -1,6 +1,6 @@
import { useEffect, useState, useCallback, useMemo } from 'react';
import { Button } from '../../ui/button';
import { Plus } from 'lucide-react';
import { Plus, AlertTriangle } from 'lucide-react';
import { GPSIcon } from '../../ui/icons';
import { useConfig, FixedExtensionEntry } from '../../ConfigContext';
import ExtensionList from './subcomponents/ExtensionList';
@@ -38,7 +38,7 @@ export default function ExtensionsSection({
onModalClose,
searchTerm = '',
}: ExtensionSectionProps) {
const { getExtensions, addExtension, removeExtension, extensionsList } = useConfig();
const { getExtensions, addExtension, removeExtension, extensionsList, extensionWarnings } = useConfig();
const [selectedExtension, setSelectedExtension] = useState<FixedExtensionEntry | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
@@ -234,6 +234,22 @@ export default function ExtensionsSection({
return (
<section id="extensions">
<div className="">
{/* Unsupported extension warnings */}
{extensionWarnings.length > 0 && (
<div className="mb-4 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
<div className="flex items-start gap-2">
<AlertTriangle className="h-5 w-5 text-yellow-500 flex-shrink-0 mt-0.5" />
<div className="text-sm text-yellow-500">
{extensionWarnings.map((warning, index) => (
<p key={index} className={index > 0 ? 'mt-1' : ''}>
{warning}
</p>
))}
</div>
</div>
</div>
)}
<ExtensionList
extensions={extensions}
onToggle={handleExtensionToggle}
@@ -10,7 +10,7 @@ type BundledExtension = {
display_name?: string;
description: string;
enabled: boolean;
type: 'builtin' | 'stdio' | 'sse';
type: 'builtin' | 'stdio' | 'streamable_http';
cmd?: string;
args?: string[];
uri?: string;
@@ -80,7 +80,7 @@ export async function syncBundledExtensions(
bundled: true,
};
break;
case 'sse':
case 'streamable_http':
extConfig = {
type: bundledExt.type,
name: bundledExt.name,
@@ -64,21 +64,6 @@ function getStdioConfig(
return config;
}
/**
* Build an extension config for SSE from the deeplink URL
*/
function getSseConfig(remoteUrl: string, name: string, description: string, timeout: number) {
const config: ExtensionConfig = {
name,
type: 'sse',
uri: remoteUrl,
description,
timeout: timeout,
};
return config;
}
/**
* Build an extension config for Streamable HTTP from the deeplink URL
*/
@@ -150,9 +135,6 @@ export async function addExtensionFromDeepLink(
const cmd = parsedUrl.searchParams.get('cmd');
const remoteUrl = parsedUrl.searchParams.get('url');
// Support both 'transport' and 'type' parameters for consistency
const transportType =
parsedUrl.searchParams.get('transport') || parsedUrl.searchParams.get('type') || 'sse'; // Default to SSE for backward compatibility
const headerParams = parsedUrl.searchParams.getAll('header');
const headers =
@@ -178,9 +160,7 @@ export async function addExtensionFromDeepLink(
: undefined;
const baseConfig = remoteUrl
? transportType === 'streamable_http'
? getStreamableHttpConfig(remoteUrl, name, description || '', timeout, headers, envs)
: getSseConfig(remoteUrl, name, description || '', timeout)
? getStreamableHttpConfig(remoteUrl, name, description || '', timeout, headers, envs)
: getStdioConfig(cmd!, parsedUrl, name, description || '', timeout);
const config = {
@@ -48,10 +48,10 @@ export default function ExtensionInfoFields({
label:
type === 'stdio'
? 'STDIO'
: type === 'sse'
? 'SSE'
: type === 'streamable_http'
? 'HTTP'
: type === 'streamable_http'
? 'HTTP'
: type === 'sse'
? 'SSE (unsupported)'
: type.toUpperCase(),
}}
onChange={(newValue: unknown) => {
@@ -62,7 +62,6 @@ export default function ExtensionInfoFields({
}}
options={[
{ value: 'stdio', label: 'Standard IO (STDIO)' },
{ value: 'sse', label: 'Server-Sent Events (SSE)' },
{ value: 'streamable_http', label: 'Streamable HTTP' },
]}
isSearchable={false}
@@ -79,31 +79,6 @@ describe('Extension Utils', () => {
});
});
it('should convert sse extension to form data', () => {
const extension: FixedExtensionEntry = {
type: 'sse',
name: 'sse-extension',
description: 'SSE description',
uri: 'http://localhost:8080/events',
enabled: false,
env_keys: ['TOKEN'],
};
const formData = extensionToFormData(extension);
expect(formData).toEqual({
name: 'sse-extension',
description: 'SSE description',
type: 'sse',
cmd: undefined,
endpoint: 'http://localhost:8080/events',
enabled: false,
timeout: undefined,
envVars: [{ key: 'TOKEN', value: '••••••••', isEdited: false }],
headers: [],
});
});
it('should convert streamable_http extension to form data', () => {
const extension: FixedExtensionEntry = {
type: 'streamable_http',
@@ -214,31 +189,6 @@ describe('Extension Utils', () => {
});
});
it('should create sse extension config', () => {
const formData = {
name: 'test-sse',
description: 'Test SSE extension',
type: 'sse' as const,
cmd: '',
endpoint: 'http://localhost:8080/events',
enabled: true,
timeout: 600,
envVars: [{ key: 'TOKEN', value: 'abc123', isEdited: true }],
headers: [],
};
const config = createExtensionConfig(formData);
expect(config).toEqual({
type: 'sse',
name: 'test-sse',
description: 'Test SSE extension',
timeout: 600,
uri: 'http://localhost:8080/events',
env_keys: ['TOKEN'],
});
});
it('should create streamable_http extension config', () => {
const formData = {
name: 'test-http',
@@ -55,8 +55,7 @@ export function getDefaultFormData(): ExtensionFormData {
export function extensionToFormData(extension: FixedExtensionEntry): ExtensionFormData {
// Type guard: Check if 'envs' property exists for this variant
const hasEnvs =
extension.type === 'sse' || extension.type === 'streamable_http' || extension.type === 'stdio';
const hasEnvs = extension.type === 'streamable_http' || extension.type === 'stdio';
// Handle both envs (legacy) and env_keys (new secrets)
let envVars = [];
@@ -106,7 +105,9 @@ export function extensionToFormData(extension: FixedExtensionEntry): ExtensionFo
: extension.type,
cmd: extension.type === 'stdio' ? combineCmdAndArgs(extension.cmd, extension.args) : undefined,
endpoint:
extension.type === 'sse' || extension.type === 'streamable_http' ? extension.uri : undefined,
extension.type === 'streamable_http' || extension.type === 'sse'
? (extension.uri ?? undefined)
: undefined,
enabled: extension.enabled,
timeout: 'timeout' in extension ? (extension.timeout ?? undefined) : undefined,
envVars,
@@ -134,15 +135,6 @@ export function createExtensionConfig(formData: ExtensionFormData): ExtensionCon
timeout: formData.timeout,
...(env_keys.length > 0 ? { env_keys } : {}),
};
} else if (formData.type === 'sse') {
return {
type: 'sse',
name: formData.name,
description: formData.description,
timeout: formData.timeout,
uri: formData.endpoint || '',
...(env_keys.length > 0 ? { env_keys } : {}),
};
} else if (formData.type === 'streamable_http') {
// Extract headers
const headers = formData.headers
+13
View File
@@ -106,6 +106,19 @@ export const initializeSystem = async (
const extensionLoadingPromises = enabledExtensions.map(async (extensionConfig) => {
const extensionName = extensionConfig.name;
// SSE is unsupported - fail immediately without calling the backend
if (extensionConfig.type === 'sse') {
const errMsg = 'SSE is unsupported, migrate to streamable_http';
extensionStatuses.set(extensionName, {
name: extensionName,
status: 'error',
error: errMsg,
recoverHints: createExtensionRecoverHints(errMsg),
});
updateToast();
return;
}
try {
await addToAgentOnStartup({
extensionConfig,