Public API and MCP Server
Overview
ET Ducky has a read-only API for outside dashboards, scripts and AI assistants. It has two surfaces that use the same credential and return the same data:
- REST endpoints under
/api/v1for agents, alerts and a fleet summary. - An MCP endpoint at
/api/mcpthat offers the same reads as tools for AI assistants that speak the Model Context Protocol.
Both are authenticated with an organization API token. A token reads one organization. Nothing on either surface changes data.
API tokens
Tokens are managed in Team → Workspace → API Tokens. Organization admins and owners can see the card's token list, create tokens and revoke them, and so can members holding the API editor privilege. It is granted like the other member privileges, for one organization or for the whole workspace. Other members see a message saying who can manage tokens. A token keeps working after the member who created it loses the privilege or leaves the organization; the card shows each token's creator so it can be revoked by hand. Tokens belong to the organization that is active in the dashboard.
Create a token
- Enter a name of 1 to 100 characters, for example the dashboard or script that will use it.
- Pick an expiry: 30 days, 90 days, 1 year (365 days), or never. The form starts at 90 days.
- Select Create token. The token is shown once, starting with
etd_api_. Copy it before you select Done.
Only a hash of the token is stored, so it cannot be shown again. If a token is lost, revoke it and create another. An organization can hold at most 25 live tokens (tokens that are neither revoked nor expired).
Token list and revocation
The list shows each token's name, its last four characters, when it was created, when it was last used and its status. Last use is updated at most once every 5 minutes. Revoke stops the token on its next request; the row stays in the list as a record. Creating and revoking a token are recorded in the audit log. A revoked or expired token is answered with 401.
A token reads every agent's hostname, OS, last signed-in user, isolation state and alerts for its organization. Store it as you would a password, give each integration its own token, and set an expiry where the integration allows it.
Authentication and base URL
Send the token in the Authorization header on every request:
Authorization: Bearer etd_api_...
The base URL is your workspace address, https://<workspace>.etducky.com/api/v1, or https://etducky.com/api/v1. The token decides which organization is read, whichever host the request is sent to. On a self-hosted instance, use the instance's own address (see Self-hosted instances).
curl -H "Authorization: Bearer etd_api_..." \
"https://<workspace>.etducky.com/api/v1/agents?os=windows&online=false"
Responses are JSON with camelCase field names. Timestamps are UTC in ISO 8601 form with a Z suffix. An agent is online when it has sent a heartbeat in the last 5 minutes. Deleted agents are never returned.
Endpoints
| Method and path | Returns |
|---|---|
GET /api/v1/agents | The organization's agents, sorted by hostname. |
GET /api/v1/agents/{agentId} | One agent, its latest health sample and its number of active alerts. |
GET /api/v1/alerts | Alerts raised for the organization's agents, newest first. |
GET /api/v1/summary | Agent counts by state and OS, and active alerts by severity. |
GET /api/v1/agents
| Query parameter | Meaning |
|---|---|
search | Part of the hostname, case-insensitive. |
os | windows, linux or other. other covers agents with no recognised OS. Any other value is answered 400. |
online | true for agents with a heartbeat in the last 5 minutes, false for the rest. |
tag | Only agents carrying this exact tag name. |
limit | Most agents to return, 1 to 500. Defaults to 100. A value outside that range is clamped to it. |
The response is { "agents": [...], "total": n, "truncated": true|false }. total counts every agent that matches the filters, and truncated is true when limit cut the list short. Each agent has:
| Field | Meaning |
|---|---|
agentId | The agent's id. Use it with /agents/{agentId} and the agentId alert filter. |
hostname | The device's hostname. |
osVersion | The OS version text the agent reports. |
osFamily | windows, linux or other. |
agentVersion, agentType | The installed agent version and type. |
isActive | Whether the agent is active in the organization. |
online, lastHeartbeat | Online state and the time of the last heartbeat. |
lastLogonUser | The last signed-in user the agent reported, or null. |
isolationState | The network isolation state, normal when the device is not isolated. |
tags | The agent's tag names. |
GET /api/v1/agents/{agentId}
Returns { "agent": {...}, "latestHealth": {...}, "activeAlerts": n }. agent has the fields above. latestHealth is the newest health sample, or null when there is none, with timestamp, cpuUsagePercent, memoryUsagePercent, diskCFreePercent, uptimeSeconds, pendingReboot, systemManufacturer and systemModel. An id that is not an agent of the organization is answered 404 with {"error":"agent_not_found"}.
GET /api/v1/alerts
| Query parameter | Meaning |
|---|---|
status | active (the default), acknowledged, resolved, auto_resolved, suppressed, or any for no status filter. Any other value is answered 400. |
severity | Only alerts of this severity, for example critical or warning. |
agentId | Only alerts for this agent. |
sinceHours | How far back to look, by the time the alert was raised: 1 to 720 hours. Defaults to 168 (7 days). A value outside that range is clamped to it. |
limit | Most alerts to return, 1 to 500. Defaults to 100. |
The response is { "alerts": [...], "total": n, "truncated": true|false }. Each alert has alertId, ruleName, agentId, hostname, severity, status, triggeredAt, resolvedAt and acknowledgedAt.
GET /api/v1/summary
Returns agents, online, offline, windows, linux, other, isolated, activeAlerts, activeAlertsBySeverity (an object keyed by severity) and generatedAt.
Errors
| Status | Body | When |
|---|---|---|
| 400 | {"error":"invalid_filter","message":"..."} | An os or status value outside its list. The message names the accepted values. |
| 401 | {"error":"invalid_token"} | The token is missing, malformed, unknown, revoked or expired, or its organization is no longer active. The response carries WWW-Authenticate: Bearer error="invalid_token". |
| 404 | {"error":"agent_not_found"} | The agent id in /agents/{agentId} is not an agent of the token's organization. |
| 429 | JSON error body | A rate limit was reached. Wait for the number of seconds in Retry-After. |
Unexpected server errors are answered with an application/problem+json body that carries a correlationId and a traceId. Responses also carry the correlation id in the X-Correlation-ID header. Quote it when you contact support. See API Rate Limits for the error format.
Rate limits
Two limits apply to /api/v1 and /api/mcp together:
| Limit | Value | X-RateLimit-Policy |
|---|---|---|
| Per client address | 600 requests a minute, counted for every request whether or not its token is valid | public-api-per-ip |
| Per token | 120 requests a minute, counted after the token is accepted | public-api-per-token |
A request over either limit is answered 429 with Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining: 0 and X-RateLimit-Policy. Requests are not queued.
MCP endpoint
The MCP endpoint is https://<workspace>.etducky.com/api/mcp, or https://etducky.com/api/mcp. It authenticates with the same API token, sent as an Authorization: Bearer header.
- It uses the Streamable HTTP transport without sessions. Each
POSTcarries one JSON-RPC 2.0 message and is answered with one JSON response.GETis answered 405, because the server sends no stream. A JSON array (a batch) is answered 400. - It negotiates protocol version
2025-06-18by default and also accepts2025-11-25and2025-03-26. A request whoseMCP-Protocol-Versionheader names another version is answered 400. - A request that carries an
Originheader is refused with 403, so a web page in a browser cannot call the endpoint. - OAuth is not offered yet. The client must be able to send the token as a header.
Tools
All four tools are marked read-only. Each returns the same JSON as the matching REST endpoint, as text and as structured content.
| Tool | Arguments | Same data as |
|---|---|---|
list_agents | search, os, online, tag, limit | GET /api/v1/agents |
get_agent | agent_id (required) | GET /api/v1/agents/{agentId} |
list_alerts | status, severity, agent_id, since_hours, limit | GET /api/v1/alerts |
get_fleet_summary | none | GET /api/v1/summary |
Arguments take the same values and defaults as the query parameters. An os or status value outside its list is answered with JSON-RPC error -32602. An unknown agent id in get_agent returns a tool result marked as an error with agent_not_found.
Connecting an AI assistant
Any MCP client that supports the Streamable HTTP transport and lets you set a request header can connect. Replace <workspace> with your workspace name and etd_api_... with your token.
Claude Code
claude mcp add --transport http etducky https://<workspace>.etducky.com/api/mcp \
--header "Authorization: Bearer etd_api_..."
Add --scope user to make the server available in every project. To share the server through a project's .mcp.json without writing the token into the file, reference an environment variable:
{
"mcpServers": {
"etducky": {
"type": "http",
"url": "https://<workspace>.etducky.com/api/mcp",
"headers": {
"Authorization": "Bearer ${ETDUCKY_API_TOKEN}"
}
}
}
}
Cursor
Add the server to ~/.cursor/mcp.json for every project, or to .cursor/mcp.json in one project:
{
"mcpServers": {
"etducky": {
"url": "https://<workspace>.etducky.com/api/mcp",
"headers": {
"Authorization": "Bearer ${env:ETDUCKY_API_TOKEN}"
}
}
}
}
Clients that need OAuth
Clients that connect to remote servers only through OAuth, or that cannot send a custom header, cannot connect yet. This includes custom connectors added in Claude Desktop and on claude.ai.
Self-hosted instances
A Local-First instance serves the same API and MCP endpoint at its own address: https://<your-instance>/api/v1 and https://<your-instance>/api/mcp. Tokens are created in the instance's own dashboard, in the same place, and read the instance's data.
When the license grace period has ended and the instance is collection-only, both surfaces still answer reads and a token can still be revoked. Creating a token is refused until the subscription is renewed. See Self-Hosted Deployment.