universal-agent-context 0.2.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.
Files changed (47) hide show
  1. uacs/__init__.py +12 -0
  2. uacs/adapters/__init__.py +19 -0
  3. uacs/adapters/agent_skill_adapter.py +202 -0
  4. uacs/adapters/agents_md_adapter.py +330 -0
  5. uacs/adapters/base.py +261 -0
  6. uacs/adapters/clinerules_adapter.py +39 -0
  7. uacs/adapters/cursorrules_adapter.py +39 -0
  8. uacs/api.py +262 -0
  9. uacs/cli/__init__.py +6 -0
  10. uacs/cli/context.py +349 -0
  11. uacs/cli/main.py +195 -0
  12. uacs/cli/mcp.py +115 -0
  13. uacs/cli/memory.py +142 -0
  14. uacs/cli/packages.py +309 -0
  15. uacs/cli/skills.py +144 -0
  16. uacs/cli/utils.py +24 -0
  17. uacs/config/repositories.yaml +26 -0
  18. uacs/context/__init__.py +0 -0
  19. uacs/context/agent_context.py +406 -0
  20. uacs/context/shared_context.py +661 -0
  21. uacs/context/unified_context.py +332 -0
  22. uacs/mcp_server_entry.py +80 -0
  23. uacs/memory/__init__.py +5 -0
  24. uacs/memory/simple_memory.py +255 -0
  25. uacs/packages/__init__.py +26 -0
  26. uacs/packages/manager.py +413 -0
  27. uacs/packages/models.py +60 -0
  28. uacs/packages/sources.py +270 -0
  29. uacs/protocols/__init__.py +5 -0
  30. uacs/protocols/mcp/__init__.py +8 -0
  31. uacs/protocols/mcp/manager.py +77 -0
  32. uacs/protocols/mcp/skills_server.py +700 -0
  33. uacs/skills_validator.py +367 -0
  34. uacs/utils/__init__.py +5 -0
  35. uacs/utils/paths.py +24 -0
  36. uacs/visualization/README.md +132 -0
  37. uacs/visualization/__init__.py +36 -0
  38. uacs/visualization/models.py +195 -0
  39. uacs/visualization/static/index.html +857 -0
  40. uacs/visualization/storage.py +402 -0
  41. uacs/visualization/visualization.py +328 -0
  42. uacs/visualization/web_server.py +364 -0
  43. universal_agent_context-0.2.0.dist-info/METADATA +873 -0
  44. universal_agent_context-0.2.0.dist-info/RECORD +47 -0
  45. universal_agent_context-0.2.0.dist-info/WHEEL +4 -0
  46. universal_agent_context-0.2.0.dist-info/entry_points.txt +2 -0
  47. universal_agent_context-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,700 @@
