myclaw-toolkit 1.0.9 → 1.0.11

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 CHANGED
@@ -4,8 +4,24 @@
4
4
  [![npm](https://img.shields.io/npm/v/myclaw-toolkit)](https://www.npmjs.com/package/myclaw-toolkit)
5
5
  [![npm downloads](https://img.shields.io/npm/dw/myclaw-toolkit)](https://www.npmjs.com/package/myclaw-toolkit)
6
6
  [![license](https://img.shields.io/npm/l/myclaw-toolkit)](./LICENSE)
7
+ [![tests](https://github.com/Dusheh/myclaw-toolkit/actions/workflows/test.yml/badge.svg)](https://github.com/Dusheh/myclaw-toolkit/actions/workflows/test.yml)
7
8
 
8
- **24-in-1 developer utility toolkit as an MCP server.** Search the web, convert currencies, check crypto prices, generate QR codes, format JSON, and more — all from any MCP-compatible AI assistant.
9
+ **24-in-1 developer utility toolkit as an MCP server — privacy-first.** Search the web, convert currencies, check crypto prices, generate QR codes, format JSON, and more — all from any MCP-compatible AI assistant.
10
+
11
+ ## 🔒 Privacy-First Design
12
+
13
+ Most tools run **100% locally on your machine** — no data ever leaves your device:
14
+
15
+ | Local tools (zero network) | Tools requiring remote API |
16
+ |---|---|
17
+ | timestamp, uuid, base64, hash | web_search, news_search |
18
+ | qrcode, wifi_qrcode | product_search, exchange_rate |
19
+ | color_tools, json_formatter | crypto_price, domain_check |
20
+ | url_tools, text_tools | rss_feed, read_page |
21
+ | bmi_calculator, vcard_generator | ai_translate, quote, compare |
22
+ | markdown_to_html | |
23
+
24
+ WiFi passwords, JSON data, text content, hashes, and Markdown documents are all processed entirely on-device. Only tools that genuinely need external data (search, exchange rates, etc.) call the remote API.
9
25
 
10
26
  ## Quick Install
11
27
 
@@ -21,7 +37,7 @@ Or add to your AI client:
21
37
  "mcpServers": {
22
38
  "myclaw-toolkit": {
23
39
  "command": "npx",
24
- "args": ["myclaw-toolkit"]
40
+ "args": ["-y", "myclaw-toolkit"]
25
41
  }
26
42
  }
27
43
  }
@@ -34,7 +50,7 @@ claude mcp add myclaw-toolkit -- npx myclaw-toolkit
34
50
 
35
51
  ## Tools
36
52
 
37
- ### Utility (Free)
53
+ ### Utility (Local)
38
54
  | Tool | Description |
39
55
  |------|-------------|
40
56
  | `timestamp` | Current Unix timestamp & ISO 8601 |
@@ -47,16 +63,16 @@ claude mcp add myclaw-toolkit -- npx myclaw-toolkit
47
63
  | `url_tools` | URL encode/decode |
48
64
  | `text_tools` | Text count, reverse, case |
49
65
 
50
- ### Data (Free)
66
+ ### Data
51
67
  | Tool | Description |
52
68
  |------|-------------|
53
- | `exchange_rate` | Real-time currency rates |
54
- | `crypto_price` | Cryptocurrency prices |
55
- | `domain_check` | Domain whois & availability |
56
- | `bmi_calculator` | BMI calculator |
57
- | `vcard_generator` | vCard (.vcf) generator |
58
- | `compare` | Side-by-side comparison |
59
- | `quote` | Random inspirational quotes |
69
+ | `exchange_rate` | Real-time currency rates (API) |
70
+ | `crypto_price` | Cryptocurrency prices (API) |
71
+ | `domain_check` | Domain whois & availability (API) |
72
+ | `bmi_calculator` | BMI calculator (local) |
73
+ | `vcard_generator` | vCard (.vcf) generator (local) |
74
+ | `compare` | Side-by-side comparison (API) |
75
+ | `quote` | Random inspirational quotes (API) |
60
76
 
61
77
  ### Search & Content
62
78
  | Tool | Description |
@@ -70,9 +86,9 @@ claude mcp add myclaw-toolkit -- npx myclaw-toolkit
70
86
  ### Processing
71
87
  | Tool | Description |
72
88
  |------|-------------|
73
- | `markdown_to_html` | Markdown → HTML |
74
- | `wifi_qrcode` | WiFi QR code generator |
75
- | `ai_translate` | AI translation |
89
+ | `markdown_to_html` | Markdown → HTML (local) |
90
+ | `wifi_qrcode` | WiFi QR code generator (local — password never sent) |
91
+ | `ai_translate` | AI translation via MyMemory API |
76
92
 
77
93
  ### Health
78
94
  | Tool | Description |
@@ -83,10 +99,14 @@ claude mcp add myclaw-toolkit -- npx myclaw-toolkit
83
99
 
84
100
  Listed on these MCP registries (help others find the toolkit):
85
101
 
86
- - [Glama](https://glama.ai/mcp/servers/Dusheh/myclaw-toolkit) — badge above
87
- - [mcp.so](https://mcp.so) — submitted via form
88
- - [Smithery](https://smithery.ai) — submitted via form
89
- - [awesome-mcp-servers](https://github.com/punkpeye/awesome-mcp-servers) — PR pending
102
+ - [Glama](https://glama.ai/mcp/servers/Dusheh/myclaw-toolkit) — auto-indexed
103
+ - [MCPFind](https://www.mcpfind.org/) — PR pending
104
+ - [mcp.so](https://mcp.so) — submitted
105
+ - [awesome-mcp-servers (punkpeye)](https://github.com/punkpeye/awesome-mcp-servers) — PR pending
106
+ - [awesome-mcp-servers (appcypher)](https://github.com/appcypher/awesome-mcp-servers) — PR pending
107
+ - [Awesome MCP List](https://github.com/MobinX/awesome-mcp-list) — PR pending
108
+ - [MCP.Directory](https://mcp.directory) — submitted
109
+ - [Official MCP Registry](https://registry.modelcontextprotocol.io) — published, awaiting indexing
90
110
 
91
111
  ## Environment Variables
92
112
 
@@ -98,11 +118,14 @@ Listed on these MCP registries (help others find the toolkit):
98
118
 
99
119
  ```bash
100
120
  git clone https://github.com/Dusheh/myclaw-toolkit
101
- cd toolkit
121
+ cd myclaw-toolkit
102
122
  npm install
103
- npm run dev
123
+ npm run dev # Run with tsx
124
+ npm run build # Compile TypeScript
125
+ npm test # Run 24 unit tests
126
+ npm run lint # ESLint check
104
127
  ```
105
128
 
106
129
  ## License
107
130
 
108
- MIT — [MyClaw](https://myclaw.dev)
131
+ MIT
package/dist/index.js CHANGED
@@ -19,6 +19,18 @@ import { marked } from "marked";
19
19
  // ── Version (read from package.json, never hardcoded) ─────────────
20
20
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
21
21
  const VERSION = pkg.version;
22
+ // ── Rate Limiter (protects backend from runaway AI clients) ─────────
23
+ const RATE_WINDOW_MS = 60_000; // 1 minute window
24
+ const MAX_REQUESTS_PER_WINDOW = 100;
25
+ let requestTimestamps = [];
26
+ function checkRateLimit() {
27
+ const now = Date.now();
28
+ requestTimestamps = requestTimestamps.filter(t => now - t < RATE_WINDOW_MS);
29
+ if (requestTimestamps.length >= MAX_REQUESTS_PER_WINDOW)
30
+ return false;
31
+ requestTimestamps.push(now);
32
+ return true;
33
+ }
22
34
  // ── Config ──────────────────────────────────────────────────────────
23
35
  const API_BASE = process.env.MYCLAW_API || "http://47.103.7.241";
24
36
  const USER_AGENT = `myclaw-toolkit-mcp/${VERSION}`;
@@ -28,6 +40,9 @@ const FETCH_TIMEOUT_MS = 15_000;
28
40
  * Only used for tools that genuinely need external data.
29
41
  */
30
42
  async function apiCall(path) {
43
+ if (!checkRateLimit()) {
44
+ return JSON.stringify({ error: "Rate limit exceeded", limit: `${MAX_REQUESTS_PER_WINDOW} requests per ${RATE_WINDOW_MS / 1000}s`, path });
45
+ }
31
46
  const controller = new AbortController();
32
47
  const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
33
48
  try {
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Basic tests for local tool implementations.
4
+ * Run: npx vitest run
5
+ */
6
+ import { describe, it, expect } from "vitest";
7
+ import crypto from "node:crypto";
8
+ // ═══════════════════════════════════════════════════════════════════
9
+ // Utility functions (replicated from index.ts for isolated testing)
10
+ // ═══════════════════════════════════════════════════════════════════
11
+ function hexToRgb(hex) {
12
+ const h = hex.replace("#", "");
13
+ return [
14
+ parseInt(h.slice(0, 2), 16),
15
+ parseInt(h.slice(2, 4), 16),
16
+ parseInt(h.slice(4, 6), 16),
17
+ ];
18
+ }
19
+ function rgbToHsl(r, g, b) {
20
+ r /= 255;
21
+ g /= 255;
22
+ b /= 255;
23
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
24
+ let h = 0, s = 0, l = (max + min) / 2;
25
+ if (max !== min) {
26
+ const d = max - min;
27
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
28
+ switch (max) {
29
+ case r:
30
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
31
+ break;
32
+ case g:
33
+ h = ((b - r) / d + 2) / 6;
34
+ break;
35
+ case b:
36
+ h = ((r - g) / d + 4) / 6;
37
+ break;
38
+ }
39
+ }
40
+ return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
41
+ }
42
+ function hslToRgb(h, s, l) {
43
+ s /= 100;
44
+ l /= 100;
45
+ const c = (1 - Math.abs(2 * l - 1)) * s;
46
+ const x = c * (1 - Math.abs((h / 60) % 2 - 1));
47
+ const m = l - c / 2;
48
+ let r = 0, g = 0, b = 0;
49
+ if (h < 60) {
50
+ r = c;
51
+ g = x;
52
+ }
53
+ else if (h < 120) {
54
+ r = x;
55
+ g = c;
56
+ }
57
+ else if (h < 180) {
58
+ g = c;
59
+ b = x;
60
+ }
61
+ else if (h < 240) {
62
+ g = x;
63
+ b = c;
64
+ }
65
+ else if (h < 300) {
66
+ r = x;
67
+ b = c;
68
+ }
69
+ else {
70
+ r = c;
71
+ b = x;
72
+ }
73
+ return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
74
+ }
75
+ // ═══════════════════════════════════════════════════════════════════
76
+ // Tests
77
+ // ═══════════════════════════════════════════════════════════════════
78
+ describe("timestamp", () => {
79
+ it("generates valid timestamps", () => {
80
+ const now = Date.now();
81
+ const iso = new Date().toISOString();
82
+ expect(now).toBeGreaterThan(1700000000000); // after 2023
83
+ expect(iso).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
84
+ });
85
+ });
86
+ describe("uuid", () => {
87
+ it("generates valid UUID v4", () => {
88
+ for (let i = 0; i < 10; i++) {
89
+ const uuid = crypto.randomUUID();
90
+ expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
91
+ }
92
+ });
93
+ it("generates unique UUIDs", () => {
94
+ const uuids = new Set(Array.from({ length: 100 }, () => crypto.randomUUID()));
95
+ expect(uuids.size).toBe(100);
96
+ });
97
+ });
98
+ describe("base64", () => {
99
+ it("encodes correctly", () => {
100
+ expect(Buffer.from("hello").toString("base64")).toBe("aGVsbG8=");
101
+ expect(Buffer.from("你好").toString("base64")).toBe("5L2g5aW9");
102
+ });
103
+ it("round-trips", () => {
104
+ const original = "Hello, World! 你好世界";
105
+ const encoded = Buffer.from(original, "utf-8").toString("base64");
106
+ const decoded = Buffer.from(encoded, "base64").toString("utf-8");
107
+ expect(decoded).toBe(original);
108
+ });
109
+ });
110
+ describe("hash", () => {
111
+ it("generates correct SHA256", () => {
112
+ const hash = crypto.createHash("sha256").update("test").digest("hex");
113
+ expect(hash).toBe("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08");
114
+ });
115
+ it("generates correct MD5", () => {
116
+ const hash = crypto.createHash("md5").update("hello").digest("hex");
117
+ expect(hash).toBe("5d41402abc4b2a76b9719d911017c592");
118
+ });
119
+ it("different inputs produce different hashes", () => {
120
+ const h1 = crypto.createHash("sha256").update("a").digest("hex");
121
+ const h2 = crypto.createHash("sha256").update("b").digest("hex");
122
+ expect(h1).not.toBe(h2);
123
+ });
124
+ });
125
+ describe("color_tools", () => {
126
+ it("converts hex to RGB correctly", () => {
127
+ expect(hexToRgb("#ff0000")).toEqual([255, 0, 0]);
128
+ expect(hexToRgb("#00ff00")).toEqual([0, 255, 0]);
129
+ expect(hexToRgb("#0000ff")).toEqual([0, 0, 255]);
130
+ });
131
+ it("converts hex to HSL correctly", () => {
132
+ const hsl = rgbToHsl(255, 0, 0);
133
+ expect(hsl[0]).toBe(0); // hue
134
+ expect(hsl[1]).toBe(100); // saturation
135
+ expect(hsl[2]).toBe(50); // lightness
136
+ });
137
+ it("round-trips hex → RGB → HSL → RGB → hex", () => {
138
+ const originalHex = "#4a90d9";
139
+ const rgb = hexToRgb(originalHex);
140
+ const hsl = rgbToHsl(...rgb);
141
+ const rgb2 = hslToRgb(...hsl);
142
+ // Allow 1-unit rounding difference
143
+ expect(Math.abs(rgb[0] - rgb2[0])).toBeLessThanOrEqual(1);
144
+ expect(Math.abs(rgb[1] - rgb2[1])).toBeLessThanOrEqual(1);
145
+ expect(Math.abs(rgb[2] - rgb2[2])).toBeLessThanOrEqual(1);
146
+ });
147
+ });
148
+ describe("bmi_calculator", () => {
149
+ it("calculates BMI correctly", () => {
150
+ const height = 175, weight = 70;
151
+ const h = height / 100;
152
+ const bmi = weight / (h * h);
153
+ expect(Math.round(bmi * 100) / 100).toBe(22.86);
154
+ });
155
+ it("correctly categorizes BMI", () => {
156
+ const categorize = (bmi) => {
157
+ if (bmi < 18.5)
158
+ return "Underweight";
159
+ if (bmi < 25)
160
+ return "Normal weight";
161
+ if (bmi < 30)
162
+ return "Overweight";
163
+ return "Obese";
164
+ };
165
+ expect(categorize(17)).toBe("Underweight");
166
+ expect(categorize(22)).toBe("Normal weight");
167
+ expect(categorize(27)).toBe("Overweight");
168
+ expect(categorize(32)).toBe("Obese");
169
+ });
170
+ });
171
+ describe("json_formatter", () => {
172
+ it("formats JSON", () => {
173
+ const obj = { a: 1, b: [2, 3] };
174
+ expect(JSON.parse(JSON.stringify(obj))).toEqual(obj);
175
+ });
176
+ it("detects invalid JSON", () => {
177
+ expect(() => JSON.parse("{bad json")).toThrow();
178
+ });
179
+ it("minifies JSON", () => {
180
+ const minified = JSON.stringify({ a: 1, b: 2 });
181
+ expect(minified).toBe('{"a":1,"b":2}');
182
+ expect(minified).not.toContain("\n");
183
+ });
184
+ });
185
+ describe("url_tools", () => {
186
+ it("encodes URL components", () => {
187
+ expect(encodeURIComponent("hello world")).toBe("hello%20world");
188
+ expect(encodeURIComponent("你好")).toBe("%E4%BD%A0%E5%A5%BD");
189
+ });
190
+ it("decodes URL components", () => {
191
+ expect(decodeURIComponent("hello%20world")).toBe("hello world");
192
+ expect(decodeURIComponent("%E4%BD%A0%E5%A5%BD")).toBe("你好");
193
+ });
194
+ it("round-trips", () => {
195
+ const original = "https://example.com?q=你好世界";
196
+ expect(decodeURIComponent(encodeURIComponent(original))).toBe(original);
197
+ });
198
+ });
199
+ describe("text_tools", () => {
200
+ it("counts characters, words, lines", () => {
201
+ const text = "Hello world\nHow are you?";
202
+ expect(text.length).toBe(24);
203
+ expect(text.trim().split(/\s+/).length).toBe(5);
204
+ expect(text.split("\n").length).toBe(2);
205
+ });
206
+ it("reverses text", () => {
207
+ expect("hello".split("").reverse().join("")).toBe("olleh");
208
+ });
209
+ it("converts case", () => {
210
+ expect("Hello".toUpperCase()).toBe("HELLO");
211
+ expect("Hello".toLowerCase()).toBe("hello");
212
+ expect("hello world".replace(/\w\S*/g, w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())).toBe("Hello World");
213
+ });
214
+ });
215
+ describe("vcard_generator", () => {
216
+ it("generates valid vCard", () => {
217
+ const vcard = [
218
+ "BEGIN:VCARD",
219
+ "VERSION:3.0",
220
+ "FN:John Doe",
221
+ "ORG:ACME Inc",
222
+ "TEL:+1234567890",
223
+ "EMAIL:john@example.com",
224
+ "END:VCARD",
225
+ ].join("\n");
226
+ expect(vcard).toContain("BEGIN:VCARD");
227
+ expect(vcard).toContain("FN:John Doe");
228
+ expect(vcard).toContain("END:VCARD");
229
+ });
230
+ });
231
+ describe("wifi_qrcode format", () => {
232
+ it("generates correct WiFi string format", () => {
233
+ const ssid = "MyNetwork";
234
+ const password = "secret123";
235
+ const security = "WPA";
236
+ const wifiString = `WIFI:S:${ssid};T:${security};P:${password};;`;
237
+ expect(wifiString).toBe("WIFI:S:MyNetwork;T:WPA;P:secret123;;");
238
+ });
239
+ });
240
+ // ═══════════════════════════════════════════════════════════════════
241
+ // Rate Limiter Tests
242
+ // ═══════════════════════════════════════════════════════════════════
243
+ describe("rate_limiter", () => {
244
+ it("allows requests within limit", () => {
245
+ const RATE_WINDOW_MS = 60_000;
246
+ const MAX = 100;
247
+ let timestamps = [];
248
+ const check = () => {
249
+ const now = Date.now();
250
+ timestamps = timestamps.filter(t => now - t < RATE_WINDOW_MS);
251
+ if (timestamps.length >= MAX)
252
+ return false;
253
+ timestamps.push(now);
254
+ return true;
255
+ };
256
+ // First 100 requests should pass
257
+ for (let i = 0; i < MAX; i++) {
258
+ expect(check()).toBe(true);
259
+ }
260
+ // 101st should fail
261
+ expect(check()).toBe(false);
262
+ });
263
+ it("resets after window expires", () => {
264
+ const now = Date.now();
265
+ // Create 100 timestamps all older than the window
266
+ const timestamps = Array.from({ length: 100 }, () => now - 90_000);
267
+ const filtered = timestamps.filter(t => now - t < 60_000);
268
+ expect(filtered.length).toBe(0); // all expired
269
+ });
270
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myclaw-toolkit",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "23-in-1 developer utility toolkit as an MCP server — search, exchange rates, crypto, QR codes, translation, and more",
5
5
  "mcpName": "io.github.Dusheh/myclaw-toolkit",
6
6
  "type": "module",
@@ -14,7 +14,10 @@
14
14
  "scripts": {
15
15
  "build": "tsc",
16
16
  "start": "node dist/index.js",
17
- "dev": "tsx src/index.ts"
17
+ "dev": "tsx src/index.ts",
18
+ "test": "vitest run",
19
+ "lint": "eslint src/",
20
+ "test:watch": "vitest"
18
21
  },
19
22
  "keywords": [
20
23
  "mcp",
@@ -60,7 +63,11 @@
60
63
  "devDependencies": {
61
64
  "@types/node": "^22.19.21",
62
65
  "@types/qrcode": "^1.5.6",
66
+ "@typescript-eslint/eslint-plugin": "^8.62.0",
67
+ "@typescript-eslint/parser": "^8.62.0",
68
+ "eslint": "^10.5.0",
63
69
  "tsx": "^4.22.4",
64
- "typescript": "^5.9.3"
70
+ "typescript": "^5.9.3",
71
+ "vitest": "^4.1.9"
65
72
  }
66
73
  }