Documentation
The control plane for ConnectWise MSPs.
Your AI does the busywork across your whole ConnectWise stack — and never changes anything until a human approves. Here's how it fits together, how to set it up, and everything it can do.
#Overview
Most automation tools bolt onto one product and stop there. Pulsatrix connects to the whole ConnectWise suite at once and exposes it as a single set of well-described actions your AI assistant can call — reading data freely, and proposing every change for your approval first.
Here's how the pieces fit together — you direct your LLM, it works through the Pulsatrix MCP into ConnectWise, and high-risk actions route to your phone for a signed approval, with the AI never in that path:
#Prerequisites & setup
Pulsatrix talks to ConnectWise with credentials you create. The golden rule: give it a dedicated, least-privilege identity per product — never your own personal login — so you can scope it tightly, audit it, and revoke it without disturbing anyone's day-to-day access.
Create a dedicated API Member (an API-only account, not a real person) with its own Security Role, then generate an API key pair:
- Manage → System → Members → API Members → new API member; assign it a dedicated Security Role (see the table below).
- On that member: API Keys → new key → that's your
CW_PUBLIC_KEY/CW_PRIVATE_KEY(the private key is shown once — store it as a secret). - Your site URL and company id become
CW_API_URL/CW_COMPANY_ID(addCW_AUTH_PREFIXonly if your tenant uses an authorization prefix).
Create an OAuth2 API client (client ID + secret) scoped to the device reads and the patch/script operations you actually want. Maps to CW_RMM_CLIENT_ID / CW_RMM_CLIENT_SECRET (+ CW_RMM_API_URL).
In Sell admin, generate an access key + API key pair. Maps to CW_CPQ_ACCESS_KEY / CW_CPQ_PUBLIC_KEY / CW_CPQ_PRIVATE_KEY.
Install the REST API extension, set a CTRLAuthHeader secret on it, and note its extension GUID. Maps to CW_SC_URL / CW_SC_AUTH_SECRET / CW_SC_EXTENSION_ID. This connector can run commands on endpoints — treat its secret accordingly.
Exact menu paths shift between product versions — check current ConnectWise docs for the click-path. The variable mapping above is what Pulsatrix reads, and won't change.
Scope each identity to the least it needs. Whatever you grant, Pulsatrix adds a second layer — the riskiest writes are gated regardless of the role (see below) — but the role is your first and hardest boundary.
| Product | Create | Recommended baseline | If over-scoped |
|---|---|---|---|
| PSA (Manage) | Dedicated API member + custom Security Role | Inquire across the modules you'll use; Add/Edit only where you want writes (tickets, time, notes); keep Delete off; grant Finance only if you'll bill | The role is the AI's blast radius. Finance + Delete are the dangerous grants |
| RMM (Asio) | OAuth2 API client | Device read + only the patch/script scopes you need | Script scope = remote code execution on endpoints |
| CPQ (Quosal) | Sell API key pair | Quote read/write | Can create and modify financial documents |
| ScreenConnect | REST extension credentials | Enable only if you want remote sessions at all | Full remote control of client machines — the largest blast radius of any connector |
Two independent guards sit in front of every action: the confirm gate (any write needs confirm:true, otherwise you get a dry-run preview), and — when the approval app is enabled — the hard-gated floor (the riskiest actions need a signed human verdict no matter what the role or ruleset allows).
confirm, and the high-risk floor always needs a signed human approval.| Action class | Examples | Risk | Gating |
|---|---|---|---|
| Read / inquire | get ticket, list devices, dashboards, analytics | Low | Never gated — runs freely |
| Routine write | add note, log time, update ticket status or priority | Medium | confirm:true (dry-run preview otherwise) |
| Remote execution | sc_run_command, sc_send_command, sc_create_session, pkg_uninstall, remote_session, run_remediation | High | Hard-gated — signed human approval |
| Finance write | create_invoice, post_invoice, generate_batch, approve_expense, approve_po | High | Hard-gated |
| Destructive / raw | every cw_api DELETE, writes to blocked paths | High | Hard-gated |
Hard-gated means out-of-band and fail-closed: even a prompt-injected model that asks nicely can't get through, and if the approval broker is unreachable the action is denied. Details in Security & privacy.
#Install the MCP
Pulsatrix ships as a single MCP server — one stdio process your AI client launches on demand. There are two ways to install it.
Download the pulsatrix-<version>.mcpb from the latest release and double-click it. Claude Desktop installs the server and prompts for each setting in its UI, storing secrets in the OS keychain. Build your own with npm run package:mcpb.
# clone, install, build → dist/index.js
cd pulsatrix/mcp
nvm use && npm install
npm run build
The server then launches with node /abs/path/to/pulsatrix/mcp/dist/index.js — that command goes into your client's config below.
Configuration is environment variables. PSA (Manage) is required; RMM, CPQ and ScreenConnect are optional and light up their tools only when set.
| Variable | Req. | What it is |
|---|---|---|
CW_API_URL | ✓ | e.g. https://na.myconnectwise.net/v4_6_release/apis/3.0 |
CW_COMPANY_ID | ✓ | Company identifier (UUID, or legacy short code) |
CW_PUBLIC_KEY / CW_PRIVATE_KEY | ✓ | Manage API key pair (Members → API Keys) |
CW_AUTH_PREFIX | — | Header prefix like ACME+, if your tenant uses one |
CW_RMM_* / CW_CPQ_* / CW_SC_* | — | Enable RMM (Asio), CPQ (Quosal) and ScreenConnect |
The full variable reference (analytics, locale, resolver, tenant tuning) lives in the repo's .env.sample. Ask the running server cw_meta(view:"status") to see what loaded.
#Set up the broker
The approval broker is optional — you only need it for the out-of-band human-approval loop, where gated actions wait for a tap in the Pulsatrix app. Without it the MCP still reads freely and dry-run-previews every write.
It's a Docker Compose stack (broker + Postgres + a Caddy edge):
cd pulsatrix/broker
# put local TLS material + secrets under broker/.secrets/ (gitignored)
docker compose up --build
Then turn on the approval hook by setting these on the MCP server's environment:
PULSATRIX_APPROVAL_URL=https://your-broker.example # https only PULSATRIX_APPROVAL_ENROLL_TOKEN=… # or mTLS client cert + key PULSATRIX_APPROVAL_BROKER_PUBLIC_KEY=… # verifies signed grants PULSATRIX_APPROVAL_MODE=enforce # enforce | observe | dry-run
The hook activates only when PULSATRIX_APPROVAL_URL (https) and one auth method are both set — anything half-configured refuses to start, loudly. If the broker is unreachable, gated actions deny (fail-closed). Full local dev loop: docs/specs/app/90-dev-loop-runbook.md.
#Add it to your AI client
Every MCP client uses the same contract: a stdio server launched with node dist/index.js and the env vars above. Only the config location changes. The base config:
{
"mcpServers": {
"pulsatrix": {
"command": "node",
"args": ["/abs/path/to/pulsatrix/mcp/dist/index.js"],
"env": {
"CW_API_URL": "https://na.myconnectwise.net/v4_6_release/apis/3.0",
"CW_COMPANY_ID": "…",
"CW_PUBLIC_KEY": "…",
"CW_PRIVATE_KEY": "…"
}
}
}
}
| Client | Where the config goes |
|---|---|
| Claude Desktop | claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/) — or just double-click the .mcpb bundle. |
| Claude Cowork | Add through Cowork's connector / MCP settings, using the command + env above. |
| Claude Code | .mcp.json at the project root, or claude mcp add pulsatrix node /abs/…/dist/index.js. Tools appear as mcp__pulsatrix__cw_*. |
| opencode | ~/.config/opencode/config.json → an "mcp" entry with "type":"local", command as an array, and environment (instead of env). |
| Hermes · Openclaw · Pi | Standard stdio MCP clients — point their MCP config at the same node dist/index.js command and env. |
| Cursor | ~/.cursor/mcp.json (global) or a workspace .cursor/mcp.json — same shape as Claude Desktop. |
Any spec-compliant MCP client works — the contract is just stdio transport, the node dist/index.js command, and the ConnectWise env vars. Verify any client by asking it to call cw_meta with view: "status".
#Quickstart
Once the MCP is installed and added to your client, confirm it's wired up. Ask your assistant to run:
cw_meta(view: "status")
It reports which connectors loaded — PSA is required; RMM, CPQ and ScreenConnect light up only when their env vars are set. Then try a front door for your role:
| Role | First call |
|---|---|
| Technician — your day | cw_dashboards(action:"my_day", params:{member}) |
| Dispatcher — the queue | cw_dashboards(action:"dispatch", params:{scope:"today"}) |
| Service manager | cw_dashboards(action:"daily_briefing") |
| vCIO — one client | cw_dashboards(action:"brief", params:{scope:<company_id>}) |
You don't have to phrase calls yourself — just talk to your assistant in plain English ("what's on my plate today?"). The calls above are what it runs under the hood. Every action's full help is one call away: cw_<category>(action:"help", for:"<name>").
#ConnectWise connectors
ConnectWise is where we started. Each connector is fully wired into the action catalog — no extra setup per module.
Service tickets, companies and contacts, agreements, time entries and billing — the operational core of the shop.
Device inventory, monitoring and health, plus remote scripts to diagnose and fix endpoints without leaving the assistant.
Build quotes and proposals, manage line items, and spin up renewal quotes straight from expiring agreements.
Launch and manage remote sessions to reach the endpoint directly — every connection a gated, approve-first action.
#Capabilities
Every connector feeds one shared action catalog — 447 actions across 11 areas. Read-only actions (300) run on their own; the rest are state-changing and always wait for your approval.
Create, triage, route, update and close service tickets — notes, statuses, SLAs and escalations.
Log, review, edit and bill time entries against tickets and agreements; spot unbilled work.
Company and contact records, configurations, plus health, risk and retention analytics.
Invoices, accounts receivable, billing, revenue rollups and procurement insight.
Members, schedules, workload and dispatch — see who's free and what's on their plate.
Quotes and proposals, line items, and renewal quotes generated from agreements.
Device inventory, monitoring and remote scripts via the Asio connector.
Opportunities, sales activities and pipeline visibility across your accounts.
Contracts and additions, MRR, renewals, utilization and coverage gaps.
Project phases, tickets and tracking for larger pieces of work.
Cross-module rollups and at-a-glance metrics for the whole tenant.
Counts reflect the current release catalog and grow with every update.
#Actions & chaining
Your assistant sees 14 tools — one per product area, plus a few fixed ones. Each tool exposes 25–45 actions through an action parameter — ~440 in total. You never memorize them: the model picks the action, and the MCP describes itself.
| Tool(s) | What they cover |
|---|---|
cw_tickets · cw_time · cw_companies · cw_agreements · cw_projects · cw_sales · cw_finance · cw_team · cw_dashboards | PSA (Manage) daily workflows + analytics |
cw_rmm | Asio devices, patches, scripts + the ScreenConnect bridge (sc_*) |
cw_cpq | Quosal Sell quotes, lines, templates, catalog |
cw_api | Raw API escape hatch (confirm-gated, path block-list) |
cw_calc | Hardened math — closes the LLM decimal-arithmetic gap |
cw_meta | Introspection: catalog, status, summary, improvements |
The win is that a multi-step ConnectWise workflow collapses into a single action. Triaging a ticket with its notes, tasks, time entries and the RMM health of the device behind it:
cw_tickets(action: "get", params: { id: 12345, depth: "full" })
One call instead of five raw API hits across PSA and RMM. Every write needs confirm: true — without it you get a dry-run preview, never a real change.
Some actions span two products and resolve IDs across them for you — and when a match is ambiguous, it's surfaced rather than guessed:
| Workflow | Action |
|---|---|
| Remote session from a ticket (PSA → RMM → ScreenConnect) | cw_tickets(action:"remote_session", params:{ticket_id}, confirm:true) |
| Run remediation on the device behind a ticket | cw_tickets(action:"run_remediation", params:{ticket_id, command}, confirm:true) |
| Find recurring issues on one device | cw_tickets(action:"recurring_on_device", params:{device_id}) |
| PSA opportunity → CPQ quote shell | cw_sales(action:"draft_quote", params:{opportunity_id}, confirm:true) |
| Expiring agreement → renewal quote | cw_agreements(action:"renewal_quote", params:{agreement_id}, confirm:true) |
| Company fleet health summary | cw_companies(action:"fleet_health", params:{id}) |
Beyond these built-in cross-product calls, the assistant chains actions itself across a conversation: read a ticket → pull the device → propose a script → log the time → update the ticket — pausing for your approval on anything that changes state.
Pulsatrix is self-documenting — there's no external manual to keep in sync. Three calls reveal the whole surface in-session:
cw_meta(view: "catalog") # all 14 tools + workflows + gotchas cw_<category>() # the action table for one area cw_<category>(action: "help", for: "<name>") # schema + examples for one action
#Recipes
Real flows — each one is a single conversation with the assistant. Reads run freely; the 📱 gated step is the moment your phone buzzes with full redacted context for a Face/Touch-ID approval (when the approval loop is on).
| Scenario | What the assistant does | 📱 Gated step |
|---|---|---|
| "Ticket #12345 says the server is slow — fix it." | Reads ticket + notes + time + RMM health of the device, diagnoses a stuck service, proposes the fix | Restart command — you see the exact command before approving |
| Overnight patch remediation | Lists failed patches across the fleet, picks the remediation script, queues per-device runs | Each script run lands as an approval card — approve from bed, or deny |
| "This printer issue keeps coming back." | Correlates tickets on the same endpoint, drafts a root-cause note, proposes a permanent fix | The script execution — the diagnosis itself is free |
| Billing day on autopilot | Flags missing time, agreement drift, unbilled work; fixes records and assembles invoices | Invoice / batch posting — money never moves without a signed verdict |
| Renewal quote in one breath | Expiring agreement → renewal quote with uplift → CPQ quote shell with catalog lines | Sending the quote — drafts are free, outbound is gated |
| Emergency CVE response | Finds every endpoint running the vulnerable package, builds the uninstall plan | Each uninstall — fleet-wide, one tap per device or batch-approve |
| Autonomous night shift, auditable | A local model works tickets overnight in observe mode — every would-be-gated action logged, nothing blocked | Next morning: review the activity feed, flip to enforce when you trust it |
#Skills
You don't need skills to use Pulsatrix. The product works on its own — just talk to your assistant in plain English (see Quickstart). Skills are an optional layer on top: a way to codify a recurring business process — your morning triage, your billing review, your renewal sweep — so the assistant runs it the same way every time, the way your shop wants it done.
They're plain-Markdown playbooks you import into Claude (Code or Desktop) — starter examples, not bundled with the MCP, yours to edit or replace. Each follows the rule Pulsatrix enforces anyway: read freely, propose changes, let the human approve.
Download a skill's SKILL.md into a folder of the same name under your skills directory:
~/.claude/skills/<skill-name>/SKILL.md # personal — available in every project .claude/skills/<skill-name>/SKILL.md # per-project — committed with the repo
Claude picks it up on the next session. It just needs the Pulsatrix MCP configured (see Add it to your AI client).
| Skill | What it does | Get it |
|---|---|---|
msp-morning-triage | Review your day + triage new tickets with full context, then propose changes you approve | SKILL.md |
msp-weekly-prebilling | Validate the week — missing time, agreement drift, unbilled work — and prep invoices for approval | SKILL.md |
msp-renewal-sweep | Find expiring agreements and draft renewal quotes (gated), so nothing lapses | SKILL.md |
msp-morning-triage---
name: msp-morning-triage
description: Use at the start of a service-desk shift to review the day
and triage new ConnectWise tickets through the Pulsatrix MCP.
---
# MSP morning triage
You have the Pulsatrix MCP (ConnectWise) available. Gather context
generously, then propose changes the human approves.
1. Pull the day: cw_dashboards(action:"my_day", params:{ member })
2. Show the queue: cw_dashboards(action:"dispatch", params:{ scope:"today" })
3. Read each ticket in full:
cw_tickets(action:"get", params:{ id, depth:"full" })
4. Propose a category, priority and one-line plan per ticket. Wait.
5. Apply on approval only (confirm:true) — each write is gated.
Rules: reads are free; never change a ticket, post a note, or touch a
device without the technician's explicit OK.
Skills are plain Markdown — rename them, edit the steps, or write your own. They live on your machine; Pulsatrix never ships or requires them.
#Approvals & safety
Pulsatrix keeps the human in the loop. The LLM never touches ConnectWise directly — it goes through the MCP, and anything that changes state is gated behind your approval.
Prompt the LLM of your choice in plain language — "what's overdue for ACME?", "rebuild this mailbox", "quote a renewal".
The assistant calls Pulsatrix to pull what it needs across your tools. Read-only actions run freely — no interruptions for looking things up.
Any state-changing action is gated: Pulsatrix asks you directly to approve — bypassing the AI entirely. Approve, and only then does it run. Decline, and nothing happens.
Results flow back up to the assistant, which explains what it did — with the ticket, time entry or change already in ConnectWise.
#Security & privacy
confirmEvery non-GET action requires confirm: true. Without it the dispatcher returns a dry-run preview and never builds the upstream request — enforced before any HTTP call is made, and locked by a release-gate test.
When the approval hook is on, these are always gated, regardless of any ruleset:
- ScreenConnect remote execution —
sc_run_command,sc_send_command,sc_create_session,pkg_uninstall - Ticket-driven remote access —
remote_session,run_remediation - Finance writes —
post_invoice,create_invoice,generate_batch,approve_expense,approve_po - Every
cw_apiDELETE and any write to a blocked path — even if a prompt-injected model asks nicely
Approval requests carry PII-redacted context. Verdicts are ES256-signed by a Secure Enclave key that never leaves your device; grants are single-use with a 30-second TTL. Nothing the model says — or a prompt injection attempts — can forge an approval. Fail-closed: broker unreachable, timeout, bad signature or expired grant ⇒ the action does not run.
Every captured string is scanned twice before it's ever stored — a key-based drop and a value regex (email, phone, Luhn card numbers, SSN/SIN, IPv4, JWT, cloud-provider tokens).
Usage is captured to a local SQLite database (90-day retention) that powers the self-improvement view — it never leaves your machine. Turn it off entirely with CW_ANALYTICS_DISABLED=true.
#Configuration
All configuration is environment variables, set in your client's MCP config (or via the MCPB UI). Set each variable in exactly one place. The full reference with defaults lives in the repo's .env.sample; the essentials:
| Variable | Notes |
|---|---|
CW_API_URL | Full URL incl. /v4_6_release/apis/3.0 |
CW_COMPANY_ID | Company identifier (UUID or legacy short code) |
CW_PUBLIC_KEY / CW_PRIVATE_KEY | Manage API key pair |
CW_AUTH_PREFIX | Header prefix like ACME+ (if your tenant uses one) |
CW_CLIENT_ID | Only for tenants that enforce the clientId header |
CW_TIMEZONE | IANA tz for display only (default America/New_York); CW stores UTC |
| Variable | Enables |
|---|---|
CW_RMM_CLIENT_ID / CW_RMM_CLIENT_SECRET (+ CW_RMM_API_URL) | cw_rmm — Asio devices, patches, scripts |
CW_CPQ_ACCESS_KEY / CW_CPQ_PUBLIC_KEY / CW_CPQ_PRIVATE_KEY | cw_cpq — Quosal Sell quoting |
CW_SC_URL / CW_SC_AUTH_SECRET / CW_SC_EXTENSION_ID | ScreenConnect sc_* actions inside cw_rmm |
A missing optional connector doesn't break anything — its tool is still registered but returns a structured "not configured" error; PSA keeps working.
| Variable | Notes |
|---|---|
PULSATRIX_APPROVAL_URL | Broker base URL — https only; absent = hook disabled |
PULSATRIX_APPROVAL_ENROLL_TOKEN | Enrollment token (or use the mTLS cert/key pair instead) |
PULSATRIX_APPROVAL_BROKER_PUBLIC_KEY | ES256 public key that verifies signed grants |
PULSATRIX_APPROVAL_MODE | enforce (default) · observe · dry-run |
PULSATRIX_APPROVAL_FAIL_MODE | closed (default) — broker down ⇒ gated actions deny |
PULSATRIX_APPROVAL_TIMEOUT_MS | How long a human has to answer (default 90 000) |
| Variable | Notes |
|---|---|
CW_ANALYTICS_DISABLED | true skips all local capture |
CW_ANALYTICS_PATH | SQLite path (default ~/.config/pulsatrix/data/analytics.db) |
CW_RESOLVER_DB_PATH / CW_RESOLVER_TTL_SECONDS | Cross-product ID cache (default 4 h) |
#FAQ & troubleshooting
Whichever you point at it. Pulsatrix is a standard MCP server, so it works with the Claude you already pay for (Desktop, Code, Cowork), or any spec-compliant client — including local models. The tool surface is tuned to stay accurate even on local-class models (Qwen-35B target).
No. Every non-GET action needs confirm: true; without it you get a dry-run preview, never a real call. With the approval hook on, the riskiest actions also require a signed human verdict.
Local-class models lose tool-selection accuracy past ~15 tools. Pulsatrix groups everything into 14 tools (one per product area) that each expose 25–45 actions via an action parameter — discoverable through cw_meta.
That connector's env vars aren't set. RMM, CPQ and ScreenConnect are optional — set their variables (see Configuration) to light them up. PSA keeps working regardless.
By design — it refuses any half-configured state. It activates only when PULSATRIX_APPROVAL_URL (https) and one auth method (enrollment token, or mTLS cert + key) are both set. Check the startup log line for what's missing.
Ask the assistant to run cw_meta(view:"status") — it lists what loaded and flags anything misconfigured.
Yes — CW_ANALYTICS_DISABLED=true. The server still works; nothing is captured.
No. Pulsatrix is an independent project. ConnectWise, Manage, Asio, Quosal Sell and ScreenConnect are trademarks of ConnectWise, LLC, used here for descriptive interoperability only.
#Custom connectors
We're all-in on ConnectWise — the aim is to cover your PSA, RMM, CPQ and remote stack end to end, not to chase every tool on the market. That focus is exactly why the integration runs deep instead of skin-deep.
Need something ConnectWise-adjacent?
If a tool your shop can't work without plugs into your ConnectWise workflow, tell us what it should do — we build bespoke connectors case by case.
Request a connector
PULSATRIX