Skip to main content
Glama

Haio Ticket MCP Server

A Model Context Protocol (MCP) server that exposes the Haio legacy ticket system and user-product catalog to AI agents. It talks directly to the production MySQL database (backend on api.haio.ir) and is reverse-proxied through nginx at https://api.haio.ir/mcp-ticket.

The server is a thin, read/write wrapper around the same MySQL tables that the Lumen/PHP backend writes to. An AI agent can list tickets, read a ticket with its full comment thread, reply as a support engineer, change status/label/ category, look up a user, and see exactly which products (servers, DNS, email, anti-sanction packages, HaioClouds, services, ...) that user currently has.


Quick reference

Item

Value

Endpoint URL

https://api.haio.ir/mcp-ticket

Transport

MCP streamable-HTTP (POST JSON; Accept: application/json, text/event-stream)

Auth header

Authorization: Bearer <MCP_TOKEN>

Server host

api.haio.ir (94.182.172.77) — runs as systemd ticket-mcp.service on port 8801

Database

MySQL backend on 127.0.0.1:3306, user mcp (limited grants)

OpenChamber

Already registered as haio-ticket in opencode.json of the production instance

Source code

server.py (this directory) — single file, FastMCP + pymysql + uvicorn

Current production token (rotate via §6 if you need a new one):

8d16eb892390eccd7f6f187988ee4ee564e918e5fe01978d

Related MCP server: gorgias-mcp

1. Architecture

  AI agent / opencode
        │
        │  POST https://api.haio.ir/mcp-ticket
        │  Authorization: Bearer <TOKEN>
        │  Content-Type: application/json
        │  Accept: application/json, text/event-stream
        ▼
  nginx (api.haio.ir:443)
        │  location = /mcp-ticket → proxy_pass http://127.0.0.1:8801/mcp
        ▼
  ticket-mcp.service (uvicorn + FastMCP, 127.0.0.1:8801)
        │  pymysql → 127.0.0.1:3306
        ▼
  MySQL `backend` (mariadbd)
        │  tables: tickets, ticket_comments, users, wallets,
        │          services, haioflash_vms, cloud_vms, anti_sanctions,
        │          user_projects, haioclouds, dns_domains,
        │          email_domains, email_accounts, ticket_categories,
        │          ticket_labels, ticket_statuses
        ▼
   ticket + user + product data

Key design choices:

  • Direct MySQL, not HTTP to the Lumen app. The Lumen/PHP API would force the agent to re-authenticate as an admin and fight Lumen's CSRF. The DB is the source of truth and is already the place reply writes go.

  • Dedicated DB user mcp with SELECT on backend.* and SELECT,INSERT, UPDATE on tickets + ticket_comments only. The server cannot accidentally drop a column or rewrite users.

  • Bearer token, not OAuth. MCP's streamable-HTTP profile doesn't require OAuth; a static token in Authorization: Bearer … is enough for an internal tool, and rotating it is one systemd restart.

  • enable_dns_rebinding_protection=False in FastMCP. The SDK enforces Host/Origin checks by default in 1.29+; with the public DNS name api.haio.ir and a TLS-terminating nginx, the default check rejects every request with 421 Invalid Host header. We disable it; the Bearer token is the actual security boundary.


2. Tools exposed (10)

All tools are namespaced under the MCP server name haio-ticket.

Tickets

Tool

Args

Purpose

ticket_statuses

Static map of status IDs → Persian labels (1=جدید … 7=بسته شده).

ticket_categories

Active categories from ticket_categories (cat_status=1).

ticket_labels

All labels from ticket_labels.

ticket_list

search?, status_id?, category_id?, page?, per_page?

Paginated list, JOIN users, includes status_title and user_name.

ticket_get

ticket_id

Ticket header + full comment thread (oldest → newest) + user summary.

ticket_reply

ticket_id, body

Insert as کارشناس (user_id=33, comment_side=2), set status_id=4 (پاسخ کارشناس), and claim the ticket by setting agent_id and catch_ticket to MCP_AGENT_USER_ID — mirroring the Lumen TicketController::reply flow so the ticket shows up in the agent's queue.

ticket_update

ticket_id, status_id?, label_id?, category_id?

Change one or more of the three fields.

