MCP server
An MCP server is a small program that gives an AI agent a set of tools it can call — MCP, the Model Context Protocol, is the open standard those tools speak. The Kenly MCP server hands your agent tools for translating books, so you can drive a whole translation in plain English instead of writing code against the API yourself.
With it, you can run a full Kenly book translation from your own AI agent — Claude Code, Claude Desktop, Cursor, or any MCP client — with only an API key and no website visit. Point the agent at files on your own computer and say "translate these to English": it creates the project, uploads the local files, prices the job, tops up credit if needed, runs the fully-automatic translation, watches its status, and writes the finished book back to your disk.
The server is a thin wrapper over this same versioned /v1 API — every guard (owner-scoping, idempotency, affordability, the automatic batch ladder, the never-exceed charge cap) stays server-side. Anything you can do through the tools, you could do with raw HTTP; the connector just makes it one natural-language instruction to your agent.
Fully automatic — there is no approve/reject step
A run auto-advances every batch (1 → 5 → 25 → 100 → rest) with no human gate. The agent starts and monitors a run; it never approves individual pages — there is deliberately no approve/reject tool. The only thing that pauses a run is affordability, and that self-heals the moment you add credit.
Every tool runs as your user, scoped to your wallet and quotas. A leaked or shared key still can't exceed your wallet or touch another account's data — but treat it like a password all the same (see Security).
Prerequisites
- Node.js ≥ 18.17 — the connector runs straight from npm with
npx, no clone or build. - A Kenly API key (
kenly_sk_live_…) in theKENLY_API_KEYenvironment variable. If you don't have one yet, jump to Get an API key — it's a two-minute, one-time step.
The package is published to npm as @kenly/mcp-server and talks to the production API (https://api.getkenly.com) by default.
Install
Add the server to your MCP client with one of the copy-paste blocks below — replace kenly_sk_live_… with your own key. Every client launches the same stdio command (npx -y @kenly/mcp-server) with KENLY_API_KEY in its environment.
Claude Code
One command registers it:
bash
claude mcp add kenly --env KENLY_API_KEY=kenly_sk_live_… -- npx -y @kenly/mcp-serverThe --env FLAG is repeatable (add --env KENLY_API_BASE_URL=… to point at a non-production API), and the -- separates Claude Code's own flags from the server command. Scope it with --scope user (all your projects) or --scope project (checked into the repo's .mcp.json for your team); the default is the current project only. The project-file form is equivalent:
jsonc
// .mcp.json (project root)
{
"mcpServers": {
"kenly": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@kenly/mcp-server"],
"env": { "KENLY_API_KEY": "kenly_sk_live_…" }
}
}
}Confirm it loaded with claude mcp list — kenly should report its tools.
Claude Desktop
Edit the config via Settings → Developer → Edit Config, or open the file directly:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
json
{
"mcpServers": {
"kenly": {
"command": "npx",
"args": ["-y", "@kenly/mcp-server"],
"env": {
"KENLY_API_KEY": "kenly_sk_live_…"
}
}
}
}Restart Claude Desktop; kenly appears in the tools (🔌) menu.
Cursor
Create the config globally (all projects) at ~/.cursor/mcp.json, or per-repo at .cursor/mcp.json:
json
{
"mcpServers": {
"kenly": {
"command": "npx",
"args": ["-y", "@kenly/mcp-server"],
"env": {
"KENLY_API_KEY": "kenly_sk_live_…"
}
}
}
}Enable it under Cursor Settings → MCP.
Any other MCP client
Launch a stdio server the same way — command: "npx", args: ["-y", "@kenly/mcp-server"], and KENLY_API_KEY in the environment. The server speaks standard MCP over stdio, so any compliant client can host it.
Get an API key
The tools authenticate with a scoped, revocable kenly_sk_live_… key — never your password. Create one in the web app (there's a full walkthrough in Authentication):
- Sign in and open Settings → Developer — https://app.getkenly.com/settings/developer.
- Click Create key, give it a descriptive name (e.g.
claude-cliormy-laptop), and choose its scopes. The defaultprojects:writescope can do everything the connector exposes (create/upload/estimate/translate/export and wallet top-up); a read-onlyprojects:readkey can only inspect status, quotes, and exports. - Copy the
kenly_sk_live_…secret now — it's shown exactly once. Put it in the MCP client'senvblock above (or an environment variable); never commit it.
Prefer the command line? Mint a key over the API instead — see Authentication & API keys → Manage keys programmatically. Revoke any key instantly from the same Settings → Developer screen; mint one key per agent/machine so you can revoke just that one.
Configuration
Two environment variables (nothing else to configure):
| Variable | Required | Default | Meaning |
|---|---|---|---|
KENLY_API_KEY | ✅ | — | Your kenly_sk_live_… key. Never hardcode it; keep it out of source control. |
KENLY_API_BASE_URL | — | https://api.getkenly.com | The API origin. Override only for local dev, e.g. http://localhost:5080 (the Aspire-pinned API port). |
The server refuses to start without KENLY_API_KEY, printing an actionable message (never a stack trace) so a bad config fails loudly at launch rather than 401-ing on the first tool call. All diagnostics go to stderr; stdout is reserved for the MCP JSON-RPC stream.
Tool catalog
The connector exposes 15 tools, grouped by what they do. Retry-safety is server-side — the API deduplicates a double-submit of a billable or state-changing call itself, so an agent retry can't double-charge or double-enqueue (nothing to send). Errors surface the API's ProblemDetails message — a 402 tells you to top_up, a 409 explains the wrong state, a 403 names the missing scope.
Each tool below lists its arguments, what it returns, and an example of the natural-language ask that triggers it (you type these to your agent — the agent picks the tool).
Translate local files
create_project
Reads file(s) off local disk, uploads them, creates the project, and splits it into pages — ready to translate, no website visit.
| Argument | Type | Required | Meaning |
|---|---|---|---|
name | string | ✅ | A title for the book/project. |
files | string[] | ✅ | Local file path(s) on this machine (PDF/PNG/JPEG/TIFF/BMP). Several files are combined into one book. |
targetLanguageCode | string | — | Destination language ISO code (e.g. "en"). Omit for your account default. |
Returns { projectId, name, pagesTotal, targetLanguageCode, sourceFiles, merged } — the projectId is what every other tool takes.
One source document per project
A book is one source document in the Kenly model. Passing several files combines them into a single PDF client-side (each PDF's pages copied, each PNG/JPEG one page) before upload — so a folder of scans becomes one book. When combining, each file must be a PDF/PNG/JPEG (convert TIFF/BMP first).
Example — "Create a Kenly project called 'Gesner 1551' from
/scans/gesner.pdfand translate it to English."
Estimate before spending
get_estimate
A free whole-book cost/time projection (page/word analysis, no GPU spend).
| Argument | Type | Required | Meaning |
|---|---|---|---|
projectId | string | ✅ | The project id from create_project. |
Returns the whole-book estimate (projected pages, words, cost, and time).
Example — "Roughly how much will the whole book cost to translate?"
get_batch_quote
The next batch's expected "about $X" and never-exceed "$Y", plus whether your wallet covers it. Use it before start_translation and whenever a run pauses for credit.
| Argument | Type | Required | Meaning |
|---|---|---|---|
projectId | string | ✅ | The project id. |
Returns the batch quote — { nextBatchSize, expectedUsd, estimatedUsd, balanceUsd, affordable, unlimited, … }.
Example — "What will the next batch cost, and can I afford it?"
Billing self-service
get_balance
Your current prepaid credit balance (or "included" for an unlimited account).
| Argument | Type | Required | Meaning |
|---|---|---|---|
| (none) |
Returns the wallet view — { balanceUsd, unlimited, lowBalance, currency }.
Example — "What's my Kenly balance?"
top_up
Add prepaid credit. The default manual method credits immediately; a card/crypto method returns a checkout link to open in a browser. After credit lands, a paused run auto-resumes.
| Argument | Type | Required | Meaning |
|---|---|---|---|
amountUsd | number | ✅ | Amount to add, in USD. |
method | manual | card | crypto | — | Payment method (default manual, credits immediately). |
Returns the top-up view — { reference, status, method, amountUsd, balanceUsd, checkoutUrl, … } (checkoutUrl is set for card/crypto).
Example — "Add $10 of credit to my wallet."
redeem_code
Redeem an operator-issued credit code — a browser-free way to add credit so a paused run resumes.
| Argument | Type | Required | Meaning |
|---|---|---|---|
code | string | ✅ | The credit code to redeem. |
Returns the top-up view with the credited amount and new balance.
Example — "Redeem the code
KENLY-XXXX-YYYY."
get_transactions
Your top-ups and processing charges, most recent first, with lifetime totals.
| Argument | Type | Required | Meaning |
|---|---|---|---|
skip | number | — | How many to skip (paging). |
take | number | — | How many to return (default 20, max 100). |
Returns the transaction history with lifetime totals.
Example — "Show my last 10 Kenly transactions."
Run (automatic)
start_translation
Kicks off the fully-automatic ladder (1 → 5 → 25 → 100 → rest). Each batch auto-advances with no human approval; the only pause is affordability. On a credit pause, call top_up and it auto-resumes.
| Argument | Type | Required | Meaning |
|---|---|---|---|
projectId | string | ✅ | The project id. |
Returns the job handle { jobId, status } — monitor it with get_status / wait_for_status.
Example — "Start translating the project."
Monitor
get_status
One described status for a book: where it is, whether it's waiting (self-heals — just poll) or blocked (needs you, e.g. add credit), progress, the active job, and the next-batch quote.
| Argument | Type | Required | Meaning |
|---|---|---|---|
projectId | string | ✅ | The project id. |
Returns the status view — state, isWaiting/isBlocked flags, progress { pagesTranslated, pagesTotal }, active job, and quote.
Example — "How's the translation going?"
wait_for_status
Blocks until the book's status meaningfully changes, becomes blocked (needs you), or reaches a terminal state — then returns the new status. The efficient "tell me when it changes" alternative to polling get_status yourself.
| Argument | Type | Required | Meaning |
|---|---|---|---|
projectId | string | ✅ | The project id. |
timeoutSeconds | number | — | Max seconds to wait before returning the latest status (default 120, max 600). |
pollSeconds | number | — | Poll interval in seconds (default 5, 2–60). |
waitForComplete | boolean | — | If true, keep waiting through intermediate changes until the run completes or blocks. |
Returns { reason, status } where reason is changed | blocked | terminal | timeout.
Example — "Wait until the book finishes (or needs me), then tell me."
Recover
retry_failed
Re-process only a project's failed pages. Returns a job handle to monitor.
| Argument | Type | Required | Meaning |
|---|---|---|---|
projectId | string | ✅ | The project id. |
Returns the job handle { jobId, status }.
Example — "Retry the pages that failed."
Results
list_exports
The export files already generated for a project, re-downloadable with download_result.
| Argument | Type | Required | Meaning |
|---|---|---|---|
projectId | string | ✅ | The project id. |
Returns the list of export files (name, size, content type, download name).
Example — "What exports are available for this book?"
download_result
Generate (if needed) and write the finished book to local disk.
| Argument | Type | Required | Meaning |
|---|---|---|---|
projectId | string | ✅ | The project id. |
format | string | ✅ | Markdown, PlainText, Html, Epub, Docx, Pdf, or Archive (aliases like md/txt/pdf/word/zip accepted). |
outputPath | string | — | Local file path or directory to save into. Defaults to ./<generated-name> in the current directory. |
matchSourcePages | boolean | — | PDF only: one output sheet per source page (page-for-page layout). |
Returns { path, bytes, contentType, format } — and the file is written to path on your disk.
Example — "Download the finished book as EPUB to
~/books/gesner.epub."
Convenience
list_projects
Your projects (paged, newest first) — to find a projectId across sessions.
| Argument | Type | Required | Meaning |
|---|---|---|---|
search | string | — | Filter by name/text. |
status | string | — | Filter by lifecycle status. |
pageSize | number | — | How many to return (default 25, max 100). |
Returns a paged list of your projects.
Example — "List my Kenly projects."
get_meta
API version, the batch ladder, export formats, and supported destination languages.
| Argument | Type | Required | Meaning |
|---|---|---|---|
| (none) |
Returns the capability document — { apiVersion, batchLadder, finalBatch, exportFormats, … }.
Example — "What languages and export formats does Kenly support?"
A full self-service run
The whole point: translate a PDF on your disk from the Claude CLI without opening the website. You give your agent one instruction — "Translate /scans/gesner.pdf to English and save it as EPUB" — and it walks the tools end to end. Under the hood that's:
text
create_project { name:"Gesner 1551", files:["/scans/gesner.pdf"] }
-> { projectId, pagesTotal } # local file uploaded + split into pages
get_batch_quote { projectId }
-> about $X, never more than $Y, affordable? # pre-flight the cost
top_up { amountUsd: 10 } # only if the quote says NOT affordable
-> balance topped up (manual = immediate)
start_translation { projectId }
-> { jobId } # ladder auto-advances 1 -> 5 -> 25 -> 100 -> rest
wait_for_status { projectId, waitForComplete: true }
-> on "blocked" (needs credit): top_up, then wait_for_status again # auto-resumes
-> on "terminal": done
download_result { projectId, format:"epub", outputPath:"/books/gesner.epub" }
-> saved to diskNo website was opened, no page was manually approved, and everything ran against a file on your own machine. The run charges your wallet as each batch settles, never exceeding the quoted ceiling, and pauses cleanly (rather than failing) if a batch would outrun your balance.
Security
- Treat the key like a password. It authenticates every call as you and can spend from your wallet. Put it in your MCP client's
envblock (or a.envyou never commit) — never hardcode it in source, and never paste it into a shared/project config that lands in git. - Scope down. Use a
projects:readkey for agents that should only inspect status/quotes/exports; reserveprojects:write(create/translate/spend) for agents you trust to run and pay for jobs. - Rotate & revoke freely. Keys are revocable in the web app (Settings → Developer); mint one key per agent/machine so you can revoke just that one. Revocation is immediate.
- Least privilege by design. Every guard — owner-scoping, affordability, the never-exceed charge cap, idempotency — is enforced server-side. A leaked key still cannot exceed your wallet or touch another user's data, but you should still revoke it.
How it relates to the HTTP API
The MCP server is a convenience layer, not a separate product: each tool maps to one or more /v1 operations. If you'd rather integrate over raw HTTP (a backend service, a non-MCP agent), the Quickstart and the API reference cover the same flow — create_project ≈ POST /v1/projects, start_translation ≈ POST /v1/projects/{id}/process, get_status ≈ GET /v1/projects/{id}/status, and so on. The tools and the API stay in lockstep because both speak the same versioned /v1 contract.