Reference implementation of a schema-driven MCP server for ConnectWise Manage
Strong schema-driven architecture with 15 well-named tools following verb_noun convention (ticket_get, ticket_search, company_get, etc.). All tools have descriptions (avg 180 chars, within 10-1024 baseline). Input schemas are complete with types and enums. However, output schemas are not explicitly documented in the source, responses are serialized via _dump() but the structure is inferred from domain models not shown. Error handling is minimal; no recovery guidance or actionable error messages visible. Tool descriptions lack dependency hints and prerequisites. No tool annotations (readOnlyHint/destructiveHint) despite clear read/write distinction.
Get one CWM company by id. Returns a typed Company record. `include=[...]` valid keys: contacts, agreements, configurations, recent_tickets, all.
Search CWM companies by name / identifier / type / status. Returns a SearchEnvelope with `rows: list[Company]`. `identifier` is the CWM short code. Use it when you know the exact code — faster than name_contains.
Get one CWM configuration by id. Returns a typed Configuration record. `include=[...]` valid keys: company, contact, tickets_referencing, all.
Search CWM configurations by name / type / company. Returns a SearchEnvelope with `rows: list[Configuration]`.
Get one CWM contact by id. Returns a typed Contact record. Contact's `email` and `phone` are the default-flagged entries from CWM's `communicationItems` array. `include=[...]` valid keys: company, recent_tickets, all.
Output schemas not documented. Responses are serialized via _dump(model.model_dump(...)) but the structure of returned objects (fields, types, nested objects) is not visible in the source. LLMs cannot plan downstream tool calls or extract fields without knowing what the response contains.
No tool annotations (readOnlyHint, destructiveHint, idempotentHint). Tools are marked with Risk: READ_ONLY or WRITE in comments, but MCP tool annotations are not present in the @mcp.tool() decorators. This prevents clients from understanding which tools are safe to retry or have side effects.
Inferred effective spec: <=2025-11-25.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | C | 66 | <=2025-11-25 | v2 |
Search CWM contacts by name / email / company. Returns a SearchEnvelope with `rows: list[Contact]`.
Get one CWM project by id. Returns a typed Project record. `include=[...]` valid keys: company, manager, tickets, phases, all.
Search CWM projects by name / company / status. Returns a SearchEnvelope with `rows: list[Project]`.
Add an internal technician note to a ticket. Notes added via this tool are marked internal and visible only to technicians, not to the client. For client-facing replies, use ticket_draft_reply.
Create a new CWM ticket (service or project). Returns the created Ticket record with its assigned id.
Draft a client-facing reply to a ticket. Compose the reply text and return it for the user to review and manually paste into CWM. Automated client email sending is not wired into this surface.
Get one CWM ticket by id. Returns a typed Ticket record. `kind="auto"` (default) tries /service/tickets/{id} then falls back to /project/tickets/{id} on 404. Use `kind="service"` or `kind="project"` to skip the auto-discrimination when you know. `include=[...]` populates related fields in the response. Valid keys: contact, company, owner, notes, time_entries, tasks, documents, changelog, configurations (service tickets only), project (project tickets only), all. Invalid keys fail with the valid list named. Always reference returned tickets back to the user via the `ticket_link_markdown` field — never the bare id.
Search CWM tickets — service and/or project tickets, unified. With `kind="auto"` (default), queries both endpoints and merges. Service- only filters (board_name) implicitly narrow to service kind; project-only filters (project_id) narrow to project kind. Pass `kind="service"` or `kind="project"` to restrict explicitly. Returns a SearchEnvelope with `rows: list[Ticket]` sorted by last_updated desc. Each ticket row carries `kind: "service" | "project"` so you know which it is. `next_page` is non-null when more pages exist. `summary_contains` searches the ticket title only, not note bodies. `include_closed=False` (default) hides closed tickets; pass True for historical / YTD queries.
Update an existing CWM ticket. Pass only the fields you want to change. Returns the updated Ticket record.
Search CWM time entries by ticket / member / date range. Returns a SearchEnvelope with `rows: list[TimeEntryRow]`.
Error handling lacks recovery guidance. No visible try-catch blocks, validation, or error messages that tell LLMs what to do next. A 404 or 403 from CWM is mentioned in instructions but not implemented in tool code. Agents cannot self-correct on invalid input.
Descriptions lack dependency hints and prerequisites. E.g., ticket_search does not state 'Call company_search first if you only have a company name.' This forces agents to discover multi-step workflows through trial and error.
ticket_draft_reply does not actually send, it returns a draft for manual paste. This is correct behavior but the description could be clearer: 'Compose a client-facing reply (does NOT send automatically; user must paste into CWM).' Current wording may mislead LLMs into thinking the reply is sent.