Maestro Helpcenter

Integrations & Interfaces

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

/api/testexecution

Start, abort, poll, and retrieve executions

Test results

/api/testresults

Search and retrieve historical results

Packages

/api/packages

Browse catalog, download, refresh registry

Station config

/api/config

Read and write global and station-local config

YAML validation

/api/yaml/validate

Validate a YAML test definition before committing

System health

/api/health

Check database, Redis, and runner health

System control

/api/system

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:

JSON
{
  "executionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "state": "Running"
}

Poll execution status:

GET /api/testexecution/{executionId}/status

Response:

JSON
{
  "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

StepStarted

Step name, index

Step begins

StepCompleted

Step name, verdict, duration, measurements

Step finishes

StepSkipped

Step name

Precondition evaluated false

MeasurementRecorded

Name, value, limits, verdict

Each measurement point

VariableUpdated

Variable name, value

Variable set or changed

LogMessage

Level, message, timestamp

Log output from runner code

ExecutionCompleted

Execution ID, verdict, duration

Test finishes or aborts

PromptRequested

Title, message, buttons, input config

Prompt step reached

JavaScript / Node.js example:

JavaScript
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):

Python
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 Upgrade and Connection headers are forwarded.


CI/CD Integration

Integration pattern

All CI/CD integrations follow the same three-step pattern:

  1. TriggerPOST /api/testexecution/run with unattendedMode: true

  2. Poll or stream — wait for a terminal state

  3. Assert — non-zero exit code if verdict is not PASS

GitLab CI

YAML
# .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

YAML
# .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)

Groovy
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:

Bash
# 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.).

C#
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.

Python
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

pyvisa

VISA instruments (multimeters, scopes, generators)

pyvisa-py

Pure-Python VISA backend (no NI-VISA required)

pyserial

Serial / RS-232 instruments

python-can

CAN bus communication

numpy, scipy

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:

  1. Run the bridge process as a Windows service on the station host (outside Docker)

  2. Expose a simple REST or gRPC endpoint from the bridge

  3. Call that endpoint from a .NET or Python runner step using standard HTTP or gRPC client code

  4. 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

get_station_info, get_system_health, get_service_logs

Config

get_merged_config, set_config_value

Packages

list_packages, download_package, activate_package, trigger_package_refresh

Execution

start_test, wait_for_test_completion, abort_test, list_available_tests

Results

get_test_report_full, get_execution_logs, search_test_results

YAML

validate_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):

JSON
{
  "mcpServers": {
    "maestro-station": {
      "type": "http",
      "url": "http://localhost:7004/mcp"
    }
  }
}

Claude Desktop (Settings → Developer → Edit Config):

JSON
{
  "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:

JSON
{
  "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

7004

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 7000, 7001, 7004 to known IP addresses

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)

read_registry, read_repository

Docker image pulls during install/upgrade

PAT (stored in DB)

read_registry, read_repository, write_repository

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:

Bash
# 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:

Bash
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 (type: http)

Latest

GitHub Copilot (VS Code)

HTTP via .mcp.json

VS Code 1.90+

GitHub Copilot (Visual Studio)

HTTP via .mcp.json

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 curl in shell script stage

GitHub Actions

REST API via curl in run: step

Jenkins

REST API via HTTP Request plugin or sh curl

Azure DevOps

REST API via bash task

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.