Users

Tool

Args

Purpose

user_search

query, limit?

LIKE on name/first_name/last_name/email/mobile (deleted_at IS NULL).

user_profile

user_id

Profile row + every wallet balance (wallets WHERE holder_type='App\\Models\\User').

user_products

user_id

Dict of product lists: services, haioflash_vms, cloud_vms, anti_sanctions, haioclouds, dns_domains, email_domains, email_accounts. Soft-deleted rows excluded.

ticket_reply is the only state-mutating tool besides ticket_update. Use it as a normal "agent reply" — the same MySQL writes that the TicketController::reply action performs.


3. Adding the MCP server to a client

3.1 OpenChamber (production, my.haio.ir)

The production OpenChamber container is compose-quantify-cross-platform-panel-gcs5w4-openchamber-1 on node60 (94.182.94.3:2280). Its opencode config lives on the host as a named Docker volume; we edit the file in-place and restart the container.

Already done for you (Sept 2026):

"mcp": {
  "haio-ticket": {
    "type": "remote",
    "url": "https://api.haio.ir/mcp-ticket",
    "enabled": true,
    "headers": {
      "Authorization": "Bearer 8d16eb892390eccd7f6f187988ee4ee564e918e5fe01978d"
    }
  }
}

opencode mcp list reports ✓ haio-ticket connected. tools/list returns all 10 tools.

To re-apply after a volume wipe or to a second instance:

# 1. Find the opencode-config volume on node60
ssh root@94.182.172.92 \
  "ssh -i /etc/haio/dokploy-operator-ed25519 -p 2280 root@94.182.94.3 \
    'ls -d /var/lib/docker/volumes/*_openchamber-opencode-config*/_data'"

# 2. Drop this into opencode.json (preserving the rest of the file)
cat >> opencode.json.snippet <<'JSON'
,
"mcp": {
  "haio-ticket": {
    "type": "remote",
    "url": "https://api.haio.ir/mcp-ticket",
    "enabled": true,
    "headers": { "Authorization": "Bearer <TOKEN>" }
  }
}
JSON

# 3. Ship + restart
scp opencode.json.snippet root@94.182.172.92:/tmp/
ssh root@94.182.172.92 \
  "scp -i /etc/haio/dokploy-operator-ed25519 -P 2280 -q /tmp/opencode.json.snippet root@94.182.94.3:/tmp/ && \
   ssh -i /etc/haio/dokploy-operator-ed25519 -p 2280 root@94.182.94.3 \
     'VOL=\$(ls -d /var/lib/docker/volumes/*_openchamber-opencode-config*/_data | head -1); \
      python3 -c \"import json;d=json.load(open(\\\"\$VOL/opencode.json\\\"));d.setdefault(\\\"mcp\\\",{})[\\\"haio-ticket\\\"]=json.load(open(\\\"/tmp/opencode.json.snippet\\\"))[\\\"mcp\\\"][\\\"haio-ticket\\\"];json.dump(d,open(\\\"\$VOL/opencode.json\\\",\\\"w\\\"),indent=2)\" && \
      chown 1000:1000 \$VOL/opencode.json && \
      docker restart \$(docker ps --format \"{{.Names}}\" | grep openchamber-1) && \
      rm /tmp/opencode.json.snippet'"

Schema gotcha: opencode requires type: "remote" and an explicit enabled: true. "streamable-http" is not a valid MCP type and triggers Configuration is invalid … Expected { readonly "type": "local", … } | { readonly "type": "remote", … }. Streamable-HTTP is selected automatically when the server is remote + HTTP.

3.2 Local OpenChamber AppImage (developer machine)

The local AppImage reads ~/.config/opencode/opencode.json (or opencode.jsonc). The same mcp block as above works verbatim. The url must be reachable from your machine — https://api.haio.ir/mcp-ticket is fine as long as your network can reach api.haio.ir:443.

3.3 Raw streamable-HTTP client (any agent)

TOKEN=8d16eb892390eccd7f6f187988ee4ee564e918e5fe01978d
URL=https://api.haio.ir/mcp-ticket

