An MCP server that enables AI agents to interact with One Identity Safeguard for Privileged Passwords (SPP) appliances.
MCP (Model Context Protocol) is a standard that lets AI assistants — Claude, GitHub Copilot, and others — call external "tools" to take actions in the real world. This server exposes Safeguard's REST API as MCP tools, so an AI agent can manage privileged access, look up accounts, create assets, run reports, diagnose appliance health, and perform cross-server operations like migrations.
Install via npm (no .NET SDK required):
npx @oneidentity/safeguard-mcpOr install globally:
npm install -g @oneidentity/safeguard-mcp
safeguard-mcpThe container is built for shared deployments and defaults to the streamable-HTTP MCP transport on port 8080. For per-user stdio use, prefer the npm package or the standalone binary instead.
docker run -d --rm \
-p 8080:8080 \
-e SAFEGUARD_HOST=safeguard.corp.example.com \
docker.io/oneidentity/safeguard-mcpTLS certificate validation is on by default. If your appliance presents a self-signed or internal-CA certificate, see SSL Certificate Validation for the opt-out — prefer trusting the issuing CA over disabling validation.
The container's ENTRYPOINT is pinned to --http; to run it in
stdio mode (e.g. wired into an MCP client that launches its own
subprocess), override the entrypoint and pass -i so stdin stays
attached:
docker run -i --rm \
--entrypoint /app/SafeguardMcp \
-e SAFEGUARD_HOST=safeguard.corp.example.com \
docker.io/oneidentity/safeguard-mcpThe container runs as a built-in nonroot user (app, UID 1654).
For production HTTP deployments, see the
reference deployment shapes.
Self-contained binaries (no runtime needed) are available from GitHub Releases for Linux x64, Windows x64, and macOS ARM64.
Each release publishes a SHA256SUMS file alongside the archives, plus a
cosign public key (cosign.pub) for
verifying container image signatures. Verify before extracting.
Checksums:
sha256sum -c SHA256SUMS --ignore-missingThe SHA256SUMS file covers the release archives, the per-platform SPDX
SBOM (sbom-linux-amd64.spdx.json), and the cosign.pub key itself.
Container image signature — the image is signed with a key
held in an Azure Key Vault HSM. The public verification key is
committed to this repo at docs/cosign.pub and is
also attached to each GitHub Release as cosign.pub. For the strongest
guarantee, use the committed key (pinned in git history, out-of-band
from the release artifacts) to verify the image digest:
cosign verify --key docs/cosign.pub docker.io/oneidentity/safeguard-mcp:<tag>Once your client is wired up (see below), see docs/EXAMPLES.md for
example prompts covering discovery, account/asset management, access requests, password
and SSH key rotation, health checks, audits, and cross-server workflows.
See docs/CLIENT-SETUP.md for copy-pasteable stdio
configurations for Claude Desktop, Claude Code, VS Code (GitHub
Copilot), and GitHub Copilot CLI.
All four clients launch the same server (npx -y @oneidentity/safeguard-mcp)
and read the same SAFEGUARD_HOST environment variable; they differ only in
config file location and JSON key names.
On first use, the server prints a verification URL and one-time code; complete the sign-in from any browser to authorize the connection.
safeguard-mcp can also run as a long-lived HTTP server that multiple users share.
A single server process is bound to one appliance via SAFEGUARD_HOST; the bearer
rides on each MCP request, so the server holds no per-user state.
OAuth-bridge setup (recommended) — point your MCP client at
https://<your-mcp-host>/mcp and let it discover OAuth via the
server's /.well-known/* metadata. The client opens a browser-based
PKCE login against the appliance's rSTS, and the resulting bearer is
sent on each request automatically. The bridge is on by default in
HTTP mode, holds no long-lived secrets, and never retains a Safeguard
token. Set BRIDGE_DISABLED=true if you front the deployment with a
separate OAuth gateway.
No Authorization header — the client discovers the bridge from
/.well-known/oauth-protected-resource and runs the PKCE flow itself.
Paste-bearer setup (manual fallback) — only for older MCP clients that don't yet implement OAuth discovery, or for scripts/CI. Each user acquires a Safeguard user token locally and drops it into their MCP client config:
safeguard-mcp login --host safeguard.corp.example.com --print-bearer-only > token.txt{
"mcpServers": {
"safeguard": {
"url": "https://mcp.corp.example.com/mcp",
"headers": { "Authorization": "Bearer <paste-token-here>" }
}
}
}When you're done with the token (e.g. switching machines, ending a shift), revoke it server-side with the paired command:
safeguard-mcp logout --host safeguard.corp.example.com --input token.txtBoth login and logout exist solely to bridge clients that can't run
the OAuth flow themselves; once your client supports MCP OAuth
discovery, neither is needed.
For production HTTP deployments, see deploy/README.md.
The Safeguard appliance is the sole authentication authority in every deployment shape. No service-account credentials are stored in MCP config and no Safeguard tokens ever live in the MCP server's persistent state.
When safeguard-mcp runs as a child process of an MCP client (the default), it uses
the OAuth 2.0 Device Authorization Grant
(RFC 8628). On first use the server
prints a verification URL and a short one-time code; you complete sign-in from any
browser — same machine or a different one — and the token flows back automatically.
This works in containers and headless environments without needing a local browser,
and supports any authentication method your appliance allows (LDAP, RADIUS, SAML, etc.).
Device code grant must be enabled on the Safeguard appliance (Safeguard Access settings).
When the server runs as a shared HTTP service, each MCP request carries its own
Authorization: Bearer <token> header and the server forwards that bearer verbatim
to the appliance. The server has no ambient identity of its own; the appliance
validates the bearer on every call, so a revoked or expired token stops working the
moment the appliance says so.
Bearers reach the client one of two ways — see HTTP Mode above for the paste-bearer and OAuth-bridge flows.
Set SAFEGUARD_HOST to tell the server which appliance to connect to. On
first use, the server displays a verification URL and code — no passwords
stored in config files. A complete stdio server entry looks like:
{
"mcpServers": {
"safeguard": {
"command": "npx",
"args": ["-y", "@oneidentity/safeguard-mcp"],
"env": {
"SAFEGUARD_HOST": "safeguard.corp.example.com"
}
}
}
}See docs/CLIENT-SETUP.md for the exact file and
top-level key for each client (Claude Desktop, Claude Code, VS Code Copilot,
GitHub Copilot CLI) — VS Code uses servers instead of mcpServers, and the
Copilot CLI adds a "type": "local" field.
Setting SAFEGUARD_HOST is recommended for every deployment. HTTP mode
requires it at startup. In stdio mode you may omit it if your MCP client
supports elicitation forms — on the first Safeguard_Connect call the server
pops a form asking for the appliance address. Without elicitation support, the
server returns an error telling you to set SAFEGUARD_HOST and restart.
TLS certificate validation is enabled by default and is the recommended posture for production. If your appliance presents a self-signed or internal-CA certificate, the right fix is to trust the issuing CA on the host running the server, not to disable validation.
Only as a last resort (e.g. a throwaway lab appliance), set
SAFEGUARD_IGNORE_SSL=true to skip validation:
{
"env": {
"SAFEGUARD_HOST": "safeguard.corp.example.com",
"SAFEGUARD_IGNORE_SSL": "true"
}
}SAFEGUARD_IGNORE_SSL is read once at process start. If the server hits a
TLS verification failure during connect and SAFEGUARD_IGNORE_SSL is not
set, stdio mode will offer an elicitation prompt asking whether to disable
TLS verification for the lifetime of the current session; accepting flips
the policy in-memory only. HTTP mode does not prompt — fix the trust chain,
or set the env var if you must.
The HTTP relay was designed around a few hard invariants worth stating explicitly:
- The appliance is the sole authentication authority. The relay never validates a Safeguard bearer locally — every MCP request is forwarded to the appliance, and appliance-side revocation takes effect immediately.
- No long-lived secrets in the server. The relay holds no client secrets, no service accounts, no refresh tokens, no cookie keys. The OAuth bridge is a public PKCE client; it issues no shared secrets to MCP clients and stores none itself.
- Bearers never enter persistent state. The bearer rides on each request in a
per-request scoped
HttpRelaySafeguardSessionand is discarded when the request completes. See Bearer token handling below for the full lifecycle in both modes. - Logs are scrubbed. Both stdio and HTTP modes route through a single
redaction provider that strips JWT-shaped tokens, OAuth
code/code_verifiervalues, andAuthorization: Bearer …headers before any log sink sees them. - Single appliance per process. A given server process is bound to one
appliance via
SAFEGUARD_HOST. Cross-appliance token confusion is not possible in HTTP mode because no process serves more than one appliance. - Plaintext credentials are tagged for an audience-aware future. Account
passwords, SSH private keys, API client secrets, TOTP codes, and secure-file
contents are returned only through
Safeguard_RetrieveCredential, in a two-block response that puts the plaintext underaudience=["user"]so a host that filters on the MCPaudienceannotation could route it to a secure pane without it entering the LLM's context. The annotation is an optional hint the spec does not require hosts to honor, so treat block 2 as potentially visible to the model unless you have verified your host filters on it; the separation is in place so any host that adds the filter gets the routing automatically.Safeguard_Executerefuses every sensitive path and redirects the caller to the matchingSafeguard_RetrieveCredentialkind. See Sensitive credential delivery below. - Forwarded-headers trust is bounded. The bridge derives its public URL from
each incoming request, with
UseForwardedHeadersenabled but configured to trust only loopback + RFC1918 (10/8, 172.16/12, 192.168/16) by default. That covers the common cluster-internal-ingress case; production deployments behind ingress should ensure their proxy is in a trusted CIDR or extend the trust list viaBRIDGE_TRUSTED_PROXIES(comma-separated CIDRs). Untrusted hops are ignored, so a malicious client cannot spoofX-Forwarded-Hostto make the bridge publish a different authorization-server URL.
Both transports must hold a Safeguard user token in memory while they are actively calling the appliance on the caller's behalf. The window is bounded, deterministic, and as defensively allocated as the framework allows. Neither mode persists the token to disk; neither mode logs it; neither mode keeps a managed-string copy on a long-lived field. The two transports differ only in how long the in-memory hold lasts, because their trust boundaries differ.
The HTTP relay does not authenticate users itself. Every MCP request must
arrive carrying its own Authorization: Bearer <token>; the relay forwards
that bearer to the appliance for the duration of that one request and is
done with it when the request returns.
-
Per-request scope, never wider.
HttpRelaySafeguardSessionis registered with ASP.NET Core's scoped lifetime, so a fresh instance is created for every MCP HTTP request and disposed when the request ends. There is no singleton, no static field, and no cross-request dictionary anywhere in the process that holds a bearer. The next request from the same caller must bring its ownAuthorizationheader — the server has nothing to fall back on. -
SecureStringfor the in-flight copy. When the session first needs to call the appliance during a request, it extracts the bearer from the request'sAuthorizationheader, copies it into a sealedSecureStringviaSessionHelpers.ToSecureString, and hands thatSecureStringto the SafeguardDotNet SDK'sSafeguard.Connect(host, secureBearer, …). The SDK keeps its ownSecureStringcopy inside the connection it returns; our local copy is wrapped inusingand zeroed the moment the SDK has the connection. The SDK uses its copy transiently when building each outboundAuthorizationheader to the appliance. -
Deterministic zeroing on disposal. When the DI scope ends —
HttpRelaySafeguardSession.Dispose()→_connection.Dispose()— the SDK'sSecureStringis zeroed before its buffer is released. The bytes do not sit in the managed heap waiting for GC to maybe-someday reclaim them. Disposal is driven by the framework's scope teardown; no code path can forget to release the connection. -
No appliance-side state to clean up. A 401 from the appliance during a request is surfaced to the caller as "token expired or revoked; re-acquire and retry" — the relay does not attempt to refresh, because the bearer is not the relay's to refresh. Token rotation is the caller's responsibility.
The end-to-end shape: the bearer enters with the request, is held just long enough to talk to the appliance, and is wiped from process memory when the request returns. The agent is the sole long-term holder of the token; the relay borrows it briefly for each request.
A stdio process is a single user's local agent talking to a single local
MCP client over the process's stdin/stdout. The process itself performs
the Safeguard login that produces the token — device-code by default, or
PKCE when SAFEGUARD_PROVIDER / SAFEGUARD_USER / SAFEGUARD_PASSWORD
are all set — so the trust boundary is the process boundary itself.
-
One cached connection per process.
StdioSafeguardSessionis a singleton; on first tool call it drives the login flow and caches the resultingISafeguardConnection. The SDK holds the token inside aSecureStringon that connection; all subsequent tool calls in the same process reuse it. There is no second copy on the session object, no on-disk file, no environment variable that retains the token after login completes. -
Refresh in place, never re-materialized as a string. Before every tool call the session checks
GetAccessTokenLifetimeRemaining()and silently callsRefreshAccessToken()if fewer than five minutes remain. Both calls happen entirely inside the SDK against its ownSecureString; the bearer is never lifted back into a managedstringon our side. -
Deterministic zeroing on logout or graceful shutdown.
Safeguard_Disconnect(the MCP tool) and graceful host shutdown both route throughDisposeConnectionLocked(), which callsconnection.LogOut()— the SDK posts to the appliance's/Token/Logoutto revoke server-side and zeroes itsSecureString— thenconnection.Dispose(). The session's_connectionfield is nulled; the next tool call re-auths from scratch. The companion CLIsafeguard-mcp logoutruns in its own short-lived process and reaches the same appliance endpoint directly; the stdio server it pairs with discovers the resulting invalidation on the next API call (a 401 surfaces as "re-authenticate" to the agent). -
No disk persistence, ever. Tokens are never written to disk by the stdio server.
safeguard-mcp login --output <path>is the one and only path that places a token on disk, and only because the user explicitly asked for it — and it does so with a restrictive ACL (Unix 0600; Windows: explicit user-only ACE, inheritance disabled).safeguard-mcp logoutrevokes that token server-side; the file itself is the user's to manage.
In both modes, RedactingLoggerProvider is wired in front of every log
sink (stderr and the rolling file) and strips JWT-shaped tokens, OAuth
code/code_verifier values, and Authorization: Bearer … headers
before any sink sees them — so even an exception that surfaces a
request/response payload cannot leak the bearer to disk through a log.
Bearer tokens authenticate the caller; the plaintext secrets Safeguard manages (account passwords, SSH private keys, API client secrets, TOTP codes, generated passwords, secure-file contents) are a separate class of sensitive material with their own delivery contract. The server's goal is that the LLM can drive the workflow that produces those secrets without the secret itself ever needing to enter the assistant's context window.
Safeguard_RetrieveCredential is the single tool that returns plaintext
credential material. Every call returns a two-block MCP response:
- Block 1 —
audience=["assistant"]. A JSON envelope:kind, the subject ids (accessRequestId,accountId,apiKeyIdas applicable), delivery flags, and any notices. This block carries no secret value; it is what the LLM reads to confirm "yes, the credential was delivered for the right subject" and to plan the next step. - Block 2 —
audience=["user"]. The human-formatted plaintext (PlatformAccount password = …,BEGIN OPENSSH PRIVATE KEY …, the TOTP code with its validity window, etc.).
The MCP spec (2025-06-18) defines audience as an optional annotation a
host MAY use to route content — for example, rendering audience=["user"]
content in a secure pane separate from the assistant's transcript. The
spec does not require hosts to filter on it, and host support varies; do
not assume block 2 is hidden from the model unless you have verified your
host filters tool-result content blocks by audience. The two-block split
is in place so that any host that does honor the annotation will keep the
plaintext out of the model's context automatically, and so that operators
who want stronger separation have a clear contract to write a custom
filter against. Because hosts may not honor the hint, Safeguard's own
controls — auditability and rotation on the appliance — are the
authoritative defense against accidental disclosure: every retrieval is
logged, and credentials can be rotated if a host's handling is wrong for
your environment.
The audience annotation is defined in the MCP specification:
Resources → Annotations
("An array indicating the intended audience(s) for this resource. Valid
values are \"user\" and \"assistant\"."). Annotations apply to
resources, resource templates, and content blocks, which is the path
this tool uses.
The dynamic catalog flags every Safeguard API path that returns plaintext
credential material as sensitive. When Safeguard_Execute is called
against any of those paths, the appliance is not contacted — the
server returns a structured sensitive_endpoint_redirected envelope
naming the matching Safeguard_RetrieveCredential kind and the
arguments to pass. The agent lifts data.next_call into a follow-up
tool invocation, which routes through the two-block response above.
This makes the audience split the only path the plaintext can take
out of the server: there is no Execute escape hatch that returns a raw
secret in a single block addressed to the assistant.
A heuristic backstop covers any future sensitive path that hasn't been
added to the catalog yet (response shape and field-name pattern
matching), tuned to not fire on audit-log payloads. If a future build
adds a sensitive endpoint and the catalog hasn't been updated, the
heuristic still steers the agent toward
Safeguard_RetrieveCredential instead of returning the secret through
Safeguard_Execute.
- Access-request material:
access-request-password,access-request-ssh-key,access-request-api-key,access-request-totp,access-request-file(each requiresaccessRequestId). - Personal vault material:
personal-account-password,personal-account-password-history,personal-account-totp(each requiresaccountId). - Asset-account API material:
asset-account-api-secret-history(requiresaccountIdANDapiKeyId). - Out-of-band generation:
generated-password(no extra args; produces a rule-compliant value without persisting it).
- It never persists plaintext credential material to disk or any log sink, regardless of audience. The redaction pipeline in front of the log sinks treats credential blocks the same as bearer tokens.
- It never reuses block-2 content for any in-process purpose other than returning it to the caller — there is no cache, no rolling buffer, no envelope around it that an internal handler reads.
- It never mints credentials client-side (no
Get-Random, no LLM- generated values) for managed accounts; password generation goes through the appliance's password rule viaPOST /v4/AssetAccounts/{id}/GeneratePasswordor.../ChangePassword, captured by thegenerated-passwordkind.
Safeguard has over 1,000 REST endpoints across three services (Core, Appliance, Notification).
The naive approach to exposing this to AI would be to register one MCP tool per endpoint —
Get_Users, Create_AssetAccount, Delete_AccessRequest, and so on. This fails in
practice: MCP clients choke on hundreds of tools, agents can't reason about which tool to
pick, and every Safeguard version change requires regenerating the tool surface.
Instead of 1,000+ individual tools, this server provides a small, stable set of meta-tools that give the agent the ability to navigate, understand, and execute against any endpoint dynamically. Think of it as providing a map and a car rather than building 1,000 individual roads.
The complete tool surface is 11 tools:
| Tool | Purpose |
|---|---|
Safeguard_Connect |
Authenticate to one or more appliances via device code |
Safeguard_Disconnect |
Revoke the active Safeguard token and drop the cached connection |
Safeguard_Discover |
Search the API catalog by keyword, service, or HTTP method (at least one narrower required) |
Safeguard_Schema |
Get the request/response shape for a specific endpoint |
Safeguard_Reference |
On-demand reference: query syntax, workflow recipes, enum values, terminology, overview, and common patterns (topic= selects the source; search= returns just one section) |
Safeguard_Execute |
Call any endpoint on any service (auto-routes from the bare /v4/... path) |
Safeguard_OpenAccessRequest |
One-call composite that pre-checks entitlements and submits an access request |
Safeguard_CloseAccessRequest |
State-aware close: dispatches to Cancel / CheckIn / Close / Acknowledge based on the request's current state |
Safeguard_RetrieveCredential |
Returns plaintext credential material (passwords, SSH keys, API secrets, TOTP codes, files) in a two-block response that splits metadata from plaintext by MCP audience — see Sensitive credential delivery |
An agent working through a task follows this pattern:
- Discover — "find me endpoints related to password change failures"
- Schema — "what fields does a POST to /v4/AssetAccounts require?"
- QueryHelp — "how do I filter by name and sort by date?"
- Execute — call the endpoint with the correct parameters and body
For a small number of end-to-end flows where the multi-step path has well-known pitfalls — opening an access request is the first example — a single composite tool collapses the sequence into one call and catches the common failure modes up front.
The API catalog is loaded from the appliance's own Swagger/OpenAPI spec at connect time, so the tool surface always matches the actual API of the connected appliance — whether it's running Safeguard 7.5, 8.2, or a future release.
When Safeguard_Connect authenticates to an appliance, it triggers a background catalog
load. The loader fetches three OpenAPI specs in parallel:
https://{host}/service/Core/swagger/v4/swagger.json
https://{host}/service/Appliance/swagger/v4/swagger.json
https://{host}/service/Notification/swagger/v4/swagger.json
These are public endpoints (no auth required on most versions). The loader parses each spec to extract:
- Endpoints — method, path, service, summary, query parameters, whether it accepts a body
- Schemas — request and response body structures with property names, types, required
flags, and descriptions (extracted from
components/schemasandrequestBodyrefs)
The result is cached per-host in memory. Each connection gets its own catalog, so connecting to two appliances running different Safeguard versions gives each one the correct endpoint list and schemas for its version.
The SSL policy used for swagger fetch matches the connection's SSL policy — if you set
SAFEGUARD_IGNORE_SSL=true for self-signed lab certs, the catalog loader respects that too.
For read operations (GET), an agent just needs Discover and Execute. But write operations (POST, PUT, PATCH) are where most AI integrations fall apart — the agent doesn't know what fields are required, what the valid values are, or how objects relate to each other.
The Schema tool solves this by extracting structured field information from the OpenAPI spec:
required fields, types, descriptions, nested object shapes, and a minimal working example.
An agent can call Safeguard_Schema for POST /v4/AssetAccounts, learn that Name and
Asset.Id are required, and construct a valid request body without trial and error.
For fields whose type is an enumeration, Safeguard_Reference topic=enum returns the valid values by name —
so the agent never has to guess whether the API expects "RemoteDesktop" or "Rdp", or
whether casing matters.
Safeguard's REST API uses different names than the product UI in several important places.
The most significant: what administrators know as Entitlements is the /v4/Roles
endpoint in the API. An agent (or user) asking to "create an entitlement" would never find
the right endpoint by searching the API literally — the word "entitlement" doesn't appear
in the Roles endpoint path or summary.
This is a common problem with enterprise products that evolved over many releases — UI terminology shifts but API paths remain stable for backward compatibility.
The server addresses this with two layers:
-
Search-time expansion — When
Safeguard_Discoverreceives a search term, it checks a terminology alias map and expands the search to include related API terms. Searching "entitlement" automatically also searches for "roles." This works transparently — the agent doesn't need to know the mapping exists. -
MCP Resource — The server exposes a
safeguard://terminologyresource that AI agents can read for full context on product-to-API naming differences. This gives agents proactive awareness of the terminology landscape before they even start searching.
Current mappings include Entitlement→Roles, Managed Account→AssetAccounts,
Partition→AssetPartitions, Platform→Platforms, and others. The map has grown beyond
simple noun-to-noun aliases to cover verbs an agent will reach for
(move/transfer/migrate → partition reassignment), umbrella concepts that
have no literal API endpoint (privileged access, pam → access requests), and
vague status questions (uptime/boot/system time → ApplianceStatus / SystemTime
/ Version / Health). It's designed to grow as real-world usage reveals additional gaps.
Safeguard operations rarely involve a single API call. Diagnosing password rotation failures requires checking task properties, pulling change request logs, testing connectivity, and inspecting service account profiles. A health check spans appliance status, hardware metrics, cluster state, and audit logs.
Without domain guidance, an agent would need dozens of exploratory calls to figure out these sequences — or worse, it guesses wrong and misses critical steps.
Workflow recipes encode the operational expertise of a Safeguard administrator into step-by-step instructions the agent follows. They specify which endpoints to call, what parameters to use, how to interpret results, and what to do next. Pre-built recipes cover:
- Appliance & cluster health assessment
- Password/SSH key rotation status and task failure triage
- Access request workflows (password checkout, RDP/SSH sessions, emergency breakglass)
- Bulk asset and account onboarding
- User permission and entitlement audit
- Backup/restore and patch/upgrade procedures
- Directory integration (LDAP/AD import)
- Certificate and SSH key management
- A2A credential retrieval setup
- Personal password vault configuration
Recipes are reachable directly via Safeguard_Reference topic=workflows, and also surface
inline whenever Safeguard_Discover or Safeguard_Execute touches an endpoint family
a recipe covers — so an agent that started in the raw API can find the higher-level
path mid-task without having to think to ask for it.
Discovering an endpoint and reading its schema is only half the battle. Agents still mistype property names, search for the wrong concept, send the wrong combination of fields, or learn from a misleading error message. The server includes a layer of correction and guidance that fires on every Discover and Execute call:
-
Search-time term expansion. Discover quietly expands a search phrase to include the API's own vocabulary. "Entitlement" also searches "roles." "Privileged access" also searches "access requests." "Move" / "transfer" / "migrate" all point to partition reassignment. Agents who phrase a request the way an administrator would still land on the right endpoint.
-
Workflow recipes ranked ahead of raw endpoints. When a search matches both a multi-step recipe and individual endpoints, the recipe is surfaced first — so the agent reaches for the documented sequence (request → approve → check out → close) before trying to assemble one from scratch.
-
Composite tools where the multi-step flow is error-prone. For access requests, where the most common failure is a misleading "not authorized" error caused by picking the wrong asset for the right account, the server exposes
Safeguard_OpenAccessRequest. It checks the agent's actual entitlements before submitting, so the agent gets a clear "this account-and-asset combination isn't in your scope" message instead of an opaque appliance error after the fact.Safeguard_CloseAccessRequestis the matching closer — it reads the request's current state and dispatches to the right verb (Cancel before approval, CheckIn while checked out, Close after check-in, Acknowledge after expiry) so the agent doesn't have to encode the close-state matrix itself. AndSafeguard_RetrieveCredentialowns every endpoint that returns plaintext secret material; see Sensitive credential delivery for the audience-split response shape and the refuse-and-redirect envelope that steersSafeguard_Executecallers back to it. -
"Did you mean" on rejected property names. When the appliance rejects a filter, sort, or field-selection because of a wrong property name, the response includes the nearest valid name from the schema ("
UserName→ did you meanName?"). Most agent loops repair themselves on the next call. -
Errors come back with next-step hints. Common HTTP errors are annotated with the tool to reach for or the field to check, instead of being passed through raw. See Response intelligence below for the full list.
-
Cross-linking between Discover, Execute, and Workflows. Any Execute call against a recipe-covered endpoint family includes a one-line pointer back to the matching recipe and any composite tool that covers it.
-
Responses are sized so they fit an agent's context window. Collection GETs get a default
limit=50injected, oversized payloads are truncated with a note explaining how to narrow the query (fields=, filters,format=csv), and the agent is told why the result was clipped instead of just receiving a silently-shortened payload. See Response intelligence for the knobs.
Taken together, these techniques mean the agent rarely fails twice in the same way: a wrong term, a wrong field, or a wrong combination produces a response that contains both the failure and the fix.
MCP Resources are URI-addressable reference documents that clients can load into the agent's context at the start of a session — before any tool calls happen. This gives the agent "day-one knowledge" of Safeguard's API without consuming tool-call round trips.
| Resource URI | Content |
|---|---|
safeguard://api-overview |
Service map — what endpoints exist, how objects relate |
safeguard://query-syntax |
Complete filter, field, ordering, and pagination syntax |
safeguard://common-patterns |
Lookup-by-name, create-with-deps, bulk ops, error handling |
safeguard://terminology |
Product UI terms → API endpoint name mappings |
Clients that support MCP resource preloading can inject all four at session start. Clients that don't can still access the same content
via tool calls (Safeguard_Reference, Safeguard_Discover).
A single safeguard-mcp process binds to a single appliance. To address more than
one appliance from a single agent conversation, run more than one server and wire
the agent to all of them — each gets its own MCP server entry in your client's
config.
In stdio mode this falls out naturally: most MCP clients let you declare
multiple servers in one config file, each launched with its own SAFEGUARD_HOST.
In HTTP mode, the same applies: deploy each appliance behind its own URL. Same
image, same chart, same configuration schema — the only thing that changes between
deployments is the value of SAFEGUARD_HOST.
A Safeguard cluster can include read-only replica appliances. Replicas serve the full access-request workflow (credential retrieval, sessions), A2A, and all configuration reads; they reject only configuration writes. That makes them useful targets for an MCP fleet in two ways:
- Read scaling / load isolation — run a second HTTP deployment with
SAFEGUARD_HOSTpointed at a replica. Agents that mostly read the catalog, query objects, or fetch credentials route there; agents that need to mutate configuration go through the primary deployment. - No-config-mutation safety boundary — to make AI agents incapable of mutating Safeguard configuration at all, deploy only an MCP fleet pointed at a replica. Credential retrieval, sessions, and A2A still work; configuration writes return a clean appliance-level error.
The deployment shape is identical to the primary fleet. The only operational
difference is which address you put in SAFEGUARD_HOST.
To talk to more than one appliance from a single MCP client, declare one
safeguard-mcp server entry per appliance in your client config — each with
its own SAFEGUARD_HOST. Each entry is its own process bound to one
appliance. There is no per-call host parameter on Safeguard_Connect or
Safeguard_Execute; a process serves exactly one appliance for its
lifetime, in both stdio and HTTP mode.
The agent routes by server name, not by appliance address: every entry
exposes the same tool names (Safeguard_Execute, Safeguard_Discover, …)
with identical descriptions, and SAFEGUARD_HOST is invisible at the tool
layer. Give each entry a descriptive key (safeguard-prod,
safeguard-dr, …) so the model has a clear signal, and tell the agent
which one to use in your prompt. Discovery and catalog results are also
per-server, so Safeguard_Discover on one entry will not surface endpoints
from another.
Setting SAFEGUARD_HOST (as in Typical Setup) is the
recommended form. As an exception, if your MCP client supports elicitation
forms you can omit it and let the server prompt for the appliance on first
connect:
{
"mcpServers": {
"safeguard": {
"command": "npx",
"args": ["-y", "@oneidentity/safeguard-mcp"]
}
}
}See docs/CLIENT-SETUP.md for the equivalent in each
client. Without elicitation support, set SAFEGUARD_HOST in env. HTTP mode
always requires SAFEGUARD_HOST at startup.
Safeguard's three services (Core, Appliance, Notification) each handle different endpoint
families. Agents and users often don't know — or shouldn't need to know — which service owns
a given endpoint. The unified Safeguard_Execute tool resolves the correct service
automatically by looking up the path in the API catalog. One tool handles everything.
Callers pass bare /v4/... paths. The appliance's real URL for any endpoint is
https://{host}/service/{Name}/v4/... (Core/Appliance/Notification), and the
dispatcher prepends /service/{Name}/ for you. Paths that already include the
/service/{name}/ prefix are rejected at pre-flight with a directive that
names the corrected /v4/... form — they would otherwise hit the wire as
/service/{name}/service/{name}/v4/... and 404. This contract applies to
Safeguard_Execute and Safeguard_Schema; Safeguard_Reference topic=query-syntax surfaces
the same directive as a notice but still returns the general syntax help.
When you call Safeguard_Execute with a path like /v4/AssetAccounts, the dispatcher:
-
Catalog lookup — scans the dynamic catalog for an endpoint whose path template matches the request path, accounting for
{id}placeholders. If found, uses that endpoint's service. -
Heuristic fallback — for the handful of undocumented endpoints that aren't in swagger, applies keyword-based routing:
ApplianceStatus,Backup,Network,DiagnosticPackage→ Appliance service/v4/Status→ Notification service- Everything else → Core service (handles ~90% of endpoints)
The dispatcher adds guardrails to prevent context window overflow and guide the agent:
-
Auto-limit injection — collection GETs without an explicit
limitparameter getlimit=50injected automatically (configurable). This prevents accidental retrieval of 10,000+ records that would overwhelm an agent's context. -
Array truncation — if a response contains a JSON array with more items than
MaxResultsBeforeTruncation(default 100), only the first N items are returned, with a note explaining how to filter or paginate for more. -
Character truncation — responses exceeding
MaxResponseChars(default 30,000) are cut with a note suggestingfieldsselection orformat=csvto reduce payload. -
Error enrichment — HTTP and appliance errors get contextual next-step hints rather than being passed through raw:
- 400 → "Use Safeguard_Schema to see required fields"
- 401 → "Call Safeguard_Connect to re-authenticate"
- 404 → "Verify the ID exists using a GET call"
- rejected property name on a filter/sort/field selection → "did you mean X?" using the nearest valid name from the schema
- access-request "not authorized to use this request type" → "Use Safeguard_OpenAccessRequest, which pre-checks entitlements before submitting"
- any call against a recipe-covered endpoint family → one-line pointer back to the matching workflow recipe and any composite tool that covers it
All response thresholds are configurable in appsettings.json:
{
"Safeguard": {
"MaxResultsBeforeTruncation": 100,
"MaxResponseChars": 30000,
"DefaultLimit": 50,
"AutoInjectLimit": true
}
}The server supports two transport modes:
Stdio (default) — the standard MCP transport used by VS Code, Claude Desktop, and most MCP clients. The server reads/writes JSON-RPC messages over stdin/stdout:
SafeguardMcp # stdio mode (default)HTTP (Streamable HTTP) — for shared, network-accessible deployments. The server
starts a web endpoint at /mcp that MCP clients connect to over HTTP. In this mode
the server is a pure relay: it holds no Safeguard tokens; every MCP request must
carry its own Authorization: Bearer <token> header, which the server forwards to
the appliance. The appliance is what validates the bearer on every call.
SafeguardMcp --http # HTTP mode, default http://0.0.0.0:8080Configure the listening URL via standard ASP.NET Core mechanisms (--urls,
ASPNETCORE_URLS environment variable, or appsettings.json).
A /healthz endpoint is exposed alongside /mcp for Kubernetes liveness/readiness
probes and load-balancer health checks; it returns 200 Healthy once the host is up.
TLS termination is your responsibility. The server binds plain HTTP by default; production deployments must front it with an authenticated reverse proxy, ingress controller, or service mesh (mTLS, OAuth proxy, API gateway, etc.) that terminates TLS. The bundled OAuth bridge enforces RFC 6749/8252 redirect-uri rules but does not itself perform TLS.
For production HTTP deployments — Kubernetes manifests, Helm chart, Docker Compose,
and the operational runbook — see deploy/README.md.
If you want to run from source or contribute:
Prerequisites: .NET 10 SDK
git clone https://github.com/OneIdentity/safeguard-mcp.git
cd safeguard-mcp/src/SafeguardMcp
dotnet runFor development workflow details, see CONTRIBUTING.md.
Under active development.
Apache 2.0 — see LICENSE
{ "mcpServers": { "safeguard": { "url": "https://mcp.corp.example.com/mcp" } } }