Search docs.molo17.com
    Integration guide Control Plane

    Automate Gluesync with the REST API, MCP, and webhooks

    Gluesync can be driven from scripts, from an automation tool, and from an AI assistant. This guide creates a Personal API Token, uses it to list pipelines and to start, stop, and measure an entity over the REST API, routes Core Hub webhooks into n8n to send alerts and restart a failed entity, and connects an MCP client, including the case where the endpoint answers 403 Invalid Host behind your own hostname.

    In this guide

    You will learn how to create a token with the scopes a job needs, call the REST API and read its error codes, receive platform webhooks in n8n and route an alert to email and to a chat-style endpoint, let n8n call the REST API to restart a failed entity, connect an MCP client with a token, and allow a custom hostname on the MCP endpoint.

    Quick information

    Goal

    Automate Gluesync operations and alerts over the REST API, webhooks, and MCP, with tokens that can do only what each job needs

    Audience

    Gluesync administrators, DevOps engineers, and integration developers

    Level

    Advanced

    Time required

    About 90 minutes

    Area

    Control Plane → Settings → Account → Personal API Tokens; Core Hub REST API; Core Hub MCP server

    Applies to

    Gluesync 2.3

    Before you begin

    Make sure that:

    1. You can sign in to the Control Plane with a user that can create Personal API Tokens, and you can reach Core Hub over HTTPS from the machine where you run curl;

    2. A pipeline with at least two entities exists. This guide uses a pipeline called Automation demo with the entities customers, orders, and heartbeat, and a pipeline that is not in production, because the guide starts and stops entities;

    3. For the alert part, you have Docker on the host that runs Gluesync, an SMTP server that Core Hub and n8n can reach (the guide uses Mailpit), and an HTTP endpoint that accepts a JSON POST (the guide uses webhook-tester as a stand-in for a Slack-style incoming webhook);

    4. For the last part, you have a Core Hub behind a reverse proxy and a hostname that you control. If you want to rehearse first, run a second Core Hub of your own, so that you never change the settings of the one you use every day;

    5. curl and jq are installed.

    Commands use $GLUESYNC_URL for the address of Core Hub (here https://localhost) and $GLUESYNC_TOKEN for a token. The kit certificate is self-signed, so the examples use curl -k. Remove -k once Core Hub serves a certificate that your machine trusts: see HTTPS and WSS encryption.

    Guide steps

    Part 1: Call the REST API with a Personal API Token

    Create a Personal API Token

    UI

    A Personal API Token lets a script act as you without your password. Each token carries permission scopes, and Core Hub enforces them on every request.

    1. Select the Settings gear icon in the header, then Account, then the Personal API Tokens tab;

    2. Select New token.

    The Personal API Tokens tab with the list of existing tokens and the New token button marked by annotation 1
    Figure 1. Open Personal API Tokens and select New token (1).
    1. In Token name, enter automation-api;

    2. Keep Expiry at 30 days, or choose 7 days, 90 days, or Never;

    3. Under Permission scopes, select Full API. This job needs to start and stop entities. A job that only reads would use API read;

    4. Select Create.

    The New Personal API Token dialog with the Token name field marked by annotation 1
    Figure 2. Name (1), expiry (2), and the Full API scope (3).
    1. Select Copy (2) and keep the token (1) in your secret store. Core Hub shows it only once and keeps only a hash. Then select I’ve copied it, close.

    The Your new API token dialog with the token value marked by annotation 1 and the Copy button marked by annotation 2
    Figure 3. The token is shown once. The value in this screenshot is a placeholder.
    Expected result

    The list shows a new row automation-api with the preview gsp_…, the permission Full API, an expiry date, and the status Active. The Last used column stays empty until the first request.

    Tokens cannot create, revoke, or delete other tokens, whatever their scopes. Create and delete tokens from a Control Plane session.

    Store the token and check what it can do

    CLI

    1. In your shell, store the address and the token. Use a secret manager or an environment file that only you can read, not a script that goes into a repository:

      export GLUESYNC_URL=https://localhost
      export GLUESYNC_TOKEN='gsp_…'     # the value you copied
    2. Ask Core Hub who the token belongs to and which scopes it has:

      curl -sk "$GLUESYNC_URL/authentication/me" \
        -H "Authorization: Bearer $GLUESYNC_TOKEN"
      curl -sk "$GLUESYNC_URL/api-tokens/current-scope" \
        -H "Authorization: Bearer $GLUESYNC_TOKEN"
    Expected result

    The first call answers with your user and role, and the second lists the scope of the token:

    {"id":"3ff2ba9b","username":"admin","role":"SUPER_ADMIN","changeRequired":false,"isOidcUser":false,...}
    {"scopes":[],"scoped":false,"permissionScopes":["gluesync.api"],"legacyFullAccess":false,"personalApiToken":true}

    "scoped":false means that the token is not limited to some pipelines or agents. Both calls work with any scope.

    List pipelines and read entity status

    API

    1. List the pipelines and find the ID of Automation demo:

      curl -sk "$GLUESYNC_URL/pipelines" -H "Authorization: Bearer $GLUESYNC_TOKEN" \
        | jq -r '.[] | select(.name=="Automation demo") | "\(.id)  \(.name)"'
    2. Keep the ID in a variable and list the entities of the pipeline. Commands take the entity ID, not its name:

      export PIPELINE_ID=9ae3b3bd
      curl -sk "$GLUESYNC_URL/pipelines/$PIPELINE_ID/entities" \
        -H "Authorization: Bearer $GLUESYNC_TOKEN" \
        | jq -r '.[] | "\(.entity.entityId)  \(.entity.entityName)"'
    3. Read the runtime status of the entities:

      curl -sk "$GLUESYNC_URL/pipelines/$PIPELINE_ID/entities-status" \
        -H "Authorization: Bearer $GLUESYNC_TOKEN" \
        | jq -c '.[] | {entityId, isSyncActive, isMigrationActive, isBusy, error: (.errorState.errorType // null)}'
    Expected result

    The first command prints 9ae3b3bd Automation demo (your ID differs). The second prints one line per entity:

    3198138e  customers
    8f2edc67  orders
    a4c72ae5  heartbeat

    The third prints a line for each entity that is running. An entity that has never been started is not listed. A failing entity carries an errorState object, shown above as error, with a type such as target_write_failed, a message, and occurredAtMs:

    {"entityId":"3198138e","isSyncActive":true,"isMigrationActive":false,"isBusy":false,"error":null}
    Start and stop an entity

    API

    Pass the entity ID in the repeatable entity query parameter. The example loads orders with a snapshot, then stops it.

    1. Start the entity with a snapshot:

      export ENTITY_ID=8f2edc67
      curl -sk -X POST "$GLUESYNC_URL/pipelines/$PIPELINE_ID/commands/sync/start?entity=$ENTITY_ID&withSnapshot=true" \
        -H "Authorization: Bearer $GLUESYNC_TOKEN"
    2. Wait a few seconds, then read the status again:

      curl -sk "$GLUESYNC_URL/pipelines/$PIPELINE_ID/entities-status" \
        -H "Authorization: Bearer $GLUESYNC_TOKEN" \
        | jq -c '.[] | select(.entityId=="'$ENTITY_ID'") | {entityId, isSyncActive}'
    3. Stop the entity:

      curl -sk -X POST "$GLUESYNC_URL/pipelines/$PIPELINE_ID/commands/sync/stop?entity=$ENTITY_ID" \
        -H "Authorization: Bearer $GLUESYNC_TOKEN"
    Expected result

    Each command answers 202 with a plain-text message, Start command received and dispatched to agents and Stop command received and dispatched to agents. After the start, the status shows "isSyncActive":true. After the stop, it shows "isSyncActive":false. In the Control Plane, the entity moves between Active and Hold.

    202 Accepted means that Core Hub dispatched the command, not that the work finished. Check the status afterwards. An entity name, or an ID that does not exist, is accepted with 202 and changes nothing, so always read the status. Without any entity parameter, the command applies to every entity of the pipeline.

    Read the metrics of a pipeline

    API

    1. Read the JSON metrics of the pipeline:

      curl -sk "$GLUESYNC_URL/pipelines/$PIPELINE_ID/metrics" \
        | jq -c '{pipelineId, totalItemsCount, orders: .entitiesMetrics["'$ENTITY_ID'"][0].snapshotMetrics}'
    2. Read the Prometheus metrics of the whole Core Hub and keep the lines of your pipeline:

      curl -sk "$GLUESYNC_URL/metrics" | grep "pipelineId=\"$PIPELINE_ID\"" | grep '^gluesync_last_rows_count'
    Expected result

    The first command prints the totals of the pipeline and the snapshot of the entity:

    {"pipelineId":"9ae3b3bd","totalItemsCount":1214,"orders":{"snapshotCount":1000,"totalSnapshotRows":1000,"snapshotProgress":1.0,"startSnapshotTime":1793222229241823672,"endSnapshotTime":1793222229302359214,"snapshotSizeBytes":24355}}

    The second prints one gluesync_last_rows_count sample for each entity, labelled with pipelineId, pipelineName, entityId, and entityName.

    /pipelines/<id>/metrics answers with JSON, and /metrics answers with the Prometheus text format for every pipeline. Both answered without a token in this release. Do not publish Core Hub on a public network. To chart these metrics, see Monitor pipelines in Grafana.

    Narrow a token and read the error codes

    UI API

    Give each job its own token, with the least access that works.

    1. In Personal API Tokens, select New token, enter automation-readonly, and select the scope API read;

    2. Call a read endpoint and a write endpoint with the new token:

      export READ_TOKEN='gsp_…'
      curl -sk -w ' [%{http_code}]\n' "$GLUESYNC_URL/pipelines/$PIPELINE_ID/entities-status" \
        -H "Authorization: Bearer $READ_TOKEN"
      curl -sk -w ' [%{http_code}]\n' -X POST "$GLUESYNC_URL/pipelines/$PIPELINE_ID/commands/sync/stop?entity=$ENTITY_ID" \
        -H "Authorization: Bearer $READ_TOKEN"
    3. Optionally, limit a token to one pipeline. The Control Plane form has no agent field, so create this token from a session. Log in, then send the agents of the pipeline in scopes:

      SESSION=$(curl -sk -X POST "$GLUESYNC_URL/authentication/login" -H 'Content-Type: application/json' \
        -d '{"username":"<user>","password":"<password>"}' | jq -r .apiToken)
      curl -sk -X POST "$GLUESYNC_URL/api-tokens" -H "Authorization: Bearer $SESSION" -H 'Content-Type: application/json' \
        -d '{"name":"automation-n8n","expiresInDays":30,"permissionScopes":["gluesync.api"],
             "scopes":[{"pipelineId":"<pipeline-id>","agentId":"<source-agent-id>"},
                       {"pipelineId":"<pipeline-id>","agentId":"<target-agent-id>"}]}' | jq

      A pipeline-wide command, such as start or stop, needs every agent of the pipeline in the token scope. The response carries the token once, in the token field;

    4. Call another pipeline with the limited token.

    Expected result

    The read call answers 200 with the entity status. The write call is refused with 403 and a message that names both scopes. The limited token sees only its own pipeline in GET /pipelines, and the other pipeline answers 403:

    {"notificationId":-1,"message":"This token's permission scopes (gluesync.api_read) do not allow this request. It needs gluesync.api."}
    {"notificationId":-1,"message":"This token is limited to specific agents and cannot reach this pipeline or agent."}

    The following answers are the ones that you should handle in a client. Error bodies are JSON with a message, except the confirmations of the command endpoints, which are plain text.

    Status When it happens What to do

    202

    A start, stop, redo, or snapshot command was dispatched, even for an entity that does not exist.

    Read entities-status to confirm the effect.

    400

    A parameter is invalid. For example snapshotWriteMethod=bogus answers Method … unknown, please use UPSERT or INSERT.

    Fix the request.

    401

    The token is missing, malformed, or deleted: Invalid authorization token, please retry or make a new login in order to obtain a fresh token.

    Create a new token. Do not retry with the same value. An expired token is refused in the same way.

    403

    The token scope does not allow the request (gluesync.api_read calling a write, or gluesync.mcp_read calling the REST API), or the token is limited to other agents. Creating or deleting a token with a token is also refused: Personal API Tokens cannot create, revoke or delete tokens. Use the Gluesync UI.

    Use a token with the scope the message names, or a Control Plane session for token management.

    404

    The pipeline does not exist: Pipeline with id … not found.

    Check the pipeline ID.

    500

    The request reached Core Hub but failed. For example, reading an entity ID that does not exist answers Entity from pipeline … not found with 500.

    Read the message, check the IDs, and retry later.

    Part 2: Route Core Hub webhooks through n8n

    Run n8n on the Gluesync network

    CLI

    Skip this step if you already run n8n and Core Hub can reach it. Run n8n on the Docker network of your Gluesync kit, so that Core Hub calls it by its container name and n8n calls Core Hub, Mailpit, and the chat endpoint the same way.

    1. Save this file as docker-compose.yml in a folder of its own. gluesync-net is the network of the kit:

      services:
        n8n:
          image: docker.n8n.io/n8nio/n8n:latest   # pin a version in production
          container_name: n8n
          restart: unless-stopped
          ports:
            - "127.0.0.1:5678:5678"
          environment:
            - TZ=Europe/Rome
            - GENERIC_TIMEZONE=Europe/Rome
            - N8N_SECURE_COOKIE=false          # only because the editor is served over plain HTTP on localhost
          volumes:
            - n8n_data:/home/node/.n8n
          networks:
            - gluesync-net
      volumes:
        n8n_data:
      networks:
        gluesync-net:
          external: true
    2. Start it:

      docker compose up -d
      curl -s http://127.0.0.1:5678/healthz
    Expected result

    The health check answers {"status":"ok"}. The first start takes a minute while n8n prepares its database.

    Publishing the port on 127.0.0.1 keeps the n8n editor reachable only from the host. Core Hub does not need the published port: it reaches http://n8n:5678 on the Docker network. If Core Hub runs on another host or network, publish an address it can reach.

    Add a webhook that points to n8n

    UI

    The fields, events, signing secret, delivery log, and dead letters of Core Hub webhooks are explained in Send platform events to webhooks. This step only sets the values that this guide needs.

    1. Select the Settings gear icon, Notifications, and the Webhooks tab, then select Add webhook;

    2. In Name, enter Automation demo to n8n;

    3. In Endpoint URL, enter http://n8n:5678/webhook/gluesync-alerts. gluesync-alerts is the path that the n8n Webhook node will use. Always enter the address that Core Hub can reach from its own network, not the one your browser uses.

    The webhook form with the Name field marked by annotation 1 and the Endpoint URL field marked by annotation 2
    Figure 4. Name (1) and endpoint (2) of the webhook.
    1. Under Objects filters, select the pipeline Automation demo;

    2. In Enabled events, tick Entity Snapshot Failed, Pipeline Error, and Pipeline Error Resolved;

    3. In Severity filter, tick Critical, Info, and Warning.

    The Objects filters
    Figure 5. One pipeline (1), 3 of 40 events (2), and all 3 severities (3).
    1. Select Create webhook. Do not select Send test yet: n8n has no workflow to receive it.

    The webhooks list with the Automation demo to n8n card marked by annotation 1
    Figure 6. The webhook is listed with its endpoint, 3 events, and 3 severities (1).
    Expected result

    The card Automation demo to n8n appears with the badges Enabled, 3 events, and 3 severities.

    Select at least one severity and one event: a webhook with none selected receives nothing. Every failed write attempt sends a gluesync.pipeline.error event, so one failure produces several requests. The routing in the next step is built for that.

    Import the alert-routing workflow into n8n

    CLI

    The workflow receives each event, picks the recipient by pipeline, sends an email, and posts a message to a chat-style endpoint.

    n8n node Type and settings What it does

    Gluesync event

    Webhook node, method POST, path gluesync-alerts, responds immediately

    Receives the CloudEvents JSON from Core Hub. The event is under body: body.type, body.severity, body.pipelineName, body.entityName, body.pipelineId, body.entityId, and body.message.

    Pick recipient

    Edit Fields (Set) node with recipient, subject, and text

    recipient is automation-team@example.test when body.pipelineName is Automation demo, and ops@example.test for every other pipeline. subject is [<severity>] <pipeline> / <entity>: <type>.

    Email the owner

    Send Email node with an SMTP credential (host mailpit, port 1025, no TLS)

    Sends text to recipient.

    Post to chat

    HTTP Request node, POST to http://webhook-tester:8080/<session-id>; with a JSON body {"text": "<subject> - <text>"}

    The Slack-style call. For a real Slack channel, use the Incoming Webhook URL of the channel instead.

    1. Save the SMTP credential for Mailpit as credentials.json. n8n stores it encrypted when you import it:

      [
        {"id": "mailpit-smtp", "name": "Mailpit SMTP", "type": "smtp",
         "data": {"user": "gluesync", "password": "gluesync", "host": "mailpit", "port": 1025, "secure": false, "disableStartTls": true}}
      ]
    2. Save the workflow below as alerts.json. Replace <session-id> with the ID of your chat-style endpoint:

      alerts.json
      [{
        "id": "gluesync-alert-routing", "name": "Gluesync alert routing",
        "settings": {"executionOrder": "v1"},
        "nodes": [
          {"id": "n1", "name": "Gluesync event", "type": "n8n-nodes-base.webhook", "typeVersion": 2, "position": [0, 200], "webhookId": "6f1e1c6a-2f4e-4d5a-9a52-0d9c0f3b1a01", "parameters": {"httpMethod": "POST", "path": "gluesync-alerts", "responseMode": "onReceived", "options": {}}},
          {"id": "n2", "name": "Pick recipient", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [260, 60], "parameters": {"assignments": {"assignments": [{"id": "a1", "name": "recipient", "type": "string", "value": "={{ $json.body.pipelineName === 'Automation demo' ? 'automation-team@example.test' : 'ops@example.test' }}"}, {"id": "a2", "name": "subject", "type": "string", "value": "={{ '[' + $json.body.severity + '] ' + $json.body.pipelineName + ' / ' + $json.body.entityName + ': ' + $json.body.type }}"}, {"id": "a3", "name": "text", "type": "string", "value": "={{ $json.body.message }}"}]}, "includeOtherFields": true, "options": {}}},
          {"id": "n3", "name": "Email the owner", "type": "n8n-nodes-base.emailSend", "typeVersion": 2.1, "position": [520, 60], "parameters": {"fromEmail": "gluesync-alerts@example.test", "toEmail": "={{ $json.recipient }}", "subject": "={{ $json.subject }}", "emailFormat": "text", "text": "={{ $json.text }}", "options": {}}, "credentials": {"smtp": {"id": "mailpit-smtp", "name": "Mailpit SMTP"}}},
          {"id": "n4", "name": "Post to chat", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [780, 60], "parameters": {"method": "POST", "url": "http://webhook-tester:8080/<session-id>", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ text: $('Pick recipient').item.json.subject + ' - ' + $('Pick recipient').item.json.text }) }}", "options": {}}}
        ],
        "connections": {"Gluesync event": {"main": [[{"node": "Pick recipient", "type": "main", "index": 0}]]}, "Pick recipient": {"main": [[{"node": "Email the owner", "type": "main", "index": 0}]]}, "Email the owner": {"main": [[{"node": "Post to chat", "type": "main", "index": 0}]]}}
      }]
    3. Copy the files into the container and import them. The credential goes first. Then publish the workflow and restart n8n, because n8n registers the production webhook when it starts:

      docker cp credentials.json n8n:/tmp/credentials.json
      docker cp alerts.json n8n:/tmp/alerts.json
      docker exec n8n n8n import:credentials --input=/tmp/credentials.json
      docker exec n8n n8n import:workflow --input=/tmp/alerts.json
      docker exec n8n n8n publish:workflow --id=gluesync-alert-routing
      docker restart n8n
    4. In the Control Plane, open the webhook Automation demo to n8n with its pencil icon, select Send test, and check the receivers.

    Expected result

    Mailpit (http://localhost:8025) holds an email from gluesync-alerts@example.test to ops@example.test with the subject [INFO] undefined / undefined: gluesync.webhook.test, and the chat endpoint shows one POST whose body starts with {"text":"[INFO] undefined / undefined: gluesync.webhook.test - …"}. The test event names no pipeline and no entity, so the subject shows undefined and the email takes the default recipient. Real events carry both, as the next step shows.

    Break an entity on purpose and receive the alerts

    SQL UI

    Use a test entity: this step makes a target table reject writes. The guide uses the entity heartbeat, which copies automation_src.heartbeat to shop_automation.heartbeat.

    1. Optional: create the test entity. Skip this if you already have an entity that you can break. In the source database, run CREATE SCHEMA automation_src; CREATE TABLE automation_src.heartbeat (id integer PRIMARY KEY, note varchar(100) NOT NULL, created_at timestamp NOT NULL DEFAULT now());. In the target database, run CREATE TABLE shop_automation.heartbeat (id integer PRIMARY KEY, note varchar(100) NOT NULL, created_at timestamp NOT NULL);. Then add the entity heartbeat to the pipeline in the Control Plane;

    2. Start the entity if it is not running (see the start and stop step);

    3. In your database tool, connect to the target database and run:

      ALTER TABLE shop_automation.heartbeat
        ADD CONSTRAINT automation_demo_block CHECK (false) NOT VALID;
    4. Connect to the source database and add a row, so that the entity must write:

      INSERT INTO automation_src.heartbeat (id, note) VALUES (103, 'fails on target');
    5. Within a minute, open Mailpit and the chat endpoint, then open the pipeline in the Control Plane.

    Mailpit showing the alert email with the recipient marked by annotation 1
    Figure 7. The alert email: recipient (1), subject (2), and the message of the event (3).
    The webhook tester showing the POST request with the JSON text body marked by annotation 1
    Figure 8. The same event posted to the chat-style endpoint (1).
    The Automation demo entities table with the heartbeat entity in the Error group marked by annotation 1
    Figure 9. The failing entity (1) is in the Error group. customers (2) is active and orders (3) is on hold.
    Expected result

    Mailpit receives one [CRITICAL] Automation demo / heartbeat: gluesync.pipeline.error email for each failed attempt, all addressed to automation-team@example.test. In this test, five arrived in the first 20 seconds and about one more every minute while the failure lasted. The chat endpoint receives the same number of posts. The entity is listed under Error, and entities-status returns "errorType":"target_write_failed" for it.

    The message of the event is the database error, and it can contain the values of the row that failed. In this test the email shows Failing row contains (103, fails on target, …). The text leaves Core Hub and reaches every system you route it to, so decide what an email or a chat channel may contain.

    One failure sends many events. To avoid a flood, filter the events in the webhook (fewer Enabled events), group them in n8n, or use the burst prevention of the email notifications described in Set up email alerts.

    Restart a failed entity from n8n

    CLI API

    The second workflow is the reverse direction: n8n calls the REST API. Every 5 minutes it reads entities-status and, for each entity that has an errorState, stops the entity, waits 5 seconds, and starts it again. A short failure that clears by itself is gone by the next run, so only an entity that is still in error is restarted.

    1. Create a token for n8n with the Full API scope, limited to the two agents of the pipeline, as in the optional part of the error-codes step. A token that reaches every pipeline is wider than this job needs;

    2. Save the token as a Header Auth credential in token-credential.json. Do not commit this file:

      [
        {"id": "gluesync-token", "name": "Gluesync automation token", "type": "httpHeaderAuth",
         "data": {"name": "Authorization", "value": "Bearer gsp_…"}}
      ]
    3. Save the workflow below as restart.json. Replace <pipeline-id> with the ID of your pipeline:

      restart.json
      [{
        "id": "gluesync-restart-failed-entities", "name": "Gluesync restart failed entities",
        "settings": {"executionOrder": "v1"},
        "nodes": [
          {"id": "b1", "name": "Every 5 minutes", "type": "n8n-nodes-base.scheduleTrigger", "typeVersion": 1.2, "position": [0, 200], "parameters": {"rule": {"interval": [{"field": "minutes", "minutesInterval": 5}]}}},
          {"id": "b2", "name": "Read entity status", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [260, 200], "parameters": {"method": "GET", "url": "https://gluesync-core-hub:1717/pipelines/<pipeline-id>/entities-status", "authentication": "genericCredentialType", "genericAuthType": "httpHeaderAuth", "options": {"allowUnauthorizedCerts": true}}, "credentials": {"httpHeaderAuth": {"id": "gluesync-token", "name": "Gluesync automation token"}}},
          {"id": "b3", "name": "Is the entity in error?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [520, 200], "parameters": {"conditions": {"options": {"caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2}, "conditions": [{"id": "c1", "leftValue": "={{ !!$json.errorState }}", "rightValue": "", "operator": {"type": "boolean", "operation": "true", "singleValue": true}}], "combinator": "and"}, "options": {}}},
          {"id": "b4", "name": "Stop entity", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [780, 200], "parameters": {"method": "POST", "url": "=https://gluesync-core-hub:1717/pipelines/{{ $json.pipelineId }}/commands/sync/stop?entity={{ $json.entityId }}", "authentication": "genericCredentialType", "genericAuthType": "httpHeaderAuth", "options": {"allowUnauthorizedCerts": true}}, "credentials": {"httpHeaderAuth": {"id": "gluesync-token", "name": "Gluesync automation token"}}},
          {"id": "b5", "name": "Wait 5 seconds", "type": "n8n-nodes-base.wait", "typeVersion": 1.1, "position": [1040, 200], "parameters": {"amount": 5, "unit": "seconds"}},
          {"id": "b6", "name": "Start entity", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [1300, 200], "parameters": {"method": "POST", "url": "=https://gluesync-core-hub:1717/pipelines/{{ $('Is the entity in error?').item.json.pipelineId }}/commands/sync/start?entity={{ $('Is the entity in error?').item.json.entityId }}&withSnapshot=false", "authentication": "genericCredentialType", "genericAuthType": "httpHeaderAuth", "options": {"allowUnauthorizedCerts": true}}, "credentials": {"httpHeaderAuth": {"id": "gluesync-token", "name": "Gluesync automation token"}}}
        ],
        "connections": {"Every 5 minutes": {"main": [[{"node": "Read entity status", "type": "main", "index": 0}]]}, "Read entity status": {"main": [[{"node": "Is the entity in error?", "type": "main", "index": 0}]]}, "Is the entity in error?": {"main": [[{"node": "Stop entity", "type": "main", "index": 0}], []]}, "Stop entity": {"main": [[{"node": "Wait 5 seconds", "type": "main", "index": 0}]]}, "Wait 5 seconds": {"main": [[{"node": "Start entity", "type": "main", "index": 0}]]}}
      }]
    4. Import and publish it, and restart n8n:

      docker cp token-credential.json n8n:/tmp/token-credential.json
      docker cp restart.json n8n:/tmp/restart.json
      docker exec n8n n8n import:credentials --input=/tmp/token-credential.json
      docker exec n8n n8n import:workflow --input=/tmp/restart.json
      docker exec n8n n8n publish:workflow --id=gluesync-restart-failed-entities
      docker restart n8n
    5. Break the entity as in the previous step and wait for the next 5-minute run;

    6. Check that Core Hub received the commands, and that the status shows the restart:

      curl -sk "$GLUESYNC_URL/pipelines/$PIPELINE_ID/entities-status" \
        -H "Authorization: Bearer $GLUESYNC_TOKEN" | jq -c '.[] | select(.errorState)'
    7. Remove the cause. In the target database, run:

      ALTER TABLE shop_automation.heartbeat DROP CONSTRAINT automation_demo_block;
    Expected result

    In the n8n execution that ran after the failure, the Stop entity node answers Stop command received and dispatched to agents, and the Start entity node answers Start command received and dispatched to agents. Until you remove the cause, the entity stays in error. After you drop the constraint, errorState disappears, the entity keeps running, and the row you inserted reaches the target.

    Core Hub retries failed writes by itself, and a restart does not fix a cause that is still there. Use this workflow as a safety net for entities that stay in error, and keep the alert email from the previous step so that a person looks at the cause. If the cause persists, the workflow restarts the entity at every run, which is why the interval is 5 minutes and not shorter.

    Part 3: Connect an MCP client

    Connect a client to the MCP endpoint with a token

    UI API

    The Core Hub MCP server is part of Core Hub. It authenticates every call with your token and applies your role, so a client can do only what the token allows. Core Hub accepts a token with the scope MCP server (all tools) or MCP server (read-only), and a token with an MCP scope cannot call the REST API.

    1. Create a token named automation-mcp-read with the scope MCP server (read-only), as in the first step;

    2. Open Settings, then AI agents, and read the Streamable HTTP address under Connect. This is the URL for your client.

    The AI agents MCP settings with the Streamable HTTP address marked by annotation 1 and the Copy HTTP URL button marked by annotation 2
    Figure 10. The Streamable HTTP address (1) and Copy HTTP URL (2).
    1. Open a session with plain JSON-RPC. The first call answers with the session ID in the mcp-session-id response header, and every later call sends it back:

      export MCP_TOKEN='gsp_…'
      MCP="$GLUESYNC_URL/mcp/http"
      SID=$(curl -sk -D - -o /dev/null -X POST "$MCP" \
        -H "Authorization: Bearer $MCP_TOKEN" -H 'Content-Type: application/json' \
        -H 'Accept: application/json, text/event-stream' \
        -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' \
        | grep -i '^mcp-session-id' | awk '{print $2}' | tr -d '\r')
      curl -sk -o /dev/null -X POST "$MCP" -H "Authorization: Bearer $MCP_TOKEN" -H "Mcp-Session-Id: $SID" \
        -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
        -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
    2. Call a tool. get_pipeline_status reads the health of every entity of a pipeline:

      curl -sk -X POST "$MCP" -H "Authorization: Bearer $MCP_TOKEN" -H "Mcp-Session-Id: $SID" \
        -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
        -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_pipeline_status","arguments":{"pipeline_id":"9ae3b3bd"}}}' \
        | jq -r '.result.content[0].text | fromjson | .by_status'
    3. Call get_system_conditions. It lists problems that affect the whole Core Hub, such as the link to Gluesync Connect being down, and answers [] when everything is healthy:

      curl -sk -X POST "$MCP" -H "Authorization: Bearer $MCP_TOKEN" -H "Mcp-Session-Id: $SID" \
        -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
        -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_system_conditions","arguments":{}}}'
    4. Ask the read-only token to change something:

      curl -sk -X POST "$MCP" -H "Authorization: Bearer $MCP_TOKEN" -H "Mcp-Session-Id: $SID" \
        -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
        -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"stop_sync","arguments":{"pipeline_id":"9ae3b3bd","entity_ids":["8f2edc67"]}}}'
    5. Optionally, use a real client. The MCP Inspector command line works with the same URL and header (tested with version 0.17):

      NODE_TLS_REJECT_UNAUTHORIZED=0 npx -y @modelcontextprotocol/inspector@0.17.2 --cli "$GLUESYNC_URL/mcp/http" \
        --transport http --header "Authorization: Bearer $MCP_TOKEN" \
        --method tools/call --tool-name get_system_conditions

      NODE_TLS_REJECT_UNAUTHORIZED=0 is only for the self-signed kit certificate. Do not use it with a certificate you can verify.

    Expected result

    The status call prints the entities grouped by state, for example {"active":["3198138e"],"inactive":["8f2edc67"],"error":["a4c72ae5"]}. get_system_conditions answers "text":"[]". The change attempt answers with "isError":true and the message 'stop_sync' changes data, and this token only has the read-only MCP scope (gluesync.mcp_read). Use a token with the gluesync.mcp scope to run it.

    A session lists 143 built-in tools with either MCP scope, plus the db_… tools of the SQL-capable agents that the token can reach. A read-only token still lists the tools that change data, and refuses them when called.

    tools/call runs with your permissions. With the MCP server scope, start_sync and stop_sync act on the entities you name. Give an AI assistant the read-only scope unless it must change something, and see Personal API Tokens and scopes for agent scopes.

    Reproduce 403 Invalid Host behind your own hostname

    Config CLI

    When a reverse proxy forwards the name your clients use, such as gluesync.example.test, in the Host header, the MCP endpoint checks it against an allow-list that holds only localhost, 127.0.0.1, and [::1]. This protects the endpoint from DNS rebinding. Rehearse on a second Core Hub, so that you do not change the settings of the one you use. Traefik is used here; any proxy that keeps the Host header behaves the same.

    1. Start a second Core Hub with its own Traefik that routes the hostname gluesync.example.test to it. Give it a compose project name, a network, and a published port of its own. This is the Traefik dynamic configuration used in the test. passHostHeader makes the proxy forward the name it received, which is what real deployments do. Do not set any GLUESYNC_MCP_* variable yet:

      http:
        routers:
          corehub:
            entryPoints: [websecure]
            rule: "Host(`gluesync.example.test`)"
            service: corehub
            tls: {}
        services:
          corehub:
            loadBalancer:
              passHostHeader: true
              servers:
                - url: "https://gluesync-core-hub:1717"
    2. On the second Core Hub, create a token with the scope MCP server (read-only) and keep it in $HUB2_TOKEN;

    3. Make the name resolve to the proxy. Add 127.0.0.1 gluesync.example.test to your hosts file, or let curl resolve it for one command with --resolve. In the example, the proxy is published on port 443:

      curl -sk -i -X POST https://gluesync.example.test/mcp/http \
        --resolve gluesync.example.test:443:127.0.0.1 \
        -H "Authorization: Bearer $HUB2_TOKEN" -H 'Content-Type: application/json' \
        -H 'Accept: application/json, text/event-stream' \
        -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
    Expected result

    Core Hub refuses every MCP request with HTTP 403, although the token is valid:

    {"error":{"code":-32000,"message":"Invalid Host: gluesync.example.test"},"jsonrpc":"2.0"}

    The same request with -H 'Host: localhost' sent straight to Core Hub answers 200 with "serverInfo":{"name":"gluesync-corehub",…}. Only the Host header makes the difference.

    Optional, for advanced users

    You do not need the Core Hub log to follow this guide. If you look, Core Hub logs the allowed hosts at startup and a warning that names the refused host, so you see at once what to add:

    Gluesync MCP: DNS-rebinding protection allows hosts [localhost, 127.0.0.1, [::1]] (origin check = default)
    Gluesync MCP: rejected request with Host 'gluesync.example.test:443' (Origin 'none') on /mcp/http — not in allowed hosts [localhost, 127.0.0.1, [::1]]. Add it to GLUESYNC_MCP_ALLOWED_HOSTS…
    Allow the hostname with GLUESYNC_MCP_ALLOWED_HOSTS

    Config

    1. Add the hostname, without a scheme or a path, to the environment of the Core Hub container:

      services:
        gluesync-core-hub:
          environment:
            - GLUESYNC_MCP_ALLOWED_HOSTS=gluesync.example.test

      Use a comma to list several hostnames. Matching ignores case and port, and wildcards are not supported;

    2. Recreate the Core Hub container so that it reads the variable, and wait until the Control Plane answers again:

      docker compose up -d gluesync-core-hub
    3. Repeat the request of the previous step.

    4. Only for a client that sends an Origin header, such as a web page or a browser-based MCP client, test the origin too:

      curl -sk -o /dev/null -w '%{http_code}\n' -X POST https://gluesync.example.test/mcp/http \
        --resolve gluesync.example.test:443:127.0.0.1 -H 'Origin: https://ai.example.test' \
        -H "Authorization: Bearer $HUB2_TOKEN" -H 'Content-Type: application/json' \
        -H 'Accept: application/json, text/event-stream' \
        -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
    5. To accept that origin, add GLUESYNC_MCP_ALLOWED_ORIGINS=https://ai.example.test to the same environment list and recreate the container;

    Expected result

    After step 3, the initialize call answers 200 and returns an mcp-session-id header, so the client from the previous step works through the custom hostname. Without GLUESYNC_MCP_ALLOWED_ORIGINS, Origin: https://gluesync.example.test (the derived https://<host>;) answers 200, and Origin: https://ai.example.test answers 403 with Invalid Origin host: ai.example.test. After step 5, https://ai.example.test answers 200. Requests without an Origin header, which is how command-line and IDE clients connect, are not affected.

    Once you set GLUESYNC_MCP_ALLOWED_ORIGINS, it replaces the derived https://<host>; origins: Origin: https://gluesync.example.test answers 403 unless you list it too. The token is still required on every request. Allow only hostnames and origins that you control and serve over TLS.

    If you cannot change the Core Hub environment yet, have the reverse proxy send Host: localhost for the paths under /mcp. That rewrite keeps working after you set the variables.

    Verify the result

    You have successfully automated Gluesync if:

    • GET /api-tokens/current-scope returns the scopes of your token, and the token list shows its Last used time;

    • A start or stop command answers 202, and entities-status shows the change;

    • A token with API read is refused with 403 when it tries to stop an entity;

    • A failed write produces an email to the recipient chosen by pipeline, and a post to the chat endpoint;

    • n8n receives Stop command received and dispatched to agents and Start command received and dispatched to agents from Core Hub when an entity stays in error;

    • An MCP client lists the tools and reads get_pipeline_status with a token, and a read-only token is refused when it calls stop_sync;

    • The MCP endpoint answers 200 on your hostname after GLUESYNC_MCP_ALLOWED_HOSTS is set, and 403 Invalid Host before.

    Clean up

    1. In the target database, drop the constraint if it is still there, and stop the test entities;

    2. In Personal API Tokens, open the actions menu of every token created for this guide and delete it, or delete them with curl -sk -X DELETE "$GLUESYNC_URL/api-tokens/<token-id>" -H "Authorization: Bearer $SESSION" (204 on success);

    3. In Notifications and Webhooks, delete the webhook Automation demo to n8n;

    4. Remove the test containers (docker compose down in the n8n folder and in the folder of the second Core Hub).

    Troubleshooting

    Problem Possible cause What to do

    Every call answers 401 Invalid authorization token

    The token was copied incompletely, was deleted, or has expired, or you sent a login response instead of a token.

    Create a new token and copy the whole gsp_… value. A session token from /authentication/login is returned in the apiToken field.

    403 … do not allow this request

    The token scope is narrower than the call: API read on a write, or an MCP scope on the REST API.

    Use a token with the scope that the message names.

    The command answers 202 but nothing happens

    You passed an entity name or an ID that does not exist, or the entity was already in that state.

    Use the entity ID from /entities, and read entities-status.

    The webhook shows Success on Send test but n8n has no execution

    The workflow is not published, n8n was not restarted after publishing, or the URL uses the host that your browser uses.

    Check the path (/webhook/gluesync-alerts, not /webhook-test/), publish the workflow, restart n8n, and use the container name in the Endpoint URL.

    n8n cannot call Core Hub

    The kit certificate is self-signed, or the token has the wrong scope.

    Keep "allowUnauthorizedCerts": true in the options of each HTTP Request node, as in the workflow above, or give n8n a certificate it trusts. Check the token with current-scope.

    Mailpit receives no email

    The SMTP credential points to localhost or to another host.

    Use the host that n8n reaches on the Docker network (mailpit, port 1025).

    MCP initialize answers 403 Invalid Host: …

    The hostname that your proxy forwards is not in the allow-list.

    Set GLUESYNC_MCP_ALLOWED_HOSTS and recreate Core Hub, or send Host: localhost from the proxy.

    MCP initialize answers 403 Invalid Origin host: …

    A browser-based client sends an origin that is not allowed.

    Set GLUESYNC_MCP_ALLOWED_ORIGINS and include every origin, including your own https://<host>;.

    Contact MOLO17 Support if you need assistance with the Core Hub REST API, MCP, or webhooks.