proxy_list_tls_fingerprints
Retrieve unique JA3/JA4 TLS client fingerprints from captured proxy traffic, with occurrence counts and hostname filter support.
Instructions
List unique client JA3/JA4 fingerprints across captured traffic with occurrence counts.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Max fingerprints to return (default: 20) | |
| hostname_filter | No | Filter by hostname substring |
Implementation Reference
- src/tools/tls.ts:49-108 (handler)The async handler function that executes the tool's logic: filters traffic by optional hostname_filter, aggregates JA3/JA4 fingerprints with counts and associated hostnames, sorts by frequency, and returns truncated JSON results.
async ({ limit, hostname_filter }) => { let traffic = proxyManager.getTraffic(); if (hostname_filter) { const h = hostname_filter.toLowerCase(); traffic = traffic.filter((t) => t.request.hostname.toLowerCase().includes(h)); } // Aggregate JA3 fingerprints const ja3Counts = new Map<string, { count: number; hostnames: Set<string> }>(); const ja4Counts = new Map<string, { count: number; hostnames: Set<string> }>(); for (const t of traffic) { if (t.tls?.client?.ja3Fingerprint) { const fp = t.tls.client.ja3Fingerprint; const entry = ja3Counts.get(fp) || { count: 0, hostnames: new Set() }; entry.count++; entry.hostnames.add(t.request.hostname); ja3Counts.set(fp, entry); } if (t.tls?.client?.ja4Fingerprint) { const fp = t.tls.client.ja4Fingerprint; const entry = ja4Counts.get(fp) || { count: 0, hostnames: new Set() }; entry.count++; entry.hostnames.add(t.request.hostname); ja4Counts.set(fp, entry); } } // Sort by count descending const ja3List = [...ja3Counts.entries()] .sort((a, b) => b[1].count - a[1].count) .slice(0, limit) .map(([fp, { count, hostnames }]) => ({ fingerprint: fp, count, hostnames: [...hostnames].slice(0, 5), })); const ja4List = [...ja4Counts.entries()] .sort((a, b) => b[1].count - a[1].count) .slice(0, limit) .map(([fp, { count, hostnames }]) => ({ fingerprint: fp, count, hostnames: [...hostnames].slice(0, 5), })); return { content: [{ type: "text" as const, text: truncateResult({ status: "success", totalExchangesWithTls: traffic.filter((t) => t.tls?.client).length, ja3: ja3List, ja4: ja4List, }), }], }; }, - src/tools/tls.ts:45-48 (schema)Input schema defining optional 'limit' (number, default 20) and 'hostname_filter' (string) parameters for proxy_list_tls_fingerprints.
{ limit: z.number().optional().default(20).describe("Max fingerprints to return (default: 20)"), hostname_filter: z.string().optional().describe("Filter by hostname substring"), }, - src/tools/tls.ts:42-109 (registration)Registration of the tool via server.tool() with name 'proxy_list_tls_fingerprints' and description 'List unique client JA3/JA4 fingerprints across captured traffic with occurrence counts.'
server.tool( "proxy_list_tls_fingerprints", "List unique client JA3/JA4 fingerprints across captured traffic with occurrence counts.", { limit: z.number().optional().default(20).describe("Max fingerprints to return (default: 20)"), hostname_filter: z.string().optional().describe("Filter by hostname substring"), }, async ({ limit, hostname_filter }) => { let traffic = proxyManager.getTraffic(); if (hostname_filter) { const h = hostname_filter.toLowerCase(); traffic = traffic.filter((t) => t.request.hostname.toLowerCase().includes(h)); } // Aggregate JA3 fingerprints const ja3Counts = new Map<string, { count: number; hostnames: Set<string> }>(); const ja4Counts = new Map<string, { count: number; hostnames: Set<string> }>(); for (const t of traffic) { if (t.tls?.client?.ja3Fingerprint) { const fp = t.tls.client.ja3Fingerprint; const entry = ja3Counts.get(fp) || { count: 0, hostnames: new Set() }; entry.count++; entry.hostnames.add(t.request.hostname); ja3Counts.set(fp, entry); } if (t.tls?.client?.ja4Fingerprint) { const fp = t.tls.client.ja4Fingerprint; const entry = ja4Counts.get(fp) || { count: 0, hostnames: new Set() }; entry.count++; entry.hostnames.add(t.request.hostname); ja4Counts.set(fp, entry); } } // Sort by count descending const ja3List = [...ja3Counts.entries()] .sort((a, b) => b[1].count - a[1].count) .slice(0, limit) .map(([fp, { count, hostnames }]) => ({ fingerprint: fp, count, hostnames: [...hostnames].slice(0, 5), })); const ja4List = [...ja4Counts.entries()] .sort((a, b) => b[1].count - a[1].count) .slice(0, limit) .map(([fp, { count, hostnames }]) => ({ fingerprint: fp, count, hostnames: [...hostnames].slice(0, 5), })); return { content: [{ type: "text" as const, text: truncateResult({ status: "success", totalExchangesWithTls: traffic.filter((t) => t.tls?.client).length, ja3: ja3List, ja4: ja4List, }), }], }; }, ); - src/index.ts:67-67 (registration)Top-level registration call to registerTlsTools(server) which registers all TLS-related tools including proxy_list_tls_fingerprints.
registerTlsTools(server); - src/utils.ts:17-43 (helper)truncateResult utility used to serialize/truncate the tool's JSON output to stay within MCP token limits (24000 chars).
export function truncateResult(data: unknown, indent?: number): string { const full = JSON.stringify(data, null, indent); if (full.length <= MAX_RESULT_CHARS) return full; if (Array.isArray(data)) { let lo = 0; let hi = data.length; while (lo < hi) { const mid = (lo + hi + 1) >>> 1; if (JSON.stringify(data.slice(0, mid), null, indent).length <= MAX_RESULT_CHARS - 200) { lo = mid; } else { hi = mid - 1; } } const truncated = data.slice(0, lo); return JSON.stringify({ items: truncated, truncated: true, showing: lo, total: data.length, message: `Showing ${lo} of ${data.length} items. Use filter/limit params to narrow results.`, }, null, indent); } return full.slice(0, MAX_RESULT_CHARS - 100) + "\n... [truncated, total " + full.length + " chars]"; }