11import { Client } from "@modelcontextprotocol/sdk/client/index.js"
22import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
33import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
4+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
45import ReconnectingEventSource from "reconnecting-eventsource"
56import {
67 CallToolResultSchema ,
@@ -35,7 +36,7 @@ import { injectEnv } from "../../utils/config"
3536export type McpConnection = {
3637 server : McpServer
3738 client : Client
38- transport : StdioClientTransport | SSEClientTransport
39+ transport : StdioClientTransport | SSEClientTransport | StreamableHTTPClientTransport
3940}
4041
4142// Base configuration schema for common settings
@@ -47,14 +48,17 @@ const BaseConfigSchema = z.object({
4748} )
4849
4950// Custom error messages for better user feedback
50- const typeErrorMessage = "Server type must be either 'stdio' or 'sse '"
51+ const typeErrorMessage = "Server type must be 'stdio', 'sse', or 'streamable-http '"
5152const stdioFieldsErrorMessage =
5253 "For 'stdio' type servers, you must provide a 'command' field and can optionally include 'args' and 'env'"
5354const sseFieldsErrorMessage =
5455 "For 'sse' type servers, you must provide a 'url' field and can optionally include 'headers'"
56+ const streamableHttpFieldsErrorMessage =
57+ "For 'streamable-http' type servers, you must provide a 'url' field and can optionally include 'headers'"
5558const mixedFieldsErrorMessage =
56- "Cannot mix 'stdio' and 'sse' fields. For 'stdio' use 'command', 'args', and 'env'. For 'sse' use 'url' and 'headers'"
57- const missingFieldsErrorMessage = "Server configuration must include either 'command' (for stdio) or 'url' (for sse)"
59+ "Cannot mix 'stdio' and ('sse' or 'streamable-http') fields. For 'stdio' use 'command', 'args', and 'env'. For 'sse'/'streamable-http' use 'url' and 'headers'"
60+ const missingFieldsErrorMessage =
61+ "Server configuration must include either 'command' (for stdio) or 'url' (for sse/streamable-http) and a corresponding 'type' if 'url' is used."
5862
5963// Helper function to create a refined schema with better error messages
6064const createServerTypeSchema = ( ) => {
@@ -90,6 +94,23 @@ const createServerTypeSchema = () => {
9094 type : "sse" as const ,
9195 } ) )
9296 . refine ( ( data ) => data . type === undefined || data . type === "sse" , { message : typeErrorMessage } ) ,
97+ // StreamableHTTP config (has url field)
98+ BaseConfigSchema . extend ( {
99+ type : z . enum ( [ "streamable-http" ] ) . optional ( ) ,
100+ url : z . string ( ) . url ( "URL must be a valid URL format" ) ,
101+ headers : z . record ( z . string ( ) ) . optional ( ) ,
102+ // Ensure no stdio fields are present
103+ command : z . undefined ( ) . optional ( ) ,
104+ args : z . undefined ( ) . optional ( ) ,
105+ env : z . undefined ( ) . optional ( ) ,
106+ } )
107+ . transform ( ( data ) => ( {
108+ ...data ,
109+ type : "streamable-http" as const ,
110+ } ) )
111+ . refine ( ( data ) => data . type === undefined || data . type === "streamable-http" , {
112+ message : typeErrorMessage ,
113+ } ) ,
93114 ] )
94115}
95116
@@ -152,33 +173,43 @@ export class McpHub {
152173 private validateServerConfig ( config : any , serverName ?: string ) : z . infer < typeof ServerConfigSchema > {
153174 // Detect configuration issues before validation
154175 const hasStdioFields = config . command !== undefined
155- const hasSseFields = config . url !== undefined
176+ const hasUrlFields = config . url !== undefined // Covers sse and streamable-http
156177
157- // Check for mixed fields
158- if ( hasStdioFields && hasSseFields ) {
178+ // Check for mixed fields (stdio vs url-based)
179+ if ( hasStdioFields && hasUrlFields ) {
159180 throw new Error ( mixedFieldsErrorMessage )
160181 }
161182
162- // Check if it's a stdio or SSE config and add type if missing
163- if ( ! config . type ) {
164- if ( hasStdioFields ) {
165- config . type = "stdio"
166- } else if ( hasSseFields ) {
167- config . type = "sse"
168- } else {
169- throw new Error ( missingFieldsErrorMessage )
170- }
171- } else if ( config . type !== "stdio" && config . type !== "sse" ) {
183+ // Infer type for stdio if not provided
184+ if ( ! config . type && hasStdioFields ) {
185+ config . type = "stdio"
186+ }
187+
188+ // For url-based configs, type must be provided by the user
189+ if ( hasUrlFields && ! config . type ) {
190+ throw new Error ( "Configuration with 'url' must explicitly specify 'type' as 'sse' or 'streamable-http'." )
191+ }
192+
193+ // Validate type if provided
194+ if ( config . type && ! [ "stdio" , "sse" , "streamable-http" ] . includes ( config . type ) ) {
172195 throw new Error ( typeErrorMessage )
173196 }
174197
175198 // Check for type/field mismatch
176199 if ( config . type === "stdio" && ! hasStdioFields ) {
177200 throw new Error ( stdioFieldsErrorMessage )
178201 }
179- if ( config . type === "sse" && ! hasSseFields ) {
202+ if ( config . type === "sse" && ! hasUrlFields ) {
180203 throw new Error ( sseFieldsErrorMessage )
181204 }
205+ if ( config . type === "streamable-http" && ! hasUrlFields ) {
206+ throw new Error ( streamableHttpFieldsErrorMessage )
207+ }
208+
209+ // If neither command nor url is present (type alone is not enough)
210+ if ( ! hasStdioFields && ! hasUrlFields ) {
211+ throw new Error ( missingFieldsErrorMessage )
212+ }
182213
183214 // Validate the config against the schema
184215 try {
@@ -441,7 +472,7 @@ export class McpHub {
441472 } ,
442473 )
443474
444- let transport : StdioClientTransport | SSEClientTransport
475+ let transport : StdioClientTransport | SSEClientTransport | StreamableHTTPClientTransport
445476
446477 // Inject environment variables to the config
447478 const configInjected = ( await injectEnv ( config ) ) as typeof config
@@ -506,8 +537,33 @@ export class McpHub {
506537 } else {
507538 console . error ( `No stderr stream for ${ name } ` )
508539 }
509- transport . start = async ( ) => { } // No-op now, .connect() won't fail
510- } else {
540+ } else if ( configInjected . type === "streamable-http" ) {
541+ // Streamable HTTP connection
542+ transport = new StreamableHTTPClientTransport ( new URL ( configInjected . url ) , {
543+ requestInit : {
544+ headers : configInjected . headers ,
545+ } ,
546+ } )
547+
548+ // Set up Streamable HTTP specific error handling
549+ transport . onerror = async ( error ) => {
550+ console . error ( `Transport error for "${ name } " (streamable-http):` , error )
551+ const connection = this . findConnection ( name , source )
552+ if ( connection ) {
553+ connection . server . status = "disconnected"
554+ this . appendErrorMessage ( connection , error instanceof Error ? error . message : `${ error } ` )
555+ }
556+ await this . notifyWebviewOfServerChanges ( )
557+ }
558+
559+ transport . onclose = async ( ) => {
560+ const connection = this . findConnection ( name , source )
561+ if ( connection ) {
562+ connection . server . status = "disconnected"
563+ }
564+ await this . notifyWebviewOfServerChanges ( )
565+ }
566+ } else if ( configInjected . type === "sse" ) {
511567 // SSE connection
512568 const sseOptions = {
513569 requestInit : {
@@ -542,7 +598,13 @@ export class McpHub {
542598 }
543599 await this . notifyWebviewOfServerChanges ( )
544600 }
601+ } else {
602+ // Correctly placed "unsupported type" else block
603+ // Should not happen if validateServerConfig is correct
604+ throw new Error ( `Unsupported MCP server type: ${ ( configInjected as any ) . type } ` )
545605 }
606+ // transport.start assignment moved after all type-specific initializations
607+ transport . start = async ( ) => { } // No-op now, .connect() won't fail
546608
547609 const connection : McpConnection = {
548610 server : {
0 commit comments