rcp-protocol 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
rcp/__init__.py ADDED
@@ -0,0 +1,66 @@
1
+ """rcp — the Retrieval Context Protocol, a native Python SDK.
2
+
3
+ Pure Python, zero dependencies (standard library only): no compiler, no C++, no
4
+ build step to ``pip install``. RCP is an open, versioned JSON-RPC protocol so any
5
+ RAG engine — any language, any vendor — can expose embed / rerank / retrieve /
6
+ graph / index / catalog, and any client can consume it uniformly. This SDK speaks
7
+ the exact same wire format as the type-theoretic C++ SDK, so a Python client and
8
+ a C++ server (or vice-versa) interoperate byte-for-byte.
9
+
10
+ Client (connects to any RCP server, over a subprocess or HTTP):
11
+
12
+ import rcp
13
+ c = rcp.connect_stdio(["python3", "my_server.py"])
14
+ print(c.server, c.capabilities)
15
+ if c.supports(rcp.Capability.Retrieve):
16
+ for hit in c.retrieve("eiffel tower", k=3):
17
+ print(hit["id"], hit["score"])
18
+
19
+ Server (expose a Python RAG engine as an RCP server):
20
+
21
+ import rcp
22
+ s = rcp.Server()
23
+ s.set_info("my-engine", "1.0")
24
+ s.advertise(rcp.Capability.Retrieve, {"maxK": 100, "modes": ["hybrid"]})
25
+
26
+ @s.on("retrieve")
27
+ def _(params):
28
+ hits = my_index.search(params["query"], params.get("k", 10))
29
+ return {"hits": [{"id": h.id, "score": h.score, "text": h.text} for h in hits]}
30
+
31
+ s.serve_stdio()
32
+ """
33
+ from ._client import Client
34
+ from ._selector import EngineSpec, Selector
35
+ from ._server import Server, make_log_notification, make_progress_notification
36
+ from ._transport import HttpTransport, StdioTransport
37
+ from ._types import (
38
+ PROTOCOL_VERSION,
39
+ Capability,
40
+ Errc,
41
+ Method,
42
+ RcpError,
43
+ negotiate_version,
44
+ )
45
+
46
+ __all__ = [
47
+ "Client", "Server", "Selector", "EngineSpec", "Capability", "PROTOCOL_VERSION",
48
+ "Method", "Errc", "RcpError", "connect_stdio", "connect_http",
49
+ "make_log_notification", "make_progress_notification",
50
+ "StdioTransport", "HttpTransport", "negotiate_version",
51
+ ]
52
+ __version__ = "1.0.0"
53
+
54
+
55
+ def connect_stdio(argv, **kwargs) -> Client:
56
+ """Spawn an RCP server subprocess and connect. `argv` e.g. ["python3","srv.py"]."""
57
+ c = Client()
58
+ c.connect_stdio(list(argv))
59
+ return c
60
+
61
+
62
+ def connect_http(base_url: str, **kwargs) -> Client:
63
+ """Connect to an HTTP RCP server, e.g. "http://127.0.0.1:8000/rcp"."""
64
+ c = Client()
65
+ c.connect_http(base_url)
66
+ return c
rcp/_client.py ADDED
@@ -0,0 +1,222 @@
1
+ """RCP client — connect to any RCP server (subprocess or HTTP), then typed calls.
2
+
3
+ Mirrors the C++ ``rcp::Client`` (``sdk/cpp/include/rcp/client.hpp``): connecting
4
+ performs the ``initialize`` handshake and caches the negotiated protocol version,
5
+ server identity, and capabilities. Every typed call is **capability-gated** — a
6
+ call to a feature the server never advertised raises before any round-trip.
7
+
8
+ Errors raise :class:`RcpError` (a ``RuntimeError``) with ``str(e)`` of the form
9
+ ``"[RCP <code>] <message>"``.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+
15
+ from ._transport import HttpTransport, StdioTransport
16
+ from ._types import (
17
+ MIN_PROTOCOL_VERSION,
18
+ PROTOCOL_VERSION,
19
+ Capability,
20
+ Errc,
21
+ Method,
22
+ RcpError,
23
+ cap_key,
24
+ )
25
+
26
+ _CLIENT_INFO = {"name": "rcp-python", "version": "1.0"}
27
+
28
+
29
+ def _hit_to_dict(h) -> dict:
30
+ """Normalise one retrieval Hit to ``{id, score, text[, citation]}`` — the
31
+ canonical hit shape (id coerced to str, extra fields dropped)."""
32
+ if not isinstance(h, dict):
33
+ return {"id": "", "score": 0.0, "text": str(h)}
34
+ raw_id = h.get("id", "")
35
+ hit_id = raw_id if isinstance(raw_id, str) else json.dumps(raw_id, separators=(",", ":"))
36
+ out = {"id": hit_id, "score": float(h.get("score", 0.0)), "text": h.get("text", "")}
37
+ if h.get("citation") is not None:
38
+ out["citation"] = h["citation"]
39
+ return out
40
+
41
+
42
+ class Client:
43
+ """A connected RCP client. Obtain one via :func:`rcp.connect_stdio` /
44
+ :func:`rcp.connect_http`, or construct and call ``connect_stdio`` /
45
+ ``connect_http`` directly — either style works."""
46
+
47
+ def __init__(self):
48
+ self._transport = None
49
+ self._next_id = 0
50
+ self._protocol_version = PROTOCOL_VERSION
51
+ self._server = {"name": "unknown", "version": "0"}
52
+ self._caps: dict = {}
53
+
54
+ # ── connection ──────────────────────────────────────────────────────────
55
+ def connect_stdio(self, argv) -> "Client":
56
+ self._transport = StdioTransport.spawn(list(argv))
57
+ self._handshake()
58
+ return self
59
+
60
+ def connect_http(self, base_url: str) -> "Client":
61
+ self._transport = HttpTransport(base_url)
62
+ self._handshake()
63
+ return self
64
+
65
+ def _handshake(self) -> None:
66
+ res = self._request(Method.INITIALIZE, {
67
+ "protocolVersion": PROTOCOL_VERSION,
68
+ "client": dict(_CLIENT_INFO),
69
+ "capabilities": {},
70
+ })
71
+ pv = res.get("protocolVersion", PROTOCOL_VERSION)
72
+ if not isinstance(pv, int) or pv < MIN_PROTOCOL_VERSION:
73
+ raise RcpError(Errc.VERSION_MISMATCH, "server offered no usable protocol version")
74
+ self._protocol_version = pv
75
+ srv = res.get("server", {}) or {}
76
+ self._server = {"name": srv.get("name", "unknown"), "version": srv.get("version", "0")}
77
+ self._caps = res.get("capabilities", {}) or {}
78
+
79
+ # ── introspection ───────────────────────────────────────────────────────
80
+ @property
81
+ def protocol_version(self) -> int:
82
+ return self._protocol_version
83
+
84
+ @property
85
+ def server(self) -> dict:
86
+ return self._server
87
+
88
+ @property
89
+ def capabilities(self) -> dict:
90
+ return self._caps
91
+
92
+ def supports(self, capability) -> bool:
93
+ return self._caps.get(cap_key(capability)) is not None
94
+
95
+ # ── typed, capability-gated calls ───────────────────────────────────────
96
+ def embed(self, texts, kind=None):
97
+ """Dense embeddings → ``list[list[float]]``. ``kind`` ("query"|"document")
98
+ selects asymmetric pooling."""
99
+ self._gate(Capability.Embed)
100
+ p = {"inputs": list(texts)}
101
+ if kind:
102
+ p["kind"] = kind
103
+ r = self._request(Method.EMBED, p)
104
+ vecs = r.get("vectors", r) if isinstance(r, dict) else r
105
+ return [[float(x) for x in row] for row in vecs]
106
+
107
+ def embed_sparse(self, texts):
108
+ """Learned-sparse (SPLADE) terms → raw result ``{sparse: [...]}``."""
109
+ self._gate(Capability.SparseEmbed)
110
+ return self._request(Method.EMBED_SPARSE, {"inputs": list(texts)})
111
+
112
+ def embed_multi(self, inputs):
113
+ """Per-token multi-vector (ColBERT/ColPali) → ``{matrices, dimension}``."""
114
+ self._gate(Capability.MultiVector)
115
+ return self._request(Method.EMBED_MULTI, {"inputs": list(inputs)})
116
+
117
+ def rerank(self, query, passages):
118
+ """Cross-encoder rerank → ``list[float]`` scores (one per passage)."""
119
+ self._gate(Capability.Rerank)
120
+ r = self._request(Method.RERANK, {"query": query, "passages": list(passages)})
121
+ scores = r.get("scores", r) if isinstance(r, dict) else r
122
+ return [float(x) for x in scores]
123
+
124
+ def search(self, query, k=10, opts=None):
125
+ """Full retrieve → ``{hits, usage, next_cursor}`` (spec §7.6)."""
126
+ self._gate(Capability.Retrieve)
127
+ if k < 1:
128
+ raise RcpError(Errc.INVALID_PARAMS, "value must be >= 1")
129
+ p = dict(opts) if opts else {}
130
+ p["query"] = query
131
+ p["k"] = k
132
+ r = self._request(Method.RETRIEVE, p)
133
+ hits = r.get("hits", []) if isinstance(r, dict) else []
134
+ return {
135
+ "hits": [_hit_to_dict(h) for h in hits],
136
+ "usage": (r.get("usage", {}) if isinstance(r, dict) else {}),
137
+ "next_cursor": (r.get("nextCursor") if isinstance(r, dict) else None),
138
+ }
139
+
140
+ def retrieve(self, query, k=10, opts=None):
141
+ """Convenience over :meth:`search` → just the ``list[dict]`` of hits."""
142
+ return self.search(query, k, opts)["hits"]
143
+
144
+ def graph(self, op, params=None):
145
+ """GraphRAG op ("local"|"global"|...) → raw result dict (spec §7.9)."""
146
+ self._gate(Capability.Graph)
147
+ p = dict(params) if params else {}
148
+ p["op"] = op
149
+ return self._request(Method.GRAPH, p)
150
+
151
+ def transform(self, query, method):
152
+ """Query transform (HyDE, expansion, ...) → raw result (spec §7.8)."""
153
+ self._gate(Capability.Transform)
154
+ return self._request(Method.TRANSFORM, {"query": query, "method": method})
155
+
156
+ def index_add(self, params=None):
157
+ """Add documents to the corpus (spec §7.10). ``params`` e.g. ``{documents:[...]}``."""
158
+ self._gate(Capability.Index)
159
+ return self._request(Method.INDEX_ADD, dict(params) if params else {})
160
+
161
+ def index_delete(self, params=None):
162
+ """Delete documents by id (spec §7.11). ``params`` e.g. ``{ids:[...]}``."""
163
+ self._gate(Capability.Index)
164
+ return self._request(Method.INDEX_DELETE, dict(params) if params else {})
165
+
166
+ def catalog(self):
167
+ """List available sub-indexes / collections (spec §7.12)."""
168
+ self._gate(Capability.Catalog)
169
+ return self._request(Method.CATALOG_LIST, {})
170
+
171
+ def info(self):
172
+ """Re-fetch server identity + capabilities without re-initialising."""
173
+ r = self._request(Method.INFO, {})
174
+ srv = r.get("server", {}) or {}
175
+ self._server = {"name": srv.get("name", "unknown"), "version": srv.get("version", "0")}
176
+ self._caps = r.get("capabilities", {}) or {}
177
+ return r
178
+
179
+ def call(self, method, params=None):
180
+ """Escape hatch: invoke an arbitrary method with raw params."""
181
+ return self._request(method, params if params is not None else {})
182
+
183
+ def ping(self, nonce=None):
184
+ """Liveness check; echoes ``{nonce}`` if a nonce is given (spec §9)."""
185
+ p = {}
186
+ if nonce is not None:
187
+ p["nonce"] = nonce
188
+ return self._request(Method.PING, p)
189
+
190
+ def shutdown(self) -> None:
191
+ """Politely ask the server to stop, then close the transport. Never raises."""
192
+ if self._transport is None:
193
+ return
194
+ try:
195
+ self._request(Method.SHUTDOWN, {})
196
+ except RcpError:
197
+ pass
198
+ finally:
199
+ self._transport.close()
200
+ self._transport = None
201
+
202
+ # ── internals ───────────────────────────────────────────────────────────
203
+ def _gate(self, capability) -> None:
204
+ if not self.supports(capability):
205
+ raise RcpError(Errc.CAPABILITY_MISSING,
206
+ f"server does not advertise '{cap_key(capability)}'")
207
+
208
+ def _request(self, method, params):
209
+ if self._transport is None:
210
+ raise RcpError(Errc.BACKEND_UNAVAILABLE, "client is not connected")
211
+ self._next_id += 1
212
+ reply = self._transport.call({
213
+ "jsonrpc": "2.0", "id": self._next_id, "method": method, "params": params,
214
+ })
215
+ if isinstance(reply, dict):
216
+ err = reply.get("error")
217
+ if err is not None:
218
+ raise RcpError(err.get("code", Errc.INTERNAL_ERROR),
219
+ err.get("message", "error"), err.get("data"))
220
+ if "result" in reply:
221
+ return reply["result"]
222
+ return {}
rcp/_selector.py ADDED
@@ -0,0 +1,143 @@
1
+ """RCP Selector — choose ONE backend from several reachable engines.
2
+
3
+ Mirrors ``rcp::Selector`` (``sdk/cpp/include/rcp/selector.hpp``): you list
4
+ candidate engines (by hand or from an ``rcp.json`` registry) and pick one — by
5
+ id, by required capability, or by priority with liveness fallback. Connection is
6
+ **lazy**: an engine is dialled only when actually selected. The result is a
7
+ fully-connected :class:`~rcp._client.Client`.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+
13
+ from ._client import Client
14
+ from ._types import Errc, RcpError, cap_key
15
+
16
+
17
+ class EngineSpec:
18
+ """How to reach one backend. Inert until selected; exactly one of ``argv``
19
+ (stdio subprocess) or ``url`` (HTTP) is set."""
20
+
21
+ def __init__(self, id, argv=None, url="", priority=0, weight=1.0, required=False):
22
+ self.id = id
23
+ self.argv = list(argv) if argv else []
24
+ self.url = url
25
+ self.priority = priority
26
+ self.weight = weight
27
+ self.required = required
28
+
29
+ def open(self) -> Client:
30
+ c = Client()
31
+ if self.argv:
32
+ return c.connect_stdio(self.argv)
33
+ if self.url:
34
+ return c.connect_http(self.url)
35
+ raise RcpError(Errc.INVALID_PARAMS, f"engine '{self.id}' has no transport")
36
+
37
+ @staticmethod
38
+ def from_json(entry) -> "EngineSpec":
39
+ """Parse one registry entry (spec §16.1). ``transport`` is "stdio" (needs
40
+ ``command``) or "http" (needs ``url``); inferred if omitted."""
41
+ if not isinstance(entry, dict) or "id" not in entry:
42
+ raise RcpError(Errc.INVALID_PARAMS, "registry entry needs an 'id'")
43
+ transport = entry.get("transport", "")
44
+ command = entry.get("command") or entry.get("argv")
45
+ url = entry.get("url", "")
46
+ spec = EngineSpec(
47
+ id=entry.get("id", ""),
48
+ priority=entry.get("priority", 0),
49
+ weight=entry.get("weight", 1.0),
50
+ required=entry.get("required", False),
51
+ )
52
+ if transport == "stdio" or (not transport and command):
53
+ if not isinstance(command, list) or not command:
54
+ raise RcpError(Errc.INVALID_PARAMS,
55
+ f"stdio engine '{spec.id}' needs a 'command' array")
56
+ spec.argv = [str(x) for x in command]
57
+ elif transport == "http" or (not transport and url):
58
+ if not url:
59
+ raise RcpError(Errc.INVALID_PARAMS, f"http engine '{spec.id}' needs a 'url'")
60
+ spec.url = url
61
+ else:
62
+ raise RcpError(Errc.INVALID_PARAMS,
63
+ f"engine '{spec.id}' has no usable transport")
64
+ return spec
65
+
66
+
67
+ class Selector:
68
+ """Registry of candidate engines; selection connects and returns a Client."""
69
+
70
+ def __init__(self):
71
+ self._specs: list[EngineSpec] = []
72
+
73
+ # ── construction ────────────────────────────────────────────────────────
74
+ @staticmethod
75
+ def load(path: str) -> "Selector":
76
+ """Build a Selector from a registry JSON file."""
77
+ with open(path, "r", encoding="utf-8") as f:
78
+ return Selector.loads(f.read())
79
+
80
+ @staticmethod
81
+ def loads(text: str) -> "Selector":
82
+ """Build a Selector from a registry JSON string
83
+ (``{"engines": [ ... ]}``)."""
84
+ try:
85
+ doc = json.loads(text)
86
+ except ValueError as e:
87
+ raise RcpError(Errc.PARSE_ERROR, f"invalid registry JSON: {e}")
88
+ engines = doc.get("engines", []) if isinstance(doc, dict) else []
89
+ sel = Selector()
90
+ for entry in engines:
91
+ sel._specs.append(EngineSpec.from_json(entry))
92
+ return sel
93
+
94
+ def add_stdio(self, id, argv, priority=0) -> "Selector":
95
+ self._specs.append(EngineSpec(id, argv=list(argv), priority=priority))
96
+ return self
97
+
98
+ def add_http(self, id, url, priority=0) -> "Selector":
99
+ self._specs.append(EngineSpec(id, url=url, priority=priority))
100
+ return self
101
+
102
+ @property
103
+ def size(self) -> int:
104
+ return len(self._specs)
105
+
106
+ # ── selection ───────────────────────────────────────────────────────────
107
+ def select(self, id) -> Client:
108
+ """Connect the engine with the given id (or raise if unknown)."""
109
+ for spec in self._specs:
110
+ if spec.id == id:
111
+ return spec.open()
112
+ raise RcpError(Errc.INVALID_PARAMS, f"no engine with id '{id}'")
113
+
114
+ def select_primary(self) -> Client:
115
+ """Connect the highest-priority engine that answers (liveness fallback)."""
116
+ last = None
117
+ for spec in self._by_priority():
118
+ try:
119
+ return spec.open()
120
+ except RcpError as e:
121
+ last = e
122
+ raise last or RcpError(Errc.BACKEND_UNAVAILABLE, "no engines configured")
123
+
124
+ def select_capable(self, capability) -> Client:
125
+ """Connect the highest-priority engine that answers AND advertises
126
+ ``capability``."""
127
+ last = None
128
+ for spec in self._by_priority():
129
+ try:
130
+ c = spec.open()
131
+ except RcpError as e:
132
+ last = e
133
+ continue
134
+ if c.supports(capability):
135
+ return c
136
+ c.shutdown()
137
+ last = RcpError(Errc.CAPABILITY_MISSING,
138
+ f"engine '{spec.id}' lacks '{cap_key(capability)}'")
139
+ raise last or RcpError(Errc.BACKEND_UNAVAILABLE, "no capable engine")
140
+
141
+ def _by_priority(self) -> list[EngineSpec]:
142
+ # Stable sort by descending priority (Python's sort is stable).
143
+ return sorted(self._specs, key=lambda s: s.priority, reverse=True)
rcp/_server.py ADDED
@@ -0,0 +1,269 @@
1
+ """RCP server — expose a Python RAG engine as an RCP/1 server.
2
+
3
+ Mirrors the C++ ``rcp::Server`` (``sdk/cpp/include/rcp/server.hpp``): it owns the
4
+ JSON-RPC framing, the ``initialize`` handshake, capability gating, and error
5
+ mapping; you provide method handlers (``on``) and identity/capabilities
6
+ (``set_info`` / ``advertise``).
7
+
8
+ Dispatch rules replicated exactly:
9
+
10
+ * ``initialize`` / ``info`` / ``ping`` / ``shutdown`` / ``notifications/cancel``
11
+ are ungated and answer before initialize.
12
+ * A gated method before ``initialize`` → **-32001** NotInitialized.
13
+ * A method whose capability is unadvertised or whose handler is unregistered →
14
+ **-32003** CapabilityMissing.
15
+ * An unknown method → **-32004** UnknownMethod. A malformed request → **-32600**.
16
+ * Batches (JSON array) yield one response per request; notifications drop out.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import socket
22
+
23
+ from ._types import (
24
+ CAP_FOR_METHOD,
25
+ MIN_PROTOCOL_VERSION,
26
+ PROTOCOL_VERSION,
27
+ Capability,
28
+ Errc,
29
+ Method,
30
+ RcpError,
31
+ cap_key,
32
+ negotiate_version,
33
+ )
34
+
35
+ _COMPACT = (",", ":")
36
+ _KNOWN_METHODS = set(CAP_FOR_METHOD)
37
+
38
+
39
+ def _dumps(obj) -> str:
40
+ return json.dumps(obj, separators=_COMPACT)
41
+
42
+
43
+ def _ok(id_, result) -> dict:
44
+ return {"jsonrpc": "2.0", "id": id_, "result": result}
45
+
46
+
47
+ def _err(id_, code: int, message: str, data=None) -> dict:
48
+ error = {"code": code, "message": message}
49
+ if data is not None:
50
+ error["data"] = data
51
+ return {"jsonrpc": "2.0", "id": id_, "error": error}
52
+
53
+
54
+ def make_log_notification(level: str, message: str, data=None) -> str:
55
+ """Build a ``notifications/log`` frame (spec §17.1) for a server to emit on
56
+ its output stream. ``level`` ∈ debug|info|notice|warning|error."""
57
+ params = {"level": level, "message": message}
58
+ if data is not None:
59
+ params["data"] = data
60
+ return _dumps({"jsonrpc": "2.0", "method": Method.LOG, "params": params})
61
+
62
+
63
+ def make_progress_notification(token, progress: float, stage: str = "", partial=None) -> str:
64
+ """Build a ``notifications/progress`` frame (spec §9) during a long retrieve.
65
+ ``token`` echoes the request's ``_meta.progressToken``; ``progress`` is 0..1."""
66
+ params = {"progressToken": token, "progress": progress}
67
+ if stage:
68
+ params["stage"] = stage
69
+ if partial is not None:
70
+ params["partial"] = partial
71
+ return _dumps({"jsonrpc": "2.0", "method": Method.PROGRESS, "params": params})
72
+
73
+
74
+ class Server:
75
+ """An RCP/1 server. Register handlers with :meth:`on` (usable as a decorator),
76
+ then serve over stdio or HTTP — or drive :meth:`handle` directly for tests."""
77
+
78
+ def __init__(self):
79
+ self._info = {"name": "unknown", "version": "0"}
80
+ self._caps: dict = {} # wire JSON key -> metadata object
81
+ self._handlers: dict = {} # method string -> callable(params) -> result
82
+ self._initialized = False
83
+
84
+ # ── configuration ───────────────────────────────────────────────────────
85
+ def set_info(self, name: str, version: str) -> None:
86
+ self._info = {"name": str(name), "version": str(version)}
87
+
88
+ def advertise(self, capability, meta=None) -> None:
89
+ """Advertise a capability with optional metadata (stored under its wire
90
+ key). Advertising is independent of registering a handler; a gated call
91
+ needs both or it is CapabilityMissing."""
92
+ self._caps[cap_key(capability)] = {} if meta is None else meta
93
+
94
+ def on(self, method, fn=None):
95
+ """Register a handler for ``method``. Works as a decorator
96
+ (``@s.on("retrieve")``) or directly (``s.on("retrieve", fn)``). The
97
+ handler takes the params dict and returns the result object."""
98
+ name = method.value if isinstance(method, Capability) else str(method)
99
+ if name not in _KNOWN_METHODS:
100
+ raise ValueError(f"unknown method hook: {name!r} "
101
+ f"(expected one of {sorted(_KNOWN_METHODS)})")
102
+ if fn is None:
103
+ def deco(f):
104
+ self._handlers[name] = f
105
+ return f
106
+ return deco
107
+ self._handlers[name] = fn
108
+ return fn
109
+
110
+ # ── dispatch ─────────────────────────────────────────────────────────────
111
+ def handle(self, request):
112
+ """Handle one JSON-RPC request object → reply dict, or ``None`` for a
113
+ notification that warrants no response. Never raises."""
114
+ id_ = request.get("id") if isinstance(request, dict) else None
115
+ if not isinstance(request, dict) or not isinstance(request.get("method"), str):
116
+ return _err(id_, Errc.INVALID_REQUEST, "missing 'method'")
117
+ m = request["method"]
118
+ params = request["params"] if "params" in request else {}
119
+
120
+ if m == Method.INITIALIZE:
121
+ self._initialized = True
122
+ neg = negotiate_version(params.get("protocolVersion", PROTOCOL_VERSION)
123
+ if isinstance(params, dict) else PROTOCOL_VERSION)
124
+ if neg < MIN_PROTOCOL_VERSION:
125
+ return _err(id_, Errc.VERSION_MISMATCH, "no common version")
126
+ return _ok(id_, self._info_result(neg))
127
+ if m == Method.INFO:
128
+ return _ok(id_, self._info_result(PROTOCOL_VERSION))
129
+ if m == Method.PING:
130
+ has_nonce = isinstance(params, dict) and "nonce" in params
131
+ return _ok(id_, {"nonce": params["nonce"]} if has_nonce else {})
132
+ if m == Method.CANCEL:
133
+ return None # cancellation is a notification: never answered
134
+ if m == Method.SHUTDOWN:
135
+ return _ok(id_, {})
136
+
137
+ if not self._initialized:
138
+ return _err(id_, Errc.NOT_INITIALIZED, "call 'initialize' first")
139
+
140
+ return self._dispatch(id_, m, params if isinstance(params, dict) else {})
141
+
142
+ def _dispatch(self, id_, m, params):
143
+ cap = CAP_FOR_METHOD.get(m)
144
+ if cap is None:
145
+ return _err(id_, Errc.UNKNOWN_METHOD, f"unknown method '{m}'")
146
+ fn = self._handlers.get(m)
147
+ if fn is None:
148
+ return _err(id_, Errc.CAPABILITY_MISSING, f"'{m}' not implemented")
149
+ if self._caps.get(cap.value) is None:
150
+ return _err(id_, Errc.CAPABILITY_MISSING, f"capability '{cap.value}' not supported")
151
+ try:
152
+ result = fn(params)
153
+ except RcpError as e:
154
+ return _err(id_, e.code, e.message, e.data)
155
+ except Exception as e: # a handler bug is an Internal error, never a crash
156
+ return _err(id_, Errc.INTERNAL_ERROR, str(e))
157
+ return _ok(id_, result if result is not None else {})
158
+
159
+ def handle_line(self, line: str) -> str:
160
+ """Parse a JSON line, dispatch (single or batch), and return the reply
161
+ string ("" when there is no response, e.g. an all-notification batch)."""
162
+ try:
163
+ msg = json.loads(line)
164
+ except ValueError as e:
165
+ return _dumps(_err(None, Errc.PARSE_ERROR, str(e)))
166
+
167
+ if isinstance(msg, list):
168
+ if not msg:
169
+ return _dumps(_err(None, Errc.INVALID_REQUEST, "empty batch"))
170
+ out = []
171
+ for el in msg:
172
+ reply = self.handle(el)
173
+ if reply is not None:
174
+ out.append(reply)
175
+ return _dumps(out) if out else ""
176
+
177
+ reply = self.handle(msg)
178
+ return _dumps(reply) if reply is not None else ""
179
+
180
+ def _info_result(self, version: int) -> dict:
181
+ return {
182
+ "protocolVersion": version,
183
+ "server": dict(self._info),
184
+ "capabilities": dict(self._caps),
185
+ }
186
+
187
+ # ── serving ──────────────────────────────────────────────────────────────
188
+ def serve_stdio(self) -> None:
189
+ """Serve newline-delimited JSON-RPC over stdin/stdout until shutdown/EOF."""
190
+ import sys
191
+
192
+ stdin = sys.stdin.buffer
193
+ stdout = sys.stdout.buffer
194
+ for raw in stdin:
195
+ line = raw.decode("utf-8", "replace").strip()
196
+ if not line:
197
+ continue
198
+ is_shutdown = False
199
+ try:
200
+ parsed = json.loads(line)
201
+ is_shutdown = isinstance(parsed, dict) and parsed.get("method") == Method.SHUTDOWN
202
+ except ValueError:
203
+ pass
204
+ reply = self.handle_line(line)
205
+ if reply:
206
+ try:
207
+ stdout.write((reply + "\n").encode("utf-8"))
208
+ stdout.flush()
209
+ except (BrokenPipeError, OSError):
210
+ break # peer gone
211
+ if is_shutdown:
212
+ break
213
+
214
+ def serve_http(self, port: int) -> None:
215
+ """Serve JSON-RPC over minimal HTTP/1.1 on ``127.0.0.1:port``. A response
216
+ returns 200; a notification (no reply) returns 204. Loops until killed."""
217
+ srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
218
+ srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
219
+ srv.bind(("127.0.0.1", port))
220
+ srv.listen(16)
221
+ try:
222
+ while True:
223
+ conn, _ = srv.accept()
224
+ try:
225
+ self._serve_http_conn(conn)
226
+ except OSError:
227
+ pass
228
+ finally:
229
+ conn.close()
230
+ finally:
231
+ srv.close()
232
+
233
+ def _serve_http_conn(self, conn: socket.socket) -> None:
234
+ conn.settimeout(30.0)
235
+ buf = b""
236
+ while b"\r\n\r\n" not in buf:
237
+ chunk = conn.recv(4096)
238
+ if not chunk:
239
+ return
240
+ buf += chunk
241
+ head, _, rest = buf.partition(b"\r\n\r\n")
242
+ length = 0
243
+ for hl in head.split(b"\r\n")[1:]:
244
+ name, _, value = hl.partition(b":")
245
+ if name.strip().lower() == b"content-length":
246
+ try:
247
+ length = int(value.strip())
248
+ except ValueError:
249
+ length = 0
250
+ body = rest
251
+ while len(body) < length:
252
+ chunk = conn.recv(4096)
253
+ if not chunk:
254
+ break
255
+ body += chunk
256
+ text = body[:length].decode("utf-8", "replace") if length else (body.decode("utf-8", "replace") or "{}")
257
+ if not text.strip():
258
+ text = "{}"
259
+ reply = self.handle_line(text)
260
+ if reply:
261
+ payload = reply.encode("utf-8")
262
+ resp = (b"HTTP/1.1 200 OK\r\n"
263
+ b"Content-Type: application/json\r\n"
264
+ b"Content-Length: " + str(len(payload)).encode() + b"\r\n"
265
+ b"Connection: close\r\n\r\n" + payload)
266
+ else:
267
+ resp = (b"HTTP/1.1 204 No Content\r\n"
268
+ b"Connection: close\r\n\r\n")
269
+ conn.sendall(resp)
rcp/_transport.py ADDED
@@ -0,0 +1,146 @@
1
+ """RCP transports — the request/reply seam (subprocess pipe or HTTP/1.1).
2
+
3
+ A transport is a total function ``dict -> dict``: send one JSON-RPC request
4
+ object, return the reply object. Two concrete transports mirror the C++ SDK
5
+ (``sdk/cpp/include/rcp/transport.hpp``):
6
+
7
+ * :class:`StdioTransport` — newline-delimited JSON over a child's stdin/stdout.
8
+ * :class:`HttpTransport` — minimal HTTP/1.1 POST to ``<base>/<method>``.
9
+
10
+ Failures raise :class:`RcpError` with the same typed codes as the C++ SDK, so a
11
+ caller sees ``[RCP -32010] ...`` whether the fault was local or remote.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import subprocess
17
+ import urllib.parse
18
+ from http.client import HTTPConnection
19
+
20
+ from ._types import Errc, RcpError
21
+
22
+ _COMPACT = (",", ":") # compact JSON separators, matching nlohmann's dump()
23
+
24
+
25
+ def _dumps(obj) -> str:
26
+ return json.dumps(obj, separators=_COMPACT)
27
+
28
+
29
+ class StdioTransport:
30
+ """Newline-delimited JSON over a subprocess' stdio.
31
+
32
+ The client writes ``json + "\\n"`` and reads reply lines, **skipping** any
33
+ server-initiated notification (a frame carrying ``method``) and any stray
34
+ reply whose ``id`` does not match the request (spec §4.7).
35
+ """
36
+
37
+ def __init__(self, proc: subprocess.Popen):
38
+ self._proc = proc
39
+
40
+ @classmethod
41
+ def spawn(cls, argv) -> "StdioTransport":
42
+ argv = list(argv)
43
+ try:
44
+ proc = subprocess.Popen(
45
+ argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE, bufsize=0,
46
+ )
47
+ except OSError as e:
48
+ raise RcpError(Errc.BACKEND_UNAVAILABLE, f"cannot spawn {argv!r}: {e}")
49
+ return cls(proc)
50
+
51
+ def call(self, request: dict) -> dict:
52
+ line = (_dumps(request) + "\n").encode("utf-8")
53
+ try:
54
+ self._proc.stdin.write(line)
55
+ self._proc.stdin.flush()
56
+ except (BrokenPipeError, OSError):
57
+ raise RcpError(Errc.BACKEND_UNAVAILABLE, "write to server failed")
58
+
59
+ want_id = request.get("id")
60
+ while True:
61
+ raw = self._proc.stdout.readline()
62
+ if not raw:
63
+ raise RcpError(Errc.BACKEND_UNAVAILABLE, "server closed the connection")
64
+ try:
65
+ reply = json.loads(raw)
66
+ except ValueError as e:
67
+ raise RcpError(Errc.PARSE_ERROR, str(e))
68
+ if isinstance(reply, dict):
69
+ # Skip a server-initiated notification (has "method", no reply id
70
+ # to match) and a mismatched-id reply from an earlier request.
71
+ if "method" in reply:
72
+ continue
73
+ if want_id is not None and "id" in reply and reply["id"] != want_id:
74
+ continue
75
+ return reply
76
+
77
+ def close(self) -> None:
78
+ proc = self._proc
79
+ if proc.poll() is not None:
80
+ return
81
+ for stream in (proc.stdin, proc.stdout):
82
+ try:
83
+ if stream is not None:
84
+ stream.close()
85
+ except OSError:
86
+ pass
87
+ try:
88
+ proc.terminate()
89
+ proc.wait(timeout=2)
90
+ except Exception:
91
+ try:
92
+ proc.kill()
93
+ except Exception:
94
+ pass
95
+
96
+
97
+ class HttpTransport:
98
+ """Minimal HTTP/1.1 client: POST the request to ``<base_url>/<method>``.
99
+
100
+ The server ignores the path and dispatches on the body's ``method``; the path
101
+ is a human-readable convenience (e.g. ``POST /rcp/retrieve``). Maps HTTP 429
102
+ → RateLimited and 503 → BackendUnavailable (spec §5.2).
103
+ """
104
+
105
+ def __init__(self, base_url: str, timeout: float = 30.0):
106
+ self._base = base_url.rstrip("/")
107
+ self._timeout = timeout
108
+
109
+ def call(self, request: dict) -> dict:
110
+ method = request.get("method", "")
111
+ url = self._base + "/" + method
112
+ parts = urllib.parse.urlsplit(url)
113
+ if parts.scheme != "http":
114
+ raise RcpError(Errc.INVALID_PARAMS, f"unsupported URL scheme '{parts.scheme}'")
115
+ path = parts.path or "/"
116
+ if parts.query:
117
+ path += "?" + parts.query
118
+ body = _dumps(request).encode("utf-8")
119
+
120
+ conn = HTTPConnection(parts.hostname or "127.0.0.1", parts.port or 80,
121
+ timeout=self._timeout)
122
+ try:
123
+ conn.request("POST", path, body=body,
124
+ headers={"Content-Type": "application/json",
125
+ "Content-Length": str(len(body))})
126
+ resp = conn.getresponse()
127
+ status = resp.status
128
+ data = resp.read()
129
+ except OSError as e:
130
+ raise RcpError(Errc.BACKEND_UNAVAILABLE, f"HTTP request failed: {e}")
131
+ finally:
132
+ conn.close()
133
+
134
+ if status == 429:
135
+ raise RcpError(Errc.RATE_LIMITED, "HTTP 429")
136
+ if status == 503:
137
+ raise RcpError(Errc.BACKEND_UNAVAILABLE, "HTTP 503")
138
+ if (status < 200 or status >= 300) and not data.strip():
139
+ raise RcpError(Errc.BACKEND_UNAVAILABLE, f"HTTP {status}")
140
+ try:
141
+ return json.loads(data)
142
+ except ValueError as e:
143
+ raise RcpError(Errc.PARSE_ERROR, str(e))
144
+
145
+ def close(self) -> None: # HTTP is connectionless here; nothing to hold open.
146
+ pass
rcp/_types.py ADDED
@@ -0,0 +1,124 @@
1
+ """Core RCP/1 wire vocabulary — protocol version, capabilities, methods, errors.
2
+
3
+ Pure Python, no dependencies. The values here mirror the type-theoretic C++ SDK
4
+ (``sdk/cpp/include/rcp/{types,protocol}.hpp``) byte-for-byte on the wire, so a
5
+ Python client and a C++ server (or vice-versa) speak the exact same JSON-RPC.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import enum
10
+
11
+ # Protocol version negotiation (spec §7.1). We speak exactly version 1.
12
+ PROTOCOL_VERSION = 1
13
+ MIN_PROTOCOL_VERSION = 1
14
+
15
+
16
+ def negotiate_version(theirs) -> int:
17
+ """min(peer, ours), clamped — mirrors rcp::negotiate_version."""
18
+ try:
19
+ t = int(theirs)
20
+ except (TypeError, ValueError):
21
+ t = PROTOCOL_VERSION
22
+ return t if t < PROTOCOL_VERSION else PROTOCOL_VERSION
23
+
24
+
25
+ class Capability(enum.Enum):
26
+ """The capability lattice (spec §7.2). ``.value`` is the wire JSON key, so a
27
+ capability check is a total match on a typed enum, never string-poking."""
28
+ Embed = "embed"
29
+ SparseEmbed = "sparseEmbed"
30
+ MultiVector = "multiVector"
31
+ Rerank = "rerank"
32
+ Retrieve = "retrieve"
33
+ Transform = "transform"
34
+ Graph = "graph"
35
+ Index = "index"
36
+ Catalog = "catalog"
37
+
38
+
39
+ class Method:
40
+ """RCP/1 method names (spec §9). String constants — usable anywhere a wire
41
+ method name is expected (``s.on(Method.RETRIEVE)`` or ``s.on("retrieve")``)."""
42
+ INITIALIZE = "initialize"
43
+ INFO = "info"
44
+ EMBED = "embed"
45
+ EMBED_SPARSE = "embed/sparse"
46
+ EMBED_MULTI = "embed/multi"
47
+ RERANK = "rerank"
48
+ RETRIEVE = "retrieve"
49
+ TRANSFORM = "query/transform"
50
+ GRAPH = "graph"
51
+ INDEX_ADD = "index/add"
52
+ INDEX_DELETE = "index/delete"
53
+ CATALOG_LIST = "catalog/list"
54
+ CANCEL = "notifications/cancel"
55
+ PROGRESS = "notifications/progress"
56
+ LOG = "notifications/log"
57
+ PING = "ping"
58
+ SHUTDOWN = "shutdown"
59
+
60
+
61
+ class Errc:
62
+ """RCP/1 error codes (spec §12). -326xx are JSON-RPC; -320xx are RCP-specific."""
63
+ PARSE_ERROR = -32700
64
+ INVALID_REQUEST = -32600
65
+ METHOD_NOT_FOUND = -32601
66
+ INVALID_PARAMS = -32602
67
+ INTERNAL_ERROR = -32603
68
+ NOT_INITIALIZED = -32001
69
+ VERSION_MISMATCH = -32002
70
+ CAPABILITY_MISSING = -32003
71
+ UNKNOWN_METHOD = -32004
72
+ OPTION_UNSUPPORTED = -32005
73
+ CANCELLED = -32006
74
+ BACKEND_UNAVAILABLE = -32010
75
+ RATE_LIMITED = -32011
76
+
77
+
78
+ class RcpError(RuntimeError):
79
+ """An RCP protocol or transport error, raised by client calls.
80
+
81
+ Subclasses ``RuntimeError`` so existing ``except RuntimeError`` handlers keep
82
+ working; ``str(e)`` is ``"[RCP <code>] <message>"`` (stable and greppable).
83
+ Carries the structured ``data`` payload (spec §12) plus retry hints.
84
+ """
85
+
86
+ def __init__(self, code: int, message: str, data=None):
87
+ super().__init__(f"[RCP {code}] {message}")
88
+ self.code = code
89
+ self.message = message
90
+ self.data = data
91
+
92
+ def retryable(self) -> bool:
93
+ """RateLimited / BackendUnavailable are transient by default; an explicit
94
+ ``data.retryable`` overrides the code-based default (spec §12)."""
95
+ if isinstance(self.data, dict) and isinstance(self.data.get("retryable"), bool):
96
+ return self.data["retryable"]
97
+ return self.code in (Errc.RATE_LIMITED, Errc.BACKEND_UNAVAILABLE)
98
+
99
+ def retry_after_ms(self) -> int:
100
+ """Server-suggested backoff in ms (``data.retryAfterMs``), or -1."""
101
+ if isinstance(self.data, dict) and isinstance(self.data.get("retryAfterMs"), int):
102
+ return self.data["retryAfterMs"]
103
+ return -1
104
+
105
+
106
+ # Which capability gates which method (spec §7.2 / §9). Used by both the client
107
+ # (pre-flight gating) and the server (dispatch gating).
108
+ CAP_FOR_METHOD = {
109
+ Method.EMBED: Capability.Embed,
110
+ Method.EMBED_SPARSE: Capability.SparseEmbed,
111
+ Method.EMBED_MULTI: Capability.MultiVector,
112
+ Method.RERANK: Capability.Rerank,
113
+ Method.RETRIEVE: Capability.Retrieve,
114
+ Method.TRANSFORM: Capability.Transform,
115
+ Method.GRAPH: Capability.Graph,
116
+ Method.INDEX_ADD: Capability.Index,
117
+ Method.INDEX_DELETE: Capability.Index,
118
+ Method.CATALOG_LIST: Capability.Catalog,
119
+ }
120
+
121
+
122
+ def cap_key(capability) -> str:
123
+ """Wire JSON key for a Capability enum member (or a raw string passthrough)."""
124
+ return capability.value if isinstance(capability, Capability) else str(capability)
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.4
2
+ Name: rcp-protocol
3
+ Version: 1.0.0
4
+ Summary: Retrieval Context Protocol — native Python SDK (pure standard library, no dependencies)
5
+ Author: RCP Working Group
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://rcp-6d6ef6d5.mintlify.site/
8
+ Project-URL: Documentation, https://rcp-6d6ef6d5.mintlify.site/
9
+ Project-URL: Specification, https://github.com/1ay1/rcp/blob/main/spec/rcp-1.0.md
10
+ Project-URL: Source, https://github.com/1ay1/rcp
11
+ Project-URL: Changelog, https://rcp-6d6ef6d5.mintlify.site/reference/changelog
12
+ Project-URL: Issues, https://github.com/1ay1/rcp/issues
13
+ Keywords: rcp,rag,retrieval,protocol,json-rpc,mcp,acp
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Dynamic: license-file
26
+
27
+ # rcp — Retrieval Context Protocol, Python SDK
28
+
29
+ A **native, pure-standard-library** Python SDK for
30
+ [RCP](https://rcp-6d6ef6d5.mintlify.site/) — the open, versioned JSON-RPC
31
+ protocol that lets any RAG engine expose
32
+ `embed` / `rerank` / `retrieve` / `graph` / `index` / `catalog`, and any client
33
+ consume it uniformly.
34
+
35
+ No dependencies, no compiler, no build step — just `subprocess`, `http`, `socket`,
36
+ and `json` from the standard library. It speaks the exact same wire format as the
37
+ [C++](../cpp), [Node.js](../node), and [Rust](../rust) SDKs, so a Python client
38
+ and a C++/Node/Rust server (or vice-versa) interoperate byte-for-byte.
39
+
40
+ ## Install
41
+
42
+ ```sh
43
+ pip install rcp-protocol
44
+ ```
45
+
46
+ Requires Python ≥ 3.9. The import name is `rcp`:
47
+
48
+ ```python
49
+ import rcp
50
+ ```
51
+
52
+ ## Client
53
+
54
+ Connect to any RCP server over a subprocess (stdio) or HTTP, then make typed,
55
+ capability-gated calls.
56
+
57
+ ```python
58
+ import rcp
59
+
60
+ c = rcp.connect_stdio(["python3", "my_server.py"])
61
+ # or: c = rcp.connect_http("http://127.0.0.1:8000/rcp")
62
+
63
+ print(c.server(), c.protocol_version(), list(c.capabilities()))
64
+
65
+ if c.supports(rcp.Capability.Retrieve):
66
+ for hit in c.retrieve("eiffel tower", k=3):
67
+ print(hit["id"], hit["score"], hit["text"])
68
+
69
+ if c.supports(rcp.Capability.Embed):
70
+ (vec,) = c.embed(["hello world"])
71
+ print("dim", len(vec))
72
+
73
+ c.shutdown()
74
+ ```
75
+
76
+ A call to a capability the server never advertised raises **before** any I/O:
77
+
78
+ ```python
79
+ try:
80
+ c.rerank("q", ["a", "b"])
81
+ except rcp.RcpError as e:
82
+ # e.code == rcp.Errc.CAPABILITY_MISSING (-32003)
83
+ ...
84
+ ```
85
+
86
+ Client methods: `embed`, `embed_sparse`, `embed_multi`, `rerank`, `retrieve`,
87
+ `search` (returns `hits` + `usage` + `nextCursor`), `graph`, `transform`,
88
+ `index_add`, `index_delete`, `catalog`, `info`, `ping`, `call`, `shutdown`.
89
+
90
+ ## Server
91
+
92
+ Expose a Python RAG engine as an RCP server.
93
+
94
+ ```python
95
+ import rcp
96
+
97
+ s = rcp.Server()
98
+ s.set_info("my-engine", "1.0")
99
+ s.advertise(rcp.Capability.Retrieve, {"maxK": 100, "modes": ["hybrid"]})
100
+
101
+ @s.on(rcp.Method.RETRIEVE)
102
+ def _(params):
103
+ hits = my_index.search(params["query"], params.get("k", 10))
104
+ return {"hits": [{"id": h.id, "score": h.score, "text": h.text} for h in hits]}
105
+
106
+ s.serve_stdio() # or: s.serve_http(8000)
107
+ ```
108
+
109
+ The `Server` owns the `initialize` handshake, capability gating, JSON-RPC framing
110
+ and batching, and error mapping. It answers `initialize` / `info` / `ping` /
111
+ `shutdown` itself; a gated call before `initialize` is `-32001`, an unadvertised
112
+ or unimplemented method is `-32003`, an unknown method is `-32004`.
113
+
114
+ ## Selecting a backend
115
+
116
+ Pick one engine from a registry — by id, by required capability, or by priority
117
+ with liveness fallback. Connection is lazy.
118
+
119
+ ```python
120
+ sel = rcp.Selector.loads('''{
121
+ "engines": [
122
+ {"id": "docs", "transport": "stdio", "command": ["python3", "server.py"], "priority": 10},
123
+ {"id": "web", "transport": "http", "url": "http://127.0.0.1:8000/rcp", "priority": 5}
124
+ ]
125
+ }''')
126
+
127
+ c = sel.select_capable(rcp.Capability.Retrieve)
128
+ ```
129
+
130
+ ## Examples & tests
131
+
132
+ ```sh
133
+ python3 examples/example_server.py # stdio (default)
134
+ python3 examples/example_server.py --http 8000
135
+ python3 examples/example_client.py # drives the example server
136
+
137
+ python3 test_bindings.py # smoke test incl. Python client ↔ C++ server
138
+ ```
139
+
140
+ ## License
141
+
142
+ MIT © 2026 Ayush Bhat.
@@ -0,0 +1,11 @@
1
+ rcp/__init__.py,sha256=N8uA_Zn2CHlUTDy2t9P61As6fFtLkzxR843ALveRUCc,2288
2
+ rcp/_client.py,sha256=oUX7HB0GwRY3GI1R8VFqbfYuQ7qk__jFdOXFzqv6DlE,9267
3
+ rcp/_selector.py,sha256=fo2RZIJBbFh3lL9AYzmBqM1AfRmr1cRc6F6lyr_jEi8,5825
4
+ rcp/_server.py,sha256=GVy4nR5AwyuHMA5AMLNghdhWtJV85lAxGEB33T1Rdv0,11066
5
+ rcp/_transport.py,sha256=90MxHyHk6oFhWQbtiJkpmkPnxv56qiH3K15MJ5U726E,5287
6
+ rcp/_types.py,sha256=t1kWDWUkMX_INbdQKLYJDLD1tai6cxOoKh3YOIdiPUk,4306
7
+ rcp_protocol-1.0.0.dist-info/licenses/LICENSE,sha256=z64CEQ5xJIj69_97LLH5UTFFKIfX7T-3hLUWRtfGt9U,1067
8
+ rcp_protocol-1.0.0.dist-info/METADATA,sha256=yGAG72Dp5uWWFzMo-P2PfWHZGB8bHiXNDJ-MBMRl1t4,4508
9
+ rcp_protocol-1.0.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
10
+ rcp_protocol-1.0.0.dist-info/top_level.txt,sha256=1fy4Y26VJuryrN2s_38oW3KxNMz6lD6MaNUZW8tKPiE,4
11
+ rcp_protocol-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ayush Bhat
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ rcp