This repository packages a standalone Java web service for SysML v2 graphical rendering and textual editing against a SysML v2 API backend.
The service is designed to run well in containers. The browser only talks to this service. The service talks to the remote SysML API, so it can be deployed behind JupyterLab or another proxy without requiring direct browser access to the SysML backend.
The service supports two deployment modes:
standaloneThe user can provide the backend base URL and bearer token in the UI.embeddedIntended for iframe deployment in JupyterLab or a similar shell. The backend base URL and bearer token come from environment variables, and the UI hides those fields.
The service has two browser UIs:
/for the graphical visualizer/editorfor textual load, edit, validate, commit, and project creation
It also exposes HTTP endpoints for:
- graphical rendering
- textual model load/readback
- textual validation
- full replacement commit
- parsed JSON export (local Pilot parse → downloadable DataVersion JSON)
- Flexo element list download (live fetch from the remote SysML API)
- project listing and creation
- branch listing and creation
The Docker image already contains:
- the Java server
- the Pilot jars
- the bundled
sysml.library - Python runtime for the commit and project-create bridge scripts
The main pieces are:
app/SysMLVizServer.javaHTTP server, routing, logging, render flow, project lookup, bridge execution, and shared helpers.app/TextualModelService.javaTextual load/resolve/serialize logic, top-level fallback resolution, validation, and commit preparation.app/sysml_replace_commit.pyPosts full replacement commits to the remote SysML v2 API.app/sysml_create_project.pyCreates projects through the remote SysML v2 API.app/static/index.htmlGraphical viewer UI.app/static/editor.htmlTextual editor UI.pilot-jars/Pilot runtime jars.sysml.library/Bundled SysML standard library used for textual validation and commit parsing.
The service:
- loads a model from a remote project and branch
- resolves the requested element, or a top-level element if blank
- renders SVG, PlantUML, or text using the Pilot visualizer
The service:
- loads a model from a remote project and branch
- resolves the requested element, or a top-level named element if blank
- tries Xtext serialization
- falls back to generated SysML-like text when repository-loaded EMF objects cannot be serialized safely
The service:
- loads the bundled standard libraries
- parses edited SysML text locally
- returns syntax and semantic diagnostics as JSON
The service:
- loads the bundled standard libraries
- parses and validates the edited text locally
- converts the parsed root element to the API change payload
- posts a full replacement commit to the remote SysML v2 API
The service:
- loads the bundled standard libraries
- parses and validates the edited SysML text locally
- converts the parsed root element to the Flexo DataVersion JSON format
- returns the JSON array as a downloadable
model.jsonfile
The output format is identical to the change payload used by the replacement commit flow.
The service:
- resolves the head commit from the selected branch via the Flexo API
- fetches all elements for that commit from
GET /projects/{id}/commits/{commitId}/elements - returns the raw Flexo response as a downloadable
elements.jsonfile
The service can:
- list projects from the remote backend
- create a new project from the textual editor UI
GET /Serves the graphical viewer UI.GET /editorServes the textual editor UI.GET /healthHealth check, returnsok.GET /configReturns default server-side configuration as JSON.GET /logsReturns in-memory server logs as JSON.POST /projectsLists projects from the remote SysML API.POST /projects/createCreates a project on the remote SysML API.POST /branchesLists branches for a project from the remote SysML API.POST /branches/createCreates a branch from an existing branch on the remote SysML API.GET /renderLoads a remote model and renders an element.GET /textualLoads a remote model and returns textual SysML for an element.POST /textual/validateParses and validates edited SysML text.POST /textual/commitParses, validates, builds change payload, and posts a replacement commit.POST /textual/jsonParses SysML text locally and returns the DataVersion JSON as a downloadable file.POST /elementsResolves the branch head commit and downloads all elements from the Flexo API as JSON.POST /renderJsonIntended for JSON-backed rendering. This path is still limited by the current Pilot jars becauseSysMLInteractive.loadJsonModel(...)is not present in the shipped runtime.
Request body:
{
"apiBase": "https://experimental.starforge.app/",
"bearerToken": "Bearer ..."
}Response:
{
"count": 2,
"projects": [
{ "id": "uuid-1", "name": "Project A" },
{ "id": "uuid-2", "name": "Project B" }
]
}Request body:
{
"apiBase": "https://experimental.starforge.app/",
"bearerToken": "Bearer ...",
"projectName": "test_rka_002",
"description": "A project created via the editor"
}Request body:
{
"apiBase": "https://experimental.starforge.app/",
"bearerToken": "Bearer ...",
"projectId": "uuid-of-project"
}Response:
{
"count": 2,
"branches": [
{ "id": "uuid-1", "name": "Initial" },
{ "id": "uuid-2", "name": "feature-x" }
]
}Request body:
{
"apiBase": "https://experimental.starforge.app/",
"bearerToken": "Bearer ...",
"projectId": "uuid-of-project",
"branchName": "my-new-branch",
"fromBranchId": "uuid-of-source-branch"
}Returns the created branch object from the remote API.
Query parameters:
apiBaseprojectIdorprojectNamebranchIdorbranchNameelementformat=svg|plantuml|textviewstyle
Example:
/render?apiBase=https://experimental.starforge.app/&projectId=...&element=pkgA&format=svg
Query parameters:
apiBaseprojectIdorprojectNamebranchIdorbranchNameelement
If element is blank, the service selects a top-level named element.
Example:
/textual?apiBase=https://experimental.starforge.app/&projectId=...&element=pkgA
Request body:
{
"modelText": "package pkgA { part p; }"
}Response fields include:
okhasErrorshasWarningsissuessyntaxErrorssemanticErrorswarnings- root element metadata when available
Request body:
{
"apiBase": "https://experimental.starforge.app/",
"bearerToken": "Bearer ...",
"projectId": "...",
"branchName": "",
"modelText": "package pkgA { part p; }",
"commitMessage": "Replace model from textual editor"
}This posts a full-model replacement commit, not a patch.
Request body:
{
"modelText": "package pkgA { part p; }"
}Response: HTTP 200, Content-Type: application/json, Content-Disposition: attachment; filename="model.json".
The body is a JSON array of DataVersion objects — the same format as the change payload used by POST /textual/commit. Returns HTTP 400 if the model text is missing or has parse errors.
Request body:
{
"apiBase": "https://experimental.starforge.app/",
"bearerToken": "Bearer ...",
"projectId": "uuid-of-project",
"branchId": "uuid-of-branch"
}Response: HTTP 200, Content-Type: application/json, Content-Disposition: attachment; filename="elements.json".
The body is the raw Flexo API response from GET /projects/{projectId}/commits/{commitId}/elements. The server resolves the head commit from the branch automatically. Returns HTTP 400 for missing parameters or HTTP 502 if the Flexo API call fails.
The following example shows how to look up a project and branch by name and then fetch a rendered SVG diagram using the requests library.
GET /render does not accept a bearer token. The Pilot library's internal load() call handles backend authentication using the API base URL directly. SYSML_API_TOKEN is used only by the server-side Python bridge scripts that handle mutation operations (project creation, replacement commit) — it is not involved in model loading or rendering.
The bearer token you pass to POST /projects and POST /branches is forwarded to the remote SysML API for those calls only.
import requests
SERVER = "http://localhost:8088" # base URL of the sysmlviz server
FLEXO_API = "https://experimental.starforge.app/"
BEARER_TOKEN = "Bearer eyJ..." # your Flexo JWT
def get_project_id(project_name: str) -> str:
"""Resolve a project name to its UUID."""
resp = requests.post(
f"{SERVER}/projects",
json={"apiBase": FLEXO_API, "bearerToken": BEARER_TOKEN},
timeout=30,
)
resp.raise_for_status()
projects = resp.json()["projects"]
match = next((p for p in projects if p.get("name") == project_name), None)
if match is None:
raise ValueError(f"Project '{project_name}' not found")
return match["id"]
def get_branch_id(project_id: str, branch_name: str) -> str:
"""Resolve a branch name to its UUID within a project."""
resp = requests.post(
f"{SERVER}/branches",
json={"apiBase": FLEXO_API, "bearerToken": BEARER_TOKEN, "projectId": project_id},
timeout=30,
)
resp.raise_for_status()
branches = resp.json()["branches"]
match = next((b for b in branches if b.get("name") == branch_name), None)
if match is None:
raise ValueError(f"Branch '{branch_name}' not found in project {project_id}")
return match["id"]
def get_svg(project_id: str, branch_id: str, element: str = "") -> str:
"""
Fetch the rendered SVG for an element in a given project and branch.
Args:
project_id: UUID of the Flexo project.
branch_id: UUID of the branch.
element: Qualified name of the element to render, e.g.
'FlashlightModel::FlashlightSpecificationAndDesign'.
Pass an empty string to render the top-level element.
Returns:
SVG XML as a string.
"""
params = {
"apiBase": FLEXO_API,
"projectId": project_id,
"branchId": branch_id,
"element": element,
"format": "svg",
}
resp = requests.get(f"{SERVER}/render", params=params, timeout=300)
resp.raise_for_status()
return resp.text
# ── Example usage ─────────────────────────────────────────────────────────────
project_id = get_project_id("MyFlashlightProject")
branch_id = get_branch_id(project_id, "main")
svg = get_svg(
project_id=project_id,
branch_id=branch_id,
element="FlashlightModel::FlashlightSpecificationAndDesign",
)
# Write to file
with open("diagram.svg", "w", encoding="utf-8") as f:
f.write(svg)
# Or embed directly in a Jupyter notebook
from IPython.display import SVG, display
display(SVG(svg))- Render timeout: The server allows up to 3 minutes per render. Set
timeout=300on theGET /rendercall. elementparameter: Use the fully qualified SysML name (PackageA::SubPackage::MyBlock). If omitted or blank the server renders the top-level element of the model.formatparameter: Supportssvg(default),plantuml/puml, andtext/txt.- Known project/branch IDs: If you already have the UUIDs (e.g. from a previous call or from the Flexo API directly) you can skip the discovery calls and invoke
get_svgdirectly.
GET /textual loads the same model as /render but returns the SysML v2 textual representation of the element instead of a diagram. The discovery helpers (get_project_id, get_branch_id) from the SVG example above are reused here.
import requests
SERVER = "http://localhost:8088"
FLEXO_API = "https://experimental.starforge.app/"
BEARER_TOKEN = "Bearer eyJ..."
def get_project_id(project_name: str) -> str:
resp = requests.post(
f"{SERVER}/projects",
json={"apiBase": FLEXO_API, "bearerToken": BEARER_TOKEN},
timeout=30,
)
resp.raise_for_status()
projects = resp.json()["projects"]
match = next((p for p in projects if p.get("name") == project_name), None)
if match is None:
raise ValueError(f"Project '{project_name}' not found")
return match["id"]
def get_branch_id(project_id: str, branch_name: str) -> str:
resp = requests.post(
f"{SERVER}/branches",
json={"apiBase": FLEXO_API, "bearerToken": BEARER_TOKEN, "projectId": project_id},
timeout=30,
)
resp.raise_for_status()
branches = resp.json()["branches"]
match = next((b for b in branches if b.get("name") == branch_name), None)
if match is None:
raise ValueError(f"Branch '{branch_name}' not found in project {project_id}")
return match["id"]
def get_sysml_text(project_id: str, branch_id: str, element: str = "") -> str:
"""
Fetch the SysML v2 textual representation of an element.
Args:
project_id: UUID of the Flexo project.
branch_id: UUID of the branch.
element: Qualified name of the element, e.g.
'FlashlightModel::FlashlightSpecificationAndDesign'.
Pass an empty string to retrieve the top-level element.
Returns:
SysML v2 text as a string.
"""
params = {
"apiBase": FLEXO_API,
"projectId": project_id,
"branchId": branch_id,
"element": element,
}
resp = requests.get(f"{SERVER}/textual", params=params, timeout=300)
resp.raise_for_status()
return resp.text
# ── Example usage ─────────────────────────────────────────────────────────────
project_id = get_project_id("MyFlashlightProject")
branch_id = get_branch_id(project_id, "main")
text = get_sysml_text(
project_id=project_id,
branch_id=branch_id,
element="FlashlightModel::FlashlightSpecificationAndDesign",
)
print(text)
# Write to file
with open("model.sysml", "w", encoding="utf-8") as f:
f.write(text)elementparameter: Same qualified-name convention as/render. If omitted, the server resolves the top-level named element in the model.- Fallback serialization: For models loaded from a remote repository, Xtext serialization may not always succeed. In that case the server returns generated SysML-like text that preserves the model structure but may differ from the original source formatting.
- Timeout: Allow up to 3 minutes (
timeout=300) — the load and serialization step can be slow for large models.
POST /textual/json parses SysML text using the Pilot runtime and returns a JSON array of DataVersion objects — the same format the server sends to Flexo during a replacement commit. This is useful for inspecting the parsed AST or for offline processing without pushing to a repository.
import requests
SERVER = "http://localhost:8088"
MODEL_TEXT = """
package FlashlightModel {
part def Flashlight {
part battery : Battery;
}
part def Battery;
}
"""
def export_parsed_json(model_text: str) -> list:
"""Parse SysML text and return it as a list of DataVersion JSON objects."""
resp = requests.post(
f"{SERVER}/textual/json",
json={"modelText": model_text},
timeout=180,
)
resp.raise_for_status()
return resp.json()
elements = export_parsed_json(MODEL_TEXT)
print(f"Exported {len(elements)} DataVersion objects")
# Write to file
import json
with open("model.json", "w", encoding="utf-8") as f:
json.dump(elements, f, indent=2)- The response body is a JSON array, not a JSON object.
- The endpoint returns HTTP 400 if the model text has parse errors. Check the error message for details.
- This endpoint does not interact with a remote SysML API — it only uses the locally bundled Pilot runtime and standard library.
POST /elements fetches all elements for a given project and branch directly from the Flexo API and returns them as a downloadable JSON file. The server resolves the branch's head commit automatically.
import json
import requests
SERVER = "http://localhost:8088"
FLEXO_API = "https://experimental.starforge.app/"
BEARER_TOKEN = "Bearer eyJ..."
def get_project_id(project_name: str) -> str:
resp = requests.post(
f"{SERVER}/projects",
json={"apiBase": FLEXO_API, "bearerToken": BEARER_TOKEN},
timeout=30,
)
resp.raise_for_status()
projects = resp.json()["projects"]
match = next((p for p in projects if p.get("name") == project_name), None)
if match is None:
raise ValueError(f"Project '{project_name}' not found")
return match["id"]
def get_branch_id(project_id: str, branch_name: str) -> str:
resp = requests.post(
f"{SERVER}/branches",
json={"apiBase": FLEXO_API, "bearerToken": BEARER_TOKEN, "projectId": project_id},
timeout=30,
)
resp.raise_for_status()
branches = resp.json()["branches"]
match = next((b for b in branches if b.get("name") == branch_name), None)
if match is None:
raise ValueError(f"Branch '{branch_name}' not found in project {project_id}")
return match["id"]
def download_elements(project_id: str, branch_id: str) -> list:
"""Fetch all elements for the branch's head commit from Flexo."""
resp = requests.post(
f"{SERVER}/elements",
json={
"apiBase": FLEXO_API,
"bearerToken": BEARER_TOKEN,
"projectId": project_id,
"branchId": branch_id,
},
timeout=300,
)
resp.raise_for_status()
return resp.json()
# ── Example usage ─────────────────────────────────────────────────────────────
project_id = get_project_id("MyFlashlightProject")
branch_id = get_branch_id(project_id, "main")
elements = download_elements(project_id, branch_id)
print(f"Downloaded {len(elements)} elements")
with open("elements.json", "w", encoding="utf-8") as f:
json.dump(elements, f, indent=2)- The server resolves the branch's head commit internally; you only need to supply the branch UUID.
- Large models may take tens of seconds. Use
timeout=300or higher. - The response is the raw Flexo API payload and may be a JSON array or a JSON object with an
elementskey depending on your Flexo version.
The service listens on PORT, default 8088.
Supported environment variables:
PORTHTTP port for the service. Default:8088SYSML_API_BASEBackend base URL. In standalone mode it prefills the UI. In embedded mode it is the effective backend base URL used by the server.SYSML_UI_MODEDeployment mode. Supported values:standaloneorembedded. Default:standaloneSYSML_ALLOW_UI_API_OVERRIDEWhether browser-suppliedapiBasevalues may override the environment default. Default:truein standalone mode,falsein embedded modePYTHON_BINPython executable used by the bridge scripts. Default in Docker:python3SYSML_RENDER_TIMEOUT_MSTimeout for graphical rendering requests. Default:180000SYSML_TEXTUAL_TIMEOUT_MSTimeout for textual load/readback requests. Default:180000SYSML_COMMIT_TIMEOUT_MSTimeout for the server-side commit workflow. Default:300000SYSML_COMMIT_HTTP_TIMEOUT_SECHTTP timeout used by the Python commit bridge. Default:300SYSML_API_TOKENServer-side bearer token used by the Python-client-backed mutation flows such as project creation and replacement commit. In embedded mode this should be supplied by the environment instead of the UI.SYSML_LIBRARY_PATHPath to the bundled SysML standard library directory. Default in Docker:/opt/sysml.library. Override only if mounting an external library.
The Docker image already contains the standard library and uses /opt/sysml.library internally. No external mount is required.
- Java 21 (Temurin recommended)
- Maven 3.9+ (required only for building pilot JARs from source)
- Python 3.9+
- Docker
The service depends on the SysML v2 Pilot Implementation (LGPL-3.0-or-later). The pinned version is recorded in .pilot-version.
Step 1 — clone the pilot source at the pinned tag:
PILOT_REF=$(grep -v '^#' .pilot-version | tr -d '[:space:]')
git clone --depth=1 --branch "$PILOT_REF" \
https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation.git \
/tmp/pilot-srcStep 2 — build with Maven (Eclipse Tycho; first run downloads Eclipse p2 deps — allow 30–60 min):
cd /tmp/pilot-src
mvn clean package -DskipTests -BStep 3 — collect the runtime JARs:
mkdir -p pilot-jars
find /tmp/pilot-src \
-name "*.jar" \
! -name "*-sources.jar" \
! -name "*-javadoc.jar" \
! -path "*/test-classes/*" \
-exec cp -n {} pilot-jars/ \;Step 4 — sync the standard library (optional but recommended):
rm -rf sysml.library
cp -r /tmp/pilot-src/sysml.library sysml.library/After these steps, pilot-jars/ and sysml.library/ are ready for the Docker build and local tests.
A convenience script build-from-pilot.sh in the repo root runs all four steps, then the tests, and then builds both Docker images.
docker build \
-t sysml-viz-starforge:latest \
-f Dockerfile \
.The OpenMBEE image is built from the same Java backend but with the OpenMBEE HTML/CSS overrides applied on top of the app/static/ assets:
docker build \
-t sysml-viz-openmbee:latest \
-f Dockerfile.openmbee \
.Both Dockerfiles:
- read the pinned Pilot ref from
.pilot-version - clone and build the upstream Pilot implementation inside the Docker build
- compile the Java sources against those generated jars
- bundle the runtime jars and
sysml.library/ - install Graphviz and Python in the runtime image
Local smoke tests still require pilot-jars/ to be populated ahead of time. The Docker image build no longer depends on committed pilot-jars/ or a committed sysml.library/.
Run locally:
docker run --name sysml-viz --rm -p 8088:8088 sysml-viz-serviceThen open:
http://localhost:8088/http://localhost:8088/editor
You can also preconfigure the backend base URL:
docker run --name sysml-viz --rm -p 8088:8088 `
-e SYSML_API_BASE=https://experimental.starforge.app/ `
sysml-viz-serviceRun in embedded mode:
docker run --name sysml-viz --rm -p 8088:8088 `
-e SYSML_API_BASE=http://sysmlv2-service-svc.<ns>.svc.cluster.local:8080 `
-e SYSML_API_TOKEN="Bearer ..." `
-e SYSML_UI_MODE=embedded `
-e SYSML_ALLOW_UI_API_OVERRIDE=false `
sysml-viz-serviceThe / UI lets the user:
- enter the backend base URL in standalone mode
- enter a bearer token in standalone mode
- load projects
- select a project
- set branch and element
- render SVG, PlantUML, or text
The /editor UI lets the user:
- enter the backend base URL in standalone mode
- enter a bearer token in standalone mode
- load projects
- create a project
- select a project
- set branch and root element
- load text
- edit text with lightweight keyword highlighting
- validate
- commit a full replacement model
- export the parsed model as a downloadable JSON file (DataVersion format)
- download all elements for the selected project/branch directly from Flexo as JSON
- open the graphical viewer for the same selection
The browser only needs to reach this service. This service must be able to reach the SysML backend over the container or host network.
Use an internal service hostname for SYSML_API_BASE, not a browser-only URL, when running in a composed environment.
The usual pattern is:
- run this service as a sidecar or separate service
- proxy it through JupyterLab
- configure
SYSML_API_BASEto the backend hostname reachable from the service - open the proxied viewer/editor URL from JupyterLab
This avoids requiring the browser to talk directly to the SysML backend.
For embedded mode, a typical environment setup is:
PORT=8088
SYSML_API_BASE=http://sysmlv2-service-svc.<ns>.svc.cluster.local:8080
SYSML_API_TOKEN=<from secret>
SYSML_UI_MODE=embedded
SYSML_ALLOW_UI_API_OVERRIDE=false
In this mode:
- the API base URL field is hidden
- the bearer token field is hidden
- the server uses environment-provided values instead of asking the user
Large replacement commits can still fail upstream if the remote SysML API times out behind Cloudflare or another proxy.
When that happens, the service now:
- returns a clearer error message for upstream timeout cases
- preserves the generated commit payload file on failure
- logs the preserved payload path for retry and debugging
This does not eliminate upstream 524 or similar timeout responses, but it makes the failure mode easier to understand and retry.
POST /renderJsonis still limited by the supplied Pilot runtime jars.- Repository-loaded models are not always serializer-safe for Xtext round-tripping, so
/textualmay return fallback SysML-like text for some remote-loaded elements. - Full replacement commits depend on the performance and timeout behavior of the remote SysML API service.
app/
SysMLVizServer.java
TextualModelService.java
sysml_replace_commit.py
sysml_create_project.py
static/
index.html ← Starforge-branded graphical viewer
editor.html ← Starforge-branded textual editor
starforge-logo.png
openmbee-logo.svg
pu-logo.png
openmbee-export/
app/static/
index.html ← OpenMBEE community graphical viewer (unbranded)
editor.html ← OpenMBEE community textual editor (unbranded)
openmbee-logo.svg
pilot-jars/
*.jar ← populated by the pilot build step
sysml.library/
... ← synced from pilot source tree
tests/
test_smoke.py
java/
.pilot-version ← pinned SysML v2 Pilot Implementation git tag
build-from-pilot.sh ← end-to-end build script (pilot → tests → Docker)
Dockerfile ← Starforge image
Dockerfile.openmbee ← OpenMBEE community image
Requires pilot-jars/ to be populated first (see Building the Pilot JARs from Source).
The test runner compiles all Java sources against pilot-jars/ at startup, starts the server on a free port, and tears it down after the run — no separate server setup needed.
The suite covers:
- route availability and basic request validation for all endpoints
- project and branch listing and creation endpoint validation
POST /textual/jsonbody and modelText validationPOST /elementsbody and parameter validation- parsed JSON export: non-empty DataVersion array for valid model, error for invalid SysML
- top-level textual resolution
- resolution tie-breaking
- serializer fallback behavior
- node-model text serialization
- validation JSON shape
- bridge helper failure handling
- feature-chain failure normalization
- branch list name correction for the Flexo API name/UUID mismatch
python -m unittest discover -s tests -vIf /render or /textual returns Missing apiBase:
- set
SYSML_API_BASE, or provideapiBasein the request/UI
If /render or /textual returns Provide projectName or projectId:
- provide a project identifier
If textual validation fails on standard library types:
- make sure you are running the current image, which already includes
sysml.library
If a large commit fails with an upstream timeout:
- the model likely parsed successfully locally
- the remote backend did not finish the replacement commit in time
- check the server logs for the preserved payload file path
If the UI loads but nothing renders:
- verify the service can reach the backend
- verify the selected project and branch exist
- check
/logsor the container logs for the detailed server trace