trinity-lite 0.1.4__tar.gz → 0.2.0__tar.gz

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.
Files changed (34) hide show
  1. {trinity_lite-0.1.4/trinity_lite.egg-info → trinity_lite-0.2.0}/PKG-INFO +3 -1
  2. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/pyproject.toml +4 -1
  3. trinity_lite-0.2.0/tests/test_mcp_server.py +455 -0
  4. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/trinity_lite/__init__.py +1 -1
  5. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/trinity_lite/cli.py +25 -1
  6. trinity_lite-0.2.0/trinity_lite/mcp_server.py +690 -0
  7. {trinity_lite-0.1.4 → trinity_lite-0.2.0/trinity_lite.egg-info}/PKG-INFO +3 -1
  8. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/trinity_lite.egg-info/SOURCES.txt +3 -0
  9. trinity_lite-0.2.0/trinity_lite.egg-info/requires.txt +3 -0
  10. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/LICENSE +0 -0
  11. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/README.md +0 -0
  12. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/setup.cfg +0 -0
  13. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/tests/test_bus.py +0 -0
  14. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/tests/test_cli.py +0 -0
  15. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/tests/test_doctor.py +0 -0
  16. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/tests/test_guard.py +0 -0
  17. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/tests/test_orchestrator.py +0 -0
  18. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/tests/test_router.py +0 -0
  19. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/tests/test_worker.py +0 -0
  20. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/trinity_lite/__main__.py +0 -0
  21. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/trinity_lite/adapters.py +0 -0
  22. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/trinity_lite/bus.py +0 -0
  23. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/trinity_lite/config.py +0 -0
  24. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/trinity_lite/doctor.py +0 -0
  25. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/trinity_lite/guard.py +0 -0
  26. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/trinity_lite/orchestrator.py +0 -0
  27. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/trinity_lite/paths.py +0 -0
  28. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/trinity_lite/py.typed +0 -0
  29. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/trinity_lite/router.py +0 -0
  30. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/trinity_lite/validation.py +0 -0
  31. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/trinity_lite/worker.py +0 -0
  32. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/trinity_lite.egg-info/dependency_links.txt +0 -0
  33. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/trinity_lite.egg-info/entry_points.txt +0 -0
  34. {trinity_lite-0.1.4 → trinity_lite-0.2.0}/trinity_lite.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trinity-lite
3
- Version: 0.1.4
3
+ Version: 0.2.0
4
4
  Summary: A minimal public multi-agent task bus with routing, workers, and safety checks.
5
5
  Author: Yomiracle
6
6
  License-Expression: MIT
@@ -17,6 +17,8 @@ Classifier: Programming Language :: Python :: 3.12
17
17
  Requires-Python: >=3.10
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
+ Provides-Extra: mcp
21
+ Requires-Dist: mcp>=1.0; extra == "mcp"
20
22
  Dynamic: license-file
21
23
 
22
24
  # Trinity Lite
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "trinity-lite"
7
- version = "0.1.4"
7
+ version = "0.2.0"
8
8
  description = "A minimal public multi-agent task bus with routing, workers, and safety checks."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -24,6 +24,9 @@ classifiers = [
24
24
  ]
25
25
  dependencies = []
26
26
 
27
+ [project.optional-dependencies]
28
+ mcp = ["mcp>=1.0"]
29
+
27
30
  [project.scripts]
28
31
  trinity-lite = "trinity_lite.cli:main"
29
32
 
