virtuous-mcp
Click on "Deploy 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., "@virtuous-mcp show me contacts who donated in the last 30 days"
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.
Virtuous CRM+ MCP Server
An MCP server, built with FastMCP, that lets an AI assistant work with Virtuous CRM+: query and read data freely, and—only with explicit user confirmation—create, update, archive, or delete data.
It provides complete coverage of the entire Virtuous API (all 291 endpoints across 39 resource groups) through a small set of convenience tools plus a generic discovery + call layer, so any endpoint can be reached without needing a separate tool per endpoint.
Safety model: reads are free, writes require confirmation
Reading (querying, searching, looking up, listing reference data) runs freely.
Every tool that changes data is "mutating" and is guarded in three layers:
Instructions — the server and each mutating tool tell the model it must describe the exact change and get explicit user approval before acting.
confirmflag — every mutating tool takesconfirm(defaultfalse). Withconfirm=falsethe tool makes no API call and returns a preview of what it would do, so the model can show the user and ask.Client backstop — the HTTP client raises
ConfirmationRequiredif a write is ever attempted without explicit confirmation, so an accidentalconfirm=trueis the only way a write can happen.
A request is classified as a read if it's a GET, or a POST to a
/Query, /QueryOptions, /Search, /Find, or /Proximity path. Everything
else is a write.
Related MCP server: lab-mcp-identity-server
Operational protocols
The HTTP layer is aligned with Virtuous's documented operational behavior:
Connection pooling — a single
httpx.AsyncClientis reused for the life of the process (per base URL), so TLS/keep-alive connections are reused instead of re-established on every call.Rate limits — Virtuous enforces an org-wide budget (documented at 5,000 requests/hour) shared by every API key/integration in the org, and returns
X-RateLimit-Limit,X-RateLimit-Remaining, andX-RateLimit-Reseton every response. The client records the latest values; callget_rate_limit_statusto inspect remaining budget.Retries + backoff — transient
429and5xxresponses are retried (up to 3 times).429waits honorRetry-After/X-RateLimit-Reset; otherwise an exponential backoff with jitter is used.Pagination — query endpoints cap at 1000 records/call;
query_allauto-pages (with a hard ceiling) so you don't manually loopskip/take.Bulk writes —
create_batchposts many contacts/gifts in one request via the recommended batch endpoints, conserving the shared rate budget.
Tools
Full-API discovery + generic call
The entire API is reachable through these. Use discovery to find the exact
method + path, then call_endpoint to invoke it.
Tool | Purpose |
| List all 39 resource groups and their read/write endpoint counts. |
| Discover any endpoint (filter by resource, text search, or |
| Full metadata + parameters for one endpoint. |
| Invoke any endpoint. Reads run freely; writes obey the confirmation gate. |
call_endpoint resolves :placeholders in the path from path_params (e.g.
/api/Contact/:contactId + {"contactId": 123}), and works even for endpoints
not in the bundled registry.
Read tools (no confirmation)
Tool | Purpose |
| List queryable object types + reference-data keys. |
| Discover queryable fields, data types, and allowed operators for an object. |
| Run a filtered bulk query (single page). |
| Auto-paginate a query up to |
| Fetch a single record by id. |
| Look up one contact. |
| Fuzzy free-text contact search. |
| All gifts for a contact. |
| Notes for a contact. |
| Individuals that make up a contact. |
| Lookup lists: contact/gift/project/task types, tags, custom fields, org groups, etc. |
| Current organization + the API key's permissions. |
| Latest observed rate-limit headers (remaining org-wide budget + reset time). |
| Escape hatch for arbitrary read-only |
| Auto-page read-only |
Write tools (MUTATING — require confirm=true after explicit user approval)
Tool | Purpose |
| Recommended way to import a single Contact or Gift (matched/validated). |
| Bulk-import many Contacts or Gifts in one request (rate-limit-friendly). |
| Create a record (e.g. ContactNote, ContactTag, Task, Relationship). |
| Update a record (PUT). |
| Archive/unarchive a record. |
| Destructive delete. |
| Escape hatch for any other write (cancel recurring gift, write off pledge, send email, toggle webhook, etc.). |
With confirm omitted/false, write tools (and call_endpoint on a write
endpoint) return a confirmation_required preview and change nothing.
Note:
call_endpointis the universal way to reach any write endpoint and is subject to the same confirmation gate. The dedicated write tools above are just ergonomic shortcuts for the most common operations.
How queries work
A query body is made of groups. Conditions within a group are AND-ed;
separate groups are OR-ed. Each condition is:
{ "parameter": "<field name>", "operator": "<operator>", "value": "<value>" }Use get_query_options to get the exact parameter and operator strings for
an object. Example: contacts created on/after 2024-01-01, sorted by id desc:
{
"object_type": "Contact",
"groups": [
{ "conditions": [
{ "parameter": "Create Date", "operator": "GreaterThanOrEqual", "value": "01/01/2024" }
] }
],
"sort_by": "Id",
"descending": true,
"take": 100
}Query endpoints return at most 1000 records per call; use skip/take to
page manually, or query_all to auto-paginate up to a max_records ceiling.
For non-query GET endpoints that expose the same skip/take pattern (for
example contacts by tag or organization-group members), use
read_paged_request.
Tasks & reminders (non-obvious gotchas)
These are surfaced at runtime via describe_endpoint (notes + body_params)
and in the server instructions, but documented here too:
Create a task with
POST /api/Task. The assignee field isownerEmail(the user's email) — notowner,ownerId, orassignedTo. A wrong key is silently ignored and the task is created unassigned, and the success response does not echoownerEmailback (its absence is not a failure).Tasks have no update or delete endpoint. To remove/resolve a task, use the Reminder endpoints:
PUT /api/Reminder/Dismissed/{id}(dismiss ≈ delete) orPUT /api/Reminder/Completed/{id}(mark resolved). There is no un-dismiss / reactivate endpoint via the API (UI only).To check if tasks are dismissed/resolved, query
Taskwith theResolvedfilter (IsTrue= dismissed/completed,IsFalse= active). The query result does not expose the owner or an explicit resolved field; theAssigned Userfilter expects an internal user id (not an email), and no users-list endpoint is exposed.
Setup
Get a Virtuous API key: in Virtuous, Settings → All Settings → Connectivity → Application Keys → Create an Application Key.
Copy
.env.exampleto.envand setVIRTUOUS_API_KEY.
Install uv
This project is managed with uv, a fast Python
package and project manager. Install it once in your environment:
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"uv is also available via other package managers if you prefer:
# Homebrew (macOS)
brew install uv
# pipx
pipx install uvAfter installing, restart your shell (or follow the printed instructions) so the
uv command is on your PATH, then verify:
uv --versionuv manages the virtual environment and can even provision a compatible Python
(3.10+) for you, so you don't need to set up Python separately.
Install dependencies
uv syncThis creates a virtual environment and installs the exact dependencies pinned in
uv.lock.
Run
VIRTUOUS_API_KEY=your_key uv run virtuous-mcpThe server speaks MCP over stdio.
Use with an MCP client
Every client below uses the same server entry — only the file it lives in (and the surrounding scope) changes:
{
"mcpServers": {
"virtuous-mcp": {
"command": "uv",
"args": ["run", "--directory", "/Users/cole.j.cantu/Programs/custom-mcp/virtuous-mcp", "virtuous-mcp"],
"env": { "VIRTUOUS_API_KEY": "your_api_key_here" }
}
}
}Cursor (global / all projects)
Add it to your global Cursor config so it's available in every project:
macOS / Linux:
~/.cursor/mcp.jsonWindows:
%USERPROFILE%\.cursor\mcp.json
Create the file if it doesn't exist and paste the JSON block above (top-level
mcpServers key). For a single project instead, use .cursor/mcp.json in that
project's root — project config takes precedence over global if both define a
server with the same name. Reload Cursor (or toggle the server in
Settings → Tools & Integrations → MCP) after saving.
Claude Code (user scope / all projects)
User scope makes the server available to you across all projects. Two ways:
CLI (recommended):
claude mcp add virtuous-mcp \
--scope user \
--env VIRTUOUS_API_KEY=your_api_key_here \
-- uv run --directory /Users/cole.j.cantu/Programs/custom-mcp/virtuous-mcp virtuous-mcp--scope user writes to ~/.claude.json under the top-level mcpServers
key. (Other scopes: project → .mcp.json in the repo root, shared with
everyone who clones it; local (default) → your private entry for the current
project only.) Everything after -- is the command Claude Code runs to launch
the server.
Edit ~/.claude.json directly:
Add the server under the top-level mcpServers object (this is what makes
it user-scoped — not nested under a specific project's entry):
{
"mcpServers": {
"virtuous-mcp": {
"command": "uv",
"args": ["run", "--directory", "/Users/cole.j.cantu/Programs/custom-mcp/virtuous-mcp", "virtuous-mcp"],
"env": { "VIRTUOUS_API_KEY": "your_api_key_here" }
}
}
}~/.claude.json also holds other Claude Code settings, so merge into the
existing mcpServers object rather than overwriting the file. Restart your
Claude Code session afterward so it re-reads the config.
Claude Code scope reference
Scope | Where it's stored | Available to |
|
| You, this project only |
|
| Anyone who clones the repo |
|
| You, all projects |
Claude Desktop
Same JSON block in Claude Desktop's claude_desktop_config.json (under
mcpServers).
Configuration
Env var | Required | Default | Description |
| yes | — | Bearer API key / Application Key. |
| no |
| API base URL. |
Available Tools
26 toolsarchive_recordA
MUTATING: archive (or unarchive) a record via PUT /api/{object_type}/Archive/{id}.
DO NOT call with confirm=true unless the user explicitly approved it. With confirm=false this performs no change and returns a preview.
Inputs: record_id (required), object_type (required), confirm, unarchive.
call_endpointA
Invoke ANY Virtuous API endpoint. This gives full coverage of the entire API.
Reads (GET, and POST to Query/QueryOptions/Search/Find/Proximity) run freely. WRITE endpoints (any other POST/PUT/PATCH/DELETE) MODIFY data and obey the confirmation policy: with confirm=false NO call is made and a preview is returned so you can show the user and ask. Only pass confirm=true after the user has explicitly approved the exact action.
Use list_endpoints / describe_endpoint to find the right method + path first.
Inputs: path (required), method (required), body, confirm, +2 more.
create_batchA
MUTATING: submit a BULK import of contacts or gifts (the rate-limit-friendly, Virtuous-recommended way to load many records at once).
Virtuous best practices say: to load many gifts/contacts, post them through the BATCH endpoints rather than one transaction per call. This both runs the records through Virtuous's matching/validation/dedupe pipeline AND makes far fewer requests against the ORG-WIDE rate budget (one batch call instead of N).
'contact' -> POST /api/Contact/Batch 'gift' -> POST /api/v2/Gift/Transactions DO NOT call with confirm=true unless the user explicitly approved it. With confirm=false this performs no change and returns a preview (including how many records are in the batch).
Inputs: body (required), kind (required), confirm.
create_recordA
MUTATING: create a new record via POST /api/{object_type}.
DO NOT call with confirm=true unless the user has explicitly approved creating
this exact record. With confirm=false this performs no change and returns a
preview to show the user. NOTE: to create Contacts/Gifts safely, prefer
create_transaction (it runs through Virtuous matching/validation).
Inputs: body (required), object_type (required), confirm.
create_transactionA
MUTATING: submit a Contact or Gift transaction (the recommended, matched/validated way to import contacts and gifts).
'contact' -> POST /api/Contact/Transaction 'gift' -> POST /api/v2/Gift/Transaction DO NOT call with confirm=true unless the user explicitly approved it. With confirm=false this performs no change and returns a preview.
Inputs: body (required), kind (required), confirm.
delete_recordA
MUTATING + DESTRUCTIVE: delete a record via DELETE /api/{object_type}/{id}.
This permanently removes data. DO NOT call with confirm=true unless the user has explicitly approved deleting this exact record. With confirm=false this performs no change and returns a preview.
Inputs: record_id (required), object_type (required), confirm.
describe_endpointA
Get full metadata for a single endpoint (method, path, title, resource, read/write, path + query parameters). Read-only.
Inputs: path (required), method (required).
find_contactA
Find a single contact by email, or by reference source + id. Read-only.
Provide either an email or a reference_source + reference_id.
Inputs: email, reference_id, reference_source.
get_contact_notesB
Get notes for a contact. Read-only.
Inputs: contact_id (required), important_only.
get_current_contextA
Get the current organization and the API key's permissions. Read-only.
Useful to understand what the key is allowed to read/write before attempting any action.
Inputs: no named parameters.
get_gifts_by_contactB
Get all gifts for a contact via GET /api/Gift/ByContact/{id}. Read-only.
Inputs: contact_id (required).
get_individuals_by_contactC
Get the individuals that make up a contact. Read-only.
Inputs: contact_id (required).
get_query_optionsA
Get the queryable fields, data types, and allowed operators for an object type so you can construct a valid query. Read-only.
Inputs: object_type (required).
get_rate_limit_statusA
Report the most recently observed Virtuous rate-limit headers. Read-only.
Virtuous enforces an ORG-WIDE request budget (documented at 5,000
requests/hour) shared by every API key/integration in the organization.
This returns the latest X-RateLimit-Limit, X-RateLimit-Remaining,
reset_at, and seconds_until_reset seen on a response — useful before
kicking off a large batch or many queries. It is empty until at least one
request has been made in this session, so a low remaining here reflects
other integrations' usage too.
Inputs: no named parameters. Returns: object
get_recordB
Fetch a single record by id via GET /api/{object_type}/{record_id}. Read-only.
Inputs: record_id (required), object_type (required).
get_reference_dataA
Fetch a reference/lookup list (contact types, tags, custom fields, task types, project types, etc.) used to build queries or understand allowed values. Read-only.
Auto-paginates: these list endpoints default to only 10 rows server-side (a
silent-truncation footgun — e.g. /api/OrganizationGroup has 30 groups but
returned just 10, hiding the 'Pending Portfolio Assignment' holding group).
When the response is a paged {"list", "total"} envelope this walks every
page and returns the FULL list (with a returned count) so callers never
reason over a truncated reference set.
Inputs: key (required).
list_endpointsA
Discover Virtuous API endpoints across the ENTIRE API. Read-only.
Returns matching endpoints with their method, path template (with
:placeholders), title, whether they read or write, and parameter names. Use
the returned method+path with call_endpoint (or describe_endpoint first).
Inputs: only, search, resource. Returns: object
list_query_object_typesA
List Virtuous object types that can be queried/read, with descriptions.
Read-only. Use the returned keys with get_query_options, query_records, and get_record.
Inputs: no named parameters. Returns: object
list_resourcesA
List every Virtuous API resource group (Contact, Gift, Project, Event, Webhook, etc.) and how many endpoints each has. Read-only.
Use with list_endpoints to drill into a resource.
Inputs: no named parameters. Returns: object
query_allA
Auto-paginate a query and return up to max_records records. Read-only.
Convenience over query_records: instead of you managing skip/take,
this loops through pages (each capped at 1000 by the API) until it has
max_records, the result set is exhausted, or the QUERY_ALL_HARD_CAP
safety ceiling is hit. Because every page is a request against the
ORG-WIDE rate budget, keep max_records as small as the task needs and
prefer a precise groups filter. The response includes a rate_limit
snapshot so you can see remaining budget after the sweep.
Inputs: object_type (required), groups, sort_by, page_size, +3 more.
query_recordsB
Run a read-only bulk query against a Virtuous object type. Read-only.
Inputs: object_type (required), skip, take, groups, +3 more.
read_paged_requestA
Auto-page arbitrary read-only GET endpoints that use skip/take.
Use for list-style GET endpoints such as /api/Contact/ByTag/{tagId} or
/api/OrganizationGroup/{id}/contacts. If the first response is not a
{"list": [...], "total": N} envelope, the original response is returned.
Inputs: path (required), params, page_size, max_records.
read_requestA
Escape hatch for arbitrary READ-ONLY GET requests to the Virtuous API.
Only GET is allowed here. Use when no dedicated read tool fits. Read-only.
Inputs: path (required), params.
search_contactsB
Fuzzy-search contacts by a free-text string. Read-only.
Inputs: search (required), skip, take.
update_recordA
MUTATING: update a record via PUT /api/{object_type}/{record_id}.
DO NOT call with confirm=true unless the user has explicitly approved this exact change. With confirm=false this performs no change and returns a preview.
Inputs: body (required), record_id (required), object_type (required), confirm.
write_requestA
MUTATING escape hatch for any write endpoint not covered by a dedicated tool (e.g. cancel a recurring gift, write off a pledge, send an email, toggle a webhook).
DO NOT call with confirm=true unless the user has explicitly approved this exact
request. With confirm=false this performs no change and returns a preview. If the
request is actually read-only it will be rejected — use read_request for reads.
Inputs: path (required), method (required), body, confirm.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 26 tools
Tools generally have distinct purposes, but query_all and query_records overlap, and generic escape hatches (call_endpoint, read_request, write_request) can create ambiguity with dedicated tools. Descriptions help clarify, but the potential for misselection exists.
All tool names follow a consistent 'verb_noun' pattern with underscore separation. Verbs are varied but predictable, and naming is uniform throughout the set.
26 tools is on the higher side but appropriate for a comprehensive CRM API server. Each tool serves a clear purpose, and the count is justified by the scope of the API.
The tool surface covers CRUD operations, querying, bulk imports, metadata discovery, rate limits, and escape hatches for missing endpoints. No obvious gaps; the generic tools ensure full API coverage.
Maintenance
Related MCP Connectors
MCP server that lets AI assistants use all OneSchema features exposed via the public API.
Hosted MCP server connecting AI assistants to 9,000+ apps and 40,000+ actions via Zapier.
MCP server unifying ERPs, CRMs, APIs and knowledge base for Claude, ChatGPT and Gemini.
The HubSpot MCP Server acts as a bridge that enables AI assistants and Large Language Models to securely interact with HubSpot CRM data through natural conversation, without requiring users to understand complex API structures. It provides read-only access to standard CRM objects (contacts, companies, deals, tickets, products, invoices, and more) and their associations, secured via OAuth 2.0, allowing AI agents to perform tasks like summarizing deals, fetching company updates, and looking up record changes.
Related MCP Servers
- AlicenseDqualityDmaintenanceAn MCP server that allows AI assistants to interact with the ServiceTitan API, requiring client credentials for authentication.4ISC
- AlicenseAqualityDmaintenanceMCP server that lets an AI assistant perform scoped Okta identity operations with an out-of-band human-approval gate on destructive actions.5MIT
- FlicenseNot gradedqualityDmaintenanceA single MCP server that exposes safe, permission-checked tools for AI assistants to reach file systems, databases, APIs, Git, cloud services, and business applications.-
- FlicenseAqualityCmaintenanceMCP server that provides an AI assistant with access to Odoo business data, enabling read operations and human-approved record creation.3-