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 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, SHIELDAPI_NETWORK = 'base', } = process.env;
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.0',
106
+ version: '1.0.2',
60
107
  });
61
- // Tools
62
- server.tool('check_url', 'Check a URL for malware, phishing, and other threats. Uses URLhaus + heuristic analysis.', { url: z.string().describe('The URL to check (e.g. https://example.com)') }, async ({ url }) => formatResult(await callShieldApi('check-url', { url })));
63
- server.tool('check_password', 'Check if a password hash (SHA-1) has been exposed in known data breaches via HIBP.', { hash: z.string().describe('SHA-1 hash of the password (40 hex chars)') }, async ({ hash }) => formatResult(await callShieldApi('check-password', { hash })));
64
- server.tool('check_password_range', 'Look up a SHA-1 hash prefix in the HIBP k-Anonymity database.', { prefix: z.string().describe('First 5 characters of the SHA-1 password hash') }, async ({ prefix }) => formatResult(await callShieldApi('check-password-range', { prefix })));
65
- server.tool('check_domain', 'Check domain reputation: DNS records, blacklists (Spamhaus, SpamCop, SORBS), SPF/DMARC, SSL.', { domain: z.string().describe('Domain name to check (e.g. example.com)') }, async ({ domain }) => formatResult(await callShieldApi('check-domain', { domain })));
66
- server.tool('check_ip', 'Check IP reputation: blacklists, Tor exit node detection, reverse DNS.', { ip: z.string().describe('IPv4 address to check (e.g. 8.8.8.8)') }, async ({ ip }) => formatResult(await callShieldApi('check-ip', { ip })));
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.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.0',
135
+ version: '1.0.2',
82
136
  });
83
137
 
84
- // Tools
85
-
86
- server.tool(
87
- 'check_url',
88
- 'Check a URL for malware, phishing, and other threats. Uses URLhaus + heuristic analysis.',
89
- { url: z.string().describe('The URL to check (e.g. https://example.com)') },
90
- async ({ url }) => formatResult(await callShieldApi('check-url', { url }))
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', { target }))
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
+ });