@mrfentmen/hashid-mcp 0.0.0-stage → 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 +32 -2
- package/dist/api.d.ts +13 -0
- package/dist/api.js +139 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +43 -0
- package/package.json +41 -4
package/README.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Hash Identifier
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Identify hash types offline: match a hex or crypt string against known algorithms, list lengths and hashcat modes - fully local.
|
|
4
|
+
|
|
5
|
+
Keyless — no signup, no API key, no environment variables.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx -y @mrfentmen/hashid-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or add it to your MCP client:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"hashid-mcp": { "command": "npx", "args": ["-y", "@mrfentmen/hashid-mcp"] }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Tools
|
|
24
|
+
|
|
25
|
+
| Tool | What it does |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `identify_hash` | Guess which hash algorithm(s) produced a string, with hashcat mode numbers where known. |
|
|
28
|
+
| `hash_details` | Parse the structure of a password hash: bcrypt cost and salt, argon2 parameters, SHA-crypt rounds and salts. |
|
|
29
|
+
| `hash_lengths` | Reference table of common hash algorithms: hex length, byte length and hashcat mode. |
|
|
30
|
+
|
|
31
|
+
## Data source
|
|
32
|
+
|
|
33
|
+
Pattern matching on well-known hash and password-hash formats, implemented locally
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hash Identifier — keyless public API client.
|
|
3
|
+
*/
|
|
4
|
+
export declare class SourceError extends Error {
|
|
5
|
+
}
|
|
6
|
+
export declare function clean(s: unknown): string;
|
|
7
|
+
export declare function identifyHash(a: {
|
|
8
|
+
text: string;
|
|
9
|
+
}): Promise<string>;
|
|
10
|
+
export declare function hashLengths(): Promise<string>;
|
|
11
|
+
export declare function hashDetails(a: {
|
|
12
|
+
text: string;
|
|
13
|
+
}): Promise<string>;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hash Identifier — keyless public API client.
|
|
3
|
+
*/
|
|
4
|
+
export class SourceError extends Error {
|
|
5
|
+
}
|
|
6
|
+
export function clean(s) {
|
|
7
|
+
return String(s ?? "").replace(/\s+/g, " ").trim();
|
|
8
|
+
}
|
|
9
|
+
const RULES = [
|
|
10
|
+
{ name: "bcrypt", test: /^\$2[abxy]\$\d{2}\$[./A-Za-z0-9]{53}$/, mode: "3200" },
|
|
11
|
+
{ name: "argon2", test: /^\$argon2(?:id|i|d)\$v=\d+\$m=\d+,t=\d+,p=\d+\$[A-Za-z0-9+/=]+\$[A-Za-z0-9+/=]+$/, note: "no common hashcat mode; use argon2-aware tools" },
|
|
12
|
+
{ name: "scrypt", test: /^\$scrypt\$[A-Za-z0-9+/=,$*]+\$[A-Za-z0-9+/=]+\$[A-Za-z0-9+/=]+$/, mode: "8900" },
|
|
13
|
+
{ name: "sha512crypt (crypt)", test: /^\$6\$(?:rounds=\d+\$)?[./A-Za-z0-9]{0,16}\$[./A-Za-z0-9]{86}$/, mode: "1800" },
|
|
14
|
+
{ name: "sha256crypt (crypt)", test: /^\$5\$(?:rounds=\d+\$)?[./A-Za-z0-9]{0,16}\$[./A-Za-z0-9]{43}$/, mode: "7400" },
|
|
15
|
+
{ name: "md5crypt (crypt)", test: /^\$1\$[./A-Za-z0-9]{0,8}\$[./A-Za-z0-9]{22}$/, mode: "500" },
|
|
16
|
+
{ name: "phpass (WordPress)", test: /^\$P\$[./A-Za-z0-9]{31}$/, mode: "400" },
|
|
17
|
+
{ name: "Drupal 7", test: /^\$S\$[./A-Za-z0-9]{52}$/, mode: "7900" },
|
|
18
|
+
{ name: "JWT", test: /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$/, note: "three base64url parts; decode with jwt-mcp" },
|
|
19
|
+
{ name: "UUID", test: /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/, note: "not a hash, but often confused with one" },
|
|
20
|
+
{ name: "MySQL 4.1+ native", test: /^\*[A-F0-9]{40}$/, mode: "300" },
|
|
21
|
+
{ name: "PostgreSQL md5", test: /^md5[0-9a-f]{32}$/, mode: "12" },
|
|
22
|
+
{ name: "md5 / md4 / NTLM / LM (hex)", test: /^[a-f0-9]{32}$/, mode: "0 (md5) / 900 (md4) / 1000 (NTLM)" },
|
|
23
|
+
{ name: "sha1 / ripemd160 / MySQL 4.1 (40 hex)", test: /^[a-f0-9]{40}$/, mode: "100 (sha1) / 6000 (ripemd160)" },
|
|
24
|
+
{ name: "sha224", test: /^[a-f0-9]{56}$/, mode: "1300" },
|
|
25
|
+
{ name: "sha256 / sha3-256 / keccak-256 / blake2s (64 hex)", test: /^[a-f0-9]{64}$/, mode: "1400 (sha256) / 17400 (sha3-256)" },
|
|
26
|
+
{ name: "sha384", test: /^[a-f0-9]{96}$/, mode: "10800" },
|
|
27
|
+
{ name: "sha512 / whirlpool (128 hex)", test: /^[a-f0-9]{128}$/, mode: "1700 (sha512) / 6100 (whirlpool)" },
|
|
28
|
+
{ name: "CRC32", test: /^[a-f0-9]{8}$/, mode: "11500" },
|
|
29
|
+
{ name: "CRC16", test: /^[a-f0-9]{4}$/, mode: "no common mode" },
|
|
30
|
+
{ name: "base64 (16 bytes)", test: /^[A-Za-z0-9+/]{22}==$/, note: "22 chars + == is 16 bytes; could wrap any 16-byte hash" },
|
|
31
|
+
{ name: "base64 (32 bytes)", test: /^[A-Za-z0-9+/]{43}=$/, note: "43 chars + = is 32 bytes" },
|
|
32
|
+
];
|
|
33
|
+
const LENGTHS = [
|
|
34
|
+
["CRC16", 4, "no common mode"],
|
|
35
|
+
["CRC32", 8, "11500"],
|
|
36
|
+
["md5 / NTLM / md4", 32, "0 / 1000 / 900"],
|
|
37
|
+
["sha1 / ripemd160", 40, "100 / 6000"],
|
|
38
|
+
["sha224", 56, "1300"],
|
|
39
|
+
["sha256 / sha3-256", 64, "1400 / 17400"],
|
|
40
|
+
["sha384", 96, "10800"],
|
|
41
|
+
["sha512 / whirlpool", 128, "1700 / 6100"],
|
|
42
|
+
];
|
|
43
|
+
export async function identifyHash(a) {
|
|
44
|
+
const text = clean(a.text);
|
|
45
|
+
if (!text)
|
|
46
|
+
throw new SourceError("give the hash text to identify");
|
|
47
|
+
const hits = RULES.filter((r) => r.test.test(text));
|
|
48
|
+
if (!hits.length) {
|
|
49
|
+
return [
|
|
50
|
+
"No known hash format matches this string.",
|
|
51
|
+
"",
|
|
52
|
+
`Length: ${text.length} characters · all hex: ${/^[a-fA-F0-9]+$/.test(text)}`,
|
|
53
|
+
"If it is a hex digest, check the length table with hash_lengths; unusual lengths often mean base64 or a salted format.",
|
|
54
|
+
].join("\n");
|
|
55
|
+
}
|
|
56
|
+
return [
|
|
57
|
+
`${hits.length} possible match(es) for a ${text.length}-character string:`,
|
|
58
|
+
"",
|
|
59
|
+
...hits.map((r) => {
|
|
60
|
+
const bits = [r.mode ? `hashcat mode ${r.mode}` : "", r.note ?? ""].filter(Boolean);
|
|
61
|
+
return `- ${r.name}${bits.length ? ` · ${bits.join(" · ")}` : ""}`;
|
|
62
|
+
}),
|
|
63
|
+
hits.length > 1 ? "\nSeveral formats share a shape; the exact one needs context (a prefix, a length table or the source system)." : "",
|
|
64
|
+
].filter(Boolean).join("\n");
|
|
65
|
+
}
|
|
66
|
+
export async function hashLengths() {
|
|
67
|
+
return [
|
|
68
|
+
"Common hex digest lengths:",
|
|
69
|
+
"",
|
|
70
|
+
"algorithm hex chars hashcat mode",
|
|
71
|
+
...LENGTHS.map(([name, len, mode]) => `${name.padEnd(28)} ${String(len).padStart(9)} ${mode}`),
|
|
72
|
+
"",
|
|
73
|
+
"Password hashes (bcrypt, argon2, crypt formats) carry their own fixed layouts - use identify_hash or hash_details on a sample.",
|
|
74
|
+
].join("\n");
|
|
75
|
+
}
|
|
76
|
+
const CRYPT_NAMES = { "1": "md5crypt", "5": "sha256crypt", "6": "sha512crypt" };
|
|
77
|
+
const CRYPT_MODES = { "1": "500", "5": "7400", "6": "1800" };
|
|
78
|
+
export async function hashDetails(a) {
|
|
79
|
+
const text = clean(a.text);
|
|
80
|
+
if (!text)
|
|
81
|
+
throw new SourceError("give a password hash string to inspect");
|
|
82
|
+
if (/^\$2[abxy]\$/.test(text)) {
|
|
83
|
+
const m = text.match(/^\$2([abxy])\$(\d{2})\$([./A-Za-z0-9]{22})([./A-Za-z0-9]{31})$/);
|
|
84
|
+
if (!m)
|
|
85
|
+
throw new SourceError("this looks like bcrypt but the layout is off (expected $2b$cost$22-char-salt+31-char-hash)");
|
|
86
|
+
const cost = Number(m[2]);
|
|
87
|
+
return [
|
|
88
|
+
"bcrypt",
|
|
89
|
+
"",
|
|
90
|
+
`version: 2${m[1]}`,
|
|
91
|
+
`cost factor: ${cost} (2^${cost} = ${2 ** cost} key expansions per guess; ${cost < 10 ? "weak by today's standards" : cost >= 12 ? "strong" : "acceptable"})`,
|
|
92
|
+
`salt (22 chars, base64): ${m[3]}`,
|
|
93
|
+
`hash (31 chars): ${m[4]}`,
|
|
94
|
+
"",
|
|
95
|
+
"hashcat mode 3200; the input for cracking is hash:salt or the full string depending on the tool.",
|
|
96
|
+
].join("\n");
|
|
97
|
+
}
|
|
98
|
+
if (/^\$argon2/.test(text)) {
|
|
99
|
+
const m = text.match(/^\$argon2(id|i|d)\$v=(\d+)\$m=(\d+),t=(\d+),p=(\d+)\$([A-Za-z0-9+/=]+)\$([A-Za-z0-9+/=]+)$/);
|
|
100
|
+
if (!m)
|
|
101
|
+
throw new SourceError("this looks like argon2 but the parameter block is malformed");
|
|
102
|
+
return [
|
|
103
|
+
`argon2${m[1]}`,
|
|
104
|
+
"",
|
|
105
|
+
`version: ${m[2]}`,
|
|
106
|
+
`memory: ${m[3]} KiB`,
|
|
107
|
+
`iterations: ${m[4]}`,
|
|
108
|
+
`parallelism: ${m[5]}`,
|
|
109
|
+
`salt (base64): ${m[6]}`,
|
|
110
|
+
`hash (base64): ${m[7]}`,
|
|
111
|
+
"",
|
|
112
|
+
"No common hashcat mode; use argon2-aware tools and keep memory cost high.",
|
|
113
|
+
].join("\n");
|
|
114
|
+
}
|
|
115
|
+
if (/^\$[156]\$/.test(text)) {
|
|
116
|
+
const m = text.match(/^\$([156])\$(rounds=(\d+)\$)?([./A-Za-z0-9]{0,16})\$(.+)$/);
|
|
117
|
+
if (!m)
|
|
118
|
+
throw new SourceError("this looks like a crypt hash but the layout is off");
|
|
119
|
+
const family = CRYPT_NAMES[m[1]] ?? "crypt";
|
|
120
|
+
return [
|
|
121
|
+
family,
|
|
122
|
+
"",
|
|
123
|
+
`rounds: ${m[3] ?? "default (5000)"}`,
|
|
124
|
+
`salt: ${m[4] || "(empty)"}`,
|
|
125
|
+
`digest (${m[5].length} chars): ${m[5].slice(0, 40)}${m[5].length > 40 ? "..." : ""}`,
|
|
126
|
+
"",
|
|
127
|
+
`hashcat mode ${CRYPT_MODES[m[1]] ?? "varies"}`,
|
|
128
|
+
].join("\n");
|
|
129
|
+
}
|
|
130
|
+
const hits = RULES.filter((r) => r.test.test(text));
|
|
131
|
+
if (!hits.length)
|
|
132
|
+
throw new SourceError("this string does not match a known password-hash format");
|
|
133
|
+
return [
|
|
134
|
+
`Plain hash format(s): ${hits.map((h) => h.name).join(", ")}`,
|
|
135
|
+
"",
|
|
136
|
+
`Length: ${text.length} characters`,
|
|
137
|
+
"For a plain digest there are no embedded parameters; identify_hash and hash_lengths cover the interpretation.",
|
|
138
|
+
].join("\n");
|
|
139
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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("hashid-mcp running on stdio");
|
|
8
|
+
}
|
|
9
|
+
main().catch((err) => {
|
|
10
|
+
console.error("Fatal error:", err);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { hashDetails, hashLengths, identifyHash, SourceError } from "./api.js";
|
|
4
|
+
const text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
5
|
+
function errorMessage(e) {
|
|
6
|
+
if (e instanceof SourceError || e instanceof Error)
|
|
7
|
+
return `Error: ${e.message}`;
|
|
8
|
+
return `Error: ${String(e)}`;
|
|
9
|
+
}
|
|
10
|
+
export function createServer() {
|
|
11
|
+
const server = new McpServer({ name: "hashid-mcp", version: "1.0.0" });
|
|
12
|
+
server.tool("identify_hash", "Guess which hash algorithm(s) produced a string, with hashcat mode numbers where known.", {
|
|
13
|
+
text: z.string().min(1).max(512).describe("The hash text, e.g. 5d41402abc4b2a76b9719d911017c592")
|
|
14
|
+
}, async (a) => {
|
|
15
|
+
try {
|
|
16
|
+
return text(await identifyHash(a));
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
return { content: [{ type: "text", text: errorMessage(e) }], isError: true };
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
server.tool("hash_details", "Parse the structure of a password hash: bcrypt cost and salt, argon2 parameters, SHA-crypt rounds and salts.", {
|
|
23
|
+
text: z.string().min(1).max(512).describe("A password hash string like $2b$10$... or $argon2id$v=19$...")
|
|
24
|
+
}, async (a) => {
|
|
25
|
+
try {
|
|
26
|
+
return text(await hashDetails(a));
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
return { content: [{ type: "text", text: errorMessage(e) }], isError: true };
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
server.tool("hash_lengths", "Reference table of common hash algorithms: hex length, byte length and hashcat mode.", {
|
|
33
|
+
// no arguments
|
|
34
|
+
}, async (a) => {
|
|
35
|
+
try {
|
|
36
|
+
return text(await hashLengths());
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
return { content: [{ type: "text", text: errorMessage(e) }], isError: true };
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
return server;
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrfentmen/hashid-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Identify hash types offline: match a hex or crypt string against known algorithms, list lengths and hashcat modes - fully local.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"mcpName": "io.github.mrfentmen/hashid-mcp",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/mrfentmen/awesome-servers.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"hashid-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
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"hash",
|
|
25
|
+
"security",
|
|
26
|
+
"cracking",
|
|
27
|
+
"hashcat",
|
|
28
|
+
"offline",
|
|
29
|
+
"keyless"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
34
|
+
"zod": "^3.23.8"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.0.0",
|
|
38
|
+
"typescript": "^5.6.0"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=20"
|
|
42
|
+
}
|
|
43
|
+
}
|