Vitally MCP Server
A remote, OAuth-secured MCP server that lets an AI client read and lightly edit Vitally customer success data (the schema here exposes 11 tools, a small read-heavy slice of the 93 the README describes).
Discover tools —
search_toolsby keyword.Find accounts —
search_accounts(name, external ID, limit),find_account_by_name, andrefresh_accountsto reload the list.Find users —
search_usersby email, external ID, or email subdomain.Account insight —
get_account_healthfor health scores.Account activity —
get_account_conversations,get_account_tasks(filter by status), andget_account_notes, each with a result limit.Notes —
get_note_by_idfor full content, andcreate_account_noteto add a note.Across the wider server (not in this schema) — full CRUD on 16 Vitally resource types (organisations, projects, meetings, custom objects, surveys, etc.), EU/US data centres, rate-limit-aware retries, and per-caller permission tiers (reader/editor/admin) that filter which tools you're shown.
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., "@Vitally MCP Server find accounts with 'Acme' in their name"
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.
Vitally MCP Server
A Model Context Protocol server that exposes the Vitally customer success platform's REST API to MCP-compatible clients such as Claude Desktop, Claude Code, VS Code, and Cursor.
Built in C# on .NET 10 and the official ModelContextProtocol SDK, hosted as a remote HTTP MCP server secured with Microsoft Entra (OAuth 2.1 / RFC 9728). Users connect by URL — no install, no executable, no per-user secrets to distribute.
Features
Full CRUD coverage of 16 Vitally resource types via 93 tools: accounts, organisations, users, conversations, messages, notes, projects, project templates, project categories, tasks, NPS responses, admins, custom objects (and instances), meetings (with participants and transcripts), custom traits, and custom surveys.
Permission-aware tools — every tool is annotated
ReadOnly/Destructive/Idempotent/OpenWorldso MCP clients can enable or disable categories of operation in bulk and reason about retry safety. Beyond those advisory hints,tools/listis filtered per caller: a client is shown only the tools the signed-in user's permission tier permits.EU and US data centres — defaults to EU (
rest.vitally-eu.io); setVitally:Region=USto point at{subdomain}.rest.vitally.io.Rate-limit-aware HTTP pipeline — auto-retries on
429 Too Many RequestshonouringRetry-AfterandX-RateLimit-Reset, and logs a warning when remaining requests drop below threshold.Client-side field & trait filtering — responses are trimmed before they reach the LLM, each resource type with sensible defaults that exclude heavy fields (rich text, transcripts, full traits objects).
Streamable HTTP transport (MCP 2026-07-28) in stateless mode — easy to scale horizontally, no sticky sessions required.
OAuth 2.1 protection against Microsoft Entra, directly on both deployed targets.
/.well-known/oauth-protected-resourceexposes the metadata document so clients discover the authorisation server automatically. The Vitally API key is fetched on demand from Azure Key Vault via the server's managed identity.
Related MCP server: Custify MCP Server
Using the server (FISCAL users)
Point your MCP client at:
https://vitally.fiscaltec.com/mcpThe first time you connect, your client will redirect you to a Microsoft sign-in to authenticate. Once authenticated, the server proxies your tool calls to Vitally using a service-account API key it fetches from Azure Key Vault.
Access & groups: authentication alone grants nothing — you must be in a
sg-vitally-*Entra group for your access tier. See ACCESS.md for connecting, the group setup, and how access is granted or revoked.
Claude Desktop
FISCAL users: the server is already published to the FISCAL Claude workspace as the organisation connector FISCAL Vitally MCP — Customize → Connectors → FISCAL Vitally MCP → Connect. (It is distinct from Vitally's own official connector, which is listed as Vitally MCP — the name this server's connector used before 2026-10-07; see ACCESS.md.)
Elsewhere: Settings → Connectors → Add custom connector → paste the URL above. Approve the Microsoft sign-in popup.
Claude Code
claude mcp add --transport http fiscal-vitally https://vitally.fiscaltec.com/mcpRun any MCP-using command (claude itself, or /mcp) and Claude Code will open the Microsoft sign-in flow on first use.
VS Code, Cursor, and other MCP-aware hosts
Most modern MCP clients support the streamable HTTP transport. Add a server entry that points at the URL above; the client handles the OAuth flow automatically via the protected-resource metadata document.
Configuration
The server reads its configuration from appsettings.json, appsettings.{Environment}.json, or environment variables (using the standard ASP.NET Core double-underscore separator for nested keys, e.g. Vitally__Region).
Setting | Required | Default | Description |
| No |
| Data centre: |
| Only when | — | Vitally subdomain, e.g. |
| Yes (prod) | — | Azure Key Vault URI, e.g. |
| No |
| Key Vault secret name holding the Vitally API key. |
| No |
| In-memory TTL for the resolved API key. |
| Yes (local) | — | Local-dev-only fallback API key, used when |
| Yes | — | The provider's OIDC issuer identifier, e.g. |
| Yes | — | The API identifier the token's |
| No | — | Canonical resource identifier published in |
| Entra: yes | (empty) | The API scope the proxy names upstream, e.g. |
| Recommended (prod) | — | Canonical public origin, e.g. |
| No |
| Deployment-wide kill switch: denies every create/update/delete regardless of tier, RBAC state or |
| No |
| Server-side RBAC enforcement. When |
| No |
| Permission required for read operations (list/get/search → HTTP GET). An internal tier name: with |
| No |
| Permission required for create/update operations (HTTP POST/PUT/PATCH). An internal tier name, as above. |
| No |
| Permission required for delete operations (HTTP DELETE). An internal tier name, as above. Set equal to |
| No |
| Optional namespaced claim also checked for permissions (alongside the standard |
| No |
| When |
| No |
| TTL for the per-user live group-membership cache. Lower = faster propagation, more Graph calls. |
| No |
| How long a successful lookup stays usable as a fallback after a Graph call fails, so an outage degrades to each caller's last known-good tier instead of denying everyone. |
| When | — | Entra security-group object ids mapped to the read / read+write / read+write+delete tiers. At least one required when live check is on. Membership is transitive — a user in a group nested inside one of these is granted the tier. |
| No |
| Emit a structured audit record per action (authenticated user + verb + resource + outcome), giving a per-user "who did what" trail despite the shared Vitally key. On FISCAL's deployments these records are exported to Application Insights |
| No |
| Also audit read operations (HTTP GET). On by default — reads are most of the traffic, and this is the only record of who accessed which customer record. Reads are the high-volume path, so this is the lever if ingest cost becomes a problem; mutations and denied attempts are recorded either way. |
| No | — | Enables the OAuth proxy / DCR shim (see OAuth proxy below). When set, every Dynamic Client Registration call returns this fixed client_id, and the server proxies |
| No | — | Confidential-client secret for |
| No |
| Allowlist of non-loopback |
| No |
| Local development only. Skips JWT validation entirely. Logs a warning at startup. |
See VitallyMcp/appsettings.Example.json for the full layout.
Running locally
Prerequisites: .NET 10 SDK.
# Restore + build + run the test suite
dotnet test VitallyMcp.sln -c Debug
# Start the server in dev mode (no identity provider, no Key Vault — uses DevelopmentApiKey from env)
$env:OAuth__NoAuth = "true"
$env:Vitally__Region = "EU"
$env:Vitally__DevelopmentApiKey = "sk_live_your_key"
$env:ASPNETCORE_URLS = "http://localhost:5099"
dotnet run --project VitallyMcp/VitallyMcp.csprojSmoke test:
# OAuth protected-resource metadata
Invoke-RestMethod http://localhost:5099/.well-known/oauth-protected-resource
# MCP initialise (returns capabilities + server info). Deliberately requests 2025-06-18: the
# `initialize` handshake exists only in revisions up to 2025-11-25, since 2026-07-28 replaced it
# with per-request `_meta` and headers. Do NOT substitute 2026-07-28 here — the call would error.
$body = @{ jsonrpc='2.0'; id=1; method='initialize'; params=@{ protocolVersion='2025-06-18'; capabilities=@{}; clientInfo=@{ name='smoke'; version='0.0.1' } } } | ConvertTo-Json -Depth 10 -Compress
Invoke-RestMethod -Method Post -Uri http://localhost:5099/mcp -ContentType 'application/json' -Headers @{ Accept='application/json, text/event-stream' } -Body $bodyThen add the dev server to Claude Code:
claude mcp add --transport http vitally-dev http://localhost:5099/mcpSelf-host (replicators)
Deploying your own instance for a different org or against a different Vitally tenant requires three things — none of which are in this repo, all of which are config:
An OIDC identity provider that issues RS256-signed JWTs for your users. FISCAL uses Microsoft Entra directly; any compliant provider works (Entra, Keycloak, Okta, etc.). Register an Application with identifier URI matching your
OAuth:Audiencevalue, plus a delegated scope (e.g.Tools.Access) and public-client redirect URIhttp://localhostfor MCP-client OAuth flows.An Azure Key Vault (or compatible secret store; see the swap notes in CLAUDE.md) containing your Vitally API key as a secret. Default secret name is
vitally-shared; change viaVitally:DefaultSecretRef.A container host that can run the published Docker image. Anywhere ASP.NET Core 10 runs (Azure Container Apps, AWS App Runner, GCP Cloud Run, plain Kubernetes) — Container Apps is what FISCAL uses.
FISCAL's deployment uses Azure Container Apps + Azure Key Vault + Microsoft Entra, reached directly on both targets. See the Deployment section in CLAUDE.md for the shape. Anyone replicating can swap Container Apps for App Service, ACR for GHCR, Entra for Keycloak, etc., without touching the application code. Bicep / azd templates aren't shipped in this repo — the surface is small enough that the README description is the contract.
Architecture
VitallyMcp/
├── Program.cs # ASP.NET Core host, JwtBearer auth, MapMcp
├── OAuthOptions.cs # Authority + Audience + Resource + NoAuth dev flag
├── UpstreamOidcMetadata.cs # Upstream endpoints from the provider's OIDC discovery document
├── VitallyServerOptions.cs # Region, KeyVaultUri, secret config
├── VitallyApiKeyProvider.cs # Fetches the API key from Key Vault (cached)
├── VitallyService.cs # HTTP client + client-side JSON filtering
├── VitallyRateLimitHandler.cs # 429 retry + rate-limit warnings
└── Tools/ # One file per Vitally resource type
├── AccountsTools.cs
├── OrganizationsTools.cs
├── UsersTools.cs
├── ConversationsTools.cs
├── MessagesTools.cs
├── NotesTools.cs
├── ProjectsTools.cs
├── ProjectTemplatesTools.cs
├── TasksTools.cs
├── NpsResponsesTools.cs
├── AdminsTools.cs
├── CustomObjectsTools.cs
├── MeetingsTools.cs
├── CustomTraitsTools.cs
└── SurveysTools.csThe MCP server runs on the ModelContextProtocol.AspNetCore package using the streamable HTTP transport in stateless mode. MapMcp("/mcp") is gated by RequireAuthorization() — JWTs are validated against whichever provider is configured in OAuth:Authority / OAuth:Audience. On each tool call, VitallyApiKeyProvider fetches the vitally-shared secret from Key Vault (cached in-memory for 5 min, using the server's user-assigned managed identity), and VitallyService uses it to call Vitally on behalf of all authenticated users.
OAuth proxy
When OAuth:SharedClientId is set the server runs an OAuth 2.1 proxy in front of the upstream identity provider — it advertises response_types_supported: ["code"], grant_types_supported: ["authorization_code", "refresh_token"] and code_challenge_methods_supported: ["S256"], and offers no implicit or password grant. It serves:
Endpoint | Purpose |
| RFC 9728 protected-resource metadata — clients use it to discover the authorisation server. |
| RFC 8414 authorisation-server metadata — declares this server's own origin as |
| Captures the client's |
| Receives the provider's redirect, looks up the original client |
| Forwards the code-exchange to the upstream |
| RFC 7591 Dynamic Client Registration shim — always returns |
This setup exists because MCP clients implement RFC 7591 (DCR) and RFC 8252 (loopback redirect with ephemeral ports), but a dynamically registered client typically triggers a per-session consent screen and providers do not natively accept arbitrary loopback ports. The proxy collapses everything onto one pre-registered first-party app, skipping the consent and accepting any loopback port (Claude Code, VS Code, Cursor, MCP Inspector all rotate ports between sessions). To support hosted MCP clients (e.g. Claude.ai), add their callback URL to OAuth:AllowedClientRedirectUris.
The VitallyService exposes two call patterns:
Standard envelope (
GetResourcesAsync,GetResourceByIdAsync,CreateResourceAsync,UpdateResourceAsync,DeleteResourceAsync) — for endpoints returning{results, next}. Applies client-side field and trait filtering with resource-specific defaults.Raw passthrough (
GetRawAsync,PostRawAsync,DeleteRawAsync) — for endpoints whose response shape differs from the standard envelope (surveys'{data}, custom-fields' bare array) or for sub-resource sub-paths (meeting participants, meeting transcripts).
All HTTP traffic flows through VitallyRateLimitHandler, a DelegatingHandler registered via AddHttpMessageHandler<>() in Program.cs.
Tool catalogue
The server publishes 93 MCP tools, mostly one per Vitally REST endpoint — though not strictly one-to-one, since Get_organization_summary is a read-only composite that fans out to four upstream calls. Each tool's [McpServerTool] attribute sets ReadOnly = true for list/get operations and Destructive = true for create/update/delete, so MCP clients can permission them in bulk, plus Idempotent and OpenWorld so they can reason about retry safety.
An individual caller will not see all 93. Each tool also carries an [Authorize] policy matching its tier, and tools/list is filtered per caller — a reader sees the 56 read tools, an editor 81, an admin all 93. See Security.
Resource | List / search | Get | Create | Update | Delete | Sub-resources |
Accounts | ✓ | ✓ | ✓ | ✓ | ✓ | health-score breakdown |
Organizations | ✓ | ✓ | ✓ | ✓ | ✓ | — |
Users | ✓ (+search) | ✓ | ✓ | ✓ | ✓ | by account, by organisation |
Conversations | ✓ | ✓ | ✓ | ✓ | ✓ | by account, by organisation |
Messages | ✓ | ✓ | ✓ | — | ✓ | by conversation |
Notes | ✓ | ✓ | ✓ | ✓ | ✓ | by account, by organisation, note categories |
Projects | ✓ | ✓ | ✓ (from template) | ✓ | ✓ | by account, by organisation |
Project templates | ✓ | ✓ | — | — | — | project categories |
Tasks | ✓ | ✓ | ✓ | ✓ | ✓ | by account, by organisation, task categories |
NPS responses | ✓ | ✓ | ✓ | ✓ | ✓ | by account, by organisation |
Admins | search by email | — | — | — | — | — |
Custom objects | ✓ | ✓ | ✓ | ✓ | — | instances (list, search, CRUD) |
Meetings | ✓ | ✓ | ✓ | ✓ | ✓ | by account, by organisation, participants, transcripts |
Custom traits | schema discovery | — | — | — | — | — |
Custom surveys | responses (list, get) | survey question | — | — | — | — |
Full per-tool descriptions are auto-generated from the [McpServerTool] attributes — call tools/list against the server to see them. Note the response contains the tools available to the caller's tier, so a reader's list is a subset of an admin's.
Security
All MCP requests require a valid JWT signed by the configured identity provider. Tokens are validated server-side against the issuer + audience and the signature.
Server-side RBAC (
Authorization:*) enforces avitally:read/vitally:write/vitally:deletepermission on every tool call, mapped from the HTTP verb at a single choke point (VitallyService.SendAsync). This is the hard backstop: theReadOnly/Destructivetool attributes are advisory hints for MCP clients, but RBAC physically prevents a caller (or a misbehaving agent) from mutating data without the permission. The tier is resolved from the caller's live Entra group membership via Microsoft Graph on every deployed target (Authorization:LiveGroupCheck), so grants and revocations take effect within about a minute while Graph is reachable, and no claim in the token can grant access. During a Graph outage each caller's last known-good tier is served for up toAuthorization:LiveGroupStaleSeconds(default 1 h) before the call is denied — so a revocation can take that long to bite. SeeACCESS.mdfor the incident procedure. A token-claim mode exists for deployments without Graph reachability and is selected by turning that flag off.Per-caller tool discovery. Every tool additionally carries an
[Authorize]policy for its tier, which the MCP SDK evaluates sotools/listadvertises only what the caller may invoke. Discovery filtering and theSendAsyncbackstop resolve permissions through the same code path, so they cannot disagree — but the security boundary remainsSendAsync. Hiding a tool is a usability improvement, not the control: an out-of-tier call is refused regardless of what the client was shown.Per-user audit trail (
Audit:*) — records are keyed on the caller's Entra object id from theoidclaim: a GUID that resolves to a person withaz ad user show --id, and no more personal than the alternatives. Where there is none, the resolver falls back to the rawsub, thenNameIdentifier, thenunknown— andanonymousfor an unauthenticated caller. A consistent-but-opaque key beats none, though an Entra token always carriesoid, so a record keyed on anything else means an unexpected token shape. Because all users share one Vitally key, Vitally's own log can't attribute actions to individuals; this server-side record can.There are four record shapes, not one, because they are emitted at different points:
Record
Emitted at
Carries
Tool call
a call-tool filter, once per executed
tools/callobject id, tool name, the arguments, the ids of the records touched, counts, outcome, duration, permission tier, correlation id
Action
VitallyService.SendAsync, after each upstream responseobject id, HTTP verb, resource path (query string stripped), status code, duration, correlation id
Service denial
SendAsync, on an RBAC refusalobject id, HTTP verb, resource path — no status, the call never happened
Tier denial
the SDK
[Authorize]checkpoint, beforeSendAsyncrunsobject id, tool name, required permission — no verb or path, no upstream call was attempted
The tool call is the primary record — it is the one that can say this user did this to these customers — and the action record corroborates it by showing what the server actually did. They share a correlation id, which is what joins them.
The tier denial exists precisely because that checkpoint rejects out-of-tier calls before the choke point, so neither the tool-call record nor the action record would ever see them. ⚠️ A tier-denied call therefore records who and which tool, but not the arguments — the trail cannot say what the caller was reaching for. That is a deliberate boundary, not a gap.
Upstream response bodies are never logged — they can carry meeting transcripts and arbitrary customer traits, and an audit trail does not need a copy of the data it is auditing access to.
Two notes on FISCAL's own deployment:
The records are queryable. Since 2026-09-25 both targets export them to Application Insights
AppEvents, verified on each by reading rows back rather than inferred from a clean deploy — a wrong routing attribute lands them inAppTraceswith no error.AppRoleNametells the two apart. ⚠️ Complete only while a release containing #178 is serving: a release without it drops the records of any request whose trace was not sampled — and traces are capped at about 5 a second by default, so under load the traffic beyond roughly that rate, which can be far more than 5 a second, can lose its records. Any period since 2026-09-25 that such a release served may be missing individual records; check which image served a window before relying on it. ⚠️ Staging is an on-demand app and the setting does not survive a recreate, so a staging app stood up without it keeps its records on stdout."Personal data is kept out of telemetry" does not describe this design. That rule was withdrawn on 2026-09-17: the trail records tool arguments in full, including free-text search terms that may contain names or email addresses, because without them it cannot say which customer was accessed. Upstream response bodies are still excluded.
Note what that means for writes, since the two rules meet there: a create or update tool's
jsonBodyis a request payload, and it is recorded — "alice set these fields on this account" is the audit record for a modification. The exclusion is of upstream response bodies, which are data the server read back on the caller's behalf, not data the caller supplied. Records are size-capped.See
docs/superpowers/specs/2026-09-17-logging-observability-design.md. The code emits all four shapes in the table above, the tool-call record included (#147).
The OAuth proxy's
/oauth/tokenonly services theauthorization_codeandrefresh_tokengrants — it rejects any other grant before injecting the confidential client secret, so the secret can't be leveraged to mint tokens without a user sign-in.Set
OAuth:PublicBaseUrlin production so the OAuth metadata documents emit a fixed canonical origin rather than reflecting the requestHost.Vitally API keys are not distributed to clients or stored in tokens — they live in Key Vault, accessed by the server's managed identity.
Tokens are short-lived (8h access, with refresh rotation). The server keeps no session state; restart is transparent to clients.
HTTPS is terminated at the platform ingress (Container Apps managed cert) — the server itself doesn't ship TLS.
The OAuth proxy validates every client
redirect_uriagainstOAuth:AllowedClientRedirectUris(plus the implicit RFC 8252 loopback rule). Without this check, an attacker could exfiltrate authorisation codes via the proxy's/oauth/callbackreflector; with it, the proxy refuses anything that isn't a loopback URI or an explicitly-allowlisted hosted callback.
Standards conformance of the OAuth proxy
The proxy presents itself as a complete authorisation server: it declares its own origin as issuer
(RFC 8414 §3.3), and /oauth/callback replaces any upstream iss with that same origin so the
authorisation response is consistent with the metadata (RFC 9207). The upstream provider remains the token issuer.
Strict clients — including MCP Inspector — complete the flow; see the complete
authorisation-server façade section in CLAUDE.md for what was verified and how.
Licence
Proprietary — © FISCAL Technologies Ltd. All rights reserved.
Support
Internal: Infrastructure team at FISCAL Technologies.
Issues: GitHub Issues.
Available Tools
11 toolscreate_account_noteC
Vitally tool to create a new note for an account
Inputs: accountId (required), content (required).
find_account_by_nameA
Vitally tool to find an account by name (partial match supported)
Inputs: name (required).
get_account_conversationsC
Vitally tool to get recent conversations for an account
Inputs: accountId (required), limit.
get_account_healthC
Vitally tool to get health scores for an account
Inputs: accountId (required).
get_account_notesC
Vitally tool to retrieve notes for an account
Inputs: accountId (required), limit.
get_account_tasksB
Vitally tool to get tasks for an account
Inputs: accountId (required), status, limit.
get_note_by_idA
Vitally tool to retrieve full content of a specific note by ID
Inputs: noteId (required).
refresh_accountsC
Vitally tool to refresh the list of accounts
Inputs: limit.
search_accountsC
Vitally tool to search for accounts by multiple criteria
Inputs: name, externalId, limit.
search_toolsB
Vitally tool to search for available tools by keyword
Inputs: keyword (required).
search_usersC
Vitally tool to search for users by email or external ID
Inputs: email, externalId, emailSubdomain.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 11 tools
Tools are clearly distinct: account operations (find, search, refresh, get health, notes, tasks, conversations), note-specific operations, and search for tools/users. No overlapping functionality.
All tools follow a consistent verb_noun snake_case pattern (e.g., create_account_note, get_account_health, search_users), making the set predictable and easy to navigate.
11 tools strike a good balance for a CRM server, covering core account-related functions, notes, conversations, health, tasks, users, and a meta-search for tools. Not overwhelming or sparse.
Lacks essential CRUD operations: no create_account, update_account, or delete_account. Also missing create/update/delete for notes, tasks, and users beyond search. The surface feels incomplete for a CRM platform.
Maintenance
Related MCP Connectors
Read-only account intelligence for Customer Success and Account Management teams.
Search, summarize, and update your RevOrbit CRM (leads, deals, contacts, tasks) in plain language.
Ask your customer portal and CRM anything, in plain English
Search and act on Letter AI sales enablement content, deals, and call recordings.
Related MCP Servers
- FlicenseNot gradedqualityNot gradedmaintenanceEnables AI assistants to interact with Attio CRM through natural language, providing complete access to companies, people, deals, tasks, lists, notes, and records with advanced search, batch operations, and relationship management.2,129 npm-
- AlicenseAqualityDmaintenanceConnects AI assistants to Custify customer success data for querying account metrics, health scores, and usage trends. It also supports taking actions such as creating notes, managing tasks, and triggering playbooks.1516 npm3MIT
- AlicenseNot gradedqualityCmaintenanceConnects Twenty CRM with AI assistants like Claude, enabling natural language interactions with customer data. Supports CRUD operations for people, companies, tasks, notes, and advanced search.55 npm108MIT
- FlicenseNot gradedqualityDmaintenanceEnables querying and managing a CRM database through natural language conversations with Claude Desktop.-