MCP server
DetectifAI runs a Model Context Protocol server, so MCP clients such as Claude Code can check your balance, detect synthetic speech and enroll, verify and manage speakers.
| Endpoint | https://platform.detectif.ai/mcp |
| Transport | Streamable HTTP, JSON responses. Protocol revision 2026-07-28, and the earlier revisions 2024-11-05 to 2025-11-25 in stateless mode (no sessions). |
| Authentication | Authorization: Bearer dtf_live_... (an API key). OAuth is not offered. |
| Billing | The same credits, prices and rules as the REST API. |
The MCP server calls the same code as the REST API: the same authentication, scopes, rate limits, audio checks, models, billing and idempotency. Operations started over MCP appear in your usage and in GET /v1/operations/{operation_id} with "source": "mcp".
Tools
| Tool | What it does | Scope | Credits |
|---|---|---|---|
get_credit_balance |
Available and reserved credits. | credits:read |
free |
detect_deepfake(audio_base64, filename, idempotency_key) |
Scores a recording for synthetic speech. | deepfake:detect |
50 |
enroll_speaker(speaker_id, audio_base64, filename, display_name?, idempotency_key) |
Adds one enrollment sample; creates the speaker on first use. | speakers:enroll |
50 |
verify_speaker(speaker_id, audio_base64, filename, detect_deepfake=false, idempotency_key) |
Compares a recording with an enrolled speaker. | speakers:verify (+ deepfake:detect) |
50, or 100 with detect_deepfake=true |
list_speakers(limit?, cursor?) |
Enrolled speakers, newest first. | speakers:read |
free |
delete_speaker(speaker_id) |
Deletes a speaker and its stored voice embeddings. | speakers:delete |
free |
get_operation(operation_id) |
Status and stored result of an operation. | scope of the operation | free |
Results are the same JSON objects the REST API returns, in the tool result's structuredContent and as JSON text in content. Read Deepfake detection and Speaker enrollment and verification for what the scores mean: a deepfake score is not proof of fraud, the default deepfake threshold was fitted on English speech, cosine similarity is not a probability, and verification is less reliable on phone-call audio.
Failed calls are not charged, exactly as on the REST API. idempotency_key is optional in the schema and recommended on every billable call; see Idempotency over MCP.
Create a key for MCP
Create a dedicated API key in the dashboard with only the scopes your MCP client needs, for example credits:read, deepfake:detect and speakers:read. An assistant connected with this key can spend credits on your behalf: every detect_deepfake, enroll_speaker and verify_speaker call costs credits. Most MCP clients ask for your approval before each tool call; keep that on for these tools.
export DETECTIF_API_KEY="dtf_live_..."Audio input
Audio is sent as base64 text in audio_base64: the file's bytes, base64-encoded (standard or URL-safe alphabet). The server does not fetch URLs and does not read file paths.
- Decoded size: up to 20 MB, the same as a REST upload. The request body can be up to about 29 MB.
- Formats and durations: the same as the REST API (WAV, MP3, FLAC, OGG/Opus, WebM, M4A/AAC or AIFF; see limits).
filenameis informational.
When an AI assistant calls a tool, the base64 text passes through the model as part of the tool call. That is practical only for short, compressed clips: one second of 16 kHz 16-bit WAV is about 43,000 base64 characters. For longer recordings, call the REST API or use an MCP client from your own code, where the audio never passes through a model.
Idempotency over MCP
Idempotency over MCP works exactly as on the REST API. The billable tools (detect_deepfake, enroll_speaker, verify_speaker) take an optional idempotency_key argument with the same rules as the REST Idempotency-Key header, and a key is used only when you pass one. The server never creates a key for you.
Pass an idempotency_key on every billable call. Generate a new UUID for each call, and reuse that same value if you retry it, for example after a timeout, a dropped connection, or a 429 or 503 error:
- A retry with the same key returns the stored result with
"idempotent_replay": trueand"credits": {"charged": 0, "balance": <current balance>, "original_charge": 50}(original_chargeis 100 for a verification withdetect_deepfake=true). The model does not run again. - A retry without the original key is a new operation and is charged again. For
enroll_speaker, it also adds a second sample. - A key that is still running returns
operation_in_progresswith anoperation_id; callget_operationinstead of sending the audio again. - Reusing a key with different audio or arguments returns
idempotency_key_reuse.
The key namespace is shared with the REST API: within your account, one key for one operation type refers to one operation whichever way it was sent. Keys are matched for 24 hours.
If you call the tools from your own code, generate the key there. If an AI assistant calls them, the tool descriptions ask it to pass a UUID and reuse it on retry; check the tool calls your client shows you.
Errors
Tool errors. When a tool call fails, the result has "isError": true. Its structuredContent is the REST error envelope, with the same error.code:
{
"error": {
"type": "not_found",
"code": "speaker_not_found",
"message": "No speaker with this speaker_id exists in your account.",
"param": "speaker_id"
},
"request_id": "req_3b9e0c5d1a7f42e8b6c0d9a1"
}The text content repeats the code, message and request_id. Rate limit and overload errors add retry_after_seconds. 409 operation_in_progress adds operation_id: call get_operation with it instead of sending the audio again. In MCP errors, param names the tool argument (audio_base64, idempotency_key, speaker_id). MCP-only codes:
error.code |
Meaning |
|---|---|
invalid_audio_base64 |
audio_base64 is empty, is not valid base64, or is a URL, data URL or path. |
invalid_filename |
filename is longer than 255 characters or contains control characters. |
Arguments that do not match a tool's input schema (for example a missing speaker_id) are reported by the MCP layer as a tool error with a validation message and no error.code.
HTTP errors. Problems found before a tool runs are returned as HTTP errors with the REST error envelope as the body, as on /v1:
| HTTP | error.code |
Meaning |
|---|---|---|
| 401 | missing_api_key, invalid_api_key, revoked_api_key |
No key, a malformed or unknown key, or a dashboard session token instead of an API key. |
| 400 | api_key_in_url |
A key was sent in the URL. |
| 403 | origin_not_allowed |
A browser Origin header that is not allowed. |
| 429 | rate_limited |
The per-key request limit (each MCP message is one request). |
| 413 | request_too_large |
The request body is larger than about 29 MB. |
| 503 | server_busy |
Too many large MCP requests are being received at once. Retry after Retry-After seconds. |
Set up Claude Code
Add the server with the Claude Code CLI. Options go before the server name; --header goes after the URL:
claude mcp add --transport http detectif https://platform.detectif.ai/mcp \
--header "Authorization: Bearer $DETECTIF_API_KEY"Your shell expands $DETECTIF_API_KEY, so the key itself is stored in your local Claude Code configuration (~/.claude.json, local scope). Check the connection with claude mcp list, or /mcp inside Claude Code.
To share the server with a project without writing the key into a file, add a project-scoped .mcp.json that reads the key from the environment of each person who uses it:
{
"mcpServers": {
"detectif": {
"type": "http",
"url": "https://platform.detectif.ai/mcp",
"headers": { "Authorization": "Bearer ${DETECTIF_API_KEY}" }
}
}
}Set up other clients
This server authenticates with a header, not OAuth. Clients that can send custom headers to a Streamable HTTP server connect directly. Example configurations:
VS Code (.vscode/mcp.json; VS Code prompts for the key and stores it securely):
{
"inputs": [
{ "type": "promptString", "id": "detectif-api-key", "description": "DetectifAI API key", "password": true }
],
"servers": {
"detectif": {
"type": "http",
"url": "https://platform.detectif.ai/mcp",
"headers": { "Authorization": "Bearer ${input:detectif-api-key}" }
}
}
}Cursor (~/.cursor/mcp.json or .cursor/mcp.json):
{
"mcpServers": {
"detectif": {
"url": "https://platform.detectif.ai/mcp",
"headers": { "Authorization": "Bearer ${env:DETECTIF_API_KEY}" }
}
}
}Claude Desktop and clients without custom headers. Use a local bridge that adds the header, such as the open-source mcp-remote package (requires Node.js). In claude_desktop_config.json:
{
"mcpServers": {
"detectif": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://platform.detectif.ai/mcp", "--transport", "http-only", "--header", "Authorization:${DETECTIF_AUTH_HEADER}"],
"env": { "DETECTIF_AUTH_HEADER": "Bearer dtf_live_..." }
}
}
}The header value is passed through env without a space after the colon in args, because some clients on Windows do not quote arguments that contain spaces. Restart the client after editing its configuration. This file holds the key in plain text; protect it like any other credential.
Use the server from code
Every example below lists the tools, reads the balance and runs one detection.
# Protocol revision 2026-07-28: each request is one POST with its metadata in
# headers and in params._meta. Runs in bash or zsh.
MCP_URL=https://platform.detectif.ai/mcp
MCP_HEADERS=(
-H "Authorization: Bearer $DETECTIF_API_KEY"
-H "Content-Type: application/json"
-H "Accept: application/json, text/event-stream"
-H "MCP-Protocol-Version: 2026-07-28"
)
META='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{}}'
# List the tools
curl -s -w '\n' "$MCP_URL" "${MCP_HEADERS[@]}" -H "Mcp-Method: tools/list" \
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{$META}}"
# Read the balance (free)
curl -s -w '\n' "$MCP_URL" "${MCP_HEADERS[@]}" -H "Mcp-Method: tools/call" -H "Mcp-Name: get_credit_balance" \
-d "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"get_credit_balance\",\"arguments\":{},$META}}"
# Detect synthetic speech (50 credits). Build the body in a file: base64 audio can be large.
# Keep KEY and request.json to retry: a retry with the same idempotency_key is not charged again.
KEY=$(uuidgen)
{
printf '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"detect_deepfake","arguments":{"idempotency_key":"%s","filename":"sample.wav","audio_base64":"' "$KEY"
base64 < sample.wav | tr -d '\n'
printf '"},%s}}' "$META"
} > request.json
curl -s -w '\n' "$MCP_URL" "${MCP_HEADERS[@]}" -H "Mcp-Method: tools/call" -H "Mcp-Name: detect_deepfake" \
--data-binary @request.json# pip install "mcp>=2.3,<3"
import asyncio
import base64
import os
import uuid
import httpx2
from mcp import Client
from mcp.client.streamable_http import streamable_http_client
MCP_URL = "https://platform.detectif.ai/mcp"
async def main() -> None:
async with httpx2.AsyncClient(
headers={"Authorization": f"Bearer {os.environ['DETECTIF_API_KEY']}"},
timeout=httpx2.Timeout(30.0, read=180.0),
) as http:
async with Client(streamable_http_client(MCP_URL, http_client=http)) as client:
tools = await client.list_tools()
print([tool.name for tool in tools.tools])
balance = await client.call_tool("get_credit_balance", {})
print(balance.structured_content["available"])
with open("sample.wav", "rb") as f:
audio = base64.b64encode(f.read()).decode("ascii")
# Reuse the same idempotency_key if you retry this call, so it is not charged twice.
args = {"audio_base64": audio, "filename": "sample.wav", "idempotency_key": str(uuid.uuid4())}
result = await client.call_tool("detect_deepfake", args)
if result.is_error:
err = result.structured_content["error"]
print("error:", err["code"], err["message"], result.structured_content["request_id"])
else:
body = result.structured_content
print(body["result"]["label"], body["result"]["score"], body["credits"])
asyncio.run(main())import { readFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";
const MCP_URL = "https://platform.detectif.ai/mcp";
const PROTOCOL = "2026-07-28";
let nextId = 1;
async function mcp(method, params = {}) {
const headers = {
Authorization: `Bearer ${process.env.DETECTIF_API_KEY}`,
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
"MCP-Protocol-Version": PROTOCOL,
"Mcp-Method": method,
};
if (params.name) headers["Mcp-Name"] = params.name;
const _meta = {
"io.modelcontextprotocol/protocolVersion": PROTOCOL,
"io.modelcontextprotocol/clientInfo": { name: "example", version: "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": {},
};
const res = await fetch(MCP_URL, {
method: "POST",
headers,
body: JSON.stringify({ jsonrpc: "2.0", id: nextId++, method, params: { ...params, _meta } }),
});
const body = await res.json();
if (!body.jsonrpc) throw new Error(`${res.status} ${body.error.code}: ${body.error.message} (${body.request_id})`);
if (body.error) throw new Error(`JSON-RPC ${body.error.code}: ${body.error.message}`);
return body.result;
}
const { tools } = await mcp("tools/list");
console.log(tools.map((t) => t.name));
const balance = await mcp("tools/call", { name: "get_credit_balance", arguments: {} });
console.log(balance.structuredContent.available);
const audio = (await readFile("sample.wav")).toString("base64");
const result = await mcp("tools/call", {
name: "detect_deepfake",
// Reuse the same idempotency_key if you retry this call, so it is not charged twice.
arguments: { audio_base64: audio, filename: "sample.wav", idempotency_key: randomUUID() },
});
if (result.isError) console.log("error:", result.structuredContent.error.code, result.structuredContent.request_id);
else console.log(result.structuredContent.result.label, result.structuredContent.result.score);