Skip to main content
Glama

Slack MCP

MIT License
696
  • Apple
  • Linux
transport.go13.1 kB
package transport import ( "bufio" "context" "crypto/tls" "crypto/x509" "encoding/base64" "fmt" "io/ioutil" "net" "net/http" "net/url" "os" "strings" "time" "github.com/korotovsky/slack-mcp-server/pkg/text" utls "github.com/refraction-networking/utls" "go.uber.org/zap" "golang.org/x/net/http2" ) const defaultUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" const toolkitPEM = `-----BEGIN CERTIFICATE----- MIIDTzCCAjegAwIBAgIRCvyMzxdGWElNljTLqOyz44owDQYJKoZIhvcNAQELBQAw QTEYMBYGA1UEAxMPSFRUUCBUb29sa2l0IENBMQswCQYDVQQGEwJYWDEYMBYGA1UE ChMPSFRUUCBUb29sa2l0IENBMB4XDTI1MDMxMjE3NTU0M1oXDTI2MDMxMzE3NTU0 M1owQTEYMBYGA1UEAxMPSFRUUCBUb29sa2l0IENBMQswCQYDVQQGEwJYWDEYMBYG A1UEChMPSFRUUCBUb29sa2l0IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB CgKCAQEArHe2mgAdlajHtNHHjCnUb7+UwsvYZ0AGPgpZT9hwAauqIa1BwH6bo4uY Zv4DnEKWVOt4wwVGNWtc5PyFAcO5lB9s9jRZYS/BIZALGrhti5YDw/IFNKFpiyeb buG18qlZz6f6+nEFd/QICl9S333hA89Pks6PUbYsbmSNmS7Dz5fxZN5QH9PoA7pa uDyJBhwJ0uB4UnkcC5tVVuH6vSd9mhtqur4cgo2Rfzz3TBMRDrrJIEVcR2tA3mPj 9cTMgvfxZ+2Cgzin1FVkYJNsuL/HzoAzL8HXM6eKM/fn9cEWJKICIDR/cbrwl/RD 6rtMWUS7Vo39Wq+OFx1Szn+8gIZUjwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/ MA4GA1UdDwEB/wQEAwIBxjAdBgNVHQ4EFgQUwGDLxZ3sJfQRBX23oJj6X1EJJ0Aw DQYJKoZIhvcNAQELBQADggEBAAMcrGqFxWK3tM/eoVcchknYYQOyOQUjhWe1wfPo Gz51GRNcKD5Ip3ItR0bah2MYwHapGOKj0rvuHbpOZl+IJM2CRSx6IMisOds0LI70 Qmz94je6GG2F3wxlF5P6BxKHuM6rCCqG1sgElnP59UomUejjWYkOXQd3xPIyJMuE qyKSCrGs0GOnDRwH6a//zLQF3KOFiN2jJj6oHijaIQfmE6Rgi+x8hGIX6J8adn0X g6ZwdV3myNqKQVJiV+6HSIO1y8tLOnBXjF751L56+fxQoP9Lh9wB0edq730mcb6y 0GhBUL73wXOL2ymHsqrUhSpmScf+YnnX9GN29520s5LFTpY= -----END CERTIFICATE-----` // UserAgentTransport wraps another RoundTripper to add User-Agent and cookies type UserAgentTransport struct { roundTripper http.RoundTripper userAgent string cookies []*http.Cookie logger *zap.Logger } // NewUserAgentTransport creates a new UserAgentTransport func NewUserAgentTransport(roundTripper http.RoundTripper, userAgent string, cookies []*http.Cookie, logger *zap.Logger) *UserAgentTransport { return &UserAgentTransport{ roundTripper: roundTripper, userAgent: userAgent, cookies: cookies, logger: logger, } } // RoundTrip implements the RoundTripper interface func (t *UserAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) { clonedReq := req.Clone(req.Context()) clonedReq.Header.Set("User-Agent", t.userAgent) for _, cookie := range t.cookies { clonedReq.AddCookie(cookie) } t.logger.Debug("Making request", zap.String("url", clonedReq.URL.String())) resp, err := t.roundTripper.RoundTrip(clonedReq) if err != nil { t.logger.Error("Request failed", zap.Error(err)) } return resp, err } // uTLSTransport is a custom http.RoundTripper that uses uTLS for TLS connections type uTLSTransport struct { dialer *net.Dialer tlsConfig *utls.Config proxy func(*http.Request) (*url.URL, error) clientHelloID utls.ClientHelloID http2Transport *http2.Transport logger *zap.Logger } // NewUTLSTransport creates a new transport with uTLS func NewUTLSTransport(tlsConfig *utls.Config, proxy func(*http.Request) (*url.URL, error), clientHelloID utls.ClientHelloID, logger *zap.Logger) *uTLSTransport { return &uTLSTransport{ dialer: &net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }, tlsConfig: tlsConfig, proxy: proxy, clientHelloID: clientHelloID, http2Transport: &http2.Transport{ AllowHTTP: false, DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) { // This won't be called since we handle TLS ourselves return nil, fmt.Errorf("DialTLS should not be called") }, }, logger: logger, } } // RoundTrip implements the http.RoundTripper interface func (t *uTLSTransport) RoundTrip(req *http.Request) (*http.Response, error) { targetAddr := req.URL.Host if req.URL.Port() == "" { if req.URL.Scheme == "https" { targetAddr += ":443" } else { targetAddr += ":80" } } var conn net.Conn var err error if t.proxy != nil { proxyURL, err := t.proxy(req) if err != nil { return nil, fmt.Errorf("proxy error: %w", err) } if proxyURL != nil { conn, err = t.dialProxy(req.Context(), proxyURL, targetAddr) if err != nil { return nil, fmt.Errorf("proxy dial error: %w", err) } } } if conn == nil { conn, err = t.dialer.DialContext(req.Context(), "tcp", targetAddr) if err != nil { return nil, fmt.Errorf("dial error: %w", err) } } if req.URL.Scheme == "https" { tlsConn, err := t.establishTLS(conn, req.URL.Hostname()) if err != nil { conn.Close() return nil, fmt.Errorf("TLS error: %w", err) } conn = tlsConn if uconn, ok := tlsConn.(*utls.UConn); ok { alpn := uconn.ConnectionState().NegotiatedProtocol t.logger.Debug("Negotiated protocol", zap.String("protocol", alpn)) switch alpn { case "h2": // Use HTTP/2 transport clientConn, err := t.http2Transport.NewClientConn(conn) if err != nil { conn.Close() return nil, fmt.Errorf("HTTP/2 client connection error: %w", err) } t.logger.Debug("Using HTTP/2 transport for request", zap.String("request", req.URL.String())) return clientConn.RoundTrip(req) default: t.logger.Debug("Using HTTP/1.1 transport for request", zap.String("request", req.URL.String())) // Fall through to HTTP/1.1 } } } err = req.Write(conn) if err != nil { conn.Close() return nil, fmt.Errorf("request write error: %w", err) } resp, err := http.ReadResponse(bufio.NewReader(conn), req) if err != nil { conn.Close() return nil, err } if resp.Close || resp.Header.Get("Connection") == "close" { conn.Close() } return resp, nil } // dialProxy establishes a connection through an HTTP proxy func (t *uTLSTransport) dialProxy(ctx context.Context, proxyURL *url.URL, targetAddr string) (net.Conn, error) { proxyAddr := proxyURL.Host if proxyURL.Port() == "" { if proxyURL.Scheme == "https" { proxyAddr += ":443" } else { proxyAddr += ":80" } } conn, err := t.dialer.DialContext(ctx, "tcp", proxyAddr) if err != nil { return nil, err } if proxyURL.Scheme == "https" { tlsConfig := &tls.Config{ ServerName: proxyURL.Hostname(), InsecureSkipVerify: t.tlsConfig.InsecureSkipVerify, RootCAs: t.tlsConfig.RootCAs, } tlsConn := tls.Client(conn, tlsConfig) err = tlsConn.Handshake() if err != nil { conn.Close() return nil, err } conn = tlsConn } connectReq := &http.Request{ Method: "CONNECT", URL: &url.URL{Opaque: targetAddr}, Host: targetAddr, Header: make(http.Header), } if proxyURL.User != nil { username := proxyURL.User.Username() password, _ := proxyURL.User.Password() connectReq.Header.Set("Proxy-Authorization", "Basic "+basicAuth(username, password)) } err = connectReq.Write(conn) if err != nil { conn.Close() return nil, err } br := bufio.NewReader(conn) resp, err := http.ReadResponse(br, connectReq) if err != nil { conn.Close() return nil, err } defer resp.Body.Close() if resp.StatusCode != 200 { conn.Close() return nil, fmt.Errorf("proxy returned status %d", resp.StatusCode) } return conn, nil } // establishTLS performs the TLS handshake using uTLS func (t *uTLSTransport) establishTLS(conn net.Conn, serverName string) (net.Conn, error) { config := t.tlsConfig.Clone() config.ServerName = serverName t.logger.Debug("Starting uTLS handshake with server", zap.String("server", serverName)) t.logger.Debug("Using ClientHello fingerprint", zap.String("fingerprint", t.getClientHelloName())) tlsConn := utls.UClient(conn, config, t.clientHelloID) err := tlsConn.Handshake() if err != nil { t.logger.Error("uTLS handshake failed", zap.Error(err)) return nil, err } state := tlsConn.ConnectionState() t.logger.Debug("uTLS handshake successful", zap.String("cipher_suite", fmt.Sprintf("%x", state.CipherSuite)), zap.String("version", fmt.Sprintf("%x", state.Version)), zap.String("negotiated_protocol", fmt.Sprintf("%x", state.NegotiatedProtocol)), zap.String("server_certificates", fmt.Sprintf("%v", text.HumanizeCertificates(state.PeerCertificates))), ) return tlsConn, nil } // getClientHelloName returns a human-readable name for the ClientHello fingerprint func (t *uTLSTransport) getClientHelloName() string { switch t.clientHelloID { case utls.HelloChrome_Auto: return "Chrome (Auto)" case utls.HelloFirefox_Auto: return "Firefox (Auto)" case utls.HelloSafari_Auto: return "Safari (Auto)" case utls.HelloEdge_Auto: return "Edge (Auto)" default: return fmt.Sprintf("Unknown (%v)", t.clientHelloID) } } // basicAuth creates a basic authentication header value func basicAuth(username, password string) string { auth := username + ":" + password return base64.StdEncoding.EncodeToString([]byte(auth)) } // detectBrowserFromUserAgent determines the browser type from user agent string func detectBrowserFromUserAgent(userAgent string) utls.ClientHelloID { ua := strings.ToLower(userAgent) if strings.Contains(ua, "edg/") || strings.Contains(ua, "edge/") { return utls.HelloEdge_Auto } if strings.Contains(ua, "firefox/") { return utls.HelloFirefox_Auto } if strings.Contains(ua, "safari/") && (!strings.Contains(ua, "chrome/") || strings.Contains(ua, "version/")) { return utls.HelloSafari_Auto } if strings.Contains(ua, "chrome/") { return utls.HelloChrome_Auto } return utls.HelloChrome_Auto } // ProvideHTTPClient creates an HTTP client with optional uTLS support func ProvideHTTPClient(cookies []*http.Cookie, logger *zap.Logger) *http.Client { if os.Getenv("SLACK_MCP_PROXY") != "" && os.Getenv("SLACK_MCP_CUSTOM_TLS") != "" { logger.Fatal("SLACK_MCP_PROXY and SLACK_MCP_CUSTOM_TLS cannot be used together", zap.String("reason", "Custom TLS fingerprinting has no effect when using a proxy, as the target server sees the proxy's TLS handshake")) } var proxy func(*http.Request) (*url.URL, error) if proxyURL := os.Getenv("SLACK_MCP_PROXY"); proxyURL != "" { parsed, err := url.Parse(proxyURL) if err != nil { logger.Fatal("Failed to parse proxy URL", zap.String("proxy_url", proxyURL), zap.Error(err)) } proxy = http.ProxyURL(parsed) } rootCAs, _ := x509.SystemCertPool() if rootCAs == nil { rootCAs = x509.NewCertPool() } if isToolkit := os.Getenv("SLACK_MCP_SERVER_CA_TOOLKIT"); isToolkit != "" { if ok := rootCAs.AppendCertsFromPEM([]byte(toolkitPEM)); !ok { logger.Warn("Failed to append toolkit certificate") } } if localCertFile := os.Getenv("SLACK_MCP_SERVER_CA"); localCertFile != "" { certs, err := ioutil.ReadFile(localCertFile) if err != nil { logger.Fatal("Failed to read local certificate file", zap.String("cert_file", localCertFile), zap.Error(err)) } if ok := rootCAs.AppendCertsFromPEM(certs); !ok { logger.Warn("No certs appended, using system certs only") } } insecure := false if os.Getenv("SLACK_MCP_SERVER_CA_INSECURE") != "" { if localCertFile := os.Getenv("SLACK_MCP_SERVER_CA"); localCertFile != "" { logger.Fatal("SLACK_MCP_SERVER_CA and SLACK_MCP_SERVER_CA_INSECURE cannot be used together") } insecure = true } userAgent := defaultUA if ua := os.Getenv("SLACK_MCP_USER_AGENT"); ua != "" { userAgent = ua } var transport http.RoundTripper if useCustomTLS := os.Getenv("SLACK_MCP_CUSTOM_TLS"); useCustomTLS != "" { logger.Debug("Custom TLS handshake enabled", zap.String("user_agent", userAgent)) utlsConfig := &utls.Config{ InsecureSkipVerify: insecure, RootCAs: rootCAs, } clientHelloID := detectBrowserFromUserAgent(userAgent) var detectedBrowser string switch clientHelloID { case utls.HelloChrome_Auto: detectedBrowser = "Chrome" case utls.HelloFirefox_Auto: detectedBrowser = "Firefox" case utls.HelloSafari_Auto: detectedBrowser = "Safari" case utls.HelloEdge_Auto: detectedBrowser = "Edge" } logger.Debug("TLS Fingerprinting Details", zap.String("detected_browser", detectedBrowser), zap.String("client_hello_id", fmt.Sprintf("%v", clientHelloID.Version)), zap.String("user_agent", userAgent), ) transport = NewUTLSTransport(utlsConfig, proxy, clientHelloID, logger) } else { logger.Debug("Using standard TLS handshake") transport = &http.Transport{ Proxy: proxy, TLSClientConfig: &tls.Config{ InsecureSkipVerify: insecure, RootCAs: rootCAs, }, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } } transport = NewUserAgentTransport(transport, userAgent, cookies, logger) client := &http.Client{ Transport: transport, Timeout: 30 * time.Second, } return client }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/korotovsky/slack-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server