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.
@@ -0,0 +1,3 @@
1
+ """Assemblix MCP server package."""
2
+
3
+ __version__ = "0.1.0"
@@ -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
@@ -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
+ }
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ assemblix-mcp = assemblix_mcp.server:main
@@ -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.