myclaw-toolkit 1.0.8 → 1.0.9
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/dist/index.js +277 -61
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -5,16 +5,28 @@
|
|
|
5
5
|
* Exposes 23 tools to AI assistants (Claude, ChatGPT, Cursor, etc.)
|
|
6
6
|
* via the Model Context Protocol (MCP).
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Privacy-first design: all local-computable operations run entirely
|
|
9
|
+
* on-device. Only tools that genuinely need external data (search,
|
|
10
|
+
* exchange rates, crypto prices, etc.) call the remote API.
|
|
10
11
|
*/
|
|
11
12
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12
13
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
14
|
import { z } from "zod";
|
|
15
|
+
import crypto from "node:crypto";
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import QRCode from "qrcode";
|
|
18
|
+
import { marked } from "marked";
|
|
19
|
+
// ── Version (read from package.json, never hardcoded) ─────────────
|
|
20
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
21
|
+
const VERSION = pkg.version;
|
|
14
22
|
// ── Config ──────────────────────────────────────────────────────────
|
|
15
23
|
const API_BASE = process.env.MYCLAW_API || "http://47.103.7.241";
|
|
16
|
-
const USER_AGENT =
|
|
24
|
+
const USER_AGENT = `myclaw-toolkit-mcp/${VERSION}`;
|
|
17
25
|
const FETCH_TIMEOUT_MS = 15_000;
|
|
26
|
+
/**
|
|
27
|
+
* Safe API call with timeout, proper error reading, and abort support.
|
|
28
|
+
* Only used for tools that genuinely need external data.
|
|
29
|
+
*/
|
|
18
30
|
async function apiCall(path) {
|
|
19
31
|
const controller = new AbortController();
|
|
20
32
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
@@ -23,10 +35,18 @@ async function apiCall(path) {
|
|
|
23
35
|
headers: { "User-Agent": USER_AGENT, Accept: "application/json" },
|
|
24
36
|
signal: controller.signal,
|
|
25
37
|
});
|
|
38
|
+
const body = await res.text();
|
|
26
39
|
if (!res.ok) {
|
|
27
|
-
|
|
40
|
+
// Parse error body if JSON, otherwise return raw text
|
|
41
|
+
try {
|
|
42
|
+
const errJson = JSON.parse(body);
|
|
43
|
+
return JSON.stringify({ error: `API ${res.status}`, detail: errJson, path });
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return JSON.stringify({ error: `API ${res.status}`, detail: body.slice(0, 500), path });
|
|
47
|
+
}
|
|
28
48
|
}
|
|
29
|
-
return
|
|
49
|
+
return body;
|
|
30
50
|
}
|
|
31
51
|
catch (err) {
|
|
32
52
|
if (err.name === "AbortError") {
|
|
@@ -38,73 +58,236 @@ async function apiCall(path) {
|
|
|
38
58
|
clearTimeout(timer);
|
|
39
59
|
}
|
|
40
60
|
}
|
|
61
|
+
// ── Local utility functions ───────────────────────────────────────
|
|
62
|
+
function hexToRgb(hex) {
|
|
63
|
+
const h = hex.replace("#", "");
|
|
64
|
+
return [
|
|
65
|
+
parseInt(h.slice(0, 2), 16),
|
|
66
|
+
parseInt(h.slice(2, 4), 16),
|
|
67
|
+
parseInt(h.slice(4, 6), 16),
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
function rgbToHsl(r, g, b) {
|
|
71
|
+
r /= 255;
|
|
72
|
+
g /= 255;
|
|
73
|
+
b /= 255;
|
|
74
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
75
|
+
let h = 0, s = 0, l = (max + min) / 2;
|
|
76
|
+
if (max !== min) {
|
|
77
|
+
const d = max - min;
|
|
78
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
79
|
+
switch (max) {
|
|
80
|
+
case r:
|
|
81
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
82
|
+
break;
|
|
83
|
+
case g:
|
|
84
|
+
h = ((b - r) / d + 2) / 6;
|
|
85
|
+
break;
|
|
86
|
+
case b:
|
|
87
|
+
h = ((r - g) / d + 4) / 6;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
|
|
92
|
+
}
|
|
93
|
+
function hslToRgb(h, s, l) {
|
|
94
|
+
s /= 100;
|
|
95
|
+
l /= 100;
|
|
96
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
97
|
+
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
|
|
98
|
+
const m = l - c / 2;
|
|
99
|
+
let r = 0, g = 0, b = 0;
|
|
100
|
+
if (h < 60) {
|
|
101
|
+
r = c;
|
|
102
|
+
g = x;
|
|
103
|
+
}
|
|
104
|
+
else if (h < 120) {
|
|
105
|
+
r = x;
|
|
106
|
+
g = c;
|
|
107
|
+
}
|
|
108
|
+
else if (h < 180) {
|
|
109
|
+
g = c;
|
|
110
|
+
b = x;
|
|
111
|
+
}
|
|
112
|
+
else if (h < 240) {
|
|
113
|
+
g = x;
|
|
114
|
+
b = c;
|
|
115
|
+
}
|
|
116
|
+
else if (h < 300) {
|
|
117
|
+
r = x;
|
|
118
|
+
b = c;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
r = c;
|
|
122
|
+
b = x;
|
|
123
|
+
}
|
|
124
|
+
return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
|
|
125
|
+
}
|
|
41
126
|
// ── Server ──────────────────────────────────────────────────────────
|
|
42
127
|
const server = new McpServer({
|
|
43
128
|
name: "myclaw-toolkit",
|
|
44
|
-
version:
|
|
129
|
+
version: VERSION,
|
|
45
130
|
});
|
|
46
131
|
// ═══════════════════════════════════════════════════════════════════
|
|
47
|
-
// CATEGORY 1: UTILITY TOOLS
|
|
132
|
+
// CATEGORY 1: UTILITY TOOLS — All local, zero network
|
|
48
133
|
// ═══════════════════════════════════════════════════════════════════
|
|
49
|
-
server.tool("timestamp", "Get current Unix timestamp and ISO 8601 datetime", {}, async () => {
|
|
50
|
-
const
|
|
51
|
-
return {
|
|
134
|
+
server.tool("timestamp", "Get current Unix timestamp and ISO 8601 datetime (runs locally, no network)", {}, async () => {
|
|
135
|
+
const now = new Date();
|
|
136
|
+
return {
|
|
137
|
+
content: [{
|
|
138
|
+
type: "text",
|
|
139
|
+
text: JSON.stringify({
|
|
140
|
+
unix_ms: now.getTime(),
|
|
141
|
+
unix_sec: Math.floor(now.getTime() / 1000),
|
|
142
|
+
iso8601: now.toISOString(),
|
|
143
|
+
utc: now.toUTCString(),
|
|
144
|
+
local: now.toString(),
|
|
145
|
+
}),
|
|
146
|
+
}],
|
|
147
|
+
};
|
|
52
148
|
});
|
|
53
|
-
server.tool("uuid", "Generate UUID v4 identifiers", {
|
|
149
|
+
server.tool("uuid", "Generate UUID v4 identifiers (runs locally, no network)", {
|
|
54
150
|
count: z.number().min(1).max(100).default(1).describe("Number of UUIDs to generate"),
|
|
55
151
|
}, async ({ count }) => {
|
|
56
|
-
const
|
|
57
|
-
return { content: [{ type: "text", text:
|
|
152
|
+
const uuids = Array.from({ length: count }, () => crypto.randomUUID());
|
|
153
|
+
return { content: [{ type: "text", text: JSON.stringify({ uuids }) }] };
|
|
58
154
|
});
|
|
59
|
-
server.tool("base64", "Encode or decode Base64 strings", {
|
|
155
|
+
server.tool("base64", "Encode or decode Base64 strings (runs locally, no data leaves your machine)", {
|
|
60
156
|
action: z.enum(["encode", "decode"]).describe("Operation to perform"),
|
|
61
157
|
text: z.string().describe("Text to encode or Base64 to decode"),
|
|
62
158
|
}, async ({ action, text }) => {
|
|
63
|
-
|
|
64
|
-
|
|
159
|
+
try {
|
|
160
|
+
if (action === "encode") {
|
|
161
|
+
const encoded = Buffer.from(text, "utf-8").toString("base64");
|
|
162
|
+
return { content: [{ type: "text", text: JSON.stringify({ action, input: text, output: encoded }) }] };
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const decoded = Buffer.from(text, "base64").toString("utf-8");
|
|
166
|
+
return { content: [{ type: "text", text: JSON.stringify({ action, input: text, output: decoded }) }] };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: err.message, action, input: text }) }] };
|
|
171
|
+
}
|
|
65
172
|
});
|
|
66
|
-
server.tool("hash", "Generate cryptographic hashes
|
|
173
|
+
server.tool("hash", "Generate cryptographic hashes — MD5, SHA1, SHA256, SHA512 (runs locally, no network)", {
|
|
67
174
|
algorithm: z.enum(["md5", "sha1", "sha256", "sha512"]).default("sha256").describe("Hash algorithm"),
|
|
68
175
|
text: z.string().describe("Text to hash"),
|
|
69
176
|
}, async ({ algorithm, text }) => {
|
|
70
|
-
const
|
|
71
|
-
return { content: [{ type: "text", text:
|
|
177
|
+
const hash = crypto.createHash(algorithm).update(text, "utf-8").digest("hex");
|
|
178
|
+
return { content: [{ type: "text", text: JSON.stringify({ algorithm, input: text, hash }) }] };
|
|
72
179
|
});
|
|
73
|
-
server.tool("qrcode", "Generate QR codes from text or URLs", {
|
|
180
|
+
server.tool("qrcode", "Generate QR codes from text or URLs (runs locally, no network)", {
|
|
74
181
|
text: z.string().describe("Text or URL to encode in QR code"),
|
|
75
182
|
}, async ({ text }) => {
|
|
76
|
-
const
|
|
77
|
-
return { content: [{ type: "text", text:
|
|
183
|
+
const dataUrl = await QRCode.toDataURL(text, { width: 400, margin: 2 });
|
|
184
|
+
return { content: [{ type: "text", text: dataUrl }] };
|
|
78
185
|
});
|
|
79
|
-
server.tool("color_tools", "Convert colors between hex, RGB, and HSL formats", {
|
|
80
|
-
color: z.string().describe("Color value (hex like #ff0000, rgb like 255,0,0, or hsl like 0,100,50)"),
|
|
186
|
+
server.tool("color_tools", "Convert colors between hex, RGB, and HSL formats (runs locally, no network)", {
|
|
187
|
+
color: z.string().describe("Color value (hex like '#ff0000', rgb like '255,0,0', or hsl like '0,100,50')"),
|
|
81
188
|
}, async ({ color }) => {
|
|
82
|
-
|
|
83
|
-
|
|
189
|
+
try {
|
|
190
|
+
const c = color.trim();
|
|
191
|
+
// Detect format
|
|
192
|
+
if (c.startsWith("#") || /^[0-9a-fA-F]{6}$/.test(c)) {
|
|
193
|
+
const rgb = hexToRgb(c);
|
|
194
|
+
const hsl = rgbToHsl(...rgb);
|
|
195
|
+
return { content: [{ type: "text", text: JSON.stringify({ hex: `#${c.replace("#", "")}`, rgb, hsl }) }] };
|
|
196
|
+
}
|
|
197
|
+
if (c.toLowerCase().includes("hsl")) {
|
|
198
|
+
const match = c.match(/[\d.]+/g);
|
|
199
|
+
if (match && match.length >= 3) {
|
|
200
|
+
const [h, s, l] = [parseFloat(match[0]), parseFloat(match[1]), parseFloat(match[2])];
|
|
201
|
+
const rgb = hslToRgb(h, s, l);
|
|
202
|
+
const hex = "#" + rgb.map(v => v.toString(16).padStart(2, "0")).join("");
|
|
203
|
+
return { content: [{ type: "text", text: JSON.stringify({ hex, rgb, hsl: [h, s, l] }) }] };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (c.includes(",")) {
|
|
207
|
+
const parts = c.split(",").map(Number);
|
|
208
|
+
if (parts.length >= 3 && !isNaN(parts[0])) {
|
|
209
|
+
const [r, g, b] = parts;
|
|
210
|
+
const hsl = rgbToHsl(r, g, b);
|
|
211
|
+
const hex = "#" + [r, g, b].map(v => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("");
|
|
212
|
+
return { content: [{ type: "text", text: JSON.stringify({ hex, rgb: [r, g, b], hsl }) }] };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Unrecognized color format. Use hex (#ff0000), rgb (255,0,0), or hsl (0,100,50)" }) }] };
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: err.message }) }] };
|
|
219
|
+
}
|
|
84
220
|
});
|
|
85
|
-
server.tool("json_formatter", "Format, validate, or minify JSON strings", {
|
|
221
|
+
server.tool("json_formatter", "Format, validate, or minify JSON strings (runs locally, no data leaves your machine)", {
|
|
86
222
|
action: z.enum(["format", "validate", "minify"]).default("format").describe("Operation"),
|
|
87
223
|
json: z.string().describe("JSON string to process"),
|
|
88
224
|
}, async ({ action, json }) => {
|
|
89
|
-
|
|
90
|
-
|
|
225
|
+
try {
|
|
226
|
+
const parsed = JSON.parse(json);
|
|
227
|
+
if (action === "validate") {
|
|
228
|
+
return { content: [{ type: "text", text: JSON.stringify({ valid: true, depth: getDepth(parsed) }) }] };
|
|
229
|
+
}
|
|
230
|
+
const output = action === "minify" ? JSON.stringify(parsed) : JSON.stringify(parsed, null, 2);
|
|
231
|
+
return { content: [{ type: "text", text: output }] };
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
if (action === "validate") {
|
|
235
|
+
return { content: [{ type: "text", text: JSON.stringify({ valid: false, error: err.message }) }] };
|
|
236
|
+
}
|
|
237
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `Invalid JSON: ${err.message}` }) }] };
|
|
238
|
+
}
|
|
91
239
|
});
|
|
92
|
-
|
|
240
|
+
function getDepth(obj, depth = 0) {
|
|
241
|
+
if (typeof obj !== "object" || obj === null || obj === undefined)
|
|
242
|
+
return depth;
|
|
243
|
+
let max = depth;
|
|
244
|
+
for (const key of Object.keys(obj)) {
|
|
245
|
+
const d = getDepth(obj[key], depth + 1);
|
|
246
|
+
if (d > max)
|
|
247
|
+
max = d;
|
|
248
|
+
}
|
|
249
|
+
return max;
|
|
250
|
+
}
|
|
251
|
+
server.tool("url_tools", "Encode or decode URL strings (runs locally, no network)", {
|
|
93
252
|
action: z.enum(["encode", "decode"]).describe("Operation"),
|
|
94
253
|
text: z.string().describe("Text to encode or URL to decode"),
|
|
95
254
|
}, async ({ action, text }) => {
|
|
96
|
-
|
|
97
|
-
|
|
255
|
+
try {
|
|
256
|
+
const output = action === "encode" ? encodeURIComponent(text) : decodeURIComponent(text);
|
|
257
|
+
return { content: [{ type: "text", text: JSON.stringify({ action, input: text, output }) }] };
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: err.message, action, input: text }) }] };
|
|
261
|
+
}
|
|
98
262
|
});
|
|
99
|
-
server.tool("text_tools", "Text processing utilities
|
|
263
|
+
server.tool("text_tools", "Text processing utilities — count, reverse, case conversion (runs locally, no network)", {
|
|
100
264
|
action: z.enum(["count", "reverse", "upper", "lower", "title"]).describe("Operation"),
|
|
101
265
|
text: z.string().describe("Text to process"),
|
|
102
266
|
}, async ({ action, text }) => {
|
|
103
|
-
|
|
104
|
-
|
|
267
|
+
let output;
|
|
268
|
+
switch (action) {
|
|
269
|
+
case "count":
|
|
270
|
+
output = { chars: text.length, words: text.trim() ? text.trim().split(/\s+/).length : 0, lines: text.split("\n").length };
|
|
271
|
+
break;
|
|
272
|
+
case "reverse":
|
|
273
|
+
output = text.split("").reverse().join("");
|
|
274
|
+
break;
|
|
275
|
+
case "upper":
|
|
276
|
+
output = text.toUpperCase();
|
|
277
|
+
break;
|
|
278
|
+
case "lower":
|
|
279
|
+
output = text.toLowerCase();
|
|
280
|
+
break;
|
|
281
|
+
case "title":
|
|
282
|
+
output = text.replace(/\w\S*/g, w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase());
|
|
283
|
+
break;
|
|
284
|
+
default:
|
|
285
|
+
output = text;
|
|
286
|
+
}
|
|
287
|
+
return { content: [{ type: "text", text: JSON.stringify({ action, input: text, output }) }] };
|
|
105
288
|
});
|
|
106
289
|
// ═══════════════════════════════════════════════════════════════════
|
|
107
|
-
// CATEGORY 2: DATA TOOLS
|
|
290
|
+
// CATEGORY 2: DATA TOOLS — Mixed local + remote
|
|
108
291
|
// ═══════════════════════════════════════════════════════════════════
|
|
109
292
|
server.tool("exchange_rate", "Get real-time currency exchange rates between any two currencies", {
|
|
110
293
|
from: z.string().length(3).describe("Source currency code (e.g., USD, CNY, EUR)"),
|
|
@@ -126,31 +309,47 @@ server.tool("domain_check", "Check domain name availability and whois informatio
|
|
|
126
309
|
const result = await apiCall(`/domain?name=${encodeURIComponent(domain)}`);
|
|
127
310
|
return { content: [{ type: "text", text: result }] };
|
|
128
311
|
});
|
|
129
|
-
server.tool("bmi_calculator", "Calculate BMI (Body Mass Index) from height and weight", {
|
|
312
|
+
server.tool("bmi_calculator", "Calculate BMI (Body Mass Index) from height and weight (runs locally, no network)", {
|
|
130
313
|
height: z.number().min(50).max(300).describe("Height in centimeters"),
|
|
131
314
|
weight: z.number().min(1).max(500).describe("Weight in kilograms"),
|
|
132
315
|
}, async ({ height, weight }) => {
|
|
133
|
-
const
|
|
134
|
-
|
|
316
|
+
const h = height / 100;
|
|
317
|
+
const bmi = weight / (h * h);
|
|
318
|
+
let category;
|
|
319
|
+
if (bmi < 18.5)
|
|
320
|
+
category = "Underweight";
|
|
321
|
+
else if (bmi < 25)
|
|
322
|
+
category = "Normal weight";
|
|
323
|
+
else if (bmi < 30)
|
|
324
|
+
category = "Overweight";
|
|
325
|
+
else
|
|
326
|
+
category = "Obese";
|
|
327
|
+
return {
|
|
328
|
+
content: [{
|
|
329
|
+
type: "text",
|
|
330
|
+
text: JSON.stringify({ height_cm: height, weight_kg: weight, bmi: Math.round(bmi * 100) / 100, category }),
|
|
331
|
+
}],
|
|
332
|
+
};
|
|
135
333
|
});
|
|
136
|
-
server.tool("vcard_generator", "Generate vCard (.vcf) contact files", {
|
|
334
|
+
server.tool("vcard_generator", "Generate vCard (.vcf) contact files (runs locally, no network)", {
|
|
137
335
|
name: z.string().describe("Full name"),
|
|
138
336
|
phone: z.string().optional().describe("Phone number"),
|
|
139
337
|
email: z.string().optional().describe("Email address"),
|
|
140
338
|
org: z.string().optional().describe("Organization/company"),
|
|
141
339
|
}, async ({ name, phone, email, org }) => {
|
|
142
|
-
const
|
|
340
|
+
const lines = ["BEGIN:VCARD", "VERSION:3.0", `FN:${name}`];
|
|
341
|
+
if (org)
|
|
342
|
+
lines.push(`ORG:${org}`);
|
|
143
343
|
if (phone)
|
|
144
|
-
|
|
344
|
+
lines.push(`TEL:${phone}`);
|
|
145
345
|
if (email)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
return { content: [{ type: "text", text: result }] };
|
|
346
|
+
lines.push(`EMAIL:${email}`);
|
|
347
|
+
lines.push("END:VCARD");
|
|
348
|
+
const vcard = lines.join("\n");
|
|
349
|
+
return { content: [{ type: "text", text: vcard }] };
|
|
151
350
|
});
|
|
152
351
|
// ═══════════════════════════════════════════════════════════════════
|
|
153
|
-
// CATEGORY 3: SEARCH & CONTENT TOOLS
|
|
352
|
+
// CATEGORY 3: SEARCH & CONTENT TOOLS — Remote API
|
|
154
353
|
// ═══════════════════════════════════════════════════════════════════
|
|
155
354
|
server.tool("web_search", "Search the web using Bing — returns titles, snippets, and URLs", {
|
|
156
355
|
query: z.string().describe("Search query"),
|
|
@@ -187,42 +386,56 @@ server.tool("read_page", "Extract readable content from any web page (like Reada
|
|
|
187
386
|
return { content: [{ type: "text", text: result }] };
|
|
188
387
|
});
|
|
189
388
|
// ═══════════════════════════════════════════════════════════════════
|
|
190
|
-
// CATEGORY 4: PROCESSING TOOLS
|
|
389
|
+
// CATEGORY 4: PROCESSING TOOLS
|
|
191
390
|
// ═══════════════════════════════════════════════════════════════════
|
|
192
|
-
server.tool("markdown_to_html", "Convert Markdown text to HTML", {
|
|
391
|
+
server.tool("markdown_to_html", "Convert Markdown text to HTML (runs locally, no data leaves your machine)", {
|
|
193
392
|
markdown: z.string().describe("Markdown text to convert"),
|
|
194
393
|
}, async ({ markdown }) => {
|
|
195
|
-
const
|
|
196
|
-
return { content: [{ type: "text", text:
|
|
394
|
+
const html = await marked.parse(markdown, { async: true });
|
|
395
|
+
return { content: [{ type: "text", text: html }] };
|
|
197
396
|
});
|
|
198
|
-
server.tool("ai_translate", "Translate text between languages
|
|
397
|
+
server.tool("ai_translate", "Translate text between languages (uses MyMemory translation API)", {
|
|
199
398
|
text: z.string().describe("Text to translate"),
|
|
200
399
|
from: z.string().default("auto").describe("Source language (auto for auto-detect)"),
|
|
201
400
|
to: z.string().default("en").describe("Target language"),
|
|
202
401
|
}, async ({ text, from, to }) => {
|
|
203
402
|
const langPair = from === "auto" ? `autodetect|${to}` : `${from}|${to}`;
|
|
204
403
|
const url = `https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=${encodeURIComponent(langPair)}`;
|
|
404
|
+
const controller = new AbortController();
|
|
405
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
205
406
|
try {
|
|
206
|
-
const res = await fetch(url);
|
|
407
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
207
408
|
const data = await res.json();
|
|
208
409
|
const translated = data?.responseData?.translatedText || `Error: ${data?.responseStatus || "unknown"}`;
|
|
209
410
|
return { content: [{ type: "text", text: JSON.stringify({ from, to, original: text, translated }) }] };
|
|
210
411
|
}
|
|
211
|
-
catch {
|
|
212
|
-
|
|
412
|
+
catch (err) {
|
|
413
|
+
if (err.name === "AbortError") {
|
|
414
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Translation request timed out", from, to, original: text }) }] };
|
|
415
|
+
}
|
|
416
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `Translation service unavailable: ${err.message}`, from, to, original: text }) }] };
|
|
417
|
+
}
|
|
418
|
+
finally {
|
|
419
|
+
clearTimeout(timer);
|
|
213
420
|
}
|
|
214
421
|
});
|
|
215
|
-
server.tool("quote", "Get inspirational or random quotes", {}, async () => {
|
|
422
|
+
server.tool("quote", "Get inspirational or random quotes (remote API, no user data sent)", {}, async () => {
|
|
216
423
|
const result = await apiCall("/quote");
|
|
217
424
|
return { content: [{ type: "text", text: result }] };
|
|
218
425
|
});
|
|
219
|
-
|
|
426
|
+
/**
|
|
427
|
+
* WiFi QR Code — CRITICAL: runs 100% locally.
|
|
428
|
+
* WiFi passwords NEVER leave the user's machine.
|
|
429
|
+
*/
|
|
430
|
+
server.tool("wifi_qrcode", "Generate WiFi connection QR codes (runs 100% locally — your password NEVER leaves your device)", {
|
|
220
431
|
ssid: z.string().describe("WiFi network name (SSID)"),
|
|
221
432
|
password: z.string().describe("WiFi password"),
|
|
222
433
|
security: z.enum(["WPA", "WEP", "nopass"]).default("WPA").describe("Security type"),
|
|
223
434
|
}, async ({ ssid, password, security }) => {
|
|
224
|
-
|
|
225
|
-
|
|
435
|
+
// WiFi QR code format: WIFI:S:<SSID>;T:<WPA|WEP|>;P:<password>;;
|
|
436
|
+
const wifiString = `WIFI:S:${ssid};T:${security};P:${password};;`;
|
|
437
|
+
const dataUrl = await QRCode.toDataURL(wifiString, { width: 400, margin: 2 });
|
|
438
|
+
return { content: [{ type: "text", text: dataUrl }] };
|
|
226
439
|
});
|
|
227
440
|
server.tool("compare", "Compare two items side-by-side (text, products, anything)", {
|
|
228
441
|
a: z.string().describe("First item to compare"),
|
|
@@ -245,6 +458,7 @@ server.tool("health_check", "Check if the MyClaw API backend is reachable and re
|
|
|
245
458
|
backend: API_BASE,
|
|
246
459
|
status: result.startsWith("{") ? "healthy" : "degraded",
|
|
247
460
|
latency_ms: elapsed,
|
|
461
|
+
version: VERSION,
|
|
248
462
|
}),
|
|
249
463
|
}],
|
|
250
464
|
};
|
|
@@ -255,6 +469,8 @@ server.tool("health_check", "Check if the MyClaw API backend is reachable and re
|
|
|
255
469
|
async function main() {
|
|
256
470
|
const transport = new StdioServerTransport();
|
|
257
471
|
await server.connect(transport);
|
|
258
|
-
console.error(
|
|
472
|
+
console.error(`MyClaw Toolkit v${VERSION} — Privacy-first MCP server running (stdio)`);
|
|
473
|
+
console.error(" Local tools: timestamp, uuid, base64, hash, qrcode, wifi_qrcode, color_tools, json_formatter, url_tools, text_tools, bmi_calculator, vcard_generator, markdown_to_html");
|
|
474
|
+
console.error(" Remote tools: web_search, news_search, product_search, exchange_rate, crypto_price, domain_check, rss_feed, read_page, ai_translate, quote, compare");
|
|
259
475
|
}
|
|
260
476
|
main().catch(console.error);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myclaw-toolkit",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "23-in-1 developer utility toolkit as an MCP server
|
|
3
|
+
"version": "1.0.9",
|
|
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",
|
|
7
7
|
"main": "dist/index.js",
|
|
@@ -53,11 +53,14 @@
|
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
56
|
+
"marked": "^18.0.5",
|
|
57
|
+
"qrcode": "^1.5.4",
|
|
56
58
|
"zod": "^3.23.0"
|
|
57
59
|
},
|
|
58
60
|
"devDependencies": {
|
|
59
61
|
"@types/node": "^22.19.21",
|
|
62
|
+
"@types/qrcode": "^1.5.6",
|
|
60
63
|
"tsx": "^4.22.4",
|
|
61
64
|
"typescript": "^5.9.3"
|
|
62
65
|
}
|
|
63
|
-
}
|
|
66
|
+
}
|