synapse-sdk 1.0.0a11__py3-none-any.whl → 2026.1.1b2__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.

Potentially problematic release.


This version of synapse-sdk might be problematic. Click here for more details.

Files changed (261) hide show
  1. synapse_sdk/__init__.py +24 -0
  2. synapse_sdk/cli/__init__.py +9 -8
  3. synapse_sdk/cli/agent/__init__.py +25 -0
  4. synapse_sdk/cli/agent/config.py +104 -0
  5. synapse_sdk/cli/agent/select.py +197 -0
  6. synapse_sdk/cli/auth.py +104 -0
  7. synapse_sdk/cli/main.py +1025 -0
  8. synapse_sdk/cli/plugin/__init__.py +58 -0
  9. synapse_sdk/cli/plugin/create.py +566 -0
  10. synapse_sdk/cli/plugin/job.py +196 -0
  11. synapse_sdk/cli/plugin/publish.py +322 -0
  12. synapse_sdk/cli/plugin/run.py +131 -0
  13. synapse_sdk/cli/plugin/test.py +200 -0
  14. synapse_sdk/clients/README.md +239 -0
  15. synapse_sdk/clients/__init__.py +5 -0
  16. synapse_sdk/clients/_template.py +266 -0
  17. synapse_sdk/clients/agent/__init__.py +84 -29
  18. synapse_sdk/clients/agent/async_ray.py +289 -0
  19. synapse_sdk/clients/agent/container.py +83 -0
  20. synapse_sdk/clients/agent/plugin.py +101 -0
  21. synapse_sdk/clients/agent/ray.py +296 -39
  22. synapse_sdk/clients/backend/__init__.py +152 -12
  23. synapse_sdk/clients/backend/annotation.py +164 -22
  24. synapse_sdk/clients/backend/core.py +101 -0
  25. synapse_sdk/clients/backend/data_collection.py +292 -0
  26. synapse_sdk/clients/backend/hitl.py +87 -0
  27. synapse_sdk/clients/backend/integration.py +374 -46
  28. synapse_sdk/clients/backend/ml.py +134 -22
  29. synapse_sdk/clients/backend/models.py +247 -0
  30. synapse_sdk/clients/base.py +538 -59
  31. synapse_sdk/clients/exceptions.py +35 -7
  32. synapse_sdk/clients/pipeline/__init__.py +5 -0
  33. synapse_sdk/clients/pipeline/client.py +636 -0
  34. synapse_sdk/clients/protocols.py +178 -0
  35. synapse_sdk/clients/utils.py +86 -8
  36. synapse_sdk/clients/validation.py +58 -0
  37. synapse_sdk/enums.py +76 -0
  38. synapse_sdk/exceptions.py +168 -0
  39. synapse_sdk/integrations/__init__.py +74 -0
  40. synapse_sdk/integrations/_base.py +119 -0
  41. synapse_sdk/integrations/_context.py +53 -0
  42. synapse_sdk/integrations/ultralytics/__init__.py +78 -0
  43. synapse_sdk/integrations/ultralytics/_callbacks.py +126 -0
  44. synapse_sdk/integrations/ultralytics/_patches.py +124 -0
  45. synapse_sdk/loggers.py +476 -95
  46. synapse_sdk/mcp/MCP.md +69 -0
  47. synapse_sdk/mcp/__init__.py +48 -0
  48. synapse_sdk/mcp/__main__.py +6 -0
  49. synapse_sdk/mcp/config.py +349 -0
  50. synapse_sdk/mcp/prompts/__init__.py +4 -0
  51. synapse_sdk/mcp/resources/__init__.py +4 -0
  52. synapse_sdk/mcp/server.py +1352 -0
  53. synapse_sdk/mcp/tools/__init__.py +6 -0
  54. synapse_sdk/plugins/__init__.py +133 -9
  55. synapse_sdk/plugins/action.py +229 -0
  56. synapse_sdk/plugins/actions/__init__.py +82 -0
  57. synapse_sdk/plugins/actions/dataset/__init__.py +37 -0
  58. synapse_sdk/plugins/actions/dataset/action.py +471 -0
  59. synapse_sdk/plugins/actions/export/__init__.py +55 -0
  60. synapse_sdk/plugins/actions/export/action.py +183 -0
  61. synapse_sdk/plugins/actions/export/context.py +59 -0
  62. synapse_sdk/plugins/actions/inference/__init__.py +84 -0
  63. synapse_sdk/plugins/actions/inference/action.py +285 -0
  64. synapse_sdk/plugins/actions/inference/context.py +81 -0
  65. synapse_sdk/plugins/actions/inference/deployment.py +322 -0
  66. synapse_sdk/plugins/actions/inference/serve.py +252 -0
  67. synapse_sdk/plugins/actions/train/__init__.py +54 -0
  68. synapse_sdk/plugins/actions/train/action.py +326 -0
  69. synapse_sdk/plugins/actions/train/context.py +57 -0
  70. synapse_sdk/plugins/actions/upload/__init__.py +49 -0
  71. synapse_sdk/plugins/actions/upload/action.py +165 -0
  72. synapse_sdk/plugins/actions/upload/context.py +61 -0
  73. synapse_sdk/plugins/config.py +98 -0
  74. synapse_sdk/plugins/context/__init__.py +109 -0
  75. synapse_sdk/plugins/context/env.py +113 -0
  76. synapse_sdk/plugins/datasets/__init__.py +113 -0
  77. synapse_sdk/plugins/datasets/converters/__init__.py +76 -0
  78. synapse_sdk/plugins/datasets/converters/base.py +347 -0
  79. synapse_sdk/plugins/datasets/converters/yolo/__init__.py +9 -0
  80. synapse_sdk/plugins/datasets/converters/yolo/from_dm.py +468 -0
  81. synapse_sdk/plugins/datasets/converters/yolo/to_dm.py +381 -0
  82. synapse_sdk/plugins/datasets/formats/__init__.py +82 -0
  83. synapse_sdk/plugins/datasets/formats/dm.py +351 -0
  84. synapse_sdk/plugins/datasets/formats/yolo.py +240 -0
  85. synapse_sdk/plugins/decorators.py +83 -0
  86. synapse_sdk/plugins/discovery.py +790 -0
  87. synapse_sdk/plugins/docs/ACTION_DEV_GUIDE.md +933 -0
  88. synapse_sdk/plugins/docs/ARCHITECTURE.md +1225 -0
  89. synapse_sdk/plugins/docs/LOGGING_SYSTEM.md +683 -0
  90. synapse_sdk/plugins/docs/OVERVIEW.md +531 -0
  91. synapse_sdk/plugins/docs/PIPELINE_GUIDE.md +145 -0
  92. synapse_sdk/plugins/docs/README.md +513 -0
  93. synapse_sdk/plugins/docs/STEP.md +656 -0
  94. synapse_sdk/plugins/enums.py +70 -10
  95. synapse_sdk/plugins/errors.py +92 -0
  96. synapse_sdk/plugins/executors/__init__.py +43 -0
  97. synapse_sdk/plugins/executors/local.py +99 -0
  98. synapse_sdk/plugins/executors/ray/__init__.py +18 -0
  99. synapse_sdk/plugins/executors/ray/base.py +282 -0
  100. synapse_sdk/plugins/executors/ray/job.py +298 -0
  101. synapse_sdk/plugins/executors/ray/jobs_api.py +511 -0
  102. synapse_sdk/plugins/executors/ray/packaging.py +137 -0
  103. synapse_sdk/plugins/executors/ray/pipeline.py +792 -0
  104. synapse_sdk/plugins/executors/ray/task.py +257 -0
  105. synapse_sdk/plugins/models/__init__.py +26 -0
  106. synapse_sdk/plugins/models/logger.py +173 -0
  107. synapse_sdk/plugins/models/pipeline.py +25 -0
  108. synapse_sdk/plugins/pipelines/__init__.py +81 -0
  109. synapse_sdk/plugins/pipelines/action_pipeline.py +417 -0
  110. synapse_sdk/plugins/pipelines/context.py +107 -0
  111. synapse_sdk/plugins/pipelines/display.py +311 -0
  112. synapse_sdk/plugins/runner.py +114 -0
  113. synapse_sdk/plugins/schemas/__init__.py +19 -0
  114. synapse_sdk/plugins/schemas/results.py +152 -0
  115. synapse_sdk/plugins/steps/__init__.py +63 -0
  116. synapse_sdk/plugins/steps/base.py +128 -0
  117. synapse_sdk/plugins/steps/context.py +90 -0
  118. synapse_sdk/plugins/steps/orchestrator.py +128 -0
  119. synapse_sdk/plugins/steps/registry.py +103 -0
  120. synapse_sdk/plugins/steps/utils/__init__.py +20 -0
  121. synapse_sdk/plugins/steps/utils/logging.py +85 -0
  122. synapse_sdk/plugins/steps/utils/timing.py +71 -0
  123. synapse_sdk/plugins/steps/utils/validation.py +68 -0
  124. synapse_sdk/plugins/templates/__init__.py +50 -0
  125. synapse_sdk/plugins/templates/base/.gitignore.j2 +26 -0
  126. synapse_sdk/plugins/templates/base/.synapseignore.j2 +11 -0
  127. synapse_sdk/plugins/templates/base/README.md.j2 +26 -0
  128. synapse_sdk/plugins/templates/base/plugin/__init__.py.j2 +1 -0
  129. synapse_sdk/plugins/templates/base/pyproject.toml.j2 +14 -0
  130. synapse_sdk/plugins/templates/base/requirements.txt.j2 +1 -0
  131. synapse_sdk/plugins/templates/custom/plugin/main.py.j2 +18 -0
  132. synapse_sdk/plugins/templates/data_validation/plugin/validate.py.j2 +32 -0
  133. synapse_sdk/plugins/templates/export/plugin/export.py.j2 +36 -0
  134. synapse_sdk/plugins/templates/neural_net/plugin/inference.py.j2 +36 -0
  135. synapse_sdk/plugins/templates/neural_net/plugin/train.py.j2 +33 -0
  136. synapse_sdk/plugins/templates/post_annotation/plugin/post_annotate.py.j2 +32 -0
  137. synapse_sdk/plugins/templates/pre_annotation/plugin/pre_annotate.py.j2 +32 -0
  138. synapse_sdk/plugins/templates/smart_tool/plugin/auto_label.py.j2 +44 -0
  139. synapse_sdk/plugins/templates/upload/plugin/upload.py.j2 +35 -0
  140. synapse_sdk/plugins/testing/__init__.py +25 -0
  141. synapse_sdk/plugins/testing/sample_actions.py +98 -0
  142. synapse_sdk/plugins/types.py +206 -0
  143. synapse_sdk/plugins/upload.py +595 -64
  144. synapse_sdk/plugins/utils.py +325 -37
  145. synapse_sdk/shared/__init__.py +25 -0
  146. synapse_sdk/utils/__init__.py +1 -0
  147. synapse_sdk/utils/auth.py +74 -0
  148. synapse_sdk/utils/file/__init__.py +58 -0
  149. synapse_sdk/utils/file/archive.py +449 -0
  150. synapse_sdk/utils/file/checksum.py +167 -0
  151. synapse_sdk/utils/file/download.py +286 -0
  152. synapse_sdk/utils/file/io.py +129 -0
  153. synapse_sdk/utils/file/requirements.py +36 -0
  154. synapse_sdk/utils/network.py +168 -0
  155. synapse_sdk/utils/storage/__init__.py +238 -0
  156. synapse_sdk/utils/storage/config.py +188 -0
  157. synapse_sdk/utils/storage/errors.py +52 -0
  158. synapse_sdk/utils/storage/providers/__init__.py +13 -0
  159. synapse_sdk/utils/storage/providers/base.py +76 -0
  160. synapse_sdk/utils/storage/providers/gcs.py +168 -0
  161. synapse_sdk/utils/storage/providers/http.py +250 -0
  162. synapse_sdk/utils/storage/providers/local.py +126 -0
  163. synapse_sdk/utils/storage/providers/s3.py +177 -0
  164. synapse_sdk/utils/storage/providers/sftp.py +208 -0
  165. synapse_sdk/utils/storage/registry.py +125 -0
  166. synapse_sdk/utils/websocket.py +99 -0
  167. synapse_sdk-2026.1.1b2.dist-info/METADATA +715 -0
  168. synapse_sdk-2026.1.1b2.dist-info/RECORD +172 -0
  169. {synapse_sdk-1.0.0a11.dist-info → synapse_sdk-2026.1.1b2.dist-info}/WHEEL +1 -1
  170. synapse_sdk-2026.1.1b2.dist-info/licenses/LICENSE +201 -0
  171. locale/en/LC_MESSAGES/messages.mo +0 -0
  172. locale/en/LC_MESSAGES/messages.po +0 -39
  173. locale/ko/LC_MESSAGES/messages.mo +0 -0
  174. locale/ko/LC_MESSAGES/messages.po +0 -34
  175. synapse_sdk/cli/create_plugin.py +0 -10
  176. synapse_sdk/clients/agent/core.py +0 -7
  177. synapse_sdk/clients/agent/service.py +0 -15
  178. synapse_sdk/clients/backend/dataset.py +0 -51
  179. synapse_sdk/clients/ray/__init__.py +0 -6
  180. synapse_sdk/clients/ray/core.py +0 -22
  181. synapse_sdk/clients/ray/serve.py +0 -20
  182. synapse_sdk/i18n.py +0 -35
  183. synapse_sdk/plugins/categories/__init__.py +0 -0
  184. synapse_sdk/plugins/categories/base.py +0 -235
  185. synapse_sdk/plugins/categories/data_validation/__init__.py +0 -0
  186. synapse_sdk/plugins/categories/data_validation/actions/__init__.py +0 -0
  187. synapse_sdk/plugins/categories/data_validation/actions/validation.py +0 -10
  188. synapse_sdk/plugins/categories/data_validation/templates/config.yaml +0 -3
  189. synapse_sdk/plugins/categories/data_validation/templates/plugin/__init__.py +0 -0
  190. synapse_sdk/plugins/categories/data_validation/templates/plugin/validation.py +0 -5
  191. synapse_sdk/plugins/categories/decorators.py +0 -13
  192. synapse_sdk/plugins/categories/export/__init__.py +0 -0
  193. synapse_sdk/plugins/categories/export/actions/__init__.py +0 -0
  194. synapse_sdk/plugins/categories/export/actions/export.py +0 -10
  195. synapse_sdk/plugins/categories/import/__init__.py +0 -0
  196. synapse_sdk/plugins/categories/import/actions/__init__.py +0 -0
  197. synapse_sdk/plugins/categories/import/actions/import.py +0 -10
  198. synapse_sdk/plugins/categories/neural_net/__init__.py +0 -0
  199. synapse_sdk/plugins/categories/neural_net/actions/__init__.py +0 -0
  200. synapse_sdk/plugins/categories/neural_net/actions/deployment.py +0 -45
  201. synapse_sdk/plugins/categories/neural_net/actions/inference.py +0 -18
  202. synapse_sdk/plugins/categories/neural_net/actions/test.py +0 -10
  203. synapse_sdk/plugins/categories/neural_net/actions/train.py +0 -143
  204. synapse_sdk/plugins/categories/neural_net/templates/config.yaml +0 -12
  205. synapse_sdk/plugins/categories/neural_net/templates/plugin/__init__.py +0 -0
  206. synapse_sdk/plugins/categories/neural_net/templates/plugin/inference.py +0 -4
  207. synapse_sdk/plugins/categories/neural_net/templates/plugin/test.py +0 -2
  208. synapse_sdk/plugins/categories/neural_net/templates/plugin/train.py +0 -14
  209. synapse_sdk/plugins/categories/post_annotation/__init__.py +0 -0
  210. synapse_sdk/plugins/categories/post_annotation/actions/__init__.py +0 -0
  211. synapse_sdk/plugins/categories/post_annotation/actions/post_annotation.py +0 -10
  212. synapse_sdk/plugins/categories/post_annotation/templates/config.yaml +0 -3
  213. synapse_sdk/plugins/categories/post_annotation/templates/plugin/__init__.py +0 -0
  214. synapse_sdk/plugins/categories/post_annotation/templates/plugin/post_annotation.py +0 -3
  215. synapse_sdk/plugins/categories/pre_annotation/__init__.py +0 -0
  216. synapse_sdk/plugins/categories/pre_annotation/actions/__init__.py +0 -0
  217. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation.py +0 -10
  218. synapse_sdk/plugins/categories/pre_annotation/templates/config.yaml +0 -3
  219. synapse_sdk/plugins/categories/pre_annotation/templates/plugin/__init__.py +0 -0
  220. synapse_sdk/plugins/categories/pre_annotation/templates/plugin/pre_annotation.py +0 -3
  221. synapse_sdk/plugins/categories/registry.py +0 -16
  222. synapse_sdk/plugins/categories/smart_tool/__init__.py +0 -0
  223. synapse_sdk/plugins/categories/smart_tool/actions/__init__.py +0 -0
  224. synapse_sdk/plugins/categories/smart_tool/actions/auto_label.py +0 -37
  225. synapse_sdk/plugins/categories/smart_tool/templates/config.yaml +0 -7
  226. synapse_sdk/plugins/categories/smart_tool/templates/plugin/__init__.py +0 -0
  227. synapse_sdk/plugins/categories/smart_tool/templates/plugin/auto_label.py +0 -11
  228. synapse_sdk/plugins/categories/templates.py +0 -32
  229. synapse_sdk/plugins/cli/__init__.py +0 -21
  230. synapse_sdk/plugins/cli/publish.py +0 -37
  231. synapse_sdk/plugins/cli/run.py +0 -67
  232. synapse_sdk/plugins/exceptions.py +0 -22
  233. synapse_sdk/plugins/models.py +0 -121
  234. synapse_sdk/plugins/templates/cookiecutter.json +0 -11
  235. synapse_sdk/plugins/templates/hooks/post_gen_project.py +0 -3
  236. synapse_sdk/plugins/templates/hooks/pre_prompt.py +0 -21
  237. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.env +0 -24
  238. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.env.dist +0 -24
  239. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.gitignore +0 -27
  240. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.pre-commit-config.yaml +0 -7
  241. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/README.md +0 -5
  242. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/config.yaml +0 -6
  243. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/main.py +0 -4
  244. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/plugin/__init__.py +0 -0
  245. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/pyproject.toml +0 -13
  246. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/requirements.txt +0 -1
  247. synapse_sdk/shared/enums.py +0 -8
  248. synapse_sdk/utils/debug.py +0 -5
  249. synapse_sdk/utils/file.py +0 -87
  250. synapse_sdk/utils/module_loading.py +0 -29
  251. synapse_sdk/utils/pydantic/__init__.py +0 -0
  252. synapse_sdk/utils/pydantic/config.py +0 -4
  253. synapse_sdk/utils/pydantic/errors.py +0 -33
  254. synapse_sdk/utils/pydantic/validators.py +0 -7
  255. synapse_sdk/utils/storage.py +0 -91
  256. synapse_sdk/utils/string.py +0 -11
  257. synapse_sdk-1.0.0a11.dist-info/LICENSE +0 -21
  258. synapse_sdk-1.0.0a11.dist-info/METADATA +0 -43
  259. synapse_sdk-1.0.0a11.dist-info/RECORD +0 -111
  260. {synapse_sdk-1.0.0a11.dist-info → synapse_sdk-2026.1.1b2.dist-info}/entry_points.txt +0 -0
  261. {synapse_sdk-1.0.0a11.dist-info → synapse_sdk-2026.1.1b2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1025 @@
1
+ """Synapse SDK CLI main entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Annotated, Optional
7
+
8
+ import typer
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+
12
+ cli = typer.Typer(
13
+ name='synapse',
14
+ help='Synapse SDK CLI.',
15
+ no_args_is_help=True,
16
+ rich_markup_mode='rich',
17
+ )
18
+ console = Console()
19
+ err_console = Console(stderr=True)
20
+
21
+ # Plugin subcommand group
22
+ plugin_app = typer.Typer(help='Plugin development commands.')
23
+ cli.add_typer(plugin_app, name='plugin')
24
+
25
+ # Job subcommand group (under plugin)
26
+ job_app = typer.Typer(help='Job management commands.')
27
+ plugin_app.add_typer(job_app, name='job')
28
+
29
+ # Agent subcommand group
30
+ agent_app = typer.Typer(help='Agent configuration commands.')
31
+ cli.add_typer(agent_app, name='agent')
32
+
33
+ # MCP subcommand group
34
+ mcp_app = typer.Typer(help='MCP server commands.')
35
+ cli.add_typer(mcp_app, name='mcp')
36
+
37
+
38
+ @job_app.command('get')
39
+ def job_get(
40
+ job_id: Annotated[
41
+ str,
42
+ typer.Argument(help='Job ID (UUID) to get details for.'),
43
+ ],
44
+ host: Annotated[
45
+ Optional[str],
46
+ typer.Option('--host', help='Synapse API host.'),
47
+ ] = None,
48
+ token: Annotated[
49
+ Optional[str],
50
+ typer.Option('--token', '-t', help='Access token.'),
51
+ ] = None,
52
+ ) -> None:
53
+ """Get job details.
54
+
55
+ [bold]Examples:[/bold]
56
+
57
+ synapse plugin job get 123
58
+ """
59
+ from synapse_sdk.cli.auth import get_auth_config
60
+ from synapse_sdk.cli.plugin.job import display_job, get_job
61
+ from synapse_sdk.plugins.errors import PluginError
62
+
63
+ try:
64
+ auth = get_auth_config(host=host, token=token, console=console, interactive=False)
65
+ job = get_job(job_id, auth, console)
66
+ display_job(job, console)
67
+
68
+ except PluginError as e:
69
+ err_console.print(f'[red]Error:[/red] {e.message}')
70
+ raise typer.Exit(1)
71
+ except Exception as e:
72
+ err_console.print(f'[red]Error:[/red] {e}')
73
+ raise typer.Exit(1)
74
+
75
+
76
+ @job_app.command('logs')
77
+ def job_logs(
78
+ job_id: Annotated[
79
+ str,
80
+ typer.Argument(help='Job ID (UUID) to get logs for.'),
81
+ ],
82
+ follow: Annotated[
83
+ bool,
84
+ typer.Option('--follow', '-f', help='Follow log output (stream).'),
85
+ ] = False,
86
+ host: Annotated[
87
+ Optional[str],
88
+ typer.Option('--host', help='Synapse API host.'),
89
+ ] = None,
90
+ token: Annotated[
91
+ Optional[str],
92
+ typer.Option('--token', '-t', help='Access token.'),
93
+ ] = None,
94
+ ) -> None:
95
+ """Get job logs.
96
+
97
+ [bold]Examples:[/bold]
98
+
99
+ synapse plugin job logs 123
100
+ synapse plugin job logs 123 -f
101
+ """
102
+ from synapse_sdk.cli.auth import get_auth_config
103
+ from synapse_sdk.cli.plugin.job import get_job_logs, tail_job_logs
104
+ from synapse_sdk.plugins.errors import PluginError
105
+
106
+ try:
107
+ auth = get_auth_config(host=host, token=token, console=console, interactive=False)
108
+
109
+ if follow:
110
+ tail_job_logs(job_id, auth, console)
111
+ else:
112
+ logs = get_job_logs(job_id, auth, console)
113
+ # Display logs
114
+ if isinstance(logs, list):
115
+ for line in logs:
116
+ if line:
117
+ console.print(line)
118
+ elif isinstance(logs, dict):
119
+ for entry in logs.get('results', []):
120
+ console.print(entry.get('message', ''))
121
+ else:
122
+ console.print(logs)
123
+
124
+ except PluginError as e:
125
+ err_console.print(f'[red]Error:[/red] {e.message}')
126
+ raise typer.Exit(1)
127
+ except Exception as e:
128
+ err_console.print(f'[red]Error:[/red] {e}')
129
+ raise typer.Exit(1)
130
+
131
+
132
+ @plugin_app.command('publish')
133
+ def plugin_publish(
134
+ path: Annotated[
135
+ Optional[Path],
136
+ typer.Option('--path', '-p', help='Plugin directory (default: current).'),
137
+ ] = None,
138
+ config: Annotated[
139
+ Optional[Path],
140
+ typer.Option('--config', '-c', help='Config file path.'),
141
+ ] = None,
142
+ host: Annotated[
143
+ Optional[str],
144
+ typer.Option('--host', help='Synapse API host.'),
145
+ ] = None,
146
+ token: Annotated[
147
+ Optional[str],
148
+ typer.Option('--token', '-t', help='Access token.'),
149
+ ] = None,
150
+ dry_run: Annotated[
151
+ bool,
152
+ typer.Option('--dry-run', help='Preview without uploading.'),
153
+ ] = False,
154
+ debug: Annotated[
155
+ bool,
156
+ typer.Option('--debug', help='Debug mode (bypasses backend validation).'),
157
+ ] = False,
158
+ yes: Annotated[
159
+ bool,
160
+ typer.Option('--yes', '-y', help='Skip confirmation.'),
161
+ ] = False,
162
+ ) -> None:
163
+ """Publish a plugin release to Synapse.
164
+
165
+ Archives plugin files and creates a new release.
166
+
167
+ [bold]Examples:[/bold]
168
+
169
+ synapse plugin publish
170
+ synapse plugin publish -p ./my-plugin --dry-run
171
+ """
172
+ import questionary
173
+
174
+ from synapse_sdk.cli.auth import get_auth_config
175
+ from synapse_sdk.cli.plugin.publish import publish_plugin
176
+ from synapse_sdk.plugins.errors import PluginError
177
+
178
+ # Resolve path
179
+ plugin_path = (path or Path.cwd()).resolve()
180
+
181
+ if not plugin_path.exists():
182
+ err_console.print(f'[red]Error:[/red] Path not found: {plugin_path}')
183
+ raise typer.Exit(1)
184
+
185
+ try:
186
+ # Get authentication (never interactive - require login first)
187
+ auth = get_auth_config(
188
+ host=host,
189
+ token=token,
190
+ console=console,
191
+ interactive=False,
192
+ )
193
+
194
+ # Confirmation prompt
195
+ if not yes and not dry_run:
196
+ if not questionary.confirm(
197
+ f'Publish plugin to {auth.host}?',
198
+ default=True,
199
+ ).ask():
200
+ console.print('[yellow]Cancelled[/yellow]')
201
+ raise typer.Exit(0)
202
+
203
+ # Execute publish
204
+ result = publish_plugin(
205
+ path=plugin_path,
206
+ auth=auth,
207
+ console=console,
208
+ config_path=config,
209
+ dry_run=dry_run,
210
+ debug=debug,
211
+ )
212
+
213
+ # Success output
214
+ if not dry_run:
215
+ console.print(
216
+ Panel(
217
+ f'[green]Published successfully![/green]\n\n'
218
+ f'Release ID: [bold]{result.release_id}[/bold]\n'
219
+ f'Version: [bold]{result.version}[/bold]\n'
220
+ f'Checksum: [dim]{result.checksum[:12]}...[/dim]',
221
+ title='Success',
222
+ border_style='green',
223
+ )
224
+ )
225
+
226
+ except typer.BadParameter as e:
227
+ err_console.print(f'[red]Error:[/red] {e.message}')
228
+ raise typer.Exit(1)
229
+ except PluginError as e:
230
+ err_console.print(f'[red]Error:[/red] {e.message}')
231
+ if e.details:
232
+ err_console.print(f'[dim]Details:[/dim] {e.details}')
233
+ raise typer.Exit(1)
234
+ except FileNotFoundError as e:
235
+ err_console.print(f'[red]Error:[/red] {e}')
236
+ raise typer.Exit(1)
237
+ except Exception as e:
238
+ err_console.print(f'[red]Unexpected error:[/red] {e}')
239
+ raise typer.Exit(1)
240
+
241
+
242
+ @plugin_app.command('create')
243
+ def plugin_create(
244
+ path: Annotated[
245
+ Optional[Path],
246
+ typer.Option('--path', '-p', help='Output directory (default: current).'),
247
+ ] = None,
248
+ name: Annotated[
249
+ Optional[str],
250
+ typer.Option('--name', '-n', help='Plugin name.'),
251
+ ] = None,
252
+ code: Annotated[
253
+ Optional[str],
254
+ typer.Option('--code', help='Plugin code (slug).'),
255
+ ] = None,
256
+ category: Annotated[
257
+ Optional[str],
258
+ typer.Option('--category', '-c', help='Plugin category.'),
259
+ ] = None,
260
+ yes: Annotated[
261
+ bool,
262
+ typer.Option('--yes', '-y', help='Skip confirmation.'),
263
+ ] = False,
264
+ ) -> None:
265
+ """Create a new plugin from template.
266
+
267
+ Interactively creates a new plugin with the specified category.
268
+
269
+ [bold]Examples:[/bold]
270
+
271
+ synapse plugin create
272
+ synapse plugin create --name "My Plugin" --category neural_net
273
+ """
274
+ from synapse_sdk.cli.plugin.create import create_plugin_interactive
275
+
276
+ try:
277
+ output_dir = (path or Path.cwd()).resolve()
278
+
279
+ result = create_plugin_interactive(
280
+ output_dir=output_dir,
281
+ name=name,
282
+ code=code,
283
+ category=category,
284
+ console=console,
285
+ yes=yes,
286
+ )
287
+
288
+ if result:
289
+ console.print()
290
+ console.print('[bold green]Next steps:[/bold green]')
291
+ console.print(f' cd {result.plugin_dir.name}')
292
+ console.print(' uv sync')
293
+ console.print(' synapse plugin publish --dry-run')
294
+
295
+ except ValueError as e:
296
+ err_console.print(f'[red]Error:[/red] {e}')
297
+ raise typer.Exit(1)
298
+ except Exception as e:
299
+ err_console.print(f'[red]Unexpected error:[/red] {e}')
300
+ raise typer.Exit(1)
301
+
302
+
303
+ @plugin_app.command('update-config')
304
+ def plugin_update_config(
305
+ path: Annotated[
306
+ Optional[Path],
307
+ typer.Option('--path', '-p', help='Plugin directory (default: current).'),
308
+ ] = None,
309
+ config: Annotated[
310
+ Optional[Path],
311
+ typer.Option('--config', '-c', help='Config file path.'),
312
+ ] = None,
313
+ ) -> None:
314
+ """Auto-discover actions and sync to config.yaml.
315
+
316
+ Scans plugin source files for BaseAction subclasses and updates config.yaml:
317
+ - Discovers new actions from code and adds them to config
318
+ - Syncs entrypoints, input_type, output_type from code
319
+ - Preserves other config fields (description, etc.)
320
+
321
+ This is automatically run during `synapse plugin publish`,
322
+ but can be run manually during development.
323
+
324
+ [bold]Examples:[/bold]
325
+
326
+ synapse plugin update-config
327
+ synapse plugin update-config -p ./my-plugin
328
+ """
329
+ from synapse_sdk.cli.plugin.publish import find_config_file
330
+ from synapse_sdk.plugins.discovery import PluginDiscovery
331
+ from synapse_sdk.plugins.errors import PluginError
332
+
333
+ # Resolve path
334
+ plugin_path = (path or Path.cwd()).resolve()
335
+
336
+ if not plugin_path.exists():
337
+ err_console.print(f'[red]Error:[/red] Path not found: {plugin_path}')
338
+ raise typer.Exit(1)
339
+
340
+ try:
341
+ import sys
342
+
343
+ # Find config file
344
+ config_file = find_config_file(plugin_path, config)
345
+ console.print(f'[dim]Config file:[/dim] {config_file}')
346
+
347
+ # Add plugin path to sys.path so action classes can be imported
348
+ plugin_dir = str(plugin_path)
349
+ if plugin_dir not in sys.path:
350
+ sys.path.insert(0, plugin_dir)
351
+
352
+ # Load discovery
353
+ discovery = PluginDiscovery.from_path(config_file)
354
+
355
+ # Sync types
356
+ changes = discovery.sync_config_file(config_file)
357
+
358
+ if changes:
359
+ console.print('\n[green]Updated config.yaml:[/green]')
360
+ for action_name, type_info in changes.items():
361
+ console.print(f' [cyan]{action_name}[/cyan]: {type_info}')
362
+ else:
363
+ console.print('[yellow]No changes needed.[/yellow]')
364
+ console.print(
365
+ '[dim]Actions have no input_type/output_type declarations, or config is already up to date.[/dim]'
366
+ )
367
+
368
+ except FileNotFoundError as e:
369
+ err_console.print(f'[red]Error:[/red] {e}')
370
+ raise typer.Exit(1)
371
+ except PluginError as e:
372
+ err_console.print(f'[red]Error:[/red] {e.message}')
373
+ raise typer.Exit(1)
374
+ except Exception as e:
375
+ err_console.print(f'[red]Error:[/red] {e}')
376
+ raise typer.Exit(1)
377
+
378
+
379
+ @plugin_app.command('run')
380
+ def plugin_run(
381
+ action: Annotated[
382
+ str,
383
+ typer.Argument(help='Action to run (e.g., test, train, deploy, infer).'),
384
+ ],
385
+ plugin: Annotated[
386
+ Optional[str],
387
+ typer.Option('--plugin', '-p', help='Plugin code. Auto-detects from config.yaml if not provided.'),
388
+ ] = None,
389
+ plugin_path: Annotated[
390
+ Optional[Path],
391
+ typer.Option('--path', help='Plugin directory (default: current).'),
392
+ ] = None,
393
+ params: Annotated[
394
+ Optional[str],
395
+ typer.Option('--params', help='JSON parameters to pass to the action.'),
396
+ ] = None,
397
+ mode: Annotated[
398
+ Optional[str],
399
+ typer.Option('--mode', '-m', help='Executor mode: local, task, job, or remote.'),
400
+ ] = None,
401
+ ray_address: Annotated[
402
+ str,
403
+ typer.Option('--ray-address', help='Ray cluster address (for task/job modes).'),
404
+ ] = 'auto',
405
+ num_gpus: Annotated[
406
+ Optional[int],
407
+ typer.Option('--gpus', help='Number of GPUs to request.'),
408
+ ] = None,
409
+ num_cpus: Annotated[
410
+ Optional[int],
411
+ typer.Option('--cpus', help='Number of CPUs to request.'),
412
+ ] = None,
413
+ input_data: Annotated[
414
+ Optional[str],
415
+ typer.Option('--input', '-i', help='JSON input for inference (for infer action).'),
416
+ ] = None,
417
+ infer_path: Annotated[
418
+ str,
419
+ typer.Option('--infer-path', help='Inference endpoint path (for infer action).'),
420
+ ] = '/',
421
+ host: Annotated[
422
+ Optional[str],
423
+ typer.Option('--host', help='Synapse API host (for remote mode).'),
424
+ ] = None,
425
+ token: Annotated[
426
+ Optional[str],
427
+ typer.Option('--token', '-t', help='Access token (for remote mode).'),
428
+ ] = None,
429
+ debug: Annotated[
430
+ bool,
431
+ typer.Option('--debug/--no-debug', help='Debug mode (default: enabled).'),
432
+ ] = True,
433
+ debug_sdk: Annotated[
434
+ bool,
435
+ typer.Option('--debug-sdk', help='Bundle local SDK with upload (for SDK development).'),
436
+ ] = False,
437
+ ) -> None:
438
+ """Run a plugin action.
439
+
440
+ [bold]Executor Modes:[/bold]
441
+ - local: In-process execution (best for debugging)
442
+ - task: Ray Actor execution (no log streaming)
443
+ - job: Ray Jobs API with log streaming (recommended for remote)
444
+ - remote: Run via Synapse backend API (requires auth)
445
+
446
+ [bold]Examples:[/bold]
447
+
448
+ synapse plugin run test
449
+ synapse plugin run test --mode local
450
+ synapse plugin run test --mode task --gpus 1
451
+ synapse plugin run train --mode job --params '{"epochs": 10}'
452
+ synapse plugin run deploy --mode remote
453
+ """
454
+ import json
455
+
456
+ import questionary
457
+ from rich.panel import Panel
458
+
459
+ from synapse_sdk.plugins.errors import PluginError
460
+
461
+ try:
462
+ path = (plugin_path or Path.cwd()).resolve()
463
+
464
+ if not path.exists():
465
+ err_console.print(f'[red]Error:[/red] Path not found: {path}')
466
+ raise typer.Exit(1)
467
+
468
+ # Parse params JSON
469
+ parsed_params: dict = {}
470
+ if params:
471
+ try:
472
+ parsed_params = json.loads(params)
473
+ except json.JSONDecodeError as e:
474
+ err_console.print(f'[red]Error:[/red] Invalid JSON params: {e}')
475
+ raise typer.Exit(1)
476
+
477
+ # Interactive executor selection if not provided
478
+ if mode is None:
479
+ mode = questionary.select(
480
+ 'Select executor mode:',
481
+ choices=[
482
+ questionary.Choice('local - In-process (best for debugging)', value='local'),
483
+ questionary.Choice('task - Ray Actor (no log streaming)', value='task'),
484
+ questionary.Choice('job - Ray Jobs API (with log streaming)', value='job'),
485
+ questionary.Choice('remote - Synapse Backend API', value='remote'),
486
+ ],
487
+ default='local',
488
+ ).ask()
489
+
490
+ if mode is None:
491
+ console.print('[yellow]Cancelled[/yellow]')
492
+ raise typer.Exit(0)
493
+
494
+ # Validate mode
495
+ if mode not in ('local', 'task', 'job', 'jobs-api', 'remote'):
496
+ err_console.print(f'[red]Error:[/red] Invalid mode: {mode}. Use local, task, job, or remote.')
497
+ raise typer.Exit(1)
498
+
499
+ # jobs-api is an alias for job
500
+ if mode == 'jobs-api':
501
+ mode = 'job'
502
+
503
+ # Handle local/task/job modes
504
+ if mode in ('local', 'task', 'job'):
505
+ from synapse_sdk.cli.plugin.test import test_plugin
506
+
507
+ # For task/job modes, get agent config for Ray address
508
+ resolved_ray_address = ray_address
509
+ if mode in ('task', 'job') and ray_address == 'auto':
510
+ from urllib.parse import urlparse
511
+
512
+ from synapse_sdk.cli.agent.config import get_agent_config
513
+ from synapse_sdk.cli.agent.select import select_agent_interactive
514
+ from synapse_sdk.cli.auth import get_auth_config
515
+
516
+ agent = get_agent_config()
517
+ if not agent:
518
+ console.print('[yellow]No agent configured. Please select one.[/yellow]\n')
519
+ try:
520
+ auth = get_auth_config(host=host, token=token, console=console, interactive=False)
521
+ agent = select_agent_interactive(auth, console)
522
+ if not agent:
523
+ console.print('[yellow]Cancelled[/yellow]')
524
+ raise typer.Exit(0)
525
+ except Exception as e:
526
+ err_console.print(f'[red]Error:[/red] Failed to get agent: {e}')
527
+ err_console.print('[dim]Run `synapse agent select` to configure an agent.[/dim]')
528
+ raise typer.Exit(1)
529
+
530
+ if agent and agent.url:
531
+ # Convert HTTP URL to Ray address (ray://<host>:10001)
532
+ parsed = urlparse(agent.url)
533
+ ray_host = parsed.hostname or 'localhost'
534
+ resolved_ray_address = f'ray://{ray_host}:10001'
535
+ console.print(f'[dim]Using agent:[/dim] {agent.name or agent.id}')
536
+ console.print(f'[dim]Ray address:[/dim] {resolved_ray_address}')
537
+
538
+ result = test_plugin(
539
+ action=action,
540
+ console=console,
541
+ path=path,
542
+ params=parsed_params if parsed_params else None,
543
+ mode=mode,
544
+ ray_address=resolved_ray_address,
545
+ num_gpus=num_gpus,
546
+ num_cpus=num_cpus,
547
+ include_sdk=debug_sdk,
548
+ )
549
+
550
+ # Success output
551
+ console.print(
552
+ Panel(
553
+ f'[green]Action completed![/green]\n\n'
554
+ f'Plugin: [bold]{result.plugin}[/bold]\n'
555
+ f'Action: [bold]{result.action}[/bold]\n'
556
+ f'Mode: [bold]{result.mode}[/bold]',
557
+ title='Success',
558
+ border_style='green',
559
+ )
560
+ )
561
+
562
+ if result.result:
563
+ console.print('\n[bold]Result:[/bold]')
564
+ if isinstance(result.result, dict):
565
+ for key, value in result.result.items():
566
+ console.print(f' {key}: {value}')
567
+ else:
568
+ console.print(f' {result.result}')
569
+
570
+ else: # mode == 'remote'
571
+ from synapse_sdk.cli.agent.config import get_agent_config
572
+ from synapse_sdk.cli.auth import get_auth_config
573
+ from synapse_sdk.cli.plugin.run import resolve_plugin_code, run_plugin
574
+
575
+ # Get auth configuration
576
+ auth = get_auth_config(host=host, token=token, console=console, interactive=False)
577
+
578
+ # Get agent configuration
579
+ agent = get_agent_config()
580
+ if not agent:
581
+ err_console.print('[red]Error:[/red] No agent configured.')
582
+ err_console.print('[dim]Run `synapse agent select` to configure an agent.[/dim]')
583
+ raise typer.Exit(1)
584
+
585
+ # Resolve plugin code
586
+ plugin_code = resolve_plugin_code(plugin, path)
587
+
588
+ # Map short action names to API action names
589
+ action_map = {'infer': 'inference', 'deploy': 'deployment'}
590
+ api_action = action_map.get(action, action)
591
+
592
+ # Handle infer action
593
+ if action == 'infer' and input_data:
594
+ try:
595
+ parsed_input = json.loads(input_data)
596
+ parsed_params['input'] = parsed_input
597
+ parsed_params['path'] = infer_path
598
+ except json.JSONDecodeError as e:
599
+ err_console.print(f'[red]Error:[/red] Invalid JSON input: {e}')
600
+ raise typer.Exit(1)
601
+
602
+ # Execute run
603
+ result = run_plugin(
604
+ action=api_action,
605
+ auth=auth,
606
+ agent=agent,
607
+ console=console,
608
+ plugin=plugin_code,
609
+ params=parsed_params if parsed_params else None,
610
+ debug=debug,
611
+ )
612
+
613
+ # Success output
614
+ console.print(
615
+ Panel(
616
+ f'[green]Action completed![/green]\n\n'
617
+ f'Plugin: [bold]{result.plugin}[/bold]\n'
618
+ f'Action: [bold]{result.action}[/bold]',
619
+ title='Success',
620
+ border_style='green',
621
+ )
622
+ )
623
+
624
+ if result.result:
625
+ console.print(f'[dim]Result:[/dim] {result.result}')
626
+
627
+ except PluginError as e:
628
+ err_console.print(f'[red]Error:[/red] {e.message}')
629
+ if e.details:
630
+ err_console.print(f'[dim]Details:[/dim] {e.details}')
631
+ raise typer.Exit(1)
632
+ except FileNotFoundError as e:
633
+ err_console.print(f'[red]Error:[/red] {e}')
634
+ raise typer.Exit(1)
635
+ except Exception as e:
636
+ err_console.print(f'[red]Unexpected error:[/red] {e}')
637
+ raise typer.Exit(1)
638
+
639
+
640
+ @agent_app.command('select')
641
+ def agent_select(
642
+ host: Annotated[
643
+ Optional[str],
644
+ typer.Option('--host', help='Synapse API host.'),
645
+ ] = None,
646
+ token: Annotated[
647
+ Optional[str],
648
+ typer.Option('--token', '-t', help='Access token.'),
649
+ ] = None,
650
+ ) -> None:
651
+ """Interactively select an agent.
652
+
653
+ Fetches available agents from the backend and prompts for selection.
654
+
655
+ [bold]Examples:[/bold]
656
+
657
+ synapse agent select
658
+ """
659
+ from synapse_sdk.cli.agent.select import select_agent_interactive
660
+ from synapse_sdk.cli.auth import get_auth_config
661
+
662
+ try:
663
+ auth = get_auth_config(host=host, token=token, console=console, interactive=False)
664
+ result = select_agent_interactive(auth, console)
665
+
666
+ if not result:
667
+ console.print('[yellow]Cancelled[/yellow]')
668
+ raise typer.Exit(0)
669
+
670
+ except typer.BadParameter as e:
671
+ err_console.print(f'[red]Error:[/red] {e.message}')
672
+ raise typer.Exit(1)
673
+ except Exception as e:
674
+ err_console.print(f'[red]Error:[/red] {e}')
675
+ raise typer.Exit(1)
676
+
677
+
678
+ @agent_app.command('show')
679
+ def agent_show() -> None:
680
+ """Show current agent configuration.
681
+
682
+ [bold]Examples:[/bold]
683
+
684
+ synapse agent show
685
+ """
686
+ from rich.table import Table
687
+
688
+ from synapse_sdk.cli.agent.config import get_agent_config
689
+
690
+ agent = get_agent_config()
691
+
692
+ if not agent:
693
+ console.print('[yellow]No agent configured.[/yellow]')
694
+ console.print('[dim]Use `synapse agent select` or `synapse agent set` to configure.[/dim]')
695
+ raise typer.Exit(0)
696
+
697
+ table = Table(title='Current Agent', show_header=False, box=None, padding=(0, 2))
698
+ table.add_column('Key', style='dim')
699
+ table.add_column('Value')
700
+ table.add_row('ID', str(agent.id))
701
+ table.add_row('Name', agent.name or '-')
702
+ table.add_row('URL', agent.url or '-')
703
+ table.add_row('Token', (agent.token[:12] + '...') if agent.token else '-')
704
+
705
+ console.print(table)
706
+
707
+
708
+ @agent_app.command('clear')
709
+ def agent_clear(
710
+ yes: Annotated[
711
+ bool,
712
+ typer.Option('--yes', '-y', help='Skip confirmation.'),
713
+ ] = False,
714
+ ) -> None:
715
+ """Clear agent configuration.
716
+
717
+ [bold]Examples:[/bold]
718
+
719
+ synapse agent clear
720
+ synapse agent clear -y
721
+ """
722
+ import questionary
723
+
724
+ from synapse_sdk.cli.agent.config import clear_agent_config, get_agent_config
725
+
726
+ agent = get_agent_config()
727
+
728
+ if not agent:
729
+ console.print('[yellow]No agent configured.[/yellow]')
730
+ raise typer.Exit(0)
731
+
732
+ if not yes:
733
+ confirmed = questionary.confirm(
734
+ f'Clear agent configuration for "{agent.name or agent.id}"?',
735
+ default=False,
736
+ ).ask()
737
+
738
+ if not confirmed:
739
+ console.print('[yellow]Cancelled[/yellow]')
740
+ raise typer.Exit(0)
741
+
742
+ clear_agent_config()
743
+ console.print('[green]Agent configuration cleared.[/green]')
744
+
745
+
746
+ @mcp_app.command('serve')
747
+ def mcp_serve(
748
+ config: Annotated[
749
+ Optional[Path],
750
+ typer.Option('--config', '-c', help='Path to config file (default: ~/.synapse/config.json).'),
751
+ ] = None,
752
+ ) -> None:
753
+ """Start the MCP server for AI assistant integration.
754
+
755
+ Runs the Synapse MCP server which provides tools for:
756
+ - Managing environments (prod, test, demo, local)
757
+ - Listing and running plugins
758
+ - Viewing job logs and status
759
+ - Managing Ray Serve deployments
760
+
761
+ [bold]Configuration:[/bold]
762
+
763
+ Run `synapse mcp init` to create ~/.synapse/config.json
764
+ (or run `synapse login` first)
765
+
766
+ default_environment: prod
767
+
768
+ environments:
769
+ prod:
770
+ backend_url: https://api.synapse.sh
771
+ access_token: your-token
772
+ # Agent is set via list_agents() + select_agent() tools
773
+
774
+ [bold]Cursor Setup:[/bold]
775
+
776
+ Add to ~/.cursor/mcp.json:
777
+
778
+ {
779
+ "mcpServers": {
780
+ "synapse": {
781
+ "command": "uvx",
782
+ "args": ["--from", "synapse-sdk[mcp]", "synapse", "mcp", "serve"]
783
+ }
784
+ }
785
+ }
786
+
787
+ [bold]Claude Code Setup:[/bold]
788
+
789
+ claude mcp add synapse -- uvx --from 'synapse-sdk[mcp]' synapse mcp serve
790
+
791
+ [bold]Examples:[/bold]
792
+
793
+ synapse mcp serve
794
+ synapse mcp serve --config ~/my-config.json
795
+ """
796
+ try:
797
+ from synapse_sdk.mcp import serve
798
+ from synapse_sdk.mcp.config import ConfigManager
799
+
800
+ # Initialize config manager with custom path if provided
801
+ if config:
802
+ from synapse_sdk.mcp.config import reset_config_manager
803
+
804
+ reset_config_manager()
805
+ # Import and set up with custom path
806
+ import synapse_sdk.mcp.config as config_module
807
+
808
+ config_module._config_manager = ConfigManager(config_path=config)
809
+
810
+ serve()
811
+
812
+ except ImportError:
813
+ err_console.print('[red]Error:[/red] MCP dependencies not installed.')
814
+ err_console.print('[dim]Install with: pip install synapse-sdk[mcp][/dim]')
815
+ raise typer.Exit(1)
816
+ except Exception as e:
817
+ err_console.print(f'[red]Error:[/red] {e}')
818
+ raise typer.Exit(1)
819
+
820
+
821
+ @mcp_app.command('init')
822
+ def mcp_init(
823
+ config: Annotated[
824
+ Optional[Path],
825
+ typer.Option('--config', '-c', help='Path to config file (default: ~/.synapse/config.json).'),
826
+ ] = None,
827
+ force: Annotated[
828
+ bool,
829
+ typer.Option('--force', '-f', help='Overwrite existing config file.'),
830
+ ] = False,
831
+ ) -> None:
832
+ """Initialize MCP configuration file with example environments.
833
+
834
+ [bold]Examples:[/bold]
835
+
836
+ synapse mcp init
837
+ synapse mcp init --force
838
+ """
839
+ import json
840
+
841
+ import questionary
842
+
843
+ from synapse_sdk.cli.auth import CONFIG_FILE, load_credentials_file
844
+
845
+ config_path = config or (Path.home() / '.synapse' / 'config.json')
846
+
847
+ if config_path.exists() and not force:
848
+ console.print(f'[yellow]Config file already exists:[/yellow] {config_path}')
849
+ console.print('[dim]Use --force to overwrite.[/dim]')
850
+ raise typer.Exit(0)
851
+
852
+ # Check for existing credentials in config.json
853
+ backend_url = 'https://api.synapse.example.com'
854
+ access_token = 'your-access-token'
855
+ use_existing = False
856
+
857
+ if CONFIG_FILE.exists():
858
+ creds = load_credentials_file()
859
+ existing_host = creds.get('SYNAPSE_HOST')
860
+ existing_token = creds.get('SYNAPSE_ACCESS_TOKEN')
861
+
862
+ if existing_host or existing_token:
863
+ console.print(f'[dim]Found existing credentials at {CONFIG_FILE}[/dim]')
864
+ if existing_host:
865
+ console.print(f' Host: {existing_host}')
866
+ if existing_token:
867
+ console.print(f' Token: {existing_token[:12]}...')
868
+
869
+ use_existing = questionary.confirm(
870
+ 'Use existing credentials for MCP?',
871
+ default=True,
872
+ ).ask()
873
+
874
+ if use_existing:
875
+ if existing_host:
876
+ backend_url = existing_host
877
+ if existing_token:
878
+ access_token = existing_token
879
+
880
+ # Create config
881
+ example_config = {
882
+ 'host': backend_url,
883
+ 'access_token': access_token,
884
+ 'default_environment': 'default',
885
+ 'environments': {
886
+ 'default': {
887
+ 'backend_url': backend_url,
888
+ 'access_token': access_token,
889
+ },
890
+ },
891
+ }
892
+
893
+ # Ensure directory exists
894
+ config_path.parent.mkdir(parents=True, exist_ok=True)
895
+
896
+ # Write config
897
+ config_path.write_text(json.dumps(example_config, indent=2))
898
+ config_path.chmod(0o600)
899
+
900
+ console.print(f'[green]Created config file:[/green] {config_path}')
901
+ console.print()
902
+
903
+ if use_existing:
904
+ console.print('[bold]Next steps:[/bold]')
905
+ console.print(' 1. Run: synapse mcp serve')
906
+ console.print(' 2. Use list_agents() and select_agent() to configure agent')
907
+ else:
908
+ console.print('[bold]Next steps:[/bold]')
909
+ console.print(f' 1. Edit {config_path} with your credentials')
910
+ console.print(' 2. Run: synapse mcp serve')
911
+
912
+ console.print()
913
+ console.print('[bold]Cursor setup:[/bold]')
914
+ console.print(' Add to ~/.cursor/mcp.json:')
915
+ console.print()
916
+ console.print(' {')
917
+ console.print(' "mcpServers": {')
918
+ console.print(' "synapse": {')
919
+ console.print(' "command": "uvx",')
920
+ console.print(' "args": ["--from", "synapse-sdk[mcp]", "synapse", "mcp", "serve"]')
921
+ console.print(' }')
922
+ console.print(' }')
923
+ console.print(' }')
924
+ console.print()
925
+ console.print('[bold]Claude Code setup:[/bold]')
926
+ console.print(" claude mcp add synapse -- uvx --from 'synapse-sdk[mcp]' synapse mcp serve")
927
+ console.print()
928
+ console.print('[dim]For local development:[/dim]')
929
+ console.print(' claude mcp add synapse -- uv run --directory <path-to-synapse-sdk> synapse mcp serve')
930
+
931
+
932
+ @cli.command('login')
933
+ def login(
934
+ host: Annotated[
935
+ Optional[str],
936
+ typer.Option('--host', help='Synapse API host.'),
937
+ ] = None,
938
+ token: Annotated[
939
+ Optional[str],
940
+ typer.Option('--token', '-t', help='Access token (will prompt if not provided).'),
941
+ ] = None,
942
+ ) -> None:
943
+ """Authenticate with Synapse.
944
+
945
+ Saves credentials to ~/.synapse/config.json for future use.
946
+
947
+ [bold]Examples:[/bold]
948
+
949
+ synapse login
950
+ synapse login --token YOUR_TOKEN
951
+ """
952
+ import json
953
+
954
+ import questionary
955
+
956
+ from synapse_sdk.cli.auth import DEFAULT_HOST
957
+
958
+ # Prompt for host if not provided
959
+ if not host:
960
+ host = questionary.text(
961
+ 'Synapse API host:',
962
+ default=DEFAULT_HOST,
963
+ ).ask()
964
+
965
+ if not host:
966
+ console.print('[yellow]Cancelled[/yellow]')
967
+ raise typer.Exit(0)
968
+
969
+ # Prompt for token if not provided
970
+ if not token:
971
+ token = questionary.text(
972
+ 'Enter your Synapse access token:',
973
+ validate=lambda x: len(x) > 0 or 'Token cannot be empty',
974
+ ).ask()
975
+
976
+ if not token:
977
+ console.print('[yellow]Cancelled[/yellow]')
978
+ raise typer.Exit(0)
979
+
980
+ # Save to config.json
981
+ config_dir = Path.home() / '.synapse'
982
+ config_dir.mkdir(exist_ok=True)
983
+
984
+ config_file = config_dir / 'config.json'
985
+ config = {}
986
+ if config_file.exists():
987
+ try:
988
+ config = json.loads(config_file.read_text())
989
+ except json.JSONDecodeError:
990
+ pass
991
+ config['host'] = host
992
+ config['access_token'] = token
993
+ config_file.write_text(json.dumps(config, indent=2))
994
+ config_file.chmod(0o600)
995
+
996
+ console.print('[green]Logged in successfully![/green]')
997
+ console.print(f'[dim]Credentials saved to {config_file}[/dim]')
998
+
999
+
1000
+ def version_callback(value: bool) -> None:
1001
+ """Show version and exit."""
1002
+ if value:
1003
+ from importlib.metadata import version
1004
+
1005
+ try:
1006
+ ver = version('synapse-sdk')
1007
+ except Exception:
1008
+ ver = 'unknown'
1009
+ console.print(f'synapse-sdk [bold cyan]{ver}[/bold cyan]')
1010
+ raise typer.Exit()
1011
+
1012
+
1013
+ @cli.callback()
1014
+ def main(
1015
+ version: Annotated[
1016
+ Optional[bool],
1017
+ typer.Option('--version', '-v', callback=version_callback, is_eager=True, help='Show version and exit.'),
1018
+ ] = None,
1019
+ ) -> None:
1020
+ """Synapse SDK CLI."""
1021
+ pass
1022
+
1023
+
1024
+ if __name__ == '__main__':
1025
+ cli()