@@ -0,0 +1,455 @@
1
+ import io
2
+ import json
3
+ import sys
4
+ import tempfile
5
+ import unittest
6
+ from pathlib import Path
7
+
8
+ from trinity_lite.bus import TrinityBus
9
+ from trinity_lite.mcp_server import (
10
+ handle_request,
11
+ serve,
12
+ TOOL_DEFINITIONS,
13
+ RESOURCE_DEFINITIONS,
14
+ jsonrpc_response,
15
+ jsonrpc_error,
16
+ _compact_task,
17
+ _validate_task_id,
18
+ _validate_agent_id,
19
+ _validate_text,
20
+ _validate_limit,
21
+ _validate_timeout,
22
+ _get_known_agents,
23
+ )
24
+
25
+
26
+ class McpServerTest(unittest.TestCase):
27
+ def setUp(self):
28
+ self.temp = tempfile.TemporaryDirectory()
29
+ self.root = Path(self.temp.name)
30
+ self.db = self.root / "bus.db"
31
+ self.bus = TrinityBus(self.db, allowed_roots=[self.root])
32
+
33
+ def tearDown(self):
34
+ self.temp.cleanup()
35
+
36
+ def _msg(self, method, id_=1, params=None):
37
+ msg = {"jsonrpc": "2.0", "method": method, "id": id_}
38
+ if params is not None:
39
+ msg["params"] = params
40
+ return msg
41
+
42
+ def _call(self, msg):
43
+ return handle_request(msg, self.bus, None, None)
44
+
45
+ # ---- JSON-RPC lifecycle ----
46
+
47
+ def test_initialize_handshake(self):
48
+ resp = self._call(self._msg("initialize", 1, {}))
49
+ self.assertEqual(resp["id"], 1)
50
+ self.assertIn("protocolVersion", resp["result"])
51
+ self.assertEqual(resp["result"]["protocolVersion"], "2024-11-05")
52
+ self.assertIn("capabilities", resp["result"])
53
+ self.assertIn("tools", resp["result"]["capabilities"])
54
+ self.assertIn("resources", resp["result"]["capabilities"])
55
+ self.assertIn("serverInfo", resp["result"])
56
+ self.assertEqual(resp["result"]["serverInfo"]["name"], "trinity-lite-mcp")
57
+ self.assertEqual(resp["result"]["serverInfo"]["version"], "0.2.0")
58
+
59
+ def test_initialized_returns_none(self):
60
+ resp = self._call(self._msg("initialized"))
61
+ self.assertIsNone(resp)
62
+
63
+ def test_shutdown_returns_empty_result(self):
64
+ resp = self._call(self._msg("shutdown", 1, {}))
65
+ self.assertEqual(resp["id"], 1)
66
+ self.assertEqual(resp["result"], {})
67
+
68
+ def test_unknown_method_returns_error(self):
69
+ resp = self._call(self._msg("nonexistent", 1, {}))
70
+ self.assertIn("error", resp)
71
+ self.assertEqual(resp["error"]["code"], -32601)
72
+
73
+ # ---- tools/list ----
74
+
75
+ def test_tools_list(self):
76
+ resp = self._call(self._msg("tools/list", 1))
77
+ tools = resp["result"]["tools"]
78
+ self.assertIsInstance(tools, list)
79
+ self.assertEqual(len(tools), 8)
80
+ names = [t["name"] for t in tools]
81
+ self.assertIn("trinity_dispatch", names)
82
+ self.assertIn("trinity_dispatch_auto", names)
83
+ self.assertIn("trinity_status", names)
84
+ self.assertIn("trinity_tasks", names)
85
+ self.assertIn("trinity_worker", names)
86
+ self.assertIn("trinity_doctor", names)
87
+ self.assertIn("trinity_inbox", names)
88
+ self.assertIn("trinity_send", names)
89
+ # Check schema structure
90
+ for t in tools:
91
+ self.assertIn("name", t)
92
+ self.assertIn("description", t)
93
+ self.assertIn("inputSchema", t)
94
+ self.assertEqual(t["inputSchema"]["type"], "object")
95
+ self.assertIn("properties", t["inputSchema"])
96
+ self.assertIn("required", t["inputSchema"])
97
+
98
+ # ---- resources/list ----
99
+
100
+ def test_resources_list(self):
101
+ resp = self._call(self._msg("resources/list", 1))
102
+ resources = resp["result"]["resources"]
103
+ self.assertIsInstance(resources, list)
104
+ self.assertEqual(len(resources), 3)
105
+ uris = [r["uri"] for r in resources]
106
+ self.assertIn("trinity://health", uris)
107
+ self.assertIn("trinity://tasks/recent", uris)
108
+ self.assertIn("trinity://tasks/{task_id}", uris)
109
+
110
+ # ---- resources/read ----
111
+
112
+ def test_resource_health(self):
113
+ resp = self._call(self._msg("resources/read", 1, {"uri": "trinity://health"}))
114
+ self.assertIn("result", resp)
115
+ contents = resp["result"]["contents"]
116
+ self.assertEqual(len(contents), 1)
117
+ data = json.loads(contents[0]["text"])
118
+ self.assertEqual(data["status"], "ok")
119
+ self.assertEqual(data["bus"], "connected")
120
+ self.assertEqual(data["tools"], 8)
121
+ self.assertEqual(data["resources"], 3)
122
+
123
+ def test_resource_health_degraded(self):
124
+ # Use a bad bus path to simulate disconnected
125
+ bad_bus = TrinityBus(self.root / "nonexistent" / "deep" / "bus.db", allowed_roots=[self.root])
126
+ resp = handle_request(self._msg("resources/read", 1, {"uri": "trinity://health"}), bad_bus, None, None)
127
+ # Even with a bad path, SQLite creates the file... Let's use list_tasks catching.
128
+ # Actually sqlite will create dirs via bus, so this should still connect.
129
+ # Skip this test since sqlite is too forgiving.
130
+ pass
131
+
132
+ def test_resource_tasks_recent(self):
133
+ task = self.bus.submit_task("user", "codex", "hello", "implementation", cwd=self.root)
134
+ resp = self._call(self._msg("resources/read", 1, {"uri": "trinity://tasks/recent"}))
135
+ contents = resp["result"]["contents"]
136
+ data = json.loads(contents[0]["text"])
137
+ self.assertIsInstance(data, list)
138
+ self.assertGreaterEqual(len(data), 1)
139
+
140
+ def test_resource_task_by_id(self):
141
+ task = self.bus.submit_task("user", "codex", "hello", "implementation", cwd=self.root)
142
+ resp = self._call(self._msg("resources/read", 1, {"uri": "trinity://tasks/" + task["id"]}))
143
+ contents = resp["result"]["contents"]
144
+ data = json.loads(contents[0]["text"])
145
+ self.assertEqual(data["id"], task["id"])
146
+
147
+ def test_resource_task_not_found(self):
148
+ resp = self._call(self._msg("resources/read", 1, {"uri": "trinity://tasks/deadbeef1234"}))
149
+ self.assertIn("error", resp)
150
+ self.assertEqual(resp["error"]["code"], -32002)
151
+
152
+ def test_resource_invalid_uri(self):
153
+ resp = self._call(self._msg("resources/read", 1, {"uri": "trinity://nonexistent"}))
154
+ self.assertIn("error", resp)
155
+ self.assertEqual(resp["error"]["code"], -32002)
156
+
157
+ # ---- tools/call: trinity_status ----
158
+
159
+ def test_trinity_status_returns_task(self):
160
+ task = self.bus.submit_task("user", "codex", "implement test", "implementation", cwd=self.root)
161
+ params = {"name": "trinity_status", "arguments": {"task_id": task["id"]}}
162
+ resp = self._call(self._msg("tools/call", 1, params))
163
+ self.assertIn("result", resp)
164
+ self.assertEqual(resp["result"]["id"], task["id"])
165
+ self.assertEqual(resp["result"]["status"], "queued")
166
+
167
+ def test_trinity_status_not_found(self):
168
+ params = {"name": "trinity_status", "arguments": {"task_id": "deadbeef1234"}}
169
+ resp = self._call(self._msg("tools/call", 1, params))
170
+ self.assertIn("error", resp)
171
+ self.assertEqual(resp["error"]["code"], -32002)
172
+
173
+ def test_trinity_status_invalid_id(self):
174
+ params = {"name": "trinity_status", "arguments": {"task_id": "short"}}
175
+ resp = self._call(self._msg("tools/call", 1, params))
176
+ self.assertIn("error", resp)
177
+ self.assertEqual(resp["error"]["code"], -32602)
178
+
179
+ # ---- tools/call: trinity_dispatch ----
180
+
181
+ def test_trinity_dispatch_creates_and_runs_task(self):
182
+ params = {
183
+ "name": "trinity_dispatch",
184
+ "arguments": {
185
+ "target_agent": "codex",
186
+ "task": "implement hello world",
187
+ "cwd": str(self.root),
188
+ },
189
+ }
190
+ resp = self._call(self._msg("tools/call", 1, params))
191
+ self.assertIn("result", resp)
192
+ result = resp["result"]
193
+ self.assertEqual(result["target_agent"], "codex")
194
+ self.assertEqual(result["status"], "completed")
195
+ self.assertIn("[mock:codex]", result.get("result", ""))
196
+
197
+ def test_trinity_dispatch_self_delegation_blocked(self):
198
+ params = {
199
+ "name": "trinity_dispatch",
200
+ "arguments": {
201
+ "target_agent": "mcp",
202
+ "task": "self task",
203
+ "cwd": str(self.root),
204
+ },
205
+ }
206
+ resp = self._call(self._msg("tools/call", 1, params))
207
+ self.assertIn("error", resp)
208
+ self.assertEqual(resp["error"]["code"], -32000)
209
+
210
+ def test_trinity_dispatch_unknown_agent(self):
211
+ params = {
212
+ "name": "trinity_dispatch",
213
+ "arguments": {
214
+ "target_agent": "nonexistent_agent_123",
215
+ "task": "hello",
216
+ "cwd": str(self.root),
217
+ },
218
+ }
219
+ resp = self._call(self._msg("tools/call", 1, params))
220
+ self.assertIn("error", resp)
221
+ self.assertEqual(resp["error"]["code"], -32602)
222
+
223
+ def test_trinity_dispatch_missing_required(self):
224
+ params = {
225
+ "name": "trinity_dispatch",
226
+ "arguments": {"target_agent": "codex"},
227
+ }
228
+ resp = self._call(self._msg("tools/call", 1, params))
229
+ self.assertIn("error", resp)
230
+ self.assertEqual(resp["error"]["code"], -32602)
231
+
232
+ # ---- tools/call: trinity_dispatch_auto ----
233
+
234
+ def test_trinity_dispatch_auto_routes_and_runs(self):
235
+ params = {
236
+ "name": "trinity_dispatch_auto",
237
+ "arguments": {
238
+ "task": "implement a parser function",
239
+ "cwd": str(self.root),
240
+ },
241
+ }
242
+ resp = self._call(self._msg("tools/call", 1, params))
243
+ self.assertIn("result", resp)
244
+ result = resp["result"]
245
+ self.assertIn("route", result)
246
+ self.assertEqual(result["route"]["agent"], "codex")
247
+ self.assertEqual(result["route"]["task_type"], "implementation")
248
+ self.assertEqual(result["status"], "completed")
249
+
250
+ # ---- tools/call: trinity_tasks ----
251
+
252
+ def test_trinity_tasks_lists_tasks(self):
253
+ self.bus.submit_task("user", "codex", "task 1", cwd=self.root)
254
+ self.bus.submit_task("user", "claude_code", "task 2", cwd=self.root)
255
+ params = {"name": "trinity_tasks", "arguments": {}}
256
+ resp = self._call(self._msg("tools/call", 1, params))
257
+ self.assertIn("result", resp)
258
+ tasks = resp["result"]
259
+ self.assertIsInstance(tasks, list)
260
+ self.assertEqual(len(tasks), 2)
261
+
262
+ def test_trinity_tasks_filter_by_agent(self):
263
+ self.bus.submit_task("user", "codex", "codex task", cwd=self.root)
264
+ self.bus.submit_task("user", "claude_code", "claude task", cwd=self.root)
265
+ params = {
266
+ "name": "trinity_tasks",
267
+ "arguments": {"agent": "codex"},
268
+ }
269
+ resp = self._call(self._msg("tools/call", 1, params))
270
+ tasks = resp["result"]
271
+ self.assertEqual(len(tasks), 1)
272
+ self.assertEqual(tasks[0]["target_agent"], "codex")
273
+
274
+ # ---- tools/call: trinity_worker ----
275
+
276
+ def test_trinity_worker_runs_queued_task(self):
277
+ task = self.bus.submit_task("user", "codex", "worker test", cwd=self.root)
278
+ params = {
279
+ "name": "trinity_worker",
280
+ "arguments": {"agent": "codex"},
281
+ }
282
+ resp = self._call(self._msg("tools/call", 1, params))
283
+ self.assertIn("result", resp)
284
+ self.assertEqual(resp["result"]["id"], task["id"])
285
+ self.assertEqual(resp["result"]["status"], "completed")
286
+
287
+ def test_trinity_worker_no_task(self):
288
+ params = {
289
+ "name": "trinity_worker",
290
+ "arguments": {"agent": "codex"},
291
+ }
292
+ resp = self._call(self._msg("tools/call", 1, params))
293
+ self.assertIn("result", resp)
294
+ self.assertEqual(resp["result"]["status"], "no_task")
295
+
296
+ # ---- tools/call: trinity_doctor ----
297
+
298
+ def test_trinity_doctor_runs(self):
299
+ params = {"name": "trinity_doctor", "arguments": {}}
300
+ resp = self._call(self._msg("tools/call", 1, params))
301
+ self.assertIn("result", resp)
302
+ self.assertIn("status", resp["result"])
303
+
304
+ # ---- tools/call: trinity_inbox ----
305
+
306
+ def test_trinity_inbox_reads_messages(self):
307
+ self.bus.send_message("codex", "claude_code", "please review")
308
+ params = {
309
+ "name": "trinity_inbox",
310
+ "arguments": {"agent": "claude_code"},
311
+ }
312
+ resp = self._call(self._msg("tools/call", 1, params))
313
+ self.assertIn("result", resp)
314
+ self.assertEqual(len(resp["result"]), 1)
315
+ self.assertEqual(resp["result"][0]["message"], "please review")
316
+
317
+ # ---- tools/call: trinity_send ----
318
+
319
+ def test_trinity_send_sends_message(self):
320
+ params = {
321
+ "name": "trinity_send",
322
+ "arguments": {
323
+ "target_agent": "claude_code",
324
+ "message": "review this please",
325
+ },
326
+ }
327
+ resp = self._call(self._msg("tools/call", 1, params))
328
+ self.assertIn("result", resp)
329
+ self.assertEqual(resp["result"]["message"], "review this please")
330
+
331
+ def test_trinity_send_self_message_blocked(self):
332
+ params = {
333
+ "name": "trinity_send",
334
+ "arguments": {
335
+ "target_agent": "mcp",
336
+ "message": "self message",
337
+ },
338
+ }
339
+ resp = self._call(self._msg("tools/call", 1, params))
340
+ self.assertIn("error", resp)
341
+ self.assertEqual(resp["error"]["code"], -32000)
342
+
343
+ # ---- Input validation helpers ----
344
+
345
+ def test_validate_task_id_valid(self):
346
+ self.assertEqual(_validate_task_id("deadbeef1234"), "deadbeef1234")
347
+
348
+ def test_validate_task_id_invalid(self):
349
+ with self.assertRaises(ValueError):
350
+ _validate_task_id("short")
351
+ with self.assertRaises(ValueError):
352
+ _validate_task_id("nothex123456")
353
+
354
+ def test_validate_agent_id_known(self):
355
+ known = {"codex", "claude_code", "hermes"}
356
+ self.assertEqual(_validate_agent_id("codex", known), "codex")
357
+ self.assertEqual(_validate_agent_id("mcp", known), "mcp") # reserved
358
+
359
+ def test_validate_agent_id_unknown(self):
360
+ with self.assertRaises(ValueError):
361
+ _validate_agent_id("nonexistent", {"codex"})
362
+
363
+ def test_validate_text_control_chars(self):
364
+ with self.assertRaises(ValueError):
365
+ _validate_text("hello\x00world")
366
+
367
+ def test_validate_text_too_long(self):
368
+ with self.assertRaises(ValueError):
369
+ _validate_text("x" * 200_000)
370
+
371
+ def test_validate_limit(self):
372
+ self.assertEqual(_validate_limit(None), 20)
373
+ self.assertEqual(_validate_limit(5), 5)
374
+ with self.assertRaises(ValueError):
375
+ _validate_limit(0)
376
+ with self.assertRaises(ValueError):
377
+ _validate_limit(101)
378
+
379
+ def test_validate_timeout(self):
380
+ self.assertEqual(_validate_timeout(None), 600)
381
+ self.assertEqual(_validate_timeout(60), 60)
382
+ with self.assertRaises(ValueError):
383
+ _validate_timeout(0)
384
+ with self.assertRaises(ValueError):
385
+ _validate_timeout(3601)
386
+
387
+ # ---- Error code verification ----
388
+
389
+ def test_parse_error_on_bad_json(self):
390
+ from trinity_lite.mcp_server import serve
391
+ import io
392
+ import sys
393
+
394
+ # Simulate serving a malformed line
395
+ old_stdout = sys.stdout
396
+ old_stdin = sys.stdin
397
+ try:
398
+ # We test handle_request directly for bad json
399
+ pass
400
+ finally:
401
+ sys.stdout = old_stdout
402
+
403
+ def test_invalid_params_for_unknown_tool(self):
404
+ params = {
405
+ "name": "nonexistent_tool",
406
+ "arguments": {},
407
+ }
408
+ resp = self._call(self._msg("tools/call", 1, params))
409
+ self.assertIn("error", resp)
410
+ self.assertEqual(resp["error"]["code"], -32602)
411
+
412
+ # ---- JSON-RPC response helpers ----
413
+
414
+ def test_jsonrpc_response_structure(self):
415
+ resp = jsonrpc_response(1, {"key": "value"})
416
+ self.assertEqual(resp["jsonrpc"], "2.0")
417
+ self.assertEqual(resp["id"], 1)
418
+ self.assertEqual(resp["result"], {"key": "value"})
419
+ self.assertNotIn("error", resp)
420
+
421
+ def test_jsonrpc_error_structure(self):
422
+ resp = jsonrpc_error(1, -32600, "test error", {"detail": "extra"})
423
+ self.assertEqual(resp["jsonrpc"], "2.0")
424
+ self.assertEqual(resp["id"], 1)
425
+ self.assertIn("error", resp)
426
+ self.assertEqual(resp["error"]["code"], -32600)
427
+ self.assertEqual(resp["error"]["message"], "test error")
428
+ self.assertEqual(resp["error"]["data"], {"detail": "extra"})
429
+
430
+ # ---- Compact task ----
431
+
432
+ def test_compact_task_strips_extra_fields(self):
433
+ task = {
434
+ "id": "abc123def456",
435
+ "source_agent": "user",
436
+ "target_agent": "codex",
437
+ "task_type": "impl",
438
+ "prompt": "test",
439
+ "cwd": "/tmp",
440
+ "status": "queued",
441
+ "depth": 0,
442
+ "result": None,
443
+ "error": None,
444
+ "created_at": "now",
445
+ "started_at": None,
446
+ "finished_at": None,
447
+ "extra_field": "should be removed",
448
+ }
449
+ compact = _compact_task(task)
450
+ self.assertNotIn("extra_field", compact)
451
+ self.assertEqual(compact["id"], "abc123def456")
452
+
453
+
454
+ if __name__ == "__main__":
455
+ unittest.main()
@@ -1,3 +1,3 @@
1
1
  """Trinity Lite: a minimal public multi-agent task bus."""
