haio-ticket
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@haio-ticketRead ticket #123 and reply as support engineer."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 |
|
Transport | MCP streamable-HTTP (POST JSON; |
Auth header |
|
Server host |
|
Database | MySQL |
OpenChamber | Already registered as |
Source code |
|
Current production token (rotate via §6 if you need a new one):
8d16eb892390eccd7f6f187988ee4ee564e918e5fe01978dRelated 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 dataKey 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
replywrites go.Dedicated DB user
mcpwithSELECTonbackend.*andSELECT,INSERT, UPDATEontickets+ticket_commentsonly. The server cannot accidentally drop a column or rewriteusers.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=Falsein FastMCP. The SDK enforcesHost/Originchecks by default in 1.29+; with the public DNS nameapi.haio.irand a TLS-terminating nginx, the default check rejects every request with421 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 |
| — | Static map of status IDs → Persian labels (1=جدید … 7=بسته شده). |
| — | Active categories from |
| — | All labels from |
|
| Paginated list, JOIN users, includes |
|
| Ticket header + full comment thread (oldest → newest) + user summary. |
|
| Insert as کارشناس ( |
|
| Change one or more of the three fields. |
Users
Tool | Args | Purpose |
|
| LIKE on |
|
| Profile row + every wallet balance ( |
|
| Dict of product lists: |
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 explicitenabled: true."streamable-http"is not a valid MCP type and triggersConfiguration 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/jsonandAccept: application/ json, text/event-stream— FastMCP will reject the request otherwise with406 Not Acceptableor415 Unsupported Media Type.Streamable-HTTP is stateful: the very first
initializeresponse contains anMcp-Session-Idheader; echo it back on every subsequent call or the server starts a new session and you lose context.If your shell has
HTTPS_PROXYset, 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":{}}}' | headNo 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;
SQL5.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.targetssh 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:
Generate a new value.
Edit the systemd unit to set the new value.
systemctl restart ticket-mcpso the server picks it up.Update the
mcp.haio-ticket.headers.Authorizationvalue in every OpenChamber / opencode config that references the server.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))"
# 8d16eb892390eccd7f6f187988ee4ee564e918e5fe01978dAny 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 gone6.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
EOFFor 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/Originagainst an allow-list. Because the public hostname isapi.haio.irand nginx terminates TLS, the check rejects every request with421 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-Idfrom theinitializeresponse 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/jsonANDAccept: application/json, text/event-stream. Both. FastMCP returns415on missing Content-Type and406on missing/wrong Accept.MySQL
rootonly via auth_socket. You cannotmysql -u root -pfrom the MCP host; usesudo mysqlor create a dedicated user (we createdmcp).Run
systemctl editafter editing the unit.Environment=lines are read at start, so a baresystemctl restartis enough, but adaemon-reloadis 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. Usesystemctl restart ticket-mcpinstead.opencode.jsonschema is strict. Usetype: "remote", NOTstreamable-http, and addenabled: 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 fileTo 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-pager9. Current live values (Sept 2026)
Setting | Value |
Public URL |
|
Token |
|
Server host | api.haio.ir (94.182.172.77), systemd |
DB | MySQL |
OpenChamber instance |
|
OpenChamber MCP name |
|
Status |
|
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.
This server cannot be installed
Maintenance
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
Read tickets, contacts, companies, agents and groups; create, update and reply to tickets.
Connect AI tools to Weav customer service. Search conversations, reply, and manage knowledge.
Manage AI assistants, history, calls, campaigns, contacts, knowledge, messaging, and automations.
Read tickets, users, orgs, macros and satisfaction ratings; create, update and comment on tickets.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables 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.71Apache 2.0
- AlicenseNot gradedqualityDmaintenanceEnables 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

Xalantis MCP Serverofficial
AlicenseAqualityBmaintenanceEnables managing support tickets from Claude, Cursor, and other AI tools, including listing, creating, updating, and replying to tickets.611MIT- FlicenseNot gradedqualityCmaintenanceEnables ticket management and AI triage through Mistral-compatible models, with tools for creating, listing, retrieving, triaging, and updating support tickets.-
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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