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 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
+ }
@@ -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
+ });
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function createServer(): McpServer;
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
+ }