shieldapi-mcp 1.0.0 → 1.0.2
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 +0 -1
- package/dist/index.js +56 -11
- package/package.json +4 -2
- package/src/index.ts +70 -49
- package/test/index.test.ts +99 -0
package/README.md
CHANGED
|
@@ -64,7 +64,6 @@ Add to `.cursor/mcp.json`:
|
|
|
64
64
|
|----------|---------|-------------|
|
|
65
65
|
| `SHIELDAPI_URL` | `https://shield.vainplex.dev` | API base URL |
|
|
66
66
|
| `SHIELDAPI_WALLET_PRIVATE_KEY` | *(none)* | EVM private key for USDC payments. If not set, uses free demo mode. |
|
|
67
|
-
| `SHIELDAPI_NETWORK` | `base` | Network for payments (Base mainnet) |
|
|
68
67
|
|
|
69
68
|
## Demo Mode
|
|
70
69
|
|
package/dist/index.js
CHANGED
|
@@ -8,8 +8,46 @@
|
|
|
8
8
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
9
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
10
|
import { z } from 'zod';
|
|
11
|
-
const { SHIELDAPI_URL = 'https://shield.vainplex.dev', SHIELDAPI_WALLET_PRIVATE_KEY,
|
|
11
|
+
const { SHIELDAPI_URL = 'https://shield.vainplex.dev', SHIELDAPI_WALLET_PRIVATE_KEY, } = process.env;
|
|
12
12
|
const demoMode = !SHIELDAPI_WALLET_PRIVATE_KEY;
|
|
13
|
+
const TOOLS = {
|
|
14
|
+
check_url: {
|
|
15
|
+
description: 'Check a URL for malware, phishing, and other threats. Uses URLhaus + heuristic analysis.',
|
|
16
|
+
param: 'url',
|
|
17
|
+
paramDesc: 'The URL to check (e.g. https://example.com)',
|
|
18
|
+
endpoint: 'check-url',
|
|
19
|
+
},
|
|
20
|
+
check_password: {
|
|
21
|
+
description: 'Check if a password hash (SHA-1) has been exposed in known data breaches via HIBP.',
|
|
22
|
+
param: 'hash',
|
|
23
|
+
paramDesc: 'SHA-1 hash of the password (40 hex chars)',
|
|
24
|
+
endpoint: 'check-password',
|
|
25
|
+
},
|
|
26
|
+
check_password_range: {
|
|
27
|
+
description: 'Look up a SHA-1 hash prefix in the HIBP k-Anonymity database.',
|
|
28
|
+
param: 'prefix',
|
|
29
|
+
paramDesc: 'First 5 characters of the SHA-1 password hash',
|
|
30
|
+
endpoint: 'check-password-range',
|
|
31
|
+
},
|
|
32
|
+
check_domain: {
|
|
33
|
+
description: 'Check domain reputation: DNS records, blacklists (Spamhaus, SpamCop, SORBS), SPF/DMARC, SSL.',
|
|
34
|
+
param: 'domain',
|
|
35
|
+
paramDesc: 'Domain name to check (e.g. example.com)',
|
|
36
|
+
endpoint: 'check-domain',
|
|
37
|
+
},
|
|
38
|
+
check_ip: {
|
|
39
|
+
description: 'Check IP reputation: blacklists, Tor exit node detection, reverse DNS.',
|
|
40
|
+
param: 'ip',
|
|
41
|
+
paramDesc: 'IPv4 address to check (e.g. 8.8.8.8)',
|
|
42
|
+
endpoint: 'check-ip',
|
|
43
|
+
},
|
|
44
|
+
check_email: {
|
|
45
|
+
description: 'Check if an email address has been exposed in known data breaches via HIBP.',
|
|
46
|
+
param: 'email',
|
|
47
|
+
paramDesc: 'Email address to check',
|
|
48
|
+
endpoint: 'check-email',
|
|
49
|
+
},
|
|
50
|
+
};
|
|
13
51
|
// --- x402 payment setup (lazy, only if wallet configured) ---
|
|
14
52
|
let paymentFetch = fetch;
|
|
15
53
|
async function initPaymentFetch() {
|
|
@@ -44,10 +82,19 @@ async function callShieldApi(endpoint, params) {
|
|
|
44
82
|
const response = await paymentFetch(url.toString());
|
|
45
83
|
if (!response.ok) {
|
|
46
84
|
const body = await response.text();
|
|
47
|
-
throw new Error(`ShieldAPI ${endpoint} failed (${response.status}): ${body}`);
|
|
85
|
+
throw new Error(`ShieldAPI ${endpoint} failed (${response.status}): ${body.substring(0, 200)}`);
|
|
48
86
|
}
|
|
49
87
|
return response.json();
|
|
50
88
|
}
|
|
89
|
+
function detectTargetType(target) {
|
|
90
|
+
if (target.includes('@'))
|
|
91
|
+
return { email: target };
|
|
92
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(target))
|
|
93
|
+
return { ip: target };
|
|
94
|
+
if (target.startsWith('http://') || target.startsWith('https://'))
|
|
95
|
+
return { url: target };
|
|
96
|
+
return { domain: target };
|
|
97
|
+
}
|
|
51
98
|
function formatResult(data) {
|
|
52
99
|
return {
|
|
53
100
|
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
@@ -56,16 +103,14 @@ function formatResult(data) {
|
|
|
56
103
|
// --- MCP Server ---
|
|
57
104
|
const server = new McpServer({
|
|
58
105
|
name: 'ShieldAPI',
|
|
59
|
-
version: '1.0.
|
|
106
|
+
version: '1.0.2',
|
|
60
107
|
});
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
server.tool(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
server.tool('
|
|
67
|
-
server.tool('check_email', 'Check if an email address has been exposed in known data breaches via HIBP.', { email: z.string().describe('Email address to check') }, async ({ email }) => formatResult(await callShieldApi('check-email', { email })));
|
|
68
|
-
server.tool('full_scan', 'Run all security checks on a target (URL, domain, IP, or email). Most comprehensive scan.', { target: z.string().describe('Target to scan — URL, domain, IP address, or email') }, async ({ target }) => formatResult(await callShieldApi('full-scan', { target })));
|
|
108
|
+
// Register standard tools from config
|
|
109
|
+
for (const [name, def] of Object.entries(TOOLS)) {
|
|
110
|
+
server.tool(name, def.description, { [def.param]: z.string().describe(def.paramDesc) }, async (params) => formatResult(await callShieldApi(def.endpoint, params)));
|
|
111
|
+
}
|
|
112
|
+
// full_scan is special — single 'target' param mapped to the correct server param
|
|
113
|
+
server.tool('full_scan', 'Run all security checks on a target (URL, domain, IP, or email). Most comprehensive scan.', { target: z.string().describe('Target to scan — URL, domain, IP address, or email') }, async ({ target }) => formatResult(await callShieldApi('full-scan', detectTargetType(target))));
|
|
69
114
|
// --- Start ---
|
|
70
115
|
async function main() {
|
|
71
116
|
await initPaymentFetch();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shieldapi-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "MCP server for ShieldAPI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsc",
|
|
11
|
+
"test": "vitest run",
|
|
11
12
|
"start": "node dist/index.js"
|
|
12
13
|
},
|
|
13
14
|
"keywords": [
|
|
@@ -31,7 +32,8 @@
|
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@types/node": "^20.11.24",
|
|
34
|
-
"typescript": "^5.3.3"
|
|
35
|
+
"typescript": "^5.3.3",
|
|
36
|
+
"vitest": "^4.0.18"
|
|
35
37
|
},
|
|
36
38
|
"type": "module"
|
|
37
39
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* ShieldAPI MCP Server
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
5
|
* Exposes ShieldAPI security intelligence as native MCP tools.
|
|
6
6
|
* Handles x402 USDC micropayments automatically, with demo fallback.
|
|
7
7
|
*/
|
|
@@ -13,18 +13,65 @@ import { z } from 'zod';
|
|
|
13
13
|
const {
|
|
14
14
|
SHIELDAPI_URL = 'https://shield.vainplex.dev',
|
|
15
15
|
SHIELDAPI_WALLET_PRIVATE_KEY,
|
|
16
|
-
SHIELDAPI_NETWORK = 'base',
|
|
17
16
|
} = process.env;
|
|
18
17
|
|
|
19
18
|
const demoMode = !SHIELDAPI_WALLET_PRIVATE_KEY;
|
|
20
19
|
|
|
20
|
+
// --- Tool definitions (single source of truth) ---
|
|
21
|
+
|
|
22
|
+
interface ToolDef {
|
|
23
|
+
description: string;
|
|
24
|
+
param: string;
|
|
25
|
+
paramDesc: string;
|
|
26
|
+
endpoint: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const TOOLS: Record<string, ToolDef> = {
|
|
30
|
+
check_url: {
|
|
31
|
+
description: 'Check a URL for malware, phishing, and other threats. Uses URLhaus + heuristic analysis.',
|
|
32
|
+
param: 'url',
|
|
33
|
+
paramDesc: 'The URL to check (e.g. https://example.com)',
|
|
34
|
+
endpoint: 'check-url',
|
|
35
|
+
},
|
|
36
|
+
check_password: {
|
|
37
|
+
description: 'Check if a password hash (SHA-1) has been exposed in known data breaches via HIBP.',
|
|
38
|
+
param: 'hash',
|
|
39
|
+
paramDesc: 'SHA-1 hash of the password (40 hex chars)',
|
|
40
|
+
endpoint: 'check-password',
|
|
41
|
+
},
|
|
42
|
+
check_password_range: {
|
|
43
|
+
description: 'Look up a SHA-1 hash prefix in the HIBP k-Anonymity database.',
|
|
44
|
+
param: 'prefix',
|
|
45
|
+
paramDesc: 'First 5 characters of the SHA-1 password hash',
|
|
46
|
+
endpoint: 'check-password-range',
|
|
47
|
+
},
|
|
48
|
+
check_domain: {
|
|
49
|
+
description: 'Check domain reputation: DNS records, blacklists (Spamhaus, SpamCop, SORBS), SPF/DMARC, SSL.',
|
|
50
|
+
param: 'domain',
|
|
51
|
+
paramDesc: 'Domain name to check (e.g. example.com)',
|
|
52
|
+
endpoint: 'check-domain',
|
|
53
|
+
},
|
|
54
|
+
check_ip: {
|
|
55
|
+
description: 'Check IP reputation: blacklists, Tor exit node detection, reverse DNS.',
|
|
56
|
+
param: 'ip',
|
|
57
|
+
paramDesc: 'IPv4 address to check (e.g. 8.8.8.8)',
|
|
58
|
+
endpoint: 'check-ip',
|
|
59
|
+
},
|
|
60
|
+
check_email: {
|
|
61
|
+
description: 'Check if an email address has been exposed in known data breaches via HIBP.',
|
|
62
|
+
param: 'email',
|
|
63
|
+
paramDesc: 'Email address to check',
|
|
64
|
+
endpoint: 'check-email',
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
21
68
|
// --- x402 payment setup (lazy, only if wallet configured) ---
|
|
22
69
|
|
|
23
70
|
let paymentFetch: typeof fetch = fetch;
|
|
24
71
|
|
|
25
72
|
async function initPaymentFetch(): Promise<void> {
|
|
26
73
|
if (demoMode) return;
|
|
27
|
-
|
|
74
|
+
|
|
28
75
|
const { wrapFetchWithPayment, x402Client } = await import('@x402/fetch');
|
|
29
76
|
const { ExactEvmScheme, toClientEvmSigner } = await import('@x402/evm');
|
|
30
77
|
const { createWalletClient, http, publicActions } = await import('viem');
|
|
@@ -63,11 +110,18 @@ async function callShieldApi(endpoint: string, params: Record<string, string>):
|
|
|
63
110
|
const response = await paymentFetch(url.toString());
|
|
64
111
|
if (!response.ok) {
|
|
65
112
|
const body = await response.text();
|
|
66
|
-
throw new Error(`ShieldAPI ${endpoint} failed (${response.status}): ${body}`);
|
|
113
|
+
throw new Error(`ShieldAPI ${endpoint} failed (${response.status}): ${body.substring(0, 200)}`);
|
|
67
114
|
}
|
|
68
115
|
return response.json();
|
|
69
116
|
}
|
|
70
117
|
|
|
118
|
+
function detectTargetType(target: string): Record<string, string> {
|
|
119
|
+
if (target.includes('@')) return { email: target };
|
|
120
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(target)) return { ip: target };
|
|
121
|
+
if (target.startsWith('http://') || target.startsWith('https://')) return { url: target };
|
|
122
|
+
return { domain: target };
|
|
123
|
+
}
|
|
124
|
+
|
|
71
125
|
function formatResult(data: unknown): { content: Array<{ type: 'text'; text: string }> } {
|
|
72
126
|
return {
|
|
73
127
|
content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }],
|
|
@@ -78,58 +132,25 @@ function formatResult(data: unknown): { content: Array<{ type: 'text'; text: str
|
|
|
78
132
|
|
|
79
133
|
const server = new McpServer({
|
|
80
134
|
name: 'ShieldAPI',
|
|
81
|
-
version: '1.0.
|
|
135
|
+
version: '1.0.2',
|
|
82
136
|
});
|
|
83
137
|
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
server.tool(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
server.tool(
|
|
94
|
-
'check_password',
|
|
95
|
-
'Check if a password hash (SHA-1) has been exposed in known data breaches via HIBP.',
|
|
96
|
-
{ hash: z.string().describe('SHA-1 hash of the password (40 hex chars)') },
|
|
97
|
-
async ({ hash }) => formatResult(await callShieldApi('check-password', { hash }))
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
server.tool(
|
|
101
|
-
'check_password_range',
|
|
102
|
-
'Look up a SHA-1 hash prefix in the HIBP k-Anonymity database.',
|
|
103
|
-
{ prefix: z.string().describe('First 5 characters of the SHA-1 password hash') },
|
|
104
|
-
async ({ prefix }) => formatResult(await callShieldApi('check-password-range', { prefix }))
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
server.tool(
|
|
108
|
-
'check_domain',
|
|
109
|
-
'Check domain reputation: DNS records, blacklists (Spamhaus, SpamCop, SORBS), SPF/DMARC, SSL.',
|
|
110
|
-
{ domain: z.string().describe('Domain name to check (e.g. example.com)') },
|
|
111
|
-
async ({ domain }) => formatResult(await callShieldApi('check-domain', { domain }))
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
server.tool(
|
|
115
|
-
'check_ip',
|
|
116
|
-
'Check IP reputation: blacklists, Tor exit node detection, reverse DNS.',
|
|
117
|
-
{ ip: z.string().describe('IPv4 address to check (e.g. 8.8.8.8)') },
|
|
118
|
-
async ({ ip }) => formatResult(await callShieldApi('check-ip', { ip }))
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
server.tool(
|
|
122
|
-
'check_email',
|
|
123
|
-
'Check if an email address has been exposed in known data breaches via HIBP.',
|
|
124
|
-
{ email: z.string().describe('Email address to check') },
|
|
125
|
-
async ({ email }) => formatResult(await callShieldApi('check-email', { email }))
|
|
126
|
-
);
|
|
138
|
+
// Register standard tools from config
|
|
139
|
+
for (const [name, def] of Object.entries(TOOLS)) {
|
|
140
|
+
server.tool(
|
|
141
|
+
name,
|
|
142
|
+
def.description,
|
|
143
|
+
{ [def.param]: z.string().describe(def.paramDesc) },
|
|
144
|
+
async (params) => formatResult(await callShieldApi(def.endpoint, params as Record<string, string>))
|
|
145
|
+
);
|
|
146
|
+
}
|
|
127
147
|
|
|
148
|
+
// full_scan is special — single 'target' param mapped to the correct server param
|
|
128
149
|
server.tool(
|
|
129
150
|
'full_scan',
|
|
130
151
|
'Run all security checks on a target (URL, domain, IP, or email). Most comprehensive scan.',
|
|
131
152
|
{ target: z.string().describe('Target to scan — URL, domain, IP address, or email') },
|
|
132
|
-
async ({ target }) => formatResult(await callShieldApi('full-scan',
|
|
153
|
+
async ({ target }) => formatResult(await callShieldApi('full-scan', detectTargetType(target)))
|
|
133
154
|
);
|
|
134
155
|
|
|
135
156
|
// --- Start ---
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Import the helpers we need to test by re-implementing them here
|
|
4
|
+
// (since they're not exported — we test the logic, not the module wiring)
|
|
5
|
+
|
|
6
|
+
function detectTargetType(target: string): Record<string, string> {
|
|
7
|
+
if (target.includes('@')) return { email: target };
|
|
8
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(target)) return { ip: target };
|
|
9
|
+
if (target.startsWith('http://') || target.startsWith('https://')) return { url: target };
|
|
10
|
+
return { domain: target };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function formatResult(data: unknown): { content: Array<{ type: 'text'; text: string }> } {
|
|
14
|
+
return {
|
|
15
|
+
content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildUrl(base: string, endpoint: string, params: Record<string, string>, demo: boolean): string {
|
|
20
|
+
const url = new URL(`${base}/api/${endpoint}`);
|
|
21
|
+
for (const [key, value] of Object.entries(params)) {
|
|
22
|
+
url.searchParams.set(key, value);
|
|
23
|
+
}
|
|
24
|
+
if (demo) {
|
|
25
|
+
url.searchParams.set('demo', 'true');
|
|
26
|
+
}
|
|
27
|
+
return url.toString();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('detectTargetType', () => {
|
|
31
|
+
it('detects email addresses', () => {
|
|
32
|
+
expect(detectTargetType('user@example.com')).toEqual({ email: 'user@example.com' });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('detects IPv4 addresses', () => {
|
|
36
|
+
expect(detectTargetType('8.8.8.8')).toEqual({ ip: '8.8.8.8' });
|
|
37
|
+
expect(detectTargetType('192.168.1.1')).toEqual({ ip: '192.168.1.1' });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('detects HTTP URLs', () => {
|
|
41
|
+
expect(detectTargetType('https://example.com/path')).toEqual({ url: 'https://example.com/path' });
|
|
42
|
+
expect(detectTargetType('http://malware.site')).toEqual({ url: 'http://malware.site' });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('falls back to domain for everything else', () => {
|
|
46
|
+
expect(detectTargetType('example.com')).toEqual({ domain: 'example.com' });
|
|
47
|
+
expect(detectTargetType('sub.domain.org')).toEqual({ domain: 'sub.domain.org' });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('does not match partial IPs as IP', () => {
|
|
51
|
+
expect(detectTargetType('8.8.8')).toEqual({ domain: '8.8.8' });
|
|
52
|
+
expect(detectTargetType('999.999.999.999')).toEqual({ ip: '999.999.999.999' }); // regex matches format, validation is server-side
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('formatResult', () => {
|
|
57
|
+
it('wraps data as MCP text content', () => {
|
|
58
|
+
const result = formatResult({ risk_score: 42 });
|
|
59
|
+
expect(result.content).toHaveLength(1);
|
|
60
|
+
expect(result.content[0].type).toBe('text');
|
|
61
|
+
expect(JSON.parse(result.content[0].text)).toEqual({ risk_score: 42 });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('handles arrays', () => {
|
|
65
|
+
const result = formatResult([1, 2, 3]);
|
|
66
|
+
expect(JSON.parse(result.content[0].text)).toEqual([1, 2, 3]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('handles null', () => {
|
|
70
|
+
const result = formatResult(null);
|
|
71
|
+
expect(result.content[0].text).toBe('null');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('buildUrl', () => {
|
|
76
|
+
const base = 'https://shield.vainplex.dev';
|
|
77
|
+
|
|
78
|
+
it('constructs correct URL with params', () => {
|
|
79
|
+
const url = buildUrl(base, 'check-url', { url: 'https://evil.com' }, false);
|
|
80
|
+
expect(url).toBe('https://shield.vainplex.dev/api/check-url?url=https%3A%2F%2Fevil.com');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('adds demo flag', () => {
|
|
84
|
+
const url = buildUrl(base, 'check-domain', { domain: 'example.com' }, true);
|
|
85
|
+
expect(url).toContain('domain=example.com');
|
|
86
|
+
expect(url).toContain('demo=true');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('handles multiple params for full-scan', () => {
|
|
90
|
+
const url = buildUrl(base, 'full-scan', { email: 'a@b.com', domain: 'b.com' }, false);
|
|
91
|
+
expect(url).toContain('email=a%40b.com');
|
|
92
|
+
expect(url).toContain('domain=b.com');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('encodes special characters', () => {
|
|
96
|
+
const url = buildUrl(base, 'check-password', { hash: 'AABBCC' }, false);
|
|
97
|
+
expect(url).toBe('https://shield.vainplex.dev/api/check-password?hash=AABBCC');
|
|
98
|
+
});
|
|
99
|
+
});
|