anki-voice 1.0.0__tar.gz

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.
@@ -0,0 +1,34 @@
1
+ name: Publish
2
+
3
+ # Push a tag v<version> (matching pyproject.toml) to publish that version to PyPI.
4
+ on:
5
+ push:
6
+ tags: ["v*"]
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ uses: ./.github/workflows/test.yml
14
+
15
+ publish:
16
+ needs: test
17
+ runs-on: ubuntu-latest
18
+ # PyPI trusted publishing: no API token, PyPI trusts this workflow in this environment.
19
+ environment: pypi
20
+ permissions:
21
+ contents: read
22
+ id-token: write
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ - uses: astral-sh/setup-uv@v6
26
+ - name: Check that the tag matches the version
27
+ run: |
28
+ version=$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')
29
+ if [ "v$version" != "$GITHUB_REF_NAME" ]; then
30
+ echo "Tag $GITHUB_REF_NAME doesn't match version $version in pyproject.toml" >&2
31
+ exit 1
32
+ fi
33
+ - run: uv build
34
+ - run: uv publish --trusted-publishing always
@@ -0,0 +1,20 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_call:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ test:
14
+ # anki-voice runs on macOS only.
15
+ runs-on: macos-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: astral-sh/setup-uv@v6
19
+ - run: uv sync --locked
20
+ - run: uv run pytest
@@ -0,0 +1,5 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .env
5
+ .pytest_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Christian Fratta
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.
@@ -0,0 +1,288 @@
1
+ Metadata-Version: 2.5
2
+ Name: anki-voice
3
+ Version: 1.0.0
4
+ Summary: MCP server for reviewing Anki cards by voice from the Claude app
5
+ Project-URL: Repository, https://github.com/EquineOntology/anki-voice
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: anki,claude,flashcards,mcp,voice
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Environment :: MacOS X
11
+ Classifier: Operating System :: MacOS
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Education
14
+ Requires-Python: >=3.12
15
+ Requires-Dist: httpx>=0.27
16
+ Requires-Dist: mcp<3,>=2.3
17
+ Requires-Dist: starlette>=0.40
18
+ Requires-Dist: uvicorn>=0.30
19
+ Description-Content-Type: text/markdown
20
+
21
+ # anki-voice
22
+
23
+ Hands-free Anki review via Claude voice mode.
24
+
25
+ Claude reads each card's front, listens to your answer, tells you whether it
26
+ was right, and records the grade in Anki. It works by running a small server
27
+ on your Mac next to Anki and Claude connects to it as a custom connector.
28
+
29
+ ## Setup
30
+
31
+ Your agent can take care of the setup, except for a few like generating a
32
+ password, signing up for Tailscale, etc.
33
+
34
+ ### Prereqs
35
+
36
+ - A Mac: anki-voice runs on macOS only, `serve` included
37
+ - [Anki](https://apps.ankiweb.net) with a recent
38
+ [AnkiConnect](https://foosoft.net/projects/anki-connect/) add-on, one that
39
+ has its JSON-RPC API v1 (see [Checking AnkiConnect](#checking-ankiconnect))
40
+ - [uv](https://docs.astral.sh/uv/)
41
+ - [Tailscale](https://tailscale.com), with
42
+ MagicDNS and [Funnel](https://tailscale.com/kb/1223/funnel) enabled for your tailnet. It
43
+ puts the server on the internet for the length of a review, so Claude can
44
+ reach it. You can do this on a free account.
45
+ - A place to keep the sign-in password, ideally a password manager with a
46
+ command-line tool
47
+ - A Claude account that can add custom connectors
48
+
49
+ ### First-time setup
50
+
51
+ 1. **Install it**:
52
+
53
+ ```bash
54
+ uv tool install anki-voice
55
+ ```
56
+ 2. **Choose a password** of at least 16 characters. Claude will ask for it
57
+ the first time it connects. Tell the server how to get it by setting, in
58
+ your shell profile, a command that prints it:
59
+
60
+ ```bash
61
+ export ANKI_VOICE_PASSWORD_COMMAND='<your password manager> get anki-voice'
62
+ ```
63
+
64
+ or the password itself, in `ANKI_VOICE_PASSWORD`. Prefer the command: the
65
+ password then exists only in the server's memory, while
66
+ `ANKI_VOICE_PASSWORD` keeps it in plain text in your profile and in the
67
+ environment of everything started from your shell.
68
+ 3. **Add the instructions field** to your note types:
69
+
70
+ ```bash
71
+ anki-voice add-field
72
+ ```
73
+
74
+ This adds an `AnkiVoice instructions` field to every note type that lacks
75
+ it (see [Instructions for single cards](#instructions-for-single-cards)).
76
+ Changing a note type needs a full sync, so sync your other devices first,
77
+ then choose "upload" at the next sync. Run it again after you create a
78
+ new note type.
79
+ 4. **Name the languages you study**, if any, so their cards are read in that
80
+ language (see [Cards in other languages](#cards-in-other-languages)):
81
+
82
+ ```bash
83
+ export ANKI_VOICE_LANGUAGES='Spanish 🇪🇸 🇲🇽'
84
+ ```
85
+ 5. **Leave out decks** that don't work by voice, if you like:
86
+
87
+ ```bash
88
+ export ANKI_VOICE_EXCLUDE_DECKS='Geography::Flags,Music'
89
+ ```
90
+ 6. **Add the connector in Claude** once a session is online (see below): add a
91
+ custom connector with the address the session prints, and choose **Sign in
92
+ now** and **Register automatically**. A sign-in page opens; enter the
93
+ password.
94
+
95
+ ## Run a review
96
+
97
+ ```bash
98
+ anki-voice session
99
+ ```
100
+
101
+ Then **wait until it prints `online at https://…/mcp`**. This often takes 10
102
+ minutes or more the first time in a while (see
103
+ [Why the wait](#why-the-wait)). Connecting earlier fails.
104
+
105
+ Once it's online, start voice mode in Claude and say something like "let's do
106
+ my Anki review" or "just my Spanish cards".
107
+
108
+ The session stops on its own two minutes after the review is done, or after 30
109
+ minutes without activity. Ctrl-C or closing the terminal stops it too. Either
110
+ way, the server goes offline and nothing is left on the internet.
111
+
112
+ ### What you can say
113
+
114
+ | Say | What happens |
115
+ |---|---|
116
+ | your answer | Claude says whether it was right, gives the correct answer if not, and moves on |
117
+ | "I don't know" | Counts as wrong ("again") |
118
+ | your answer and "easy", or "that one was hard" | A correct answer is graded "easy" or "hard" instead of "good". Said after the grade, it changes that grade |
119
+ | "skip" | The card stays due in Anki; you get the next one |
120
+ | "don't ask me this one again" | The card's note gets the tag `anki-voice::skip` and voice reviews leave it out from now on. Remove the tag in Anki to bring it back |
121
+ | "undo", or "that was right!" | Takes back the last grade or skip, once |
122
+ | "flag this red" | Flags the card in Anki for you to look at later. Claude asks for a colour if you don't name one |
123
+ | "on this card, accept either order" | Claude rewrites the card's instructions (see below) |
124
+ | "just maths" / "everything again" | Limits the review to one topic, or lifts the limit |
125
+
126
+ Grading is strict: only a correct and complete answer is "good" (or "hard" or
127
+ "easy", when you ask). For nouns in languages whose articles show gender, the
128
+ article is part of the answer.
129
+
130
+ ### Cards in other languages
131
+
132
+ Each language in `ANKI_VOICE_LANGUAGES` is a name followed by any flag emoji
133
+ that mark its cards. A card is in that language when its deck or note type name
134
+ contains the name (`Spanish`, `Study::Spanish`, `Spanish vocab`), or its front
135
+ or back shows one of the flags. Separate languages with commas:
136
+ `'Spanish 🇪🇸 🇲🇽, French 🇫🇷'`.
137
+
138
+ Claude reads these cards with its voice for that language, and speaks only
139
+ that language on them. Or that's how it works most of the time, it's a bit
140
+ finicky. If it doesn't work well, try doing a session only on that deck.
141
+
142
+ ### Instructions for single cards
143
+
144
+ The `AnkiVoice instructions` field on a note tells Claude how to treat its
145
+ cards: what counts as an answer, how to read the prompt. Claude follows them
146
+ over its general rules and never reads them aloud. No card template shows the
147
+ field, so the cards look the same in Anki.
148
+
149
+ You can write them in Anki, or ask Claude during a review ("on this card,
150
+ accept either order").
151
+
152
+ ### While a session runs
153
+
154
+ **Don't use Anki's window.** Two reasons:
155
+
156
+ - "undo" uses Anki's own undo, which takes back whatever changed last, even if
157
+ that was you in the window.
158
+ - Once Anki's window shows a deck or its reviewer, Anki only accepts answers
159
+ for the card at the top of its own queue. The session puts the window back on
160
+ the Decks screen when it starts, and again if Anki refuses an answer.
161
+
162
+ ### Which cards come up
163
+
164
+ 1. Cards in learning that are due now
165
+ 2. Reviews due, shuffled within each due day
166
+ 3. Cards in learning due within the next 20 minutes
167
+
168
+ New cards **do not** come up. Neither do suspended or buried cards, cards from
169
+ excluded decks, or cards whose front needs a picture or a sound. Claude tells
170
+ you at the end how many of those last were left out.
171
+
172
+ Each grade is saved in Anki the moment it's given, so a card you got wrong
173
+ comes back when its learning step makes it due. Anki logs each answer with the
174
+ time from when Claude got the card to when it was graded, less 10 seconds for
175
+ the speech you can't skip.
176
+
177
+ ## Technical details
178
+
179
+ ### Security
180
+
181
+ - **Online only during a review.** The Funnel and its public address exist only
182
+ while a session runs. A small guard process takes the Funnel down as soon as
183
+ the session is gone, even after `kill -9`. To check that nothing is
184
+ published:
185
+
186
+ ```bash
187
+ tailscale funnel status
188
+ ```
189
+
190
+ It should say "No serve config". If only the Mac app is installed, the
191
+ command is `/Applications/Tailscale.app/Contents/MacOS/Tailscale`.
192
+ - **Password sign-in.** The server signs Claude in itself, with one password.
193
+ Every request without a valid token is refused.
194
+ - **Only Claude can sign in.** The sign-in only ever sends its result to
195
+ Claude's own address, so a link to the sign-in page can't be used to phish
196
+ the password into someone else's hands.
197
+ - **Tokens expire.** Claude's access lasts an hour and is renewed for up to 48
198
+ hours of disuse; after that, it asks for the password again.
199
+ - **Tokens are stored as hashes**, in `~/.local/share/anki-voice/oauth.json`
200
+ (readable only by you), so restarting doesn't sign Claude out. Delete that
201
+ file to sign everything out.
202
+ - **Guessing is limited.** After 10 wrong passwords in 15 minutes, sign-in is
203
+ refused for the rest of that window. The limit is shared by everyone, so
204
+ someone who finds the address can keep you from signing in for as long as
205
+ they keep guessing. Claude stays connected if it already is.
206
+ - **Card text reaches Claude as-is.** Each card's front, back and
207
+ `AnkiVoice instructions` field go to Claude, including text a template
208
+ hides, and Claude follows the instructions field over its general rules. A
209
+ shared deck written by someone else could use that to have Claude misgrade
210
+ cards or rewrite other notes' instructions. Check the instructions field of
211
+ shared decks before you review them by voice, or leave them out with
212
+ `ANKI_VOICE_EXCLUDE_DECKS`.
213
+
214
+ ### Why the wait when starting a session
215
+
216
+ Tailscale publishes the server's public address in DNS only when the Funnel
217
+ starts, and the record often takes 10 minutes or more to appear. If anything
218
+ looks the address up before then, such as Claude when you try to connect, the
219
+ answer "no such name" is cached for up to 5 minutes. So the session waits until
220
+ the record exists, then until any such cached answer has expired, then until a
221
+ request through the public relay actually reaches the server. Only then does it
222
+ say it's online. After that it checks every minute and tells you if the server
223
+ stops answering.
224
+
225
+ ### Other ways to run it
226
+
227
+ **Behind a tunnel of your own**, instead of Tailscale:
228
+
229
+ ```bash
230
+ export ANKI_VOICE_PUBLIC_URL=https://<your tunnel's address>
231
+ anki-voice serve
232
+ ```
233
+
234
+ It gets the password the same way a session does. The server listens on
235
+ `127.0.0.1:8787`; point the tunnel there. The connector
236
+ address is `$ANKI_VOICE_PUBLIC_URL/mcp`. Unlike a session, this runs until you
237
+ stop it.
238
+
239
+ **For a local client** such as Claude Code, without sign-in:
240
+
241
+ ```bash
242
+ anki-voice serve --stdio
243
+ ```
244
+
245
+ ### Settings
246
+
247
+ All settings are environment variables.
248
+
249
+ | Variable | Default | What it sets |
250
+ |---|---|---|
251
+ | `ANKI_VOICE_PASSWORD_COMMAND` | | A command that prints the sign-in password |
252
+ | `ANKI_VOICE_PASSWORD` | | The password itself, instead of a command |
253
+ | `ANKI_VOICE_LANGUAGES` | none | Languages to read in their own voice, each with its flags, comma-separated |
254
+ | `ANKI_VOICE_EXCLUDE_DECKS` | none | Decks to leave out, with their subdecks, comma-separated |
255
+ | `ANKI_VOICE_IDLE_MINUTES` | `30` | Minutes without activity before a session stops |
256
+ | `ANKI_VOICE_LEARN_AHEAD_MINUTES` | `20` | How far ahead cards in learning come up |
257
+ | `ANKI_VOICE_OVERHEAD_SECONDS` | `10` | Taken off each answer time logged in Anki |
258
+ | `ANKI_VOICE_DRY_RUN` | | `1` to try things out: grades, instructions and flags aren't saved to Anki |
259
+ | `ANKI_VOICE_PORT` | `8787` | Local port of the server |
260
+ | `ANKI_VOICE_STATE_FILE` | `~/.local/share/anki-voice/oauth.json` | Where tokens are stored |
261
+ | `ANKICONNECT_URL` | `http://127.0.0.1:8765` | Where AnkiConnect listens |
262
+
263
+ ### Checking AnkiConnect
264
+
265
+ anki-voice uses AnkiConnect's long-standing API (version 6) to read cards and
266
+ change notes, and its newer JSON-RPC API v1 (at `/api/v1`) to grade, flag and
267
+ undo. Older AnkiConnect releases have only the first. To check yours, with Anki
268
+ open:
269
+
270
+ ```bash
271
+ curl -s localhost:8765/api/v1 -d '{"jsonrpc":"2.0","id":1,"method":"operations.get","params":{"operationId":"check"}}'
272
+ ```
273
+
274
+ A reply that starts with `{"jsonrpc"` (here, "Operation not found") means API v1
275
+ is there. anki-voice is tested with the AnkiConnect release of October 2026,
276
+ which needs Anki 26.8 or later.
277
+
278
+
279
+ ## For developers
280
+
281
+ - How Claude runs a review is set by the instructions at the top of
282
+ `src/anki_voice/server.py`.
283
+
284
+ - You can run tests via uv: `uv run pytest`, and anki-voice from the
285
+ checkout with `uv run anki-voice …`.
286
+ - To release: set the new version in `pyproject.toml` (and `uv lock`), commit,
287
+ then push a tag `v<version>`. GitHub Actions runs the tests, and publishes to
288
+ PyPI only if the tag matches the version.
@@ -0,0 +1,268 @@
1
+ # anki-voice
2
+
3
+ Hands-free Anki review via Claude voice mode.
4
+
5
+ Claude reads each card's front, listens to your answer, tells you whether it
6
+ was right, and records the grade in Anki. It works by running a small server
7
+ on your Mac next to Anki and Claude connects to it as a custom connector.
8
+
9
+ ## Setup
10
+
11
+ Your agent can take care of the setup, except for a few like generating a
12
+ password, signing up for Tailscale, etc.
13
+
14
+ ### Prereqs
15
+
16
+ - A Mac: anki-voice runs on macOS only, `serve` included
17
+ - [Anki](https://apps.ankiweb.net) with a recent
18
+ [AnkiConnect](https://foosoft.net/projects/anki-connect/) add-on, one that
19
+ has its JSON-RPC API v1 (see [Checking AnkiConnect](#checking-ankiconnect))
20
+ - [uv](https://docs.astral.sh/uv/)
21
+ - [Tailscale](https://tailscale.com), with
22
+ MagicDNS and [Funnel](https://tailscale.com/kb/1223/funnel) enabled for your tailnet. It
23
+ puts the server on the internet for the length of a review, so Claude can
24
+ reach it. You can do this on a free account.
25
+ - A place to keep the sign-in password, ideally a password manager with a
26
+ command-line tool
27
+ - A Claude account that can add custom connectors
28
+
29
+ ### First-time setup
30
+
31
+ 1. **Install it**:
32
+
33
+ ```bash
34
+ uv tool install anki-voice
35
+ ```
36
+ 2. **Choose a password** of at least 16 characters. Claude will ask for it
37
+ the first time it connects. Tell the server how to get it by setting, in
38
+ your shell profile, a command that prints it:
39
+
40
+ ```bash
41
+ export ANKI_VOICE_PASSWORD_COMMAND='<your password manager> get anki-voice'
42
+ ```
43
+
44
+ or the password itself, in `ANKI_VOICE_PASSWORD`. Prefer the command: the
45
+ password then exists only in the server's memory, while
46
+ `ANKI_VOICE_PASSWORD` keeps it in plain text in your profile and in the
47
+ environment of everything started from your shell.
48
+ 3. **Add the instructions field** to your note types:
49
+
50
+ ```bash
51
+ anki-voice add-field
52
+ ```
53
+
54
+ This adds an `AnkiVoice instructions` field to every note type that lacks
55
+ it (see [Instructions for single cards](#instructions-for-single-cards)).
56
+ Changing a note type needs a full sync, so sync your other devices first,
57
+ then choose "upload" at the next sync. Run it again after you create a
58
+ new note type.
59
+ 4. **Name the languages you study**, if any, so their cards are read in that
60
+ language (see [Cards in other languages](#cards-in-other-languages)):
61
+
62
+ ```bash
63
+ export ANKI_VOICE_LANGUAGES='Spanish 🇪🇸 🇲🇽'
64
+ ```
65
+ 5. **Leave out decks** that don't work by voice, if you like:
66
+
67
+ ```bash
68
+ export ANKI_VOICE_EXCLUDE_DECKS='Geography::Flags,Music'
69
+ ```
70
+ 6. **Add the connector in Claude** once a session is online (see below): add a
71
+ custom connector with the address the session prints, and choose **Sign in
72
+ now** and **Register automatically**. A sign-in page opens; enter the
73
+ password.
74
+
75
+ ## Run a review
76
+
77
+ ```bash
78
+ anki-voice session
79
+ ```
80
+
81
+ Then **wait until it prints `online at https://…/mcp`**. This often takes 10
82
+ minutes or more the first time in a while (see
83
+ [Why the wait](#why-the-wait)). Connecting earlier fails.
84
+
85
+ Once it's online, start voice mode in Claude and say something like "let's do
86
+ my Anki review" or "just my Spanish cards".
87
+
88
+ The session stops on its own two minutes after the review is done, or after 30
89
+ minutes without activity. Ctrl-C or closing the terminal stops it too. Either
90
+ way, the server goes offline and nothing is left on the internet.
91
+
92
+ ### What you can say
93
+
94
+ | Say | What happens |
95
+ |---|---|
96
+ | your answer | Claude says whether it was right, gives the correct answer if not, and moves on |
97
+ | "I don't know" | Counts as wrong ("again") |
98
+ | your answer and "easy", or "that one was hard" | A correct answer is graded "easy" or "hard" instead of "good". Said after the grade, it changes that grade |
99
+ | "skip" | The card stays due in Anki; you get the next one |
100
+ | "don't ask me this one again" | The card's note gets the tag `anki-voice::skip` and voice reviews leave it out from now on. Remove the tag in Anki to bring it back |
101
+ | "undo", or "that was right!" | Takes back the last grade or skip, once |
102
+ | "flag this red" | Flags the card in Anki for you to look at later. Claude asks for a colour if you don't name one |
103
+ | "on this card, accept either order" | Claude rewrites the card's instructions (see below) |
104
+ | "just maths" / "everything again" | Limits the review to one topic, or lifts the limit |
105
+
106
+ Grading is strict: only a correct and complete answer is "good" (or "hard" or
107
+ "easy", when you ask). For nouns in languages whose articles show gender, the
108
+ article is part of the answer.
109
+
110
+ ### Cards in other languages
111
+
112
+ Each language in `ANKI_VOICE_LANGUAGES` is a name followed by any flag emoji
113
+ that mark its cards. A card is in that language when its deck or note type name
114
+ contains the name (`Spanish`, `Study::Spanish`, `Spanish vocab`), or its front
115
+ or back shows one of the flags. Separate languages with commas:
116
+ `'Spanish 🇪🇸 🇲🇽, French 🇫🇷'`.
117
+
118
+ Claude reads these cards with its voice for that language, and speaks only
119
+ that language on them. Or that's how it works most of the time, it's a bit
120
+ finicky. If it doesn't work well, try doing a session only on that deck.
121
+
122
+ ### Instructions for single cards
123
+
124
+ The `AnkiVoice instructions` field on a note tells Claude how to treat its
125
+ cards: what counts as an answer, how to read the prompt. Claude follows them
126
+ over its general rules and never reads them aloud. No card template shows the
127
+ field, so the cards look the same in Anki.
128
+
129
+ You can write them in Anki, or ask Claude during a review ("on this card,
130
+ accept either order").
131
+
132
+ ### While a session runs
133
+
134
+ **Don't use Anki's window.** Two reasons:
135
+
136
+ - "undo" uses Anki's own undo, which takes back whatever changed last, even if
137
+ that was you in the window.
138
+ - Once Anki's window shows a deck or its reviewer, Anki only accepts answers
139
+ for the card at the top of its own queue. The session puts the window back on
140
+ the Decks screen when it starts, and again if Anki refuses an answer.
141
+
142
+ ### Which cards come up
143
+
144
+ 1. Cards in learning that are due now
145
+ 2. Reviews due, shuffled within each due day
146
+ 3. Cards in learning due within the next 20 minutes
147
+
148
+ New cards **do not** come up. Neither do suspended or buried cards, cards from
149
+ excluded decks, or cards whose front needs a picture or a sound. Claude tells
150
+ you at the end how many of those last were left out.
151
+
152
+ Each grade is saved in Anki the moment it's given, so a card you got wrong
153
+ comes back when its learning step makes it due. Anki logs each answer with the
154
+ time from when Claude got the card to when it was graded, less 10 seconds for
155
+ the speech you can't skip.
156
+
157
+ ## Technical details
158
+
159
+ ### Security
160
+
161
+ - **Online only during a review.** The Funnel and its public address exist only
162
+ while a session runs. A small guard process takes the Funnel down as soon as
163
+ the session is gone, even after `kill -9`. To check that nothing is
164
+ published:
165
+
166
+ ```bash
167
+ tailscale funnel status
168
+ ```
169
+
170
+ It should say "No serve config". If only the Mac app is installed, the
171
+ command is `/Applications/Tailscale.app/Contents/MacOS/Tailscale`.
172
+ - **Password sign-in.** The server signs Claude in itself, with one password.
173
+ Every request without a valid token is refused.
174
+ - **Only Claude can sign in.** The sign-in only ever sends its result to
175
+ Claude's own address, so a link to the sign-in page can't be used to phish
176
+ the password into someone else's hands.
177
+ - **Tokens expire.** Claude's access lasts an hour and is renewed for up to 48
178
+ hours of disuse; after that, it asks for the password again.
179
+ - **Tokens are stored as hashes**, in `~/.local/share/anki-voice/oauth.json`
180
+ (readable only by you), so restarting doesn't sign Claude out. Delete that
181
+ file to sign everything out.
182
+ - **Guessing is limited.** After 10 wrong passwords in 15 minutes, sign-in is
183
+ refused for the rest of that window. The limit is shared by everyone, so
184
+ someone who finds the address can keep you from signing in for as long as
185
+ they keep guessing. Claude stays connected if it already is.
186
+ - **Card text reaches Claude as-is.** Each card's front, back and
187
+ `AnkiVoice instructions` field go to Claude, including text a template
188
+ hides, and Claude follows the instructions field over its general rules. A
189
+ shared deck written by someone else could use that to have Claude misgrade
190
+ cards or rewrite other notes' instructions. Check the instructions field of
191
+ shared decks before you review them by voice, or leave them out with
192
+ `ANKI_VOICE_EXCLUDE_DECKS`.
193
+
194
+ ### Why the wait when starting a session
195
+
196
+ Tailscale publishes the server's public address in DNS only when the Funnel
197
+ starts, and the record often takes 10 minutes or more to appear. If anything
198
+ looks the address up before then, such as Claude when you try to connect, the
199
+ answer "no such name" is cached for up to 5 minutes. So the session waits until
200
+ the record exists, then until any such cached answer has expired, then until a
201
+ request through the public relay actually reaches the server. Only then does it
202
+ say it's online. After that it checks every minute and tells you if the server
203
+ stops answering.
204
+
205
+ ### Other ways to run it
206
+
207
+ **Behind a tunnel of your own**, instead of Tailscale:
208
+
209
+ ```bash
210
+ export ANKI_VOICE_PUBLIC_URL=https://<your tunnel's address>
211
+ anki-voice serve
212
+ ```
213
+
214
+ It gets the password the same way a session does. The server listens on
215
+ `127.0.0.1:8787`; point the tunnel there. The connector
216
+ address is `$ANKI_VOICE_PUBLIC_URL/mcp`. Unlike a session, this runs until you
217
+ stop it.
218
+
219
+ **For a local client** such as Claude Code, without sign-in:
220
+
221
+ ```bash
222
+ anki-voice serve --stdio
223
+ ```
224
+
225
+ ### Settings
226
+
227
+ All settings are environment variables.
228
+
229
+ | Variable | Default | What it sets |
230
+ |---|---|---|
231
+ | `ANKI_VOICE_PASSWORD_COMMAND` | | A command that prints the sign-in password |
232
+ | `ANKI_VOICE_PASSWORD` | | The password itself, instead of a command |
233
+ | `ANKI_VOICE_LANGUAGES` | none | Languages to read in their own voice, each with its flags, comma-separated |
234
+ | `ANKI_VOICE_EXCLUDE_DECKS` | none | Decks to leave out, with their subdecks, comma-separated |
235
+ | `ANKI_VOICE_IDLE_MINUTES` | `30` | Minutes without activity before a session stops |
236
+ | `ANKI_VOICE_LEARN_AHEAD_MINUTES` | `20` | How far ahead cards in learning come up |
237
+ | `ANKI_VOICE_OVERHEAD_SECONDS` | `10` | Taken off each answer time logged in Anki |
238
+ | `ANKI_VOICE_DRY_RUN` | | `1` to try things out: grades, instructions and flags aren't saved to Anki |
239
+ | `ANKI_VOICE_PORT` | `8787` | Local port of the server |
240
+ | `ANKI_VOICE_STATE_FILE` | `~/.local/share/anki-voice/oauth.json` | Where tokens are stored |
241
+ | `ANKICONNECT_URL` | `http://127.0.0.1:8765` | Where AnkiConnect listens |
242
+
243
+ ### Checking AnkiConnect
244
+
245
+ anki-voice uses AnkiConnect's long-standing API (version 6) to read cards and
246
+ change notes, and its newer JSON-RPC API v1 (at `/api/v1`) to grade, flag and
247
+ undo. Older AnkiConnect releases have only the first. To check yours, with Anki
248
+ open:
249
+
250
+ ```bash
251
+ curl -s localhost:8765/api/v1 -d '{"jsonrpc":"2.0","id":1,"method":"operations.get","params":{"operationId":"check"}}'
252
+ ```
253
+
254
+ A reply that starts with `{"jsonrpc"` (here, "Operation not found") means API v1
255
+ is there. anki-voice is tested with the AnkiConnect release of October 2026,
256
+ which needs Anki 26.8 or later.
257
+
258
+
259
+ ## For developers
260
+
261
+ - How Claude runs a review is set by the instructions at the top of
262
+ `src/anki_voice/server.py`.
263
+
264
+ - You can run tests via uv: `uv run pytest`, and anki-voice from the
265
+ checkout with `uv run anki-voice …`.
266
+ - To release: set the new version in `pyproject.toml` (and `uv lock`), commit,
267
+ then push a tag `v<version>`. GitHub Actions runs the tests, and publishes to
268
+ PyPI only if the tag matches the version.
@@ -0,0 +1,41 @@
1
+ [project]
2
+ name = "anki-voice"
3
+ version = "1.0.0"
4
+ description = "MCP server for reviewing Anki cards by voice from the Claude app"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.12"
9
+ keywords = ["anki", "mcp", "claude", "voice", "flashcards"]
10
+ classifiers = [
11
+ "Development Status :: 5 - Production/Stable",
12
+ "Environment :: MacOS X",
13
+ "Operating System :: MacOS",
14
+ "Programming Language :: Python :: 3",
15
+ "Topic :: Education",
16
+ ]
17
+ dependencies = [
18
+ "mcp>=2.3,<3",
19
+ "httpx>=0.27",
20
+ "starlette>=0.40",
21
+ "uvicorn>=0.30",
22
+ ]
23
+
24
+ [project.urls]
25
+ Repository = "https://github.com/EquineOntology/anki-voice"
26
+
27
+ [project.scripts]
28
+ anki-voice = "anki_voice.server:main"
29
+
30
+ [dependency-groups]
31
+ dev = [
32
+ "pytest>=8",
33
+ "anyio>=4",
34
+ ]
35
+
36
+ [build-system]
37
+ requires = ["hatchling"]
38
+ build-backend = "hatchling.build"
39
+
40
+ [tool.pytest.ini_options]
41
+ testpaths = ["tests"]
File without changes