The nREPL protocol is designed to solve the "narrow waist" problem where you have N languages and M editors, and you want to avoid having to create NxM adapters across them. In this way it shares a lot with the Language Server Protocol, though it predates LSP by several years. However, the focus is different; LSP achieves its integration using static analysis, while nREPL achieves it by running project code. Writing an nREPL server is much easier than writing an LSP server and can be done in a couple hundred lines.
The nREPL protocol has its roots in the Clojure community; at the time of this writing most nREPL users are Clojure users. But the design of the protocol is language-agnostic and can be applied to any language that can evaluate code at runtime.
Note: This document is an in-progress draft of version 1.0.0 of the nREPL protocol. Open design questions are tracked in the issue tracker; feedback and contributions are very welcome.
The nREPL protocol operates by default by exchanging bencoded messages over a socket. Various implementations may also offer other encodings and transports, such as JSON over stdio, but these are not standardized.
While it's somewhat unusual, bencode was chosen because it is a good deal easier to implement than JSON; usually it can be done in under a hundred lines of code. Unfortunately bencode is not particularly readable, so examples in this document will show messages in JSON.
Every message sent by the client must have an op field,
for operation.
Every message sent by the client is a request. Requests can have one
or more response messages associated with them. Because a request can
have many responses, every request must be considered active until a
response is received with a status field which is a list
that contains the string done. Requests may remain active
for a long time before completing, so the responses should be handled
asynchronously.
Requests should have an id field, and responses to that
request should also include the same id.
In this document, the examples use UUID id strings, but
the only requirement is that they are strings that are unique to the
life of the specific server process and all clients connecting to
it.
When an error is encountered in the nREPL server itself (rather than
in the code that the client has sent) it should send a response with
err containing a message describing the error, and a
status containing server-error as well as
done.
The first three ops, describe, eval, and
stdin, are the absolute minimum a server needs to
support.
describe opThe client should send a describe request upon
connecting so it knows which ops the server supports. Servers should not
require clients to make this request first.
// client -> server
{"op": "describe",
"id": "5d90576e-b5e1-4499-a43d-c75c60b579ff",
"client": "nREPL documentation demo 1.0.0"}The client field may be used for debugging purposes, but
the server should not treat the client differently based on its name or
version.
The response must have a list of ops supported by the
server. It may also have a dictionary of versions for
debugging purposes as well.
// client <- server
{"id": "5d90576e-b5e1-4499-a43d-c75c60b579ff",
"ops": ["clone", "eval", "stdin", "describe", "load-file", "sandbox"],
"versions": {"nrepl": "1.0.0",
"lua": "5.4"},
"status": ["done"]}Compatibility note: previous versions of the protocol had
ops defined as a dictionary with the operation names as the
keys and an unspecified dictionary as the values. This is no longer
recommended.
eval opThis is the main workhorse operation where code gets run. The
code field contains the code to be run.
// client -> server
{"op": "eval",
"id": "297a1dc1-e8ea-4a71-ac58-977841a301f4",
"code": "99 + 121"}In the event that the code runs successfully this should return a
message with a value field containing a string
representation of the return value or values.
// client <- server
{"id": "297a1dc1-e8ea-4a71-ac58-977841a301f4",
"value": "220",
"status": ["done"]}In the case that evaluated code produces output, the server should
send messages that have out or err fields, for
stdout and stderr respectively. These may be sent in a separate message
sent before the "done" message that has value in it, or
they may be present in that message. The client should display these to
the user in a way that makes it clear they are part of the session; for
example, in the editor console right below where the code was
entered.
// client -> server
{"op": "eval",
"id": "35e53d19-9f4a-4329-a820-d71481fdfec1",
"code": "print('hello, world')"}
// client <- server
{"id": "35e53d19-9f4a-4329-a820-d71481fdfec1",
"value": "nil",
"out": "hello, world",
"status": ["done"]}The server should make an effort to send out messages
with an id field where possible; however, it may be
unavoidable to have some output which is not tied to a specific request,
so clients should be prepared for output to arrive which is missing an
id field or contains an id for a request that
is already considered done.
In the case that evaluated code encounters an error, the response
message should include an ex field instead of
value. The format of this field will vary depending on the
way the language in question represents errors.
// client -> server
{"op": "eval",
"id": "b9616f31-9fbd-4a76-b7d6-ab98eb9f7641",
"code": "client.connect(config.hostname, config.port)"}
// client <- server
{"id": "b9616f31-9fbd-4a76-b7d6-ab98eb9f7641",
"ex": "connection refused",
"status": ["done"]}The eval message may also contain a file
field indicating that the code in question should be treated as if it
came from a given file. If this is not an absolute path, it should be
interpreted as being relative to the directory from which the server was
started. The line and column fields may
include numbers indicating where in the file the code was from. Lines
should start at 1 and columns start at 0. These fields typically do not
affect how the code is run, but they may help improve stack traces if
there is an error.
In some languages, evaluation always happens in the context of a
specific namespace or module. For those languages, an ns
field may be included in the request which indicates the namespace to
evaluate the code in.
// client -> server
{"op": "eval",
"id": "2102c017-2ff5-4ddd-9067-a54ec62fc0c8",
"file": "src/display/avatar.lua",
"line": 21,
"column": 8,
"ns": "display.avatar",
"code": "avatar.reload()"}stdin opIn the case that evaluated code tries to read input from stdin, the
server should send a message to the client with a status of
need-input. When this happens, the client should accept
input and send what it receives using the stdin
operation.
// client -> server
{"op": "eval",
"id": "78f78353-c185-4211-a868-b19eaa85e054",
"code": "subsystem.activate()"}
// client <- server
{"id": "78f78353-c185-4211-a868-b19eaa85e054",
"out": "Username: ",
"status": ["need-input"]}
// client -> server
{"op": "stdin",
"id": "78f78353-c185-4211-a868-b19eaa85e054",
"stdin": "gorkon"}
// client <- server
{"id": "78f78353-c185-4211-a868-b19eaa85e054",
"out": "Activated.\n",
"value": "nil",
"status": ["done"]}Clients and servers may choose to support these if they make sense.
If a server receives a request with an op it does not
recognize, it must reply with a message whose status
contains unknown-op along with done.
interrupt opFor servers that support interrupting running code, the client may
send an interrupt op. Requests may optionally contain an
interrupt-id field which corresponds to the id
of the request to be interrupted. If this is omitted, it interrupts the
most recent request of the current session.
// client -> server
{"op": "eval",
"id": "71629c7e-6c73-4dea-85f8-102d4b64c07f",
"code": "calculate_matrix()"}
// client -> server
{"op": "interrupt",
"id": "71629c7e-6c73-4dea-85f8-102d4b64c07f"}The reply to this request should be a message with statuses
interrupted and done both:
// client <- server
{"id": "71629c7e-6c73-4dea-85f8-102d4b64c07f",
"status": ["interrupted", "done"]}lookup opFor servers that support providing documentation and reflective
information for functions and other values, the client may send a
lookup op containing a sym field for the item
being looked up.
// client -> server
{"op": "lookup",
"id": "d30f8bb9-4e6e-48a8-b0f8-58adf5b353a7",
"sym": "mymodule.reloader"}The response will vary from one server to another due to language
variation, but all data should be under info. If
documentation is available, it should be under doc, and
argument lists for functions should be under arglist.
If the definition of the requested value can be traced to a file,
then a file and line field should be included.
A column may also be provided. Line numbers are counted
from 1, and column numbers are counted from 0. If it was found inside an
archive, for example a zip file or a jar file, it should also include an
archive field containing a path to the archive file. If
archive is present, then file is interpreted
as being inside the archive; otherwise it is either an absolute path or
interpreted as being relative to the directory in which the server was
started.
// client <- server
{"id": "d30f8bb9-4e6e-48a8-b0f8-58adf5b353a7",
"info": {"doc": "Reloads the configuration.",
"arglist": ["path", "restart"],
"file": "src/mymodule.lua",
"line": 27,
"column": 2,
"archive": "lib/extras.zip"},
"status": ["done"]}If the sym is not found, then the info
field should be omitted.
load-file opA client may instruct the server to load an entire file instead of
sending its contents across the wire. This can be especially useful if
the server is running on a different machine from the client. It may
send a request with a load-file op which has
file-path indicating the path to the file to load.
Depending on the server, in some cases this may result in the loaded
code having better stack trace information.
If file-path is not an absolute path, it should be
interpreted as being relative to the directory from which the server was
started.
// client -> server
{"op": "load-file",
"id": "b5c90973-bf4f-4626-a825-e493eed4759a",
"file-path": "src/utils.lua"}The response should be interpreted similarly to the eval
op: a value or ex may be included, but
omitting both is also allowed. It may also include out
and/or err.
// client <- server
{"id": "b5c90973-bf4f-4626-a825-e493eed4759a",
"value": "{debug=#<function: 0x5618eb88b180>}",
"status": ["done"]}Compatibility note: older versions of nREPL required a
file field which contained code that was simply evaluated.
This behavior is no longer recommended.
completions opA client may request completions for a given input using the
completions op. The prefix field should be a
string indicating the input to be completed.
For servers where completions may be specific to a module or
namespace context, an ns field may also be included
indicating this. The client may also include file,
line, and column which the server may use to
get better context to provide completions.
// client -> server
{"op": "completions",
"id": "d862f516-a232-4e01-a4c1-1afb42e04637",
"prefix": "math.s"}The response should contain a completions field with a
list of completion candidates: dictionaries with a
candidate field with the full text to complete, and
optionally a type field describing the candidate's
type.
// client <- server
{"id": "d862f516-a232-4e01-a4c1-1afb42e04637",
"completions": [{"candidate": "math.sqrt", "type": "function"},
{"candidate": "math.sin", "type": "function"},
{"candidate": "math.sinh", "type": "function"}],
"status": ["done"]}clone opSome servers may wish to implement sessions. Requests and responses may be sent inside a session, which has two effects: all requests within one session are run in serial; that is, no request can be accepted until the previous one has completed. In addition, thread-local state (if applicable) should be kept consistent across a session.
In order to start a session, the client sends a message with the
clone op:
// client -> server
{"op": "clone",
"id": "01e0bbed-2819-41b8-9642-4c16a79f7efc"}If the request contains a session field, then that
session is cloned to make the requested session and all its state is
passed on; if not then the new session is not connected to an existing
one.
The response should contain a new new-session id which
should be included as the session for all subsequent
requests:
// client <- server
{"new-session": "afd3c88e-707f-4169-a265-892f29476333",
"id": "01e0bbed-2819-41b8-9642-4c16a79f7efc",
"status": ["done"]}close opA client may send a close op to terminate the
session.
// client -> server
{"op": "close",
"session": "afd3c88e-707f-4169-a265-892f29476333",
"id": "5da7c522-24eb-4a77-a133-63f08c4bdc1e"}
// client <- server
{"session": "afd3c88e-707f-4169-a265-892f29476333",
"id": "5da7c522-24eb-4a77-a133-63f08c4bdc1e",
"status": ["done"]}The server may close the socket after the reply is sent, if the session is connected over a socket and the socket is not being used for other sessions. The client may close the socket to achieve the same effect.
If the server was communicating over stdio, it may exit if no other sessions are active, but if it was communicating over a socket, it should remain running to accept sessions from other clients.
Each server may include support for additional ops that are not part
of this protocol specification. Support for these ops should be
indicated by including them in the ops from the
describe op so clients can discover them dynamically.
If your client wants to perform some operation that is not part of
the spec and it only needs to support a single language, it can send the
implementation of this operation across the wire using
eval, and indeed, many clients have done this. For example,
earlier versions of the nREPL protocol did not have a
completions operation, and so some clients sent Clojure
code to calculate completions and parsed the value reply to
determine what to display to the user. However, this is discouraged as
it creates incompatibilities across languages and puts functionality
that belongs to the server in the client.
Rather than sending code across the wire for an op, implementers may propose a draft extension to the nREPL protocol so that clients can standardize on it instead. Future versions of this document may link to a list of proposed extensions.
Except where otherwise noted, nrepl.org is licensed under the Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.
© 2025-2026 nREPL contributors | site source