codewars-mcp 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.
- package/README.md +24 -0
- package/dist/api.d.ts +21 -0
- package/dist/api.js +63 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +50 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Codewars MCP
|
|
2
|
+
|
|
3
|
+
Keyless MCP server for Codewars: user ranks, honor, kata.
|
|
4
|
+
|
|
5
|
+
Pairs well with `leetcode-mcp`, `codeforces-mcp`, and `github-intel-mcp`.
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
npm run build
|
|
12
|
+
node dist/index.js
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The server uses stdio, so it can be connected to Claude Desktop, Cursor, VS Code, MCP Inspector, or another compatible MCP client.
|
|
16
|
+
|
|
17
|
+
## Tools at a glance
|
|
18
|
+
|
|
19
|
+
- `get_user`: Honor, rank, clan, languages.
|
|
20
|
+
- `get_kata`: Kata by id or slug.
|
|
21
|
+
|
|
22
|
+
## Limits and privacy
|
|
23
|
+
|
|
24
|
+
This project is intentionally narrow. It should be treated as a practical helper, not a complete certification or security audit. Check the implementation and the returned data before using it with sensitive material. No credentials are required.
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare class CodewarsError extends Error {
|
|
2
|
+
}
|
|
3
|
+
export interface Codewarrior {
|
|
4
|
+
username: string;
|
|
5
|
+
honor?: number;
|
|
6
|
+
clan?: string;
|
|
7
|
+
leaderboard?: number;
|
|
8
|
+
overall?: string;
|
|
9
|
+
totalCompleted?: number;
|
|
10
|
+
languages: string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function getUser(username: string): Promise<Codewarrior>;
|
|
13
|
+
export interface Kata {
|
|
14
|
+
id?: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
rank?: string;
|
|
17
|
+
tags: string[];
|
|
18
|
+
}
|
|
19
|
+
export declare function getKata(idOrSlug: string): Promise<Kata>;
|
|
20
|
+
export declare function formatUser(u: Codewarrior): string;
|
|
21
|
+
export declare function formatKata(k: Kata): string;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codewars API v1 client, keyless.
|
|
3
|
+
* Docs: https://dev.codewars.com/
|
|
4
|
+
*/
|
|
5
|
+
const BASE = "https://www.codewars.com/api/v1";
|
|
6
|
+
export class CodewarsError extends Error {
|
|
7
|
+
}
|
|
8
|
+
async function getJson(path) {
|
|
9
|
+
const res = await fetch(`${BASE}${path}`, {
|
|
10
|
+
headers: { "User-Agent": "codewars-mcp/1.0", Accept: "application/json" },
|
|
11
|
+
signal: AbortSignal.timeout(20000),
|
|
12
|
+
});
|
|
13
|
+
if (res.status === 404)
|
|
14
|
+
throw new CodewarsError("Not found on Codewars.");
|
|
15
|
+
if (!res.ok)
|
|
16
|
+
throw new CodewarsError(`Codewars error ${res.status}`);
|
|
17
|
+
return (await res.json());
|
|
18
|
+
}
|
|
19
|
+
export async function getUser(username) {
|
|
20
|
+
if (!/^[\w-]+$/.test(username.trim()))
|
|
21
|
+
throw new CodewarsError(`Not a username: "${username}".`);
|
|
22
|
+
const u = await getJson(`/users/${encodeURIComponent(username.trim())}`);
|
|
23
|
+
const ranks = (u.ranks ?? {});
|
|
24
|
+
const overall = (ranks.overall ?? {});
|
|
25
|
+
const langs = (ranks.languages ?? {});
|
|
26
|
+
const completed = (u.codeChallenges ?? {});
|
|
27
|
+
return {
|
|
28
|
+
username: String(u.username ?? username),
|
|
29
|
+
honor: u.honor,
|
|
30
|
+
clan: u.clan && u.clan !== "None" ? String(u.clan) : undefined,
|
|
31
|
+
leaderboard: u.leaderboardPosition,
|
|
32
|
+
overall: overall.name,
|
|
33
|
+
totalCompleted: completed.totalCompleted,
|
|
34
|
+
languages: Object.entries(langs)
|
|
35
|
+
.map(([lang, r]) => `${lang}(${r.name ?? "?"})`)
|
|
36
|
+
.slice(0, 8),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export async function getKata(idOrSlug) {
|
|
40
|
+
if (!/^[\w-]+$/.test(idOrSlug.trim()))
|
|
41
|
+
throw new CodewarsError(`Not a kata id or slug: "${idOrSlug}".`);
|
|
42
|
+
const k = await getJson(`/code-challenges/${encodeURIComponent(idOrSlug.trim().toLowerCase())}`);
|
|
43
|
+
return {
|
|
44
|
+
id: k.id ?? k.slug,
|
|
45
|
+
name: k.name,
|
|
46
|
+
rank: k.rank?.name ?? k.rank,
|
|
47
|
+
tags: Array.isArray(k.tags) ? k.tags.map(String).slice(0, 6) : [],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function formatUser(u) {
|
|
51
|
+
const lines = [
|
|
52
|
+
`${u.username}${u.overall ? ` (${u.overall})` : ""}`,
|
|
53
|
+
u.honor !== undefined ? `Honor: ${u.honor.toLocaleString()}` : "",
|
|
54
|
+
u.leaderboard ? `Leaderboard: #${u.leaderboard}` : "",
|
|
55
|
+
u.clan ? `Clan: ${u.clan}` : "",
|
|
56
|
+
u.totalCompleted !== undefined ? `Completed kata: ${u.totalCompleted}` : "",
|
|
57
|
+
u.languages.length ? `Languages: ${u.languages.join(", ")}` : "",
|
|
58
|
+
].filter(Boolean);
|
|
59
|
+
return lines.join("\n");
|
|
60
|
+
}
|
|
61
|
+
export function formatKata(k) {
|
|
62
|
+
return `[${k.id ?? "?"}] ${k.name ?? "(unnamed)"}${k.rank ? ` (${k.rank})` : ""}${k.tags.length ? `\nTags: ${k.tags.join(", ")}` : ""}`;
|
|
63
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { createServer } from "./server.js";
|
|
3
|
+
async function main() {
|
|
4
|
+
const server = createServer();
|
|
5
|
+
const transport = new StdioServerTransport();
|
|
6
|
+
await server.connect(transport);
|
|
7
|
+
console.error("MCP server running on stdio");
|
|
8
|
+
}
|
|
9
|
+
main().catch((err) => {
|
|
10
|
+
console.error("Fatal error:", err);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { CodewarsError, formatKata, formatUser, getKata, getUser } from "./api.js";
|
|
4
|
+
const text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
5
|
+
const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
|
|
6
|
+
const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
|
|
7
|
+
export function createServer() {
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: "codewars-mcp",
|
|
10
|
+
version: "1.0.0",
|
|
11
|
+
});
|
|
12
|
+
server.registerTool("get_user", {
|
|
13
|
+
title: "Get user",
|
|
14
|
+
description: "A Codewars user: honor, rank, clan, languages, completed kata.",
|
|
15
|
+
inputSchema: z.object({
|
|
16
|
+
username: z.string().describe("Codewars username"),
|
|
17
|
+
}),
|
|
18
|
+
annotations: READ_ONLY,
|
|
19
|
+
}, async ({ username }) => {
|
|
20
|
+
try {
|
|
21
|
+
return text(formatUser(await getUser(username)));
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
return textError(errorMessage(e));
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
server.registerTool("get_kata", {
|
|
28
|
+
title: "Get kata",
|
|
29
|
+
description: "One kata by id or slug: name, rank, tags.",
|
|
30
|
+
inputSchema: z.object({
|
|
31
|
+
id_or_slug: z.string().describe("Kata id or slug, e.g. 'valid-parentheses'"),
|
|
32
|
+
}),
|
|
33
|
+
annotations: READ_ONLY,
|
|
34
|
+
}, async ({ id_or_slug }) => {
|
|
35
|
+
try {
|
|
36
|
+
return text(formatKata(await getKata(id_or_slug)));
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
return textError(errorMessage(e));
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
return server;
|
|
43
|
+
}
|
|
44
|
+
function errorMessage(e) {
|
|
45
|
+
if (e instanceof CodewarsError)
|
|
46
|
+
return `Error: ${e.message}`;
|
|
47
|
+
if (e instanceof Error)
|
|
48
|
+
return `Error: ${e.message}`;
|
|
49
|
+
return `Error: ${String(e)}`;
|
|
50
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codewars-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Keyless MCP server for Codewars: user ranks, honor, kata.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"mcpName": "io.github.mrfentmen/codewars-mcp",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/mrfentmen/awesome-mcps.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"codewars-mcp": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"start": "node dist/index.js",
|
|
21
|
+
"dev": "rm -rf dist && tsc -p tsconfig.json && node dist/index.js",
|
|
22
|
+
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"mcp",
|
|
26
|
+
"codewars",
|
|
27
|
+
"coding",
|
|
28
|
+
"api"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
33
|
+
"zod": "^3.23.8"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.0.0",
|
|
37
|
+
"typescript": "^5.6.0"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=20"
|
|
41
|
+
}
|
|
42
|
+
}
|