Skip to content

add the NO_PROXY env variable feature to avoid proxy calling MCP servers listening on localhost #3340

Description

@biagiomaf

What happened?

I am using gemini-cli behind my company proxy that works only for internet websites and doesn't work for the local network or localhost. I put the proxy url in the http_proxy and https_proxy enviroment variables in my .bat, It is ok only for the tools that go to the internet, unfortunately for the MCP servers that are listening on localhost or 127.0.0.1 those MCP servers don't work because there is no way to exclude localhost and 127.0.0.1 from the proxy. I use many nodejs apps and they all use the env variable NO_PROXY to exclude some address, that doesnt work for gemini-cli. So how can I use my MCP servers if I am behind a company proxy?

What did you expect to happen?

setting NO_PROXY=localhost I expected gemini-cli working with MCP server or services on the localhost even if I am behind a proxy to navigate internet.

Client information

Details
$ gemini /about
 About Gemini CLI                                   │
│                                                    │
│ CLI Version       0.1.9                            │
│ Git Commit        34935d6                          │
│ Model             gemini-2.5-pro                   │
│ Sandbox           no sandbox                       │
│ OS                win32                            │
│ Auth Method       OAuth

Login information

No response

Anything else we need to know?

No response

Activity

  1. added
    area/coreIssues related to User Interface, OS Support, Core Functionality
    priority/p2Important but can be addressed in a future release.
    and removed
    status/need-triageIssues that need to be triaged by the triage automation.
    on Jul 5, 2025
  2. biagiomaf commented on Jul 6, 2025

    @biagiomaf
    Author

    in the meanwhile the matter gets the proper solution, I've resolved patching the file node_modules@google\gemini-cli\node_modules\undici\lib\dispatcher\proxy-agent.js
    putting in it the conditions no to use a proxy if url belongs to localhost or a private network ip address, so now my MCP servers listening on the localhost work even if the internet is accessed only through the company proxy.

  3. hu-bo commented on Jul 11, 2025

    @hu-bo

    in the meanwhile the matter gets the proper solution, I've resolved patching the file node_modules@google\gemini-cli\node_modules\undici\lib\dispatcher\proxy-agent.js putting in it the conditions no to use a proxy if url belongs to localhost or a private network ip address, so now my MCP servers listening on the localhost work even if the internet is accessed only through the company proxy.

    Could you please refer to the code

  4. biagiomaf commented on Jul 11, 2025

    @biagiomaf
    Author

    hu-bo, you asked for the code, I am copyng it here BUT what I did it is absolutely not a good practice, I made that patch only because I was desperate to make my local MCP servers listening on localhost work behind the company proxy. Keep in mind that every time you update the gemini-cli project it will overwrite your patch, so you must patch again the file "node_modules@google\gemini-cli\node_modules\undici\lib\dispatcher\proxy-agent.js" until they officially fix this matter. Forgive the italian comments in my code (I am Italian), use this code at your own risk, just use it to understand the logic because if they change something on it before to fix the matter, you have to implement again this patch logic.

    the code below
    .................................................................

    'use strict'

    const { kProxy, kClose, kDestroy, kDispatch, kConnector } = require('../core/symbols')
    const { URL } = require('node:url')
    const Agent = require('./agent')
    const Pool = require('./pool')
    const DispatcherBase = require('./dispatcher-base')
    const { InvalidArgumentError, RequestAbortedError, SecureProxyConnectionError } = require('../core/errors')
    const buildConnector = require('../core/connect')
    const Client = require('./client')

    const kAgent = Symbol('proxy agent')
    const kClient = Symbol('proxy client')
    const kProxyHeaders = Symbol('proxy headers')
    const kRequestTls = Symbol('request tls settings')
    const kProxyTls = Symbol('proxy tls settings')
    const kConnectEndpoint = Symbol('connect endpoint function')
    const kTunnelProxy = Symbol('tunnel proxy')

    // ---------- INIZIO MODIFICA: AGGIUNTA LOGICA BYPASS ----------
    const kDirectClient = Symbol('direct client')

    /**

    • Determina se un host deve bypassare il proxy.
    • @PARAM {string} hostname L'hostname di destinazione.
    • @returns {boolean} true se il proxy deve essere bypassato.
      */
      function shouldBypassProxy (hostname) {
      if (!hostname) return false

    // Bypass per localhost e indirizzi IP non pubblici
    if (hostname === 'localhost' || hostname === '0.0.0.0' || /^(?:10.|127.|169.254|192.168.|172.(?:1[6-9]|2\d|3[0-1]))/.test(hostname)) {
    return true
    }

    // Bypass in base alla variabile d'ambiente NO_PROXY
    const noProxyEnv = process.env.NO_PROXY || process.env.no_proxy
    if (!noProxyEnv) return false

    const noProxyRules = noProxyEnv.split(',').map(r => r.trim()).filter(Boolean)

    for (const rule of noProxyRules) {
    if (rule === '*') return true
    if (rule.startsWith('.') && hostname.endsWith(rule)) return true
    if (hostname === rule) return true
    }

    return false
    }
    // ---------- FINE MODIFICA ----------

    function defaultProtocolPort (protocol) {
    return protocol === 'https:' ? 443 : 80
    }

    function defaultFactory (origin, opts) {
    return new Pool(origin, opts)
    }

    const noop = () => {}

    class ProxyClient extends DispatcherBase {
    #client = null
    constructor (origin, opts) {
    if (typeof origin === 'string') {
    origin = new URL(origin)
    }

    if (origin.protocol !== 'http:' && origin.protocol !== 'https:') {
      throw new InvalidArgumentError('ProxyClient only supports http and https protocols')
    }
    
    super()
    
    this.#client = new Client(origin, opts)
    

    }

    async [kClose] () {
    await this.#client.close()
    }

    async [kDestroy] () {
    await this.#client.destroy()
    }

    async [kDispatch] (opts, handler) {
    const { method, origin } = opts
    if (method === 'CONNECT') {
    this.#client[kConnector]({
    origin,
    port: opts.port || defaultProtocolPort(opts.protocol),
    path: opts.host,
    signal: opts.signal,
    headers: {
    ...this[kProxyHeaders],
    host: opts.host
    },
    servername: this[kProxyTls]?.servername || opts.servername
    },
    (err, socket) => {
    if (err) {
    handler.callback(err)
    } else {
    handler.callback(null, { socket, statusCode: 200 })
    }
    }
    )
    return
    }
    if (typeof origin === 'string') {
    opts.origin = new URL(origin)
    }

    return this.#client.dispatch(opts, handler)
    

    }
    }
    class ProxyAgent extends DispatcherBase {
    constructor (opts) {
    if (!opts || (typeof opts === 'object' && !(opts instanceof URL) && !opts.uri)) {
    throw new InvalidArgumentError('Proxy uri is mandatory')
    }

    const { clientFactory = defaultFactory } = opts
    if (typeof clientFactory !== 'function') {
      throw new InvalidArgumentError('Proxy opts.clientFactory must be a function.')
    }
    
    const { proxyTunnel = true } = opts
    
    super()
    
    const url = this.#getUrl(opts)
    const { href, origin, port, protocol, username, password, hostname: proxyHostname } = url
    
    this[kProxy] = { uri: href, protocol }
    this[kRequestTls] = opts.requestTls
    this[kProxyTls] = opts.proxyTls
    this[kProxyHeaders] = opts.headers || {}
    
    if (opts.auth && opts.token) {
      throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token')
    } else if (opts.auth) {
      /* @deprecated in favour of opts.token */
      this[kProxyHeaders]['proxy-authorization'] = `Basic ${opts.auth}`
    } else if (opts.token) {
      this[kProxyHeaders]['proxy-authorization'] = opts.token
    } else if (username && password) {
      this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}`
    }
    
    const factory = (!proxyTunnel && protocol === 'http:')
      ? (origin, options) => {
          if (origin.protocol === 'http:') {
            return new ProxyClient(origin, options)
          }
          return new Client(origin, options)
        }
      : undefined
    
    const connect = buildConnector({ ...opts.proxyTls })
    this[kConnectEndpoint] = buildConnector({ ...opts.requestTls })
    this[kClient] = clientFactory(url, { connect, factory })
    this[kTunnelProxy] = proxyTunnel
    this[kAgent] = new Agent({
      ...opts,
      connect: async (opts, callback) => {
        let requestedPath = opts.host
        if (!opts.port) {
          requestedPath += `:${defaultProtocolPort(opts.protocol)}`
        }
        try {
          const { socket, statusCode } = await this[kClient].connect({
            origin,
            port,
            path: requestedPath,
            signal: opts.signal,
            headers: {
              ...this[kProxyHeaders],
              host: opts.host,
              ...(opts.connections == null || opts.connections > 0 ? { 'proxy-connection': 'keep-alive' } : {})
            },
            servername: this[kProxyTls]?.servername || proxyHostname
          })
          if (statusCode !== 200) {
            socket.on('error', noop).destroy()
            callback(new RequestAbortedError(`Proxy response (${statusCode}) !== 200 when HTTP Tunneling`))
          }
          if (opts.protocol !== 'https:') {
            callback(null, socket)
            return
          }
          let servername
          if (this[kRequestTls]) {
            servername = this[kRequestTls].servername
          } else {
            servername = opts.servername
          }
          this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback)
        } catch (err) {
          if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
            // Throw a custom error to avoid loop in client.js#connect
            callback(new SecureProxyConnectionError(err))
          } else {
            callback(err)
          }
        }
      }
    })
    // ---------- INIZIO MODIFICA: AGGIUNTA AGENT DIRETTO ----------
    this[kDirectClient] = new Agent({ ...opts, factory: clientFactory })
    // ---------- FINE MODIFICA ----------
    

    }

    dispatch (opts, handler) {
    const targetUrl = new URL(opts.origin)

    // ---------- INIZIO MODIFICA: CONTROLLO BYPASS PRIMA DI TUTTO ----------
    if (shouldBypassProxy(targetUrl.hostname)) {
      // Usa l'agent diretto invece di quello proxy
      return this[kDirectClient].dispatch(opts, handler)
    }
    // ---------- FINE MODIFICA ----------
    
    const headers = buildHeaders(opts.headers)
    throwIfProxyAuthIsSent(headers)
    
    if (headers && !('host' in headers) && !('Host' in headers)) {
      headers.host = targetUrl.host
    }
    
    if (!this.#shouldConnect(targetUrl)) {
      opts.path = opts.origin + opts.path
    }
    
    return this[kAgent].dispatch(
      {
        ...opts,
        headers
      },
      handler
    )
    

    }

    /**

    • @PARAM {import('../types/proxy-agent').ProxyAgent.Options | string | URL} opts
    • @returns {URL}
      */
      #getUrl (opts) {
      if (typeof opts === 'string') {
      return new URL(opts)
      } else if (opts instanceof URL) {
      return opts
      } else {
      return new URL(opts.uri)
      }
      }

    async [kClose] () {
    await this[kAgent].close()
    await this[kClient].close()
    // ---------- INIZIO MODIFICA: CHIUSURA AGENT DIRETTO ----------
    await this[kDirectClient].close()
    // ---------- FINE MODIFICA ----------
    }

    async [kDestroy] () {
    await this[kAgent].destroy()
    await this[kClient].destroy()
    // ---------- INIZIO MODIFICA: DISTRUZIONE AGENT DIRETTO ----------
    await this[kDirectClient].destroy()
    // ---------- FINE MODIFICA ----------
    }

    #shouldConnect (uri) {
    if (typeof uri === 'string') {
    uri = new URL(uri)
    }
    if (this[kTunnelProxy]) {
    return true
    }
    if (uri.protocol !== 'http:' || this[kProxy].protocol !== 'http:') {
    return true
    }
    return false
    }
    }

    /**

    • @PARAM {string[] | Record<string, string>} headers

    • @returns {Record<string, string>}
      /
      function buildHeaders (headers) {
      // When using undici.fetch, the headers list is stored
      // as an array.
      if (Array.isArray(headers)) {
      /
      * @type {Record<string, string>} */
      const headersPair = {}

      for (let i = 0; i < headers.length; i += 2) {
      headersPair[headers[i]] = headers[i + 1]
      }

      return headersPair
      }

    return headers
    }

    /**

    • @PARAM {Record<string, string>} headers
    • Previous versions of ProxyAgent suggests the Proxy-Authorization in request headers
    • Nevertheless, it was changed and to avoid a security vulnerability by end users
    • this check was created.
    • It should be removed in the next major version for performance reasons
      */
      function throwIfProxyAuthIsSent (headers) {
      const existProxyAuth = headers && Object.keys(headers)
      .find((key) => key.toLowerCase() === 'proxy-authorization')
      if (existProxyAuth) {
      throw new InvalidArgumentError('Proxy-Authorization should be sent in ProxyAgent constructor')
      }
      }

    module.exports = ProxyAgent

  5. zobi commented on Jul 30, 2025

    @zobi

    Same request, i use MCP server on a LAN server and proxy web for internet.
    env variable work for http_proxy and https_proxy but didn't work for no_proxy
    Thanks a lot

  6. Blackoutta commented on Aug 15, 2025

    @Blackoutta
    Contributor

    same problem here

  7. lmagnien commented on Aug 19, 2025

    @lmagnien

    I seem to be experiencing the same issue.

    My NO_PROXY environment variable is set as follows:

    echo %NO_PROXY%
    localhost,127.0.0.1
    

    But I still get this error when launching gemini:

    failed to start or connect to MCP server 'local-mcp-server'
       {"url":"http://localhost:8081/sse"};
       Error: SSE error: TypeError: fetch failed: Error: Request was cancelled.: Proxy response
       (403) !== 200 when HTTP Tunneling
    

    And indeed, if I run /mcp, the local server is disconnected :

    > /mcp
    
    
    ℹ Configured MCP servers:
    
      🔴 local-mcp-server - Disconnected (0 tools cached)
        No tools available
    
    

    Environment details:

    CLI Version: 0.1.4
    Git Commit: 1078a54
    Operating System: Windows (Node v24.3.0)
    Sandbox Environment: none
    Model Version: gemini-2.5-pro
    Memory Usage: 137.3 MB
    
  8. fredericdasilva commented on Aug 31, 2025

    @fredericdasilva

    +1

  9. jmrichardson commented on Sep 2, 2025

    @jmrichardson

    +1

  10. kuanyong-wong-partior commented on Sep 18, 2025

    @kuanyong-wong-partior

    +1

  11. 3 remaining items

  12. fireinice commented on Mar 14, 2026

    @fireinice

    +1

  13. cocosheng-g commented on Apr 9, 2026

    @cocosheng-g
    Contributor

    Closing because the comments indicate this issue might be fixed, has a workaround, is no longer an issue, or is resolved. Feel free to reopen if the problem persists.

  14. fireinice commented on Apr 9, 2026

    @fireinice

    all pr related are not merged yet and i dont see any work around

  15. self-assigned this
    on May 13, 2026
  16. added
    area/extensionsIssues related to Gemini CLI extensions capability
    priority/p3Backlog - a good idea but not currently a priority.
    and removed
    area/coreIssues related to User Interface, OS Support, Core Functionality
    priority/p2Important but can be addressed in a future release.
    on May 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions