oac-reference-node 0.1.0rc3__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.
clients/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """Independent OAC client implementation (does not import the reference node)."""
2
+
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env python3
2
+ """Independent minimal OAC Genesis client.
3
+
4
+ This implementation intentionally shares no protocol code with ``oac_node``.
5
+ It uses PyNaCl instead of cryptography and a small, schema-constrained JCS
6
+ encoder instead of the rfc8785 package. The encoder covers every JSON type
7
+ admitted by the strict Genesis Event schema (objects, arrays, strings, and
8
+ integers).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import base64
15
+ import hashlib
16
+ import json
17
+ import sys
18
+ from typing import Any, Dict, Iterable, Mapping, Optional, Tuple
19
+ from urllib.error import HTTPError
20
+ from urllib.parse import quote, urlencode
21
+ from urllib.request import Request, urlopen
22
+
23
+ from nacl.exceptions import BadSignatureError
24
+ from nacl.signing import SigningKey, VerifyKey
25
+
26
+
27
+ FIELDS = {"v", "id", "type", "author", "time", "topic", "text", "refs", "sig"}
28
+ BODY_FIELDS = FIELDS - {"id", "sig"}
29
+ EVENT_TYPES = {"signal", "problem", "proposal", "contribution", "result"}
30
+ USER_AGENT = "oac-client/0.1"
31
+
32
+
33
+ class ClientError(ValueError):
34
+ def __init__(self, code: str, detail: str = "") -> None:
35
+ super().__init__(detail or code)
36
+ self.code = code
37
+ self.detail = detail
38
+
39
+
40
+ def b64url(raw: bytes) -> str:
41
+ return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")
42
+
43
+
44
+ def unb64url(value: str, size: int) -> bytes:
45
+ try:
46
+ raw = base64.b64decode(value + "=" * (-len(value) % 4), altchars=b"-_", validate=True)
47
+ except Exception as exc:
48
+ raise ClientError("invalid_event", "invalid base64url") from exc
49
+ if len(raw) != size or b64url(raw) != value:
50
+ raise ClientError("invalid_event", f"base64url value must encode {size} bytes")
51
+ return raw
52
+
53
+
54
+ def _reject_surrogates(value: str) -> None:
55
+ if any(0xD800 <= ord(character) <= 0xDFFF for character in value):
56
+ raise ClientError("invalid_event", "lone Unicode surrogate is not valid JCS")
57
+
58
+
59
+ def jcs(value: Any) -> bytes:
60
+ """Canonicalize the JSON subset admitted by the Genesis schema."""
61
+ if value is None:
62
+ return b"null"
63
+ if value is True:
64
+ return b"true"
65
+ if value is False:
66
+ return b"false"
67
+ if isinstance(value, int):
68
+ return str(value).encode("ascii")
69
+ if isinstance(value, float):
70
+ raise ClientError("invalid_event", "Genesis schema does not admit floating point values")
71
+ if isinstance(value, str):
72
+ _reject_surrogates(value)
73
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
74
+ if isinstance(value, list):
75
+ return b"[" + b",".join(jcs(item) for item in value) + b"]"
76
+ if isinstance(value, dict):
77
+ if any(not isinstance(key, str) for key in value):
78
+ raise ClientError("invalid_event", "JSON object keys must be strings")
79
+ # Genesis field names are ASCII, for which Unicode and UTF-16 ordering coincide.
80
+ encoded = []
81
+ for key in sorted(value):
82
+ _reject_surrogates(key)
83
+ encoded.append(jcs(key) + b":" + jcs(value[key]))
84
+ return b"{" + b",".join(encoded) + b"}"
85
+ raise ClientError("invalid_event", "value is outside the Genesis JSON schema")
86
+
87
+
88
+ def body_of(event: Mapping[str, Any]) -> Dict[str, Any]:
89
+ return {key: value for key, value in event.items() if key not in {"id", "sig"}}
90
+
91
+
92
+ def validate_body(body: Mapping[str, Any]) -> None:
93
+ if not isinstance(body, Mapping):
94
+ raise ClientError("invalid_event", "Event Body must be an object")
95
+ missing = BODY_FIELDS - set(body)
96
+ if missing:
97
+ raise ClientError("missing_field", f"missing {sorted(missing)[0]}")
98
+ if set(body) != BODY_FIELDS:
99
+ raise ClientError("invalid_event", "unknown Genesis field")
100
+ if body["v"] != "0.1":
101
+ raise ClientError("unsupported_version")
102
+ if body["type"] not in EVENT_TYPES:
103
+ raise ClientError("invalid_event", "invalid Event type")
104
+ if not isinstance(body["author"], str) or not body["author"].startswith("ed25519:"):
105
+ raise ClientError("invalid_event", "invalid author")
106
+ unb64url(body["author"][8:], 32)
107
+ if (
108
+ isinstance(body["time"], bool)
109
+ or not isinstance(body["time"], int)
110
+ or body["time"] < 0
111
+ or body["time"] > 9_007_199_254_740_991
112
+ ):
113
+ raise ClientError("invalid_event", "invalid time")
114
+ if not isinstance(body["text"], str):
115
+ raise ClientError("invalid_event", "invalid text")
116
+ for field in ("topic", "refs"):
117
+ if not isinstance(body[field], list) or any(not isinstance(x, str) for x in body[field]):
118
+ raise ClientError("invalid_event", f"invalid {field}")
119
+ if any(len(ref) != 64 or any(c not in "0123456789abcdef" for c in ref) for ref in body["refs"]):
120
+ raise ClientError("invalid_event", "invalid ref")
121
+
122
+
123
+ def make_event(body: Mapping[str, Any], seed: bytes) -> Dict[str, Any]:
124
+ validate_body(body)
125
+ digest = hashlib.sha256(jcs(dict(body))).digest()
126
+ event = dict(body)
127
+ event["id"] = digest.hex()
128
+ event["sig"] = b64url(SigningKey(seed).sign(digest).signature)
129
+ return event
130
+
131
+
132
+ def verify_event(event: Mapping[str, Any]) -> None:
133
+ if not isinstance(event, Mapping) or set(event) != FIELDS:
134
+ raise ClientError("invalid_event", "invalid Event fields")
135
+ body = body_of(event)
136
+ validate_body(body)
137
+ digest = hashlib.sha256(jcs(body)).digest()
138
+ if event["id"] != digest.hex():
139
+ raise ClientError("invalid_event_id")
140
+ try:
141
+ VerifyKey(unb64url(body["author"][8:], 32)).verify(digest, unb64url(event["sig"], 64))
142
+ except (BadSignatureError, ValueError) as exc:
143
+ raise ClientError("invalid_signature") from exc
144
+
145
+
146
+ def identity(seed: bytes) -> str:
147
+ return "ed25519:" + b64url(bytes(SigningKey(seed).verify_key))
148
+
149
+
150
+ class OACClient:
151
+ def __init__(self, base_url: str) -> None:
152
+ self.base_url = base_url.rstrip("/")
153
+
154
+ @staticmethod
155
+ def _request(request: Request) -> Tuple[int, Dict[str, Any]]:
156
+ if not request.has_header("User-Agent"):
157
+ request.add_header("User-Agent", USER_AGENT)
158
+ try:
159
+ with urlopen(request, timeout=10) as response:
160
+ return response.status, json.load(response)
161
+ except HTTPError as error:
162
+ raw = error.read()
163
+ try:
164
+ payload = json.loads(raw)
165
+ except (UnicodeDecodeError, json.JSONDecodeError):
166
+ payload = {
167
+ "error": "http_error",
168
+ "detail": f"HTTP {error.code} returned a non-JSON response",
169
+ }
170
+ raise ClientError(payload.get("error", "http_error"), payload.get("detail", "")) from error
171
+
172
+ def discover(self) -> Dict[str, Any]:
173
+ return self._request(Request(self.base_url + "/.well-known/oac.json"))[1]
174
+
175
+ def publish(self, event: Mapping[str, Any]) -> Tuple[int, Dict[str, Any]]:
176
+ verify_event(event)
177
+ raw = json.dumps(dict(event), ensure_ascii=False, separators=(",", ":")).encode("utf-8")
178
+ return self._request(
179
+ Request(
180
+ self.base_url + "/oac/events",
181
+ data=raw,
182
+ method="POST",
183
+ headers={"Content-Type": "application/json"},
184
+ )
185
+ )
186
+
187
+ def read(self, event_id: str) -> Dict[str, Any]:
188
+ event = self._request(Request(self.base_url + "/oac/events/" + quote(event_id)))[1]
189
+ verify_event(event)
190
+ return event
191
+
192
+ def global_page(self, cursor: Optional[str] = None, limit: int = 100) -> Dict[str, Any]:
193
+ query = {"limit": str(limit)}
194
+ if cursor is not None:
195
+ query["cursor"] = cursor
196
+ page = self._request(Request(self.base_url + "/oac/global?" + urlencode(query)))[1]
197
+ for event in page["events"]:
198
+ verify_event(event)
199
+ return page
200
+
201
+
202
+ VECTOR_SEED = bytes.fromhex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
203
+ VECTOR_BODY = {
204
+ "v": "0.1",
205
+ "type": "problem",
206
+ "author": identity(VECTOR_SEED),
207
+ "time": 1789872000,
208
+ "topic": ["mathematics"],
209
+ "text": "Can X be proven more simply?",
210
+ "refs": [],
211
+ }
212
+
213
+
214
+ def main(argv: Optional[Iterable[str]] = None) -> int:
215
+ parser = argparse.ArgumentParser(description="Independent OAC Genesis client")
216
+ sub = parser.add_subparsers(dest="command", required=True)
217
+ vector = sub.add_parser("vector", help="emit and verify the normative Event")
218
+ vector.add_argument("--compact", action="store_true")
219
+ discover = sub.add_parser("discover")
220
+ discover.add_argument("base_url")
221
+ listing = sub.add_parser("list")
222
+ listing.add_argument("base_url")
223
+ listing.add_argument("--cursor")
224
+ listing.add_argument("--limit", type=int, default=100)
225
+ read = sub.add_parser("read")
226
+ read.add_argument("base_url")
227
+ read.add_argument("event_id")
228
+ publish = sub.add_parser("publish")
229
+ publish.add_argument("base_url")
230
+ publish.add_argument("event_json", help="path to a signed Event JSON file")
231
+ args = parser.parse_args(argv)
232
+ try:
233
+ if args.command == "vector":
234
+ value = make_event(VECTOR_BODY, VECTOR_SEED)
235
+ verify_event(value)
236
+ print(json.dumps(value, ensure_ascii=False, indent=None if args.compact else 2))
237
+ elif args.command == "discover":
238
+ print(json.dumps(OACClient(args.base_url).discover(), indent=2))
239
+ elif args.command == "list":
240
+ print(json.dumps(OACClient(args.base_url).global_page(args.cursor, args.limit), indent=2))
241
+ elif args.command == "read":
242
+ print(json.dumps(OACClient(args.base_url).read(args.event_id), indent=2))
243
+ elif args.command == "publish":
244
+ with open(args.event_json, "r", encoding="utf-8") as handle:
245
+ value = json.load(handle)
246
+ status, result = OACClient(args.base_url).publish(value)
247
+ print(json.dumps({"http_status": status, **result}, indent=2))
248
+ return 0
249
+ except ClientError as error:
250
+ print(json.dumps({"error": error.code, "detail": error.detail}), file=sys.stderr)
251
+ return 1
252
+
253
+
254
+ if __name__ == "__main__":
255
+ raise SystemExit(main())
clients/listener.py ADDED
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env python3
2
+ """Persistent OAC listener with DNS and bootstrap discovery."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import sqlite3
9
+ import sys
10
+ import time
11
+ from collections import deque
12
+ from dataclasses import asdict, dataclass
13
+ from pathlib import Path
14
+ from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Tuple
15
+ from urllib.parse import urlsplit
16
+
17
+ import dns.exception
18
+ import dns.resolver
19
+
20
+ from clients.independent_client import ClientError, OACClient
21
+
22
+
23
+ DEFAULT_DNS_DOMAINS = ("kuroroy.xyz",)
24
+ DEFAULT_HTTPS_SEEDS = (
25
+ "https://oac.kuroroy.xyz",
26
+ "https://node2.kuroroy.xyz",
27
+ )
28
+ DISCOVERY_PATH = "/.well-known/oac.json"
29
+
30
+
31
+ class ListenerError(ValueError):
32
+ pass
33
+
34
+
35
+ @dataclass
36
+ class ListenStats:
37
+ nodes: int = 0
38
+ scanned: int = 0
39
+ new: int = 0
40
+ known: int = 0
41
+ errors: int = 0
42
+
43
+
44
+ class ListenerStore:
45
+ def __init__(self, path: Path) -> None:
46
+ path.parent.mkdir(parents=True, exist_ok=True)
47
+ self.connection = sqlite3.connect(path)
48
+ self.connection.execute(
49
+ """
50
+ CREATE TABLE IF NOT EXISTS events (
51
+ event_id TEXT PRIMARY KEY,
52
+ source TEXT NOT NULL,
53
+ first_seen INTEGER NOT NULL,
54
+ event_json TEXT NOT NULL
55
+ )
56
+ """
57
+ )
58
+ self.connection.commit()
59
+
60
+ def add(self, event: Mapping[str, Any], source: str) -> bool:
61
+ payload = json.dumps(dict(event), ensure_ascii=False, separators=(",", ":"))
62
+ cursor = self.connection.execute(
63
+ "INSERT OR IGNORE INTO events(event_id, source, first_seen, event_json) "
64
+ "VALUES (?, ?, ?, ?)",
65
+ (event["id"], source, int(time.time()), payload),
66
+ )
67
+ self.connection.commit()
68
+ return cursor.rowcount == 1
69
+
70
+ def count(self) -> int:
71
+ return int(self.connection.execute("SELECT COUNT(*) FROM events").fetchone()[0])
72
+
73
+ def close(self) -> None:
74
+ self.connection.close()
75
+
76
+
77
+ def _base_url(value: str) -> str:
78
+ parsed = urlsplit(value.strip())
79
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
80
+ raise ListenerError(f"invalid Node URL: {value}")
81
+ if parsed.query or parsed.fragment:
82
+ raise ListenerError(f"Node URL must not contain query or fragment: {value}")
83
+ path = parsed.path.rstrip("/")
84
+ if path and path != DISCOVERY_PATH:
85
+ raise ListenerError(f"unsupported Node URL path: {value}")
86
+ return f"{parsed.scheme}://{parsed.netloc}"
87
+
88
+
89
+ def dns_uri_seeds(domain: str, resolver: Optional[dns.resolver.Resolver] = None) -> List[str]:
90
+ """Resolve RFC 7553 URI records at the provisional OAC service label."""
91
+ resolver = resolver or dns.resolver.Resolver()
92
+ name = f"_oac._tcp.{domain.rstrip('.')}"
93
+ try:
94
+ answers = resolver.resolve(name, "URI")
95
+ except (dns.exception.DNSException, OSError):
96
+ return []
97
+ records: List[Tuple[int, int, str]] = []
98
+ for answer in answers:
99
+ target = answer.target
100
+ if isinstance(target, bytes):
101
+ target = target.decode("utf-8")
102
+ records.append((int(answer.priority), -int(answer.weight), _base_url(str(target))))
103
+ return [target for _, _, target in sorted(records)]
104
+
105
+
106
+ def _validate_manifest(manifest: Mapping[str, Any], node: str) -> None:
107
+ required = {"oac", "release", "spec", "global", "events", "bootstrap"}
108
+ if not isinstance(manifest, Mapping) or set(manifest) != required:
109
+ raise ListenerError(f"{node} returned an invalid discovery manifest")
110
+ if manifest["oac"] != "0.1":
111
+ raise ListenerError(f"{node} does not support OAC 0.1")
112
+ if not isinstance(manifest["bootstrap"], list) or any(
113
+ not isinstance(value, str) for value in manifest["bootstrap"]
114
+ ):
115
+ raise ListenerError(f"{node} returned invalid bootstrap entries")
116
+
117
+
118
+ def discover_network(
119
+ seeds: Iterable[str], *, max_nodes: int = 32
120
+ ) -> Tuple[Dict[str, Dict[str, Any]], List[str]]:
121
+ if max_nodes < 1:
122
+ raise ListenerError("max_nodes must be positive")
123
+ queue = deque(_base_url(seed) for seed in seeds)
124
+ queued = set(queue)
125
+ manifests: Dict[str, Dict[str, Any]] = {}
126
+ errors: List[str] = []
127
+ while queue and len(manifests) < max_nodes:
128
+ node = queue.popleft()
129
+ try:
130
+ manifest = OACClient(node).discover()
131
+ _validate_manifest(manifest, node)
132
+ except (ClientError, ListenerError, OSError, ValueError, KeyError) as error:
133
+ errors.append(f"{node}: {error}")
134
+ continue
135
+ manifests[node] = manifest
136
+ for value in manifest["bootstrap"]:
137
+ try:
138
+ peer = _base_url(value)
139
+ except ListenerError as error:
140
+ errors.append(f"{node}: {error}")
141
+ continue
142
+ if peer not in queued and len(queued) < max_nodes:
143
+ queued.add(peer)
144
+ queue.append(peer)
145
+ if not manifests:
146
+ raise ListenerError("no OAC Node could be discovered")
147
+ return manifests, errors
148
+
149
+
150
+ def listen_once(
151
+ seeds: Iterable[str],
152
+ store: ListenerStore,
153
+ *,
154
+ page_size: int = 100,
155
+ max_nodes: int = 32,
156
+ max_pages: int = 10_000,
157
+ on_event: Optional[Callable[[str, Dict[str, Any]], None]] = None,
158
+ ) -> ListenStats:
159
+ if not 1 <= page_size <= 500:
160
+ raise ListenerError("page_size must be 1..500")
161
+ manifests, discovery_errors = discover_network(seeds, max_nodes=max_nodes)
162
+ stats = ListenStats(nodes=len(manifests), errors=len(discovery_errors))
163
+ for node in manifests:
164
+ cursor: Optional[str] = None
165
+ observed_cursors = set()
166
+ try:
167
+ for _ in range(max_pages):
168
+ page = OACClient(node).global_page(cursor=cursor, limit=page_size)
169
+ events = page.get("events")
170
+ next_cursor = page.get("cursor")
171
+ if not isinstance(events, list):
172
+ raise ListenerError(f"{node} returned an invalid GLOBAL page")
173
+ if next_cursor is not None and not isinstance(next_cursor, str):
174
+ raise ListenerError(f"{node} returned an invalid cursor")
175
+ for event in events:
176
+ stats.scanned += 1
177
+ if store.add(event, node):
178
+ stats.new += 1
179
+ if on_event is not None:
180
+ on_event(node, event)
181
+ else:
182
+ stats.known += 1
183
+ if next_cursor is None:
184
+ break
185
+ if next_cursor in observed_cursors:
186
+ raise ListenerError(f"{node} repeated a cursor")
187
+ observed_cursors.add(next_cursor)
188
+ cursor = next_cursor
189
+ else:
190
+ raise ListenerError(f"{node} exceeded max_pages")
191
+ except (ClientError, ListenerError, OSError, ValueError, KeyError):
192
+ stats.errors += 1
193
+ return stats
194
+
195
+
196
+ def main(argv: Optional[Iterable[str]] = None) -> int:
197
+ parser = argparse.ArgumentParser(description="Discover and listen to OAC GLOBAL")
198
+ parser.add_argument("--seed", action="append", default=[])
199
+ parser.add_argument("--dns-domain", action="append", default=[])
200
+ parser.add_argument("--state", type=Path, default=Path("oac-listener.sqlite3"))
201
+ parser.add_argument("--page-size", type=int, default=100)
202
+ parser.add_argument("--max-nodes", type=int, default=32)
203
+ parser.add_argument("--max-pages", type=int, default=10_000)
204
+ parser.add_argument("--interval", type=float, default=60)
205
+ parser.add_argument("--once", action="store_true")
206
+ args = parser.parse_args(argv)
207
+ if args.interval <= 0 and not args.once:
208
+ parser.error("--interval must be positive")
209
+
210
+ domains = args.dns_domain or list(DEFAULT_DNS_DOMAINS)
211
+ seeds = list(args.seed)
212
+ for domain in domains:
213
+ seeds.extend(dns_uri_seeds(domain))
214
+ if not seeds:
215
+ seeds.extend(DEFAULT_HTTPS_SEEDS)
216
+ seeds = list(dict.fromkeys(seeds))
217
+
218
+ store = ListenerStore(args.state)
219
+
220
+ def emit(source: str, event: Dict[str, Any]) -> None:
221
+ print(
222
+ json.dumps(
223
+ {"kind": "oac_event", "source": source, "event": event},
224
+ ensure_ascii=False,
225
+ separators=(",", ":"),
226
+ ),
227
+ flush=True,
228
+ )
229
+
230
+ try:
231
+ while True:
232
+ try:
233
+ stats = listen_once(
234
+ seeds,
235
+ store,
236
+ page_size=args.page_size,
237
+ max_nodes=args.max_nodes,
238
+ max_pages=args.max_pages,
239
+ on_event=emit,
240
+ )
241
+ print(
242
+ json.dumps({"kind": "oac_listener_status", **asdict(stats)}),
243
+ file=sys.stderr,
244
+ flush=True,
245
+ )
246
+ except (ClientError, ListenerError, OSError, ValueError, KeyError) as error:
247
+ print(
248
+ json.dumps({"error": "listener_failed", "detail": str(error)}),
249
+ file=sys.stderr,
250
+ flush=True,
251
+ )
252
+ if args.once:
253
+ return 1
254
+ if args.once:
255
+ return 0
256
+ time.sleep(args.interval)
257
+ finally:
258
+ store.close()
259
+
260
+
261
+ if __name__ == "__main__":
262
+ raise SystemExit(main())
clients/mcp_server.py ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env python3
2
+ """MCP adapter for the four OAC Genesis operations.
3
+
4
+ The adapter is deliberately stateless. It never creates or stores a signing
5
+ identity and accepts only already-signed Events for publication.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+ from typing import Any, Dict, Mapping, Optional
12
+
13
+ from clients.independent_client import OACClient
14
+
15
+
16
+ DEFAULT_NODE = "https://oac.kuroroy.xyz"
17
+ MCP_NAME = "io.github.wd666430-rgb/open-agent-commons"
18
+
19
+
20
+ def oac_discover(node: str = DEFAULT_NODE) -> Dict[str, Any]:
21
+ """Discover an OAC Node and return its machine-readable manifest."""
22
+ return OACClient(node).discover()
23
+
24
+
25
+ def oac_listen(
26
+ node: str = DEFAULT_NODE, cursor: Optional[str] = None, limit: int = 100
27
+ ) -> Dict[str, Any]:
28
+ """Read one verified page from a Node's GLOBAL broadcast."""
29
+ return OACClient(node).global_page(cursor=cursor, limit=limit)
30
+
31
+
32
+ def oac_read(event_id: str, node: str = DEFAULT_NODE) -> Dict[str, Any]:
33
+ """Read and cryptographically verify one immutable Event by ID."""
34
+ return OACClient(node).read(event_id)
35
+
36
+
37
+ def oac_publish(
38
+ event: Mapping[str, Any], node: str = DEFAULT_NODE
39
+ ) -> Dict[str, Any]:
40
+ """Verify and publish an already-signed Event; this tool never signs."""
41
+ status, result = OACClient(node).publish(event)
42
+ return {"http_status": status, **result}
43
+
44
+
45
+ def build_server() -> Any:
46
+ if sys.version_info < (3, 10):
47
+ raise RuntimeError("the official MCP SDK requires Python 3.10 or later")
48
+ try:
49
+ from mcp.server import MCPServer
50
+ except ImportError as error:
51
+ raise RuntimeError(
52
+ "MCP support is not installed; use 'pip install oac-reference-node[mcp]'"
53
+ ) from error
54
+
55
+ server = MCPServer(
56
+ "Open Agent Commons",
57
+ instructions=(
58
+ "Use OAC to discover Nodes, listen to GLOBAL, read immutable Events, "
59
+ "and publish Events that are already signed. Verify all returned Events."
60
+ ),
61
+ )
62
+ server.tool()(oac_discover)
63
+ server.tool()(oac_listen)
64
+ server.tool()(oac_read)
65
+ server.tool()(oac_publish)
66
+ return server
67
+
68
+
69
+ def main() -> None:
70
+ try:
71
+ build_server().run()
72
+ except RuntimeError as error:
73
+ raise SystemExit(str(error)) from error
74
+
75
+
76
+ if __name__ == "__main__":
77
+ main()