diff --git a/frontend/src/components/ServerForm.tsx b/frontend/src/components/ServerForm.tsx index a935bce..1df5552 100644 --- a/frontend/src/components/ServerForm.tsx +++ b/frontend/src/components/ServerForm.tsx @@ -12,9 +12,21 @@ interface ServerFormProps { const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formError = null }: ServerFormProps) => { const { t } = useTranslation() - const [serverType, setServerType] = useState<'sse' | 'stdio'>( - initialData && initialData.config && initialData.config.url ? 'sse' : 'stdio', - ) + + // Determine the initial server type from the initialData + const getInitialServerType = () => { + if (!initialData || !initialData.config) return 'stdio'; + + if (initialData.config.type) { + return initialData.config.type; // Use explicit type if available + } else if (initialData.config.url) { + return 'sse'; // Fallback to SSE if URL exists + } else { + return 'stdio'; // Default to stdio + } + }; + + const [serverType, setServerType] = useState<'stdio' | 'sse' | 'streamable-http'>(getInitialServerType()); const [formData, setFormData] = useState({ name: (initialData && initialData.name) || '', @@ -27,6 +39,8 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr : String(initialData.config.args) : '', args: (initialData && initialData.config && initialData.config.args) || [], + type: getInitialServerType(), // Initialize the type field + env: [] }) const [envVars, setEnvVars] = useState( @@ -49,6 +63,11 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr setFormData({ ...formData, arguments: value, args }) } + const updateServerType = (type: 'stdio' | 'sse' | 'streamable-http') => { + setServerType(type); + setFormData(prev => ({ ...prev, type })); + } + const handleEnvVarChange = (index: number, field: 'key' | 'value', value: string) => { const newEnvVars = [...envVars] newEnvVars[index][field] = value @@ -80,14 +99,17 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr const payload = { name: formData.name, - config: - serverType === 'sse' + config: { + type: serverType, // Always include the type + ...(serverType === 'sse' || serverType === 'streamable-http' ? { url: formData.url } : { - command: formData.command, - args: formData.args, - env: Object.keys(env).length > 0 ? env : undefined, - }, + command: formData.command, + args: formData.args, + env: Object.keys(env).length > 0 ? env : undefined, + } + ) + } } onSubmit(payload) @@ -139,10 +161,10 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr name="serverType" value="command" checked={serverType === 'stdio'} - onChange={() => setServerType('stdio')} + onChange={() => updateServerType('stdio')} className="mr-1" /> - +
setServerType('sse')} + onChange={() => updateServerType('sse')} className="mr-1" /> - + +
+
+ updateServerType('streamable-http')} + className="mr-1" + /> +
- {serverType === 'sse' ? ( + {serverType === 'sse' || serverType === 'streamable-http' ? (
) : ( diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 66a9fd8..2755a54 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -71,6 +71,7 @@ export interface Tool { // Server config types export interface ServerConfig { + type?: 'stdio' | 'sse' | 'streamable-http'; url?: string; command?: string; args?: string[]; @@ -108,6 +109,8 @@ export interface ServerFormData { url: string; command: string; arguments: string; + args?: string[]; // Added explicit args field + type?: 'stdio' | 'sse' | 'streamable-http'; // Added type field env: EnvVar[]; } @@ -157,4 +160,4 @@ export interface AuthResponse { token?: string; user?: IUser; message?: string; -} \ No newline at end of file +} diff --git a/src/controllers/serverController.ts b/src/controllers/serverController.ts index 442d165..22b798c 100644 --- a/src/controllers/serverController.ts +++ b/src/controllers/serverController.ts @@ -69,6 +69,24 @@ export const createServer = async (req: Request, res: Response): Promise = return; } + // Validate the server type if specified + if (config.type && !['stdio', 'sse', 'streamable-http'].includes(config.type)) { + res.status(400).json({ + success: false, + message: 'Server type must be one of: stdio, sse, streamable-http', + }); + return; + } + + // Validate that URL is provided for sse and streamable-http types + if ((config.type === 'sse' || config.type === 'streamable-http') && !config.url) { + res.status(400).json({ + success: false, + message: `URL is required for ${config.type} server type`, + }); + return; + } + const result = await addServer(name, config); if (result.success) { notifyToolChanged(); @@ -150,6 +168,24 @@ export const updateServer = async (req: Request, res: Response): Promise = return; } + // Validate the server type if specified + if (config.type && !['stdio', 'sse', 'streamable-http'].includes(config.type)) { + res.status(400).json({ + success: false, + message: 'Server type must be one of: stdio, sse, streamable-http', + }); + return; + } + + // Validate that URL is provided for sse and streamable-http types + if ((config.type === 'sse' || config.type === 'streamable-http') && !config.url) { + res.status(400).json({ + success: false, + message: `URL is required for ${config.type} server type`, + }); + return; + } + const result = await updateMcpServer(name, config); if (result.success) { notifyToolChanged(); @@ -248,58 +284,62 @@ export const toggleServer = async (req: Request, res: Response): Promise = export const updateSystemConfig = (req: Request, res: Response): void => { try { const { routing, install } = req.body; - - if ((!routing || (typeof routing.enableGlobalRoute !== 'boolean' && typeof routing.enableGroupNameRoute !== 'boolean')) - && (!install || typeof install.pythonIndexUrl !== 'string')) { + + if ( + (!routing || + (typeof routing.enableGlobalRoute !== 'boolean' && + typeof routing.enableGroupNameRoute !== 'boolean')) && + (!install || typeof install.pythonIndexUrl !== 'string') + ) { res.status(400).json({ success: false, message: 'Invalid system configuration provided', }); return; } - + const settings = loadSettings(); if (!settings.systemConfig) { settings.systemConfig = { routing: { enableGlobalRoute: true, - enableGroupNameRoute: true + enableGroupNameRoute: true, }, install: { - pythonIndexUrl: '' - } + pythonIndexUrl: '', + }, }; } - + if (!settings.systemConfig.routing) { settings.systemConfig.routing = { enableGlobalRoute: true, - enableGroupNameRoute: true + enableGroupNameRoute: true, }; } - + if (!settings.systemConfig.install) { settings.systemConfig.install = { - pythonIndexUrl: '' + pythonIndexUrl: '', }; } - + if (routing) { if (typeof routing.enableGlobalRoute === 'boolean') { settings.systemConfig.routing.enableGlobalRoute = routing.enableGlobalRoute; } - + if (typeof routing.enableGroupNameRoute === 'boolean') { settings.systemConfig.routing.enableGroupNameRoute = routing.enableGroupNameRoute; } } - + if (install) { if (typeof install.pythonIndexUrl === 'string') { settings.systemConfig.install.pythonIndexUrl = install.pythonIndexUrl; } } - + if (saveSettings(settings)) { res.json({ success: true, diff --git a/src/services/mcpService.ts b/src/services/mcpService.ts index 85a3195..c0056f9 100644 --- a/src/services/mcpService.ts +++ b/src/services/mcpService.ts @@ -3,6 +3,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { ServerInfo, ServerConfig } from '../types/index.js'; import { loadSettings, saveSettings, expandEnvVars } from '../config/index.js'; import config from '../config/index.js'; @@ -79,9 +80,13 @@ export const initializeClientsFromSettings = (isInit: boolean): ServerInfo[] => } let transport; - if (conf.url) { + if (conf.type === 'streamable-http') { + transport = new StreamableHTTPClientTransport(new URL(conf.url || '')); + } else if (conf.url) { + // Default to SSE only when 'conf.type' is not specified and 'conf.url' is available transport = new SSEClientTransport(new URL(conf.url)); } else if (conf.command && conf.args) { + // If type is stdio or if command and args are provided without type const env: Record = conf.env || {}; env['PATH'] = expandEnvVars(process.env.PATH as string) || ''; diff --git a/src/types/index.ts b/src/types/index.ts index 564d7cf..7e8038f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,6 +1,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; // User interface export interface IUser { @@ -11,8 +12,8 @@ export interface IUser { // Group interface for server grouping export interface IGroup { - id: string; // Unique UUID for the group - name: string; // Display name of the group + id: string; // Unique UUID for the group + name: string; // Display name of the group description?: string; // Optional description of the group servers: string[]; // Array of server names that belong to this group } @@ -92,7 +93,8 @@ export interface McpSettings { // Configuration details for an individual server export interface ServerConfig { - url?: string; // URL for SSE-based servers + type?: 'stdio' | 'sse' | 'streamable-http'; // Type of server + url?: string; // URL for SSE or streamable HTTP servers command?: string; // Command to execute for stdio-based servers args?: string[]; // Arguments for the command env?: Record; // Environment variables @@ -106,7 +108,7 @@ export interface ServerInfo { error: string | null; // Error message if any tools: ToolInfo[]; // List of tools available on the server client?: Client; // Client instance for communication - transport?: SSEClientTransport | StdioClientTransport; // Transport mechanism used + transport?: SSEClientTransport | StdioClientTransport | StreamableHTTPClientTransport; // Transport mechanism used createTime: number; // Timestamp of when the server was created enabled?: boolean; // Flag to indicate if the server is enabled }