diff --git a/client/commands.go b/client/commands.go
index 101c7d3..dac7059 100644
--- a/client/commands.go
+++ b/client/commands.go
@@ -84,6 +84,23 @@ func splitMessage(msg string, splitLen int) (msgs []string) {
 	return append(msgs, msg)
 }
 
+func splitArgs(args []string, maxLen int) []string {
+	res := make([]string, 0)
+
+	i := 0
+	for i < len(args) {
+		currArg := args[i]
+		i++
+
+		for i < len(args) && len(currArg)+len(args[i])+1 < maxLen {
+			currArg += " " + args[i]
+			i++
+		}
+		res = append(res, currArg)
+	}
+	return res
+}
+
 // Raw sends a raw line to the server, should really only be used for
 // debugging purposes but may well come in handy.
 func (conn *Conn) Raw(rawline string) {
@@ -299,6 +316,9 @@ func (conn *Conn) Cap(subcommmand string, capabilities ...string) {
 	if len(capabilities) == 0 {
 		conn.Raw(CAP + " " + subcommmand)
 	} else {
-		conn.Raw(CAP + " " + subcommmand + " :" + strings.Join(capabilities, " "))
+		cmdPrefix := CAP + " " + subcommmand + " :"
+		for _, args := range splitArgs(capabilities, defaultSplit-len(cmdPrefix)) {
+			conn.Raw(cmdPrefix + args)
+		}
 	}
 }
diff --git a/client/commands_test.go b/client/commands_test.go
index 15a8a05..25af371 100644
--- a/client/commands_test.go
+++ b/client/commands_test.go
@@ -2,6 +2,8 @@ package client
 
 import (
 	"reflect"
+	"strconv"
+	"strings"
 	"testing"
 )
 
@@ -203,3 +205,20 @@ func TestClientCommands(t *testing.T) {
 	c.VHost("user", "pass")
 	s.nc.Expect("VHOST user pass")
 }
+
+func TestSplitCommand(t *testing.T) {
+	nArgs := 100
+
+	args := make([]string, 0)
+	for i := 0; i < nArgs; i++ {
+		args = append(args, "arg"+strconv.Itoa(i))
+	}
+
+	for maxLen := 1; maxLen <= defaultSplit; maxLen *= 2 {
+		for _, argStr := range splitArgs(args, maxLen) {
+			if len(argStr) > maxLen && len(strings.Split(argStr, " ")) > 1 {
+				t.Errorf("maxLen = %d, but len(cmd) = %d", maxLen, len(argStr))
+			}
+		}
+	}
+}
diff --git a/client/connection.go b/client/connection.go
index 43bcc24..8ed5a84 100644
--- a/client/connection.go
+++ b/client/connection.go
@@ -45,6 +45,12 @@ type Conn struct {
 	out         chan string
 	connected   bool
 
+	// Capabilities supported by the server
+	supportedCaps *capSet
+
+	// Capabilites currently enabled
+	currCaps *capSet
+
 	// CancelFunc and WaitGroup for goroutines
 	die context.CancelFunc
 	wg  sync.WaitGroup
@@ -89,6 +95,12 @@ type Config struct {
 	// Passed through to https://golang.org/pkg/net/#Dialer
 	DualStack bool
 
+	// Enable IRCv3 capability negotiation.
+	EnableCapabilityNegotiation bool
+
+	// A list of capabilities to request to the server during registration.
+	Capabilites []string
+
 	// Replaceable function to customise the 433 handler's new nick.
 	// By default an underscore "_" is appended to the current nick.
 	NewNick func(string) string
@@ -125,12 +137,13 @@ type Config struct {
 // name, but these are optional.
 func NewConfig(nick string, args ...string) *Config {
 	cfg := &Config{
-		Me:       &state.Nick{Nick: nick},
-		PingFreq: 3 * time.Minute,
-		NewNick:  DefaultNewNick,
-		Recover:  (*Conn).LogPanic, // in dispatch.go
-		SplitLen: defaultSplit,
-		Timeout:  60 * time.Second,
+		Me:                          &state.Nick{Nick: nick},
+		PingFreq:                    3 * time.Minute,
+		NewNick:                     DefaultNewNick,
+		Recover:                     (*Conn).LogPanic, // in dispatch.go
+		SplitLen:                    defaultSplit,
+		Timeout:                     60 * time.Second,
+		EnableCapabilityNegotiation: false,
 	}
 	cfg.Me.Ident = "goirc"
 	if len(args) > 0 && args[0] != "" {
@@ -204,13 +217,15 @@ func Client(cfg *Config) *Conn {
 	}
 
 	conn := &Conn{
-		cfg:         cfg,
-		dialer:      dialer,
-		intHandlers: handlerSet(),
-		fgHandlers:  handlerSet(),
-		bgHandlers:  handlerSet(),
-		stRemovers:  make([]Remover, 0, len(stHandlers)),
-		lastsent:    time.Now(),
+		cfg:           cfg,
+		dialer:        dialer,
+		intHandlers:   handlerSet(),
+		fgHandlers:    handlerSet(),
+		bgHandlers:    handlerSet(),
+		stRemovers:    make([]Remover, 0, len(stHandlers)),
+		lastsent:      time.Now(),
+		supportedCaps: capabilitySet(),
+		currCaps:      capabilitySet(),
 	}
 	conn.addIntHandlers()
 	return conn
@@ -289,6 +304,16 @@ func (conn *Conn) DisableStateTracking() {
 	}
 }
 
+// SupportsCapability returns true if the server supports the given capability.
+func (conn *Conn) SupportsCapability(cap string) bool {
+	return conn.supportedCaps.Has(cap)
+}
+
+// HasCapability returns true if the given capability has been acked by the server during negotiation.
+func (conn *Conn) HasCapability(cap string) bool {
+	return conn.currCaps.Has(cap)
+}
+
 // Per-connection state initialisation.
 func (conn *Conn) initialise() {
 	conn.io = nil
diff --git a/client/handlers.go b/client/handlers.go
index 24165ad..d2317df 100644
--- a/client/handlers.go
+++ b/client/handlers.go
@@ -4,7 +4,9 @@ package client
 // to manage tracking an irc connection etc.
 
 import (
+	"sort"
 	"strings"
+	"sync"
 	"time"
 
 	"github.com/fluffle/goirc/logging"
@@ -18,8 +20,13 @@ var intHandlers = map[string]HandlerFunc{
 	CTCP:     (*Conn).h_CTCP,
 	NICK:     (*Conn).h_NICK,
 	PING:     (*Conn).h_PING,
+	CAP:      (*Conn).h_CAP,
+	"410":    (*Conn).h_410,
 }
 
+// set up the ircv3 capabilities supported by this client which will be requested by default to the server.
+var defaultCaps = []string{}
+
 func (conn *Conn) addIntHandlers() {
 	for n, h := range intHandlers {
 		// internal handlers are essential for the IRC client
@@ -35,6 +42,10 @@ func (conn *Conn) h_PING(line *Line) {
 
 // Handler for initial registration with server once tcp connection is made.
 func (conn *Conn) h_REGISTER(line *Line) {
+	if conn.cfg.EnableCapabilityNegotiation {
+		conn.Cap(CAP_LS)
+	}
+
 	if conn.cfg.Pass != "" {
 		conn.Pass(conn.cfg.Pass)
 	}
@@ -42,6 +53,134 @@ func (conn *Conn) h_REGISTER(line *Line) {
 	conn.User(conn.cfg.Me.Ident, conn.cfg.Me.Name)
 }
 
+func (conn *Conn) getRequestCapabilities() *capSet {
+	s := capabilitySet()
+
+	// add capabilites supported by the client
+	s.Add(defaultCaps...)
+
+	// add capabilites requested by the user
+	s.Add(conn.cfg.Capabilites...)
+
+	return s
+}
+
+func (conn *Conn) negotiateCapabilities(supportedCaps []string) {
+	conn.supportedCaps.Add(supportedCaps...)
+
+	reqCaps := conn.getRequestCapabilities()
+	reqCaps.Intersect(conn.supportedCaps)
+
+	if reqCaps.Size() > 0 {
+		conn.Cap(CAP_REQ, reqCaps.Slice()...)
+	} else {
+		conn.Cap(CAP_END)
+	}
+}
+
+func (conn *Conn) handleCapAck(caps []string) {
+	for _, cap := range caps {
+		conn.currCaps.Add(cap)
+	}
+	conn.Cap(CAP_END)
+}
+
+func (conn *Conn) handleCapNak(caps []string) {
+	conn.Cap(CAP_END)
+}
+
+const (
+	CAP_LS  = "LS"
+	CAP_REQ = "REQ"
+	CAP_ACK = "ACK"
+	CAP_NAK = "NAK"
+	CAP_END = "END"
+)
+
+type capSet struct {
+	caps map[string]bool
+	mu   sync.RWMutex
+}
+
+func capabilitySet() *capSet {
+	return &capSet{
+		caps: make(map[string]bool),
+	}
+}
+
+func (c *capSet) Add(caps ...string) {
+	c.mu.Lock()
+	for _, cap := range caps {
+		if strings.HasPrefix(cap, "-") {
+			c.caps[cap[1:]] = false
+		} else {
+			c.caps[cap] = true
+		}
+	}
+	c.mu.Unlock()
+}
+
+func (c *capSet) Has(cap string) bool {
+	c.mu.RLock()
+	defer c.mu.RUnlock()
+	return c.caps[cap]
+}
+
+// Intersect computes the intersection of two sets.
+func (c *capSet) Intersect(other *capSet) {
+	c.mu.Lock()
+
+	for cap := range c.caps {
+		if !other.Has(cap) {
+			delete(c.caps, cap)
+		}
+	}
+
+	c.mu.Unlock()
+}
+
+func (c *capSet) Slice() []string {
+	c.mu.RLock()
+	defer c.mu.RUnlock()
+
+	capSlice := make([]string, 0, len(c.caps))
+	for cap := range c.caps {
+		capSlice = append(capSlice, cap)
+	}
+
+	// make output predictable for testing
+	sort.Strings(capSlice)
+	return capSlice
+}
+
+func (c *capSet) Size() int {
+	c.mu.RLock()
+	defer c.mu.RUnlock()
+	return len(c.caps)
+}
+
+// This handler is triggered when an invalid cap command is received by the server.
+func (conn *Conn) h_410(line *Line) {
+	logging.Warn("Invalid cap subcommand: ", line.Args[1])
+}
+
+// Handler for capability negotiation commands.
+// Note that even if multiple CAP_END commands may be sent to the server during negotiation,
+// only the first will be considered.
+func (conn *Conn) h_CAP(line *Line) {
+	subcommand := line.Args[1]
+
+	caps := strings.Fields(line.Text())
+	switch subcommand {
+	case CAP_LS:
+		conn.negotiateCapabilities(caps)
+	case CAP_ACK:
+		conn.handleCapAck(caps)
+	case CAP_NAK:
+		conn.handleCapNak(caps)
+	}
+}
+
 // Handler to trigger a CONNECTED event on receipt of numeric 001
 // :<server> 001 <nick> :Welcome message <nick>!<user>@<host>
 func (conn *Conn) h_001(line *Line) {
diff --git a/client/handlers_test.go b/client/handlers_test.go
index 171e5c9..1e374d5 100644
--- a/client/handlers_test.go
+++ b/client/handlers_test.go
@@ -456,3 +456,54 @@ func Test671(t *testing.T) {
 	s.st.EXPECT().GetNick("user2").Return(nil)
 	c.h_671(ParseLine(":irc.server.org 671 test user2 :some ignored text"))
 }
+
+func TestCap(t *testing.T) {
+	c, s := setUp(t)
+	defer s.tearDown()
+
+	c.Config().EnableCapabilityNegotiation = true
+	c.Config().Capabilites = []string{"cap1", "cap2", "cap3", "cap4"}
+
+	c.h_REGISTER(&Line{Cmd: REGISTER})
+	s.nc.Expect("CAP LS")
+	s.nc.Expect("NICK test")
+	s.nc.Expect("USER test 12 * :Testing IRC")
+
+	// Ensure that capabilities not supported by the server are not requested
+	s.nc.Send("CAP * LS :cap2 cap4")
+	s.nc.Expect("CAP REQ :cap2 cap4")
+
+	s.nc.Send("CAP * ACK :cap2 cap4")
+	s.nc.Expect("CAP END")
+
+	for _, cap := range []string{"cap2", "cap4"} {
+		if !c.SupportsCapability(cap) {
+			t.Fail()
+		}
+
+		if !c.HasCapability(cap) {
+			t.Fail()
+		}
+	}
+
+	for _, cap := range []string{"cap1", "cap3"} {
+		if c.HasCapability(cap) {
+			t.Fail()
+		}
+	}
+
+	// test disable capability after registration
+	s.c.Cap("REQ", "-cap4")
+	s.nc.Expect("CAP REQ :-cap4")
+
+	s.nc.Send("CAP * ACK :-cap4")
+	s.nc.Expect("CAP END")
+
+	if !c.HasCapability("cap2") {
+		t.Fail()
+	}
+
+	if c.HasCapability("cap4") {
+		t.Fail()
+	}
+}