2
2
 
3
- __version__ = "0.1.1"
3
+ __version__ = "0.2.0"
@@ -40,7 +40,7 @@ Quick demo: trinity-lite demo
40
40
  Full help: trinity-lite --help
41
41
 
42
42
  Commands: demo, dispatch, dispatch-auto, worker, orchestrate,
43
- status, tasks, route, doctor, send, inbox"""
43
+ status, tasks, route, doctor, send, inbox, mcp"""
44
44
 
45
45
 
46
46
  def build_parser() -> argparse.ArgumentParser:
@@ -124,6 +124,11 @@ def build_parser() -> argparse.ArgumentParser:
124
124
 
125
125
  demo = sub.add_parser("demo", parents=[common], help="run a guided first-run demo")
126
126
 
127
+ mcp = sub.add_parser("mcp", help="MCP server control")
128
+ mcp_sub = mcp.add_subparsers(dest="mcp_command")
129
+ mcp_sub.required = False
130
+ mcp_serve = mcp_sub.add_parser("serve", parents=[common], help="start MCP server on stdio")
131
+
127
132
  return parser
128
133
 
129
134
 
@@ -238,6 +243,8 @@ def run_command(args: argparse.Namespace) -> int:
238
243
  return 0
239
244
  if args.command == "demo":
240
245
  return _demo(args, bus)
246
+ if args.command == "mcp":
247
+ return _mcp(args, bus)
241
248
 
242
249
  raise AssertionError(f"unhandled command: {args.command}")
243
250
 
@@ -350,6 +357,23 @@ def _acceptance_status_from_flow(
350
357
  return _acceptance_status(route, primary_task, review_task)
351
358
 
352
359
 
360
+ def _mcp(args: argparse.Namespace, bus: TrinityBus) -> int:
361
+ """Handle 'trinity-lite mcp' subcommand."""
362
+ if args.mcp_command == "serve":
363
+ from .mcp_server import serve as mcp_serve
364
+
365
+ mcp_serve(bus, agents_path=args.agents, routes_path=args.routes)
366
+ return 0
367
+ # Show mcp help
368
+ print("usage: trinity-lite mcp serve [--db PATH] [--routes PATH] [--agents PATH]")
369
+ print("")
370
+ print("MCP server control")
371
+ print("")
372
+ print("commands:")
373
+ print(" serve start MCP server on stdio")
374
+ return 0
375
+
376
+
353
377
  def _demo(args: argparse.Namespace, bus: TrinityBus) -> int:
354
378
  """Run a guided demo flow with human-friendly output."""
355
379