{"id":"61108ebb-d360-4681-9a83-fe0a7e9cb48a","slug":"taddiemason-connectwise-mcp-server","kind":"mcp","name":"Connectwise-MCP-Server","summary":"A Docker-based Model Context Protocol (MCP) bridge that enables AI-powered natural language access to ConnectWise Manage data through OpenWebUI.","owner":"taddiemason","repoUrl":"https://github.com/taddiemason/Connectwise-MCP-Server","license":"MIT","licenseTier":"permissive","trust":"community","stars":5,"match":null,"fewStars":false,"format":"mcp","createdAt":"2025-11-18T21:04:51.000Z","addedAt":"2026-10-21T16:17:57.570Z","logoUrl":"https://avatars.githubusercontent.com/u/137760738?u=09ba968d135eeab079868d369d54a98fb448f0d5&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["cloud-infra","data-analytics","memory-knowledge"],"tags":[],"runtime":"binary","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-26T20:28:40.892Z","sample":false,"url":"https://conduits.dev/tools/taddiemason-connectwise-mcp-server","language":"Python","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# ConnectWise MCP Server\n\n## Overview\n\nA Model Context Protocol (MCP) server giving AI agents access to ConnectWise Manage. **Read-only by default**; writes exist but must be switched on per category — see [Writes](#writes). One Python process serves two transports:\n\n```\n┌──────────────────┐  stdio  ┐\n│  Claude Code /   │─────────┤\n│  Claude Desktop  │         │   ┌──────────────────┐  HTTPS  ┌────────────────┐\n└──────────────────┘         ├──►│   ConnectWise    │────────►│  ConnectWise   │\n┌──────────────────┐  HTTP   │   │   MCP Server     │         │   Manage API   │\n│    OpenWebUI     │─────────┘   └──────────────────┘         └────────────────┘\n└──────────────────┘ :3002\n```\n\n- **stdio** — add it to Claude Code or Claude Desktop like any other MCP server.\n- **HTTP** — serves OpenWebUI, including a `/v1/tools/execute` route kept for compatibility with tools written against the old Node bridge.\n\n### Changed in 2.0.0\n\nThe previous release **did not run**. `requirements.txt` specified `mcp>=1.0.0`; once mcp 2.x shipped it removed the decorator API the server was written against, so a fresh install failed at import with `AttributeError: 'Server' object has no attribute 'list_tools'` and the container could not start.\n\nFixing that pulled several other things with it:\n\n- **Dependencies are pinned** with upper bounds. The unbounded range was the bug.\n- **The Node bridge is gone.** It spawned a fresh Python process per request, invoked `python` in an image that only installed `python3`, and sent `tools/call` without an MCP `initialize` handshake. FastMCP serves HTTP directly, so all of that went with it.\n- **One container instead of two.** The old `connectwise-mcp-server` service ran a stdio server with nothing attached to stdin, so it exited and was restarted forever while doing no work.\n- **Credentials are no longer needed to import.** The client is built on first use, so the tool list can be inspected and the whole surface tested without keys.\n- **There is now a test suite** that runs with no credentials and no network.\n\nAll 30 tool names are unchanged, and `/v1/tools/execute` returns the same shape it used to, so existing OpenWebUI installs keep working.\n\n## Key Features\n\n**Company Management:**\n- Search companies with advanced filtering\n- Retrieve complete company information\n- View company contacts and relationships\n\n**Service Tickets:**\n- Search tickets using ConnectWise conditions\n- Retrieve full ticket details including notes and history\n- Filter by status, company, date, and more\n\n**Contact & Sales:**\n- Search and retrieve contacts with company associations\n- View sales opportunities and pipeline tracking\n- Access opportunity details and status\n\n**Agreements & Time Tracking:**\n- Search agreements and contracts\n- View agreement additions and add-ons\n- View detailed time tracking data\n- Filter time entries by member, date, or ticket\n\n**Project & Activity Management:**\n- Search projects and track progress\n- View activities and tasks\n- Access team member information\n\n**IT Asset Management:**\n- Search and view IT configurations/assets\n- Retrieve configuration types for asset categorization\n- Access company sites and locations\n- Track hardware and software inventory\n\n**Finance & Billing:**\n- Search and retrieve invoices\n- View expense entries and reports\n- Access billing cycles for recurring billing\n- Review agreement additions and billing details\n\n**Reference Data:**\n- Company types and statuses for categorization\n- Ticket priorities and sources\n- Contact types for organization\n- Service board configurations and statuses\n\n**Service Desk Enhancements:**\n- View service boards and workflows\n- Access board-specific statuses\n- Retrieve ticket tasks and checklists\n- View scheduled work for tickets\n\n**AI Integration:**\n- Natural language command support in OpenWebUI\n- Read-only access to all ConnectWise data\n- Advanced filtering and search capabilities\n\n## Prerequisites\n\n- Docker and Docker Compose installed\n- ConnectWise Manage API credentials (Company ID, Public Key, Private Key)\n- OpenWebUI running (any version with Tools support)\n\n## Quick Start\n\n### 1. Clone Repository\n```bash\ngit clone https://github.com/taddiemason/Connectwise-MCP-Server\ncd ConnectWise-MCP-Server\n```\n\n### 2. Obtain ConnectWise Credentials\n\nTo get your ConnectWise API credentials:\n\n1. Log in to your ConnectWise Manage instance\n2. Navigate to **System → Members**\n3. Create a new API Member or use existing credentials\n4. Generate API Keys:\n   - Go to **My Account → API Keys**\n   - Click **New** to create new API keys\n   - Save the **Public Key** and **Private Key**\n5. Your **Company ID** is typically shown in the URL or system settings\n\n**Important:** For read-only access, ensure the API member only has read permissions in ConnectWise.\n\n### 3. Configure Credentials\n\nCreate a `.env` file from the example:\n```bash\ncp .env.example .env\n```\n\nEdit `.env` and add your ConnectWise credentials:\n```bash\n# ConnectWise API Credentials (required)\nCW_COMPANY_ID=your_company_id_here\nCW_PUBLIC_KEY=your_public_key_here\nCW_PRIVATE_KEY=your_private_key_here\n\n# ConnectWise API Configuration\nCW_API_URL=https://api-na.myconnectwise.net\nCW_API_VERSION=v2023.2\nCW_CLIENT_ID=mcp-connectwise-server\n\n# MCP Bridge Server Port (default: 3002)\nMCP_PORT=3002\n\n# Log level (DEBUG, INFO, WARNING, ERROR)\nLOG_LEVEL=INFO\n```\n\n**API URL Configuration:**\n- North America: `https://api-na.myconnectwise.net`\n- Europe: `https://api-eu.myconnectwise.net`\n- Australia: `https://api-au.myconnectwise.net`\n- On-premise: Use your server URL\n\n### 4. Start Services\n\n**Using the setup script (recommended):**\n```bash\nchmod +x setup.sh\n./setup.sh\n```\nSelect option 1 to start the server.\n\n**Or manually with Docker Compose:**\n```bash\ndocker-compose up -d --build\n```\n\nBridge available at: **http://localhost:3002**\n\n### 5a. Use it from Claude Code or Claude Desktop\n\nNo Docker needed — stdio is the default transport. Install into a venv so the\ninterpreter you point at definitely has the dependencies:\n\n```bash\npython -m venv .venv\n.venv/bin/python -m pip install -r requirements.txt    # .venv\\Scripts\\python.exe on Windows\n```\n\n**Claude Code**, one command:\n\n```bash\nclaude mcp add connectwise \\\n  -e CW_COMPANY_ID=your_company_id \\\n  -e CW_PUBLIC_KEY=your_public_key \\\n  -e CW_PRIVATE_KEY=your_private_key \\\n  -- /absolute/path/to/.venv/bin/python /absolute/path/to/connectwise_mcp.py\n```\n\nCheck it took:\n\n```bash\nclaude mcp list        # connectwise: ... - ✔ Connected\n```\n\n**Claude Desktop**, or Claude Code via file: copy `.mcp.json.example` to\n`.mcp.json` and fill it in. Claude Desktop takes the same `mcpServers` block in\n`claude_desktop_config.json`.\n\nTwo things that bite:\n\n- **Use absolute paths, and point `command` at the venv's Python.** The MCP\n  client does not inherit your shell, your `PATH` or your activated venv. A bare\n  `python` will often be a different interpreter without `fastmcp` installed.\n- **That config file holds live API keys in plaintext** — the same keys that\n  read every client's tickets, contacts and agreements. `.mcp.json` and\n  `claude_desktop_config.json` are both gitignored here for that reason. Commit\n  the `.example`, never the real one.\n\nCredentials are only needed to *call* a tool. The server starts and lists its 30\ntools without them, and an uncredentialled call fails with a message naming the\nvariables that are missing rather than a stack trace.\n\n### 5b. Add Tool to OpenWebUI\n\n1. Open OpenWebUI in your browser\n2. Navigate to **Settings → Admin Panel → Tools**\n3. Click **\"+ Create New Tool\"**\n4. Copy the contents of `connectwise_tools.py` from this repository\n5. Paste into the tool editor\n6. Configure the bridge URL in the Valves section:\n   - Docker: `http://connectwise-mcp-bridge:3002`\n   - Local: `http://localhost:3002`\n7. Click **\"Save\"**\n8. Enable the ConnectWise Tools\n\n### 6. Verify Installation\n```bash\ncurl http://localhost:3002/health\n```\n\nExpected response:\n```json\n{\n  \"status\": \"ok\",\n  \"service\": \"connectwise-mcp-bridge\"\n}\n```\n\n## Available Tools in OpenWebUI\n\n**Company Tools:**\n- `get_companies()` - Search companies with advanced filtering\n- `get_company()` - Get specific company details\n- `get_company_sites()` - Get sites/locations for a specific company\n- `get_company_types()` - Get all company types for categorization\n- `get_company_statuses()` - Get all company statuses\n\n**Ticket Tools:**\n- `get_tickets()` - Search tickets with conditions\n- `get_ticket()` - Get specific ticket details\n- `get_ticket_notes()` - Retrieve ticket notes and history\n- `get_ticket_tasks()` - Get tasks/checklist items for a ticket\n- `get_ticket_schedules()` - Get scheduled work for a ticket\n- `get_ticket_priorities()` - Get all ticket priorities\n- `get_ticket_sources()` - Get all ticket sources\n\n**Contact Tools:**\n- `get_contacts()` - Search contacts\n- `get_contact()` - Get specific contact details\n- `get_contact_types()` - Get all contact types\n\n**Sales Tools:**\n- `get_opportunities()` - Search sales opportunities\n\n**Agreement Tools:**\n- `get_agreements()` - Search agreements and contracts\n- `get_agreement_additions()` - Get add-ons for a specific agreement\n\n**Time Entry Tools:**\n- `get_time_entries()` - Search time tracking data\n\n**Project Tools:**\n- `get_projects()` - Search projects\n\n**Activity Tools:**\n- `get_activities()` - Search activities and tasks\n\n**Member Tools:**\n- `get_members()` - Search team members\n\n**IT Asset Management Tools:**\n- `get_configurations()` - Search IT configurations/assets\n- `get_configuration()` - Get specific configuration by ID\n- `get_configuration_types()` - Get all configuration types\n\n**Finance & Billing Tools:**\n- `get_invoices()` - Search and retrieve invoices\n- `get_expense_entries()` - Search expense entries\n- `get_billing_cycles()` - Get all billing cycles\n\n**Service Desk Tools:**\n- `get_service_boards()` - Get all service boards\n- `get_board_statuses()` - Get statuses for a specific board\n- `get_board_teams()` - Teams defined on a board\n- `get_board_types()` / `get_board_subtypes()` - Ticket types and subtypes on a board\n- `get_ticket_configurations()` - Which IT assets a ticket is actually about\n- `get_slas()` - Service level agreements and their targets\n\n**Documents:**\n- `get_documents(recordType, recordId)` - Attachments on a record. Both arguments are required — `/system/documents` returns 400 without them. `recordType` is `Ticket`, `Company`, `Contact`, `Opportunity`, `Project`, `Agreement`, `Configuration` and so on. Metadata only, not file contents.\n\n**Scheduling:**\n- `get_schedule_entries()` - Scheduled work across the business, filterable by member or date\n\n**Notes:**\n- `get_company_notes()` / `get_contact_notes()` - Notes on a company or contact\n- `get_company_note_types()` - Note type reference data\n- `get_company_teams()` - Who is assigned to a company\n\n**Procurement:**\n- `get_purchase_orders()` / `get_purchase_order_line_items()`\n- `get_product_catalog()` - What can be sold\n- `get_products()` - What has been sold, on tickets, projects and agreements\n- `get_manufacturers()` / `get_product_types()`\n\n**Projects:**\n- `get_project_tickets()` - Project tickets, which are separate from service tickets\n- `get_project_phases()` / `get_project_team_members()`\n\n**System:**\n- `get_departments()`\n\nCollection tools take `conditions`, `orderBy`, `page`, `pageSize` and `fetchAll`. See [Reliability](#reliability) for what `fetchAll` is for.\n\n## Usage Examples\n\nIn OpenWebUI, communicate naturally:\n\n**General Queries:**\n- \"Show me all open tickets for ACME Corp\"\n- \"Find all companies with 'Tech' in their name\"\n- \"Get details for ticket #12345\"\n- \"List all opportunities with status 'Open'\"\n- \"Show me time entries for this week\"\n- \"Find contacts at Microsoft\"\n- \"What projects are currently active?\"\n- \"Show me all agreements expiring this month\"\n\n**IT Asset Management:**\n- \"Show me all servers for ACME Corp\"\n- \"What configurations are assigned to ticket #5678?\"\n- \"List all company sites for XYZ Corporation\"\n- \"What configuration types do we have?\"\n\n**Finance & Billing:**\n- \"Show me all open invoices for this month\"\n- \"What are the expense entries for John this week?\"\n- \"List all billing cycles\"\n- \"What add-ons are included in agreement #123?\"\n\n**Service Desk:**\n- \"What service boards do we have?\"\n- \"Show me all statuses for the Help Desk board\"\n- \"What tasks are on ticket #9999?\"\n- \"Show scheduled work for ticket #1234\"\n\n**Reference Data:**\n- \"What company types are available?\"\n- \"List all ticket priorities\"\n- \"Show me all contact types\"\n- \"What ticket sources do we track?\"\n\nThe AI will automatically call the appropriate ConnectWise tool functions to complete your request.\n\n## ConnectWise API Conditions\n\nConnectWise uses a powerful condition syntax for filtering:\n\n```\n# String equality\nidentifier=\"ACME\"\n\n# Contains (like)\nname like \"%Corp%\"\n\n# Date comparisons\ndateEntered > [2024-01-01]\n\n# Numeric comparisons\nid > 1000\n\n# Multiple conditions\nstatus/name=\"New\" and company/identifier=\"ACME\"\n\n# Nested fields\ncompany/name like \"%Tech%\"\n```\n\n## File Structure\n\n```\nConnectWise-MCP-Server/\n├── connectwise_mcp.py          (the whole server: tools, both transports)\n├── connectwise_tools.py        (OpenWebUI tool)\n├── test_connectwise_mcp.py     (offline test suite)\n├── conftest.py                 (mock ConnectWise fixtures)\n├── pytest.ini\n├── docker-compose.yml          (one service)\n├── Dockerfile\n├── requirements.txt            (pinned)\n├── requirements-dev.txt        (test only)\n├── .env.example\n├── Setup.sh                    (management script)\n├── test.sh                     (live deployment smoke check)\n└── README.md\n```\n\n## Management Commands\n\n**Using setup script:**\n```bash\n./setup.sh\n```\n\nOptions:\n1. Start ConnectWise MCP Server\n2. Stop ConnectWise MCP Server\n3. Restart ConnectWise MCP Server\n4. View logs\n5. Check status\n6. Update server\n7. Clean up\n8. Exit\n\n**Direct Docker commands:**\n```bash\n# Start servers\ndocker-compose up -d --build\n\n# View logs\ndocker-compose logs -f\n\n# View bridge logs only\ndocker-compose logs -f mcp-bridge\n\n# View MCP server logs only\ndocker-compose logs -f connectwise-mcp-server\n\n# Stop servers\ndocker-compose down\n\n# Restart servers\ndocker-compose restart\n\n# Check status\ndocker-compose ps\n```\n\n## Troubleshooting\n\n**Authentication Errors:**\n- Verify your Company ID, Public Key, and Private Key are correct\n- Ensure the API member has appropriate permissions\n- Check that the API URL matches your ConnectWise instance\n- Generate new API keys if needed\n\n**Port Conflicts:**\n\nIf port 3002 is already in use:\n1. Edit `.env` file: `MCP_PORT=3003`\n2. Edit `docker-compose.yml`: Update port mapping to `3003:3002` and environment `PORT=3003`\n3. Restart: `docker-compose down && docker-compose up -d --build`\n4. Update bridge URL in OpenWebUI Tool Valves\n\n**Bridge Connection Issues:**\n- Verify bridge is running: `docker-compose ps`\n- Check bridge logs: `docker-compose logs mcp-bridge`\n- Test health endpoint: `curl http://localhost:3002/health`\n- Verify bridge URL in OpenWebUI Tool Valves settings:\n  - Both in Docker: `http://connectwise-mcp-bridge:3002`\n  - OpenWebUI local: `http://localhost:3002`\n\n**OpenWebUI Can't Connect:**\n- Ensure `connectwise_tools.py` is saved in OpenWebUI Tools\n- Enable the ConnectWise Tools in OpenWebUI Settings\n- Verify the bridge URL is correct in Valves settings\n- Check Docker networks: `docker network connect openwebui_network connectwise-mcp-bridge`\n\n**Rate Limiting:**\n\nConnectWise has API rate limits. If you encounter rate limiting:\n- Reduce the `pageSize` parameter\n- Add delays between requests\n- Check ConnectWise API documentation for current limits\n\n## Security\n\n### The HTTP transport requires a bearer token\n\n`MCP_AUTH_TOKEN` is mandatory — **the server refuses to start on HTTP without it** rather than coming up wide open. Generate one:\n\n```bash\npython -c \"import secrets; print(secrets.token_urlsafe(32))\"\n```\n\nCallers send it on every request:\n\n```bash\ncurl -X POST http://localhost:3002/v1/tools/execute \\\n  -H \"Authorization: Bearer $MCP_AUTH_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"tool_name\": \"connectwise_get_companies\", \"arguments\": {\"pageSize\": 5}}'\n```\n\nIn OpenWebUI, add the same header in the tool's Valves.\n\n`/health` stays unauthenticated so the container healthcheck works; it returns a fixed status string and nothing about your instance. Tokens are compared with `hmac.compare_digest`, so a wrong one cannot be narrowed down by timing.\n\n**The stdio transport has no token** and does not need one — the trust boundary there is the process, not the network.\n\n> Before this, port 3002 answered anyone who could reach it with your entire ConnectWise instance: every client's tickets, contacts, agreements and invoices, no credential required. If you have been running the previous version on a reachable interface, treat that as exposure and rotate the ConnectWise API keys.\n\n### The rest\n\n- Never commit `.env`, `.mcp.json` or `claude_desktop_config.json` — all three hold live API keys and all three are gitignored\n- Use an API-only member with the minimum required permissions\n- **Read-only unless enabled:** every read tool issues `GET`, enforced by a test. Write tools are not registered at all unless `CW_ENABLE_WRITES` names their category\n- Rotate API credentials regularly\n- Restrict IP access to the ConnectWise API where you can\n- Keep the HTTP port on a private network even with the token\n- Errors never echo the API key, and that is tested\n\n## Writes\n\n**This server is read-only by default and stays that way until you say otherwise.** Write tools are not merely refused when disabled — they are not registered, so they never appear in the tool list and a model cannot be talked into calling one it cannot see.\n\nEnable by category, not by boolean:\n\n```bash\nCW_ENABLE_WRITES=notes          # notes on tickets and companies\nCW_ENABLE_WRITES=notes,time     # ...and time entries\nCW_ENABLE_WRITES=all            # ...and creating tickets and changing any field\n```\n\n`CW_ENABLE_WRITES=true` is **not** valid. It enables nothing and logs a warning, because quietly granting every write because a string looked truthy is the worst reading of an ambiguous setting.\n\n| Category | Tools | Risk |\n|---|---|---|\n| `notes` | `add_ticket_note`, `add_company_note` | Low. A wrong note is embarrassing and easy to spot |\n| `time` | `create_time_entry` | Medium. Wrong time affects billing |\n| `tickets` | `create_ticket`, `update_ticket` | **High.** Status changes move work in and out of technicians' queues |\n\n### Before you turn any of it on\n\n**Use dry run first.** `CW_WRITE_DRY_RUN=true` logs what would be sent and returns it, without sending. Worth leaving on for the first day of any category.\n\n**Use a separate API member** with write rights, distinct from your read-only one. ConnectWise's own audit trail then tells you which changes came from here.\n\n**Notes default to internal.** `add_ticket_note` writes to internal analysis unless you pass `internal=False`. An AI-written note appearing in a client's ticket feed unannounced is a surprise worth opting into rather than out of.\n\n**`update_ticket` takes a field path, not an object**, because ConnectWise PATCH is JSON Patch:\n\n```\nupdate_ticket(ticket_id=42, path=\"status/id\", value=5)\n  -> [{\"op\": \"replace\", \"path\": \"status/id\", \"value\": 5}]\n```\n\nSending `{\"status\": {\"id\": 5}}` instead returns 200 and changes nothing — a failure that looks like a success. There is a test pinning the patch shape.\n\n**Writes are never retried on 5xx.** A 500 on a POST does not mean nothing happened; the note may have been created before the error surfaced. Retrying would duplicate it on a customer's ticket. A 429 *is* retried, because it is rejected before processing and cannot duplicate. Reads still retry 5xx as before.\n\n**Every write returns the record it created or changed**, and is logged in full at INFO, so the transcript shows exactly what was done rather than \"ok\".\n\n### Still worth deciding yourself\n\nNothing here restricts writes to tickets the caller named explicitly rather than found via search — a model can pass any id. If that matters for your deployment, `notes` with `internal=True` is the conservative starting point, and `tickets` is the one to leave off longest.\n\n## Reliability\n\n**Retries.** ConnectWise rate-limits. A 429 or a 5xx is retried `CW_MAX_RETRIES` times (default 3) with exponential backoff, honouring `Retry-After` when ConnectWise sends it. Client errors — 400, 401, 403, 404 — are *not* retried, because repeating them only burns the rate limit. Without this a 429 surfaced as a failed tool call, which reads to a model as \"that data does not exist\" rather than \"ask again shortly\".\n\n**Pagination.** Collection tools take `fetchAll`:\n\n```\nget_tickets(conditions='status/name=\"New\"', fetchAll=True)\n```\n\nOff by default, a query matching 400 tickets returns the first 25 and says nothing about the rest — and \"25\" is a wrong answer rather than a partial one. `fetchAll` walks every page, capped at `CW_MAX_RECORDS` (default 1000) so \"every ticket\" cannot become fifty thousand records and a rate-limit ban. The result is still a bare array either way.\n\n**Error messages** name the endpoint, the status, and what to do:\n\n```\nConnectWise service/tickets [401]: authentication was rejected. Check\nCW_PUBLIC_KEY and CW_PRIVATE_KEY, that CW_COMPANY_ID matches the instance,\nand that the API member has read rights to this area. Body: ...\n```\n\n## Development & Testing\n\n**Tests** — no credentials, no network, no Docker. A `httpx.MockTransport` stands in for ConnectWise:\n\n```bash\npip install -r requirements-dev.txt\npytest\n```\n\nThey cover the endpoint and query string each tool produces, the auth headers, error paths (400/401/404/429/500, malformed JSON), that errors never echo the private key, that every tool issues `GET` and nothing else, and that the module imports with no credentials set — the failure that prompted the 2.0.0 rewrite.\n\n**Running it locally:**\n\n```bash\npip install -r requirements.txt\nexport CW_COMPANY_ID=your_company_id\nexport CW_PUBLIC_KEY=your_public_key\nexport CW_PRIVATE_KEY=your_private_key\n\npython connectwise_mcp.py                    # stdio, for MCP clients\nMCP_TRANSPORT=http python connectwise_mcp.py # HTTP on :3002, for OpenWebUI\n```\n\n**Testing API Endpoints:**\n\nHealth check:\n```bash\ncurl http://localhost:3002/health\n```\n\nTest search:\n```bash\ncurl -X POST http://localhost:3002/v1/tools/execute \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"tool_name\": \"connectwise_get_companies\",\n    \"arguments\": {\n      \"conditions\": \"identifier=\\\"ACME\\\"\",\n      \"pageSize\": 5\n    }\n  }'\n```\n\n## Dependencies\n\n- `fastmcp` — MCP server framework; brings `mcp`, `starlette` and `uvicorn` with it\n- `httpx` — async HTTP client\n\nBoth are pinned with upper bounds in `requirements.txt`. That is deliberate: an unbounded `mcp>=1.0.0` is exactly what broke the previous release when a major version shipped. Test-only dependencies live in `requirements-dev.txt`.\n\nNo Node.js. The bridge that needed it is gone.\n\n## Resources\n\n- [ConnectWise Developer Network](https://developer.connectwise.com/)\n- [Model Context Protocol (MCP)](https://modelcontextprotocol.io/)\n- [OpenWebUI](https://openwebui.com/)\n- [ConnectWise REST API Documentation](https://developer.connectwise.com/Products/Manage/REST)\n\n## Important Notes\n\n- **Read-only out of the box.** Write tools are not registered unless `CW_ENABLE_WRITES` names their category — see [Writes](#writes)\n- The HTTP transport requires a bearer token and refuses to start without one\n- Retries rate limits and transient errors; never retries a write on 5xx\n- Requires valid ConnectWise API credentials to call a tool, though not to start\n\n## Contributing\n\nContributions are welcome! Please:\n1. Fork the repository\n2. Create a feature branch\n3. Make your changes\n4. Test thoroughly\n5. Submit a pull request\n\n## Support\n\nFor issues, questions, or contributions:\n- Open an issue on GitHub\n- Check existing documentation\n- Review ConnectWise API documentation\n- Check OpenWebUI documentation\n\n## License\n\nMIT License - See LICENSE file for details\n\n## Version\n\n**Current Version:** 2.3.0\n\n### Changelog\n\n**Version 2.3.0 (Opt-in writes)**\n- Added write tools, **off by default**: `add_ticket_note`, `add_company_note`, `create_time_entry`, `create_ticket`, `update_ticket`\n- Enabled per category via `CW_ENABLE_WRITES=notes|time|tickets|all`. Disabled categories are not registered, so the tools do not appear in the tool list at all. `true` is not a valid value and enables nothing\n- `CW_WRITE_DRY_RUN` logs and returns what would be sent, without sending\n- Writes are never retried on 5xx — a 500 on a POST does not mean nothing happened, and a duplicate note on a customer ticket is worse than an error. 429 is still retried, being rejected before processing\n- `update_ticket` sends JSON Patch, which is what ConnectWise actually expects; a plain object returns 200 and changes nothing\n- Ticket notes default to internal analysis rather than the customer-visible description\n- 125 tests\n\n**Version 2.2.0 (Wider read coverage)**\n- 21 new read-only tools, taking the total to 51: documents/attachments, schedule entries, SLAs, company and contact notes, company teams, ticket configurations, board teams/types/subtypes, procurement (purchase orders, line items, catalog, products, manufacturers, product types), project tickets/phases/team members, and departments\n- Endpoint paths were taken from the [ConnectWiseManageAPI](https://github.com/christaylorcodes/ConnectWiseManageAPI) module rather than guessed. Several are not what you would assume — `/service/SLAs` is capitalised, product types are at `/procurement/types`, note types at `/company/noteTypes` — and each is pinned by a test\n- `get_documents` requires `recordType` and `recordId`, because `/system/documents` returns 400 without them\n- 102 tests\n\n**Not included:** knowledge base articles. No endpoint for them appears in the reference used for everything else here, and a guessed path would 404 only against a live instance. Worth adding once the real path is confirmed.\n\n**Version 2.1.0 (Hardening)**\n- **The HTTP transport now requires a bearer token** and refuses to start without one. It previously served the entire ConnectWise instance to anyone who could reach the port\n- Retries 429 and 5xx with exponential backoff, honouring `Retry-After`; client errors are not retried\n- Added `fetchAll` to collection tools, capped by `CW_MAX_RECORDS`, so a large result set cannot be silently truncated to page one\n- Error messages name the endpoint, the status and the likely fix\n- 81 tests\n\n**Version 2.0.0 (Runs again)**\n- Fixed the server failing at import: `mcp>=1.0.0` resolved to 2.x, which removed the decorator API it was built on\n- Pinned all dependencies with upper bounds\n- Ported to FastMCP: tool schemas come from type hints, deleting ~740 lines of hand-written JSON schema and a ~190-line dispatcher\n- Removed the Node bridge — HTTP is served in-process, so no spawn per request, no `python`/`python3` mismatch, no missing MCP handshake\n- Collapsed two containers into one; the second never did anything but restart\n- Added stdio transport, so it works in Claude Code and Claude Desktop\n- Credentials are no longer required at import time\n- Added an offline test suite (51 tests, no credentials, no network)\n- All 30 tool names and the `/v1/tools/execute` response shape are unchanged\n\n**Version 1.1.0 (Enhanced Feature Set)**\n- Added IT Asset Management (configurations, configuration types, company sites)\n- Added Finance & Billing (invoices, expense entries, billing cycles, agreement additions)\n- Added Reference Data endpoints (company types/statuses, ticket priorities/sources, contact types)\n- Added Service Desk Enhancements (service boards, board statuses, ticket tasks, ticket schedules)\n- Total of 18 new read-only tools\n- Enhanced documentation with new usage examples\n\n**Version 1.0.0 (Initial Release)**\n- Read-only access to ConnectWise Manage\n- Support for companies, tickets, contacts, opportunities, agreements\n- Time entries, projects, activities, and members\n- Docker-based deployment\n- OpenWebUI integration\n- MCP bridge architecture\n- Comprehensive documentation\n"}