@udondan/duolingo
v1.0.0
Published
Duolingo API client library and MCP server
Readme
@udondan/duolingo
A TypeScript package that provides both a Duolingo API client library and an MCP server for LLM agents (e.g. Claude).
Built natively in TypeScript against the unofficial Duolingo REST API — no third-party Duolingo library dependency.
Table of Contents
Getting Your JWT Token
Both the MCP server and the library require a Duolingo JWT token for authentication.
- Log in to duolingo.com in your browser.
- Open the browser developer console (F12 → Console tab).
- Run:
document.cookie.match(new RegExp('(^| )jwt_token=([^;]+)'))[0].slice(11) - Copy the output — that is your JWT token.
Note: JWT tokens expire. If you get authentication errors, repeat the steps above.
MCP Server
MCP Installation
Option A — run directly with npx (no install needed):
DUOLINGO_USERNAME=your_username DUOLINGO_JWT=your_jwt npx @udondan/duolingoOption B — install globally:
npm install -g @udondan/duolingo
DUOLINGO_USERNAME=your_username DUOLINGO_JWT=your_jwt duolingo-mcpOption C — clone and build from source:
git clone https://github.com/udondan/duolingo.git
cd duolingo
npm install && npm run build
node dist/server.jsClaude Desktop
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"duolingo": {
"command": "npx",
"args": ["-y", "@udondan/duolingo"],
"env": {
"DUOLINGO_USERNAME": "your_username",
"DUOLINGO_JWT": "your_jwt_token"
}
}
}
}Claude Code
claude mcp add duolingo -- npx -y @udondan/duolingoThen set the environment variables before starting Claude Code:
export DUOLINGO_USERNAME="your_username"
export DUOLINGO_JWT="your_jwt_token"Available Tools
All tools are read-only — the server never modifies your Duolingo account.
Account
| Tool | Description |
|------|-------------|
| duolingo_get_user_info | Profile: username, name, bio, location, avatar, followers, learning language |
| duolingo_get_settings | Notification and account settings |
| duolingo_get_streak_info | Current streak, longest streak, daily goal, extended today |
| duolingo_get_daily_xp_progress | XP goal, XP earned today, lessons completed today |
| duolingo_get_languages | Languages being learned (full names or abbreviations) |
| duolingo_get_courses | All courses including Math, Chess, and Music with XP per course |
| duolingo_get_friends | Users the authenticated user follows, with total XP |
| duolingo_get_calendar | Recent activity calendar for the current course (~last 2 weeks) |
| duolingo_get_leaderboard | Authenticated user's friends sorted by XP for week or month |
| duolingo_get_shop_items | Full shop catalogue with prices and item types |
| duolingo_get_health | Current hearts count, max hearts, refill timing |
| duolingo_get_currencies | Gem and lingot balances |
| duolingo_get_streak_goal | Current streak goal with upcoming checkpoints |
Language
| Tool | Description |
|------|-------------|
| duolingo_get_language_details | Level, points, streak for a specific language |
| duolingo_get_language_progress | Detailed progress: level %, points to next level, fluency |
| duolingo_get_known_topics | Learned topic/skill names |
| duolingo_get_unknown_topics | Not-yet-learned topics |
| duolingo_get_golden_topics | Fully mastered topics (strength = 1.0) |
| duolingo_get_reviewable_topics | Learned but not fully mastered topics |
| duolingo_get_known_words | Set of known words for a language |
| duolingo_get_learned_skills | Full skill objects sorted by learning order |
| duolingo_get_language_voices | Available TTS voice names for a language |
| duolingo_get_audio_url | Pronunciation audio URL for a word |
Utilities
| Tool | Description |
|------|-------------|
| duolingo_get_language_from_abbr | Convert language abbreviation to full name (e.g. fr → French) |
| duolingo_get_abbreviation_of | Convert full language name to abbreviation (e.g. French → fr) |
Library
Library Installation
npm install @udondan/duolingoQuick Start
import { DuolingoClient } from '@udondan/duolingo';
const client = new DuolingoClient('your_username', 'your_jwt_token');
// Get all courses including Math, Chess, and Music
const userData = await client.getUserData();
const userId = userData.id;
const v2 = await client.getUserDataV2(userId);
for (const course of v2.courses) {
console.log(`${course.subject}: ${course.xp} XP`);
}
// Get streak info
const streak = v2.streak;
const longestStreak = v2.streakData.longestStreak?.length;
// Get friends
const friends = await client.getFollowing(userId);
for (const friend of friends) {
console.log(`${friend.username}: ${friend.totalXp} XP`);
}
// Get shop items
const items = await client.getShopItems();
const streakFreeze = items.find(i => i.id === 'streak_freeze');
// Get hearts
const health = await client.getHealth();
console.log(`Hearts: ${health.hearts}/${health.maxHearts}`);API Reference
new DuolingoClient(username, jwt)
Creates a new client instance. Results are cached per instance.
const client = new DuolingoClient('your_username', 'your_jwt_token');User Data
// Legacy API — returns language_data with skills, calendar, etc.
// Required for: language details, topics, known words, learned skills
const userData = await client.getUserData(username?);
// 2023-05-23 API — returns all courses (language + math/chess/music),
// richer streak data, subscriber level, gems, health
const userId = userData.id;
const v2 = await client.getUserDataV2(userId);
// Resolve a username to a numeric user ID
const id = await client.getUserIdByUsername('someuser');Courses (all subjects)
const v2 = await client.getUserDataV2(userId);
// All courses
v2.courses; // DuolingoCourse[]
// Filter by subject
const langCourses = v2.courses.filter(c => c.subject === 'language');
const mathCourses = v2.courses.filter(c => c.subject === 'math');
const chessCourses = v2.courses.filter(c => c.subject === 'chess');
const musicCourses = v2.courses.filter(c => c.subject === 'music');Each course has: id, subject, topic, xp, fromLanguage.
Language courses also have: learningLanguage, title, authorId.
Daily XP Progress
const progress = await client.getUserDataById(userId, ['xpGoal', 'xpGains', 'streakData']);
// progress.xpGoal — daily XP goal
// progress.xpGains — array of { xp, skillId, time } for recent lessons
// progress.streakData.updatedTimestamp — last streak updateFriends & Social
// People this user follows (= friends)
const following = await client.getFollowing(userId);
// following[0].username, .totalXp, .userScore?.score (weekly XP), .isFollowedBy
// People who follow this user
const followers = await client.getFollowers(userId);Shop, Health & Currencies
// Full shop catalogue (read-only)
const items = await client.getShopItems();
// items[0].id, .name, .type, .price, .currencyType, .lastUsedDate
// Hearts / health (authenticated user only)
const health = await client.getHealth();
// health.hearts, .maxHearts, .eligibleForFreeRefill, .secondsUntilNextHeartSegment
// Gem and lingot balances (authenticated user only)
const { gems, lingots } = await client.getCurrencies();Streak Goals
// Current streak goal with checkpoints
const goal = await client.getStreakGoalCurrent();
// goal.hasActiveGoal, goal.streakGoal.lastCompleteGoal, .checkpoints, .nextSelectedGoal
// Available next goal options
const options = await client.getStreakGoalNextOptions();TTS Audio
// Discover available voices for a language
const voices = await client.getLanguageVoices('es'); // ['beaes', 'juniores', ...]
// Build an audio URL
const url = await client.buildAudioUrl('hola', 'es');
const urlWithVoice = await client.buildAudioUrl('hola', 'es', 'beaes');Error Handling
import {
DuolingoClient,
DuolingoAuthError,
DuolingoNotFoundError,
DuolingoCaptchaError,
DuolingoClientError,
} from '@udondan/duolingo';
try {
const data = await client.getUserData('someuser');
} catch (err) {
if (err instanceof DuolingoAuthError) {
// JWT expired — extract a new one from the browser
} else if (err instanceof DuolingoNotFoundError) {
// User does not exist
} else if (err instanceof DuolingoCaptchaError) {
// Duolingo blocked the request — try again later
} else if (err instanceof DuolingoClientError) {
// Other API error
}
}Development
git clone https://github.com/udondan/duolingo.git
cd duolingo
npm install
# Build
npm run build
# Run tests (unit + integration)
npm test
# Integration tests require credentials:
export DUOLINGO_USERNAME="your_username"
export DUOLINGO_JWT="your_jwt_token"
npm test
# Type-check without building
npm run typecheck
# Run MCP server in dev mode (no build step)
npm run devProject Structure
src/
├── index.ts # Library entry point — exports client, types, errors
├── server.ts # MCP server entry point (stdio transport)
├── client/
│ ├── duolingo.ts # DuolingoClient — all API methods
│ ├── types.ts # TypeScript interfaces for all API responses
│ └── errors.ts # Custom error classes
└── tools/
├── account.ts # Account tools (13)
├── language.ts # Language tools (11)
├── shop.ts # Utility tools (2)
└── helpers.ts # Shared utilities (error handling, Zod schemas)API Versions Used
| Endpoint | API Version | Used For |
|----------|-------------|----------|
| /users/<username> | Legacy | User profile, language data, skills, calendar |
| /2023-05-23/users/{id} | Current | Courses (all subjects), streak, health, gems |
| /2023-05-23/friends/users/{id}/following | Current | Friends, leaderboard |
| /2023-05-23/shop-items | Current | Shop catalogue |
| /users/{id}/streak-goal-current | Current | Streak goals |
| /2017-06-30/sessions | Current | TTS voice discovery |
License
MIT — see LICENSE