1
+ """MCP Tools Server for Skills & Context Management.
2
+
3
+ Exposes all unified context capabilities as MCP tools following
4
+ the Model Context Protocol specification.
5
+
6
+ Spec: https://modelcontextprotocol.io/specification/
7
+ """
8
+
9
+ import json
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from mcp.server import Server
14
+ from mcp.types import TextContent, Tool
15
+
16
+ # Initialize server
17
+ server = Server("uacs-mcp-server")
18
+
19
+
20
+ @server.list_tools()
21
+ async def list_tools() -> list[Tool]:
22
+ """List all available MCP tools."""
23
+ return [
24
+ # Skills Management
25
+ Tool(
26
+ name="skills_list",
27
+ description="List all available agent skills",
28
+ inputSchema={
29
+ "type": "object",
30
+ "properties": {
31
+ "skills_file": {
32
+ "type": "string",
33
+ "description": "Optional path to skills file or directory",
34
+ }
35
+ },
36
+ },
37
+ ),
38
+ Tool(
39
+ name="skills_show",
40
+ description="Show detailed information about a specific skill",
41
+ inputSchema={
42
+ "type": "object",
43
+ "properties": {
44
+ "skill_name": {
45
+ "type": "string",
46
+ "description": "Name of the skill to show",
47
+ },
48
+ "skills_file": {
49
+ "type": "string",
50
+ "description": "Optional path to skills file or directory",
51
+ },
52
+ },
53
+ "required": ["skill_name"],
54
+ },
55
+ ),
56
+ Tool(
57
+ name="skills_test_trigger",
58
+ description="Test which skill would be triggered by a query",
59
+ inputSchema={
60
+ "type": "object",
61
+ "properties": {
62
+ "query": {"type": "string", "description": "Query to test"},
63
+ "skills_file": {
64
+ "type": "string",
65
+ "description": "Optional path to skills file or directory",
66
+ },
67
+ },
68
+ "required": ["query"],
69
+ },
70
+ ),
71
+ Tool(
72
+ name="skills_validate",
73
+ description="Validate agent skills file format",
74
+ inputSchema={
75
+ "type": "object",
76
+ "properties": {
77
+ "skills_file": {
78
+ "type": "string",
79
+ "description": "Path to skills file (SKILL.md or directory)",
80
+ }
81
+ },
82
+ },
83
+ ),
84
+ # Context Management
85
+ Tool(
86
+ name="context_stats",
87
+ description="Get context and token usage statistics",
88
+ inputSchema={"type": "object", "properties": {}},
89
+ ),
90
+ Tool(
91
+ name="context_get_compressed",
92
+ description="Get compressed context within token budget",
93
+ inputSchema={
94
+ "type": "object",
95
+ "properties": {
96
+ "agent": {
97
+ "type": "string",
98
+ "description": "Filter by agent name (optional)",
99
+ },
100
+ "max_tokens": {
101
+ "type": "integer",
102
+ "description": "Maximum tokens to return",
103
+ "default": 4000,
104
+ },
105
+ },
106
+ },
107
+ ),
108
+ Tool(
109
+ name="context_add_entry",
110
+ description="Add a new context entry",
111
+ inputSchema={
112
+ "type": "object",
113
+ "properties": {
114
+ "content": {"type": "string", "description": "Context content"},
115
+ "agent": {"type": "string", "description": "Agent name"},
116
+ "references": {
117
+ "type": "array",
118
+ "items": {"type": "string"},
119
+ "description": "Referenced entry IDs",
120
+ },
121
+ },
122
+ "required": ["content", "agent"],
123
+ },
124
+ ),
125
+ Tool(
126
+ name="context_compress",
127
+ description="Manually trigger context compression",
128
+ inputSchema={"type": "object", "properties": {}},
129
+ ),
130
+ Tool(
131
+ name="context_graph",
132
+ description="Get context relationship graph",
133
+ inputSchema={"type": "object", "properties": {}},
134
+ ),
135
+ # AGENTS.md Management
136
+ Tool(
137
+ name="agents_md_load",
138
+ description="Load and parse AGENTS.md file",
139
+ inputSchema={
140
+ "type": "object",
141
+ "properties": {
142
+ "project_root": {
143
+ "type": "string",
144
+ "description": "Project root directory",
145
+ }
146
+ },
147
+ },
148
+ ),
149
+ Tool(
150
+ name="agents_md_to_prompt",
151
+ description="Convert AGENTS.md to system prompt",
152
+ inputSchema={
153
+ "type": "object",
154
+ "properties": {
155
+ "project_root": {
156
+ "type": "string",
157
+ "description": "Project root directory",
158
+ }
159
+ },
160
+ },
161
+ ),
162
+ # Unified Context
163
+ Tool(
164
+ name="unified_build_prompt",
165
+ description="Build complete agent prompt with all context sources",
166
+ inputSchema={
167
+ "type": "object",
168
+ "properties": {
169
+ "user_query": {"type": "string", "description": "User's query"},
170
+ "agent_name": {
171
+ "type": "string",
172
+ "description": "Agent receiving the prompt",
173
+ },
174
+ "include_history": {
175
+ "type": "boolean",
176
+ "description": "Include shared context history",
177
+ "default": True,
178
+ },
179
+ "max_context_tokens": {
180
+ "type": "integer",
181
+ "description": "Max tokens for context",
182
+ "default": 4000,
183
+ },
184
+ },
185
+ "required": ["user_query", "agent_name"],
186
+ },
187
+ ),
188
+ Tool(
189
+ name="unified_capabilities",
190
+ description="Get all unified capabilities (Agent Skills + AGENTS.md + Context)",
191
+ inputSchema={"type": "object", "properties": {}},
192
+ ),
193
+ Tool(
194
+ name="unified_token_stats",
195
+ description="Get token usage statistics across all sources",
196
+ inputSchema={"type": "object", "properties": {}},
197
+ ),
198
+ # Package Management
199
+ Tool(
200
+ name="install_package",
201
+ description="Install package from GitHub, Git URL, or local path",
202
+ inputSchema={
203
+ "type": "object",
204
+ "properties": {
205
+ "source": {
206
+ "type": "string",
207
+ "description": "Package source (owner/repo, git URL, or local path)",
208
+ },
209
+ "validate": {
210
+ "type": "boolean",
211
+ "description": "Whether to validate before installing",
212
+ "default": True,
213
+ },
214
+ },
215
+ "required": ["source"],
216
+ },
217
+ ),
218
+ Tool(
219
+ name="list_installed_packages",
220
+ description="List all installed packages",
221
+ inputSchema={"type": "object", "properties": {}},
222
+ ),
223
+ Tool(
224
+ name="validate_package",
225
+ description="Validate a package without installing",
226
+ inputSchema={
227
+ "type": "object",
228
+ "properties": {
229
+ "source": {
230
+ "type": "string",
231
+ "description": "Package source to validate",
232
+ },
233
+ },
234
+ "required": ["source"],
235
+ },
236
+ ),
237
+ # Project Validation
238
+ Tool(
239
+ name="project_validate",
240
+ description="Validate AGENTS.md and agent skills configuration",
241
+ inputSchema={
242
+ "type": "object",
243
+ "properties": {
244
+ "project_root": {
245
+ "type": "string",
246
+ "description": "Project root directory",
247
+ },
248
+ "include_suggestions": {
249
+ "type": "boolean",
250
+ "description": "Include suggestions in report",
251
+ "default": True,
252
+ },
253
+ },
254
+ },
255
+ ),
256
+ # Claude Code Context Retrieval
257
+ Tool(
258
+ name="uacs_search_context",
259
+ description="Search stored Claude Code conversations by topic or query",
260
+ inputSchema={
261
+ "type": "object",
262
+ "properties": {
263
+ "query": {
264
+ "type": "string",
265
+ "description": "What to search for in stored conversations",
266
+ },
267
+ "topics": {
268
+ "type": "array",
269
+ "items": {"type": "string"},
270
+ "description": "Filter by topics (e.g., security, performance, bug)",
271
+ },
272
+ "max_tokens": {
273
+ "type": "integer",
274
+ "description": "Maximum tokens to return",
275
+ "default": 4000,
276
+ },
277
+ },
278
+ "required": ["query"],
279
+ },
280
+ ),
281
+ Tool(
282
+ name="uacs_list_topics",
283
+ description="List all topics in stored Claude Code conversations",
284
+ inputSchema={"type": "object", "properties": {}},
285
+ ),
286
+ Tool(
287
+ name="uacs_get_recent_sessions",
288
+ description="Get most recent Claude Code sessions",
289
+ inputSchema={
290
+ "type": "object",
291
+ "properties": {
292
+ "limit": {
293
+ "type": "integer",
294
+ "description": "Number of recent sessions to return",
295
+ "default": 5,
296
+ }
297
+ },
298
+ },
299
+ ),
300
+ ]
301
+
302
+
303
+ @server.call_tool()
304
+ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
305
+ """Handle tool invocation."""
306
+ from uacs.adapters.agent_skill_adapter import AgentSkillAdapter
307
+ from uacs.adapters.agents_md_adapter import AgentsMDAdapter
308
+ from uacs.context.unified_context import UnifiedContextAdapter
309
+ from uacs.packages import PackageManager
310
+ from uacs.cli.utils import get_project_root
311
+
312
+ # Skills Management Tools
313
+ if name == "skills_list":
314
+ adapters = AgentSkillAdapter.discover_skills(get_project_root())
315
+ skills = [a.parsed.name for a in adapters if a.parsed]
316
+ return [TextContent(type="text", text=json.dumps({"skills": skills}, indent=2))]
317
+
318
+ if name == "skills_show":
319
+ adapters = AgentSkillAdapter.discover_skills(get_project_root())
320
+ skill_name = arguments["skill_name"]
321
+ for adapter in adapters:
322
+ if adapter.parsed and adapter.parsed.name == skill_name:
323
+ return [
324
+ TextContent(
325
+ type="text",
326
+ text=adapter.to_system_prompt(),
327
+ )
328
+ ]
329
+ return [TextContent(type="text", text=f"Skill '{skill_name}' not found")]
330
+
331
+ if name == "skills_test_trigger":
332
+ adapters = AgentSkillAdapter.discover_skills(get_project_root())
333
+ query = arguments["query"].lower()
334
+ matched_skill = None
335
+
336
+ for adapter in adapters:
337
+ if adapter.parsed and hasattr(adapter.parsed, "triggers"):
338
+ for trigger in adapter.parsed.triggers:
339
+ if trigger.lower() in query or query in trigger.lower():
340
+ matched_skill = adapter.parsed
341
+ break
342
+ if matched_skill:
343
+ break
344
+
345
+ if matched_skill:
346
+ return [
347
+ TextContent(
348
+ type="text",
349
+ text=json.dumps(
350
+ {
351
+ "matched": True,
352
+ "skill_name": matched_skill.name,
353
+ "triggers": matched_skill.triggers,
354
+ },
355
+ indent=2,
356
+ ),
357
+ )
358
+ ]
359
+ return [TextContent(type="text", text=json.dumps({"matched": False}, indent=2))]
360
+
361
+ if name == "skills_validate":
362
+ adapters = AgentSkillAdapter.discover_skills(get_project_root())
363
+ issues = []
364
+ skill_count = 0
365
+ for adapter in adapters:
366
+ if adapter.parsed:
367
+ skill_count += 1
368
+ if not adapter.parsed.description:
369
+ issues.append(f"Skill '{adapter.parsed.name}': Missing description")
370
+ if (
371
+ not hasattr(adapter.parsed, "triggers")
372
+ or not adapter.parsed.triggers
373
+ ):
374
+ issues.append(f"Skill '{adapter.parsed.name}': No triggers")
375
+
376
+ return [
377
+ TextContent(
378
+ type="text",
379
+ text=json.dumps(
380
+ {
381
+ "valid": len(issues) == 0,
382
+ "skill_count": skill_count,
383
+ "issues": issues,
384
+ },
385
+ indent=2,
386
+ ),
387
+ )
388
+ ]
389
+
390
+ # Context Management Tools
391
+ if name == "context_stats":
392
+ context_adapter = UnifiedContextAdapter()
393
+ stats = context_adapter.get_token_stats()
394
+ return [TextContent(type="text", text=json.dumps(stats, indent=2))]
395
+
396
+ if name == "context_get_compressed":
397
+ context_adapter = UnifiedContextAdapter()
398
+ context = context_adapter.shared_context.get_compressed_context(
399
+ agent=arguments.get("agent"), max_tokens=arguments.get("max_tokens", 4000)
400
+ )
401
+ return [TextContent(type="text", text=context)]
402
+
403
+ if name == "context_add_entry":
404
+ context_adapter = UnifiedContextAdapter()
405
+ entry_id = context_adapter.shared_context.add_entry(
406
+ content=arguments["content"],
407
+ agent=arguments["agent"],
408
+ references=arguments.get("references", []),
409
+ )
410
+ return [
411
+ TextContent(type="text", text=json.dumps({"entry_id": entry_id}, indent=2))
412
+ ]
413
+
414
+ if name == "context_compress":
415
+ context_adapter = UnifiedContextAdapter()
416
+ before = context_adapter.shared_context.get_stats()
417
+ context_adapter.optimize_context()
418
+ after = context_adapter.shared_context.get_stats()
419
+
420
+ return [
421
+ TextContent(
422
+ type="text",
423
+ text=json.dumps(
424
+ {
425
+ "before_tokens": before["total_tokens"],
426
+ "after_tokens": after["total_tokens"],
427
+ "saved": before["total_tokens"] - after["total_tokens"],
428
+ },
429
+ indent=2,
430
+ ),
431
+ )
432
+ ]
433
+
434
+ if name == "context_graph":
435
+ context_adapter = UnifiedContextAdapter()
436
+ graph = context_adapter.shared_context.get_context_graph()
437
+ return [TextContent(type="text", text=json.dumps(graph, indent=2))]
438
+
439
+ # AGENTS.md Tools
440
+ if name == "agents_md_load":
441
+ agents_adapter = AgentsMDAdapter(
442
+ Path(arguments.get("project_root"))
443
+ if arguments.get("project_root")
444
+ else None
445
+ )
446
+ if agents_adapter.config:
447
+ return [
448
+ TextContent(
449
+ type="text",
450
+ text=json.dumps(agents_adapter.to_adk_capabilities(), indent=2),
451
+ )
452
+ ]
453
+ return [TextContent(type="text", text="AGENTS.md not found")]
454
+
455
+ if name == "agents_md_to_prompt":
456
+ agents_adapter = AgentsMDAdapter(
457
+ Path(arguments.get("project_root"))
458
+ if arguments.get("project_root")
459
+ else None
460
+ )
461
+ prompt = agents_adapter.to_system_prompt()
462
+ return [TextContent(type="text", text=prompt)]
463
+
464
+ # Unified Context Tools
465
+ if name == "unified_build_prompt":
466
+ context_adapter = UnifiedContextAdapter()
467
+ prompt = context_adapter.build_agent_prompt(
468
+ user_query=arguments["user_query"],
469
+ agent_name=arguments["agent_name"],
470
+ include_history=arguments.get("include_history", True),
471
+ max_context_tokens=arguments.get("max_context_tokens", 4000),
472
+ )
473
+ return [TextContent(type="text", text=prompt)]
474
+
475
+ if name == "unified_capabilities":
476
+ context_adapter = UnifiedContextAdapter()
477
+ caps = context_adapter.get_unified_capabilities()
478
+ return [TextContent(type="text", text=json.dumps(caps, indent=2))]
479
+
480
+ if name == "unified_token_stats":
481
+ context_adapter = UnifiedContextAdapter()
482
+ stats = context_adapter.get_token_stats()
483
+ return [TextContent(type="text", text=json.dumps(stats, indent=2))]
484
+
485
+ # Package Management Tools
486
+ if name == "install_package":
487
+ project_path = get_project_root()
488
+ manager = PackageManager(project_path)
489
+ try:
490
+ package = manager.install(
491
+ arguments["source"],
492
+ validate=arguments.get("validate", True)
493
+ )
494
+ return [
495
+ TextContent(
496
+ type="text",
497
+ text=json.dumps(
498
+ {
499
+ "installed": True,
500
+ "package_name": package.name,
501
+ "version": package.version,
502
+ "path": str(package.path),
503
+ },
504
+ indent=2,
505
+ ),
506
+ )
507
+ ]
508
+ except Exception as e:
509
+ return [
510
+ TextContent(
511
+ type="text",
512
+ text=json.dumps(
513
+ {"installed": False, "error": str(e)},
514
+ indent=2,
515
+ ),
516
+ )
517
+ ]
518
+
519
+ if name == "list_installed_packages":
520
+ project_path = get_project_root()
521
+ manager = PackageManager(project_path)
522
+ packages = manager.list_installed()
523
+ return [
524
+ TextContent(
525
+ type="text",
526
+ text=json.dumps(
527
+ [
528
+ {
529
+ "name": pkg.name,
530
+ "version": pkg.version,
531
+ "source": pkg.source,
532
+ "path": str(pkg.path),
533
+ }
534
+ for pkg in packages
535
+ ],
536
+ indent=2,
537
+ ),
538
+ )
539
+ ]
540
+
541
+ if name == "validate_package":
542
+ project_path = get_project_root()
543
+ manager = PackageManager(project_path)
544
+ try:
545
+ is_valid = manager.validate(arguments["source"])
546
+ return [
547
+ TextContent(
548
+ type="text",
549
+ text=json.dumps(
550
+ {"valid": is_valid},
551
+ indent=2,
552
+ ),
553
+ )
554
+ ]
555
+ except Exception as e:
556
+ return [
557
+ TextContent(
558
+ type="text",
559
+ text=json.dumps(
560
+ {"valid": False, "error": str(e)},
561
+ indent=2,
562
+ ),
563
+ )
564
+ ]
565
+
566
+ # Project Validation
567
+ if name == "project_validate":
568
+ # Note: ProjectValidator not yet extracted, will be done in future phase
569
+ return [
570
+ TextContent(
571
+ type="text",
572
+ text=json.dumps(
573
+ {"error": "ProjectValidator not yet available in UACS"}, indent=2
574
+ ),
575
+ )
576
+ ]
577
+
578
+ # Claude Code Context Retrieval Tools
579
+ if name == "uacs_search_context":
580
+ context_adapter = UnifiedContextAdapter()
581
+ query = arguments["query"]
582
+ topics = arguments.get("topics")
583
+ max_tokens = arguments.get("max_tokens", 4000)
584
+
585
+ # Use focused context if topics provided, otherwise compressed context
586
+ if topics and len(topics) > 0:
587
+ context = context_adapter.shared_context.get_focused_context(
588
+ topics=topics, max_tokens=max_tokens
589
+ )
590
+ else:
591
+ context = context_adapter.shared_context.get_compressed_context(
592
+ max_tokens=max_tokens
593
+ )
594
+
595
+ if not context or len(context.strip()) == 0:
596
+ return [
597
+ TextContent(
598
+ type="text",
599
+ text=json.dumps(
600
+ {
601
+ "found": False,
602
+ "message": f"No stored conversations found for: {query}",
603
+ },
604
+ indent=2,
605
+ ),
606
+ )
607
+ ]
608
+
609
+ return [
610
+ TextContent(
611
+ type="text",
612
+ text=json.dumps(
613
+ {
614
+ "found": True,
615
+ "query": query,
616
+ "topics": topics,
617
+ "context": context,
618
+ "tokens": context_adapter.shared_context.count_tokens(context),
619
+ },
620
+ indent=2,
621
+ ),
622
+ )
623
+ ]
624
+
625
+ if name == "uacs_list_topics":
626
+ context_adapter = UnifiedContextAdapter()
627
+
628
+ # Get all unique topics from entries
629
+ topics = set()
630
+ for entry in context_adapter.shared_context.entries.values():
631
+ if entry.topics:
632
+ topics.update(entry.topics)
633
+
634
+ return [
635
+ TextContent(
636
+ type="text",
637
+ text=json.dumps(
638
+ {"topics": sorted(list(topics)), "count": len(topics)}, indent=2
639
+ ),
640
+ )
641
+ ]
642
+
643
+ if name == "uacs_get_recent_sessions":
644
+ context_adapter = UnifiedContextAdapter()
645
+ limit = arguments.get("limit", 5)
646
+
647
+ # Get entries sorted by timestamp (most recent first)
648
+ all_entries = list(context_adapter.shared_context.entries.values())
649
+ all_entries.sort(key=lambda e: e.timestamp, reverse=True)
650
+
651
+ # Filter for Claude Code sessions
652
+ sessions = []
653
+ seen_sessions = set()
654
+
655
+ for entry in all_entries:
656
+ if len(sessions) >= limit:
657
+ break
658
+
659
+ # Check if from Claude Code hook
660
+ if entry.metadata and entry.metadata.get("source") == "claude-code-hook":
661
+ session_id = entry.metadata.get("session_id")
662
+ if session_id and session_id not in seen_sessions:
663
+ seen_sessions.add(session_id)
664
+ sessions.append({
665
+ "session_id": session_id,
666
+ "timestamp": entry.timestamp,
667
+ "topics": entry.topics,
668
+ "turn_count": entry.metadata.get("turn_count", 0),
669
+ "preview": entry.content[:200] + "..." if len(entry.content) > 200 else entry.content
670
+ })
671
+
672
+ return [
673
+ TextContent(
674
+ type="text",
675
+ text=json.dumps(
676
+ {"sessions": sessions, "count": len(sessions)}, indent=2
677
+ ),
678
+ )
679
+ ]
680
+
681
+ return [TextContent(type="text", text=f"Unknown tool: {name}")]
682
+
683
+
684
+ async def main():
685
+ """Run MCP server."""
686
+ from mcp.server.stdio import stdio_server
687
+
688
+ async with stdio_server() as (read_stream, write_stream):
689
+ await server.run(
690
+ read_stream, write_stream, server.create_initialization_options()
691
+ )
692
+
693
+
694
+ if __name__ == "__main__":
695
+ import asyncio
696
+
697
+ asyncio.run(main())
698
+
699
+
700
+ __all__ = ["server", "main"]