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 +44 -21
- package/dist/index.js +15 -0
- package/dist/tools.test.js +270 -0
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -4,8 +4,24 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/myclaw-toolkit)
|
|
5
5
|
[](https://www.npmjs.com/package/myclaw-toolkit)
|
|
6
6
|
[](./LICENSE)
|
|
7
|
+
[](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 (
|
|
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
|
|
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) —
|
|
87
|
-
- [
|
|
88
|
-
- [
|
|
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
|
|
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.
|
|
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
|
}
|