assemblix-mcp 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- assemblix_mcp/__init__.py +3 -0
- assemblix_mcp/client.py +192 -0
- assemblix_mcp/config.py +38 -0
- assemblix_mcp/resources/__init__.py +50 -0
- assemblix_mcp/resources/examples/branching.json +63 -0
- assemblix_mcp/resources/examples/minimal.json +36 -0
- assemblix_mcp/server.py +42 -0
- assemblix_mcp/tools/__init__.py +1 -0
- assemblix_mcp/tools/executions.py +115 -0
- assemblix_mcp/tools/workflows.py +76 -0
- assemblix_mcp-0.1.0.dist-info/METADATA +95 -0
- assemblix_mcp-0.1.0.dist-info/RECORD +15 -0
- assemblix_mcp-0.1.0.dist-info/WHEEL +4 -0
- assemblix_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- assemblix_mcp-0.1.0.dist-info/licenses/LICENSE +21 -0
assemblix_mcp/client.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Thin async HTTP client over the Assemblix REST API.
|
|
2
|
+
|
|
3
|
+
Wire contract: query params are snake_case; request bodies are camelCase;
|
|
4
|
+
responses are camelCase. ``project_id`` is injected only where the endpoint
|
|
5
|
+
expects it (workflows list query + create body). Id-addressed and key-scoped
|
|
6
|
+
endpoints do not need it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AssemblixAPIError(Exception):
|
|
17
|
+
def __init__(self, status_code: int, detail: str) -> None:
|
|
18
|
+
self.status_code = status_code
|
|
19
|
+
self.detail = detail
|
|
20
|
+
super().__init__(f"{status_code}: {detail}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AssemblixClient:
|
|
24
|
+
def __init__(self, base_url: str, api_key: str, project_id: str) -> None:
|
|
25
|
+
self._base = base_url.rstrip("/")
|
|
26
|
+
self._project_id = project_id
|
|
27
|
+
self._headers = {"Authorization": f"Bearer {api_key}"}
|
|
28
|
+
|
|
29
|
+
async def _request(
|
|
30
|
+
self,
|
|
31
|
+
method: str,
|
|
32
|
+
path: str,
|
|
33
|
+
*,
|
|
34
|
+
params: dict | None = None,
|
|
35
|
+
json: dict | None = None,
|
|
36
|
+
) -> Any:
|
|
37
|
+
async with httpx.AsyncClient(base_url=self._base, headers=self._headers) as http:
|
|
38
|
+
resp = await http.request(method, path, params=params, json=json)
|
|
39
|
+
if resp.status_code >= 400:
|
|
40
|
+
raise AssemblixAPIError(resp.status_code, _extract_detail(resp))
|
|
41
|
+
if resp.status_code == 204 or not resp.content:
|
|
42
|
+
return None
|
|
43
|
+
return resp.json()
|
|
44
|
+
|
|
45
|
+
# --- nodes ---
|
|
46
|
+
async def list_node_types(self) -> Any:
|
|
47
|
+
return await self._request("GET", "/api/nodes")
|
|
48
|
+
|
|
49
|
+
# --- workflows ---
|
|
50
|
+
async def list_workflows(
|
|
51
|
+
self,
|
|
52
|
+
is_active: bool | None = None,
|
|
53
|
+
is_published: bool | None = None,
|
|
54
|
+
is_template: bool | None = None,
|
|
55
|
+
) -> Any:
|
|
56
|
+
params = _clean(
|
|
57
|
+
{
|
|
58
|
+
"project_id": self._project_id,
|
|
59
|
+
"is_active": is_active,
|
|
60
|
+
"is_published": is_published,
|
|
61
|
+
"is_template": is_template,
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
return await self._request("GET", "/api/workflows/", params=params)
|
|
65
|
+
|
|
66
|
+
async def get_workflow(self, workflow_id: str) -> Any:
|
|
67
|
+
return await self._request("GET", f"/api/workflows/{workflow_id}")
|
|
68
|
+
|
|
69
|
+
async def create_workflow(
|
|
70
|
+
self,
|
|
71
|
+
name: str | None = None,
|
|
72
|
+
description: str | None = None,
|
|
73
|
+
nodes: list | None = None,
|
|
74
|
+
edges: list | None = None,
|
|
75
|
+
state: list | None = None,
|
|
76
|
+
) -> Any:
|
|
77
|
+
body = _clean(
|
|
78
|
+
{
|
|
79
|
+
"projectId": self._project_id,
|
|
80
|
+
"name": name,
|
|
81
|
+
"description": description,
|
|
82
|
+
"nodes": nodes,
|
|
83
|
+
"edges": edges,
|
|
84
|
+
"state": state,
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
return await self._request("POST", "/api/workflows/", json=body)
|
|
88
|
+
|
|
89
|
+
async def update_workflow(
|
|
90
|
+
self,
|
|
91
|
+
workflow_id: str,
|
|
92
|
+
name: str | None = None,
|
|
93
|
+
description: str | None = None,
|
|
94
|
+
nodes: list | None = None,
|
|
95
|
+
edges: list | None = None,
|
|
96
|
+
state: list | None = None,
|
|
97
|
+
) -> Any:
|
|
98
|
+
body = _clean(
|
|
99
|
+
{
|
|
100
|
+
"name": name,
|
|
101
|
+
"description": description,
|
|
102
|
+
"nodes": nodes,
|
|
103
|
+
"edges": edges,
|
|
104
|
+
"state": state,
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
return await self._request("PATCH", f"/api/workflows/{workflow_id}", json=body)
|
|
108
|
+
|
|
109
|
+
async def publish_workflow(self, workflow_id: str) -> Any:
|
|
110
|
+
return await self._request("POST", f"/api/workflows/{workflow_id}/publish")
|
|
111
|
+
|
|
112
|
+
# --- execution ---
|
|
113
|
+
async def execute_workflow(
|
|
114
|
+
self,
|
|
115
|
+
workflow_id: str,
|
|
116
|
+
input: dict,
|
|
117
|
+
task: bool = False,
|
|
118
|
+
state: dict | None = None,
|
|
119
|
+
project_state: dict | None = None,
|
|
120
|
+
client_id: str | None = None,
|
|
121
|
+
metadata: dict | None = None,
|
|
122
|
+
) -> Any:
|
|
123
|
+
body = _clean(
|
|
124
|
+
{
|
|
125
|
+
"input": input,
|
|
126
|
+
"task": task,
|
|
127
|
+
"state": state,
|
|
128
|
+
"projectState": project_state,
|
|
129
|
+
"clientId": client_id,
|
|
130
|
+
"metadata": metadata,
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
return await self._request(
|
|
134
|
+
"POST", f"/api/workflows/{workflow_id}/execute", json=body
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
async def get_task_result(self, execution_id: str) -> Any:
|
|
138
|
+
return await self._request("GET", f"/api/executions/task/{execution_id}")
|
|
139
|
+
|
|
140
|
+
async def list_executions(
|
|
141
|
+
self,
|
|
142
|
+
workflow_id: str | None = None,
|
|
143
|
+
status: str | None = None,
|
|
144
|
+
date_from: str | None = None,
|
|
145
|
+
date_to: str | None = None,
|
|
146
|
+
page: int = 1,
|
|
147
|
+
limit: int = 50,
|
|
148
|
+
) -> Any:
|
|
149
|
+
params = _clean(
|
|
150
|
+
{
|
|
151
|
+
"project_id": self._project_id,
|
|
152
|
+
"workflow_id": workflow_id,
|
|
153
|
+
"status": status,
|
|
154
|
+
"date_from": date_from,
|
|
155
|
+
"date_to": date_to,
|
|
156
|
+
"page": page,
|
|
157
|
+
"limit": limit,
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
return await self._request("GET", "/api/executions/", params=params)
|
|
161
|
+
|
|
162
|
+
async def get_execution_detail(self, execution_id: str) -> Any:
|
|
163
|
+
return await self._request("GET", f"/api/executions/{execution_id}")
|
|
164
|
+
|
|
165
|
+
async def list_in_flight(self) -> Any:
|
|
166
|
+
return await self._request("GET", "/api/executions/in-flight")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async def fetch_project_id(base_url: str, api_key: str) -> str:
|
|
170
|
+
"""Resolve the key's project via the whoami endpoint."""
|
|
171
|
+
async with httpx.AsyncClient(
|
|
172
|
+
base_url=base_url.rstrip("/"),
|
|
173
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
174
|
+
) as http:
|
|
175
|
+
resp = await http.get("/api/api-keys/whoami")
|
|
176
|
+
if resp.status_code >= 400:
|
|
177
|
+
raise AssemblixAPIError(resp.status_code, _extract_detail(resp))
|
|
178
|
+
return str(resp.json()["projectId"])
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _clean(d: dict) -> dict:
|
|
182
|
+
return {k: v for k, v in d.items() if v is not None}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _extract_detail(resp: httpx.Response) -> str:
|
|
186
|
+
try:
|
|
187
|
+
body = resp.json()
|
|
188
|
+
if isinstance(body, dict) and "detail" in body:
|
|
189
|
+
return str(body["detail"])
|
|
190
|
+
except Exception:
|
|
191
|
+
pass
|
|
192
|
+
return resp.text or resp.reason_phrase
|
assemblix_mcp/config.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Configuration and per-request API key resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Settings(BaseModel):
|
|
11
|
+
api_url: str
|
|
12
|
+
api_key: str | None
|
|
13
|
+
transport: str
|
|
14
|
+
host: str
|
|
15
|
+
port: int
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_env(cls) -> "Settings":
|
|
19
|
+
return cls(
|
|
20
|
+
api_url=os.environ.get("ASSEMBLIX_API_URL", "https://app.assmblx.com"),
|
|
21
|
+
api_key=os.environ.get("ASSEMBLIX_API_KEY"),
|
|
22
|
+
transport=os.environ.get("ASSEMBLIX_MCP_TRANSPORT", "stdio"),
|
|
23
|
+
host=os.environ.get("ASSEMBLIX_MCP_HOST", "0.0.0.0"),
|
|
24
|
+
port=int(os.environ.get("ASSEMBLIX_MCP_PORT", "8000")),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def resolve_api_key(settings: Settings, header_value: str | None) -> str:
|
|
29
|
+
"""Return the bearer token, preferring an incoming Authorization header."""
|
|
30
|
+
if header_value:
|
|
31
|
+
token = header_value.removeprefix("Bearer ").strip()
|
|
32
|
+
if token:
|
|
33
|
+
return token
|
|
34
|
+
if settings.api_key:
|
|
35
|
+
return settings.api_key
|
|
36
|
+
raise ValueError(
|
|
37
|
+
"No API key: set ASSEMBLIX_API_KEY or send an Authorization: Bearer header."
|
|
38
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Bundled example workflows + authoring prompt registered on the MCP server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib import resources
|
|
6
|
+
|
|
7
|
+
from fastmcp import FastMCP
|
|
8
|
+
|
|
9
|
+
_EXAMPLES = resources.files("assemblix_mcp.resources") / "examples"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _load(name: str) -> str:
|
|
13
|
+
return (_EXAMPLES / name).read_text(encoding="utf-8")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def register_resources(mcp: FastMCP) -> None:
|
|
17
|
+
@mcp.resource("assemblix://examples/minimal")
|
|
18
|
+
def minimal_example() -> str:
|
|
19
|
+
"""A runnable START -> AGENT -> END workflow (create -> update ->
|
|
20
|
+
publish -> run). Shows the START node shape, which is not in
|
|
21
|
+
list_node_types."""
|
|
22
|
+
return _load("minimal.json")
|
|
23
|
+
|
|
24
|
+
@mcp.resource("assemblix://examples/branching")
|
|
25
|
+
def branching_example() -> str:
|
|
26
|
+
"""A START -> CONDITION -> two AGENT branches -> END workflow. Shows how
|
|
27
|
+
condition branch edges use sourceHandle 'source_<conditionId>_<index>'."""
|
|
28
|
+
return _load("branching.json")
|
|
29
|
+
|
|
30
|
+
@mcp.prompt
|
|
31
|
+
def author_workflow() -> str:
|
|
32
|
+
"""Guidance for authoring an Assemblix workflow via these tools."""
|
|
33
|
+
return (
|
|
34
|
+
"To author and run an Assemblix workflow:\n"
|
|
35
|
+
"1. Call list_node_types to see AGENT/CONDITION/HTTP_REQUEST/"
|
|
36
|
+
"SET_VARIABLE/DELAY/END config schemas. START is implicit and NOT "
|
|
37
|
+
"listed — copy its shape from the example resources.\n"
|
|
38
|
+
"2. Read assemblix://examples/minimal (and /branching) for the exact "
|
|
39
|
+
"node/edge JSON shape: node = {id, type, position:{x,y}, config}, "
|
|
40
|
+
"edge = {id, source, target, sourceHandle?}. CONDITION branch edges "
|
|
41
|
+
"use sourceHandle 'source_<conditionNodeId>_<index>'.\n"
|
|
42
|
+
"3. create_workflow -> update_workflow(nodes, edges, state) -> "
|
|
43
|
+
"publish_workflow. Runs always execute the PUBLISHED snapshot, so "
|
|
44
|
+
"publish after every change you want to run.\n"
|
|
45
|
+
"4. run_workflow (async, poll get_execution) or run_workflow_and_wait. "
|
|
46
|
+
"input usually is {\"message\": \"...\"}.\n"
|
|
47
|
+
"5. Inspect with get_execution_detail / list_executions. AGENT nodes "
|
|
48
|
+
"need a provider credential configured in the Assemblix UI; if missing, "
|
|
49
|
+
"the run fails with a clear error."
|
|
50
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Branching agent",
|
|
3
|
+
"description": "START -> CONDITION -> two AGENT branches -> END. The condition routes by index; branch edges use sourceHandle source_<conditionId>_<index>.",
|
|
4
|
+
"nodes": [
|
|
5
|
+
{
|
|
6
|
+
"id": "start-1",
|
|
7
|
+
"type": "start",
|
|
8
|
+
"position": { "x": 0, "y": 0 },
|
|
9
|
+
"config": { "firstPhrase": null, "acceptVoice": false }
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"id": "cond-1",
|
|
13
|
+
"type": "condition",
|
|
14
|
+
"position": { "x": 320, "y": 0 },
|
|
15
|
+
"config": {
|
|
16
|
+
"conditions": [
|
|
17
|
+
{ "name": "mentions bug", "expression": "input.message.contains('bug')" },
|
|
18
|
+
{ "name": "default", "expression": "true" }
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "agent-bug",
|
|
24
|
+
"type": "agent",
|
|
25
|
+
"position": { "x": 640, "y": -120 },
|
|
26
|
+
"config": {
|
|
27
|
+
"name": "Bug helper",
|
|
28
|
+
"provider": "openai",
|
|
29
|
+
"model": "gpt-4o-mini",
|
|
30
|
+
"instructions": [
|
|
31
|
+
{ "role": "system", "content": "The user reported a bug. Ask one clarifying question." }
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "agent-default",
|
|
37
|
+
"type": "agent",
|
|
38
|
+
"position": { "x": 640, "y": 120 },
|
|
39
|
+
"config": {
|
|
40
|
+
"name": "General helper",
|
|
41
|
+
"provider": "openai",
|
|
42
|
+
"model": "gpt-4o-mini",
|
|
43
|
+
"instructions": [
|
|
44
|
+
{ "role": "system", "content": "Answer the user's message helpfully." }
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "end-1",
|
|
50
|
+
"type": "end",
|
|
51
|
+
"position": { "x": 960, "y": 0 },
|
|
52
|
+
"config": { "name": "Done", "outputMode": "last_agent" }
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"edges": [
|
|
56
|
+
{ "id": "e1", "source": "start-1", "target": "cond-1" },
|
|
57
|
+
{ "id": "e2", "source": "cond-1", "target": "agent-bug", "sourceHandle": "source_cond-1_0" },
|
|
58
|
+
{ "id": "e3", "source": "cond-1", "target": "agent-default", "sourceHandle": "source_cond-1_1" },
|
|
59
|
+
{ "id": "e4", "source": "agent-bug", "target": "end-1" },
|
|
60
|
+
{ "id": "e5", "source": "agent-default", "target": "end-1" }
|
|
61
|
+
],
|
|
62
|
+
"state": []
|
|
63
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Minimal chat agent",
|
|
3
|
+
"description": "START -> AGENT -> END. Replies to input.message with an LLM.",
|
|
4
|
+
"nodes": [
|
|
5
|
+
{
|
|
6
|
+
"id": "start-1",
|
|
7
|
+
"type": "start",
|
|
8
|
+
"position": { "x": 0, "y": 0 },
|
|
9
|
+
"config": { "firstPhrase": null, "acceptVoice": false }
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"id": "agent-1",
|
|
13
|
+
"type": "agent",
|
|
14
|
+
"position": { "x": 320, "y": 0 },
|
|
15
|
+
"config": {
|
|
16
|
+
"name": "Assistant",
|
|
17
|
+
"provider": "openai",
|
|
18
|
+
"model": "gpt-4o-mini",
|
|
19
|
+
"instructions": [
|
|
20
|
+
{ "role": "system", "content": "You are a helpful assistant. Answer the user's message." }
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "end-1",
|
|
26
|
+
"type": "end",
|
|
27
|
+
"position": { "x": 640, "y": 0 },
|
|
28
|
+
"config": { "name": "Done", "outputMode": "last_agent" }
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"edges": [
|
|
32
|
+
{ "id": "e1", "source": "start-1", "target": "agent-1" },
|
|
33
|
+
{ "id": "e2", "source": "agent-1", "target": "end-1" }
|
|
34
|
+
],
|
|
35
|
+
"state": []
|
|
36
|
+
}
|
assemblix_mcp/server.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""FastMCP server assembly and entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastmcp import FastMCP
|
|
6
|
+
from fastmcp.server.dependencies import get_http_headers
|
|
7
|
+
|
|
8
|
+
from assemblix_mcp.client import AssemblixClient, fetch_project_id
|
|
9
|
+
from assemblix_mcp.config import Settings, resolve_api_key
|
|
10
|
+
from assemblix_mcp.resources import register_resources
|
|
11
|
+
from assemblix_mcp.tools.executions import register_execution_tools
|
|
12
|
+
from assemblix_mcp.tools.workflows import register_workflow_tools
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_server(settings: Settings) -> FastMCP:
|
|
16
|
+
mcp = FastMCP("assemblix")
|
|
17
|
+
|
|
18
|
+
async def get_client() -> AssemblixClient:
|
|
19
|
+
headers = get_http_headers(include={"authorization"}) or {}
|
|
20
|
+
api_key = resolve_api_key(settings, headers.get("authorization"))
|
|
21
|
+
project_id = await fetch_project_id(settings.api_url, api_key)
|
|
22
|
+
return AssemblixClient(
|
|
23
|
+
base_url=settings.api_url, api_key=api_key, project_id=project_id
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
register_workflow_tools(mcp, get_client)
|
|
27
|
+
register_execution_tools(mcp, get_client)
|
|
28
|
+
register_resources(mcp)
|
|
29
|
+
return mcp
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main() -> None:
|
|
33
|
+
settings = Settings.from_env()
|
|
34
|
+
mcp = build_server(settings)
|
|
35
|
+
if settings.transport == "http":
|
|
36
|
+
mcp.run(transport="http", host=settings.host, port=settings.port)
|
|
37
|
+
else:
|
|
38
|
+
mcp.run()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCP tool registration."""
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Run & inspect tools. project_id is implicit in the API key."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from fastmcp import FastMCP
|
|
10
|
+
|
|
11
|
+
from assemblix_mcp.tools.workflows import GetClient
|
|
12
|
+
|
|
13
|
+
_PENDING = {"running", "queued", "pending"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _is_terminal(payload: Any) -> bool:
|
|
17
|
+
status = str((payload or {}).get("status", "")).lower()
|
|
18
|
+
return status not in _PENDING
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def register_execution_tools(mcp: FastMCP, get_client: GetClient) -> None:
|
|
22
|
+
@mcp.tool
|
|
23
|
+
async def run_workflow(
|
|
24
|
+
workflow_id: str,
|
|
25
|
+
input: dict,
|
|
26
|
+
state: dict | None = None,
|
|
27
|
+
project_state: dict | None = None,
|
|
28
|
+
client_id: str | None = None,
|
|
29
|
+
metadata: dict | None = None,
|
|
30
|
+
) -> Any:
|
|
31
|
+
"""Start the published workflow asynchronously. Returns immediately with
|
|
32
|
+
an executionId; poll it with get_execution. `input` typically holds a
|
|
33
|
+
'message' field. Publish the workflow first."""
|
|
34
|
+
client = await get_client()
|
|
35
|
+
return await client.execute_workflow(
|
|
36
|
+
workflow_id,
|
|
37
|
+
input=input,
|
|
38
|
+
task=True,
|
|
39
|
+
state=state,
|
|
40
|
+
project_state=project_state,
|
|
41
|
+
client_id=client_id,
|
|
42
|
+
metadata=metadata,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@mcp.tool
|
|
46
|
+
async def run_workflow_and_wait(
|
|
47
|
+
workflow_id: str,
|
|
48
|
+
input: dict,
|
|
49
|
+
state: dict | None = None,
|
|
50
|
+
project_state: dict | None = None,
|
|
51
|
+
timeout_seconds: float = 120.0,
|
|
52
|
+
poll_interval: float = 2.0,
|
|
53
|
+
) -> Any:
|
|
54
|
+
"""Start the published workflow and poll until it finishes or the timeout
|
|
55
|
+
elapses. Returns the final result — including an error payload on failure
|
|
56
|
+
(does not raise). On timeout returns the last status with timedOut=true."""
|
|
57
|
+
client = await get_client()
|
|
58
|
+
started = await client.execute_workflow(
|
|
59
|
+
workflow_id,
|
|
60
|
+
input=input,
|
|
61
|
+
task=True,
|
|
62
|
+
state=state,
|
|
63
|
+
project_state=project_state,
|
|
64
|
+
)
|
|
65
|
+
if _is_terminal(started):
|
|
66
|
+
return started
|
|
67
|
+
execution_id = str(started["executionId"])
|
|
68
|
+
deadline = time.monotonic() + timeout_seconds
|
|
69
|
+
while time.monotonic() < deadline:
|
|
70
|
+
await asyncio.sleep(poll_interval)
|
|
71
|
+
result = await client.get_task_result(execution_id)
|
|
72
|
+
if _is_terminal(result):
|
|
73
|
+
return result
|
|
74
|
+
return {"executionId": execution_id, "status": "running", "timedOut": True}
|
|
75
|
+
|
|
76
|
+
@mcp.tool
|
|
77
|
+
async def get_execution(execution_id: str) -> Any:
|
|
78
|
+
"""Get the status/result of one run (poll after run_workflow). Returns
|
|
79
|
+
status 'running' while in progress, else the completed or error payload."""
|
|
80
|
+
client = await get_client()
|
|
81
|
+
return await client.get_task_result(execution_id)
|
|
82
|
+
|
|
83
|
+
@mcp.tool
|
|
84
|
+
async def list_executions(
|
|
85
|
+
workflow_id: str | None = None,
|
|
86
|
+
status: str | None = None,
|
|
87
|
+
date_from: str | None = None,
|
|
88
|
+
date_to: str | None = None,
|
|
89
|
+
page: int = 1,
|
|
90
|
+
limit: int = 50,
|
|
91
|
+
) -> Any:
|
|
92
|
+
"""List the project's run history (paginated). status is one of QUEUED,
|
|
93
|
+
RUNNING, COMPLETED, ERROR, FAILED. Dates are ISO 8601."""
|
|
94
|
+
client = await get_client()
|
|
95
|
+
return await client.list_executions(
|
|
96
|
+
workflow_id=workflow_id,
|
|
97
|
+
status=status,
|
|
98
|
+
date_from=date_from,
|
|
99
|
+
date_to=date_to,
|
|
100
|
+
page=page,
|
|
101
|
+
limit=limit,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
@mcp.tool
|
|
105
|
+
async def get_execution_detail(execution_id: str) -> Any:
|
|
106
|
+
"""Get step-level detail for a run (node inputs/outputs, errors, timing) —
|
|
107
|
+
use this to diagnose why a run failed."""
|
|
108
|
+
client = await get_client()
|
|
109
|
+
return await client.get_execution_detail(execution_id)
|
|
110
|
+
|
|
111
|
+
@mcp.tool
|
|
112
|
+
async def list_in_flight() -> list:
|
|
113
|
+
"""List runs currently QUEUED or RUNNING in the project."""
|
|
114
|
+
client = await get_client()
|
|
115
|
+
return await client.list_in_flight()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Workflow authoring tools. project_id is implicit in the API key."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Awaitable, Callable
|
|
6
|
+
|
|
7
|
+
from fastmcp import FastMCP
|
|
8
|
+
|
|
9
|
+
from assemblix_mcp.client import AssemblixClient
|
|
10
|
+
|
|
11
|
+
GetClient = Callable[[], Awaitable[AssemblixClient]]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def register_workflow_tools(mcp: FastMCP, get_client: GetClient) -> None:
|
|
15
|
+
@mcp.tool
|
|
16
|
+
async def list_node_types() -> list:
|
|
17
|
+
"""List available node types with their config schemas, for building a
|
|
18
|
+
workflow graph. Note: START is implicit and not listed here — see the
|
|
19
|
+
example workflows (resources) for its shape and the create->update->
|
|
20
|
+
publish->run lifecycle."""
|
|
21
|
+
client = await get_client()
|
|
22
|
+
return await client.list_node_types()
|
|
23
|
+
|
|
24
|
+
@mcp.tool
|
|
25
|
+
async def list_workflows(
|
|
26
|
+
is_active: bool | None = None,
|
|
27
|
+
is_published: bool | None = None,
|
|
28
|
+
is_template: bool | None = None,
|
|
29
|
+
) -> list:
|
|
30
|
+
"""List the project's workflows (optionally filtered by status)."""
|
|
31
|
+
client = await get_client()
|
|
32
|
+
return await client.list_workflows(
|
|
33
|
+
is_active=is_active, is_published=is_published, is_template=is_template
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@mcp.tool
|
|
37
|
+
async def get_workflow(workflow_id: str) -> Any:
|
|
38
|
+
"""Get a workflow by id, including its nodes, edges and state."""
|
|
39
|
+
client = await get_client()
|
|
40
|
+
return await client.get_workflow(workflow_id)
|
|
41
|
+
|
|
42
|
+
@mcp.tool
|
|
43
|
+
async def create_workflow(name: str, description: str | None = None) -> Any:
|
|
44
|
+
"""Create a new draft workflow in the project. Add nodes/edges with
|
|
45
|
+
update_workflow, then publish_workflow before running."""
|
|
46
|
+
client = await get_client()
|
|
47
|
+
return await client.create_workflow(name=name, description=description)
|
|
48
|
+
|
|
49
|
+
@mcp.tool
|
|
50
|
+
async def update_workflow(
|
|
51
|
+
workflow_id: str,
|
|
52
|
+
name: str | None = None,
|
|
53
|
+
description: str | None = None,
|
|
54
|
+
nodes: list | None = None,
|
|
55
|
+
edges: list | None = None,
|
|
56
|
+
state: list | None = None,
|
|
57
|
+
) -> Any:
|
|
58
|
+
"""Replace a draft workflow's fields. nodes/edges/state fully replace the
|
|
59
|
+
existing graph when provided; omit to leave unchanged."""
|
|
60
|
+
client = await get_client()
|
|
61
|
+
return await client.update_workflow(
|
|
62
|
+
workflow_id,
|
|
63
|
+
name=name,
|
|
64
|
+
description=description,
|
|
65
|
+
nodes=nodes,
|
|
66
|
+
edges=edges,
|
|
67
|
+
state=state,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
@mcp.tool
|
|
71
|
+
async def publish_workflow(workflow_id: str) -> Any:
|
|
72
|
+
"""Publish the draft as the runnable snapshot. run_workflow always
|
|
73
|
+
executes the published version, so publish after every edit you want to
|
|
74
|
+
run."""
|
|
75
|
+
client = await get_client()
|
|
76
|
+
return await client.publish_workflow(workflow_id)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: assemblix-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for Assemblix — author, run & inspect workflows over a project API key
|
|
5
|
+
Project-URL: Homepage, https://github.com/nmamizerov/assemblix-aitools
|
|
6
|
+
Project-URL: Repository, https://github.com/nmamizerov/assemblix-aitools
|
|
7
|
+
Project-URL: Issues, https://github.com/nmamizerov/assemblix-aitools/issues
|
|
8
|
+
Author-email: nmamizerov <nmamizerov@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai-agents,assemblix,llm,mcp,model-context-protocol,workflows
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: fastmcp>=3
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: pydantic>=2
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# assemblix-mcp
|
|
27
|
+
|
|
28
|
+
MCP server for [Assemblix](https://app.assmblx.com). Author, run & inspect your
|
|
29
|
+
project's workflows from any MCP client using a single project API key (`sk_...`).
|
|
30
|
+
`projectId` is never needed — the key defines the project.
|
|
31
|
+
|
|
32
|
+
Get your `sk_` key from the project's **API keys** page in the Assemblix UI.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
### Hosted (easiest — nothing to install)
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
claude mcp add --transport http assemblix https://mcp.assmblx.com \
|
|
40
|
+
--header "Authorization: Bearer sk_your_key"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Local via uvx (privacy / self-host)
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
claude mcp add assemblix \
|
|
47
|
+
--env ASSEMBLIX_API_KEY=sk_your_key \
|
|
48
|
+
--env ASSEMBLIX_API_URL=https://app.assmblx.com \
|
|
49
|
+
-- uvx assemblix-mcp
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Claude Desktop / Cursor (JSON config)
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"mcpServers": {
|
|
57
|
+
"assemblix": {
|
|
58
|
+
"command": "uvx",
|
|
59
|
+
"args": ["assemblix-mcp"],
|
|
60
|
+
"env": {
|
|
61
|
+
"ASSEMBLIX_API_KEY": "sk_your_key",
|
|
62
|
+
"ASSEMBLIX_API_URL": "https://app.assmblx.com"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
For the hosted server, use your client's "add custom connector → URL + header" flow.
|
|
70
|
+
|
|
71
|
+
## Configuration
|
|
72
|
+
|
|
73
|
+
| Env var | Default | Notes |
|
|
74
|
+
| --- | --- | --- |
|
|
75
|
+
| `ASSEMBLIX_API_URL` | `https://app.assmblx.com` | Base URL of your Assemblix instance |
|
|
76
|
+
| `ASSEMBLIX_API_KEY` | — | Project `sk_` key. Required for stdio; for hosted HTTP the `Authorization` header is used instead |
|
|
77
|
+
| `ASSEMBLIX_MCP_TRANSPORT` | `stdio` | `stdio` or `http` |
|
|
78
|
+
| `ASSEMBLIX_MCP_HOST` / `ASSEMBLIX_MCP_PORT` | `0.0.0.0` / `8000` | HTTP transport bind |
|
|
79
|
+
|
|
80
|
+
## Tools
|
|
81
|
+
|
|
82
|
+
**Authoring:** `list_node_types`, `list_workflows`, `get_workflow`, `create_workflow`,
|
|
83
|
+
`update_workflow`, `publish_workflow`
|
|
84
|
+
**Run & inspect:** `run_workflow`, `run_workflow_and_wait`, `get_execution`,
|
|
85
|
+
`list_executions`, `get_execution_detail`, `list_in_flight`
|
|
86
|
+
|
|
87
|
+
**Resources:** `assemblix://examples/minimal`, `assemblix://examples/branching`
|
|
88
|
+
(example workflow JSON). **Prompt:** `author_workflow` (authoring guide).
|
|
89
|
+
|
|
90
|
+
Lifecycle: `create_workflow → update_workflow(nodes, edges) → publish_workflow →
|
|
91
|
+
run_workflow`. Runs always execute the **published** snapshot. AGENT nodes need a
|
|
92
|
+
provider credential configured in the Assemblix UI.
|
|
93
|
+
|
|
94
|
+
All tools operate on the single project your API key is scoped to; `projectId` is
|
|
95
|
+
never a parameter.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
assemblix_mcp/__init__.py,sha256=91rmbF1l62LY9vLutkiElpoRXeG1aiaSJiMWF7w9voE,59
|
|
2
|
+
assemblix_mcp/client.py,sha256=MMi_-iXDadNYCucAijlg4yhmUgQmlqNwm02uN_GZ9B0,6113
|
|
3
|
+
assemblix_mcp/config.py,sha256=qNga8y7KgQF88UHxMDt90c3PEBaoqLjE8odgvMS7b1s,1137
|
|
4
|
+
assemblix_mcp/server.py,sha256=EtXvKX7URtUE0ZulL92PBkISZhma0TXJ0_fkmMSKF4o,1352
|
|
5
|
+
assemblix_mcp/resources/__init__.py,sha256=tNriWtzCwv1iL9mlaV8lqkAvZRjoQER6c8b469NpsQk,2295
|
|
6
|
+
assemblix_mcp/resources/examples/branching.json,sha256=5y1C2Oh2Futdvy9iyFh9sr9uP76vnNXxr6H7QrUQahs,1952
|
|
7
|
+
assemblix_mcp/resources/examples/minimal.json,sha256=bEVDMXQmexjoYFPldUIVi6o9ZYQrrbGFYuiNGGYEaak,957
|
|
8
|
+
assemblix_mcp/tools/__init__.py,sha256=XIKsuG0ZC4LCrRAkWwVoU4QdNU3oKqU1hE0tR5GzSQQ,29
|
|
9
|
+
assemblix_mcp/tools/executions.py,sha256=eaGJ8cemeziHbC1URTFEMNP-CsxJxFa1Tw7dJ2dq1s4,3992
|
|
10
|
+
assemblix_mcp/tools/workflows.py,sha256=UCdlnk9C5SIzMi89blnFL6WdWDK9riwUxpagADxomE8,2787
|
|
11
|
+
assemblix_mcp-0.1.0.dist-info/METADATA,sha256=7DfiVXU3IcI9m3BIjAfHbHlUXt6fnlDok8sxDd77EHQ,3321
|
|
12
|
+
assemblix_mcp-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
13
|
+
assemblix_mcp-0.1.0.dist-info/entry_points.txt,sha256=yfDJw6gku4jI8dxUL28AT2_OS-maBuoRicC2XJMTAb8,60
|
|
14
|
+
assemblix_mcp-0.1.0.dist-info/licenses/LICENSE,sha256=IOH4DMiVqZGtDnhN-KdL7yZvXDRtfqjZCAAcMt3UBNA,1078
|
|
15
|
+
assemblix_mcp-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 the Assemblix authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|