hacktricks-mcp-server 1.3.1
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/.github/workflows/publish-mcp.yml +49 -0
- package/.github/workflows/test-event.json +9 -0
- package/.gitmodules +3 -0
- package/.mcp.json +11 -0
- package/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +238 -0
- package/TESTING.md +188 -0
- package/bun.lock +202 -0
- package/dist/index.js +779 -0
- package/example-settings.json +9 -0
- package/package.json +32 -0
- package/server.json +21 -0
- package/src/index.ts +952 -0
- package/test-mcp.js +127 -0
- package/tsconfig.json +16 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { execFile } from "child_process";
|
|
6
|
+
import { promisify } from "util";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { dirname, join } from "path";
|
|
9
|
+
import { readFile, readdir } from "fs/promises";
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
// Get the directory where this script is running
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
const HACKTRICKS_PATH = join(__dirname, "..", "hacktricks");
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// HELPER FUNCTIONS
|
|
17
|
+
// ============================================================================
|
|
18
|
+
/**
|
|
19
|
+
* Extract title (first H1) from markdown content
|
|
20
|
+
*/
|
|
21
|
+
function extractTitle(content) {
|
|
22
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
23
|
+
return match ? match[1].trim() : "Untitled";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Extract all section headers from markdown content
|
|
27
|
+
*/
|
|
28
|
+
function extractHeaders(content) {
|
|
29
|
+
const headers = [];
|
|
30
|
+
const lines = content.split("\n");
|
|
31
|
+
for (let i = 0; i < lines.length; i++) {
|
|
32
|
+
const match = lines[i].match(/^(#{1,6})\s+(.+)$/);
|
|
33
|
+
if (match) {
|
|
34
|
+
headers.push({
|
|
35
|
+
level: match[1].length,
|
|
36
|
+
text: match[2].trim(),
|
|
37
|
+
line: i + 1,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return headers;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Find section headers near a given line number
|
|
45
|
+
*/
|
|
46
|
+
function findNearestSection(headers, targetLine) {
|
|
47
|
+
let nearestHeader = null;
|
|
48
|
+
for (const header of headers) {
|
|
49
|
+
if (header.line <= targetLine) {
|
|
50
|
+
nearestHeader = header;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return nearestHeader ? nearestHeader.text : null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Extract a specific section from markdown content
|
|
60
|
+
*/
|
|
61
|
+
function extractSection(content, sectionName) {
|
|
62
|
+
const lines = content.split("\n");
|
|
63
|
+
const searchLower = sectionName.toLowerCase();
|
|
64
|
+
let startLine = -1;
|
|
65
|
+
let startLevel = 0;
|
|
66
|
+
// Find the section header
|
|
67
|
+
for (let i = 0; i < lines.length; i++) {
|
|
68
|
+
const match = lines[i].match(/^(#{1,6})\s+(.+)$/);
|
|
69
|
+
if (match && match[2].toLowerCase().includes(searchLower)) {
|
|
70
|
+
startLine = i;
|
|
71
|
+
startLevel = match[1].length;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (startLine === -1)
|
|
76
|
+
return null;
|
|
77
|
+
// Find the end of the section (next header of same or higher level)
|
|
78
|
+
let endLine = lines.length;
|
|
79
|
+
for (let i = startLine + 1; i < lines.length; i++) {
|
|
80
|
+
const match = lines[i].match(/^(#{1,6})\s+/);
|
|
81
|
+
if (match && match[1].length <= startLevel) {
|
|
82
|
+
endLine = i;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return lines.slice(startLine, endLine).join("\n");
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Extract all code blocks from markdown content
|
|
90
|
+
*/
|
|
91
|
+
function extractCodeBlocks(content) {
|
|
92
|
+
const blocks = [];
|
|
93
|
+
const regex = /```(\w*)\n([\s\S]*?)```/g;
|
|
94
|
+
let match;
|
|
95
|
+
while ((match = regex.exec(content)) !== null) {
|
|
96
|
+
blocks.push({
|
|
97
|
+
language: match[1] || "text",
|
|
98
|
+
code: match[2].trim(),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return blocks;
|
|
102
|
+
}
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// CORE FUNCTIONS
|
|
105
|
+
// ============================================================================
|
|
106
|
+
async function searchHackTricks(query, category, limit = 50) {
|
|
107
|
+
try {
|
|
108
|
+
if (!query || query.trim().length === 0) {
|
|
109
|
+
throw new Error("Search query cannot be empty");
|
|
110
|
+
}
|
|
111
|
+
const searchPath = category
|
|
112
|
+
? join(HACKTRICKS_PATH, "src", category)
|
|
113
|
+
: HACKTRICKS_PATH;
|
|
114
|
+
console.error(`[HackTricks MCP] Searching for: "${query}"${category ? ` in category: ${category}` : ""}`);
|
|
115
|
+
const { stdout } = await execFileAsync("rg", ["-n", "-i", "--type", "md", query, searchPath], { maxBuffer: 1024 * 1024 * 10 });
|
|
116
|
+
const results = [];
|
|
117
|
+
const lines = stdout.trim().split("\n");
|
|
118
|
+
for (const line of lines) {
|
|
119
|
+
const match = line.match(/^([^:]+):(\d+):(.+)$/);
|
|
120
|
+
if (match) {
|
|
121
|
+
const [, file, lineNum, content] = match;
|
|
122
|
+
results.push({
|
|
123
|
+
file: file.replace(HACKTRICKS_PATH + "/", ""),
|
|
124
|
+
line: parseInt(lineNum, 10),
|
|
125
|
+
content: content.trim(),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const limitedResults = results.slice(0, limit);
|
|
130
|
+
console.error(`[HackTricks MCP] Found ${results.length} results (showing ${limitedResults.length})`);
|
|
131
|
+
return limitedResults;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error.code === 1) {
|
|
135
|
+
console.error(`[HackTricks MCP] No results found for: "${query}"`);
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
if (error.code === 2) {
|
|
139
|
+
console.error(`[HackTricks MCP] Invalid search pattern: ${error.message}`);
|
|
140
|
+
throw new Error(`Invalid search pattern: ${error.message}`);
|
|
141
|
+
}
|
|
142
|
+
console.error(`[HackTricks MCP] Search failed: ${error.message}`);
|
|
143
|
+
throw new Error(`Search failed: ${error.message}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Group search results by file with context
|
|
148
|
+
*/
|
|
149
|
+
async function searchHackTricksGrouped(query, category, limit = 50) {
|
|
150
|
+
const rawResults = await searchHackTricks(query, category, limit * 3); // Get more raw results for better grouping
|
|
151
|
+
// Group by file
|
|
152
|
+
const fileGroups = new Map();
|
|
153
|
+
for (const result of rawResults) {
|
|
154
|
+
const existing = fileGroups.get(result.file) || [];
|
|
155
|
+
existing.push(result);
|
|
156
|
+
fileGroups.set(result.file, existing);
|
|
157
|
+
}
|
|
158
|
+
// Process each file group
|
|
159
|
+
const groupedResults = [];
|
|
160
|
+
for (const [file, matches] of fileGroups) {
|
|
161
|
+
try {
|
|
162
|
+
const filePath = join(HACKTRICKS_PATH, file);
|
|
163
|
+
const content = await readFile(filePath, "utf-8");
|
|
164
|
+
const headers = extractHeaders(content);
|
|
165
|
+
const title = extractTitle(content);
|
|
166
|
+
// Find unique sections that contain matches
|
|
167
|
+
const sections = new Set();
|
|
168
|
+
for (const match of matches) {
|
|
169
|
+
const section = findNearestSection(headers, match.line);
|
|
170
|
+
if (section)
|
|
171
|
+
sections.add(section);
|
|
172
|
+
}
|
|
173
|
+
groupedResults.push({
|
|
174
|
+
file,
|
|
175
|
+
title,
|
|
176
|
+
matchCount: matches.length,
|
|
177
|
+
relevantSections: Array.from(sections).slice(0, 5),
|
|
178
|
+
topMatches: matches.slice(0, 3).map((m) => ({
|
|
179
|
+
line: m.line,
|
|
180
|
+
content: m.content.length > 150 ? m.content.slice(0, 150) + "..." : m.content,
|
|
181
|
+
})),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// If file reading fails, still include basic info
|
|
186
|
+
groupedResults.push({
|
|
187
|
+
file,
|
|
188
|
+
title: file.split("/").pop()?.replace(".md", "") || "Unknown",
|
|
189
|
+
matchCount: matches.length,
|
|
190
|
+
relevantSections: [],
|
|
191
|
+
topMatches: matches.slice(0, 3).map((m) => ({
|
|
192
|
+
line: m.line,
|
|
193
|
+
content: m.content.length > 150 ? m.content.slice(0, 150) + "..." : m.content,
|
|
194
|
+
})),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Sort by match count (most relevant first)
|
|
199
|
+
groupedResults.sort((a, b) => b.matchCount - a.matchCount);
|
|
200
|
+
return groupedResults.slice(0, limit);
|
|
201
|
+
}
|
|
202
|
+
async function getPage(path) {
|
|
203
|
+
try {
|
|
204
|
+
if (!path || path.trim().length === 0) {
|
|
205
|
+
throw new Error("File path cannot be empty");
|
|
206
|
+
}
|
|
207
|
+
const normalizedPath = path.replace(/\\/g, "/");
|
|
208
|
+
if (normalizedPath.includes("..") || normalizedPath.startsWith("/")) {
|
|
209
|
+
throw new Error("Invalid file path: directory traversal not allowed");
|
|
210
|
+
}
|
|
211
|
+
const filePath = join(HACKTRICKS_PATH, normalizedPath);
|
|
212
|
+
if (!filePath.startsWith(HACKTRICKS_PATH)) {
|
|
213
|
+
throw new Error("Invalid file path: must be within HackTricks directory");
|
|
214
|
+
}
|
|
215
|
+
console.error(`[HackTricks MCP] Reading file: ${normalizedPath}`);
|
|
216
|
+
const content = await readFile(filePath, "utf-8");
|
|
217
|
+
console.error(`[HackTricks MCP] File size: ${content.length} bytes`);
|
|
218
|
+
return content;
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
if (error.code === "ENOENT") {
|
|
222
|
+
console.error(`[HackTricks MCP] File not found: ${path}`);
|
|
223
|
+
throw new Error(`File not found: ${path}`);
|
|
224
|
+
}
|
|
225
|
+
if (error.code === "EISDIR") {
|
|
226
|
+
console.error(`[HackTricks MCP] Path is a directory: ${path}`);
|
|
227
|
+
throw new Error(`Path is a directory, not a file: ${path}`);
|
|
228
|
+
}
|
|
229
|
+
console.error(`[HackTricks MCP] Error reading file: ${error.message}`);
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async function getPageOutline(path) {
|
|
234
|
+
const content = await getPage(path);
|
|
235
|
+
const headers = extractHeaders(content);
|
|
236
|
+
if (headers.length === 0) {
|
|
237
|
+
return "No headers found in this file.";
|
|
238
|
+
}
|
|
239
|
+
// Format headers with indentation based on level
|
|
240
|
+
return headers
|
|
241
|
+
.map((h) => {
|
|
242
|
+
const indent = " ".repeat(h.level - 1);
|
|
243
|
+
return `${indent}${"#".repeat(h.level)} ${h.text}`;
|
|
244
|
+
})
|
|
245
|
+
.join("\n");
|
|
246
|
+
}
|
|
247
|
+
async function getPageSection(path, sectionName) {
|
|
248
|
+
const content = await getPage(path);
|
|
249
|
+
const section = extractSection(content, sectionName);
|
|
250
|
+
if (!section) {
|
|
251
|
+
throw new Error(`Section "${sectionName}" not found in ${path}`);
|
|
252
|
+
}
|
|
253
|
+
return section;
|
|
254
|
+
}
|
|
255
|
+
async function getPageCheatsheet(path) {
|
|
256
|
+
const content = await getPage(path);
|
|
257
|
+
const blocks = extractCodeBlocks(content);
|
|
258
|
+
if (blocks.length === 0) {
|
|
259
|
+
return "No code blocks found in this file.";
|
|
260
|
+
}
|
|
261
|
+
return blocks
|
|
262
|
+
.map((b) => `\`\`\`${b.language}\n${b.code}\n\`\`\``)
|
|
263
|
+
.join("\n\n");
|
|
264
|
+
}
|
|
265
|
+
async function listDirectoryTree(dirPath, basePath = HACKTRICKS_PATH, depth = 0, maxDepth = 3) {
|
|
266
|
+
if (depth > maxDepth)
|
|
267
|
+
return [];
|
|
268
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
269
|
+
const tree = [];
|
|
270
|
+
for (const entry of entries) {
|
|
271
|
+
if (entry.name.startsWith(".") || entry.name === "images")
|
|
272
|
+
continue;
|
|
273
|
+
const fullPath = join(dirPath, entry.name);
|
|
274
|
+
const relativePath = fullPath.replace(basePath + "/", "");
|
|
275
|
+
if (entry.isDirectory()) {
|
|
276
|
+
const children = await listDirectoryTree(fullPath, basePath, depth + 1, maxDepth);
|
|
277
|
+
tree.push({
|
|
278
|
+
name: entry.name,
|
|
279
|
+
path: relativePath,
|
|
280
|
+
type: "directory",
|
|
281
|
+
children: children.length > 0 ? children : undefined,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
else if (entry.name.endsWith(".md")) {
|
|
285
|
+
tree.push({
|
|
286
|
+
name: entry.name,
|
|
287
|
+
path: relativePath,
|
|
288
|
+
type: "file",
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return tree.sort((a, b) => {
|
|
293
|
+
if (a.type !== b.type) {
|
|
294
|
+
return a.type === "directory" ? -1 : 1;
|
|
295
|
+
}
|
|
296
|
+
return a.name.localeCompare(b.name);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
async function listCategories(category) {
|
|
300
|
+
try {
|
|
301
|
+
const srcPath = join(HACKTRICKS_PATH, "src");
|
|
302
|
+
if (category) {
|
|
303
|
+
console.error(`[HackTricks MCP] Listing contents of category: ${category}`);
|
|
304
|
+
const categoryPath = join(srcPath, category);
|
|
305
|
+
const tree = await listDirectoryTree(categoryPath, HACKTRICKS_PATH);
|
|
306
|
+
console.error(`[HackTricks MCP] Found ${tree.length} items in ${category}`);
|
|
307
|
+
return tree;
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
console.error(`[HackTricks MCP] Listing categories in ${srcPath}`);
|
|
311
|
+
const entries = await readdir(srcPath, { withFileTypes: true });
|
|
312
|
+
const categories = entries
|
|
313
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "images")
|
|
314
|
+
.map((entry) => entry.name)
|
|
315
|
+
.sort();
|
|
316
|
+
console.error(`[HackTricks MCP] Found ${categories.length} categories`);
|
|
317
|
+
return categories;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
console.error(`[HackTricks MCP] Error listing categories: ${error.message}`);
|
|
322
|
+
throw new Error(`Failed to list categories: ${error.message}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Common abbreviation aliases for better search matching
|
|
326
|
+
const SEARCH_ALIASES = {
|
|
327
|
+
"sqli": ["SQL injection", "SQLi"],
|
|
328
|
+
"xss": ["Cross-site scripting", "XSS"],
|
|
329
|
+
"rce": ["Remote code execution", "RCE", "command injection"],
|
|
330
|
+
"lfi": ["Local file inclusion", "LFI"],
|
|
331
|
+
"rfi": ["Remote file inclusion", "RFI"],
|
|
332
|
+
"ssrf": ["Server-side request forgery", "SSRF"],
|
|
333
|
+
"csrf": ["Cross-site request forgery", "CSRF"],
|
|
334
|
+
"xxe": ["XML external entity", "XXE"],
|
|
335
|
+
"ssti": ["Server-side template injection", "SSTI"],
|
|
336
|
+
"idor": ["Insecure direct object reference", "IDOR"],
|
|
337
|
+
"jwt": ["JSON Web Token", "JWT"],
|
|
338
|
+
"suid": ["SUID", "setuid"],
|
|
339
|
+
"privesc": ["privilege escalation", "privesc"],
|
|
340
|
+
"deserialization": ["deserialization", "insecure deserialization"],
|
|
341
|
+
};
|
|
342
|
+
// Priority sections to extract for quick lookup
|
|
343
|
+
const PRIORITY_SECTIONS = [
|
|
344
|
+
"exploitation",
|
|
345
|
+
"exploit",
|
|
346
|
+
"example",
|
|
347
|
+
"poc",
|
|
348
|
+
"proof of concept",
|
|
349
|
+
"payload",
|
|
350
|
+
"bypass",
|
|
351
|
+
"attack",
|
|
352
|
+
"abuse",
|
|
353
|
+
"technique",
|
|
354
|
+
];
|
|
355
|
+
/**
|
|
356
|
+
* Quick lookup: Search + get best page + extract exploitation-relevant sections
|
|
357
|
+
* One-shot answer for "how do I exploit X"
|
|
358
|
+
*/
|
|
359
|
+
async function quickLookup(topic, category) {
|
|
360
|
+
// Expand aliases
|
|
361
|
+
const topicLower = topic.toLowerCase();
|
|
362
|
+
let searchTerms = [topic];
|
|
363
|
+
if (SEARCH_ALIASES[topicLower]) {
|
|
364
|
+
searchTerms = [...searchTerms, ...SEARCH_ALIASES[topicLower]];
|
|
365
|
+
}
|
|
366
|
+
console.error(`[HackTricks MCP] Quick lookup: "${topic}" (terms: ${searchTerms.join(", ")})`);
|
|
367
|
+
// Search for the topic
|
|
368
|
+
let bestResult = null;
|
|
369
|
+
let bestScore = 0;
|
|
370
|
+
for (const term of searchTerms) {
|
|
371
|
+
try {
|
|
372
|
+
const results = await searchHackTricksGrouped(term, category, 10);
|
|
373
|
+
if (results.length > 0) {
|
|
374
|
+
for (const result of results) {
|
|
375
|
+
// Score based on relevance
|
|
376
|
+
let score = result.matchCount;
|
|
377
|
+
const titleLower = result.title.toLowerCase();
|
|
378
|
+
const termLower = term.toLowerCase();
|
|
379
|
+
const pathLower = result.file.toLowerCase();
|
|
380
|
+
// Strong preference for title containing search term
|
|
381
|
+
if (titleLower.includes(termLower) || titleLower.includes(topicLower)) {
|
|
382
|
+
score += 100;
|
|
383
|
+
}
|
|
384
|
+
// Prefer README files (main pages for topic folders)
|
|
385
|
+
if (pathLower.endsWith("readme.md") && pathLower.includes(topicLower)) {
|
|
386
|
+
score += 200; // This is likely THE main page for the topic
|
|
387
|
+
}
|
|
388
|
+
// Prefer folder names matching topic (e.g., ssrf-server-side-request-forgery/)
|
|
389
|
+
if (pathLower.includes(`/${topicLower}`) || pathLower.includes(`${topicLower}-`)) {
|
|
390
|
+
score += 50;
|
|
391
|
+
}
|
|
392
|
+
// Bonus for having exploitation-related sections
|
|
393
|
+
const hasExploitSection = result.relevantSections.some((s) => PRIORITY_SECTIONS.some((p) => s.toLowerCase().includes(p)));
|
|
394
|
+
if (hasExploitSection) {
|
|
395
|
+
score += 10;
|
|
396
|
+
}
|
|
397
|
+
if (score > bestScore) {
|
|
398
|
+
bestScore = score;
|
|
399
|
+
bestResult = result;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
// Continue with other terms
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (!bestResult) {
|
|
409
|
+
throw new Error(`No results found for: "${topic}". Try a different term or specify a category.`);
|
|
410
|
+
}
|
|
411
|
+
console.error(`[HackTricks MCP] Best match: ${bestResult.file} (${bestResult.matchCount} matches)`);
|
|
412
|
+
// Read the page content
|
|
413
|
+
const content = await getPage(bestResult.file);
|
|
414
|
+
const title = extractTitle(content);
|
|
415
|
+
const headers = extractHeaders(content);
|
|
416
|
+
// Extract priority sections
|
|
417
|
+
const extractedSections = [];
|
|
418
|
+
for (const header of headers) {
|
|
419
|
+
const headerLower = header.text.toLowerCase();
|
|
420
|
+
if (PRIORITY_SECTIONS.some((p) => headerLower.includes(p))) {
|
|
421
|
+
try {
|
|
422
|
+
const section = extractSection(content, header.text);
|
|
423
|
+
if (section && section.length > 50) {
|
|
424
|
+
extractedSections.push(section);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
// Section extraction failed, skip
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
// If no priority sections found, get the first substantial section after title
|
|
433
|
+
if (extractedSections.length === 0 && headers.length > 1) {
|
|
434
|
+
try {
|
|
435
|
+
const section = extractSection(content, headers[1].text);
|
|
436
|
+
if (section) {
|
|
437
|
+
extractedSections.push(section);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
// Fallback failed
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// Extract code blocks
|
|
445
|
+
const codeBlocks = extractCodeBlocks(content);
|
|
446
|
+
const codeOutput = codeBlocks.length > 0
|
|
447
|
+
? codeBlocks.slice(0, 5).map((b) => `\`\`\`${b.language}\n${b.code}\n\`\`\``).join("\n\n")
|
|
448
|
+
: "No code blocks found.";
|
|
449
|
+
return {
|
|
450
|
+
page: bestResult.file,
|
|
451
|
+
title,
|
|
452
|
+
sections: extractedSections.length > 0
|
|
453
|
+
? extractedSections.join("\n\n---\n\n")
|
|
454
|
+
: "No exploitation sections found. Use get_hacktricks_page for full content.",
|
|
455
|
+
codeBlocks: codeOutput,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
// ============================================================================
|
|
459
|
+
// MCP SERVER SETUP
|
|
460
|
+
// ============================================================================
|
|
461
|
+
const server = new Server({
|
|
462
|
+
name: "hacktricks-mcp",
|
|
463
|
+
version: "1.3.0",
|
|
464
|
+
}, {
|
|
465
|
+
capabilities: {
|
|
466
|
+
tools: {},
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
// List available tools
|
|
470
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
471
|
+
return {
|
|
472
|
+
tools: [
|
|
473
|
+
{
|
|
474
|
+
name: "search_hacktricks",
|
|
475
|
+
description: "Search HackTricks for pentesting techniques, exploits, and security info. Returns results GROUPED BY FILE with: page title, match count, relevant sections, and top matches. WORKFLOW: search ā get_hacktricks_outline (see structure) ā get_hacktricks_section (read specific part). ALWAYS use category filter when possible - saves time and tokens.",
|
|
476
|
+
inputSchema: {
|
|
477
|
+
type: "object",
|
|
478
|
+
properties: {
|
|
479
|
+
query: {
|
|
480
|
+
type: "string",
|
|
481
|
+
description: "Search term. Be specific (e.g., 'SUID privilege escalation' not just 'privilege'). Supports regex.",
|
|
482
|
+
},
|
|
483
|
+
category: {
|
|
484
|
+
type: "string",
|
|
485
|
+
description: "STRONGLY RECOMMENDED. Common categories: 'pentesting-web' (XSS,SQLi,SSRF), 'linux-hardening' (privesc,capabilities), 'network-services-pentesting' (SMB,FTP,SSH), 'windows-hardening', 'mobile-pentesting', 'cloud-security'. Use list_hacktricks_categories to see all.",
|
|
486
|
+
},
|
|
487
|
+
limit: {
|
|
488
|
+
type: "number",
|
|
489
|
+
description: "Max files to return (default: 20). Lower = faster. Set to 5 for quick lookups.",
|
|
490
|
+
default: 20,
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
required: ["query"],
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
name: "get_hacktricks_page",
|
|
498
|
+
description: "Get FULL page content. ā ļø EXPENSIVE: Pages average 3000-15000 tokens. PREFER: get_hacktricks_section for specific topics, get_hacktricks_cheatsheet for just commands. Only use this when you need the complete page or multiple sections.",
|
|
499
|
+
inputSchema: {
|
|
500
|
+
type: "object",
|
|
501
|
+
properties: {
|
|
502
|
+
path: {
|
|
503
|
+
type: "string",
|
|
504
|
+
description: "Path from search results (e.g., 'src/linux-hardening/privilege-escalation/README.md')",
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
required: ["path"],
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
name: "get_hacktricks_outline",
|
|
512
|
+
description: "Get TABLE OF CONTENTS (all section headers) of a page. Returns ~20-50 lines showing page structure. Use this FIRST after search to: (1) verify page is relevant, (2) find exact section names for get_hacktricks_section. Cost: ~100 tokens vs 3000+ for full page.",
|
|
513
|
+
inputSchema: {
|
|
514
|
+
type: "object",
|
|
515
|
+
properties: {
|
|
516
|
+
path: {
|
|
517
|
+
type: "string",
|
|
518
|
+
description: "Path from search results",
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
required: ["path"],
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
name: "get_hacktricks_section",
|
|
526
|
+
description: "Extract ONE SECTION from a page. MOST EFFICIENT way to read content. Typical sections: 'Exploitation', 'Enumeration', 'Prevention', 'Example', 'Payload', 'PoC', 'Bypass'. Use get_hacktricks_outline first to see exact section names. Returns ~200-500 tokens vs 3000+ for full page.",
|
|
527
|
+
inputSchema: {
|
|
528
|
+
type: "object",
|
|
529
|
+
properties: {
|
|
530
|
+
path: {
|
|
531
|
+
type: "string",
|
|
532
|
+
description: "Path from search results",
|
|
533
|
+
},
|
|
534
|
+
section: {
|
|
535
|
+
type: "string",
|
|
536
|
+
description: "Section name (partial match, case-insensitive). From outline or common: 'exploitation', 'enumeration', 'bypass', 'payload', 'example', 'poc', 'prevention', 'detection'",
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
required: ["path", "section"],
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
name: "get_hacktricks_cheatsheet",
|
|
544
|
+
description: "Extract ALL CODE BLOCKS from a page (commands, payloads, scripts, one-liners). Skips explanatory text. Perfect for: 'give me the exploit command', 'show me the payload', 'what's the syntax'. Returns code with language tags (bash, python, etc.).",
|
|
545
|
+
inputSchema: {
|
|
546
|
+
type: "object",
|
|
547
|
+
properties: {
|
|
548
|
+
path: {
|
|
549
|
+
type: "string",
|
|
550
|
+
description: "Path from search results",
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
required: ["path"],
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
name: "list_hacktricks_categories",
|
|
558
|
+
description: "Browse HackTricks structure. Without params: list all categories. With category: show all pages in that category. Use when: (1) unsure which category to search, (2) want to explore what's available, (3) need exact file paths.",
|
|
559
|
+
inputSchema: {
|
|
560
|
+
type: "object",
|
|
561
|
+
properties: {
|
|
562
|
+
category: {
|
|
563
|
+
type: "string",
|
|
564
|
+
description: "Category to explore. Popular: 'pentesting-web', 'linux-hardening', 'windows-hardening', 'network-services-pentesting', 'mobile-pentesting'",
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
name: "hacktricks_quick_lookup",
|
|
571
|
+
description: "ā” ONE-SHOT exploitation lookup. Searches, finds best page, and returns exploitation sections + code blocks. Use for: 'how do I exploit X', 'give me X payload', 'X attack technique'. Handles aliases (sqliāSQL injection, xssāCross-site scripting, rce, lfi, ssrf, etc.). Returns: page title, exploitation sections, and top 5 code blocks.",
|
|
572
|
+
inputSchema: {
|
|
573
|
+
type: "object",
|
|
574
|
+
properties: {
|
|
575
|
+
topic: {
|
|
576
|
+
type: "string",
|
|
577
|
+
description: "Attack/technique to look up. Examples: 'SUID', 'sqli', 'xss', 'ssrf', 'jwt', 'docker escape', 'kerberoasting'. Aliases auto-expand.",
|
|
578
|
+
},
|
|
579
|
+
category: {
|
|
580
|
+
type: "string",
|
|
581
|
+
description: "Optional category filter. Speeds up search. Common: 'pentesting-web', 'linux-hardening', 'windows-hardening', 'network-services-pentesting'",
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
required: ["topic"],
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
],
|
|
588
|
+
};
|
|
589
|
+
});
|
|
590
|
+
// Handle tool calls
|
|
591
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
592
|
+
const { name, arguments: args } = request.params;
|
|
593
|
+
if (!args) {
|
|
594
|
+
throw new Error("Arguments are required");
|
|
595
|
+
}
|
|
596
|
+
if (name === "search_hacktricks") {
|
|
597
|
+
const query = args.query;
|
|
598
|
+
const category = args.category;
|
|
599
|
+
const limit = Math.min(args.limit || 20, 50);
|
|
600
|
+
if (!query) {
|
|
601
|
+
throw new Error("Query parameter is required");
|
|
602
|
+
}
|
|
603
|
+
const results = await searchHackTricksGrouped(query, category, limit);
|
|
604
|
+
if (results.length === 0) {
|
|
605
|
+
return {
|
|
606
|
+
content: [
|
|
607
|
+
{
|
|
608
|
+
type: "text",
|
|
609
|
+
text: `No results found for: "${query}"${category ? ` in category: ${category}` : ""}\n\nTip: Try broader terms or different category.`,
|
|
610
|
+
},
|
|
611
|
+
],
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
// Format grouped results
|
|
615
|
+
let output = `Found matches in ${results.length} files for: "${query}"${category ? ` in category: ${category}` : ""}\n`;
|
|
616
|
+
output += `\n${"ā".repeat(60)}\n`;
|
|
617
|
+
for (const result of results) {
|
|
618
|
+
output += `\nš **${result.title}**\n`;
|
|
619
|
+
output += ` Path: ${result.file}\n`;
|
|
620
|
+
output += ` Matches: ${result.matchCount}\n`;
|
|
621
|
+
if (result.relevantSections.length > 0) {
|
|
622
|
+
output += ` Sections: ${result.relevantSections.join(" | ")}\n`;
|
|
623
|
+
}
|
|
624
|
+
output += ` Preview:\n`;
|
|
625
|
+
for (const match of result.topMatches) {
|
|
626
|
+
output += ` L${match.line}: ${match.content}\n`;
|
|
627
|
+
}
|
|
628
|
+
output += `\n${"ā".repeat(60)}\n`;
|
|
629
|
+
}
|
|
630
|
+
output += `\nš” Tips:\n`;
|
|
631
|
+
output += `⢠Use get_hacktricks_outline(path) to see page structure\n`;
|
|
632
|
+
output += `⢠Use get_hacktricks_section(path, section) to read specific sections\n`;
|
|
633
|
+
output += `⢠Use get_hacktricks_cheatsheet(path) to get just the code/commands`;
|
|
634
|
+
return {
|
|
635
|
+
content: [
|
|
636
|
+
{
|
|
637
|
+
type: "text",
|
|
638
|
+
text: output,
|
|
639
|
+
},
|
|
640
|
+
],
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
if (name === "get_hacktricks_page") {
|
|
644
|
+
const path = args.path;
|
|
645
|
+
if (!path) {
|
|
646
|
+
throw new Error("Path parameter is required");
|
|
647
|
+
}
|
|
648
|
+
const content = await getPage(path);
|
|
649
|
+
return {
|
|
650
|
+
content: [
|
|
651
|
+
{
|
|
652
|
+
type: "text",
|
|
653
|
+
text: content,
|
|
654
|
+
},
|
|
655
|
+
],
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
if (name === "get_hacktricks_outline") {
|
|
659
|
+
const path = args.path;
|
|
660
|
+
if (!path) {
|
|
661
|
+
throw new Error("Path parameter is required");
|
|
662
|
+
}
|
|
663
|
+
const outline = await getPageOutline(path);
|
|
664
|
+
return {
|
|
665
|
+
content: [
|
|
666
|
+
{
|
|
667
|
+
type: "text",
|
|
668
|
+
text: `Outline of ${path}:\n\n${outline}\n\nš” Use get_hacktricks_section(path, "section name") to read a specific section.`,
|
|
669
|
+
},
|
|
670
|
+
],
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
if (name === "get_hacktricks_section") {
|
|
674
|
+
const path = args.path;
|
|
675
|
+
const section = args.section;
|
|
676
|
+
if (!path) {
|
|
677
|
+
throw new Error("Path parameter is required");
|
|
678
|
+
}
|
|
679
|
+
if (!section) {
|
|
680
|
+
throw new Error("Section parameter is required");
|
|
681
|
+
}
|
|
682
|
+
const sectionContent = await getPageSection(path, section);
|
|
683
|
+
return {
|
|
684
|
+
content: [
|
|
685
|
+
{
|
|
686
|
+
type: "text",
|
|
687
|
+
text: sectionContent,
|
|
688
|
+
},
|
|
689
|
+
],
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
if (name === "get_hacktricks_cheatsheet") {
|
|
693
|
+
const path = args.path;
|
|
694
|
+
if (!path) {
|
|
695
|
+
throw new Error("Path parameter is required");
|
|
696
|
+
}
|
|
697
|
+
const cheatsheet = await getPageCheatsheet(path);
|
|
698
|
+
return {
|
|
699
|
+
content: [
|
|
700
|
+
{
|
|
701
|
+
type: "text",
|
|
702
|
+
text: `Code blocks from ${path}:\n\n${cheatsheet}`,
|
|
703
|
+
},
|
|
704
|
+
],
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
if (name === "list_hacktricks_categories") {
|
|
708
|
+
const category = args.category;
|
|
709
|
+
const result = await listCategories(category);
|
|
710
|
+
let output;
|
|
711
|
+
if (category) {
|
|
712
|
+
const formatTree = (items, indent = "") => {
|
|
713
|
+
return items
|
|
714
|
+
.map((item) => {
|
|
715
|
+
const icon = item.type === "directory" ? "š" : "š";
|
|
716
|
+
let line = `${indent}${icon} ${item.name}`;
|
|
717
|
+
if (item.type === "file") {
|
|
718
|
+
line += ` ā ${item.path}`;
|
|
719
|
+
}
|
|
720
|
+
if (item.children && item.children.length > 0) {
|
|
721
|
+
line += "\n" + formatTree(item.children, indent + " ");
|
|
722
|
+
}
|
|
723
|
+
return line;
|
|
724
|
+
})
|
|
725
|
+
.join("\n");
|
|
726
|
+
};
|
|
727
|
+
output = `Contents of category: ${category}\n\n${formatTree(result)}`;
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
const categories = result;
|
|
731
|
+
output = `Available HackTricks Categories (${categories.length}):\n\n${categories.map((cat) => `- ${cat}`).join("\n")}\n\nTip: Use category parameter to see contents (e.g., category="pentesting-web")`;
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
content: [
|
|
735
|
+
{
|
|
736
|
+
type: "text",
|
|
737
|
+
text: output,
|
|
738
|
+
},
|
|
739
|
+
],
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
if (name === "hacktricks_quick_lookup") {
|
|
743
|
+
const topic = args.topic;
|
|
744
|
+
const category = args.category;
|
|
745
|
+
if (!topic) {
|
|
746
|
+
throw new Error("Topic parameter is required");
|
|
747
|
+
}
|
|
748
|
+
const result = await quickLookup(topic, category);
|
|
749
|
+
let output = `ā” Quick Lookup: ${result.title}\n`;
|
|
750
|
+
output += `š Page: ${result.page}\n`;
|
|
751
|
+
output += `\n${"ā".repeat(60)}\n`;
|
|
752
|
+
output += `\n## Exploitation Info\n\n`;
|
|
753
|
+
output += result.sections;
|
|
754
|
+
output += `\n\n${"ā".repeat(60)}\n`;
|
|
755
|
+
output += `\n## Code/Payloads\n\n`;
|
|
756
|
+
output += result.codeBlocks;
|
|
757
|
+
output += `\n\n${"ā".repeat(60)}\n`;
|
|
758
|
+
output += `š” Need more? Use get_hacktricks_page("${result.page}") for full content.`;
|
|
759
|
+
return {
|
|
760
|
+
content: [
|
|
761
|
+
{
|
|
762
|
+
type: "text",
|
|
763
|
+
text: output,
|
|
764
|
+
},
|
|
765
|
+
],
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
769
|
+
});
|
|
770
|
+
// Start server
|
|
771
|
+
async function main() {
|
|
772
|
+
const transport = new StdioServerTransport();
|
|
773
|
+
await server.connect(transport);
|
|
774
|
+
console.error("HackTricks MCP Server v1.3.0 running on stdio");
|
|
775
|
+
}
|
|
776
|
+
main().catch((error) => {
|
|
777
|
+
console.error("Server error:", error);
|
|
778
|
+
process.exit(1);
|
|
779
|
+
});
|