@athanlab/mcp 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AthanLab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,223 @@
1
- # Temporary Holding Version
1
+ # @athanlab/mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An [MCP](https://modelcontextprotocol.io) server for the [AthanLab Developer API](https://athanlab.com/docs): natural Myanmar (Burmese) text-to-speech for AI agents in Claude Code, Claude Desktop, Cursor and any other MCP client that runs local (stdio) servers.
4
+
5
+ It wraps the API's asynchronous speech jobs so an agent can say "read this aloud" and get a file back. It creates the job, waits for it, downloads the audio into a local folder and returns the path. For dialogue and video work it turns a whole script into one file per line, a `manifest.json` timeline and, optionally, a single combined WAV.
6
+
7
+ > **Not published yet.** `@athanlab/mcp` is not on npm (`package.json` is `private`), so `npx -y @athanlab/mcp` fails with a 404 for now. Until it is published, run it from a local build (see [Before the package is published](#before-the-package-is-published)). Every `npx -y @athanlab/mcp` below assumes the published package.
8
+
9
+ ## Contents
10
+
11
+ - [Tools](#tools)
12
+ - [What you need](#what-you-need)
13
+ - [Create a key for an agent](#create-a-key-for-an-agent)
14
+ - [Install](#install): [Claude Code](#claude-code) · [Claude Desktop](#claude-desktop) · [Cursor](#cursor) · [the skill on its own](#the-skill-on-its-own)
15
+ - [Configuration](#configuration)
16
+ - [Output files](#output-files)
17
+ - [How it talks to the API](#how-it-talks-to-the-api)
18
+ - [Safety](#safety)
19
+ - [Troubleshooting](#troubleshooting)
20
+ - [References](#references)
21
+
22
+ ## Tools
23
+
24
+ Each tool returns a short text summary plus `structuredContent` with the full result.
25
+
26
+ | Tool | What it does | API calls | Key permission | Spends characters? |
27
+ |---|---|---|---|---|
28
+ | `athanlab_list_voices` | Voices the key can use: `id`, `name`, `category`, `source` (`athanlab` official or `user` your library), `is_default`, plus `default_voice_id` | `GET /voices` | `voices:read` | No |
29
+ | `athanlab_preview_voice` | A signed sample URL for a voice (valid about an hour); `save: true` also downloads it into the output folder | `GET /voices/{id}/preview` | `voices:read` | No |
30
+ | `athanlab_quote` | Dry run of a text: `characters`, `dispatches` (pieces), `spendable`, `sufficient` | `POST /speech` with `dry_run` | `speech:write` | No |
31
+ | `athanlab_speak` | One text → one audio file: creates the job, polls, downloads; returns `path`, `job_id`, `duration_seconds`, `characters_charged`, `output_format`, and the speech-timing fields once the API reports them | `POST /speech`, `GET /speech/{id}`, `GET /speech/{id}/audio` | `speech:write`, `speech:read` | Yes: the text's length |
32
+ | `athanlab_speak_script` | A script of lines (each with a speaker and optional voice) → one file per line + `manifest.json` (+ `combined.wav`); quotes first, asks for `confirm: true` above 2,000 characters or 20 lines, resumable | the above, plus `GET /usage` | `speech:write`, `speech:read`; with `usage:read` it runs several lines at once (without it, one at a time) | Yes: the sum of the lines |
33
+ | `athanlab_get_job` | One job's status, progress, cost and audio details | `GET /speech/{id}` | `speech:read` | No |
34
+ | `athanlab_list_jobs` | A page of jobs, newest first, optionally filtered by `status` | `GET /speech` | `speech:read` | No |
35
+ | `athanlab_cancel_job` | Stops a job that is still processing; the unprocessed share is refunded | `POST /speech/{id}/cancel` | `speech:write` | Refunds |
36
+ | `athanlab_download_audio` | Saves a finished job's audio (as `mp3` or `wav`) into the output folder | `GET /speech/{id}/audio` | `speech:read` | No (paid at creation) |
37
+ | `athanlab_usage` | Plan, entitlement, monthly allowance, Athan Tokens, `spendable`, this key's monthly budget, `active_jobs` and `max_concurrent_jobs` | `GET /usage` | `usage:read` | No |
38
+
39
+ It also serves **`athanlab-writing-guide`**, both as an MCP prompt and as the resource `athanlab://guides/writing`. It carries the rules for writing Burmese text that reads well: sentence punctuation (`။ ! ?`), Burmese script for every word, marks the voice does not read, `number_mode` with an example for each mode, and how to split scripts.
40
+
41
+ **Features the API does not have yet** (speaking `speed`, pronunciation `replacements`, `POST /speech/batch` and speech timing) are detected from the API's `openapi.json` when the server starts. Their tool parameters appear only once the API supports them. Until then the timing fields (`speech_start_seconds`, `speech_end_seconds`, `segments`) are `null`.
42
+
43
+ ## What you need
44
+
45
+ - **Node.js 20 or later** on the machine running the MCP client (`node --version`).
46
+ - **AthanLab API access**: an approved developer application on the [Developer API page](https://athanlab.com/dashboard/api), and the **Max plan** (or access granted by AthanLab). Without them, keys are read-only and creating audio answers `402 plan_required`. Access is invite-only for now; see [/docs](https://athanlab.com/docs#access).
47
+ - **An API key** (`ak_live_` followed by 32 hex characters), ideally one made for the agent, as below.
48
+
49
+ ## Create a key for an agent
50
+
51
+ Give every agent or machine its own key, so you can cap it, see what it spends and revoke it without touching your servers.
52
+
53
+ 1. Open [athanlab.com/dashboard/api](https://athanlab.com/dashboard/api), tab **Keys**, and choose **Create key**.
54
+ 2. **Key name**: say where it runs, for example `Claude Code – laptop`.
55
+ 3. **Permissions**: tick only what the agent needs.
56
+ - All four (`speech:write`, `speech:read`, `voices:read`, `usage:read`) for every tool. Scripts need only `speech:write` and `speech:read`; `usage:read` lets them run several lines at once.
57
+ - Without `speech:write` the agent can list voices, read jobs, download audio and check usage, but cannot create (or cancel) jobs. That suits a read-only helper.
58
+ 4. **Expires after**: **30 days** for an agent key. Make a new one when it runs out.
59
+ 5. **Monthly character budget**: set a low one, for example 20,000. Past it, creating a job answers `key_budget_exceeded` and nothing is charged, so a looping agent cannot drain your balance.
60
+ 6. Copy the key when it is shown. It is shown **once**. Put it in your environment or the client config (below), never in a chat, a repository or a shared document.
61
+
62
+ ## Install
63
+
64
+ ### Claude Code
65
+
66
+ ```bash
67
+ claude mcp add athanlab -e ATHANLAB_API_KEY=ak_live_your_key_here -- npx -y @athanlab/mcp
68
+ ```
69
+
70
+ - The options go before `--`; everything after `--` is the server command. `-e` can be repeated, for example `-e ATHANLAB_OUTPUT_DIR=/Users/you/athanlab-audio`.
71
+ - The default scope is **local**: this project only, stored in `~/.claude.json`. Add `--scope user` to use it in every project. Audio still goes to `athanlab-audio` in the project you are working in, because Claude Code tells the server its project folder (`CLAUDE_PROJECT_DIR`). If you set `ATHANLAB_OUTPUT_DIR` to one absolute folder instead, every project shares it: give each project's scripts their own `out_subdir` names, or one project's re-run overwrites another's lines.
72
+ - `-e` stores the key **in plain text** in `~/.claude.json`. Keep it out of your shell history: with `setopt HIST_IGNORE_SPACE` (zsh) or `HISTCONTROL=ignorespace` (bash), a command typed with a leading space is not saved. Never use `--scope project` with a literal key, because that writes the committed `.mcp.json`.
73
+ - To share the setup with a team without sharing a key, commit a project `.mcp.json` that reads the key from each person's environment. Claude Code expands `${VAR}` and `${VAR:-default}` there:
74
+
75
+ ```json
76
+ {
77
+ "mcpServers": {
78
+ "athanlab": {
79
+ "command": "npx",
80
+ "args": ["-y", "@athanlab/mcp"],
81
+ "env": {
82
+ "ATHANLAB_API_KEY": "${ATHANLAB_API_KEY}",
83
+ "ATHANLAB_OUTPUT_DIR": "${ATHANLAB_OUTPUT_DIR:-./athanlab-audio}"
84
+ }
85
+ }
86
+ }
87
+ }
88
+ ```
89
+
90
+ Check it with `claude mcp list`, or `/mcp` inside a session. In Claude Code, a tool call that runs longer than about two minutes moves to the background on its own, so a long script does not block the conversation.
91
+
92
+ ### Claude Desktop
93
+
94
+ Open **Settings → Developer → Edit Config**. The file is `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS and `%APPDATA%\Claude\claude_desktop_config.json` on Windows. Add:
95
+
96
+ ```json
97
+ {
98
+ "mcpServers": {
99
+ "athanlab": {
100
+ "command": "npx",
101
+ "args": ["-y", "@athanlab/mcp"],
102
+ "env": {
103
+ "ATHANLAB_API_KEY": "ak_live_your_key_here",
104
+ "ATHANLAB_OUTPUT_DIR": "/Users/you/Documents/athanlab-audio"
105
+ }
106
+ }
107
+ }
108
+ }
109
+ ```
110
+
111
+ Then quit Claude Desktop completely and start it again.
112
+
113
+ - Claude Desktop does not read your shell profile, so the key goes in this file, in plain text. Keep the file private and use a budget-capped agent key.
114
+ - Set `ATHANLAB_OUTPUT_DIR` to an **absolute** path. The server's working directory under Claude Desktop is not a project folder.
115
+ - If the server does not start, Claude Desktop probably cannot find `npx`. Apps opened from the Dock get a short `PATH`, so use the full path that `which npx` prints as `command`. Logs: `~/Library/Logs/Claude/mcp-server-athanlab.log` (macOS) or `%APPDATA%\Claude\logs` (Windows).
116
+ - Claude Desktop may stop waiting for a long tool call sooner than this server does. Ask Claude to pass a shorter `timeout_seconds` (for example 120), so the tool returns the `job_id` itself. If `athanlab_speak` or a script ends without its files, the jobs keep running: finish them with `athanlab_get_job` and `athanlab_download_audio`, or by calling the same script again, rather than starting them again.
117
+
118
+ ### Cursor
119
+
120
+ Global config: `~/.cursor/mcp.json`. For a single project, use `.cursor/mcp.json` in that project.
121
+
122
+ ```json
123
+ {
124
+ "mcpServers": {
125
+ "athanlab": {
126
+ "type": "stdio",
127
+ "command": "npx",
128
+ "args": ["-y", "@athanlab/mcp"],
129
+ "env": {
130
+ "ATHANLAB_API_KEY": "${env:ATHANLAB_API_KEY}",
131
+ "ATHANLAB_OUTPUT_DIR": "${workspaceFolder}/athanlab-audio"
132
+ }
133
+ }
134
+ }
135
+ }
136
+ ```
137
+
138
+ - `${env:NAME}` reads your environment, so the key stays out of the file. If Cursor was opened from the Dock and does not see your shell's variables, use `"envFile"` with an absolute path to a file **outside** any repository, holding the line `ATHANLAB_API_KEY=ak_live_…` (and `chmod 600` it).
139
+ - In the global file, use `"${userHome}/athanlab-audio"` for the output folder.
140
+
141
+ ### The skill on its own
142
+
143
+ The package ships the [`athanlab-voice`](https://unpkg.com/@athanlab/mcp/skill/athanlab-voice/SKILL.md) [Agent Skill](https://agentskills.io): it teaches Claude when to use AthanLab, how to write text for speech, how to cast voices and quote costs, and how to turn a script's manifest into subtitles and a timeline. The skill only guides Claude; the tools come from the MCP server above. Install it for every project:
144
+
145
+ ```bash
146
+ mkdir -p ~/.claude/skills/athanlab-voice
147
+ curl -fsSL https://unpkg.com/@athanlab/mcp/skill/athanlab-voice/SKILL.md -o ~/.claude/skills/athanlab-voice/SKILL.md
148
+ ```
149
+
150
+ or put it in `.claude/skills/athanlab-voice/` inside one project. Start a new session to load it.
151
+
152
+ ## Configuration
153
+
154
+ All configuration is environment variables. Logs go to **stderr** only (stdout is the MCP channel), and the key is never logged, echoed or included in an error.
155
+
156
+ | Variable | Default | Meaning |
157
+ |---|---|---|
158
+ | `ATHANLAB_API_KEY` | (required) | Your API key, `ak_live_` + 32 lowercase hex characters. The format is checked locally **before any request**, so a malformed key (a missing prefix, a truncated paste, the placeholder) never reaches the API or counts towards its failed-key limit. A well-formed but wrong key does reach it; after one `invalid_api_key` the server sends nothing more until it is restarted with a new key. If it is missing or malformed, the server still starts; every API tool says what to fix and the writing guide still works. |
159
+ | `ATHANLAB_API_BASE` | `https://api.athanlab.com/api/v1` | API base URL. Must be `https`, except `http://localhost` or `http://127.0.0.1` for development. |
160
+ | `ATHANLAB_OUTPUT_DIR` | `athanlab-audio` in the project | Where audio and manifests are written. A relative path resolves against the server's working directory. Unset, it is `athanlab-audio` in Claude Code's project folder (`CLAUDE_PROJECT_DIR`, which Claude Code sets), or in the working directory for other clients. It is created on demand. |
161
+ | `ATHANLAB_MAX_CHARS_PER_CALL` | (none) | Optional safety cap. An `athanlab_speak` or `athanlab_speak_script` call with more characters is refused before anything is sent. |
162
+ | `ATHANLAB_LOG_LEVEL` | `info` | `debug` adds detail to the stderr log. The key is removed from every line either way. |
163
+
164
+ ## Output files
165
+
166
+ - Everything is written inside `ATHANLAB_OUTPUT_DIR`, and nothing outside it. A `filename` from the agent is cut to its last path segment, cleaned of unsafe characters and `..`, and given the extension of the audio format. The final path is checked to still be inside the folder. `athanlab_speak`, `athanlab_download_audio` and a saved voice preview never overwrite a file: a clashing name gets a suffix. `athanlab_speak_script` owns its `out_subdir` and rewrites the files there on a re-run (see Re-runs).
167
+ - `athanlab_speak` without a `filename` names the file `athanlab-<time>-<job id prefix>.wav` (or `.mp3`).
168
+ - `athanlab_speak_script` writes into `<output dir>/<out_subdir>/`: one file per line, named by position plus the line `id` or speaker (`001-l001.wav`, `002-narrator.wav`…; a `/` or `\` in an id or speaker becomes `-`), and `manifest.json`. Without `out_subdir` the folder is `script-<hash of the script>`, so always pass one if the script may be edited and re-run.
169
+ - **The manifest.** Each entry has `index`, `id`, `speaker`, `voice_id`, `text`, `file`, `job_id`, `characters_charged`, `duration_seconds`, `start_seconds` and `end_seconds`, all on one timeline. The gap after a line is `gaps.sentence` (default 0.4 s) when the next line has the same speaker, and `gaps.turn` (default 0.55 s) when the speaker changes.
170
+ - **One track.** With `combine: true` and WAV output it also writes `combined.wav`, with exactly those gaps as silence.
171
+ - **Re-runs.** Re-running into the same folder skips finished lines and resumes running jobs, so an interrupted run finishes without paying twice. Lines are matched by position: a line whose text, voice or number mode changed is regenerated (and charged). Inserting a line in the middle regenerates every line after it. A line whose earlier job can no longer deliver audio (deleted after 30 days, gone, or failed) is quoted again as a new line, so it counts towards `ATHANLAB_MAX_CHARS_PER_CALL` and the confirmation threshold.
172
+ - **One run per folder.** While a script runs, a second call on the same `out_subdir` is refused with `script_running` (nothing is sent). A lock file, `.<out_subdir>.lock`, sits in the output directory meanwhile; one left behind by a crashed process is taken over automatically.
173
+ - AthanLab keeps audio for 30 days after a job is created; the local files are yours to keep.
174
+
175
+ ## How it talks to the API
176
+
177
+ - **Asynchronous jobs.** `POST /speech` answers `202` with the job, a `Location` and `Retry-After: 2`. The server waits that long, then polls `GET /speech/{id}`, backing off ×1.5 up to about 10 seconds, and downloads `GET /speech/{id}/audio` when the job has succeeded.
178
+ - **Safe retries.** Every create carries an `Idempotency-Key` generated once and **reused on every retry of that POST**. Within 24 hours a retry returns the original job (`Idempotent-Replayed: true`) instead of creating and charging a second one.
179
+ - **Only retryable errors are retried**, a bounded number of times, after the `Retry-After` the API sends: `429 rate_limited`, `429 concurrency_limit`, the `503`s and `500 internal_error`. Everything else is reported at once, with the error `code`, `message`, `retryable`, `Retry-After`, the `request_id` and an action to take.
180
+ - **DigitalOcean's 504 page.** The API runs on DigitalOcean App Platform, which replaces any HTTP 503 from the API with its own **HTML 504** page. The headers survive: `Retry-After`, `X-Request-Id`, plus `x-do-orig-status: 503`. The server treats a 504 with `x-do-orig-status: 503`, and any non-JSON 502/503/504 with a `Retry-After`, as a retryable capacity or restart error. It never tries to parse that HTML as JSON.
181
+ - **Scripts** run as many lines at once as the account allows (`max_concurrent_jobs − active_jobs` from `GET /usage`, at least 1). Each line is its own job with its own reused Idempotency-Key. A script stops creating jobs on `plan_required`, `insufficient_characters` or `key_budget_exceeded` and reports which lines are done.
182
+ - **Long waits.** `athanlab_speak` and `athanlab_speak_script` wait up to 15 minutes by default (`timeout_seconds`). When the client asks for progress, they send an MCP progress notification on every poll (every 2–10 seconds), even while a job sits at 0% through a cold start, so a client that resets its timeout on progress keeps waiting. If a job is still processing then, `athanlab_speak` returns its `job_id`: finish with `athanlab_get_job` and `athanlab_download_audio`, because re-running `athanlab_speak` would pay twice. A script that runs out of time is finished by calling it again with the same arguments.
183
+
184
+ ## Safety
185
+
186
+ - **The key stays on your machine.** It lives in the MCP server's environment. Claude never needs to see it, so never paste a key into a conversation. The server checks the key's format locally and never logs, echoes or returns it.
187
+ - **Cap every agent key**: its own key per agent or machine, a low monthly budget, a 30-day expiry, and only the permissions it needs (`speech:write`, `speech:read`, `voices:read`, `usage:read`). See [Create a key for an agent](#create-a-key-for-an-agent).
188
+ - **Cap each call** if you like, with `ATHANLAB_MAX_CHARS_PER_CALL`. `athanlab_speak_script` already refuses large scripts (over 2,000 characters or 20 lines) without `confirm: true`, and the skill tells Claude to show you the quote first.
189
+ - **Config files hold secrets in plain text** when you paste a key into them (`~/.claude.json`, `claude_desktop_config.json`, Cursor's `mcp.json`). Keep them out of repositories, backups you share and screen recordings. Prefer environment expansion where the client supports it.
190
+ - **If a key leaks**, revoke it on the Developer API page at once, or rotate it and choose **Stop the old secret immediately**. The 24-hour rotation grace is for planned rotations only.
191
+ - Use only voices you are allowed to use. Voices in your library must be your own, or used with the speaker's explicit permission.
192
+
193
+ ## Troubleshooting
194
+
195
+ | Symptom | What it means | What to do |
196
+ |---|---|---|
197
+ | `ATHANLAB_API_KEY is not set` / `is malformed` | Checked locally, nothing was sent | Set the key in the server's environment and restart the client. |
198
+ | `401 invalid_api_key` | The key is wrong, revoked or expired | **Never retried.** Create or copy a valid key. Every failed key check counts towards a per-network breaker: **20 failures in 5 minutes block every request from your network for 10 minutes** (`429 auth_blocked`, curl and other keys included). Don't loop on a bad key. |
199
+ | `402 plan_required` | Keys are read-only: no Max plan and no founder access | Subscribe to Max (or ask about founder access). Listing voices and jobs, downloading audio you already paid for, cancelling and usage keep working. |
200
+ | `402 insufficient_characters` | Not enough characters; nothing was charged | Top up Athan Tokens, or wait for the monthly allowance to reset (`athanlab_usage` shows when). |
201
+ | `429 key_budget_exceeded` | This key reached its monthly budget; nothing was charged | Raise the key's budget on the Developer API page, or wait for the next UTC month. Not retried. |
202
+ | `403 scope_missing` | The key lacks a permission | Create a key with the permission the tool needs (see [Tools](#tools)). |
203
+ | `403 developer_pending` / `developer_rejected` / `developer_suspended` / `account_suspended` | Account status | See the Developer API page, or email support@athanlab.com. |
204
+ | `429 rate_limited` / `concurrency_limit` | Too many requests, or too many jobs running | Handled automatically. Avoid running two scripts at once on one account. |
205
+ | An HTML 504 page, `x-do-orig-status: 503` | DigitalOcean replaced an API 503 (capacity, restart, read-only mode) | Retried automatically after `Retry-After`; nothing was charged. If it persists, the API may be paused for maintenance. Try later, and quote the `request_id` to support. |
206
+ | `422 unspeakable_text` / `text_too_long` | The text needs changing; nothing was charged | Follow the writing guide: sentence punctuation, Burmese script, a `number_mode` that does not blow up long numbers. |
207
+ | `410 audio_expired` | The audio is older than 30 days and was deleted | Generate it again (a new charge) if you still need it. A script re-run quotes such lines before creating them. |
208
+ | `script_running` | Another `athanlab_speak_script` call is already working on that `out_subdir` | Wait for it to return, then call again. Never run the same script twice at once. |
209
+ | `athanlab_speak` returned a `job_id` but no file | It stopped waiting (15 minutes) or the download failed | `athanlab_get_job`, then `athanlab_download_audio`. Don't run `athanlab_speak` again. |
210
+ | `npx -y @athanlab/mcp` → 404 | The package is not published yet | Use a [local build](#before-the-package-is-published). |
211
+ | The server never starts in Claude Desktop | Usually `npx` not found on the app's `PATH` | Use the absolute path from `which npx`; read `~/Library/Logs/Claude/mcp-server-athanlab.log`. |
212
+
213
+ For anything else, email **support@athanlab.com** with the `request_id` from the error (and the job id, if any). **Never send your API key.**
214
+
215
+ ## References
216
+
217
+ Official documentation this package and its setup instructions follow (checked October 2026):
218
+
219
+ - AthanLab Developer API: [athanlab.com/docs](https://athanlab.com/docs) and the [OpenAPI 3.1 document](https://api.athanlab.com/api/v1/openapi.json)
220
+ - Claude Code: [MCP](https://code.claude.com/docs/en/mcp), [skills](https://code.claude.com/docs/en/skills)
221
+ - [Agent Skills specification](https://agentskills.io/specification)
222
+ - Claude Desktop: [Connect to local MCP servers](https://modelcontextprotocol.io/docs/develop/connect-local-servers)
223
+ - Cursor: [MCP](https://cursor.com/docs/context/mcp)
@@ -0,0 +1,282 @@
1
+ import { systemClock } from '../clock.js';
2
+ import { silentLogger } from '../log.js';
3
+ import { USER_AGENT } from '../version.js';
4
+ import { AthanLabError, isRecord } from './errors.js';
5
+ const DEFAULT_TIMEOUT_MS = 30_000;
6
+ const MAX_ERROR_BODY_CHARS = 64 * 1024;
7
+ /** Retry-After as delta-seconds or an HTTP date; null when absent or unreadable. */
8
+ export function parseRetryAfter(value, nowMs) {
9
+ if (!value)
10
+ return null;
11
+ const trimmed = value.trim();
12
+ if (/^\d+(\.\d+)?$/.test(trimmed))
13
+ return Math.max(0, Math.ceil(Number(trimmed)));
14
+ const date = Date.parse(trimmed);
15
+ if (Number.isFinite(date))
16
+ return Math.max(0, Math.ceil((date - nowMs) / 1000));
17
+ return null;
18
+ }
19
+ function combineSignals(timeoutMs, signal) {
20
+ const timeout = AbortSignal.timeout(timeoutMs);
21
+ if (!signal)
22
+ return timeout;
23
+ const any = AbortSignal.any;
24
+ if (any)
25
+ return any([timeout, signal]);
26
+ return signal;
27
+ }
28
+ export class ApiClient {
29
+ baseUrl;
30
+ apiKey;
31
+ fetchImpl;
32
+ clock;
33
+ logger;
34
+ errorCatalog;
35
+ maxAttempts;
36
+ maxRetryWaitSeconds;
37
+ /** Set by the first 401 invalid_api_key / missing_api_key: no further request is sent. */
38
+ keyRejected = null;
39
+ constructor(opts) {
40
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
41
+ this.apiKey = opts.apiKey;
42
+ this.fetchImpl = opts.fetch ?? ((input, init) => fetch(input, init));
43
+ this.clock = opts.clock ?? systemClock;
44
+ this.logger = opts.logger ?? silentLogger;
45
+ this.errorCatalog = opts.errorCatalog ?? new Map();
46
+ this.maxAttempts = opts.maxAttempts ?? 5;
47
+ this.maxRetryWaitSeconds = opts.maxRetryWaitSeconds ?? 60;
48
+ }
49
+ /** Throws the configuration error when the key is missing or malformed — before any request. */
50
+ assertKey() {
51
+ if (!this.apiKey.ok) {
52
+ throw AthanLabError.client(this.apiKey.code, this.apiKey.message);
53
+ }
54
+ if (this.keyRejected) {
55
+ throw new AthanLabError({
56
+ code: this.keyRejected.code,
57
+ message: `The API refused ATHANLAB_API_KEY earlier in this session (${this.keyRejected.code}); no further requests are sent with it, to keep your network clear of the failed-key block. Fix the key and restart the MCP server.`,
58
+ source: 'client',
59
+ requestId: this.keyRejected.requestId,
60
+ });
61
+ }
62
+ return this.apiKey.key;
63
+ }
64
+ url(path, query) {
65
+ const url = new URL(`${this.baseUrl}${path.startsWith('/') ? path : `/${path}`}`);
66
+ for (const [k, v] of Object.entries(query ?? {})) {
67
+ if (v !== undefined && v !== null)
68
+ url.searchParams.set(k, String(v));
69
+ }
70
+ return url.toString();
71
+ }
72
+ async json(path, opts = {}) {
73
+ const res = await this.send(path, opts);
74
+ const text = await res.text();
75
+ let data;
76
+ try {
77
+ data = text ? JSON.parse(text) : null;
78
+ }
79
+ catch {
80
+ throw new AthanLabError({
81
+ code: 'invalid_response',
82
+ message: `The API answered HTTP ${res.status} with a body that is not JSON.`,
83
+ source: 'api',
84
+ status: res.status,
85
+ retryable: false,
86
+ requestId: res.headers.get('x-request-id'),
87
+ });
88
+ }
89
+ return {
90
+ status: res.status,
91
+ headers: res.headers,
92
+ data: data,
93
+ retryAfterSeconds: parseRetryAfter(res.headers.get('retry-after'), this.clock.now()),
94
+ requestId: res.headers.get('x-request-id'),
95
+ };
96
+ }
97
+ /** A successful response whose body the caller streams (audio). */
98
+ async raw(path, opts = {}) {
99
+ return this.send(path, { timeoutMs: 300_000, ...opts });
100
+ }
101
+ async send(path, opts) {
102
+ const key = this.assertKey();
103
+ const method = opts.method ?? 'GET';
104
+ const url = this.url(path, opts.query);
105
+ const headers = {
106
+ 'X-API-Key': key,
107
+ Accept: opts.accept ?? 'application/json',
108
+ 'User-Agent': USER_AGENT,
109
+ };
110
+ let body;
111
+ if (opts.body !== undefined) {
112
+ headers['Content-Type'] = 'application/json';
113
+ body = JSON.stringify(opts.body);
114
+ }
115
+ if (opts.idempotencyKey)
116
+ headers['Idempotency-Key'] = opts.idempotencyKey;
117
+ for (let attempt = 1;; attempt++) {
118
+ let failure;
119
+ try {
120
+ const res = await this.fetchImpl(url, {
121
+ method,
122
+ headers,
123
+ ...(body !== undefined ? { body } : {}),
124
+ signal: combineSignals(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS, opts.signal),
125
+ });
126
+ if (res.ok)
127
+ return res;
128
+ failure = await this.readFailure(res);
129
+ }
130
+ catch (err) {
131
+ if (opts.signal?.aborted)
132
+ throw err;
133
+ if (err instanceof AthanLabError)
134
+ throw err;
135
+ const timedOut = err instanceof Error && err.name === 'TimeoutError';
136
+ failure = new AthanLabError({
137
+ code: 'network_error',
138
+ message: timedOut
139
+ ? 'The request to the AthanLab API timed out.'
140
+ : `The request to the AthanLab API failed: ${networkReason(err)}.`,
141
+ source: 'network',
142
+ retryable: true,
143
+ outcomeUnknown: true,
144
+ });
145
+ }
146
+ if (failure.code === 'invalid_api_key' || failure.code === 'missing_api_key') {
147
+ this.keyRejected = failure;
148
+ }
149
+ const wait = this.retryWaitMs(failure, attempt, opts);
150
+ if (wait === null)
151
+ throw failure;
152
+ this.logger.warn('retrying AthanLab API request', {
153
+ method,
154
+ path: new URL(url).pathname,
155
+ code: failure.code,
156
+ status: failure.status,
157
+ attempt,
158
+ wait_ms: wait,
159
+ request_id: failure.requestId,
160
+ });
161
+ await this.clock.sleep(wait, opts.signal);
162
+ }
163
+ }
164
+ /** ms to wait before the next attempt, or null when this failure must be returned. */
165
+ retryWaitMs(failure, attempt, opts) {
166
+ if (!failure.retryable)
167
+ return null;
168
+ if (opts.noRetryCodes?.includes(failure.code))
169
+ return null;
170
+ const budget = failure.code === 'concurrency_limit'
171
+ ? (opts.concurrencyAttempts ?? 30)
172
+ : failure.source === 'network' || failure.code === 'gateway_unavailable'
173
+ ? (opts.maxAttempts ?? this.maxAttempts) + 1
174
+ : (opts.maxAttempts ?? this.maxAttempts);
175
+ if (attempt >= budget)
176
+ return null;
177
+ const seconds = failure.retryAfterSeconds ?? Math.min(60, 2 ** attempt);
178
+ if (seconds > this.maxRetryWaitSeconds)
179
+ return null;
180
+ const ms = Math.max(0, seconds * 1000);
181
+ if (opts.deadline !== undefined && this.clock.now() + ms > opts.deadline)
182
+ return null;
183
+ return ms;
184
+ }
185
+ async readFailure(res) {
186
+ const retryAfter = parseRetryAfter(res.headers.get('retry-after'), this.clock.now());
187
+ const requestId = res.headers.get('x-request-id');
188
+ let text = '';
189
+ try {
190
+ text = (await res.text()).slice(0, MAX_ERROR_BODY_CHARS);
191
+ }
192
+ catch {
193
+ text = '';
194
+ }
195
+ const contentType = res.headers.get('content-type') ?? '';
196
+ if (contentType.includes('json') || /^\s*\{/.test(text)) {
197
+ try {
198
+ const parsed = JSON.parse(text);
199
+ if (isRecord(parsed) && isRecord(parsed.error) && typeof parsed.error.code === 'string') {
200
+ return AthanLabError.fromEnvelope(parsed.error, res.status, retryAfter, requestId);
201
+ }
202
+ }
203
+ catch {
204
+ // fall through to the non-JSON handling
205
+ }
206
+ }
207
+ // Not the API's envelope: an edge page (DigitalOcean / Cloudflare) or a bare status.
208
+ const origStatus = Number.parseInt(res.headers.get('x-do-orig-status') ?? '', 10);
209
+ const status = Number.isFinite(origStatus) ? origStatus : res.status;
210
+ const headerCode = res.headers.get('x-athanlab-error-code');
211
+ if (headerCode && /^[a-z0-9_]{1,64}$/.test(headerCode)) {
212
+ const known = this.errorCatalog.get(headerCode);
213
+ const retryableHeader = res.headers.get('x-athanlab-retryable');
214
+ return new AthanLabError({
215
+ code: headerCode,
216
+ message: known?.message ?? `The API refused the request (${headerCode}); the edge replaced its error body.`,
217
+ source: 'edge',
218
+ status,
219
+ type: known?.type ?? null,
220
+ retryable: retryableHeader !== null ? retryableHeader === 'true' : known?.retryable ?? status >= 500,
221
+ retryAfterSeconds: retryAfter,
222
+ requestId,
223
+ });
224
+ }
225
+ if ([502, 503, 504].includes(res.status)) {
226
+ if (origStatus === 503 || retryAfter !== null) {
227
+ // The origin's own 503 (every one is retryable, and charged nothing),
228
+ // with its body swapped for an HTML page by DigitalOcean.
229
+ return new AthanLabError({
230
+ code: 'service_unavailable',
231
+ message: 'The AthanLab API is temporarily unavailable (capacity, a restart or maintenance). Nothing was charged.',
232
+ source: 'edge',
233
+ status: 503,
234
+ type: 'service_unavailable',
235
+ retryable: true,
236
+ retryAfterSeconds: retryAfter,
237
+ requestId,
238
+ });
239
+ }
240
+ return new AthanLabError({
241
+ code: 'gateway_unavailable',
242
+ message: `The API gateway answered HTTP ${res.status} without a JSON body; the request may or may not have been processed.`,
243
+ source: 'edge',
244
+ status: res.status,
245
+ retryable: true,
246
+ retryAfterSeconds: null,
247
+ requestId,
248
+ outcomeUnknown: true,
249
+ });
250
+ }
251
+ if (res.status >= 500) {
252
+ return new AthanLabError({
253
+ code: 'internal_error',
254
+ message: `The API answered HTTP ${res.status} without a JSON body.`,
255
+ source: 'edge',
256
+ status: res.status,
257
+ retryable: true,
258
+ retryAfterSeconds: retryAfter,
259
+ requestId,
260
+ outcomeUnknown: true,
261
+ });
262
+ }
263
+ return new AthanLabError({
264
+ code: `http_${res.status}`,
265
+ message: `The API answered HTTP ${res.status} without a JSON error body.`,
266
+ source: 'edge',
267
+ status: res.status,
268
+ retryable: res.status === 429,
269
+ retryAfterSeconds: retryAfter,
270
+ requestId,
271
+ });
272
+ }
273
+ }
274
+ function networkReason(err) {
275
+ if (!(err instanceof Error))
276
+ return 'network error';
277
+ const cause = err.cause;
278
+ const code = isRecord(cause) && typeof cause.code === 'string' ? cause.code : null;
279
+ // Only the error class/code — never anything that could carry request data.
280
+ return code ? `${err.message} (${code})` : err.message;
281
+ }
282
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"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"}