Skip to content
JsonRpcRemote is in pre-alpha and under active development. This documentation describes the development version on main, which may differ from the latest release, and more features are coming: see Status and roadmap.

Protocol

JsonRpcRemote and the server talk JSON-RPC 2.0. The protocol is language-agnostic by design: a server can be written in any language that reads and writes JSON over a stream, and JSON-RPC libraries that support the framing of the Language Server Protocol handle the transport part.

This page describes the messages, for implementing a server of your own. The package robotframework-jsonrpcremote-protocol defines them as Python dataclasses in the module robot_jsonrpcremote_protocol; the client, the server and the example server at the end of this page use them.

The protocol needs a stream that delivers bytes in order, such as a TCP connection or the stdin and stdout of a process. Each message starts with a header part, as in the Language Server Protocol:

Content-Length: 60\r\n
\r\n
{"jsonrpc":"2.0","method":"robot/initialized","params":{}}\r\n
  • The headers are ASCII, each ends with \r\n, and an empty line ends the header part.
  • Content-Length is the length of the content in bytes. The reference implementation ends the content with \r\n and counts it.
  • Content-Type may give a charset for the content; the default is UTF-8. Other headers are ignored.
  • Field names are the names of the dataclass fields, such as library_token. The reference implementation leaves out optional fields that are not set.
  • Arguments and return values of keywords, the argument defaults in a library definition and the arguments of robot/import_library are JSON values.
  • In addition, the Python implementation sends Python objects as {"__pickled__": "<hex>"}, where <hex> is a Python pickle of the object in hexadecimal, so they travel unchanged between Python peers. It does so for every keyword argument, return value and default that is not null, a string, a number or a boolean, lists and dicts included.
  • Reading a pickle can run arbitrary code; see the caution in TCP and stdio.
Constant Method Type Params Result
INITIALIZE_REQUEST robot/initialize request InitializeParams InitializeResult
INITIALIZED_NOTIFICATION robot/initialized notification InitializedParams
IMPORT_LIBRARY_REQUEST robot/import_library request ImportLibraryParams ImportLibraryResult
RUN_KEYWORD_REQUEST robot/run_keyword request RunKeywordParams RunKeywordResult
LOG_NOTIFICATION robot/log notification LogParams
TRACE_NOTIFICATION robot/trace notification LogParams
FINALIZE_LIBRARY_REQUEST robot/finalize_library request FinalizeLibraryParams FinalizeLibraryResult
SHUTDOWN_REQUEST robot/shutdown request ShutdownParams ShutdownResult
EXIT_NOTIFICATION robot/exit notification ExitParams

The server sends robot/log, and the client every other message; the reference implementation does not use robot/trace.

