Reference for advanced and technical users connecting Maestro to external systems, instruments, CI/CD pipelines, and AI assistants.
Overview of Integrations
Maestro exposes multiple integration surfaces at different abstraction levels. Choose the one that fits the integration's purpose.
|
Integration surface |
Transport |
Best for |
|---|---|---|
|
REST API |
HTTP/JSON |
CI/CD triggers, MES integration, scripted automation, external dashboards |
|
SignalR hub |
WebSocket |
Real-time event streaming — step progress, measurements, verdicts as they happen |
|
MCP server |
HTTP or stdio |
AI assistant control (Claude, GitHub Copilot, Cursor) |
|
gRPC (internal) |
gRPC / Protocol Buffers |
Building custom test runners — not for external callers |
|
PostgreSQL (direct) |
SQL |
Bulk analytics, SPC, ML training data, custom reporting |
|
GitLab registry |
Git + HTTPS |
Test package distribution, version management, CI/CD promotion |
Which surface to use
Need to trigger a test run? → REST API POST /api/testexecution/run
Need live step-by-step progress? → SignalR hub /hubs/testexecution
Need AI-assisted test authoring? → MCP server :7004/mcp
Need yield trends or SPC queries? → PostgreSQL direct query
Need to deploy a new test package? → GitLab registry + POST /api/packages/refresh
REST API
Base URL and discovery
Each station exposes its own API on port 7000 by default. The API is fully documented via Swagger/OpenAPI:
http://<station-ip>:7000/swagger
Swagger provides an interactive browser where every endpoint can be explored and called. All request/response schemas are documented inline.
Key endpoint groups
|
Group |
Prefix |
Purpose |
|---|---|---|
|
Test execution |
|
Start, abort, poll, and retrieve executions |
|
Test results |
|
Search and retrieve historical results |
|
Packages |
|
Browse catalog, download, refresh registry |
|
Station config |
|
Read and write global and station-local config |
|
YAML validation |
|
Validate a YAML test definition before committing |
|
System health |
|
Check database, Redis, and runner health |
|
System control |
|
Trigger updates, retrieve container logs and events |
Common REST operations
Start a test execution:
POST /api/testexecution/run
Content-Type: application/json
{
"yamlFilePath": "/data/tat-packages/board-test/tests/main.yaml",
"serialNumber": "UNIT-042",
"operatorId": "ci-pipeline",
"stationId": "ST-01",
"unattendedMode": true
}
Response:
{
"executionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"state": "Running"
}
Poll execution status:
GET /api/testexecution/{executionId}/status
Response:
{
"executionId": "3fa85f64-...",
"state": "Completed",
"verdict": "PASS"
}
Terminal states: Completed, Failed, Aborted.
Retrieve the full report:
GET /api/testexecution/{executionId}/report
Returns step-level results, individual measurements (actual value, limits, verdict), artifacts, and the config snapshot captured at execution time.
Validate a YAML definition:
POST /api/yaml/validate
Content-Type: application/json
{
"yamlContent": "<yaml string>"
}
Returns a list of validation errors and their line numbers. An empty errors array means the file is valid.
Discover available test files:
GET /api/packages/test-files
Returns the absolute server-side paths of all installed YAML files. Always use this to resolve file paths before passing them to /api/testexecution/run.
SignalR Real-Time Events
For integrations that need live updates as a test runs — rather than polling — connect to the SignalR hub.
Hub URL:
http://<station-ip>:7000/hubs/testexecution
Events emitted during execution:
|
Event |
Payload |
When emitted |
|---|---|---|
|
|
Step name, index |
Step begins |
|
|
Step name, verdict, duration, measurements |
Step finishes |
|
|
Step name |
Precondition evaluated false |
|
|
Name, value, limits, verdict |
Each measurement point |
|
|
Variable name, value |
Variable set or changed |
|
|
Level, message, timestamp |
Log output from runner code |
|
|
Execution ID, verdict, duration |
Test finishes or aborts |
|
|
Title, message, buttons, input config |
Prompt step reached |
JavaScript / Node.js example:
const { HubConnectionBuilder } = require("@microsoft/signalr");
const connection = new HubConnectionBuilder()
.withUrl("http://192.168.1.50:7000/hubs/testexecution")
.withAutomaticReconnect()
.build();
connection.on("StepCompleted", (step) => {
console.log(`[${step.verdict.padEnd(14)}] ${step.name} (${step.durationMs}ms)`);
});
connection.on("ExecutionCompleted", (execution) => {
console.log(`\nFinal verdict: ${execution.verdict}`);
process.exit(execution.verdict === "PASS" ? 0 : 1);
});
await connection.start();
Python example (using signalrcore):
from signalrcore.hub_connection_builder import HubConnectionBuilder
hub = (HubConnectionBuilder()
.with_url("http://192.168.1.50:7000/hubs/testexecution")
.build())
hub.on("ExecutionCompleted", lambda args: print(f"Verdict: {args[0]['verdict']}"))
hub.start()
SignalR requires WebSocket support. If connecting through a reverse proxy, ensure
UpgradeandConnectionheaders are forwarded.
CI/CD Integration
Integration pattern
All CI/CD integrations follow the same three-step pattern:
-
Trigger —
POST /api/testexecution/runwithunattendedMode: true -
Poll or stream — wait for a terminal state
-
Assert — non-zero exit code if verdict is not
PASS
GitLab CI
# .gitlab-ci.yml
hardware-test:
stage: test
script:
- |
RESULT=$(curl -sf -X POST "http://${MAESTRO_HOST}:7000/api/testexecution/run" \
-H "Content-Type: application/json" \
-d "{
\"yamlFilePath\": \"${TEST_FILE_PATH}\",
\"serialNumber\": \"CI-${CI_PIPELINE_ID}\",
\"operatorId\": \"gitlab-ci\",
\"unattendedMode\": true
}")
EXEC_ID=$(echo "$RESULT" | jq -r .executionId)
- |
for i in $(seq 1 60); do
STATE=$(curl -sf "http://${MAESTRO_HOST}:7000/api/testexecution/${EXEC_ID}/status" | jq -r .state)
echo "[$i] State: $STATE"
[[ "$STATE" =~ ^(Completed|Failed|Aborted)$ ]] && break
sleep 5
done
- |
VERDICT=$(curl -sf "http://${MAESTRO_HOST}:7000/api/testexecution/${EXEC_ID}/report" | jq -r .verdict)
echo "Test verdict: $VERDICT"
[[ "$VERDICT" == "PASS" ]] || exit 1
variables:
MAESTRO_HOST: "192.168.1.50"
TEST_FILE_PATH: "/data/tat-packages/board-test/tests/main.yaml"
timeout: 10 minutes
artifacts:
reports:
# Optionally download and attach the JSON report as a CI artifact
junit: maestro-report.xml
GitHub Actions
# .github/workflows/hardware-test.yml
name: Hardware test
on: [push, workflow_dispatch]
jobs:
hardware-test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Trigger Maestro test
id: trigger
run: |
RESULT=$(curl -sf -X POST "http://${{ vars.MAESTRO_HOST }}:7000/api/testexecution/run" \
-H "Content-Type: application/json" \
-d '{
"yamlFilePath": "${{ vars.TEST_FILE_PATH }}",
"serialNumber": "GH-${{ github.run_id }}",
"operatorId": "github-actions",
"unattendedMode": true
}')
echo "exec_id=$(echo $RESULT | jq -r .executionId)" >> $GITHUB_OUTPUT
- name: Wait for result
run: |
EXEC_ID="${{ steps.trigger.outputs.exec_id }}"
for i in {1..60}; do
STATE=$(curl -sf "http://${{ vars.MAESTRO_HOST }}:7000/api/testexecution/${EXEC_ID}/status" | jq -r .state)
echo "[$i] $STATE"
[[ "$STATE" =~ ^(Completed|Failed|Aborted)$ ]] && break
sleep 5
done
- name: Assert verdict
run: |
EXEC_ID="${{ steps.trigger.outputs.exec_id }}"
VERDICT=$(curl -sf "http://${{ vars.MAESTRO_HOST }}:7000/api/testexecution/${EXEC_ID}/report" | jq -r .verdict)
echo "Verdict: $VERDICT"
[[ "$VERDICT" == "PASS" ]] || (echo "::error::Hardware test failed" && exit 1)
Jenkins (Declarative pipeline)
pipeline {
agent any
environment {
MAESTRO_HOST = '192.168.1.50'
TEST_FILE = '/data/tat-packages/board-test/tests/main.yaml'
}
stages {
stage('Hardware test') {
steps {
script {
def body = """{
"yamlFilePath": "${env.TEST_FILE}",
"serialNumber": "JENKINS-${env.BUILD_NUMBER}",
"operatorId": "jenkins",
"unattendedMode": true
}"""
def response = httpRequest(
url: "http://${env.MAESTRO_HOST}:7000/api/testexecution/run",
httpMode: 'POST',
contentType: 'APPLICATION_JSON',
requestBody: body
)
def execId = readJSON(text: response.content).executionId
env.EXEC_ID = execId
timeout(time: 10, unit: 'MINUTES') {
waitUntil {
def status = httpRequest(
url: "http://${env.MAESTRO_HOST}:7000/api/testexecution/${execId}/status"
)
def state = readJSON(text: status.content).state
return state in ['Completed', 'Failed', 'Aborted']
}
}
}
}
}
stage('Assert verdict') {
steps {
script {
def report = httpRequest(
url: "http://${env.MAESTRO_HOST}:7000/api/testexecution/${env.EXEC_ID}/report"
)
def verdict = readJSON(text: report.content).verdict
if (verdict != 'PASS') {
error("Hardware test failed with verdict: ${verdict}")
}
}
}
}
}
}
The Jenkins pipeline above requires the HTTP Request plugin (
http_request). Install it via Manage Jenkins → Plugins.
Package promotion in CI
To deploy a new test package version as part of a CI pipeline:
# 1. Push the updated package repository (done in your package repo's pipeline)
git push origin main
# 2. Trigger the station to re-scan the registry
curl -sf -X POST "http://${MAESTRO_HOST}:7000/api/packages/refresh"
# 3. Wait for refresh, then install
sleep 5
curl -sf -X POST "http://${MAESTRO_HOST}:7000/api/packages/${PACKAGE_NAME}/download"
Instrument & Hardware Integrations
Maestro itself does not speak to instruments directly — test code in runners does. Maestro's job is to call the right runner method at the right time and evaluate the measurements it returns.
.NET runner — VISA instruments
The .NET runner is the standard choice for instruments that expose a VISA or SCPI interface (multimeters, oscilloscopes, power supplies, spectrum analysers, etc.).
using Ivi.Visa;
public class VoltageTests
{
public Dictionary<string, string> MeasureOutputVoltage(
string cfg_DMM_VISA, // injected from Station Config: "TCPIP::192.168.1.100::INSTR"
string channel)
{
using var session = GlobalResourceManager.Open(cfg_DMM_VISA);
session.FormattedIO.WriteLine($"MEAS:VOLT:DC? (@{channel})");
var voltage = double.Parse(session.FormattedIO.ReadLine());
return new Dictionary<string, string>
{
["voltage"] = voltage.ToString("F4")
};
}
}
The instrument address (cfg_DMM_VISA) comes from Station Config and never appears in the YAML or test code — it is injected automatically at runtime. The same YAML runs on a station with a Keysight DMM or an NI DMM without modification.
Supported VISA backends:
-
NI-VISA (National Instruments)
-
Keysight IO Libraries Suite
-
R&S VISA
-
Any VISA implementation that provides the IVI-COM or .NET VISA interop assembly
Python runner — PyVISA
The Python runner is typically used for instruments with Python-native drivers, scientific libraries (NumPy, SciPy, Pandas), and custom serial/TCP instrument protocols.
import pyvisa
def measure_output_voltage(cfg_dmm_visa: str, channel: str) -> dict:
rm = pyvisa.ResourceManager()
inst = rm.open_resource(cfg_dmm_visa)
inst.write(f"MEAS:VOLT:DC? (@{channel})")
voltage = float(inst.read())
inst.close()
return {"voltage": f"{voltage:.4f}"}
Common Python instrument libraries:
|
Library |
Purpose |
|---|---|
|
|
VISA instruments (multimeters, scopes, generators) |
|
|
Pure-Python VISA backend (no NI-VISA required) |
|
|
Serial / RS-232 instruments |
|
|
CAN bus communication |
|
|
Signal processing and limit evaluation |
Python dependencies are declared in the package's requirements.txt. Maestro installs them automatically into the runner's virtual environment when the package is activated.
Accordion hardware modules
For teams using Accordion hardware boards (power monitors, channel multiplexers, PoE sources), the AccordionPilot desktop application and the AccordionShell CLI provide direct hardware control. Both expose a REST API that can be called from Maestro test steps using standard HTTP libraries.
The Accordion client SDKs (.NET and Python) are available as packages from the same GitLab registry as Maestro itself. Add them as dependencies in your test package and use them in runner code alongside your own instrument drivers.
Hardware that needs a local bridge process
For instruments that require a local process (LabVIEW VIs, proprietary closed-source drivers that cannot run inside Docker), the recommended pattern is:
-
Run the bridge process as a Windows service on the station host (outside Docker)
-
Expose a simple REST or gRPC endpoint from the bridge
-
Call that endpoint from a .NET or Python runner step using standard HTTP or gRPC client code
-
Store the bridge endpoint URL in Station Config so it can vary per station
MCP Server (AI Assistant Integration)
The MCP (Model Context Protocol) server exposes the full Maestro API surface to AI assistants. It runs as a Docker container and is included in the standard station stack.
How it works
AI assistant (Claude, Copilot, Cursor)
│ HTTP MCP (port 7004)
▼
WorkflowEngine.McpServer ← thin proxy, no state
│ REST HTTP
▼
WorkflowEngine.Api ← local per-station API
The MCP server has 41 tools across six categories:
|
Category |
Tools |
Examples |
|---|---|---|
|
System |
7 |
|
|
Config |
— |
|
|
Packages |
— |
|
|
Execution |
— |
|
|
Results |
— |
|
|
YAML |
— |
|
Connecting an AI assistant
The MCP server listens at <http://<station-ip>>:7004/mcp. Add it to your AI client configuration:
.mcp.json (VS Code / GitHub Copilot — place at repo root):
{
"mcpServers": {
"maestro-station": {
"type": "http",
"url": "http://localhost:7004/mcp"
}
}
}
Claude Desktop (Settings → Developer → Edit Config):
{
"mcpServers": {
"maestro-ST01": {
"type": "http",
"url": "http://192.168.1.50:7004/mcp"
}
}
}
Cursor: Add the URL under MCP settings.
CLI: mcp add maestro-st01 <http://192.168.1.50:7004/mcp>
Multi-station AI control
Each station gets its own MCP server entry. The station label is stamped on every tool response so the AI assistant knows which station produced each result:
{
"mcpServers": {
"maestro-ST-01": { "type": "http", "url": "http://192.168.1.50:7004/mcp" },
"maestro-ST-02": { "type": "http", "url": "http://192.168.1.51:7004/mcp" },
"maestro-ST-03": { "type": "http", "url": "http://192.168.1.52:7004/mcp" }
}
}
An AI assistant can target a specific station by name: "On ST-02, run board-test for UNIT-007." It can also start tests on all stations in parallel and collect results when they complete.
MCP network requirements
|
Port |
On |
Allowed from |
|---|---|---|
|
|
Station host |
AI assistant client machine |
If the AI assistant runs on a developer laptop and the station is on a factory LAN, ensure port 7004 is reachable. No inbound ports are needed on the client machine.
Authentication & Access for Integrations
Current state
Maestro does not enforce authentication by default. Any client that can reach the API port can call any endpoint. This is appropriate for isolated station LANs and development environments.
The authentication model is implemented through a thin IPermissionService interface. The default implementation (AllowAllPermissionService) permits every action. A custom implementation — backed by Active Directory, LDAP, OAuth 2.0, or API keys — can be swapped in via dependency injection without changing any calling code.
Securing integrations today
Until API-level authentication is enabled in your deployment, restrict access at the network layer:
|
Approach |
How |
|---|---|
|
LAN isolation |
Place stations on a dedicated VLAN not reachable from the open internet |
|
Firewall rules |
Restrict inbound TCP on ports |
|
Reverse proxy with auth |
Place nginx or Caddy in front of the API and enforce HTTP Basic Auth or mTLS at the proxy |
|
VPN |
Require engineers to be on VPN to reach station ports |
GitLab registry authentication
The Maestro container registry and test package repositories are access-controlled by GitLab. Two token types are used:
|
Token |
Scope |
Used for |
|---|---|---|
|
Personal Access Token (PAT) |
|
Docker image pulls during install/upgrade |
|
PAT (stored in DB) |
|
Package download and publish from within Maestro |
PATsare user-scoped. For automated pipelines, create a dedicated service account in GitLab and generate a token for it. Do not embed personal tokens in CI/CD configuration.
CI/CD GitLab token configuration:
# Store as a CI/CD masked variable — not plaintext
# GitLab: Settings → CI/CD → Variables → MAESTRO_PACKAGE_TOKEN
Then pass it to the package registry endpoint:
curl -sf -X POST "http://${MAESTRO_HOST}:7000/api/packages/${PACKAGE_NAME}/download" \
-H "X-GitLab-Token: ${MAESTRO_PACKAGE_TOKEN}"
Planned authentication features
API-level role enforcement (Operator, Engineer, Admin) is on the Maestro roadmap. When released, it will gate all REST API endpoints and MCP tool calls against the caller's role. The IPermissionService interface is already wired throughout the codebase — enabling enforcement is an implementation swap, not an architectural change.
Version Compatibility Matrix
Component versions (current release)
|
Component |
Version |
Notes |
|---|---|---|
|
Maestro API |
http://ASP.NET Core 10 |
Kestrel HTTP server |
|
Maestro UI |
Blazor Server (.NET 10) |
|
|
.NET Runner (modern) |
.NET 10 |
64-bit, arm64/amd64 |
|
.NET Runner (legacy) |
.NET Framework 4.8 |
32-bit Windows only; for legacy instrument COM/interop |
|
Python Runner |
Python 3.11 – 3.12 |
|
|
PostgreSQL |
15, 16 |
|
|
Redis |
7.x |
|
|
gRPC transport |
protobuf 3 |
|
|
MCP protocol |
MCP 2024-11-05 |
HTTP transport, stdio transport |
Supported AI assistant clients (MCP)
|
Client |
Transport |
Minimum version |
|---|---|---|
|
Claude Desktop |
HTTP ( |
Latest |
|
GitHub Copilot (VS Code) |
HTTP via |
VS Code 1.90+ |
|
GitHub Copilot (Visual Studio) |
HTTP via |
VS 2022 17.10+ |
|
Cursor |
HTTP |
Latest |
|
Any MCP-compatible client |
stdio or HTTP |
MCP spec 2024-11-05+ |
Supported CI/CD platforms
Any CI/CD system that can execute curl and jq can integrate with Maestro. The following have been tested:
|
Platform |
Integration method |
|---|---|
|
GitLab CI |
REST API via |
|
GitHub Actions |
REST API via |
|
Jenkins |
REST API via HTTP Request plugin or |
|
Azure DevOps |
REST API via |
|
TeamCity |
REST API via build step |
|
Buildkite |
REST API via command step |
API stability
The Maestro REST API follows semantic versioning. Breaking changes (removed endpoints, changed response schemas) are announced with a major version bump. The API version is returned by GET /api/health/version and by the MCP get_system_version tool.
Runner versions are independent of the API version. Check runner versions via GET /api/health/version — each registered runner reports its own version string. Runner and API versions should be kept in sync within the same minor release line; mixing major versions may cause gRPC contract errors at startup.
Upgrade compatibility
|
Scenario |
Supported |
|---|---|
|
Station on version N, central DB on version N |
✅ Standard |
|
Station on version N, central DB on version N+1 |
⚠️ DB migrations have run; station may miss new features |
|
Station on version N+1, central DB on version N |
❌ Station startup will fail — DB migrations not yet applied |
Always upgrade the central server (and run DB migrations) before upgrading individual stations. During a rolling upgrade, keep all stations offline until the DB is fully migrated.