# 1. initialize → grab Mcp-Session-Id from response headers
SID=$(curl -s --noproxy '*' -D - -o /dev/null \
  -X POST "$URL" \
  -H "Authorization: Bearer $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":"my-agent","version":"1.0"}}}' \
  | awk -F': ' 'tolower($1)=="mcp-session-id"{gsub(/\r/,"",$2);print $2}')

# 2. tools/list (note: stateful — must reuse the SID header)
curl -s --noproxy '*' -X POST "$URL" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Mcp-Session-Id: $SID" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

# 3. tools/call
curl -s --noproxy '*' -X POST "$URL" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Mcp-Session-Id: $SID" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"user_search","arguments":{"query":"ali","limit":5}}}'
  • Every POST must be Content-Type: application/json and Accept: application/ json, text/event-stream — FastMCP will reject the request otherwise with 406 Not Acceptable or 415 Unsupported Media Type.

  • Streamable-HTTP is stateful: the very first initialize response contains an Mcp-Session-Id header; echo it back on every subsequent call or the server starts a new session and you lose context.

  • If your shell has HTTPS_PROXY set, add --noproxy '*' to curl or the proxy will re-emit a 421 from the FastMCP transport-security middleware.


4. Local development

The local dev server binds to 127.0.0.1:8801 and talks to a MySQL you bring up yourself (or via SSH tunnel to api.haio.ir).

cd ticket-mcp
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt

# Option A: SSH tunnel to production DB (read-only is enough for most tools)
ssh -f -N -L 3307:127.0.0.1:3306 root@api.haio.ir

# Option B: local MySQL with a dump of `backend`
#   mysqldump -h api.haio.ir backend tickets ticket_comments users ... | mysql backend_local

export MCP_DB_HOST=127.0.0.1
export MCP_DB_PORT=3307   # 3306 if local
export MCP_DB_USER=mcp
export MCP_DB_PASSWORD=…
export MCP_TOKEN=test123
export MCP_PORT=8801

python server.py
# uvicorn now serving http://127.0.0.1:8801 (POST /mcp, GET /health → 200)

A quick smoke test:

