diff --git a/client.go b/client.go
index 683d03958644376d588c0c8e7ba6da708084f6d7..8788b951dd2ff566b50331d633f80f224527ce8e 100644
--- a/client.go
+++ b/client.go
@@ -170,6 +170,8 @@ func DialContext(ctx context.Context, rawurl string) (*Client, error) {
 		return DialHTTP(rawurl)
 	case "ws", "wss":
 		return DialWebsocket(ctx, rawurl, "")
+	case "tcp":
+		return DialTCP(ctx, rawurl)
 	case "stdio":
 		return DialStdIO(ctx)
 	case "":
diff --git a/ipc.go b/ipc.go
index 221daa08b6ee3c2af164d6676ce6fd875ab2a595..35dd3d75926fc1e12eadf8e887e9fd684ed5aa50 100644
--- a/ipc.go
+++ b/ipc.go
@@ -19,6 +19,7 @@ package jrpc
 import (
 	"context"
 	"net"
+	"net/url"
 
 	"github.com/ethereum/go-ethereum/log"
 	"github.com/ethereum/go-ethereum/p2p/netutil"
@@ -39,6 +40,41 @@ func (s *Server) ServeListener(l net.Listener) error {
 	}
 }
 
+// DialTCP create a new TCP client that connects to the given endpoint.
+//
+// The context is used for the initial connection establishment. It does not
+// affect subsequent interactions with the client.
+func DialTCP(ctx context.Context, endpoint string) (*Client, error) {
+	parsed, err := url.Parse(endpoint)
+	if err != nil {
+		return nil, err
+	}
+	ans := make(chan *Client)
+	errc := make(chan error)
+	go func() {
+		client, err := newClient(ctx, func(ctx context.Context) (ServerCodec, error) {
+			conn, err := net.Dial("tcp", parsed.Host)
+			if err != nil {
+				return nil, err
+			}
+			return NewCodec(conn), nil
+		})
+		if err != nil {
+			errc <- err
+			return
+		}
+		ans <- client
+	}()
+	select {
+	case err := <-errc:
+		return nil, err
+	case a := <-ans:
+		return a, nil
+	case <-ctx.Done():
+		return nil, ctx.Err()
+	}
+}
+
 // DialIPC create a new IPC client that connects to the given endpoint. On Unix it assumes
 // the endpoint is the full path to a unix socket, and Windows the endpoint is an
 // identifier for a named pipe.