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,1352 @@
1
+ """MCP Server for Synapse SDK.
2
+
3
+ This module provides the FastMCP server instance and registers all tools,
4
+ resources, and prompts for interacting with Synapse infrastructure.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+ from typing import TYPE_CHECKING
11
+
12
+ try:
13
+ from mcp.server.fastmcp import FastMCP
14
+ except ImportError:
15
+ raise ImportError('MCP dependencies not installed. Install with: pip install synapse-sdk[mcp]')
16
+
17
+ from synapse_sdk.mcp.config import get_config_manager
18
+
19
+ if TYPE_CHECKING:
20
+ from synapse_sdk.mcp.config import ConfigManager
21
+
22
+ # Create the MCP server instance
23
+ mcp = FastMCP(
24
+ name='synapse',
25
+ instructions='Synapse SDK MCP Server - Manage plugins, executions, and deployments',
26
+ )
27
+
28
+
29
+ def _log(message: str) -> None:
30
+ """Log to stderr (safe for MCP STDIO transport)."""
31
+ print(f'[synapse-mcp] {message}', file=sys.stderr)
32
+
33
+
34
+ def _get_config() -> ConfigManager:
35
+ """Get the config manager instance."""
36
+ return get_config_manager()
37
+
38
+
39
+ # =============================================================================
40
+ # Environment Tools
41
+ # =============================================================================
42
+
43
+
44
+ @mcp.tool()
45
+ def switch_environment(name: str) -> dict:
46
+ """Switch to a different environment (e.g., prod, test, demo, local).
47
+
48
+ Args:
49
+ name: The environment name to switch to
50
+
51
+ Returns:
52
+ Status of the switch operation with environment details
53
+ """
54
+ config = _get_config()
55
+
56
+ if name not in config.list_environments():
57
+ return {
58
+ 'success': False,
59
+ 'error': f'Environment "{name}" not found',
60
+ 'available_environments': config.list_environments(),
61
+ }
62
+
63
+ config.set_active_environment(name)
64
+ env = config.get_active_environment()
65
+
66
+ return {
67
+ 'success': True,
68
+ 'message': f'Switched to environment: {name}',
69
+ 'environment': env.to_dict() if env else None,
70
+ }
71
+
72
+
73
+ @mcp.tool()
74
+ def list_environments() -> dict:
75
+ """List all configured environments.
76
+
77
+ Returns:
78
+ List of environment names and their configuration status
79
+ """
80
+ config = _get_config()
81
+ environments = []
82
+
83
+ for name in config.list_environments():
84
+ env = config.get_environment(name)
85
+ if env:
86
+ environments.append(env.to_dict())
87
+
88
+ return {
89
+ 'environments': environments,
90
+ 'active_environment': config.get_active_environment_name(),
91
+ 'config_path': str(config.config_path),
92
+ }
93
+
94
+
95
+ @mcp.tool()
96
+ def get_current_environment() -> dict:
97
+ """Get the currently active environment and its connection status.
98
+
99
+ Returns:
100
+ Current environment details including backend/agent availability
101
+ """
102
+ config = _get_config()
103
+ env = config.get_active_environment()
104
+
105
+ if not env:
106
+ return {
107
+ 'active': False,
108
+ 'message': 'No environment is currently active. Use switch_environment() to select one.',
109
+ 'available_environments': config.list_environments(),
110
+ }
111
+
112
+ # Test connections
113
+ backend_status = 'not_configured'
114
+ agent_status = 'not_configured'
115
+
116
+ if env.has_backend():
117
+ try:
118
+ client = config.get_backend_client()
119
+ if client:
120
+ backend_status = 'connected'
121
+ except Exception as e:
122
+ backend_status = f'error: {e}'
123
+
124
+ if env.has_agent():
125
+ try:
126
+ client = config.get_agent_client()
127
+ if client:
128
+ agent_status = 'connected'
129
+ except Exception as e:
130
+ agent_status = f'error: {e}'
131
+
132
+ return {
133
+ 'active': True,
134
+ 'environment': env.to_dict(),
135
+ 'backend_status': backend_status,
136
+ 'agent_status': agent_status,
137
+ }
138
+
139
+
140
+ @mcp.tool()
141
+ def add_environment(
142
+ name: str,
143
+ backend_url: str | None = None,
144
+ access_token: str | None = None,
145
+ tenant: str | None = None,
146
+ set_as_default: bool = False,
147
+ ) -> dict:
148
+ """Add or update an environment configuration.
149
+
150
+ After adding, use list_agents() to see available agents,
151
+ then select_agent() to configure the agent for this environment.
152
+
153
+ Args:
154
+ name: Environment name (e.g., 'prod', 'test', 'demo')
155
+ backend_url: Synapse backend API URL
156
+ access_token: Your API access token
157
+ tenant: Tenant identifier
158
+ set_as_default: Whether to set this as the default environment
159
+
160
+ Returns:
161
+ The created/updated environment configuration
162
+ """
163
+ config = _get_config()
164
+
165
+ env = config.add_environment(
166
+ name=name,
167
+ backend_url=backend_url,
168
+ access_token=access_token,
169
+ tenant=tenant,
170
+ set_as_default=set_as_default,
171
+ )
172
+
173
+ return {
174
+ 'success': True,
175
+ 'message': f'Environment "{name}" added/updated. Use list_agents() and select_agent() to configure an agent.',
176
+ 'environment': env.to_dict(),
177
+ }
178
+
179
+
180
+ @mcp.tool()
181
+ def list_agents() -> dict:
182
+ """List available agents from the backend.
183
+
184
+ Fetches agents from the current environment's backend.
185
+ Use select_agent() to choose one for execution.
186
+
187
+ Returns:
188
+ List of available agents with their IDs, names, URLs, and status
189
+ """
190
+ config = _get_config()
191
+ client = config.get_backend_client()
192
+
193
+ if not client:
194
+ return {
195
+ 'success': False,
196
+ 'error': 'No backend configured. Use add_environment() first with backend_url and access_token.',
197
+ }
198
+
199
+ try:
200
+ agents = client.list_agents()
201
+ agent_list = []
202
+ for agent in agents:
203
+ agent_list.append({
204
+ 'id': agent.id,
205
+ 'name': agent.name,
206
+ 'url': agent.url,
207
+ 'status': agent.status,
208
+ 'is_connected': agent.is_connected,
209
+ })
210
+
211
+ env = config.get_active_environment()
212
+ return {
213
+ 'success': True,
214
+ 'agents': agent_list,
215
+ 'current_agent_id': env.agent_id if env else None,
216
+ 'hint': 'Use select_agent(agent_id) to select an agent for execution.',
217
+ }
218
+ except Exception as e:
219
+ return {'success': False, 'error': str(e)}
220
+
221
+
222
+ @mcp.tool()
223
+ def select_agent(agent_id: int) -> dict:
224
+ """Select an agent for the current environment.
225
+
226
+ Fetches the agent details from the backend and saves
227
+ the agent URL and token for the current environment.
228
+
229
+ Args:
230
+ agent_id: The agent ID from list_agents()
231
+
232
+ Returns:
233
+ The selected agent configuration
234
+ """
235
+ config = _get_config()
236
+ client = config.get_backend_client()
237
+
238
+ if not client:
239
+ return {
240
+ 'success': False,
241
+ 'error': 'No backend configured. Use add_environment() first.',
242
+ }
243
+
244
+ try:
245
+ agents = client.list_agents()
246
+ selected = None
247
+ for agent in agents:
248
+ if agent.id == agent_id:
249
+ selected = agent
250
+ break
251
+
252
+ if not selected:
253
+ return {
254
+ 'success': False,
255
+ 'error': f'Agent {agent_id} not found.',
256
+ 'available_ids': [a.id for a in agents],
257
+ }
258
+
259
+ # Save to config
260
+ config.set_agent(
261
+ agent_id=selected.id,
262
+ agent_name=selected.name,
263
+ agent_url=selected.url,
264
+ agent_token=selected.token,
265
+ )
266
+
267
+ return {
268
+ 'success': True,
269
+ 'message': f'Agent "{selected.name}" selected for current environment.',
270
+ 'agent': {
271
+ 'id': selected.id,
272
+ 'name': selected.name,
273
+ 'url': selected.url,
274
+ 'status': selected.status,
275
+ },
276
+ }
277
+ except Exception as e:
278
+ return {'success': False, 'error': str(e)}
279
+
280
+
281
+ @mcp.tool()
282
+ def clear_agent() -> dict:
283
+ """Clear the agent configuration for the current environment.
284
+
285
+ Returns:
286
+ Status of the operation
287
+ """
288
+ config = _get_config()
289
+
290
+ if config.clear_agent():
291
+ return {'success': True, 'message': 'Agent cleared for current environment.'}
292
+ else:
293
+ return {'success': False, 'error': 'No active environment.'}
294
+
295
+
296
+ # =============================================================================
297
+ # Plugin Tools
298
+ # =============================================================================
299
+
300
+
301
+ @mcp.tool()
302
+ def list_plugin_releases(limit: int = 20, offset: int = 0) -> dict:
303
+ """List published plugin releases from the current environment.
304
+
305
+ Args:
306
+ limit: Maximum number of results to return
307
+ offset: Number of results to skip
308
+
309
+ Returns:
310
+ List of plugin releases
311
+ """
312
+ config = _get_config()
313
+ client = config.get_agent_client()
314
+
315
+ if not client:
316
+ return {
317
+ 'success': False,
318
+ 'error': 'No agent configured for current environment. Use switch_environment() first.',
319
+ }
320
+
321
+ try:
322
+ releases, total = client.list_plugin_releases(
323
+ params={'limit': limit, 'offset': offset},
324
+ list_all=False,
325
+ )
326
+ return {
327
+ 'success': True,
328
+ 'releases': releases,
329
+ 'total': total,
330
+ 'limit': limit,
331
+ 'offset': offset,
332
+ }
333
+ except Exception as e:
334
+ return {'success': False, 'error': str(e)}
335
+
336
+
337
+ @mcp.tool()
338
+ def get_plugin_release(lookup: str) -> dict:
339
+ """Get details of a specific plugin release.
340
+
341
+ Args:
342
+ lookup: Plugin release ID or 'code:version' string
343
+
344
+ Returns:
345
+ Plugin release details
346
+ """
347
+ config = _get_config()
348
+ client = config.get_agent_client()
349
+
350
+ if not client:
351
+ return {
352
+ 'success': False,
353
+ 'error': 'No agent configured for current environment.',
354
+ }
355
+
356
+ try:
357
+ release = client.get_plugin_release(lookup)
358
+ return {'success': True, 'release': release}
359
+ except Exception as e:
360
+ return {'success': False, 'error': str(e)}
361
+
362
+
363
+ @mcp.tool()
364
+ def discover_local_plugin(path: str) -> dict:
365
+ """Discover actions in a local plugin directory.
366
+
367
+ Args:
368
+ path: Path to the plugin directory
369
+
370
+ Returns:
371
+ Plugin configuration and list of available actions
372
+ """
373
+ from pathlib import Path
374
+
375
+ from synapse_sdk.plugins.discovery import PluginDiscovery
376
+
377
+ plugin_path = Path(path).expanduser().resolve()
378
+
379
+ if not plugin_path.exists():
380
+ return {'success': False, 'error': f'Path not found: {plugin_path}'}
381
+
382
+ try:
383
+ discovery = PluginDiscovery.from_path(plugin_path)
384
+ actions = discovery.list_actions()
385
+
386
+ action_details = []
387
+ for action_name in actions:
388
+ try:
389
+ action_config = discovery.get_action_config(action_name)
390
+ action_details.append({
391
+ 'name': action_name,
392
+ 'description': action_config.description,
393
+ 'method': action_config.method.value if action_config.method else None,
394
+ })
395
+ except Exception:
396
+ action_details.append({'name': action_name})
397
+
398
+ return {
399
+ 'success': True,
400
+ 'path': str(plugin_path),
401
+ 'plugin_config': discovery.to_config_dict(include_ui_schemas=False),
402
+ 'actions': action_details,
403
+ }
404
+ except Exception as e:
405
+ return {'success': False, 'error': str(e)}
406
+
407
+
408
+ @mcp.tool()
409
+ def get_action_config(path: str, action: str) -> dict:
410
+ """Get detailed configuration and parameter schema for a plugin action.
411
+
412
+ Args:
413
+ path: Path to the plugin directory
414
+ action: Name of the action
415
+
416
+ Returns:
417
+ Action configuration including parameter schema
418
+ """
419
+ from pathlib import Path
420
+
421
+ from synapse_sdk.plugins.discovery import PluginDiscovery
422
+
423
+ plugin_path = Path(path).expanduser().resolve()
424
+
425
+ if not plugin_path.exists():
426
+ return {'success': False, 'error': f'Path not found: {plugin_path}'}
427
+
428
+ try:
429
+ discovery = PluginDiscovery.from_path(plugin_path)
430
+
431
+ if not discovery.has_action(action):
432
+ return {
433
+ 'success': False,
434
+ 'error': f'Action "{action}" not found',
435
+ 'available_actions': discovery.list_actions(),
436
+ }
437
+
438
+ action_config = discovery.get_action_config(action)
439
+ params_model = discovery.get_action_params_model(action)
440
+ result_model = discovery.get_action_result_model(action)
441
+
442
+ params_schema = None
443
+ if params_model:
444
+ params_schema = params_model.model_json_schema()
445
+
446
+ result_schema = None
447
+ if result_model:
448
+ result_schema = result_model.model_json_schema()
449
+
450
+ return {
451
+ 'success': True,
452
+ 'action': action,
453
+ 'config': {
454
+ 'name': action_config.name,
455
+ 'description': action_config.description,
456
+ 'entrypoint': action_config.entrypoint,
457
+ 'method': action_config.method.value if action_config.method else None,
458
+ },
459
+ 'params_schema': params_schema,
460
+ 'result_schema': result_schema,
461
+ }
462
+ except Exception as e:
463
+ return {'success': False, 'error': str(e)}
464
+
465
+
466
+ @mcp.tool()
467
+ def validate_plugin_config(path: str) -> dict:
468
+ """Validate a plugin's config.yaml file.
469
+
470
+ Args:
471
+ path: Path to the plugin directory
472
+
473
+ Returns:
474
+ Validation result with any errors or warnings
475
+ """
476
+ from pathlib import Path
477
+
478
+ import yaml
479
+
480
+ from synapse_sdk.plugins.discovery import PluginDiscovery
481
+
482
+ plugin_path = Path(path).expanduser().resolve()
483
+ config_file = plugin_path / 'config.yaml'
484
+
485
+ if not plugin_path.exists():
486
+ return {'success': False, 'error': f'Path not found: {plugin_path}'}
487
+
488
+ if not config_file.exists():
489
+ return {'success': False, 'error': f'config.yaml not found at: {config_file}'}
490
+
491
+ errors = []
492
+ warnings = []
493
+
494
+ # Check YAML syntax
495
+ try:
496
+ with open(config_file) as f:
497
+ raw_config = yaml.safe_load(f)
498
+ except yaml.YAMLError as e:
499
+ return {'success': False, 'valid': False, 'errors': [f'YAML syntax error: {e}']}
500
+
501
+ # Check required fields
502
+ if not raw_config:
503
+ errors.append('config.yaml is empty')
504
+ else:
505
+ if 'code' not in raw_config:
506
+ errors.append("Missing required field: 'code'")
507
+ if 'name' not in raw_config:
508
+ warnings.append("Missing recommended field: 'name'")
509
+ if 'version' not in raw_config:
510
+ warnings.append("Missing recommended field: 'version'")
511
+ if 'actions' not in raw_config or not raw_config.get('actions'):
512
+ errors.append('No actions defined')
513
+
514
+ # Try to load as a full plugin
515
+ try:
516
+ discovery = PluginDiscovery.from_path(plugin_path)
517
+ actions = discovery.list_actions()
518
+
519
+ # Validate each action
520
+ for action_name in actions:
521
+ try:
522
+ discovery.get_action_config(action_name)
523
+ except Exception as e:
524
+ errors.append(f"Action '{action_name}' config error: {e}")
525
+
526
+ try:
527
+ discovery.get_action_params_model(action_name)
528
+ except Exception as e:
529
+ warnings.append(f"Action '{action_name}' params model warning: {e}")
530
+
531
+ except Exception as e:
532
+ errors.append(f'Plugin discovery failed: {e}')
533
+
534
+ return {
535
+ 'success': True,
536
+ 'valid': len(errors) == 0,
537
+ 'path': str(plugin_path),
538
+ 'errors': errors,
539
+ 'warnings': warnings,
540
+ 'config': raw_config,
541
+ }
542
+
543
+
544
+ @mcp.tool()
545
+ def publish_plugin(path: str, version: str | None = None) -> dict:
546
+ """Publish a local plugin to the registry.
547
+
548
+ Args:
549
+ path: Path to the plugin directory
550
+ version: Version string (overrides config.yaml version if provided)
551
+
552
+ Returns:
553
+ Published plugin release details
554
+ """
555
+ from pathlib import Path
556
+
557
+ from synapse_sdk.plugins.discovery import PluginDiscovery
558
+
559
+ config = _get_config()
560
+ client = config.get_agent_client()
561
+
562
+ if not client:
563
+ return {
564
+ 'success': False,
565
+ 'error': 'No agent configured for current environment.',
566
+ }
567
+
568
+ plugin_path = Path(path).expanduser().resolve()
569
+ if not plugin_path.exists():
570
+ return {'success': False, 'error': f'Path not found: {plugin_path}'}
571
+
572
+ # Validate first
573
+ validation = validate_plugin_config(str(plugin_path))
574
+ if not validation.get('valid', False):
575
+ return {
576
+ 'success': False,
577
+ 'error': 'Plugin validation failed',
578
+ 'validation_errors': validation.get('errors', []),
579
+ }
580
+
581
+ try:
582
+ discovery = PluginDiscovery.from_path(plugin_path)
583
+ plugin_config = discovery.config
584
+
585
+ # Use provided version or fall back to config
586
+ publish_version = version or plugin_config.version
587
+ if not publish_version:
588
+ return {
589
+ 'success': False,
590
+ 'error': 'No version specified. Provide version parameter or set in config.yaml',
591
+ }
592
+
593
+ result = client.publish_plugin_release(
594
+ plugin_path=str(plugin_path),
595
+ version=publish_version,
596
+ )
597
+
598
+ return {
599
+ 'success': True,
600
+ 'message': f'Plugin "{plugin_config.code}" version {publish_version} published',
601
+ 'release': result,
602
+ }
603
+ except Exception as e:
604
+ return {'success': False, 'error': str(e)}
605
+
606
+
607
+ # =============================================================================
608
+ # Execution Tools
609
+ # =============================================================================
610
+
611
+
612
+ @mcp.tool()
613
+ def run_plugin(plugin: str, action: str, params: dict | None = None) -> dict:
614
+ """Run a published plugin action via the agent.
615
+
616
+ Args:
617
+ plugin: Plugin code or 'code:version' string
618
+ action: Action name to execute
619
+ params: Parameters to pass to the action
620
+
621
+ Returns:
622
+ Execution result or job ID for async execution
623
+ """
624
+ config = _get_config()
625
+ client = config.get_agent_client()
626
+
627
+ if not client:
628
+ return {
629
+ 'success': False,
630
+ 'error': 'No agent configured for current environment.',
631
+ }
632
+
633
+ try:
634
+ result = client.run_plugin_release(
635
+ lookup=plugin,
636
+ action=action,
637
+ params=params or {},
638
+ )
639
+ return {'success': True, 'result': result}
640
+ except Exception as e:
641
+ return {'success': False, 'error': str(e)}
642
+
643
+
644
+ @mcp.tool()
645
+ def run_debug_plugin(
646
+ path: str,
647
+ action: str,
648
+ params: dict | None = None,
649
+ ) -> dict:
650
+ """Run a local plugin for debugging via the agent.
651
+
652
+ This uploads the plugin code temporarily and runs it on the agent.
653
+
654
+ Args:
655
+ path: Path to the local plugin directory
656
+ action: Action name to execute
657
+ params: Parameters to pass to the action
658
+
659
+ Returns:
660
+ Execution result
661
+ """
662
+ from pathlib import Path
663
+
664
+ config = _get_config()
665
+ client = config.get_agent_client()
666
+
667
+ if not client:
668
+ return {
669
+ 'success': False,
670
+ 'error': 'No agent configured for current environment.',
671
+ }
672
+
673
+ plugin_path = Path(path).expanduser().resolve()
674
+ if not plugin_path.exists():
675
+ return {'success': False, 'error': f'Path not found: {plugin_path}'}
676
+
677
+ try:
678
+ result = client.run_debug_plugin_release(
679
+ action=action,
680
+ params=params or {},
681
+ plugin_path=str(plugin_path),
682
+ )
683
+ return {'success': True, 'result': result}
684
+ except Exception as e:
685
+ return {'success': False, 'error': str(e)}
686
+
687
+
688
+ @mcp.tool()
689
+ def run_local_plugin(
690
+ path: str,
691
+ action: str,
692
+ params: dict | None = None,
693
+ mode: str = 'local',
694
+ ) -> dict:
695
+ """Run a local plugin directly (without agent).
696
+
697
+ Args:
698
+ path: Path to the local plugin directory
699
+ action: Action name to execute
700
+ params: Parameters to pass to the action
701
+ mode: Execution mode - 'local' (in-process), 'task' (Ray Actor), or 'job' (Ray Job)
702
+
703
+ Returns:
704
+ Execution result
705
+ """
706
+ from pathlib import Path
707
+
708
+ from synapse_sdk.plugins.runner import run_plugin as sdk_run_plugin
709
+
710
+ plugin_path = Path(path).expanduser().resolve()
711
+ if not plugin_path.exists():
712
+ return {'success': False, 'error': f'Path not found: {plugin_path}'}
713
+
714
+ if mode not in ('local', 'task', 'job'):
715
+ return {'success': False, 'error': f'Invalid mode: {mode}. Use local, task, or job.'}
716
+
717
+ try:
718
+ result = sdk_run_plugin(
719
+ plugin_code=str(plugin_path),
720
+ action=action,
721
+ params=params or {},
722
+ mode=mode,
723
+ )
724
+ return {'success': True, 'result': result, 'mode': mode}
725
+ except Exception as e:
726
+ return {'success': False, 'error': str(e)}
727
+
728
+
729
+ # =============================================================================
730
+ # Jobs & Logs Tools
731
+ # =============================================================================
732
+
733
+
734
+ @mcp.tool()
735
+ def list_jobs(limit: int = 20, status: str | None = None) -> dict:
736
+ """List jobs from the current environment.
737
+
738
+ Args:
739
+ limit: Maximum number of jobs to return
740
+ status: Filter by status (e.g., 'RUNNING', 'SUCCEEDED', 'FAILED', 'STOPPED', 'PENDING')
741
+
742
+ Returns:
743
+ List of jobs
744
+ """
745
+ config = _get_config()
746
+ client = config.get_backend_client()
747
+
748
+ if not client:
749
+ return {
750
+ 'success': False,
751
+ 'error': 'No backend configured for current environment.',
752
+ }
753
+
754
+ env = config.get_active_environment()
755
+ if not env or not env.agent_id:
756
+ return {
757
+ 'success': False,
758
+ 'error': 'No agent selected. Use list_agents() and select_agent() first.',
759
+ }
760
+
761
+ try:
762
+ params = {'limit': limit, 'agent': env.agent_id}
763
+ if status:
764
+ params['status'] = status.lower()
765
+
766
+ result = client.list_jobs(params=params)
767
+
768
+ # Backend returns paginated response
769
+ jobs = result.get('results', []) if isinstance(result, dict) else result
770
+
771
+ return {'success': True, 'jobs': jobs, 'count': len(jobs)}
772
+ except Exception as e:
773
+ return {'success': False, 'error': str(e)}
774
+
775
+
776
+ @mcp.tool()
777
+ def get_job(job_id: str) -> dict:
778
+ """Get details of a specific job.
779
+
780
+ Args:
781
+ job_id: The job ID (integer or UUID string)
782
+
783
+ Returns:
784
+ Job details including status, timestamps, and metadata
785
+ """
786
+ config = _get_config()
787
+ client = config.get_backend_client()
788
+
789
+ if not client:
790
+ return {
791
+ 'success': False,
792
+ 'error': 'No backend configured for current environment.',
793
+ }
794
+
795
+ try:
796
+ job = client.get_job(job_id)
797
+ return {'success': True, 'job': job}
798
+ except Exception as e:
799
+ return {'success': False, 'error': str(e)}
800
+
801
+
802
+ @mcp.tool()
803
+ def get_job_logs(job_id: str, lines: int = 100) -> dict:
804
+ """Get logs from a job.
805
+
806
+ Args:
807
+ job_id: The job ID (integer or UUID string)
808
+ lines: Number of log lines to return (tail)
809
+
810
+ Returns:
811
+ Job logs
812
+ """
813
+ config = _get_config()
814
+ client = config.get_backend_client()
815
+
816
+ if not client:
817
+ return {
818
+ 'success': False,
819
+ 'error': 'No backend configured for current environment.',
820
+ }
821
+
822
+ try:
823
+ result = client.list_job_console_logs(job_id)
824
+
825
+ # Extract log entries from response
826
+ log_entries = result.get('results', []) if isinstance(result, dict) else result
827
+
828
+ # Format logs as text
829
+ log_lines = []
830
+ for entry in log_entries:
831
+ if isinstance(entry, dict):
832
+ message = entry.get('message', entry.get('log', str(entry)))
833
+ log_lines.append(message)
834
+ else:
835
+ log_lines.append(str(entry))
836
+
837
+ # Return last N lines
838
+ if len(log_lines) > lines:
839
+ log_lines = log_lines[-lines:]
840
+
841
+ logs = '\n'.join(log_lines)
842
+ return {'success': True, 'logs': logs, 'job_id': job_id}
843
+ except Exception as e:
844
+ return {'success': False, 'error': str(e)}
845
+
846
+
847
+ @mcp.tool()
848
+ def stop_job(job_id: str) -> dict:
849
+ """Stop a running job.
850
+
851
+ Args:
852
+ job_id: The job ID (integer or UUID string) to stop
853
+
854
+ Returns:
855
+ Stop operation result
856
+ """
857
+ config = _get_config()
858
+ client = config.get_backend_client()
859
+
860
+ if not client:
861
+ return {
862
+ 'success': False,
863
+ 'error': 'No backend configured for current environment.',
864
+ }
865
+
866
+ try:
867
+ # Update job status to stopped
868
+ result = client.update_job(int(job_id), {'status': 'stopped'})
869
+ return {'success': True, 'result': result, 'job_id': job_id}
870
+ except Exception as e:
871
+ return {'success': False, 'error': str(e)}
872
+
873
+
874
+ # =============================================================================
875
+ # Deployment Tools
876
+ # =============================================================================
877
+
878
+
879
+ @mcp.tool()
880
+ def list_serve_applications() -> dict:
881
+ """List deployed Ray Serve applications.
882
+
883
+ Returns:
884
+ List of serve applications
885
+ """
886
+ config = _get_config()
887
+ client = config.get_backend_client()
888
+
889
+ if not client:
890
+ return {
891
+ 'success': False,
892
+ 'error': 'No backend configured for current environment.',
893
+ }
894
+
895
+ try:
896
+ result = client.list_serve_applications()
897
+
898
+ # Backend returns paginated response
899
+ applications = result.get('results', []) if isinstance(result, dict) else result
900
+
901
+ return {'success': True, 'applications': applications}
902
+ except Exception as e:
903
+ return {'success': False, 'error': str(e)}
904
+
905
+
906
+ @mcp.tool()
907
+ def get_serve_application(name: str) -> dict:
908
+ """Get details of a Ray Serve application.
909
+
910
+ Args:
911
+ name: Application name
912
+
913
+ Returns:
914
+ Application details
915
+ """
916
+ config = _get_config()
917
+ client = config.get_agent_client()
918
+
919
+ if not client:
920
+ return {
921
+ 'success': False,
922
+ 'error': 'No agent configured for current environment.',
923
+ }
924
+
925
+ try:
926
+ application = client.get_serve_application(name)
927
+ return {'success': True, 'application': application}
928
+ except Exception as e:
929
+ return {'success': False, 'error': str(e)}
930
+
931
+
932
+ @mcp.tool()
933
+ def delete_serve_application(name: str) -> dict:
934
+ """Delete a Ray Serve application.
935
+
936
+ Args:
937
+ name: Application name to delete
938
+
939
+ Returns:
940
+ Deletion result
941
+ """
942
+ config = _get_config()
943
+ client = config.get_agent_client()
944
+
945
+ if not client:
946
+ return {
947
+ 'success': False,
948
+ 'error': 'No agent configured for current environment.',
949
+ }
950
+
951
+ try:
952
+ client.delete_serve_application(name)
953
+ return {'success': True, 'message': f'Application "{name}" deleted'}
954
+ except Exception as e:
955
+ return {'success': False, 'error': str(e)}
956
+
957
+
958
+ # =============================================================================
959
+ # Model Tools
960
+ # =============================================================================
961
+
962
+
963
+ @mcp.tool()
964
+ def list_models(limit: int = 20, offset: int = 0) -> dict:
965
+ """List available models from the current environment.
966
+
967
+ Args:
968
+ limit: Maximum number of results
969
+ offset: Number of results to skip
970
+
971
+ Returns:
972
+ List of models
973
+ """
974
+ config = _get_config()
975
+ client = config.get_backend_client()
976
+
977
+ if not client:
978
+ return {
979
+ 'success': False,
980
+ 'error': 'No backend configured for current environment.',
981
+ }
982
+
983
+ try:
984
+ result = client.list_models(params={'limit': limit, 'offset': offset})
985
+ return {'success': True, 'models': result}
986
+ except Exception as e:
987
+ return {'success': False, 'error': str(e)}
988
+
989
+
990
+ @mcp.tool()
991
+ def get_model(model_id: int) -> dict:
992
+ """Get details of a specific model.
993
+
994
+ Args:
995
+ model_id: The model ID
996
+
997
+ Returns:
998
+ Model details
999
+ """
1000
+ config = _get_config()
1001
+ client = config.get_backend_client()
1002
+
1003
+ if not client:
1004
+ return {
1005
+ 'success': False,
1006
+ 'error': 'No backend configured for current environment.',
1007
+ }
1008
+
1009
+ try:
1010
+ model = client.get_model(model_id)
1011
+ return {'success': True, 'model': model}
1012
+ except Exception as e:
1013
+ return {'success': False, 'error': str(e)}
1014
+
1015
+
1016
+ # =============================================================================
1017
+ # MCP Resources
1018
+ # =============================================================================
1019
+
1020
+
1021
+ @mcp.resource('synapse://config')
1022
+ def resource_config() -> str:
1023
+ """Current Synapse configuration (sanitized, no tokens)."""
1024
+ import json
1025
+
1026
+ config = _get_config()
1027
+ env_list = []
1028
+
1029
+ for name in config.list_environments():
1030
+ env = config.get_environment(name)
1031
+ if env:
1032
+ env_list.append({
1033
+ 'name': name,
1034
+ 'backend_url': env.backend_url,
1035
+ 'agent_url': env.agent_url,
1036
+ 'tenant': env.tenant,
1037
+ 'has_access_token': bool(env.access_token),
1038
+ 'has_agent_token': bool(env.agent_token),
1039
+ 'plugin_paths': env.plugin_paths,
1040
+ })
1041
+
1042
+ return json.dumps(
1043
+ {
1044
+ 'config_path': str(config.config_path),
1045
+ 'default_environment': config._default_environment,
1046
+ 'active_environment': config.get_active_environment_name(),
1047
+ 'environments': env_list,
1048
+ },
1049
+ indent=2,
1050
+ )
1051
+
1052
+
1053
+ @mcp.resource('synapse://environments')
1054
+ def resource_environments() -> str:
1055
+ """List of all configured environments."""
1056
+ import json
1057
+
1058
+ config = _get_config()
1059
+ environments = []
1060
+
1061
+ for name in config.list_environments():
1062
+ env = config.get_environment(name)
1063
+ if env:
1064
+ environments.append(env.to_dict())
1065
+
1066
+ return json.dumps(
1067
+ {
1068
+ 'environments': environments,
1069
+ 'active': config.get_active_environment_name(),
1070
+ },
1071
+ indent=2,
1072
+ )
1073
+
1074
+
1075
+ @mcp.resource('synapse://plugin/{path}/config')
1076
+ def resource_plugin_config(path: str) -> str:
1077
+ """Plugin config.yaml content."""
1078
+ from pathlib import Path
1079
+
1080
+ import yaml
1081
+
1082
+ plugin_path = Path(path).expanduser().resolve()
1083
+ config_file = plugin_path / 'config.yaml'
1084
+
1085
+ if not config_file.exists():
1086
+ return f'Error: config.yaml not found at {config_file}'
1087
+
1088
+ with open(config_file) as f:
1089
+ return yaml.safe_dump(yaml.safe_load(f), default_flow_style=False)
1090
+
1091
+
1092
+ @mcp.resource('synapse://plugin/{path}/actions')
1093
+ def resource_plugin_actions(path: str) -> str:
1094
+ """List of actions in a plugin."""
1095
+ import json
1096
+ from pathlib import Path
1097
+
1098
+ from synapse_sdk.plugins.discovery import PluginDiscovery
1099
+
1100
+ plugin_path = Path(path).expanduser().resolve()
1101
+
1102
+ if not plugin_path.exists():
1103
+ return f'Error: Path not found: {plugin_path}'
1104
+
1105
+ try:
1106
+ discovery = PluginDiscovery.from_path(plugin_path)
1107
+ actions = []
1108
+
1109
+ for action_name in discovery.list_actions():
1110
+ try:
1111
+ action_config = discovery.get_action_config(action_name)
1112
+ actions.append({
1113
+ 'name': action_name,
1114
+ 'description': action_config.description,
1115
+ 'method': action_config.method.value if action_config.method else None,
1116
+ 'entrypoint': action_config.entrypoint,
1117
+ })
1118
+ except Exception:
1119
+ actions.append({'name': action_name})
1120
+
1121
+ return json.dumps({'plugin': str(plugin_path), 'actions': actions}, indent=2)
1122
+ except Exception as e:
1123
+ return f'Error: {e}'
1124
+
1125
+
1126
+ @mcp.resource('synapse://plugin/{path}/action/{action}/schema')
1127
+ def resource_action_schema(path: str, action: str) -> str:
1128
+ """Action parameter JSON schema."""
1129
+ import json
1130
+ from pathlib import Path
1131
+
1132
+ from synapse_sdk.plugins.discovery import PluginDiscovery
1133
+
1134
+ plugin_path = Path(path).expanduser().resolve()
1135
+
1136
+ if not plugin_path.exists():
1137
+ return f'Error: Path not found: {plugin_path}'
1138
+
1139
+ try:
1140
+ discovery = PluginDiscovery.from_path(plugin_path)
1141
+
1142
+ if not discovery.has_action(action):
1143
+ return f'Error: Action "{action}" not found. Available: {discovery.list_actions()}'
1144
+
1145
+ params_model = discovery.get_action_params_model(action)
1146
+ result_model = discovery.get_action_result_model(action)
1147
+
1148
+ schema = {
1149
+ 'action': action,
1150
+ 'params_schema': params_model.model_json_schema() if params_model else None,
1151
+ 'result_schema': result_model.model_json_schema() if result_model else None,
1152
+ }
1153
+
1154
+ return json.dumps(schema, indent=2)
1155
+ except Exception as e:
1156
+ return f'Error: {e}'
1157
+
1158
+
1159
+ # =============================================================================
1160
+ # MCP Prompts
1161
+ # =============================================================================
1162
+
1163
+
1164
+ @mcp.prompt()
1165
+ def debug_plugin(plugin_path: str, action: str = '') -> str:
1166
+ """Guide through debugging a local plugin.
1167
+
1168
+ Args:
1169
+ plugin_path: Path to the local plugin directory
1170
+ action: Optional action name to focus on
1171
+ """
1172
+ prompt = f"""You are helping debug a local Synapse plugin.
1173
+
1174
+ Plugin Path: {plugin_path}
1175
+
1176
+ Steps to debug:
1177
+
1178
+ 1. First, discover the plugin to see available actions:
1179
+ Use the `discover_local_plugin` tool with path="{plugin_path}"
1180
+
1181
+ 2. Validate the plugin configuration:
1182
+ Use the `validate_plugin_config` tool with path="{plugin_path}"
1183
+
1184
+ 3. If there are validation errors, help fix them by examining the config.yaml
1185
+
1186
+ 4. To test an action locally (without remote agent):
1187
+ Use the `run_local_plugin` tool with path="{plugin_path}", action="<action_name>", params={{...}}, mode="local"
1188
+
1189
+ 5. To test via the remote agent (for debugging in prod-like environment):
1190
+ Use the `run_debug_plugin` tool with path="{plugin_path}", action="<action_name>", params={{...}}
1191
+
1192
+ 6. Check job logs if execution fails:
1193
+ Use `list_jobs` to find recent jobs, then `get_job_logs` for details
1194
+ """
1195
+
1196
+ if action:
1197
+ prompt += f"""
1198
+ Focus on action: {action}
1199
+
1200
+ Get the action schema:
1201
+ Use the `get_action_config` tool with path="{plugin_path}", action="{action}"
1202
+ """
1203
+
1204
+ return prompt
1205
+
1206
+
1207
+ @mcp.prompt()
1208
+ def publish_plugin_workflow(plugin_path: str, version: str = '') -> str:
1209
+ """Guide through publishing a plugin to the registry.
1210
+
1211
+ Args:
1212
+ plugin_path: Path to the local plugin directory
1213
+ version: Version to publish (optional)
1214
+ """
1215
+ version_note = f'Version to publish: {version}' if version else 'Version: (will use config.yaml version)'
1216
+
1217
+ return f"""You are helping publish a Synapse plugin to the registry.
1218
+
1219
+ Plugin Path: {plugin_path}
1220
+ {version_note}
1221
+
1222
+ Pre-publish checklist:
1223
+
1224
+ 1. Validate the plugin configuration:
1225
+ Use `validate_plugin_config` with path="{plugin_path}"
1226
+ - Ensure no errors (warnings are okay)
1227
+ - Verify 'code', 'version', and 'actions' are properly defined
1228
+
1229
+ 2. Test the plugin locally:
1230
+ Use `run_local_plugin` with path="{plugin_path}" and test each action
1231
+
1232
+ 3. Verify the current environment:
1233
+ Use `get_current_environment` to confirm you're publishing to the right registry
1234
+
1235
+ 4. Publish the plugin:
1236
+ Use `publish_plugin` with path="{plugin_path}"{f', version="{version}"' if version else ''}
1237
+
1238
+ 5. Verify the publication:
1239
+ Use `get_plugin_release` with the plugin code:version to confirm
1240
+
1241
+ Post-publish:
1242
+ - The plugin is now available in the registry
1243
+ - Others can run it using `run_plugin` with the plugin code
1244
+ """
1245
+
1246
+
1247
+ @mcp.prompt()
1248
+ def diagnose_job(job_id: str) -> str:
1249
+ """Help diagnose a failed or problematic job.
1250
+
1251
+ Args:
1252
+ job_id: The job ID to diagnose
1253
+ """
1254
+ return f"""You are helping diagnose a Synapse job.
1255
+
1256
+ Job ID: {job_id}
1257
+
1258
+ Diagnostic steps:
1259
+
1260
+ 1. Get job details:
1261
+ Use `get_job` with job_id="{job_id}"
1262
+ - Check status: PENDING, RUNNING, SUCCEEDED, FAILED, STOPPED
1263
+ - Note the plugin/action that was run
1264
+ - Check start/end times
1265
+
1266
+ 2. Get job logs:
1267
+ Use `get_job_logs` with job_id="{job_id}", lines=200
1268
+ - Look for error messages, stack traces
1269
+ - Check for timeout issues
1270
+ - Look for resource constraints (OOM, CPU limits)
1271
+
1272
+ 3. If the job is stuck RUNNING:
1273
+ - Check if it's actually making progress in logs
1274
+ - Consider using `stop_job` if it's hung
1275
+
1276
+ 4. Common issues to check:
1277
+ - Plugin code errors (stack traces in logs)
1278
+ - Configuration errors (missing params, wrong types)
1279
+ - Resource limits (memory, CPU, GPU)
1280
+ - Network issues (can't reach external services)
1281
+ - Timeout (job took too long)
1282
+
1283
+ 5. If you need to re-run with fixes:
1284
+ - For published plugins: use `run_plugin`
1285
+ - For local plugins: use `run_local_plugin` or `run_debug_plugin`
1286
+ """
1287
+
1288
+
1289
+ @mcp.prompt()
1290
+ def setup_environment(env_name: str = 'prod') -> str:
1291
+ """Guide through setting up a new Synapse environment.
1292
+
1293
+ Args:
1294
+ env_name: Name for the new environment (default: prod)
1295
+ """
1296
+ return f"""You are helping set up a new Synapse environment.
1297
+
1298
+ Environment Name: {env_name}
1299
+
1300
+ Setup steps:
1301
+
1302
+ 1. First, check existing environments:
1303
+ Use `list_environments` to see what's already configured
1304
+
1305
+ 2. Gather the required information:
1306
+ - Backend URL: The Synapse API server URL (e.g., https://api.synapse.example.com)
1307
+ - Access Token: Your API access token
1308
+ - Tenant: Your tenant identifier (optional)
1309
+
1310
+ 3. Add the environment:
1311
+ Use `add_environment` with:
1312
+ - name="{env_name}"
1313
+ - backend_url="<your_backend_url>"
1314
+ - access_token="<your_access_token>"
1315
+ - tenant="<your_tenant>" (if applicable)
1316
+ - set_as_default=True (if this should be the default)
1317
+
1318
+ 4. Switch to the new environment:
1319
+ Use `switch_environment` with name="{env_name}"
1320
+
1321
+ 5. Configure the agent (fetched from backend):
1322
+ - Use `list_agents` to see available agents
1323
+ - Use `select_agent(agent_id)` to choose one
1324
+
1325
+ 6. Verify the connection:
1326
+ Use `get_current_environment` to check backend/agent status
1327
+
1328
+ 7. Test basic functionality:
1329
+ - Use `list_plugin_releases` to verify backend connection
1330
+ - Use `list_jobs` to verify agent connection
1331
+
1332
+ Configuration file location: ~/.synapse/config.yaml
1333
+ """
1334
+
1335
+
1336
+ # =============================================================================
1337
+ # Server Entry Point
1338
+ # =============================================================================
1339
+
1340
+
1341
+ def serve() -> None:
1342
+ """Run the MCP server."""
1343
+ _log('Starting Synapse MCP server...')
1344
+ config = _get_config()
1345
+ _log(f'Config path: {config.config_path}')
1346
+ _log(f'Environments: {config.list_environments()}')
1347
+ _log(f'Active environment: {config.get_active_environment_name()}')
1348
+ mcp.run()
1349
+
1350
+
1351
+ if __name__ == '__main__':
1352
+ serve()