Safeguard.sh Documentation Center

Servers & Agents

How Guard maintains an inventory of the MCP servers and AI agents in your environment, and how policy targets them.

Servers & Agents

Guard enforces policy against traffic flowing between AI agents and MCP servers — but to do that precisely, it first needs to know which servers and agents actually exist in your environment. Guard keeps an inventory of both as first-class records: MCP servers (the tool-providing backends your agents call) and agents (the AI clients making those calls). This page covers why that inventory matters, how records get created, and how Guard learns about the tools a registered server exposes.

Why Guard needs an inventory

Without a record of a server or agent, Guard has no name to target. Registering a server or agent gives Safeguard something concrete to reference elsewhere in the product:

  • Scoped policy. Rules can be written against a specific server or agent rather than applying blindly to every request in the org. See Guard Policies for how targeting works once a server or agent exists.
  • Lifecycle control. Agents can be individually blocked, unblocked, or quarantined (see Agent status below) — actions that only make sense once Guard has a durable record of the agent to act on.
  • Health and activity tracking. Each server carries a health status, and each agent accumulates request counts, a risk score, and an anomaly score over time, all attached to that inventory record.
  • Audit context. Alerts and audit trails reference servers and agents by their registered name and ID, rather than by raw upstream URLs or ephemeral connection details. See Audit Trail & Alerts.

Registering an MCP server

An MCP server is registered explicitly — there is currently no mechanism that scans your network or environment and creates server records automatically. You (or your integration) create the record via the API, supplying the server's identifying details: a name and description, its upstream URL, which transport it speaks, and how Guard should authenticate to it.

FieldNotes
NameA human-readable identifier for the server.
DescriptionOptional free-text context.
Upstream URLThe address Guard calls to reach the server — used both for tool discovery and health checks.
Transportsse or http.
Auth typenone, bearer, basic, or api_key.
Auth configCredentials/config matching the chosen auth type.
Statusactive, inactive, or error — controls whether the server is included in scheduled discovery (see below).

Once created, a server also carries a health status (healthy, unhealthy, or unknown) and a timestamp of the last health check, both maintained by Guard rather than set by you directly.

New servers always start as active with health status unknown. Whatever value you send for status on the create request is ignored — Guard forces every newly created server to active (and health_status to unknown), the same way agent creation forces active (see Registering an agent below). If you need a server excluded from scheduled discovery and health checks right away, create it and then follow up with a PUT to set status: "inactive" — unlike create, update does respect whatever status you send.

auth_config by auth type

The keys Guard reads out of auth_config when it calls the server depend on auth_type:

auth_typeExpected auth_config keysSent upstream as
noneNo Authorization header.
bearertokenAuthorization: Bearer <token>
basicusername, passwordHTTP Basic auth
api_keykey, header (optional)<header>: <key>header defaults to X-API-Key if omitted

Example: register a server

curl -X POST https://<your-guard-host>/api/v1/servers \
  -H "Authorization: Bearer <your-jwt>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "billing-mcp",
    "description": "Internal billing tools server",
    "upstream_url": "https://internal-mcp.example.com",
    "transport": "http",
    "auth_type": "bearer",
    "auth_config": { "token": "upstream-service-token" }
  }'

Guard responds 201 Created with the full record, including the fields it fills in itself:

{
  "id": "5b6b1e3a-2f36-4b8b-9b8a-6f2a2c9e6b41",
  "org_id": "a3d2f9c1-...",
  "name": "billing-mcp",
  "description": "Internal billing tools server",
  "upstream_url": "https://internal-mcp.example.com",
  "transport": "http",
  "auth_type": "bearer",
  "auth_config": { "token": "upstream-service-token" },
  "status": "active",
  "health_status": "unknown",
  "last_health_at": null,
  "created_at": "2026-07-07T00:00:00Z",
  "updated_at": "2026-07-07T00:00:00Z"
}

Servers support full CRUD: list (GET /api/v1/servers), fetch by ID (GET /api/v1/servers/{id}), update (PUT /api/v1/servers/{id}), or delete (DELETE /api/v1/servers/{id}) — for example, to rotate credentials or change the upstream URL.

PUT replaces the whole record — it isn't a partial patch. The request body is decoded straight into the server's fields and written back as-is, so a PUT containing only {"name": "new-name"} overwrites every other field (including upstream_url and auth_config) with empty values. GET the current record first, change only what you need in that object, and send the complete object back.

Deleting a server removes its inventory record. Make sure no policy still depends on targeting that server before deleting it, and expect prior audit history to continue referencing the server's ID even after removal.

Registering an agent

Agents are also registered explicitly, by supplying a name and description. There's no equivalent of server tool-discovery for agents — Guard does not automatically detect new agents on the network; an agent record must be created before Guard can associate activity, risk scoring, or lifecycle actions with it.

When an agent is created, Guard always sets its status to active, regardless of what status value (if any) is supplied in the request. From there, an agent accumulates:

FieldNotes
Statusactive, blocked, or quarantined.
Risk scoreA running score maintained by Guard.
Anomaly scoreA running score maintained by Guard.
Total requests / Blocked requestsCounters maintained by Guard.
Last seen atTimestamp of the agent's most recent activity.
MetadataFree-form key/value data.

Example: register an agent