A session is one connection, on which the client imports one library:

  1. The client connects and sends robot/initialize. The server answers with its capabilities and information about itself.
  2. The client sends robot/initialized. The reference server refuses robot/import_library and robot/run_keyword before it has received both.
  3. The client sends robot/import_library. The server imports the library and answers with a token for this instance and the library definition, from which Robot Framework learns the keywords.
  4. For each keyword call, the client sends robot/run_keyword with the token. While the keyword runs, the server sends its log messages as robot/log, which the client logs under the keyword; then it sends the response.
  5. When the session ends, the client sends robot/finalize_library with the token, then robot/shutdown, then robot/exit if the server announced support_exit, and closes the connection.
  • InitializeParams

    • capabilities: ClientCapabilities (optional): what the client supports.
    • client_info: ClientInfo (optional): name and version of the client.
    • initialization_options: any value (optional): options for the server.
    • trace: string (optional): the initial trace setting, such as "off".
  • InitializedParams: empty.

  • InitializeResult

    • capabilities: ServerCapabilities (optional): what the server supports.
    • server_info: ServerInfo (optional): name and version of the server.
  • ClientInfo, ServerInfo

    • name: string.
    • version: string.
  • ClientCapabilities

    • supports_log: boolean (optional).
    • supports_trace: boolean (optional).

    The client sends both unset, and the reference server does not read them.

  • ServerCapabilities

    • support_exit: boolean (optional): whether the server handles robot/exit. The client sends robot/exit only if it is true.
    • libraries: list of strings (optional): the libraries that clients may import, as given on the server’s command line; empty if clients may import any library.
  • ImportLibraryParams
    • name: string: the library to import. An empty string asks for the server’s default library; the reference server then imports the first library of its command line.
    • args: list (optional): positional arguments for the library.
    • kw_args: object (optional): named arguments for the library. The reference server passes them to Robot Framework as name=value, which converts them according to the library’s __init__.
  • ImportLibraryResult
    • token: string: identifies the imported library instance in later requests. The server chooses it; it is unique for each import.
    • definition: LibraryDefinition.
  • FinalizeLibraryParams
    • token: string: the instance to release.
  • FinalizeLibraryResult: empty.
  • LibraryDefinition
    • name: string.
    • keywords: list of KeywordDefinition.
    • doc: string (optional): the library documentation.
    • doc_format: string (optional): ROBOT, REST, HTML or TEXT.
    • scope: string (optional): GLOBAL, SUITE or TEST.
    • version: string (optional).
    • source: string (optional): the path of the library’s source file.
    • lineno: integer (optional): the line where the library is defined.
  • KeywordDefinition
    • name: string.
    • args: list of ArgumentDefinition.
    • doc: string (optional).
    • tags: list of strings (optional).
    • source: string (optional).
    • lineno: integer (optional).
  • ArgumentDefinition
    • name: string.
    • type: string (optional): the type hint as text, such as "int". The client passes it to Robot Framework as the type of the argument.
    • has_default: boolean (optional): whether the argument has a default. If true, default holds it, which may be null.
    • default: any value (optional).
    • required: boolean: reserved, always true. Use has_default instead.
    • kind: ArgumentKind (optional), a string: POSITIONAL_ONLY, POSITIONAL_OR_NAMED, POSITIONAL_ONLY_MARKER (the /), NAMED_ONLY_MARKER (the *), VAR_POSITIONAL (*args), NAMED_ONLY or VAR_NAMED (**kwargs).
    • doc: string (optional).
  • RunKeywordParams

    • library_token: string: the token from ImportLibraryResult.
    • name: string: the keyword name from the library definition.
    • args: list (optional): positional arguments.
    • kwargs: object (optional): named arguments.
  • RunKeywordResult

    • result: any value (optional): the return value of the keyword.
    • error: RunKeywordError (optional): set if the keyword failed.
  • RunKeywordError

    • message: string.
    • type: string (optional): the name of the exception, such as "AssertionError".
    • traceback: string (optional).
    • mode: RunKeywordErrorMode (optional), a string: CONTINUABLE, FATAL or SKIP.

    The reference server sets message, and the client fails the keyword with the error.

  • LogParams
    • message: string.
    • level: LogLevel (optional), a string: TRACE, DEBUG, INFO, WARN, ERROR, CONSOLE or HTML. Without it, the client logs at INFO.
    • html: boolean (optional): whether the message is HTML.
    • console: boolean (optional): whether the client also writes the message to the console.
    • timestamp: string (optional): when the message was logged. The client does not read it.
  • ShutdownParams, ShutdownResult: empty. The client announces that it is about to close the connection.
  • ExitParams: empty. Sent with robot/exit; the server then closes the connection.
  • A keyword that fails is not a JSON-RPC error: the response is a RunKeywordResult with error.
  • A request that the server cannot handle, such as the import of a library that it does not serve, gets a JSON-RPC error response. The client fails the library import or the keyword call with its message.
  • The module also defines the error codes RobotJsonRpcErrorCode: NOT_INITIALIZED (-32000), KEYWORD_NOT_FOUND (-32001) and ARGUMENT_MISMATCH (-32002). The reference implementation answers with the standard error codes of JSON-RPC.

The example server in the repository, examples/simple-robot-jsonrpcserver, implements the protocol by hand with jsonrpcpeer and the dataclasses of robot_jsonrpcremote_protocol, without the server package. It handles every message that the client sends and serves the library SimpleRobotLibrary with the keyword Echo, which returns its argument and sends a few log messages while it runs:

