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, a fleet summary and the context of one device for a ticket. - 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.
- Optionally, enter Allowed addresses: up to 20 IPv4 or IPv6 addresses or CIDR ranges, separated by commas, for example
203.0.113.10, 198.51.100.0/24. Leave the field empty for a token that works from any address. - Select Create token. The token is shown once, starting with
etd_api_. Copy it before you select Done.
A range is written by its network address (198.51.100.0/24, not 198.51.100.5/24). IPv4 addresses are written as four decimal numbers. A /0 range is refused; leave the field empty instead. An entry that does not follow these rules is refused with a message naming it, and no token is created. The addresses cannot be changed after the token is created; create a new token instead.
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, its allowed addresses (or Any), 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.
Allowed addresses
A token with allowed addresses is accepted only from a client address inside one of them. A request from any other address is answered 401 with {"error":"invalid_token"}, as an unknown token is. The address checked is the one that connects to ET Ducky, so for a PSA or automation server it is that server's public address. Give a token allowed addresses when the program that uses it runs on servers with fixed addresses, such as a PSA. A token for an AI assistant on a laptop usually needs to work from any address.
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, set an expiry where the integration allows it, and set allowed addresses where the integration runs from fixed addresses.
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/device-context | The agents matching one device name or agent id, each with its latest health sample, its active alerts and dashboard links. |
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/device-context
Returns everything about one device in one call, for a PSA ticket or a script that knows the device by name.
| Query parameter | Meaning |
|---|---|
hostname | The device name, as the PSA or ticket holds it. Matching ignores case. |
agentId | An agent id. When given, hostname is ignored. |
One of the two is required. A request with neither, or with a value longer than 255 characters or containing control characters, is answered 400 with {"error":"invalid_filter","message":"..."}.
A hostname is matched in two steps. First, agents whose hostname is the whole name. If none, the short name: the part before the first dot, cut to 15 characters. Agents whose hostname is the short name, or whose hostname starts with the short name followed by a dot, match. Windows agents report the computer name, which is at most 15 characters, so financeworkstation07.corp.example.com in a PSA finds the Windows agent FINANCEWORKSTAT. Two computers whose names share the first 15 characters both report the same short name, so a short_name match can be a different computer; check the agent's details before acting on it.
The response is { "matchedBy": "...", "devices": [...], "truncated": true|false }. matchedBy is agent_id, hostname, short_name, or none when no agent matched; no match is answered 200 with an empty devices list. At most 5 devices are returned, sorted by hostname, and truncated is true when more matched. Each device has:
| Field | Meaning |
|---|---|
agent | The agent, with the fields GET /api/v1/agents returns. |
latestHealth | The newest health sample, as in GET /api/v1/agents/{agentId}, or null. |
activeAlertCount | The number of active alerts for the agent. |
activeAlerts | The 10 newest active alerts, with the fields GET /api/v1/alerts returns. |
links | open, liveSession and remoteControl: dashboard addresses for this device. See Opening a device from a link. |
curl -H "Authorization: Bearer etd_api_..." \
"https://<workspace>.etducky.com/api/v1/device-context?hostname=app01.corp.example.com"
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, or a device-context request with no hostname and no agentId. The message says what is accepted. |
| 401 | {"error":"invalid_token"} | The token is missing, malformed, unknown, revoked or expired, its organization is no longer active, or the request came from an address outside the token's allowed addresses. 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. Calling a tool needs the same API token, sent as an Authorization: Bearer header.
- A
POSTwith noAuthorizationheader can callinitialize,pingandtools/listand send notifications, so MCP directories and registries can read the tool list and check that the server answers. These answers carry no organization data. AGETwith noAuthorizationheader is answered 405, as it is with a token.tools/callwithout a token is answered 401 withWWW-Authenticate: Bearerand a JSON-RPC error. A request that carries anAuthorizationheader is checked as a token for every method, so an unknown, revoked or expired token is answered 401. - 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 five 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 |
get_device_context | hostname or agent_id | GET /api/v1/device-context |
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. get_device_context with neither argument is answered with JSON-RPC error -32602.
The server's initialize answer tells the assistant that hostnames, user names, OS text and alert rule names come from managed devices and from the organization's settings and are data, not instructions. No tool changes anything. A link a tool returns opens the dashboard, where a person signs in and confirms before a session starts.
Opening a device from a link
The links in a device-context answer open the dashboard of the workspace the token's organization belongs to, https://<workspace>.etducky.com, whether the request was sent to the workspace address or to etducky.com. On a self-hosted instance they open the address the request was sent to. A PSA can show them on a ticket, and an AI assistant can hand them to a technician.
- open asks which session to start. liveSession and remoteControl ask the same question with that choice highlighted.
- A link carries the agent id and the organization's id, and no credential. The person who opens it signs in to the dashboard as usual, and the dashboard shows a dialog naming the device. Nothing starts until they select Live Session or Remote control in that dialog. The buttons are unavailable while the device is offline.
- When the device belongs to an organization that is not active in the dashboard, the dialog offers Switch organization. Switching works only for a member of that organization, and the dialog then opens again.
- A link is remembered in the browser tab for 10 minutes, so it still opens after a sign-in. It is used once.
- The dialog starts a session the same way the Live Session and Remote buttons in the Agents list do, so the same permissions apply.
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.