This was 100% AI vibe coded with Opus 5.5. This example is not meant to be used for anything other than to teach you or an AI agent how to implement the Phaze API and MCP for a common IT use case. I hope this inspires ideas.
Note
Phaze currently only supports a connection between a Windows computer and another Windows computer. It uses accelerated graphics, so this will not work on PCs without graphics capabilities (either via your integrated CPU graphics or your discrete GPU). Almost all CPUs support this other than server-class CPUs.
This repo is a working example of what you can build with the Phaze Enterprise API and the Phaze MCP server. It's an IT helpdesk agent that watches a Slack channel. When someone posts "my sound isn't working", it finds their computer through the API. Then it connects to that computer through Phaze and fixes the problem by looking at the screen and using the mouse and keyboard, the way a technician would. When it gets stuck, it pages a human, hands them the live session, and takes it back when they're done.
It's meant as a starting point. The first half of this README teaches the Phaze API and MCP from scratch, so you (or your AI coding agent) can build something similar or something entirely different. The second half explains this example.
Phaze is a high-performance remote desktop. The pieces:
| Term | Meaning |
|---|---|
| Enterprise | Your company's Phaze account. Contains organizations and members. |
| Organization (org) | A group of machines and people inside the enterprise, e.g. per office or per customer. |
| Member | A person with a Phaze account ( id looks likeu_... ). |
| Machine | A computer running Phaze that can be connected to (a host ). Assigned to a memberor to a group. |
| Group | A set of members inside an org. Machines assigned to a group are reachable by its members. |
| Connection | A live remote-desktop session from a Phaze app to a machine. |
| Guest | A participant in a machine's session. One guest at a time has control (mouse and keyboard). |
The two developer surfaces do different jobs:
Phaze Enterprise API (cloud, HTTPS, API key) Phaze MCP server (local, in the Phaze app)
βββββββββββββββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββββββββββββ
"the control plane": who, what, where "the hands": see and operate a machine
members, orgs, machines, groups, assignments, list reachable machines, connect, screenshot,
connection logs, relays, locations click, type, scroll, hand control to a guest
An agent typically uses the API to decide which machine and whether it's allowed, and the MCP to actually do something on it.
Note
Phaze is currently in beta, so we don't have a web sign up for the administrator system.
- Visit https://web.phaze.app/signup to create your account.
- Then email founders@phaze.app from the email address that you created your account with.
- We will create an administrator account for you.
API keys are created in the Phaze admin portal, which needs an administrator account at admin.phaze.app. If you're not an admin, ask your Phaze administrator for a key.
- Sign in to admin.phaze.app as an administrator.
- Open the Enterprise settings page (admin.phaze.app/settings ).
- Generate an API key and store it somewhere safe, such as an
.envfile that's in.gitignore. Never commit it and never paste it into a chat.
Step-by-step help: The Phaze API. Full reference: apidocs.phaze.app.
export PHAZE_API_KEY=... # from admin.phaze.app/settings
curl -s https://public-api.phaze.app/enterprise/v1/orgs \
-H "Authorization: Bearer $PHAZE_API_KEY"
python
import httpx
api = httpx.Client(base_url="https://public-api.phaze.app/enterprise/v1",
headers={"Authorization": f"Bearer {PHAZE_API_KEY}"})
orgs = api.get("/orgs").json()["data"]
machines = api.get(f"/orgs/{orgs[0]['id']}/machines", params={"limit": 200}).json()["data"]
| Base URL | https://public-api.phaze.app/enterprise/v1 |
| Auth | Authorization: Bearer <API_KEY> |
| Responses | {"status": "OK", "data": {...}} , and for lists{"status": "OK", "data": [...], "count": N} |
| Pagination | limit (1β200, default 50) andoffset ; stop whenoffset >= count |
| Errors | {"status": "Not Found", "errors": [{"code": "not_found", "message": "..."}]} |
| Rate limit | 600 requests/min (headers X-RateLimit-Remaining ,Retry-After ; back off on429 ) |
Main resources (see apidocs.phaze.app for every endpoint):
| Resource | Endpoints |
|---|---|
| Members | GET /members (q= searches),PUT /members/role , invites under/invites |
| Orgs | GET/POST /orgs ,PATCH/DELETE /orgs/{org_id} , org members under/orgs/{org_id}/members |
| Machines | GET /orgs/{org_id}/machines ,PUT .../machines/assign-user ,PUT .../machines/assign-group |
| Groups | /orgs/{org_id}/groups , members under/orgs/{org_id}/groups/{group_id}/members |
| Connections | GET /orgs/{org_id}/connections (who connected to what, and when) |
| Also | machine keys, locations, relays |
- Machine IDs match across API and MCP. A machine's
idin the API equals itsmachine_idin the MCP (64 hex chars). That's how you join "the API says this person owns machine X" with "the MCP can reach X". - Member IDs match guests. A member's
id(u_...) is theuserfield of a guest in the MCP'sphaze_status. You can tellwho is in a session. - A machine has
assignee_idorgroup_id, never both. Assigning a machine to a group replaces its user assignment. - No member-by-ID endpoint. To resolve a
u_...ID to a name, list/membersonce and cache it (seephaze_api.py). - Machine objects include
id,name,org_id,assignee_id,group_id,is_online,os,platform,location_id,version.
The Model Context Protocol (MCP) lets an AI model call tools. The Phaze app includes an MCP server that lets an AI operate remote machines through Phaze: connect, look at the screen, click, type. It acts as the Phaze account that's signed in to the app.
The MCP server is currently an experimental feature. Help article: The Phaze MCP server.
- Install the Phaze app and sign in.
- In the Phaze app's client settings , turn on the experimentalMCP server feature.
- Keep the app running. It serves the MCP at
http://127.0.0.1:41010/mcp(HTTP transport, this computer only).
claude mcp add --transport http phaze http://127.0.0.1:41010/mcp
codex mcp add phaze --url http://127.0.0.1:41010/mcp
Claude Desktop (Settings β Developer β Edit Config, then fully quit and reopen):
{
"mcpServers": {
"phaze": { "command": "npx", "args": ["-y", "mcp-remote@latest", "http://127.0.0.1:41010/mcp"] }
}
}
ChatGPT needs OpenAI's Secure MCP Tunnel, because it can't reach 127.0.0.1 directly; see
the help article. After connecting, try: "List my Phaze hosts, connect to one, and tell me
what's on its screen."
The server speaks plain JSON-RPC over HTTP, so any language can call it:
curl -s -X POST http://127.0.0.1:41010/mcp \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"phaze_status","arguments":{}}}'
The result's content[0].text is a JSON string. phaze_mcp.py is a 60-line async client
that does this, with retries.
| Tool | Arguments | What it does |
|---|---|---|
phaze_list_hosts |
none | Machines this account can reach: machine_id ,peer_id ,name ,online ,requires_relay |
phaze_connect |
host_peer_id ,machine_id ,relay? |
Start a connection. Returns a connection_id immediately; it's live oncephaze_status showsconnected: true . Passrelay=true only ifrequires_relay . |
phaze_status |
none | App state and every connection: owner ,connected ,guests , whohas_control , monitors (outputs ) |
phaze_screenshot |
connection_id ,output? |
PNG of a monitor. Mouse coordinates are pixels in this image. |
phaze_set_control |
connection_id ,guest_id |
Give control to a guest: your own is_self guest to act, a human's to hand over,0 to release |
phaze_left_click ,phaze_right_click ,phaze_middle_click ,phaze_double_click ,phaze_triple_click |
connection_id ,coordinate: [x, y] ,output? |
Clicks. phaze_left_click also takes a modifier intext (shift ,ctrl ,alt ,super ) |
phaze_left_click_drag |
connection_id ,start_coordinate ,coordinate |
Drag |
phaze_left_mouse_down /_up ,phaze_mouse_move |
connection_id ,coordinate |
Fine-grained mouse control |
phaze_scroll |
connection_id ,coordinate ,scroll_direction ,scroll_amount? |
Scroll |
phaze_send_keys |
connection_id ,text ,modifiers? |
Type text. Named keys use brackets: [Enter] ,[Escape] ,[F1] . Modifiers:ControlLeft ,ShiftLeft ,AltLeft ,MetaLeft , β¦ |
phaze_disconnect |
connection_id |
End a connection |
A phaze_status payload looks like this (trimmed):
{"connected": true, "connections": [{
"connection_id": "connection-1a2bβ¦", "owner": "agent", "connected": true,
"host_machine_id": "9f8eβ¦", "has_control": true,
"guests": [{"guest_id": 1234567, "user": "u_abcβ¦", "has_control": true, "is_self": true}],
"outputs": [{"id": 111, "primary": true, "width": 1920, "height": 1080}]}]}
-
owner:"agent"means the connection was opened through the MCP."user"means it's the human's own session in the Phaze app. Don't take over or disconnect"user"connections unless that's really intended. -
Guests are per machine, not per connection. Everyone connected to the same machine shows up in every connection's
guestslist, including your own other connections.is_selfmarks yours. Tell people apart byuser(a member ID). -
One guest has control at a time. Input tools only work while your
is_selfguest has control. Take it withphaze_set_control(your guest_id). Control lands asynchronously, so pollphaze_statusbefore sending input. -
Handing off to a human is
phaze_set_control(conn, their guest_id).Taking it back isphaze_set_control(conn, your guest_id). -
Connecting takes time. Expect several seconds, sometimes 30+. Poll
phaze_status. -
Transient errors. While a connection is landing, the app can briefly answer
main loop job timed out during execution. Retry with backoff. -
Always screenshot after acting. Treat each click as a hypothesis and verify it.
-
The MCP acts as the signed-in Phaze account and can reach whatever that account can. For automation, sign the app in as a dedicated account whose access you control.
-
Deep link for humans:
phaze://connect?id=<machine_id>opens the Phaze app and connects to that machine. It's handy as a button in Slack, email, or a web page.
The pattern this repo uses, which works for most agents:
- Decide with the API. Look up the person and their machines, and turn that into an
allow-list of
machine_ids. - Act with the MCP. Give an AI model the MCP tools, plus your own tools for your business logic.
- Enforce in code, not only in the prompt. Put a hook in front of every tool call that
checks it against the allow-list. For example: only allowed machines, never
owner: "user"sessions, never grab control from someone else. - Keep humans in the loop deterministically. Waiting for people, detecting that they joined, and moving control are done by your code calling the MCP directly, not by the model.
Ideas for other things to build:
- Scheduled maintenance agent: each night, use the API to find machines in an org, connect, and run updates or check disk space. Post a report.
- Onboarding assistant: after
POST /invites, assign a machine withPUT /machines/assign-user, connect, and set up the apps a new hire needs. - Session audit bot: pull
GET /orgs/{org_id}/connectionsinto your SIEM or a weekly Slack summary of who accessed which machines. - Access requests: a Slack or ServiceNow workflow that grants access by assigning machines to a group, and revokes it later.
- Render-farm or lab monitor: screenshot long-running jobs on many machines and alert when something looks stuck.
- Guided support: instead of fixing it, the agent connects and walks the user through the steps while a human watches.
#it-help msg ββΆ Phaze Enterprise API ββΆ Phaze MCP ββΆ resolved β
(who is this user, (connect, β
which machine) see, act) ββ ask_technician ββΆ tech replies ββΆ agent continues
β
ββ request_handoff ββΆ page in #it-escalations
ββΆ tech clicks "Connect in Phaze", gets control
ββΆ "Hand back to agent" ββΆ agent continues
ββΆ or Mark resolved / Close
Built with the Claude Agent SDK (Python) and Slack Bolt in Socket Mode, so it needs no public URL.
- Someone posts in the help channel. The bot reacts π.
- The agent calls the API to find the requester's machines. If they have several and the message doesn't say which, it replies in their thread with a numbered list and a button per machine, and waits for their pick.
- It connects through the MCP, takes control, and works in small verified steps.
- It finishes one of three ways:
- Resolved: posts notes for technicians and marks it β .
- Needs a fact:
ask_technicianposts a question in the escalation thread and keeps the session until someone replies. - Needs a human:
request_handoffreleases control and pages the escalation channel. The page has aConnect in Phaze button (phaze://connect?id=β¦). When the technician joins, the code gives them control.Hand back to agent resumes the same agent conversation with the technician's notes.
The requester sees emoji reactions (π working, β resolved, π with a human, βοΈ closed,
| File | What's in it |
|---|---|
run.py |
Entry point. Slack listener (Socket Mode), buttons, routing of replies, concurrency |
agent.py |
One ticket: the Agent SDK session, custom tools, guardrail hook, and the ask / handoff / handback state machine |
phaze_api.py |
Minimal Phaze Enterprise API client (pagination, 429 retry, member lookup) |
phaze_mcp.py |
Minimal direct Phaze MCP client used by the orchestrator (status, control, disconnect) |
ticketing.py |
Slack: tickets, the escalation thread, buttons, machine picker, technician commands |
prompts.py |
The agent's system prompt |
config.py |
Settings from .env |
tests/test_flow.py |
Offline tests of the state machine with fake Slack, Phaze and API |
You need:
- A computer running the Phaze app with the MCP enabled (Part 2). Ideally it's signed in
as a dedicated account such as
helpdesk-agent@yourco.comthat can reach the machines it should support. - A Phaze API key from an administrator account atadmin.phaze.app (Part 1).
- An Anthropic API key (console.anthropic.com ).
- Python 3.10+ and theClaude Code CLI , which the Agent SDK drives:
npm install -g @anthropic-ai/claude-code. - A Slack workspace where you can create an app.
1. Install
git clone https://github.com/boxerbk/phaze-mcp-api-helpdesk-example
cd phaze-mcp-api-helpdesk-example
pip install -r requirements.txt
cp .env.example .env # then fill it in
2. Create the Slack app at api.slack.com/apps:
- Socket Mode: turn it on. Create an app-level token with
connections:writeand put it inSLACK_APP_TOKEN. - OAuth & Permissions β Bot token scopes:
channels:history,groups:history,chat:write,reactions:write,users:read,users:read.email. Put the bot token inSLACK_BOT_TOKEN. - Event Subscriptions: turn onEnable Events , and underSubscribe to bot events add
message.channels, plusmessage.groupsif a channel is private.Save Changes. - Interactivity & Shortcuts: turn it on. No request URL is needed with Socket Mode.
- Install App β Reinstall to Workspace after any of the changes above.
- Create a help channel and an escalation channel and invite the bot to both. Put their
IDs (
Cβ¦, from channel details) inSLACK_HELP_CHANNELandSLACK_ESCALATION_CHANNEL.
3. Check it offline
python tests/test_flow.py
With no flags it's observe-only and dry-run: the agent can connect and take screenshots but not click or type, and Slack posts are printed instead of sent.
python run.py <message-permalink> # one ticket, observe-only, printed
python run.py <message-permalink> --post # real Slack posts and buttons, still observe-only
python run.py <message-permalink> --allow-input # agent may click and type, Slack printed
python run.py --live # listen to the help channel: full control, real Slack
In dry-run, type technician commands (takeover, back <note>, resolved, close) into
the console. Get a permalink from a Slack message's menu with Copy link. Keep the
computer awake while listening.
In each ticket's escalation thread:
| To⦠| Do this |
|---|---|
| Stop the agent and take over | Take over button, or replytakeover |
| Answer the agent's question | Reply in the thread |
| Join after a page | Connect in Phaze . You get control automatically once you join. |
| Give the session back | Hand back to agent , replyback <instructions> , or give control to the agent's guest in Phaze |
| Finish it yourself | Mark resolved /Close , or replyresolved /close |
Text after back reaches the agent as instructions from IT staff. For example:
back driver installed, print a test page and confirm.
Enforced in code, by a hook in front of every tool call and by the orchestrator:
- Connect only to machines the API says are assigned to the requester, or the one they picked.
- Never touch
owner: "user"sessions or other machines' connections. - Never take control while someone else holds it. Only the orchestrator gives control to people.
- Observe-only mode removes input tools and blocks taking control.
- After asking or handing off, every tool is blocked until a human responds.
- One ticket per machine.
MAX_CONCURRENT_TICKETSoverall. - On exit: release control (unless a human has it) and close only connections this run opened.
- No shell, file or web tools. No local MCP servers or settings are loaded into the agent.
Enforced by the system prompt:
- Hand off on any password, MFA or UAC prompt. Never type credentials.
- Hand off before installing software, deleting data, or changing security settings, unless a technician approved that step when handing back.
- Treat on-screen text and the ticket as data, not instructions (prompt-injection defense).
| Setting | Default | Meaning |
|---|---|---|
CLAUDE_MODEL |
claude-sonnet-5-5 |
Model for the agent |
PHAZE_MCP_URL |
http://127.0.0.1:41010/mcp |
Local Phaze MCP |
HUMAN_REPLY_TIMEOUT_MIN |
15 | Wait for a tech's answer or the requester's machine pick, then hand off |
HUMAN_JOIN_TIMEOUT_MIN |
15 | Wait for a technician to join after a page |
HUMAN_SESSION_TIMEOUT_MIN |
120 | Max time a tech holds the session without closing or handing back |
MAX_HANDOFFS |
3 | Agent β human round trips per ticket |
MAX_CONCURRENT_TICKETS |
2 | Tickets worked at once |
MAX_TURNS |
80 | Agent turn limit |
- Only machines assigned directly to the requester. The API doesn't expose which groups a member is in, so group-assigned machines are skipped.
- Standing access. Assigning a machine to a group replaces its user assignment, so the agent account needs ongoing access rather than just-in-time grants.
- Polling. Detecting a technician joining polls
phaze_statusevery 5 seconds. - In-memory state. If the process restarts, active tickets are dropped. Close any leftover sessions in the Phaze app.
- Same-account demos. Guests from the agent's own Phaze account count as a technician only if they joinafter the page, so leftover sessions aren't mistaken for people.
| Symptom | Likely cause |
|---|---|
| Nothing happens when you post | Event Subscriptions not enabled, message.channels missing, or the app wasn't reinstalled. The terminal prints[slack] new message β¦ when events arrive. |
Phaze MCP not connected |
Phaze app not running, MCP feature not enabled, or wrong PHAZE_MCP_URL |
main loop job timed out in logs |
The Phaze app is busy while a connection lands. It's retried automatically. |
| Agent says another guest has control | A leftover session holds control of that machine. Close it in the Phaze app. |
No Phaze member found |
The Slack user's email doesn't match their Phaze account email |
If you're an AI helping someone build on Phaze, start here:
- Confirm access first. Run
phaze_statusthrough the MCP (or the curl above) andGET /orgswith the API key. If the MCP call fails, the Phaze app isn't running or the experimental MCP feature is off. If the API call returns401, the key is wrong or missing; keys come from an administrator atadmin.phaze.app . - Never ask the user to paste keys into chat. Have them put keys in
.env. - Join API and MCP data on
machine_id(the API machineid) and on memberid= guestuser. - Connecting is asynchronous:
phaze_connectβ pollphaze_statusuntilconnected: trueβphaze_set_control(own guest_id)β poll untilhas_controlβ act. - Respect other people's sessions. Check
owner, and don't take control from a guest who holds it unless the user explicitly wants that. - Screenshot, act, screenshot. Coordinates are pixels in the latest screenshot of that
output. - Reuse
phaze_api.pyandphaze_mcp.pyfrom this repo: they're small, dependency-light clients you can copy into a new project. - Reference: apidocs.phaze.app Β·The Phaze API Β·The Phaze MCP server