@@ -11,6 +11,8 @@ import {
1111import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" ;
1212import type { FetchLike , Transport } from "@modelcontextprotocol/sdk/shared/transport.js" ;
1313import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce" ;
14+ import { substituteString , containsEnvVarReference } from "../config/env-substitution.js" ;
15+ import { isDangerousHostEnvVarName } from "../infra/host-env-security.js" ;
1416import { logDebug } from "../logger.js" ;
1517import {
1618 buildMcpHttpFetch ,
@@ -95,10 +97,24 @@ export function resolveMcpTransport(
9597 return null ;
9698 }
9799 if ( resolved . kind === "stdio" ) {
100+ const processEnv = process . env ;
101+ const finalEnv : Record < string , string > = { } ;
102+ for ( const [ key , value ] of Object . entries ( resolved . env || { } ) ) {
103+ if ( isDangerousHostEnvVarName ( key ) ) {
104+ throw new Error (
105+ `Dynamic environment variable substitution generated a dangerous host environment variable: ${ key } ` ,
106+ ) ;
107+ }
108+ finalEnv [ key ] =
109+ value && containsEnvVarReference ( value )
110+ ? substituteString ( value , processEnv , "mcp.servers.*.env" )
111+ : value ;
112+ }
113+
98114 const transport = new OpenClawStdioClientTransport ( {
99115 command : resolved . command ,
100116 args : resolved . args ,
101- env : resolved . env ,
117+ env : finalEnv ,
102118 cwd : resolved . cwd ,
103119 stderr : "pipe" ,
104120 } ) ;
@@ -120,22 +136,50 @@ export function resolveMcpTransport(
120136 config : resolved . oauth ,
121137 } )
122138 : undefined ;
139+ const headers =
140+ resolved . auth === "oauth" ? withoutMcpAuthorizationHeader ( resolved . headers ) : resolved . headers ;
141+
123142 const baseFetch = buildMcpHttpFetch ( {
124143 sslVerify : resolved . sslVerify ,
125144 clientCert : resolved . clientCert ,
126145 clientKey : resolved . clientKey ,
127146 resourceUrl : resolved . url ,
128147 } ) ;
129- const headers =
130- resolved . auth === "oauth" ? withoutMcpAuthorizationHeader ( resolved . headers ) : resolved . headers ;
148+
149+ const substitutingFetch : FetchLike = ( url , init ) => {
150+ let finalInit = init ;
151+ if ( init ?. headers ) {
152+ const mergedHeaders = new Headers ( init . headers ) ;
153+ let needsReplace = false ;
154+ const processEnv = process . env ;
155+ for ( const [ key , value ] of mergedHeaders . entries ( ) ) {
156+ if ( value && containsEnvVarReference ( value ) ) {
157+ mergedHeaders . set ( key , substituteString ( value , processEnv , "mcp.servers.*.headers" ) ) ;
158+ needsReplace = true ;
159+ }
160+ }
161+ if ( needsReplace ) {
162+ // We reconstruct as a plain object to avoid surprising any fetch polyfills or assertions
163+ // that expect a plain object, particularly in tests.
164+ const plainHeaders : Record < string , string > = { } ;
165+ for ( const [ key , value ] of mergedHeaders . entries ( ) ) {
166+ plainHeaders [ key ] = value ;
167+ }
168+ finalInit = { ...init , headers : plainHeaders } ;
169+ }
170+ }
171+ return baseFetch ( url , finalInit ) ;
172+ } ;
173+
131174 const httpFetch =
132175 resolved . auth === "oauth"
133176 ? withSameOriginMcpHttpHeaders ( {
134- fetchFn : baseFetch ,
177+ fetchFn : substitutingFetch ,
135178 headers,
136179 resourceUrl : resolved . url ,
137180 } )
138- : baseFetch ;
181+ : substitutingFetch ;
182+
139183 if ( resolved . transportType === "streamable-http" ) {
140184 return {
141185 transport : new StreamableHTTPClientTransport ( new URL ( resolved . url ) , {
@@ -150,11 +194,15 @@ export function resolveMcpTransport(
150194 supportsParallelToolCalls : resolved . supportsParallelToolCalls ,
151195 } ;
152196 }
153- const sseHeaders : Record < string , string > = { ...headers } ;
154- const hasHeaders = Object . keys ( sseHeaders ) . length > 0 ;
197+
198+ const sseHeaders = headers ? { ...headers } : { } ;
199+ if ( resolved . auth === "oauth" && sseHeaders . authorization ) {
200+ delete sseHeaders . authorization ;
201+ }
202+
155203 return {
156204 transport : new SSEClientTransport ( new URL ( resolved . url ) , {
157- requestInit : resolved . auth === "oauth" || ! hasHeaders ? undefined : { headers : sseHeaders } ,
205+ requestInit : resolved . auth === "oauth" || ! headers ? undefined : { headers } ,
158206 fetch : httpFetch ,
159207 eventSourceInit : {
160208 fetch : buildSseEventSourceFetch ( resolved . auth === "oauth" ? { } : sseHeaders , httpFetch ) ,
0 commit comments