examples/simple-robot-jsonrpcserver/src/simple_robot_jsonrpcserver/__main__.py
import asyncio
import itertools
from jsonrpcpeer import JsonRpcPeer, rpc_notification, rpc_request
from robot_jsonrpcremote_protocol import (
EXIT_NOTIFICATION,
FINALIZE_LIBRARY_REQUEST,
IMPORT_LIBRARY_REQUEST,
INITIALIZE_REQUEST,
INITIALIZED_NOTIFICATION,
LOG_NOTIFICATION,
RUN_KEYWORD_REQUEST,
SHUTDOWN_REQUEST,
ArgumentDefinition,
ExitParams,
FinalizeLibraryParams,
FinalizeLibraryResult,
ImportLibraryParams,
ImportLibraryResult,
InitializedParams,
InitializeParams,
InitializeResult,
KeywordDefinition,
LibraryDefinition,
LogLevel,
LogParams,
RunKeywordError,
RunKeywordParams,
RunKeywordResult,
ServerCapabilities,
ServerInfo,
ShutdownParams,
ShutdownResult,
)
class SimpleRobotServerEndpoint:
def __init__(self) -> None:
self.initialize_received: bool = False
self.initialized_received: bool = False
self.rpc_peer: JsonRpcPeer | None = None
self._token_ids = itertools.count()
async def log(
self, message: str, level: LogLevel = LogLevel.INFO, console: bool = False, html: bool = False
) -> None:
if self.rpc_peer:
await self.rpc_peer.send_notification(
LOG_NOTIFICATION,
LogParams(message=message, level=level, console=console, html=html),
)
@rpc_request(INITIALIZE_REQUEST)
async def initialize(self, params: InitializeParams) -> InitializeResult:
print(f"Initializing client: {params}")
self.initialize_received = True
return InitializeResult(
capabilities=ServerCapabilities(support_exit=True, libraries=["SimpleRobotLibrary"]),
server_info=ServerInfo(name="SimpleRobotServer", version="1.0"),
)
@rpc_notification(INITIALIZED_NOTIFICATION)
async def initialized(self, params: InitializedParams) -> None:
print(f"Client initialized with params: {params}")
self.initialized_received = True
@rpc_request(IMPORT_LIBRARY_REQUEST)
async def import_library(self, params: ImportLibraryParams) -> ImportLibraryResult:
if not self.initialize_received or not self.initialized_received:
raise Exception("Initialize and Initialized must be received before ImportLibrary.")
print(f"Importing library: {params.name}")
return ImportLibraryResult(
token=f"simple-robot-library-{next(self._token_ids)}",
definition=LibraryDefinition(
name="SimpleRobotLibrary",
doc="A simple robot library exposed via JSON-RPC.",
keywords=[
KeywordDefinition(
name="echo",
args=[ArgumentDefinition(name="message", type="Any", doc="Message to echo back.")],
doc="Echoes the given message.",
tags=["example"],
source=__file__ + ":19",
)
],
),
)
@rpc_request(RUN_KEYWORD_REQUEST)
async def run_keyword(self, params: RunKeywordParams) -> RunKeywordResult:
if not self.initialize_received or not self.initialized_received:
raise Exception("Initialize and Initialized must be received before RunKeyword.")
print(f"Running keyword: {params.name} with args: {params.args} and kwargs: {params.kwargs}")
await self.log("RUN_KEYWORD_REQUEST processing...", console=True)
if params.name == "echo":
message = params.args[0] if params.args else params.kwargs.get("message", "") if params.kwargs else ""
result = message
for i in range(3):
await self.log(f"Echo {i} request received with params: {params}", level=LogLevel.WARN, html=True)
return RunKeywordResult(result=result)
return RunKeywordResult(error=RunKeywordError(message=f"Keyword '{params.name}' not found."))
@rpc_request(FINALIZE_LIBRARY_REQUEST)
async def finalize_library(self, params: FinalizeLibraryParams) -> FinalizeLibraryResult:
print(f"Finalizing library: {params.token}")
return FinalizeLibraryResult()
@rpc_request(SHUTDOWN_REQUEST)
async def shutdown(self, params: ShutdownParams) -> ShutdownResult:
print("Shutdown requested")
return ShutdownResult()
@rpc_notification(EXIT_NOTIFICATION)
async def exit(self, params: ExitParams) -> None:
print("Exit received, closing the connection")
# The end of the input ends the peer's read loop, and the peer closes the connection.
if self.rpc_peer:
self.rpc_peer.reader.feed_eof()
async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
addr = writer.get_extra_info("peername")
print(f"Client connected from {addr}")
peer = JsonRpcPeer(reader, writer)
endpoint = SimpleRobotServerEndpoint()
peer.attach_endpoint(endpoint)
try:
await peer.run()
except Exception as e:
print(f"Error handling client from {addr}: {type(e).__name__}: {e}")
finally:
print(f"Client from {addr} disconnected")
async def run_server() -> None:
server = await asyncio.start_server(handle_client, "127.0.0.1", 8271)
async with server:
print("Server started on 127.0.0.1:8271")
await server.serve_forever()
def run() -> None:
asyncio.run(run_server())
if __name__ == "__main__":
run()

A suite uses it like any other server:

echo.robot
*** Settings ***
Library JsonRpcRemote uri=tcp://127.0.0.1:8271 library_name=SimpleRobotLibrary
*** Test Cases ***
Echo Returns The Message
${result}= Echo Hello World
Should Be Equal ${result} Hello World

In a clone of the repository, uv run simple-robot-jsonrpcserver starts the example server on 127.0.0.1:8271. The suite is in docs/examples/own-server in the repository.