@rog0x/mcp-crypto-tools 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/src/index.ts ADDED
@@ -0,0 +1,338 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ } from "@modelcontextprotocol/sdk/types.js";
9
+
10
+ import { hashText, hmacText, compareHashes, hashMultiple } from "./tools/hash.js";
11
+ import {
12
+ base64Encode, base64Decode,
13
+ urlEncode, urlDecode,
14
+ htmlEntitiesEncode, htmlEntitiesDecode,
15
+ hexEncode, hexDecode,
16
+ binaryEncode, binaryDecode,
17
+ detectEncoding,
18
+ } from "./tools/encode-decode.js";
19
+ import {
20
+ generateUUIDv4, generateNanoid, generateULID,
21
+ generateCUID, generateRandomString,
22
+ } from "./tools/uuid-generator.js";
23
+ import { generatePassword, checkPasswordStrength } from "./tools/password-tools.js";
24
+ import { decodeJWT, checkJWTExpiry, createUnsignedJWT } from "./tools/jwt-tools.js";
25
+
26
+ const server = new Server(
27
+ {
28
+ name: "mcp-crypto-tools",
29
+ version: "1.0.0",
30
+ },
31
+ {
32
+ capabilities: {
33
+ tools: {},
34
+ },
35
+ }
36
+ );
37
+
38
+ // List available tools
39
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
40
+ tools: [
41
+ {
42
+ name: "hash",
43
+ description:
44
+ "Hash text using MD5, SHA-1, SHA-256, SHA-512, or HMAC. Can also compare two hash values or compute all hash algorithms at once.",
45
+ inputSchema: {
46
+ type: "object" as const,
47
+ properties: {
48
+ action: {
49
+ type: "string",
50
+ enum: ["hash", "hmac", "compare", "hash_all"],
51
+ description: "Action to perform: hash (single algorithm), hmac (keyed hash), compare (compare two hashes), hash_all (all algorithms at once)",
52
+ },
53
+ text: { type: "string", description: "Text to hash (for hash, hmac, hash_all actions)" },
54
+ algorithm: {
55
+ type: "string",
56
+ enum: ["md5", "sha1", "sha256", "sha512"],
57
+ description: "Hash algorithm (for hash and hmac actions). Default: sha256",
58
+ },
59
+ key: { type: "string", description: "Secret key (for hmac action)" },
60
+ hash1: { type: "string", description: "First hash (for compare action)" },
61
+ hash2: { type: "string", description: "Second hash (for compare action)" },
62
+ encoding: {
63
+ type: "string",
64
+ enum: ["hex", "base64"],
65
+ description: "Output encoding. Default: hex",
66
+ },
67
+ },
68
+ required: ["action"],
69
+ },
70
+ },
71
+ {
72
+ name: "encode_decode",
73
+ description:
74
+ "Encode or decode text using Base64, URL encoding, HTML entities, hex, or binary. Can also auto-detect the encoding format of input.",
75
+ inputSchema: {
76
+ type: "object" as const,
77
+ properties: {
78
+ action: {
79
+ type: "string",
80
+ enum: [
81
+ "base64_encode", "base64_decode",
82
+ "url_encode", "url_decode",
83
+ "html_encode", "html_decode",
84
+ "hex_encode", "hex_decode",
85
+ "binary_encode", "binary_decode",
86
+ "detect",
87
+ ],
88
+ description: "Encoding/decoding action to perform",
89
+ },
90
+ text: { type: "string", description: "Text to encode, decode, or detect" },
91
+ },
92
+ required: ["action", "text"],
93
+ },
94
+ },
95
+ {
96
+ name: "generate_id",
97
+ description:
98
+ "Generate unique identifiers: UUID v4, nanoid, ULID, CUID, or random strings with configurable length and charset.",
99
+ inputSchema: {
100
+ type: "object" as const,
101
+ properties: {
102
+ type: {
103
+ type: "string",
104
+ enum: ["uuid", "nanoid", "ulid", "cuid", "random"],
105
+ description: "Type of ID to generate",
106
+ },
107
+ count: { type: "number", description: "Number of IDs to generate (max 100). Default: 1" },
108
+ length: { type: "number", description: "Length for nanoid or random string. Default: 21 for nanoid, 16 for random" },
109
+ charset: {
110
+ type: "string",
111
+ description: "Charset for random strings: alphanumeric, alpha, numeric, hex, lowercase, uppercase, symbols, all, or a custom string of characters. Default: alphanumeric",
112
+ },
113
+ },
114
+ required: ["type"],
115
+ },
116
+ },
117
+ {
118
+ name: "password",
119
+ description:
120
+ "Generate secure passwords with configurable options, or check password strength with entropy calculation and crack time estimation.",
121
+ inputSchema: {
122
+ type: "object" as const,
123
+ properties: {
124
+ action: {
125
+ type: "string",
126
+ enum: ["generate", "check_strength"],
127
+ description: "Action: generate a password or check strength of an existing one",
128
+ },
129
+ password: { type: "string", description: "Password to check (for check_strength action)" },
130
+ length: { type: "number", description: "Password length (for generate). Default: 16" },
131
+ uppercase: { type: "boolean", description: "Include uppercase letters. Default: true" },
132
+ lowercase: { type: "boolean", description: "Include lowercase letters. Default: true" },
133
+ digits: { type: "boolean", description: "Include digits. Default: true" },
134
+ symbols: { type: "boolean", description: "Include symbols. Default: true" },
135
+ exclude_ambiguous: {
136
+ type: "boolean",
137
+ description: "Exclude ambiguous characters (0, O, l, I, 1). Default: false",
138
+ },
139
+ count: { type: "number", description: "Number of passwords to generate (max 100). Default: 1" },
140
+ },
141
+ required: ["action"],
142
+ },
143
+ },
144
+ {
145
+ name: "jwt",
146
+ description:
147
+ "Decode JWT tokens to inspect header and payload, check expiry status, or create unsigned JWTs for testing purposes. Not for production authentication.",
148
+ inputSchema: {
149
+ type: "object" as const,
150
+ properties: {
151
+ action: {
152
+ type: "string",
153
+ enum: ["decode", "check_expiry", "create_unsigned"],
154
+ description: "Action: decode (full decode), check_expiry (just expiry info), create_unsigned (create test token)",
155
+ },
156
+ token: { type: "string", description: "JWT token string (for decode and check_expiry)" },
157
+ payload: {
158
+ type: "object",
159
+ description: "Payload object for creating unsigned JWT (for create_unsigned)",
160
+ },
161
+ expires_in_seconds: {
162
+ type: "number",
163
+ description: "Expiration time in seconds from now (for create_unsigned)",
164
+ },
165
+ },
166
+ required: ["action"],
167
+ },
168
+ },
169
+ ],
170
+ }));
171
+
172
+ // Handle tool calls
173
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
174
+ const { name, arguments: args } = request.params;
175
+
176
+ try {
177
+ switch (name) {
178
+ case "hash": {
179
+ const action = args?.action as string;
180
+ const encoding = (args?.encoding as "hex" | "base64") || "hex";
181
+
182
+ switch (action) {
183
+ case "hash": {
184
+ const text = args?.text as string;
185
+ if (!text) throw new Error("'text' is required for hash action");
186
+ const algorithm = (args?.algorithm as string) || "sha256";
187
+ const result = hashText(text, algorithm, encoding);
188
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
189
+ }
190
+ case "hmac": {
191
+ const text = args?.text as string;
192
+ const key = args?.key as string;
193
+ if (!text) throw new Error("'text' is required for hmac action");
194
+ if (!key) throw new Error("'key' is required for hmac action");
195
+ const algorithm = (args?.algorithm as string) || "sha256";
196
+ const result = hmacText(text, key, algorithm, encoding);
197
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
198
+ }
199
+ case "compare": {
200
+ const h1 = args?.hash1 as string;
201
+ const h2 = args?.hash2 as string;
202
+ if (!h1 || !h2) throw new Error("'hash1' and 'hash2' are required for compare action");
203
+ const result = compareHashes(h1, h2);
204
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
205
+ }
206
+ case "hash_all": {
207
+ const text = args?.text as string;
208
+ if (!text) throw new Error("'text' is required for hash_all action");
209
+ const result = hashMultiple(text, encoding);
210
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
211
+ }
212
+ default:
213
+ throw new Error(`Unknown hash action: ${action}`);
214
+ }
215
+ }
216
+
217
+ case "encode_decode": {
218
+ const action = args?.action as string;
219
+ const text = args?.text as string;
220
+ if (!text) throw new Error("'text' is required");
221
+
222
+ let result: unknown;
223
+ switch (action) {
224
+ case "base64_encode": result = base64Encode(text); break;
225
+ case "base64_decode": result = base64Decode(text); break;
226
+ case "url_encode": result = urlEncode(text); break;
227
+ case "url_decode": result = urlDecode(text); break;
228
+ case "html_encode": result = htmlEntitiesEncode(text); break;
229
+ case "html_decode": result = htmlEntitiesDecode(text); break;
230
+ case "hex_encode": result = hexEncode(text); break;
231
+ case "hex_decode": result = hexDecode(text); break;
232
+ case "binary_encode": result = binaryEncode(text); break;
233
+ case "binary_decode": result = binaryDecode(text); break;
234
+ case "detect": result = detectEncoding(text); break;
235
+ default: throw new Error(`Unknown encode_decode action: ${action}`);
236
+ }
237
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
238
+ }
239
+
240
+ case "generate_id": {
241
+ const type = args?.type as string;
242
+ const count = (args?.count as number) || 1;
243
+
244
+ let result: unknown;
245
+ switch (type) {
246
+ case "uuid": result = generateUUIDv4(count); break;
247
+ case "nanoid": {
248
+ const length = (args?.length as number) || 21;
249
+ result = generateNanoid(length, count);
250
+ break;
251
+ }
252
+ case "ulid": result = generateULID(count); break;
253
+ case "cuid": result = generateCUID(count); break;
254
+ case "random": {
255
+ const length = (args?.length as number) || 16;
256
+ const charset = (args?.charset as string) || "alphanumeric";
257
+ result = generateRandomString(length, charset, count);
258
+ break;
259
+ }
260
+ default: throw new Error(`Unknown ID type: ${type}`);
261
+ }
262
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
263
+ }
264
+
265
+ case "password": {
266
+ const action = args?.action as string;
267
+
268
+ switch (action) {
269
+ case "generate": {
270
+ const result = generatePassword({
271
+ length: args?.length as number | undefined,
272
+ uppercase: args?.uppercase as boolean | undefined,
273
+ lowercase: args?.lowercase as boolean | undefined,
274
+ digits: args?.digits as boolean | undefined,
275
+ symbols: args?.symbols as boolean | undefined,
276
+ exclude_ambiguous: args?.exclude_ambiguous as boolean | undefined,
277
+ count: args?.count as number | undefined,
278
+ });
279
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
280
+ }
281
+ case "check_strength": {
282
+ const password = args?.password as string;
283
+ if (!password) throw new Error("'password' is required for check_strength action");
284
+ const result = checkPasswordStrength(password);
285
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
286
+ }
287
+ default:
288
+ throw new Error(`Unknown password action: ${action}`);
289
+ }
290
+ }
291
+
292
+ case "jwt": {
293
+ const action = args?.action as string;
294
+
295
+ switch (action) {
296
+ case "decode": {
297
+ const token = args?.token as string;
298
+ if (!token) throw new Error("'token' is required for decode action");
299
+ const result = decodeJWT(token);
300
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
301
+ }
302
+ case "check_expiry": {
303
+ const token = args?.token as string;
304
+ if (!token) throw new Error("'token' is required for check_expiry action");
305
+ const result = checkJWTExpiry(token);
306
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
307
+ }
308
+ case "create_unsigned": {
309
+ const payload = (args?.payload as Record<string, unknown>) || {};
310
+ const expiresIn = args?.expires_in_seconds as number | undefined;
311
+ const result = createUnsignedJWT(payload, expiresIn);
312
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
313
+ }
314
+ default:
315
+ throw new Error(`Unknown jwt action: ${action}`);
316
+ }
317
+ }
318
+
319
+ default:
320
+ throw new Error(`Unknown tool: ${name}`);
321
+ }
322
+ } catch (error: unknown) {
323
+ const message = error instanceof Error ? error.message : String(error);
324
+ return {
325
+ content: [{ type: "text", text: `Error: ${message}` }],
326
+ isError: true,
327
+ };
328
+ }
329
+ });
330
+
331
+ // Start server
332
+ async function main() {
333
+ const transport = new StdioServerTransport();
334
+ await server.connect(transport);
335
+ console.error("MCP Crypto Tools server running on stdio");
336
+ }
337
+
338
+ main().catch(console.error);
@@ -0,0 +1,150 @@
1
+ const HTML_ENTITY_MAP: Record<string, string> = {
2
+ "&": "&amp;",
3
+ "<": "&lt;",
4
+ ">": "&gt;",
5
+ '"': "&quot;",
6
+ "'": "&#39;",
7
+ };
8
+
9
+ const HTML_ENTITY_REVERSE: Record<string, string> = {};
10
+ for (const [char, entity] of Object.entries(HTML_ENTITY_MAP)) {
11
+ HTML_ENTITY_REVERSE[entity] = char;
12
+ }
13
+
14
+ export function base64Encode(text: string): { encoded: string; original_length: number } {
15
+ return {
16
+ encoded: Buffer.from(text, "utf-8").toString("base64"),
17
+ original_length: text.length,
18
+ };
19
+ }
20
+
21
+ export function base64Decode(encoded: string): { decoded: string; encoded_length: number } {
22
+ return {
23
+ decoded: Buffer.from(encoded, "base64").toString("utf-8"),
24
+ encoded_length: encoded.length,
25
+ };
26
+ }
27
+
28
+ export function urlEncode(text: string): { encoded: string } {
29
+ return { encoded: encodeURIComponent(text) };
30
+ }
31
+
32
+ export function urlDecode(encoded: string): { decoded: string } {
33
+ return { decoded: decodeURIComponent(encoded) };
34
+ }
35
+
36
+ export function htmlEntitiesEncode(text: string): { encoded: string } {
37
+ const encoded = text.replace(/[&<>"']/g, (ch) => HTML_ENTITY_MAP[ch] || ch);
38
+ return { encoded };
39
+ }
40
+
41
+ export function htmlEntitiesDecode(text: string): { decoded: string } {
42
+ let decoded = text;
43
+ for (const [entity, char] of Object.entries(HTML_ENTITY_REVERSE)) {
44
+ decoded = decoded.split(entity).join(char);
45
+ }
46
+ // Handle numeric entities
47
+ decoded = decoded.replace(/&#(\d+);/g, (_, num) => String.fromCharCode(parseInt(num, 10)));
48
+ decoded = decoded.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) =>
49
+ String.fromCharCode(parseInt(hex, 16))
50
+ );
51
+ return { decoded };
52
+ }
53
+
54
+ export function hexEncode(text: string): { encoded: string } {
55
+ return { encoded: Buffer.from(text, "utf-8").toString("hex") };
56
+ }
57
+
58
+ export function hexDecode(hex: string): { decoded: string } {
59
+ const cleaned = hex.replace(/\s+/g, "").replace(/^0x/i, "");
60
+ if (!/^[0-9a-fA-F]*$/.test(cleaned) || cleaned.length % 2 !== 0) {
61
+ throw new Error("Invalid hex string");
62
+ }
63
+ return { decoded: Buffer.from(cleaned, "hex").toString("utf-8") };
64
+ }
65
+
66
+ export function binaryEncode(text: string): { encoded: string } {
67
+ const binary = Array.from(Buffer.from(text, "utf-8"))
68
+ .map((b) => b.toString(2).padStart(8, "0"))
69
+ .join(" ");
70
+ return { encoded: binary };
71
+ }
72
+
73
+ export function binaryDecode(binary: string): { decoded: string } {
74
+ const cleaned = binary.replace(/[^01\s]/g, "");
75
+ const bytes = cleaned.split(/\s+/).filter(Boolean);
76
+ for (const b of bytes) {
77
+ if (b.length !== 8) throw new Error(`Invalid binary octet: "${b}" (must be 8 bits)`);
78
+ }
79
+ const buf = Buffer.from(bytes.map((b) => parseInt(b, 2)));
80
+ return { decoded: buf.toString("utf-8") };
81
+ }
82
+
83
+ interface DetectionResult {
84
+ input: string;
85
+ detected_formats: string[];
86
+ analysis: Record<string, string>;
87
+ }
88
+
89
+ export function detectEncoding(text: string): DetectionResult {
90
+ const detected: string[] = [];
91
+ const analysis: Record<string, string> = {};
92
+ const trimmed = text.trim();
93
+
94
+ // Base64 check
95
+ if (/^[A-Za-z0-9+/]+=*$/.test(trimmed) && trimmed.length >= 4 && trimmed.length % 4 === 0) {
96
+ detected.push("base64");
97
+ try {
98
+ analysis["base64_decoded"] = Buffer.from(trimmed, "base64").toString("utf-8");
99
+ } catch {
100
+ // not valid base64
101
+ }
102
+ }
103
+
104
+ // Hex check
105
+ if (/^(0x)?[0-9a-fA-F]+$/i.test(trimmed) && trimmed.replace(/^0x/i, "").length % 2 === 0) {
106
+ detected.push("hex");
107
+ try {
108
+ analysis["hex_decoded"] = Buffer.from(trimmed.replace(/^0x/i, ""), "hex").toString("utf-8");
109
+ } catch {
110
+ // not valid hex
111
+ }
112
+ }
113
+
114
+ // URL encoded check
115
+ if (/%[0-9A-Fa-f]{2}/.test(trimmed)) {
116
+ detected.push("url_encoded");
117
+ try {
118
+ analysis["url_decoded"] = decodeURIComponent(trimmed);
119
+ } catch {
120
+ // not valid url encoding
121
+ }
122
+ }
123
+
124
+ // HTML entities check
125
+ if (/&(?:#\d+|#x[0-9a-fA-F]+|[a-zA-Z]+);/.test(trimmed)) {
126
+ detected.push("html_entities");
127
+ analysis["html_decoded"] = htmlEntitiesDecode(trimmed).decoded;
128
+ }
129
+
130
+ // Binary check
131
+ if (/^[01]{8}(\s+[01]{8})*$/.test(trimmed)) {
132
+ detected.push("binary");
133
+ try {
134
+ analysis["binary_decoded"] = binaryDecode(trimmed).decoded;
135
+ } catch {
136
+ // not valid binary
137
+ }
138
+ }
139
+
140
+ // JWT check
141
+ if (/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$/.test(trimmed)) {
142
+ detected.push("jwt");
143
+ }
144
+
145
+ if (detected.length === 0) {
146
+ detected.push("plain_text");
147
+ }
148
+
149
+ return { input: trimmed.substring(0, 100), detected_formats: detected, analysis };
150
+ }
@@ -0,0 +1,88 @@
1
+ import crypto from "node:crypto";
2
+
3
+ const SUPPORTED_ALGORITHMS = ["md5", "sha1", "sha256", "sha512"] as const;
4
+ type HashAlgorithm = (typeof SUPPORTED_ALGORITHMS)[number];
5
+
6
+ function normalizeAlgorithm(input: string): HashAlgorithm {
7
+ const normalized = input.toLowerCase().replace(/-/g, "").replace("sha_", "sha") as string;
8
+ const map: Record<string, HashAlgorithm> = {
9
+ md5: "md5",
10
+ sha1: "sha1",
11
+ sha256: "sha256",
12
+ sha512: "sha512",
13
+ };
14
+ const result = map[normalized];
15
+ if (!result) {
16
+ throw new Error(
17
+ `Unsupported algorithm: "${input}". Supported: ${SUPPORTED_ALGORITHMS.join(", ")}`
18
+ );
19
+ }
20
+ return result;
21
+ }
22
+
23
+ function getCryptoName(algo: HashAlgorithm): string {
24
+ const map: Record<HashAlgorithm, string> = {
25
+ md5: "md5",
26
+ sha1: "sha1",
27
+ sha256: "sha256",
28
+ sha512: "sha512",
29
+ };
30
+ return map[algo];
31
+ }
32
+
33
+ export function hashText(
34
+ text: string,
35
+ algorithm: string,
36
+ encoding: "hex" | "base64" = "hex"
37
+ ): { algorithm: string; hash: string; encoding: string; input_length: number } {
38
+ const algo = normalizeAlgorithm(algorithm);
39
+ const hash = crypto.createHash(getCryptoName(algo)).update(text, "utf-8").digest(encoding);
40
+ return {
41
+ algorithm: algo,
42
+ hash,
43
+ encoding,
44
+ input_length: text.length,
45
+ };
46
+ }
47
+
48
+ export function hmacText(
49
+ text: string,
50
+ key: string,
51
+ algorithm: string,
52
+ encoding: "hex" | "base64" = "hex"
53
+ ): { algorithm: string; hmac: string; encoding: string; input_length: number } {
54
+ const algo = normalizeAlgorithm(algorithm);
55
+ const hmac = crypto.createHmac(getCryptoName(algo), key).update(text, "utf-8").digest(encoding);
56
+ return {
57
+ algorithm: algo,
58
+ hmac,
59
+ encoding,
60
+ input_length: text.length,
61
+ };
62
+ }
63
+
64
+ export function compareHashes(
65
+ hash1: string,
66
+ hash2: string
67
+ ): { match: boolean; hash1: string; hash2: string } {
68
+ const a = hash1.toLowerCase().trim();
69
+ const b = hash2.toLowerCase().trim();
70
+ let match: boolean;
71
+ try {
72
+ match = crypto.timingSafeEqual(Buffer.from(a, "utf-8"), Buffer.from(b, "utf-8"));
73
+ } catch {
74
+ match = false;
75
+ }
76
+ return { match, hash1: a, hash2: b };
77
+ }
78
+
79
+ export function hashMultiple(
80
+ text: string,
81
+ encoding: "hex" | "base64" = "hex"
82
+ ): Record<string, string> {
83
+ const result: Record<string, string> = {};
84
+ for (const algo of SUPPORTED_ALGORITHMS) {
85
+ result[algo] = crypto.createHash(getCryptoName(algo)).update(text, "utf-8").digest(encoding);
86
+ }
87
+ return result;
88
+ }