@@ -3178,13 +3178,66 @@ export async function fetchReviewSession(sessionId: string): Promise<ReviewSessi
31783178
31793179// ========== MCP API ==========
31803180
3181- export interface McpServer {
3181+ /**
3182+ * Base fields shared by all MCP server types
3183+ */
3184+ interface McpServerBase {
31823185 name : string ;
3186+ enabled : boolean ;
3187+ scope : 'project' | 'global' ;
3188+ }
3189+
3190+ /**
3191+ * STDIO-based MCP server (traditional command-based)
3192+ * Uses child process communication via stdin/stdout
3193+ */
3194+ export interface StdioMcpServer extends McpServerBase {
3195+ transport : 'stdio' ;
31833196 command : string ;
31843197 args ?: string [ ] ;
31853198 env ?: Record < string , string > ;
3186- enabled : boolean ;
3187- scope : 'project' | 'global' ;
3199+ cwd ?: string ;
3200+ }
3201+
3202+ /**
3203+ * HTTP-based MCP server (remote/streamable)
3204+ * Uses HTTP/HTTPS transport for remote MCP servers
3205+ *
3206+ * Supports two config formats:
3207+ * - Claude format: { type: 'http', url, headers }
3208+ * - Codex format: { url, bearer_token_env_var, http_headers, env_http_headers }
3209+ */
3210+ export interface HttpMcpServer extends McpServerBase {
3211+ transport : 'http' ;
3212+ url : string ;
3213+ /** HTTP headers to include in requests (Claude format) */
3214+ headers ?: Record < string , string > ;
3215+ /** Environment variable name containing bearer token (Codex format) */
3216+ bearerTokenEnvVar ?: string ;
3217+ /** Static HTTP headers (Codex format) */
3218+ httpHeaders ?: Record < string , string > ;
3219+ /** Environment variable names whose values are injected as headers (Codex format) */
3220+ envHttpHeaders ?: string [ ] ;
3221+ }
3222+
3223+ /**
3224+ * Discriminated union type for MCP server configurations
3225+ * Use type guards to distinguish between STDIO and HTTP servers
3226+ */
3227+ export type McpServer = StdioMcpServer | HttpMcpServer ;
3228+
3229+ /**
3230+ * Type guard to check if a server is STDIO-based
3231+ */
3232+ export function isStdioMcpServer ( server : McpServer ) : server is StdioMcpServer {
3233+ return server . transport === 'stdio' ;
3234+ }
3235+
3236+ /**
3237+ * Type guard to check if a server is HTTP-based
3238+ */
3239+ export function isHttpMcpServer ( server : McpServer ) : server is HttpMcpServer {
3240+ return server . transport === 'http' ;
31883241}
31893242
31903243export interface McpServerConflict {
@@ -3257,17 +3310,80 @@ function findProjectConfigKey(projects: Record<string, unknown>, projectPath?: s
32573310 return projectPath in projects ? projectPath : null ;
32583311}
32593312
3260- function normalizeServerConfig ( config : unknown ) : { command : string ; args ?: string [ ] ; env ?: Record < string , string > } {
3313+ /**
3314+ * Normalize raw server config to discriminated union type
3315+ * Preserves HTTP-specific fields instead of flattening to command field
3316+ *
3317+ * Supports dual-format parsing:
3318+ * - Claude format: { type: 'http', url, headers }
3319+ * - Codex format: { url, bearer_token_env_var, http_headers, env_http_headers }
3320+ */
3321+ function normalizeServerConfig ( config : unknown ) : Omit < StdioMcpServer , 'name' | 'enabled' | 'scope' > | Omit < HttpMcpServer , 'name' | 'enabled' | 'scope' > {
32613322 if ( ! isUnknownRecord ( config ) ) {
3262- return { command : '' } ;
3323+ // Default to STDIO with empty command
3324+ return { transport : 'stdio' , command : '' } ;
3325+ }
3326+
3327+ // Detect HTTP transport by presence of url field (both Claude and Codex formats)
3328+ const hasUrl = typeof config . url === 'string' && config . url . trim ( ) !== '' ;
3329+ const hasHttpType = config . type === 'http' || config . transport === 'http' ;
3330+
3331+ if ( hasUrl || hasHttpType ) {
3332+ // HTTP-based server (Claude or Codex format)
3333+ const url = typeof config . url === 'string' ? config . url : '' ;
3334+
3335+ // Parse Claude format headers: { headers: { "Authorization": "Bearer xxx" } }
3336+ const headers = isUnknownRecord ( config . headers )
3337+ ? Object . fromEntries (
3338+ Object . entries ( config . headers ) . flatMap ( ( [ key , value ] ) =>
3339+ typeof value === 'string' ? [ [ key , value ] ] : [ ]
3340+ )
3341+ )
3342+ : undefined ;
3343+
3344+ // Parse Codex format fields
3345+ const bearerTokenEnvVar = typeof config . bearer_token_env_var === 'string'
3346+ ? config . bearer_token_env_var
3347+ : undefined ;
3348+
3349+ // Parse Codex http_headers: { http_headers: { "Authorization": "Bearer xxx" } }
3350+ const httpHeaders = isUnknownRecord ( config . http_headers )
3351+ ? Object . fromEntries (
3352+ Object . entries ( config . http_headers ) . flatMap ( ( [ key , value ] ) =>
3353+ typeof value === 'string' ? [ [ key , value ] ] : [ ]
3354+ )
3355+ )
3356+ : undefined ;
3357+
3358+ // Parse Codex env_http_headers: { env_http_headers: ["API_KEY", "SECRET"] }
3359+ const envHttpHeaders = Array . isArray ( config . env_http_headers )
3360+ ? config . env_http_headers . filter ( ( item ) : item is string => typeof item === 'string' )
3361+ : undefined ;
3362+
3363+ const result : Omit < HttpMcpServer , 'name' | 'enabled' | 'scope' > = {
3364+ transport : 'http' ,
3365+ url,
3366+ } ;
3367+
3368+ // Only add optional fields if they have values
3369+ if ( headers && Object . keys ( headers ) . length > 0 ) {
3370+ result . headers = headers ;
3371+ }
3372+ if ( bearerTokenEnvVar ) {
3373+ result . bearerTokenEnvVar = bearerTokenEnvVar ;
3374+ }
3375+ if ( httpHeaders && Object . keys ( httpHeaders ) . length > 0 ) {
3376+ result . httpHeaders = httpHeaders ;
3377+ }
3378+ if ( envHttpHeaders && envHttpHeaders . length > 0 ) {
3379+ result . envHttpHeaders = envHttpHeaders ;
3380+ }
3381+
3382+ return result ;
32633383 }
32643384
3265- const command =
3266- typeof config . command === 'string'
3267- ? config . command
3268- : typeof config . url === 'string'
3269- ? config . url
3270- : '' ;
3385+ // STDIO-based server (traditional command format)
3386+ const command = typeof config . command === 'string' ? config . command : '' ;
32713387
32723388 const args = Array . isArray ( config . args )
32733389 ? config . args . filter ( ( arg ) : arg is string => typeof arg === 'string' )
@@ -3281,11 +3397,24 @@ function normalizeServerConfig(config: unknown): { command: string; args?: strin
32813397 )
32823398 : undefined ;
32833399
3284- return {
3400+ const cwd = typeof config . cwd === 'string' ? config . cwd : undefined ;
3401+
3402+ const result : Omit < StdioMcpServer , 'name' | 'enabled' | 'scope' > = {
3403+ transport : 'stdio' ,
32853404 command,
3286- args : args && args . length > 0 ? args : undefined ,
3287- env : env && Object . keys ( env ) . length > 0 ? env : undefined ,
32883405 } ;
3406+
3407+ if ( args && args . length > 0 ) {
3408+ result . args = args ;
3409+ }
3410+ if ( env && Object . keys ( env ) . length > 0 ) {
3411+ result . env = env ;
3412+ }
3413+ if ( cwd ) {
3414+ result . cwd = cwd ;
3415+ }
3416+
3417+ return result ;
32893418}
32903419
32913420/**
@@ -3371,15 +3500,66 @@ function requireProjectPath(projectPath: string | undefined, ctx: string): strin
33713500 return trimmed ;
33723501}
33733502
3374- function toServerConfig ( server : { command : string ; args ?: string [ ] ; env ?: Record < string , string > } ) : UnknownRecord {
3375- const config : UnknownRecord = { command : server . command } ;
3376- if ( server . args && server . args . length > 0 ) config . args = server . args ;
3377- if ( server . env && Object . keys ( server . env ) . length > 0 ) config . env = server . env ;
3503+ /**
3504+ * Convert McpServer to raw config format for persistence
3505+ * Handles both STDIO and HTTP server types
3506+ */
3507+ function toServerConfig ( server : Partial < McpServer > ) : UnknownRecord {
3508+ // Check if this is an HTTP server
3509+ if ( server . transport === 'http' ) {
3510+ const config : UnknownRecord = { url : server . url } ;
3511+
3512+ // Claude format: type field
3513+ config . type = 'http' ;
3514+
3515+ // Claude format: headers
3516+ if ( server . headers && Object . keys ( server . headers ) . length > 0 ) {
3517+ config . headers = server . headers ;
3518+ }
3519+
3520+ // Codex format: bearer_token_env_var
3521+ if ( server . bearerTokenEnvVar ) {
3522+ config . bearer_token_env_var = server . bearerTokenEnvVar ;
3523+ }
3524+
3525+ // Codex format: http_headers
3526+ if ( server . httpHeaders && Object . keys ( server . httpHeaders ) . length > 0 ) {
3527+ config . http_headers = server . httpHeaders ;
3528+ }
3529+
3530+ // Codex format: env_http_headers
3531+ if ( server . envHttpHeaders && server . envHttpHeaders . length > 0 ) {
3532+ config . env_http_headers = server . envHttpHeaders ;
3533+ }
3534+
3535+ return config ;
3536+ }
3537+
3538+ // STDIO server (default)
3539+ const config : UnknownRecord = { } ;
3540+
3541+ if ( typeof server . command === 'string' ) {
3542+ config . command = server . command ;
3543+ }
3544+
3545+ if ( server . args && server . args . length > 0 ) {
3546+ config . args = server . args ;
3547+ }
3548+
3549+ if ( server . env && Object . keys ( server . env ) . length > 0 ) {
3550+ config . env = server . env ;
3551+ }
3552+
3553+ if ( server . cwd ) {
3554+ config . cwd = server . cwd ;
3555+ }
3556+
33783557 return config ;
33793558}
33803559
33813560/**
33823561 * Update MCP server configuration
3562+ * Supports both STDIO and HTTP server types
33833563 */
33843564export async function updateMcpServer (
33853565 serverName : string ,
@@ -3389,15 +3569,20 @@ export async function updateMcpServer(
33893569 if ( ! config . scope ) {
33903570 throw new Error ( 'updateMcpServer: scope is required' ) ;
33913571 }
3392- if ( typeof config . command !== 'string' || ! config . command . trim ( ) ) {
3393- throw new Error ( 'updateMcpServer: command is required' ) ;
3572+
3573+ // Validate based on transport type
3574+ if ( config . transport === 'http' ) {
3575+ if ( typeof config . url !== 'string' || ! config . url . trim ( ) ) {
3576+ throw new Error ( 'updateMcpServer: url is required for HTTP servers' ) ;
3577+ }
3578+ } else {
3579+ // STDIO server (default)
3580+ if ( typeof config . command !== 'string' || ! config . command . trim ( ) ) {
3581+ throw new Error ( 'updateMcpServer: command is required for STDIO servers' ) ;
3582+ }
33943583 }
33953584
3396- const serverConfig = toServerConfig ( {
3397- command : config . command ,
3398- args : config . args ,
3399- env : config . env ,
3400- } ) ;
3585+ const serverConfig = toServerConfig ( config ) ;
34013586
34023587 if ( config . scope === 'global' ) {
34033588 const result = await fetchApi < { success ?: boolean ; error ?: string } > ( '/api/mcp-add-global-server' , {
@@ -3436,26 +3621,29 @@ export async function updateMcpServer(
34363621 const servers = await fetchMcpServers ( options . projectPath ) ;
34373622 return [ ...servers . project , ...servers . global ] . find ( ( s ) => s . name === serverName ) ?? {
34383623 name : serverName ,
3439- command : config . command ,
3624+ transport : config . transport ?? 'stdio' ,
3625+ ...( config . transport === 'http' ? { url : config . url ! } : { command : config . command ! } ) ,
34403626 args : config . args ,
34413627 env : config . env ,
34423628 enabled : config . enabled ?? true ,
34433629 scope : config . scope ,
3444- } ;
3630+ } as McpServer ;
34453631 }
34463632
34473633 return {
34483634 name : serverName ,
3449- command : config . command ,
3635+ transport : config . transport ?? 'stdio' ,
3636+ ...( config . transport === 'http' ? { url : config . url ! } : { command : config . command ! } ) ,
34503637 args : config . args ,
34513638 env : config . env ,
34523639 enabled : config . enabled ?? true ,
34533640 scope : config . scope ,
3454- } ;
3641+ } as McpServer ;
34553642}
34563643
34573644/**
34583645 * Create a new MCP server
3646+ * Supports both STDIO and HTTP server types
34593647 */
34603648export async function createMcpServer (
34613649 server : McpServer ,
@@ -3464,8 +3652,17 @@ export async function createMcpServer(
34643652 if ( ! server . name ?. trim ( ) ) {
34653653 throw new Error ( 'createMcpServer: name is required' ) ;
34663654 }
3467- if ( ! server . command ?. trim ( ) ) {
3468- throw new Error ( 'createMcpServer: command is required' ) ;
3655+
3656+ // Validate based on transport type
3657+ if ( server . transport === 'http' ) {
3658+ if ( ! server . url ?. trim ( ) ) {
3659+ throw new Error ( 'createMcpServer: url is required for HTTP servers' ) ;
3660+ }
3661+ } else {
3662+ // STDIO server (default)
3663+ if ( ! server . command ?. trim ( ) ) {
3664+ throw new Error ( 'createMcpServer: command is required for STDIO servers' ) ;
3665+ }
34693666 }
34703667
34713668 const serverName = server . name . trim ( ) ;
0 commit comments