curl -X POST https://<your-guard-host>/api/v1/agents \
  -H "Authorization: Bearer <your-jwt>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "checkout-assistant",
    "description": "Customer-facing checkout agent",
    "metadata": { "team": "commerce", "env": "prod" }
  }'
{
  "id": "9e1c2a4d-8f21-4a3e-9b2f-3e5d7c1a9b02",
  "org_id": "a3d2f9c1-...",
  "name": "checkout-assistant",
  "description": "Customer-facing checkout agent",
  "status": "active",
  "risk_score": 0,
  "anomaly_score": 0,
  "total_requests": 0,
  "blocked_requests": 0,
  "last_seen_at": null,
  "metadata": { "team": "commerce", "env": "prod" },
  "created_at": "2026-07-07T00:00:00Z",
  "updated_at": "2026-07-07T00:00:00Z"
}

Sending "status": "blocked" in that same request body has no effect — the response still comes back with "status": "active".

Agent status

Agent status is changed through dedicated lifecycle actions rather than a general update endpoint:

  • BlockPOST /api/v1/agents/{id}/block — sets the agent's status to blocked.
  • UnblockPOST /api/v1/agents/{id}/unblock — sets the agent's status back to active.
  • QuarantinePOST /api/v1/agents/{id}/quarantine — sets the agent's status to quarantined.

Each takes no request body and returns the new status:

curl -X POST https://<your-guard-host>/api/v1/agents/9e1c2a4d-8f21-4a3e-9b2f-3e5d7c1a9b02/block \
  -H "Authorization: Bearer <your-jwt>"
{ "status": "blocked" }

Agents can also be listed (GET /api/v1/agents), fetched individually (GET /api/v1/agents/{id}), and deleted (DELETE /api/v1/agents/{id}).

Blocking or quarantining an agent is a status change on the agent's inventory record itself — it's a different mechanism from writing a policy rule that targets the agent. Use lifecycle actions for an immediate, direct response to a specific agent; use policy for standing rules. See Guard Policies.

For a live, per-request history of an agent's risk, jailbreak, and anomaly scores — rather than the single running numbers on the agent record — query the audit trail filtered by that agent's ID. See Audit Trail & Alerts.

How discovery works

"Discovery" in Guard refers specifically to tool discovery for a server that is already registered — Guard does not discover and auto-create new server or agent inventory records on its own.

Once a server exists and is reachable, Guard queries it for the tools it exposes:

  1. Guard sends a JSON-RPC tools/list request to the server's upstream URL:

    { "jsonrpc": "2.0", "method": "tools/list", "id": 1 }
  2. The server responds with its available tools, each with a name, description, and input schema:

    {
      "result": {
        "tools": [
          {
            "name": "get_invoice",
            "description": "Fetch an invoice by ID",
            "inputSchema": { "type": "object", "properties": { "invoice_id": { "type": "string" } } }
          }
        ]
      }
    }
  3. Guard upserts those tools into its own store, associated with that server's ID and your org.

This runs automatically, on a schedule: a background worker process iterates over active servers and re-runs discovery for each on a fixed interval — 5 minutes by default (DISCOVERY_INTERVAL), configurable per deployment — running once immediately when the worker starts and then repeating. There is currently no API call to trigger a one-off discovery run for a single server on demand. If a server's tool list needs to be refreshed sooner than the next scheduled pass, the practical options are waiting for the interval to elapse or, for self-hosted deployments, lowering DISCOVERY_INTERVAL.

Separately, the same background worker periodically checks each server's reachability on its own interval — 1 minute by default (HEALTH_CHECK_INTERVAL) — with a plain HTTP request to the server's upstream URL, and records the result as health_status: healthy if the request succeeds with a non-5xx response, unhealthy if the request fails outright or the server responds with a 5xx. This health check is about whether the server is up, not about discovering new servers or tools, and it runs independently of tool discovery.

Tool discovery only runs for servers with active status, and requires the server to be reachable at its configured upstream URL. If a server was just registered, just switched to active, or its upstream URL just changed, its tool list may be stale for up to one discovery interval until the next scheduled pass completes.

FAQ & Troubleshooting

Does Guard automatically find MCP servers running in my environment? No. Server records are created explicitly, with their upstream URL and connection details. What Guard automates is discovering the tools a server exposes, once that server is registered.

Does Guard automatically find agents talking to my servers? Not from these inventory endpoints — agents are created explicitly. Guard doesn't currently create agent records on its own.

Why did a newly created agent come back as active even though I sent a different status? Agent creation always forces status to active; any status field in the create request is ignored. Use the block/unblock/quarantine actions to change it afterward.

I registered a server with "status": "inactive", but it came back active. Creating a server always forces status to active and health_status to unknown — the same way agent creation forces active — so any status you send on create is ignored. PUT, unlike POST, does respect whatever status you send: set it to inactive in a follow-up update if you need the server excluded from scheduled discovery and health checks right away.

I sent a PUT with only the one field I wanted to change, and the rest of the server's config got wiped. PUT /api/v1/servers/{id} replaces the whole record — it isn't a partial patch. Fields you omit from the request body are written back as empty. GET the current server first, change only what you need in that object, and send the complete object back in the PUT.

My server's tool list looks out of date. What's happening? Tool discovery only runs for servers with active status, on a fixed background schedule (5 minutes by default) rather than instantly on every change — there's no on-demand "discover now" call today. If a server is inactive or error, it's skipped entirely until you set it back to active.

Can I target a policy at one server or agent instead of all of them? Yes — that's the main reason to register servers and agents individually rather than relying on org-wide defaults. See Guard Policies for how targeting is configured.

  • Guard Policies — write rules that target specific servers or agents.
  • Audit Trail & Alerts — see activity and alerts tied to a server or agent's inventory record.
  • Guard SDK — embed policy enforcement directly inside an MCP server or agent process.
  • Guard Proxy — run Guard in front of MCP servers you don't control the code of, using the same server-registration API described on this page.

On this page