SID=$(curl -s -D - -o /dev/null -X POST http://127.0.0.1:8801/mcp \
  -H "Authorization: Bearer test123" \
  -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":"smoke","version":"1.0"}}}' \
  | awk -F': ' 'tolower($1)=="mcp-session-id"{gsub(/\r/,"",$2);print $2}')

curl -s -X POST http://127.0.0.1:8801/mcp \
  -H "Authorization: Bearer test123" -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" -H "Mcp-Session-Id: $SID" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"ticket_statuses","arguments":{}}}' | head

No 401? You got the bearer right. Empty []/list? The tools loaded. You're good.


5. Deploy / re-deploy on api.haio.ir

api.haio.ir is a stock Ubuntu 24.04 host without Docker. The MCP server runs under systemd as ticket-mcp.service against the local MariaDB.

5.1 One-time host setup

ssh root@api.haio.ir
apt-get update && apt-get install -y python3.12-venv
# MariaDB is already running, listens on 127.0.0.1:3306.

5.2 Drop the code

# from your dev machine
scp -r ticket-mcp/ root@api.haio.ir:/opt/
ssh root@api.haio.ir "cd /opt/ticket-mcp && python3.12 -m venv .venv && .venv/bin/pip install -r requirements.txt"

5.3 Create the dedicated MySQL user (least privilege)

ssh root@api.haio.ir
# MySQL root uses auth_socket — log in via sudo or as root and grant the mcp user.
mysql -u root <<'SQL'
CREATE USER 'mcp'@'127.0.0.1' IDENTIFIED BY '…a-strong-secret…';
CREATE USER 'mcp'@'localhost'   IDENTIFIED BY '…a-strong-secret…';
GRANT SELECT ON backend.* TO 'mcp'@'127.0.0.1';
GRANT SELECT, INSERT, UPDATE ON backend.tickets         TO 'mcp'@'127.0.0.1';
GRANT SELECT, INSERT, UPDATE ON backend.ticket_comments TO 'mcp'@'127.0.0.1';
GRANT SELECT ON backend.* TO 'mcp'@'localhost';
GRANT SELECT, INSERT, UPDATE ON backend.tickets         TO 'mcp'@'localhost';
GRANT SELECT, INSERT, UPDATE ON backend.ticket_comments TO 'mcp'@'localhost';
FLUSH PRIVILEGES;
SQL

5.4 systemd unit

/etc/systemd/system/ticket-mcp.service:

[Unit]
Description=Haio Ticket MCP (streamable-HTTP)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
WorkingDirectory=/opt/ticket-mcp
ExecStart=/opt/ticket-mcp/.venv/bin/python /opt/ticket-mcp/server.py
Restart=always
RestartSec=3
Environment=MCP_HOST=0.0.0.0
Environment=MCP_PORT=8801
Environment=MCP_DB_HOST=127.0.0.1
Environment=MCP_DB_PORT=3306
Environment=MCP_DB_USER=mcp
Environment=MCP_DB_PASSWORD=…a-strong-secret…
Environment=MCP_DB_NAME=backend
Environment=MCP_AGENT_USER_ID=33
Environment=MCP_TOKEN=…long-random-token…

[Install]
WantedBy=multi-user.target
ssh root@api.haio.ir
systemctl daemon-reload
systemctl enable --now ticket-mcp
systemctl status ticket-mcp --no-pager
curl -s http://127.0.0.1:8801/health
# {"status":"ok"}

5.5 nginx reverse proxy

Port 8801 is not exposed externally (datacenter firewall). Add a single line inside the api.haio.ir 443 server block of /etc/nginx/conf.d/haio.conf:

location = /mcp-ticket {
    proxy_pass http://127.0.0.1:8801/mcp;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header Connection "";
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 3600s;
    proxy_buffering off;
}

Reload:

ssh root@api.haio.ir "nginx -t && systemctl reload nginx"

Why an exact-match (=) location: it prevents any path-suffix from being appended, so the upstream always sees POST /mcp regardless of how the client spells the URL. If you ever move the MCP behind a prefix (e.g. /mcp/ticket), remember to also accept /mcp/ticket/mcp for clients that append the standard suffix:

location ^~ /mcp/ticket/ {
    proxy_pass http://127.0.0.1:8801/mcp/;
    # …same headers as above
}

6. How to create / rotate the Bearer token

The token is a single shared secret stored in one place: the MCP_TOKEN env var inside the systemd unit. Rotating it means:

  1. Generate a new value.

  2. Edit the systemd unit to set the new value.

  3. systemctl restart ticket-mcp so the server picks it up.

  4. Update the mcp.haio-ticket.headers.Authorization value in every OpenChamber / opencode config that references the server.

  5. Restart the OpenChamber container so it reconnects.

6.1 Generate a strong token

# 96 hex chars (48 random bytes) — same recipe as the production one.
python3 -c "import secrets;print(secrets.token_hex(24))"
# 8d16eb892390eccd7f6f187988ee4ee564e918e5fe01978d

Any opaque random string works; the server does a constant-time auth != expected compare so length doesn't matter much, but 32+ bytes is plenty.

6.2 Apply on the server

NEW=$(python3 -c "import secrets;print(secrets.token_hex(24))")
ssh root@api.haio.ir bash - <<EOF
sed -i "s|^Environment=MCP_TOKEN=.*|Environment=MCP_TOKEN=$NEW|" \
  /etc/systemd/system/ticket-mcp.service
systemctl daemon-reload
systemctl restart ticket-mcp
sleep 2
systemctl is-active ticket-mcp
EOF
echo "New token: $NEW"
# store it in your secret manager NOW — the shell variable is gone

6.3 Update every consumer

For the production OpenChamber instance, edit the same volume file:

NEW=…new-token…
ssh root@94.182.172.92 <<EOF
ssh -i /etc/haio/dokploy-operator-ed25519 -p 2280 root@94.182.94.3 bash <<INNER
VOL=\$(ls -d /var/lib/docker/volumes/*_openchamber-opencode-config*/_data | head -1)
python3 -c "
import json
p='$VOL/opencode.json'
d=json.load(open(p))
d['mcp']['haio-ticket']['headers']['Authorization']='Bearer $NEW'
json.dump(d,open(p,'w'),indent=2)
print('updated')
"
chown 1000:1000 \$VOL/opencode.json
docker restart \$(docker ps --format '{{.Names}}' | grep openchamber-1)
INNER
EOF

For any other consumer (a developer AppImage, a CI agent, a custom script), update its opencode.json (or its hard-coded header) the same way and restart it.

6.4 Per-agent tokens (optional)

If you want to give different agents their own credentials (so you can revoke just one without breaking the others), the code today only supports a single MCP_TOKEN. To split, change the AuthMiddleware in server.py:

TOKENS = {
    secrets.compare_digest:  # placeholder — use a dict of name → token
    # e.g. {"agent-router": "…", "openchamber-1": "…"}
}
# in __call__:
auth = headers.get(b"authorization", b"").decode()
if not any(secrets.compare_digest(auth, f"Bearer {t}") for t in TOKENS.values()):
    return JSONResponse({"error": "unauthorized"}, status_code=401)

Pick the bearer by prefixing the token with a label (agent-router:abcd…) if you want a self-identifying scheme, or just keep N opaque tokens and a side-table of which agent owns which.


7. Operational notes & gotchas

  • FastMCP 1.29+ enables DNS-rebinding protection by default. The middleware checks Host/Origin against an allow-list. Because the public hostname is api.haio.ir and nginx terminates TLS, the check rejects every request with 421 Invalid Host header. transport_security=TransportSecuritySettings (enable_dns_rebinding_protection=False) is required. The Bearer token is the security boundary.

  • Streamable-HTTP is stateful. Reuse Mcp-Session-Id from the initialize response on every subsequent call, or the server will treat each request as a new session and you'll lose tool-call context.

  • Every POST needs Content-Type: application/json AND Accept: application/json, text/event-stream. Both. FastMCP returns 415 on missing Content-Type and 406 on missing/wrong Accept.

  • MySQL root only via auth_socket. You cannot mysql -u root -p from the MCP host; use sudo mysql or create a dedicated user (we created mcp).

  • Run systemctl edit after editing the unit. Environment= lines are read at start, so a bare systemctl restart is enough, but a daemon-reload is required if you changed the unit file itself.

  • Don't pkill -f server.py. It kills the bash that's running pkill, because the pattern matches the shell command line. Use systemctl restart ticket-mcp instead.

  • opencode.json schema is strict. Use type: "remote", NOT streamable-http, and add enabled: true. See §3.1.

  • Long replies time out nginx by default. proxy_read_timeout 3600s; is required because the agent may keep an SSE channel open while the LLM thinks.


8. Files in this directory

ticket-mcp/
├── server.py         # the entire MCP server (single file, FastMCP + pymysql)
├── requirements.txt  # mcp>=1.9.0,<2  pymysql  uvicorn  starlette
├── Dockerfile        # alternative container build (NOT used in prod — api.haio.ir has no Docker)
├── .venv/            # dev virtualenv
└── README.md         # this file

To rebuild the systemd deployment after editing server.py:

scp server.py root@api.haio.ir:/opt/ticket-mcp/server.py
ssh root@api.haio.ir systemctl restart ticket-mcp
ssh root@api.haio.ir journalctl -u ticket-mcp -n 20 --no-pager

9. Current live values (Sept 2026)

Setting

Value

Public URL

https://api.haio.ir/mcp-ticket

Token

8d16eb892390eccd7f6f187988ee4ee564e918e5fe01978d

Server host

api.haio.ir (94.182.172.77), systemd ticket-mcp.service

DB

MySQL backend @ 127.0.0.1:3306, user mcp (limited grants)

OpenChamber instance

compose-quantify-cross-platform-panel-gcs5w4-openchamber-1 on node60

OpenChamber MCP name

haio-ticket (registered in that container's opencode.json)

Status

opencode mcp list reports ✓ haio-ticket connected; all 10 tools load

Rotate at any time using §6.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

No tool schema history has been recorded yet.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    Enables comprehensive management of Zendesk tickets, comments, and Help Center articles through tools for searching, creating, and updating content. It includes specialized prompts for ticket analysis and response drafting to streamline support workflows.
    7
    1
    Apache 2.0
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables AI agents to manage Gorgias support tickets: list open tickets, read conversation history, draft internal notes, send outbound replies (gated), and look up customer history.
    MIT

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/haioco/haio-ticket-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server