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.
Framing
Section titled “Framing”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-Lengthis the length of the content in bytes. The reference implementation ends the content with\r\nand counts it.Content-Typemay give acharsetfor the content; the default is UTF-8. Other headers are ignored.
Values
Section titled “Values”- 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_libraryare 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 notnull, 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.
Methods
Section titled “Methods”| 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
Section titled “A session”A session is one connection, on which the client imports one library:
- The client connects and sends
robot/initialize. The server answers with its capabilities and information about itself. - The client sends
robot/initialized. The reference server refusesrobot/import_libraryandrobot/run_keywordbefore it has received both. - 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. - For each keyword call, the client sends
robot/run_keywordwith the token. While the keyword runs, the server sends its log messages asrobot/log, which the client logs under the keyword; then it sends the response. - When the session ends, the client sends
robot/finalize_librarywith the token, thenrobot/shutdown, thenrobot/exitif the server announcedsupport_exit, and closes the connection.
Data structures
Section titled “Data structures”Initialization
Section titled “Initialization”-
InitializeParamscapabilities: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. -
InitializeResultcapabilities:ServerCapabilities(optional): what the server supports.server_info:ServerInfo(optional): name and version of the server.
-
ClientInfo,ServerInfoname: string.version: string.
-
ClientCapabilitiessupports_log: boolean (optional).supports_trace: boolean (optional).
The client sends both unset, and the reference server does not read them.
-
ServerCapabilitiessupport_exit: boolean (optional): whether the server handlesrobot/exit. The client sendsrobot/exitonly if it istrue.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.
Library import
Section titled “Library import”ImportLibraryParamsname: 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 asname=value, which converts them according to the library’s__init__.
ImportLibraryResulttoken: string: identifies the imported library instance in later requests. The server chooses it; it is unique for each import.definition:LibraryDefinition.
FinalizeLibraryParamstoken: string: the instance to release.
FinalizeLibraryResult: empty.
Library definition
Section titled “Library definition”LibraryDefinitionname: string.keywords: list ofKeywordDefinition.doc: string (optional): the library documentation.doc_format: string (optional):ROBOT,REST,HTMLorTEXT.scope: string (optional):GLOBAL,SUITEorTEST.version: string (optional).source: string (optional): the path of the library’s source file.lineno: integer (optional): the line where the library is defined.
KeywordDefinitionname: string.args: list ofArgumentDefinition.doc: string (optional).tags: list of strings (optional).source: string (optional).lineno: integer (optional).
ArgumentDefinitionname: 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. Iftrue,defaultholds it, which may benull.default: any value (optional).required: boolean: reserved, alwaystrue. Usehas_defaultinstead.kind:ArgumentKind(optional), a string:POSITIONAL_ONLY,POSITIONAL_OR_NAMED,POSITIONAL_ONLY_MARKER(the/),NAMED_ONLY_MARKER(the*),VAR_POSITIONAL(*args),NAMED_ONLYorVAR_NAMED(**kwargs).doc: string (optional).
Keyword calls
Section titled “Keyword calls”-
RunKeywordParamslibrary_token: string: the token fromImportLibraryResult.name: string: the keyword name from the library definition.args: list (optional): positional arguments.kwargs: object (optional): named arguments.
-
RunKeywordResultresult: any value (optional): the return value of the keyword.error:RunKeywordError(optional): set if the keyword failed.
-
RunKeywordErrormessage: string.type: string (optional): the name of the exception, such as"AssertionError".traceback: string (optional).mode:RunKeywordErrorMode(optional), a string:CONTINUABLE,FATALorSKIP.
The reference server sets
message, and the client fails the keyword with the error.
Log messages
Section titled “Log messages”LogParamsmessage: string.level:LogLevel(optional), a string:TRACE,DEBUG,INFO,WARN,ERROR,CONSOLEorHTML. Without it, the client logs atINFO.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.
End of a session
Section titled “End of a session”ShutdownParams,ShutdownResult: empty. The client announces that it is about to close the connection.ExitParams: empty. Sent withrobot/exit; the server then closes the connection.
Errors
Section titled “Errors”- A keyword that fails is not a JSON-RPC error: the response is a
RunKeywordResultwitherror. - 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) andARGUMENT_MISMATCH(-32002). The reference implementation answers with the standard error codes of JSON-RPC.
Example: a hand-written server
Section titled “Example: a hand-written server”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:
import asyncioimport itertools
from jsonrpcpeer import JsonRpcPeer, rpc_notification, rpc_requestfrom 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:
*** 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 WorldIn 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.