@udondan/duolingo 1.0.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.
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Utility Duolingo MCP tools.
3
+ *
4
+ * Tools: get_language_from_abbr, get_abbreviation_of
5
+ */
6
+ import { z } from 'zod';
7
+ import { getClient } from '../client/duolingo.js';
8
+ import { handleError, UsernameFieldSchema } from './helpers.js';
9
+ export function registerShopTools(server) {
10
+ // -------------------------------------------------------------------------
11
+ // Get Language from Abbreviation
12
+ // -------------------------------------------------------------------------
13
+ server.registerTool('duolingo_get_language_from_abbr', {
14
+ title: 'Get Duolingo Language Name from Abbreviation',
15
+ description: 'Convert a language abbreviation to its full name. ' +
16
+ 'Only works for languages the given user is currently learning.',
17
+ inputSchema: {
18
+ language_abbr: z
19
+ .string()
20
+ .min(2)
21
+ .max(5)
22
+ .describe("Language abbreviation to look up (e.g. 'fr', 'es', 'de')."),
23
+ username: UsernameFieldSchema,
24
+ },
25
+ annotations: {
26
+ readOnlyHint: true,
27
+ destructiveHint: false,
28
+ idempotentHint: true,
29
+ openWorldHint: true,
30
+ },
31
+ }, async ({ language_abbr, username }) => {
32
+ try {
33
+ const client = getClient();
34
+ let userId;
35
+ if (!username) {
36
+ const userData = await client.getUserData();
37
+ userId = userData.id;
38
+ }
39
+ else {
40
+ userId = await client.getUserIdByUsername(username);
41
+ }
42
+ const v2 = await client.getUserDataV2(userId);
43
+ const course = v2.courses.find((c) => c.subject === 'language' &&
44
+ (c.learningLanguage === language_abbr || c.topic === language_abbr));
45
+ if (!course) {
46
+ return {
47
+ content: [
48
+ {
49
+ type: 'text',
50
+ text: `No language found for abbreviation '${language_abbr}'. ` +
51
+ 'Make sure the user is learning this language.',
52
+ },
53
+ ],
54
+ };
55
+ }
56
+ return {
57
+ content: [
58
+ {
59
+ type: 'text',
60
+ text: course.title ?? course.learningLanguage ?? language_abbr,
61
+ },
62
+ ],
63
+ };
64
+ }
65
+ catch (err) {
66
+ return { content: [{ type: 'text', text: handleError(err) }] };
67
+ }
68
+ });
69
+ // -------------------------------------------------------------------------
70
+ // Get Abbreviation Of
71
+ // -------------------------------------------------------------------------
72
+ server.registerTool('duolingo_get_abbreviation_of', {
73
+ title: 'Get Duolingo Language Abbreviation',
74
+ description: 'Convert a full language name to its abbreviation. ' +
75
+ 'Only works for languages the given user is currently learning.',
76
+ inputSchema: {
77
+ language_name: z
78
+ .string()
79
+ .min(1)
80
+ .describe("Full language name to look up (e.g. 'French', 'Spanish')."),
81
+ username: UsernameFieldSchema,
82
+ },
83
+ annotations: {
84
+ readOnlyHint: true,
85
+ destructiveHint: false,
86
+ idempotentHint: true,
87
+ openWorldHint: true,
88
+ },
89
+ }, async ({ language_name, username }) => {
90
+ try {
91
+ const client = getClient();
92
+ let userId;
93
+ if (!username) {
94
+ const userData = await client.getUserData();
95
+ userId = userData.id;
96
+ }
97
+ else {
98
+ userId = await client.getUserIdByUsername(username);
99
+ }
100
+ const v2 = await client.getUserDataV2(userId);
101
+ const course = v2.courses.find((c) => c.subject === 'language' &&
102
+ (c.title ?? '').toLowerCase() === language_name.toLowerCase());
103
+ if (!course) {
104
+ return {
105
+ content: [
106
+ {
107
+ type: 'text',
108
+ text: `No abbreviation found for language '${language_name}'. ` +
109
+ 'Make sure the user is learning this language.',
110
+ },
111
+ ],
112
+ };
113
+ }
114
+ return {
115
+ content: [
116
+ { type: 'text', text: course.learningLanguage ?? course.topic },
117
+ ],
118
+ };
119
+ }
120
+ catch (err) {
121
+ return { content: [{ type: 'text', text: handleError(err) }] };
122
+ }
123
+ });
124
+ }
125
+ //# sourceMappingURL=shop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shop.js","sourceRoot":"","sources":["../../src/tools/shop.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@udondan/duolingo",
3
+ "version": "1.0.0",
4
+ "description": "Duolingo API client library and MCP server",
5
+ "author": {
6
+ "name": "Daniel Schroeder",
7
+ "url": "https://www.udondan.com/"
8
+ },
9
+ "funding": [
10
+ {
11
+ "type": "github",
12
+ "url": "https://github.com/sponsors/udondan"
13
+ }
14
+ ],
15
+ "homepage": "https://github.com/udondan/duolingo",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/udondan/duolingo.git"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/udondan/duolingo/issues"
22
+ },
23
+ "keywords": [
24
+ "duolingo",
25
+ "api",
26
+ "mcp"
27
+ ],
28
+ "type": "module",
29
+ "main": "dist/index.js",
30
+ "types": "dist/index.d.ts",
31
+ "bin": {
32
+ "duolingo-mcp": "dist/server.js"
33
+ },
34
+ "exports": {
35
+ ".": {
36
+ "import": "./dist/index.js",
37
+ "types": "./dist/index.d.ts"
38
+ },
39
+ "./server": {
40
+ "import": "./dist/server.js"
41
+ }
42
+ },
43
+ "files": [
44
+ "dist/**/*.js",
45
+ "dist/**/*.d.ts",
46
+ "dist/**/*.d.ts.map",
47
+ "dist/**/*.js.map",
48
+ "README.md",
49
+ "LICENSE"
50
+ ],
51
+ "scripts": {
52
+ "build": "tsc",
53
+ "start": "node dist/server.js",
54
+ "dev": "tsx src/server.ts",
55
+ "test": "vitest run",
56
+ "test:watch": "vitest",
57
+ "test:coverage": "vitest run --coverage",
58
+ "typecheck": "tsc --noEmit",
59
+ "lint": "eslint .",
60
+ "lint:fix": "eslint . --fix",
61
+ "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
62
+ "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\""
63
+ },
64
+ "dependencies": {
65
+ "@modelcontextprotocol/sdk": "^1.10.2",
66
+ "axios": "^1.7.9",
67
+ "zod": "^3.24.2"
68
+ },
69
+ "devDependencies": {
70
+ "@types/node": "^22.10.7",
71
+ "@vitest/coverage-v8": "^3.1.1",
72
+ "axios-mock-adapter": "^2.1.0",
73
+ "eslint": "^10.2.0",
74
+ "eslint-config-prettier": "^10.1.8",
75
+ "eslint-plugin-prettier": "^5.5.5",
76
+ "prettier": "^3.8.1",
77
+ "tsx": "^4.19.2",
78
+ "typescript": "^5.7.3",
79
+ "typescript-eslint": "^8.58.0",
80
+ "vitest": "^3.1.1"
81
+ },
82
+ "engines": {
83
+ "node": ">=18"
84
+ }
85
+ }