feat: support streamable http upstream server (#75)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
samanhappy
2025-05-12 22:12:48 +08:00
committed by GitHub
parent f1a5f692cc
commit 040782da8d
5 changed files with 121 additions and 37 deletions

View File

@@ -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<ServerFormData>({
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<EnvVar[]>(
@@ -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"
/>
<label htmlFor="command">stdio</label>
<label htmlFor="command">STDIO</label>
</div>
<div>
<input
@@ -151,15 +173,27 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
name="serverType"
value="url"
checked={serverType === 'sse'}
onChange={() => setServerType('sse')}
onChange={() => updateServerType('sse')}
className="mr-1"
/>
<label htmlFor="url">sse</label>
<label htmlFor="url">SSE</label>
</div>
<div>
<input
type="radio"
id="streamable-http"
name="serverType"
value="streamable-http"
checked={serverType === 'streamable-http'}
onChange={() => updateServerType('streamable-http')}
className="mr-1"
/>
<label htmlFor="streamable-http">Streamable HTTP</label>
</div>
</div>
</div>
{serverType === 'sse' ? (
{serverType === 'sse' || serverType === 'streamable-http' ? (
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="url">
{t('server.url')}
@@ -171,8 +205,8 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
value={formData.url}
onChange={handleInputChange}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
placeholder="e.g.: http://localhost:3000/sse"
required={serverType === 'sse'}
placeholder={serverType === 'streamable-http' ? "e.g.: http://localhost:3000/mcp" : "e.g.: http://localhost:3000/sse"}
required={serverType === 'sse' || serverType === 'streamable-http'}
/>
</div>
) : (

View File

@@ -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;
}
}

View File

@@ -69,6 +69,24 @@ export const createServer = async (req: Request, res: Response): Promise<void> =
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<void> =
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<void> =
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,

View File

@@ -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<string, string> = conf.env || {};
env['PATH'] = expandEnvVars(process.env.PATH as string) || '';

View File

@@ -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<string, string>; // 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
}