code-tutor-narrator
MCP server that lets AI agents drive VS Code as a coding tutor, highlighting ranges of code, narrating explanations aloud via TTS, and running scripted walkthroughs.
README
Code Tutor Narrator
⚠️ Vibe-coded by an AI, start to finish
Every line of this extension was written by Claude (Claude Code, Opus 5), across a handful of conversational sessions. I directed it and listened to the output; I did not write it, and I have not reviewed most of it line by line.
What that means for you:
- Treat it as a prototype, not as engineering you can lean on. It works for me, on my machine, for the thing I use it for.
- It spawns child processes, reads your settings, and sends the text you narrate to a third-party API. Read the source before you run it — it's ~1000 lines, and that is genuinely the whole review that has happened.
- The tests are real and they pass, but they were written by the same AI that wrote the code, so they encode the same assumptions and blind spots.
- Design decisions in here were made mid-conversation and may be wrong in ways neither of us noticed.
Issues and PRs are welcome, but no support is promised. If you need something dependable, this isn't it.
Lets an agent drive VS Code like a tutor: scroll to a range of code, highlight it, dim everything else, and narrate it out loud — then read back what you're looking at.
Requires macOS: speech goes through say and playback through afplay.
It exposes an MCP server over loopback HTTP, so Claude Code calls it as ordinary tools rather than shelling out.
Tools
| Tool | What it does |
|---|---|
show_code |
Scrolls to file + start_line–end_line, highlights it, pins an optional note beside it, optionally speaks say. Returns the numbered source lines. |
walkthrough |
A planned sequence of beats, played back-to-back with no pauses. Each beat takes the same fields as show_code. |
speak |
Says something aloud without moving the editor. Interrupts narration in progress. |
editor_state |
Active file, cursor line, current selection, visible line range, and any errors. |
clear_highlight |
Drops the highlight and stops narration. |
show_code blocks until narration finishes, so consecutive calls pace themselves
into a walkthrough instead of talking over each other.
show_code or walkthrough?
Chained show_code calls leave an audible gap between beats, and the gap is not TTS
latency — it is the agent's round-trip to compose the next call. Nothing on this side
can shrink it.
walkthrough removes it by taking the whole sequence at once and synthesising beat
N+1 while N is still speaking, so playback is continuous. The trade is adaptivity: you
commit to all the narration before reading any of the code back.
walkthrough— the tour you have already planned, an overview of a file you've read, anything scripted.show_code— you need the returned source of one beat before deciding the next, the user is likely to interrupt, or you're following their cursor.
Mixing them is normal: walkthrough a section, then drop to show_code for a part
worth pausing on. Interrupting a walkthrough stops it and reports which beat it
reached.
Driving it from an agent
Claude Code should enter through the tutor skill rather than calling these tools
cold — the skill carries the pedagogy; this section covers the mechanics.
Sequence the calls. show_code blocks. Issue one, wait, issue the next. Batching
them into a single parallel block defeats the pacing and produces overlapping audio.
When the beats are already planned, use walkthrough instead of chaining — same
result, no gaps.
Open with editor_state. It reports the active file, cursor line, selection,
visible range, and current diagnostics. If something is selected or the cursor is
parked, start there instead of at line 1.
One idea per call, 5–20 lines. A whole file in one show_code is a wall of audio
with a highlight that means nothing. Use note as the anchor that survives after the
audio has passed — a short label like "the move happens here", not a summary.
Jumping is cheap. Following the difficulty — to a definition and back — is what a human tutor does. Line order is not an obligation.
Speak or write, never both. If narration is playing, don't also paste the same explanation into chat. Written text is the fallback when audio is unavailable, not an accompaniment.
Always clear_highlight at the end, so the editor is handed back clean.
Failure modes. ECONNREFUSED means no VS Code window is open — the server lives in
the extension host and there is nothing to reconnect to. Say so and stop; do not
silently fall back to pasting the walkthrough into chat, and do not retry. A 402 is
voice plan-gating, not a code problem. See Troubleshooting for the rest.
Diagnostics are advisory. The errors array is whatever the language server
currently believes, which on a file full of stubs can be stale or misleading. Don't
narrate them as fact without reading the code.
Install
npm install
npm run package # builds code-tutor-narrator-<version>.vsix
npm run install-ext # installs it into VS Code
Then reload VS Code (Developer: Reload Window) and register it once with Claude Code:
claude mcp add --transport http tutor http://127.0.0.1:51730/mcp --scope user
The server only exists while VS Code is open. If Claude Code reports the tutor
server as unreachable, that just means no window is running.
Raise the tool timeout. walkthrough holds its response open for as long as the
narration lasts, and an HTTP MCP server gets only 60 seconds per request by default —
so a tour of any length reports The operation timed out while the audio carries on
playing. Progress notifications do not extend that window; only config does. Add a
timeout to the server entry in ~/.claude.json:
"tutor": {
"type": "http",
"url": "http://127.0.0.1:51730/mcp",
"timeout": 600000
}
Restart Claude Code afterwards — the value is read at connect. Note the separate
five-minute idle timeout for HTTP servers: a single walkthrough that narrates for
longer than that aborts regardless, so keep one call under ~5 minutes of speech, or
raise CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT.
Choosing a voice
codeTutor.tts.provider picks the backend. The built-in macOS voices are the
default because they always work, but they are the weakest option.
| Provider | Quality | Latency | Cost | Notes |
|---|---|---|---|---|
say |
Poor → decent | Instant | Free | Decent only with Premium voices installed — see below. Offline. |
elevenlabs |
Best | ~75ms (flash) | ~$0.05 / 1k chars | Needs an API key. Audio is cached, so replays are free. |
command |
Whatever you point it at | Depends | Usually free | Escape hatch for local models (Kokoro, Piper) or another API. |
Free first: install better macOS voices
The stock Samantha is a compact voice, and it sounds like 2005. macOS ships far
better ones, they're just not downloaded by default:
System Settings → Accessibility → Spoken Content → System Voice → Manage Voices
Grab an Enhanced or Premium English voice (Ava, Zoe, and Evan are the good
ones), then set codeTutor.voice to its exact name. Confirm with say -v '?'.
This costs nothing and closes most of the gap.
ElevenLabs
"codeTutor.tts.provider": "elevenlabs",
"codeTutor.elevenlabs.voiceId": "<see below>",
"codeTutor.elevenlabs.modelId": "eleven_flash_v2_5"
Voice ids are plan-gated. Voice Library (community) voices are paid-only via the
API. On a free plan they play fine in the web UI and then fail here with
402 paid_plan_required: Free users cannot use library voices via the API — so copying
an id out of the library is the one obvious move that doesn't work. Either use a default
voice, or add a library voice to your workspace, which mints a new id you own. List what
your key can actually reach:
curl -s -H "xi-api-key: $KEY" https://api.elevenlabs.io/v1/voices \
| jq -r '.voices[] | "\(.category)\t\(.voice_id)\t\(.name)"'
Anything premade works on the free plan. Buying pay-as-you-go credits does not
lift this: PAYG is a balance, not a tier, and plan-level restrictions come from the
subscription. Starter ($6/mo) is the cheapest plan that unlocks library voices.
Store the key with ⌘⇧P → "Code Tutor: Set ElevenLabs API Key". It goes into the
OS login keychain via VS Code's SecretStorage — deliberately not into
settings.json, which is easy to commit by accident. ELEVENLABS_API_KEY is also
honoured as a fallback.
eleven_flash_v2_5 is the right default here: ~75ms and half the price of
eleven_multilingual_v2, and narration doesn't need the extra expressiveness.
Speaking faster
There are two speed knobs and they are not interchangeable.
codeTutor.elevenlabs.speed changes how the voice is synthesised, and the API caps
it at 1.2 — 1.25 is a 400, not a clamp, so narration fails outright.
codeTutor.playbackRate speeds up the audio after it arrives, via afplay -r with
the high-quality algorithm, so pitch is preserved rather than chipmunked. It has no
1.2 ceiling, costs nothing, and doesn't invalidate the cache — playback speed isn't
part of the cache key, so you can re-tune it as often as you like without paying for
another synthesis. It's the right knob for genuinely fast narration:
"codeTutor.playbackRate": 1.5
The two stack, so 1.2 synthesis × 1.5 playback ≈ 1.8× overall. Past roughly 2× the articulation starts to blur, but that's taste rather than a limit.
Budget roughly: continuous speech runs ~800 characters/minute, so a solid hour of talking is around $2.50 — realistically closer to $1/hour once you account for pauses and questions. Synthesised audio is cached by content hash, so re-walking the same code is free.
Local models (free, offline, private)
Point command at anything that reads text on stdin and plays it:
"codeTutor.tts.provider": "command",
"codeTutor.tts.command": "kokoro-tts - --stdout | afplay -"
Kokoro-82M is the sweet spot on Apple Silicon — Apache-licensed, runs faster than
real time on CPU, ~100ms, and clearly better than say. It's still short of
ElevenLabs on expressiveness, which matters less for steady narration than it
would for character work.
Settings
| Setting | Default | Meaning |
|---|---|---|
codeTutor.port |
51730 |
Loopback port for the MCP server. |
codeTutor.tts.provider |
say |
say, elevenlabs, or command. |
codeTutor.tts.command |
— | Command for the command provider; text arrives on stdin. |
codeTutor.voice |
Samantha |
macOS say voice. say only — ElevenLabs uses elevenlabs.voiceId. say -v '?' lists them. |
codeTutor.rate |
175 |
Words per minute. say only — silently ignored by the other providers. |
codeTutor.elevenlabs.voiceId |
Rachel | Voice id. Plan-gated — see above. Only applies when the provider is elevenlabs; the say provider uses codeTutor.voice. |
codeTutor.elevenlabs.modelId |
eleven_flash_v2_5 |
Model id. |
codeTutor.elevenlabs.baseUrl |
https://api.elevenlabs.io |
Override for a proxy or a test stub. |
codeTutor.elevenlabs.stability |
0.5 |
0 is expressive and variable, 1 is flat. |
codeTutor.elevenlabs.similarityBoost |
0.75 |
How closely to match the reference voice. |
codeTutor.elevenlabs.speed |
1 |
Synthesis speed. Must be 0.7–1.2; outside that the API returns 400 and nothing is spoken. |
codeTutor.playbackRate |
1 |
Local playback speed, applied after synthesis. No 1.2 ceiling — this is the knob for fast narration. |
codeTutor.cacheAudio |
true |
Cache synthesised audio so replays are free. |
Blank values fall back to the default rather than being sent as empty, so clearing a setting is the same as unsetting it.
Troubleshooting
| Symptom | Cause |
|---|---|
connect ECONNREFUSED 127.0.0.1:51730 |
No VS Code window. The server lives inside the extension host; nothing to reconnect to. |
walkthrough returns "The operation timed out" |
The call outlived the MCP request window, but the narration is still playing. Never retry — you'd double-trigger audio. Raise the per-server timeout; see Install. |
| A new tool doesn't appear after installing | The MCP client caches tools/list from when it connected. Reloading VS Code is not enough; reconnect the server (/mcp in Claude Code). |
402 paid_plan_required |
Library voice on a free plan. See Choosing a voice. |
401 from ElevenLabs |
No key stored. Run Code Tutor: Set ElevenLabs API Key, or export ELEVENLABS_API_KEY. |
| A settings change does nothing | Stale build. Config is read per call, so if a new setting is ignored the installed VSIX predates it. |
| Speech speed setting does nothing | codeTutor.rate is say-only. On ElevenLabs the knob is codeTutor.elevenlabs.speed, clamped by the API to ~0.7–1.2. |
stability / similarityBoost change does nothing |
Cache hit. The key covers provider, voice, model, and speed — but not those two. Set codeTutor.cacheAudio to false while tuning them. |
Silence, no error, command provider |
The command is responsible for playing, not just synthesising. It needs to end in something like afplay -. |
npm run install-ext installs alongside older versions rather than replacing them —
VS Code runs the highest. To see what is actually installed:
ls -d ~/.vscode/extensions/*code-tutor*
Reload the window (Developer: Reload Window) after installing; the extension host does not pick up a new build on its own. Code Tutor: Show server status reports the port and active provider, and the Code Tutor output channel logs each request.
Development
npm run watch # recompile on change
npm test # MCP transport tests + TTS backend tests
src/mcp.ts and src/tts.ts deliberately have no vscode import, which is what
makes them testable outside the extension host — the ElevenLabs client is verified
against a local stub server, so the request shape is covered without a real key or
a single billed character. Everything that touches the editor lives in
src/extension.ts and is covered by the type checker instead.
Without an agent
A cue file is also watched, so you can drive it from a terminal. bin/narrate is a
small bash wrapper around that:
./bin/narrate --file src/main.rs --lines 12-20 --note "the borrow" \
--say "Here we take a reference instead of moving the value."
./bin/narrate --clear
It writes to ~/.claude/tutor/cue.json and speaks with say, configurable through
TUTOR_VOICE, TUTOR_RATE, TUTOR_TTS_CMD, and TUTOR_MUTE. It does not go through
the MCP server at all, so it works without Claude Code.
The agent side
skill/SKILL.md is the Claude Code skill that
teaches an agent how to tutor with these tools — when to narrate, how long a beat should
be, when to stop and ask. Copy it into your skills directory to use it:
mkdir -p ~/.claude/skills/tutor && cp skill/SKILL.md ~/.claude/skills/tutor/
The extension is usable without it; you'll just be explaining the tools to your agent yourself each time.
License
MIT — see LICENSE.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。