@burtthecoder/mcp-shodan 1.0.16 → 1.0.20

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
@@ -181,7 +181,7 @@ npm run build
181
181
 
182
182
  ## Requirements
183
183
 
184
- - Node.js (v18 or later)
184
+ - Node.js (v20 or later)
185
185
  - A valid [Shodan API Key](https://account.shodan.io/)
186
186
 
187
187
  ## Troubleshooting
@@ -235,9 +235,15 @@ If you see module loading errors:
235
235
 
236
236
  ## Development
237
237
 
238
- To run in development mode with hot reloading:
238
+ Build the project:
239
239
  ```bash
240
- npm run dev
240
+ npm install
241
+ npm run build
242
+ ```
243
+
244
+ Test interactively with FastMCP's built-in dev tool:
245
+ ```bash
246
+ npx fastmcp dev build/index.js
241
247
  ```
242
248
 
243
249
  ## Error Handling
@@ -254,6 +260,7 @@ The server includes comprehensive error handling for:
254
260
 
255
261
  ## Version History
256
262
 
263
+ - v1.1.0: Migrated from raw `@modelcontextprotocol/sdk` to [FastMCP](https://github.com/punkpeye/fastmcp) — modular tool files, automatic schema validation, simplified error handling
257
264
  - v1.0.12: Added reverse DNS lookup and improved output formatting
258
265
  - v1.0.7: Added CVEs by Product search functionality and renamed vulnerabilities tool to cve_lookup
259
266
  - v1.0.6: Added CVEDB integration for enhanced CVE lookups and CPE search functionality
@@ -0,0 +1,69 @@
1
+ import axios from "axios";
2
+ import { UserError } from "fastmcp";
3
+ export const API_BASE_URL = "https://api.shodan.io";
4
+ export const CVEDB_API_URL = "https://cvedb.shodan.io";
5
+ export const SHODAN_API_KEY = process.env.SHODAN_API_KEY;
6
+ export async function queryShodan(endpoint, params) {
7
+ try {
8
+ const response = await axios.get(`${API_BASE_URL}${endpoint}`, {
9
+ params: { ...params, key: SHODAN_API_KEY },
10
+ timeout: 10000,
11
+ });
12
+ return response.data;
13
+ }
14
+ catch (error) {
15
+ const errorMessage = error.response?.data?.error || error.message;
16
+ console.error(`Shodan API error: ${errorMessage}`);
17
+ throw new UserError(`Shodan API error: ${errorMessage}`);
18
+ }
19
+ }
20
+ export async function queryCVEDB(cveId) {
21
+ try {
22
+ const response = await axios.get(`${CVEDB_API_URL}/cve/${cveId}`);
23
+ return response.data;
24
+ }
25
+ catch (error) {
26
+ if (error.response?.status === 422) {
27
+ throw new UserError(`Invalid CVE ID format: ${cveId}`);
28
+ }
29
+ if (error.response?.status === 404) {
30
+ throw new UserError(`CVE not found: ${cveId}`);
31
+ }
32
+ throw new UserError(`CVEDB API error: ${error.message}`);
33
+ }
34
+ }
35
+ export async function queryCPEDB(params) {
36
+ try {
37
+ const response = await axios.get(`${CVEDB_API_URL}/cpes`, { params });
38
+ return response.data;
39
+ }
40
+ catch (error) {
41
+ if (error.response?.status === 422) {
42
+ throw new UserError(`Invalid parameters: ${error.response.data?.detail || error.message}`);
43
+ }
44
+ throw new UserError(`CVEDB API error: ${error.message}`);
45
+ }
46
+ }
47
+ export async function queryCVEsByProduct(params) {
48
+ try {
49
+ const response = await axios.get(`${CVEDB_API_URL}/cves`, { params });
50
+ return response.data;
51
+ }
52
+ catch (error) {
53
+ if (error.response?.status === 422) {
54
+ throw new UserError(`Invalid parameters: ${error.response.data?.detail || error.message}`);
55
+ }
56
+ throw new UserError(`CVEDB API error: ${error.message}`);
57
+ }
58
+ }
59
+ export function getCvssSeverity(score) {
60
+ if (score >= 9.0)
61
+ return "Critical";
62
+ if (score >= 7.0)
63
+ return "High";
64
+ if (score >= 4.0)
65
+ return "Medium";
66
+ if (score >= 0.1)
67
+ return "Low";
68
+ return "None";
69
+ }
package/build/index.js CHANGED
@@ -1,160 +1,21 @@
1
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, InitializeRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
- import axios from "axios";
2
+ import { FastMCP } from "fastmcp";
6
3
  import dotenv from "dotenv";
7
- import { z } from "zod";
8
- import { zodToJsonSchema } from "zod-to-json-schema";
9
- import fs from "fs";
10
- import path from "path";
11
- import os from "os";
4
+ import { registerIpLookup } from "./tools/ip-lookup.js";
5
+ import { registerShodanSearch } from "./tools/shodan-search.js";
6
+ import { registerCveLookup } from "./tools/cve-lookup.js";
7
+ import { registerDnsLookup } from "./tools/dns-lookup.js";
8
+ import { registerReverseDnsLookup } from "./tools/reverse-dns-lookup.js";
9
+ import { registerCpeLookup } from "./tools/cpe-lookup.js";
10
+ import { registerCvesByProduct } from "./tools/cves-by-product.js";
12
11
  dotenv.config();
13
- const logFilePath = path.join(os.tmpdir(), "mcp-shodan-server.log");
14
- const SHODAN_API_KEY = process.env.SHODAN_API_KEY;
15
- if (!SHODAN_API_KEY) {
12
+ if (!process.env.SHODAN_API_KEY) {
16
13
  throw new Error("SHODAN_API_KEY environment variable is required.");
17
14
  }
18
- const API_BASE_URL = "https://api.shodan.io";
19
- const CVEDB_API_URL = "https://cvedb.shodan.io";
20
- // Logging Helper Function
21
- function logToFile(message) {
22
- try {
23
- const timestamp = new Date().toISOString();
24
- const formattedMessage = `[${timestamp}] ${message}\n`;
25
- fs.appendFileSync(logFilePath, formattedMessage, "utf8");
26
- console.error(formattedMessage.trim()); // Use stderr for logging to avoid interfering with stdout
27
- }
28
- catch (error) {
29
- console.error(`Failed to write to log file: ${error}`);
30
- }
31
- }
32
- // Tool Schemas
33
- const IpLookupArgsSchema = z.object({
34
- ip: z.string().describe("The IP address to query."),
35
- });
36
- const ShodanSearchArgsSchema = z.object({
37
- query: z.string().describe("Search query for Shodan."),
38
- max_results: z
39
- .number()
40
- .optional()
41
- .default(10)
42
- .describe("Maximum results to return."),
43
- });
44
- const CVELookupArgsSchema = z.object({
45
- cve: z.string()
46
- .regex(/^CVE-\d{4}-\d{4,}$/i, "Must be a valid CVE ID format (e.g., CVE-2021-44228)")
47
- .describe("The CVE identifier to query (format: CVE-YYYY-NNNNN)."),
48
- });
49
- const DnsLookupArgsSchema = z.object({
50
- hostnames: z.array(z.string()).describe("List of hostnames to resolve."),
51
- });
52
- const ReverseDnsLookupArgsSchema = z.object({
53
- ips: z.array(z.string()).describe("List of IP addresses to perform reverse DNS lookup on."),
54
- });
55
- const CpeLookupArgsSchema = z.object({
56
- product: z.string().describe("The name of the product to search for CPEs."),
57
- count: z.boolean().optional().default(false).describe("If true, returns only the count of matching CPEs."),
58
- skip: z.number().optional().default(0).describe("Number of CPEs to skip (for pagination)."),
59
- limit: z.number().optional().default(1000).describe("Maximum number of CPEs to return (max 1000)."),
60
- });
61
- const CVEsByProductArgsSchema = z.object({
62
- cpe23: z.string().optional().describe("The CPE version 2.3 identifier (format: cpe:2.3:part:vendor:product:version)."),
63
- product: z.string().optional().describe("The name of the product to search for CVEs."),
64
- count: z.boolean().optional().default(false).describe("If true, returns only the count of matching CVEs."),
65
- is_kev: z.boolean().optional().default(false).describe("If true, returns only CVEs with the KEV flag set."),
66
- sort_by_epss: z.boolean().optional().default(false).describe("If true, sorts CVEs by EPSS score in descending order."),
67
- skip: z.number().optional().default(0).describe("Number of CVEs to skip (for pagination)."),
68
- limit: z.number().optional().default(1000).describe("Maximum number of CVEs to return (max 1000)."),
69
- start_date: z.string().optional().describe("Start date for filtering CVEs (format: YYYY-MM-DDTHH:MM:SS)."),
70
- end_date: z.string().optional().describe("End date for filtering CVEs (format: YYYY-MM-DDTHH:MM:SS).")
71
- }).refine(data => !(data.cpe23 && data.product), { message: "Cannot specify both cpe23 and product. Use only one." }).refine(data => data.cpe23 || data.product, { message: "Must specify either cpe23 or product." });
72
- // Helper Function to Query Shodan API
73
- async function queryShodan(endpoint, params) {
74
- try {
75
- const response = await axios.get(`${API_BASE_URL}${endpoint}`, {
76
- params: { ...params, key: SHODAN_API_KEY },
77
- timeout: 10000,
78
- });
79
- return response.data;
80
- }
81
- catch (error) {
82
- const errorMessage = error.response?.data?.error || error.message;
83
- logToFile(`Shodan API error: ${errorMessage}`);
84
- throw new Error(`Shodan API error: ${errorMessage}`);
85
- }
86
- }
87
- // Helper Function for CVE lookups using CVEDB
88
- async function queryCVEDB(cveId) {
89
- try {
90
- logToFile(`Querying CVEDB for: ${cveId}`);
91
- const response = await axios.get(`${CVEDB_API_URL}/cve/${cveId}`);
92
- return response.data;
93
- }
94
- catch (error) {
95
- if (error.response?.status === 422) {
96
- throw new Error(`Invalid CVE ID format: ${cveId}`);
97
- }
98
- if (error.response?.status === 404) {
99
- throw new Error(`CVE not found: ${cveId}`);
100
- }
101
- throw new Error(`CVEDB API error: ${error.message}`);
102
- }
103
- }
104
- // Helper Function for CPE lookups using CVEDB
105
- async function queryCPEDB(params) {
106
- try {
107
- logToFile(`Querying CVEDB for CPEs with params: ${JSON.stringify(params)}`);
108
- const response = await axios.get(`${CVEDB_API_URL}/cpes`, { params });
109
- return response.data;
110
- }
111
- catch (error) {
112
- if (error.response?.status === 422) {
113
- throw new Error(`Invalid parameters: ${error.response.data?.detail || error.message}`);
114
- }
115
- throw new Error(`CVEDB API error: ${error.message}`);
116
- }
117
- }
118
- // Helper Function for CVEs by product/CPE lookups using CVEDB
119
- async function queryCVEsByProduct(params) {
120
- try {
121
- logToFile(`Querying CVEDB for CVEs with params: ${JSON.stringify(params)}`);
122
- const response = await axios.get(`${CVEDB_API_URL}/cves`, { params });
123
- return response.data;
124
- }
125
- catch (error) {
126
- if (error.response?.status === 422) {
127
- throw new Error(`Invalid parameters: ${error.response.data?.detail || error.message}`);
128
- }
129
- throw new Error(`CVEDB API error: ${error.message}`);
130
- }
131
- }
132
- // Server Setup
133
- const server = new Server({
15
+ const server = new FastMCP({
134
16
  name: "shodan-mcp",
135
17
  version: "1.0.0",
136
- }, {
137
- capabilities: {
138
- tools: {
139
- listChanged: true,
140
- },
141
- },
142
- });
143
- // Handle Initialization
144
- server.setRequestHandler(InitializeRequestSchema, async (request) => {
145
- logToFile("Received initialize request.");
146
- return {
147
- protocolVersion: "2024-11-05",
148
- capabilities: {
149
- tools: {
150
- listChanged: true,
151
- },
152
- },
153
- serverInfo: {
154
- name: "shodan-mcp",
155
- version: "1.0.0",
156
- },
157
- instructions: `This MCP server provides comprehensive access to Shodan's network intelligence and security services:
18
+ instructions: `This MCP server provides comprehensive access to Shodan's network intelligence and security services:
158
19
 
159
20
  - Network Reconnaissance: Query detailed information about IP addresses, including open ports, services, and vulnerabilities
160
21
  - DNS Operations: Forward and reverse DNS lookups for domains and IP addresses
@@ -162,494 +23,12 @@ server.setRequestHandler(InitializeRequestSchema, async (request) => {
162
23
  - Device Discovery: Search Shodan's database of internet-connected devices with advanced filtering
163
24
 
164
25
  Each tool provides structured, formatted output for easy analysis and integration.`,
165
- };
166
- });
167
- // Register Tools
168
- server.setRequestHandler(ListToolsRequestSchema, async () => {
169
- const tools = [
170
- {
171
- name: "ip_lookup",
172
- description: "Retrieve comprehensive information about an IP address, including geolocation, open ports, running services, SSL certificates, hostnames, and cloud provider details if available. Returns service banners and HTTP server information when present.",
173
- inputSchema: zodToJsonSchema(IpLookupArgsSchema),
174
- },
175
- {
176
- name: "shodan_search",
177
- description: "Search Shodan's database of internet-connected devices. Returns detailed information about matching devices including services, vulnerabilities, and geographic distribution. Supports advanced search filters and returns country-based statistics.",
178
- inputSchema: zodToJsonSchema(ShodanSearchArgsSchema),
179
- },
180
- {
181
- name: "cve_lookup",
182
- description: "Query detailed vulnerability information from Shodan's CVEDB. Returns comprehensive CVE details including CVSS scores (v2/v3), EPSS probability and ranking, KEV status, proposed mitigations, ransomware associations, and affected products (CPEs).",
183
- inputSchema: zodToJsonSchema(CVELookupArgsSchema),
184
- },
185
- {
186
- name: "dns_lookup",
187
- description: "Resolve domain names to IP addresses using Shodan's DNS service. Supports batch resolution of multiple hostnames in a single query. Returns IP addresses mapped to their corresponding hostnames.",
188
- inputSchema: zodToJsonSchema(DnsLookupArgsSchema),
189
- },
190
- {
191
- name: "cpe_lookup",
192
- description: "Search for Common Platform Enumeration (CPE) entries by product name in Shodan's CVEDB. Supports pagination and can return either full CPE details or just the total count. Useful for identifying specific versions and configurations of software and hardware.",
193
- inputSchema: zodToJsonSchema(CpeLookupArgsSchema),
194
- },
195
- {
196
- name: "cves_by_product",
197
- description: "Search for vulnerabilities affecting specific products or CPEs. Supports filtering by KEV status, sorting by EPSS score, date ranges, and pagination. Can search by product name or CPE 2.3 identifier. Returns detailed vulnerability information including severity scores and impact assessments.",
198
- inputSchema: zodToJsonSchema(CVEsByProductArgsSchema),
199
- },
200
- {
201
- name: "reverse_dns_lookup",
202
- description: "Perform reverse DNS lookups to find hostnames associated with IP addresses. Supports batch lookups of multiple IP addresses in a single query. Returns all known hostnames for each IP address, with clear indication when no hostnames are found.",
203
- inputSchema: zodToJsonSchema(ReverseDnsLookupArgsSchema),
204
- },
205
- ];
206
- logToFile("Registered tools.");
207
- return { tools };
208
- });
209
- // Handle Tool Calls
210
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
211
- logToFile(`Tool called: ${request.params.name}`);
212
- try {
213
- const { name, arguments: args } = request.params;
214
- switch (name) {
215
- case "ip_lookup": {
216
- const parsedIpArgs = IpLookupArgsSchema.safeParse(args);
217
- if (!parsedIpArgs.success) {
218
- throw new Error("Invalid ip_lookup arguments");
219
- }
220
- const result = await queryShodan(`/shodan/host/${parsedIpArgs.data.ip}`, {});
221
- // Format the response in a user-friendly way
222
- const formattedResult = {
223
- "IP Information": {
224
- "IP Address": result.ip_str,
225
- "Organization": result.org,
226
- "ISP": result.isp,
227
- "ASN": result.asn,
228
- "Last Update": result.last_update
229
- },
230
- "Location": {
231
- "Country": result.country_name,
232
- "City": result.city,
233
- "Coordinates": `${result.latitude}, ${result.longitude}`,
234
- "Region": result.region_code
235
- },
236
- "Services": result.ports.map((port) => {
237
- const service = result.data.find((d) => d.port === port);
238
- return {
239
- "Port": port,
240
- "Protocol": service?.transport || "unknown",
241
- "Service": service?.data?.trim() || "No banner",
242
- ...(service?.http ? {
243
- "HTTP": {
244
- "Server": service.http.server,
245
- "Title": service.http.title,
246
- }
247
- } : {})
248
- };
249
- }),
250
- "Cloud Provider": result.data[0]?.cloud ? {
251
- "Provider": result.data[0].cloud.provider,
252
- "Service": result.data[0].cloud.service,
253
- "Region": result.data[0].cloud.region
254
- } : "Not detected",
255
- "Hostnames": result.hostnames || [],
256
- "Domains": result.domains || [],
257
- "Tags": result.tags || []
258
- };
259
- return {
260
- content: [
261
- {
262
- type: "text",
263
- text: JSON.stringify(formattedResult, null, 2),
264
- },
265
- ],
266
- };
267
- }
268
- case "shodan_search": {
269
- const parsedSearchArgs = ShodanSearchArgsSchema.safeParse(args);
270
- if (!parsedSearchArgs.success) {
271
- throw new Error("Invalid search arguments");
272
- }
273
- const result = await queryShodan("/shodan/host/search", {
274
- query: parsedSearchArgs.data.query,
275
- limit: parsedSearchArgs.data.max_results,
276
- });
277
- // Format the response in a user-friendly way
278
- const formattedResult = {
279
- "Search Summary": {
280
- "Query": parsedSearchArgs.data.query,
281
- "Total Results": result.total,
282
- "Results Returned": result.matches.length
283
- },
284
- "Country Distribution": result.facets?.country?.map(country => ({
285
- "Country": country.value,
286
- "Count": country.count,
287
- "Percentage": `${((country.count / result.total) * 100).toFixed(2)}%`
288
- })) || [],
289
- "Matches": result.matches.map(match => ({
290
- "Basic Information": {
291
- "IP Address": match.ip_str,
292
- "Organization": match.org,
293
- "ISP": match.isp,
294
- "ASN": match.asn,
295
- "Last Update": match.timestamp
296
- },
297
- "Location": {
298
- "Country": match.location.country_name,
299
- "City": match.location.city || "Unknown",
300
- "Region": match.location.region_code || "Unknown",
301
- "Coordinates": `${match.location.latitude}, ${match.location.longitude}`
302
- },
303
- "Service Details": {
304
- "Port": match.port,
305
- "Transport": match.transport,
306
- "Product": match.product || "Unknown",
307
- "Version": match.version || "Unknown",
308
- "CPE": match.cpe || []
309
- },
310
- "Web Information": match.http ? {
311
- "Server": match.http.server,
312
- "Title": match.http.title,
313
- "Robots.txt": match.http.robots ? "Present" : "Not found",
314
- "Sitemap": match.http.sitemap ? "Present" : "Not found"
315
- } : "No HTTP information",
316
- "Hostnames": match.hostnames,
317
- "Domains": match.domains
318
- }))
319
- };
320
- return {
321
- content: [
322
- {
323
- type: "text",
324
- text: JSON.stringify(formattedResult, null, 2),
325
- },
326
- ],
327
- };
328
- }
329
- case "cve_lookup": {
330
- const parsedCveArgs = CVELookupArgsSchema.safeParse(args);
331
- if (!parsedCveArgs.success) {
332
- throw new Error("Invalid CVE format. Please use format: CVE-YYYY-NNNNN (e.g., CVE-2021-44228)");
333
- }
334
- const cveId = parsedCveArgs.data.cve.toUpperCase();
335
- logToFile(`Looking up CVE: ${cveId}`);
336
- try {
337
- const result = await queryCVEDB(cveId);
338
- // Helper function to format CVSS score severity
339
- const getCvssSeverity = (score) => {
340
- if (score >= 9.0)
341
- return "Critical";
342
- if (score >= 7.0)
343
- return "High";
344
- if (score >= 4.0)
345
- return "Medium";
346
- if (score >= 0.1)
347
- return "Low";
348
- return "None";
349
- };
350
- // Format the response in a user-friendly way
351
- const formattedResult = {
352
- "Basic Information": {
353
- "CVE ID": result.cve_id,
354
- "Published": new Date(result.published_time).toLocaleString(),
355
- "Summary": result.summary
356
- },
357
- "Severity Scores": {
358
- "CVSS v3": result.cvss_v3 ? {
359
- "Score": result.cvss_v3,
360
- "Severity": getCvssSeverity(result.cvss_v3)
361
- } : "Not available",
362
- "CVSS v2": result.cvss_v2 ? {
363
- "Score": result.cvss_v2,
364
- "Severity": getCvssSeverity(result.cvss_v2)
365
- } : "Not available",
366
- "EPSS": result.epss ? {
367
- "Score": `${(result.epss * 100).toFixed(2)}%`,
368
- "Ranking": `Top ${(result.ranking_epss * 100).toFixed(2)}%`
369
- } : "Not available"
370
- },
371
- "Impact Assessment": {
372
- "Known Exploited Vulnerability": result.kev ? "Yes" : "No",
373
- "Proposed Action": result.propose_action || "No specific action proposed",
374
- "Ransomware Campaign": result.ransomware_campaign || "No known ransomware campaigns"
375
- },
376
- "Affected Products": result.cpes?.length > 0 ? result.cpes : ["No specific products listed"],
377
- "Additional Information": {
378
- "References": result.references?.length > 0 ? result.references : ["No references provided"]
379
- }
380
- };
381
- return {
382
- content: [
383
- {
384
- type: "text",
385
- text: JSON.stringify(formattedResult, null, 2),
386
- },
387
- ],
388
- };
389
- }
390
- catch (error) {
391
- return {
392
- content: [
393
- {
394
- type: "text",
395
- text: error.message,
396
- },
397
- ],
398
- isError: true,
399
- };
400
- }
401
- }
402
- case "dns_lookup": {
403
- const parsedDnsArgs = DnsLookupArgsSchema.safeParse(args);
404
- if (!parsedDnsArgs.success) {
405
- throw new Error("Invalid dns_lookup arguments");
406
- }
407
- // Join hostnames with commas for the API request
408
- const hostnamesString = parsedDnsArgs.data.hostnames.join(",");
409
- const result = await queryShodan("/dns/resolve", {
410
- hostnames: hostnamesString
411
- });
412
- // Format the response in a user-friendly way
413
- const formattedResult = {
414
- "DNS Resolutions": Object.entries(result).map(([hostname, ip]) => ({
415
- "Hostname": hostname,
416
- "IP Address": ip
417
- })),
418
- "Summary": {
419
- "Total Lookups": Object.keys(result).length,
420
- "Queried Hostnames": parsedDnsArgs.data.hostnames
421
- }
422
- };
423
- return {
424
- content: [
425
- {
426
- type: "text",
427
- text: JSON.stringify(formattedResult, null, 2)
428
- },
429
- ],
430
- };
431
- }
432
- case "cpe_lookup": {
433
- const parsedCpeArgs = CpeLookupArgsSchema.safeParse(args);
434
- if (!parsedCpeArgs.success) {
435
- throw new Error("Invalid cpe_lookup arguments");
436
- }
437
- try {
438
- const result = await queryCPEDB({
439
- product: parsedCpeArgs.data.product,
440
- count: parsedCpeArgs.data.count,
441
- skip: parsedCpeArgs.data.skip,
442
- limit: parsedCpeArgs.data.limit
443
- });
444
- // Format the response based on whether it's a count request or full CPE list
445
- const formattedResult = parsedCpeArgs.data.count
446
- ? { total_cpes: result.total }
447
- : {
448
- cpes: result.cpes,
449
- skip: parsedCpeArgs.data.skip,
450
- limit: parsedCpeArgs.data.limit,
451
- total_returned: result.cpes.length
452
- };
453
- return {
454
- content: [
455
- {
456
- type: "text",
457
- text: JSON.stringify(formattedResult, null, 2),
458
- },
459
- ],
460
- };
461
- }
462
- catch (error) {
463
- return {
464
- content: [
465
- {
466
- type: "text",
467
- text: error.message,
468
- },
469
- ],
470
- isError: true,
471
- };
472
- }
473
- }
474
- case "cves_by_product": {
475
- const parsedArgs = CVEsByProductArgsSchema.safeParse(args);
476
- if (!parsedArgs.success) {
477
- throw new Error("Invalid arguments. Must provide either cpe23 or product name, but not both.");
478
- }
479
- try {
480
- const result = await queryCVEsByProduct({
481
- cpe23: parsedArgs.data.cpe23,
482
- product: parsedArgs.data.product,
483
- count: parsedArgs.data.count,
484
- is_kev: parsedArgs.data.is_kev,
485
- sort_by_epss: parsedArgs.data.sort_by_epss,
486
- skip: parsedArgs.data.skip,
487
- limit: parsedArgs.data.limit,
488
- start_date: parsedArgs.data.start_date,
489
- end_date: parsedArgs.data.end_date
490
- });
491
- // Helper function to format CVSS score severity
492
- const getCvssSeverity = (score) => {
493
- if (score >= 9.0)
494
- return "Critical";
495
- if (score >= 7.0)
496
- return "High";
497
- if (score >= 4.0)
498
- return "Medium";
499
- if (score >= 0.1)
500
- return "Low";
501
- return "None";
502
- };
503
- // Format the response based on whether it's a count request or full CVE list
504
- const formattedResult = parsedArgs.data.count
505
- ? {
506
- "Query Information": {
507
- "Product": parsedArgs.data.product || "N/A",
508
- "CPE 2.3": parsedArgs.data.cpe23 || "N/A",
509
- "KEV Only": parsedArgs.data.is_kev ? "Yes" : "No",
510
- "Sort by EPSS": parsedArgs.data.sort_by_epss ? "Yes" : "No"
511
- },
512
- "Results": {
513
- "Total CVEs Found": result.total
514
- }
515
- }
516
- : {
517
- "Query Information": {
518
- "Product": parsedArgs.data.product || "N/A",
519
- "CPE 2.3": parsedArgs.data.cpe23 || "N/A",
520
- "KEV Only": parsedArgs.data.is_kev ? "Yes" : "No",
521
- "Sort by EPSS": parsedArgs.data.sort_by_epss ? "Yes" : "No",
522
- "Date Range": parsedArgs.data.start_date ?
523
- `${parsedArgs.data.start_date} to ${parsedArgs.data.end_date || 'now'}` :
524
- "All dates"
525
- },
526
- "Results Summary": {
527
- "Total CVEs Found": result.total,
528
- "CVEs Returned": result.cves.length,
529
- "Page": `${Math.floor(parsedArgs.data.skip / parsedArgs.data.limit) + 1}`,
530
- "CVEs per Page": parsedArgs.data.limit
531
- },
532
- "Vulnerabilities": result.cves.map((cve) => ({
533
- "Basic Information": {
534
- "CVE ID": cve.cve_id,
535
- "Published": new Date(cve.published_time).toLocaleString(),
536
- "Summary": cve.summary
537
- },
538
- "Severity Scores": {
539
- "CVSS v3": cve.cvss_v3 ? {
540
- "Score": cve.cvss_v3,
541
- "Severity": getCvssSeverity(cve.cvss_v3)
542
- } : "Not available",
543
- "CVSS v2": cve.cvss_v2 ? {
544
- "Score": cve.cvss_v2,
545
- "Severity": getCvssSeverity(cve.cvss_v2)
546
- } : "Not available",
547
- "EPSS": cve.epss ? {
548
- "Score": `${(cve.epss * 100).toFixed(2)}%`,
549
- "Ranking": `Top ${(cve.ranking_epss * 100).toFixed(2)}%`
550
- } : "Not available"
551
- },
552
- "Impact Assessment": {
553
- "Known Exploited Vulnerability": cve.kev ? "Yes" : "No",
554
- "Proposed Action": cve.propose_action || "No specific action proposed",
555
- "Ransomware Campaign": cve.ransomware_campaign || "No known ransomware campaigns"
556
- },
557
- "References": cve.references?.length > 0 ? cve.references : ["No references provided"]
558
- }))
559
- };
560
- return {
561
- content: [
562
- {
563
- type: "text",
564
- text: JSON.stringify(formattedResult, null, 2),
565
- },
566
- ],
567
- };
568
- }
569
- catch (error) {
570
- return {
571
- content: [
572
- {
573
- type: "text",
574
- text: error.message,
575
- },
576
- ],
577
- isError: true,
578
- };
579
- }
580
- }
581
- case "reverse_dns_lookup": {
582
- const parsedArgs = ReverseDnsLookupArgsSchema.safeParse(args);
583
- if (!parsedArgs.success) {
584
- throw new Error("Invalid reverse_dns_lookup arguments");
585
- }
586
- // Join IPs with commas for the API request
587
- const ipsString = parsedArgs.data.ips.join(",");
588
- const result = await queryShodan("/dns/reverse", {
589
- ips: ipsString
590
- });
591
- // Format the response in a user-friendly way
592
- const formattedResult = {
593
- "Reverse DNS Resolutions": Object.entries(result).map(([ip, hostnames]) => ({
594
- "IP Address": ip,
595
- "Hostnames": hostnames.length > 0 ? hostnames : ["No hostnames found"]
596
- })),
597
- "Summary": {
598
- "Total IPs Queried": parsedArgs.data.ips.length,
599
- "IPs with Results": Object.keys(result).length,
600
- "Queried IP Addresses": parsedArgs.data.ips
601
- }
602
- };
603
- return {
604
- content: [
605
- {
606
- type: "text",
607
- text: JSON.stringify(formattedResult, null, 2)
608
- },
609
- ],
610
- };
611
- }
612
- default:
613
- throw new Error(`Unknown tool: ${name}`);
614
- }
615
- }
616
- catch (error) {
617
- const errorMessage = error instanceof Error ? error.message : String(error);
618
- logToFile(`Error handling tool call: ${errorMessage}`);
619
- return {
620
- content: [
621
- {
622
- type: "text",
623
- text: `Error: ${errorMessage}`,
624
- },
625
- ],
626
- isError: true,
627
- };
628
- }
629
- });
630
- // Start the Server
631
- async function runServer() {
632
- logToFile("Starting Shodan MCP Server...");
633
- try {
634
- const transport = new StdioServerTransport();
635
- await server.connect(transport);
636
- logToFile("Shodan MCP Server is running.");
637
- }
638
- catch (error) {
639
- logToFile(`Error connecting server: ${error.message}`);
640
- process.exit(1);
641
- }
642
- }
643
- // Handle process events
644
- process.on('uncaughtException', (error) => {
645
- logToFile(`Uncaught exception: ${error.message}`);
646
- process.exit(1);
647
- });
648
- process.on('unhandledRejection', (reason) => {
649
- logToFile(`Unhandled rejection: ${reason}`);
650
- process.exit(1);
651
- });
652
- runServer().catch((error) => {
653
- logToFile(`Fatal error: ${error.message}`);
654
- process.exit(1);
655
26
  });
27
+ registerIpLookup(server);
28
+ registerShodanSearch(server);
29
+ registerCveLookup(server);
30
+ registerDnsLookup(server);
31
+ registerReverseDnsLookup(server);
32
+ registerCpeLookup(server);
33
+ registerCvesByProduct(server);
34
+ server.start({ transportType: "stdio" });
@@ -0,0 +1,49 @@
1
+ import { z } from "zod";
2
+ import { queryCPEDB } from "../helpers.js";
3
+ export function registerCpeLookup(server) {
4
+ server.addTool({
5
+ name: "cpe_lookup",
6
+ description: "Search for Common Platform Enumeration (CPE) entries by product name in Shodan's CVEDB. Supports pagination and can return either full CPE details or just the total count. Useful for identifying specific versions and configurations of software and hardware.",
7
+ parameters: z.object({
8
+ product: z
9
+ .string()
10
+ .describe("The name of the product to search for CPEs."),
11
+ count: z
12
+ .boolean()
13
+ .optional()
14
+ .default(false)
15
+ .describe("If true, returns only the count of matching CPEs."),
16
+ skip: z
17
+ .number()
18
+ .optional()
19
+ .default(0)
20
+ .describe("Number of CPEs to skip (for pagination)."),
21
+ limit: z
22
+ .number()
23
+ .optional()
24
+ .default(1000)
25
+ .describe("Maximum number of CPEs to return (max 1000)."),
26
+ }),
27
+ annotations: {
28
+ readOnlyHint: true,
29
+ openWorldHint: true,
30
+ },
31
+ execute: async (args) => {
32
+ const result = await queryCPEDB({
33
+ product: args.product,
34
+ count: args.count,
35
+ skip: args.skip,
36
+ limit: args.limit,
37
+ });
38
+ const formattedResult = args.count
39
+ ? { total_cpes: result.total }
40
+ : {
41
+ cpes: result.cpes,
42
+ skip: args.skip,
43
+ limit: args.limit,
44
+ total_returned: result.cpes.length,
45
+ };
46
+ return JSON.stringify(formattedResult, null, 2);
47
+ },
48
+ });
49
+ }
@@ -0,0 +1,63 @@
1
+ import { z } from "zod";
2
+ import { queryCVEDB, getCvssSeverity } from "../helpers.js";
3
+ export function registerCveLookup(server) {
4
+ server.addTool({
5
+ name: "cve_lookup",
6
+ description: "Query detailed vulnerability information from Shodan's CVEDB. Returns comprehensive CVE details including CVSS scores (v2/v3), EPSS probability and ranking, KEV status, proposed mitigations, ransomware associations, and affected products (CPEs).",
7
+ parameters: z.object({
8
+ cve: z
9
+ .string()
10
+ .regex(/^CVE-\d{4}-\d{4,}$/i, "Must be a valid CVE ID format (e.g., CVE-2021-44228)")
11
+ .describe("The CVE identifier to query (format: CVE-YYYY-NNNNN)."),
12
+ }),
13
+ annotations: {
14
+ readOnlyHint: true,
15
+ openWorldHint: true,
16
+ },
17
+ execute: async (args) => {
18
+ const cveId = args.cve.toUpperCase();
19
+ const result = await queryCVEDB(cveId);
20
+ const formattedResult = {
21
+ "Basic Information": {
22
+ "CVE ID": result.cve_id,
23
+ Published: new Date(result.published_time).toLocaleString(),
24
+ Summary: result.summary,
25
+ },
26
+ "Severity Scores": {
27
+ "CVSS v3": result.cvss_v3
28
+ ? {
29
+ Score: result.cvss_v3,
30
+ Severity: getCvssSeverity(result.cvss_v3),
31
+ }
32
+ : "Not available",
33
+ "CVSS v2": result.cvss_v2
34
+ ? {
35
+ Score: result.cvss_v2,
36
+ Severity: getCvssSeverity(result.cvss_v2),
37
+ }
38
+ : "Not available",
39
+ EPSS: result.epss
40
+ ? {
41
+ Score: `${(result.epss * 100).toFixed(2)}%`,
42
+ Ranking: `Top ${(result.ranking_epss * 100).toFixed(2)}%`,
43
+ }
44
+ : "Not available",
45
+ },
46
+ "Impact Assessment": {
47
+ "Known Exploited Vulnerability": result.kev ? "Yes" : "No",
48
+ "Proposed Action": result.propose_action || "No specific action proposed",
49
+ "Ransomware Campaign": result.ransomware_campaign || "No known ransomware campaigns",
50
+ },
51
+ "Affected Products": result.cpes?.length > 0
52
+ ? result.cpes
53
+ : ["No specific products listed"],
54
+ "Additional Information": {
55
+ References: result.references?.length > 0
56
+ ? result.references
57
+ : ["No references provided"],
58
+ },
59
+ };
60
+ return JSON.stringify(formattedResult, null, 2);
61
+ },
62
+ });
63
+ }
@@ -0,0 +1,141 @@
1
+ import { UserError } from "fastmcp";
2
+ import { z } from "zod";
3
+ import { queryCVEsByProduct, getCvssSeverity } from "../helpers.js";
4
+ export function registerCvesByProduct(server) {
5
+ server.addTool({
6
+ name: "cves_by_product",
7
+ description: "Search for vulnerabilities affecting specific products or CPEs. Supports filtering by KEV status, sorting by EPSS score, date ranges, and pagination. Can search by product name or CPE 2.3 identifier. Returns detailed vulnerability information including severity scores and impact assessments.",
8
+ parameters: z.object({
9
+ cpe23: z
10
+ .string()
11
+ .optional()
12
+ .describe("The CPE version 2.3 identifier (format: cpe:2.3:part:vendor:product:version)."),
13
+ product: z
14
+ .string()
15
+ .optional()
16
+ .describe("The name of the product to search for CVEs."),
17
+ count: z
18
+ .boolean()
19
+ .optional()
20
+ .default(false)
21
+ .describe("If true, returns only the count of matching CVEs."),
22
+ is_kev: z
23
+ .boolean()
24
+ .optional()
25
+ .default(false)
26
+ .describe("If true, returns only CVEs with the KEV flag set."),
27
+ sort_by_epss: z
28
+ .boolean()
29
+ .optional()
30
+ .default(false)
31
+ .describe("If true, sorts CVEs by EPSS score in descending order."),
32
+ skip: z
33
+ .number()
34
+ .optional()
35
+ .default(0)
36
+ .describe("Number of CVEs to skip (for pagination)."),
37
+ limit: z
38
+ .number()
39
+ .optional()
40
+ .default(1000)
41
+ .describe("Maximum number of CVEs to return (max 1000)."),
42
+ start_date: z
43
+ .string()
44
+ .optional()
45
+ .describe("Start date for filtering CVEs (format: YYYY-MM-DDTHH:MM:SS)."),
46
+ end_date: z
47
+ .string()
48
+ .optional()
49
+ .describe("End date for filtering CVEs (format: YYYY-MM-DDTHH:MM:SS)."),
50
+ }),
51
+ annotations: {
52
+ readOnlyHint: true,
53
+ openWorldHint: true,
54
+ },
55
+ execute: async (args) => {
56
+ if (args.cpe23 && args.product) {
57
+ throw new UserError("Cannot specify both cpe23 and product. Use only one.");
58
+ }
59
+ if (!args.cpe23 && !args.product) {
60
+ throw new UserError("Must specify either cpe23 or product.");
61
+ }
62
+ const result = await queryCVEsByProduct({
63
+ cpe23: args.cpe23,
64
+ product: args.product,
65
+ count: args.count,
66
+ is_kev: args.is_kev,
67
+ sort_by_epss: args.sort_by_epss,
68
+ skip: args.skip,
69
+ limit: args.limit,
70
+ start_date: args.start_date,
71
+ end_date: args.end_date,
72
+ });
73
+ const formattedResult = args.count
74
+ ? {
75
+ "Query Information": {
76
+ Product: args.product || "N/A",
77
+ "CPE 2.3": args.cpe23 || "N/A",
78
+ "KEV Only": args.is_kev ? "Yes" : "No",
79
+ "Sort by EPSS": args.sort_by_epss ? "Yes" : "No",
80
+ },
81
+ Results: {
82
+ "Total CVEs Found": result.total,
83
+ },
84
+ }
85
+ : {
86
+ "Query Information": {
87
+ Product: args.product || "N/A",
88
+ "CPE 2.3": args.cpe23 || "N/A",
89
+ "KEV Only": args.is_kev ? "Yes" : "No",
90
+ "Sort by EPSS": args.sort_by_epss ? "Yes" : "No",
91
+ "Date Range": args.start_date
92
+ ? `${args.start_date} to ${args.end_date || "now"}`
93
+ : "All dates",
94
+ },
95
+ "Results Summary": {
96
+ "Total CVEs Found": result.total,
97
+ "CVEs Returned": result.cves.length,
98
+ Page: `${Math.floor(args.skip / args.limit) + 1}`,
99
+ "CVEs per Page": args.limit,
100
+ },
101
+ Vulnerabilities: result.cves.map((cve) => ({
102
+ "Basic Information": {
103
+ "CVE ID": cve.cve_id,
104
+ Published: new Date(cve.published_time).toLocaleString(),
105
+ Summary: cve.summary,
106
+ },
107
+ "Severity Scores": {
108
+ "CVSS v3": cve.cvss_v3
109
+ ? {
110
+ Score: cve.cvss_v3,
111
+ Severity: getCvssSeverity(cve.cvss_v3),
112
+ }
113
+ : "Not available",
114
+ "CVSS v2": cve.cvss_v2
115
+ ? {
116
+ Score: cve.cvss_v2,
117
+ Severity: getCvssSeverity(cve.cvss_v2),
118
+ }
119
+ : "Not available",
120
+ EPSS: cve.epss
121
+ ? {
122
+ Score: `${(cve.epss * 100).toFixed(2)}%`,
123
+ Ranking: `Top ${(cve.ranking_epss * 100).toFixed(2)}%`,
124
+ }
125
+ : "Not available",
126
+ },
127
+ "Impact Assessment": {
128
+ "Known Exploited Vulnerability": cve.kev ? "Yes" : "No",
129
+ "Proposed Action": cve.propose_action || "No specific action proposed",
130
+ "Ransomware Campaign": cve.ransomware_campaign ||
131
+ "No known ransomware campaigns",
132
+ },
133
+ References: cve.references?.length > 0
134
+ ? cve.references
135
+ : ["No references provided"],
136
+ })),
137
+ };
138
+ return JSON.stringify(formattedResult, null, 2);
139
+ },
140
+ });
141
+ }
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+ import { queryShodan } from "../helpers.js";
3
+ export function registerDnsLookup(server) {
4
+ server.addTool({
5
+ name: "dns_lookup",
6
+ description: "Resolve domain names to IP addresses using Shodan's DNS service. Supports batch resolution of multiple hostnames in a single query. Returns IP addresses mapped to their corresponding hostnames.",
7
+ parameters: z.object({
8
+ hostnames: z
9
+ .array(z.string())
10
+ .describe("List of hostnames to resolve."),
11
+ }),
12
+ annotations: {
13
+ readOnlyHint: true,
14
+ openWorldHint: true,
15
+ },
16
+ execute: async (args) => {
17
+ const hostnamesString = args.hostnames.join(",");
18
+ const result = await queryShodan("/dns/resolve", {
19
+ hostnames: hostnamesString,
20
+ });
21
+ const formattedResult = {
22
+ "DNS Resolutions": Object.entries(result).map(([hostname, ip]) => ({
23
+ Hostname: hostname,
24
+ "IP Address": ip,
25
+ })),
26
+ Summary: {
27
+ "Total Lookups": Object.keys(result).length,
28
+ "Queried Hostnames": args.hostnames,
29
+ },
30
+ };
31
+ return JSON.stringify(formattedResult, null, 2);
32
+ },
33
+ });
34
+ }
@@ -0,0 +1,60 @@
1
+ import { z } from "zod";
2
+ import { queryShodan } from "../helpers.js";
3
+ export function registerIpLookup(server) {
4
+ server.addTool({
5
+ name: "ip_lookup",
6
+ description: "Retrieve comprehensive information about an IP address, including geolocation, open ports, running services, SSL certificates, hostnames, and cloud provider details if available. Returns service banners and HTTP server information when present.",
7
+ parameters: z.object({
8
+ ip: z.string().describe("The IP address to query."),
9
+ }),
10
+ annotations: {
11
+ readOnlyHint: true,
12
+ openWorldHint: true,
13
+ },
14
+ execute: async (args) => {
15
+ const result = await queryShodan(`/shodan/host/${args.ip}`, {});
16
+ const formattedResult = {
17
+ "IP Information": {
18
+ "IP Address": result.ip_str,
19
+ Organization: result.org,
20
+ ISP: result.isp,
21
+ ASN: result.asn,
22
+ "Last Update": result.last_update,
23
+ },
24
+ Location: {
25
+ Country: result.country_name,
26
+ City: result.city,
27
+ Coordinates: `${result.latitude}, ${result.longitude}`,
28
+ Region: result.region_code,
29
+ },
30
+ Services: result.ports.map((port) => {
31
+ const service = result.data.find((d) => d.port === port);
32
+ return {
33
+ Port: port,
34
+ Protocol: service?.transport || "unknown",
35
+ Service: service?.data?.trim() || "No banner",
36
+ ...(service?.http
37
+ ? {
38
+ HTTP: {
39
+ Server: service.http.server,
40
+ Title: service.http.title,
41
+ },
42
+ }
43
+ : {}),
44
+ };
45
+ }),
46
+ "Cloud Provider": result.data[0]?.cloud
47
+ ? {
48
+ Provider: result.data[0].cloud.provider,
49
+ Service: result.data[0].cloud.service,
50
+ Region: result.data[0].cloud.region,
51
+ }
52
+ : "Not detected",
53
+ Hostnames: result.hostnames || [],
54
+ Domains: result.domains || [],
55
+ Tags: result.tags || [],
56
+ };
57
+ return JSON.stringify(formattedResult, null, 2);
58
+ },
59
+ });
60
+ }
@@ -0,0 +1,35 @@
1
+ import { z } from "zod";
2
+ import { queryShodan } from "../helpers.js";
3
+ export function registerReverseDnsLookup(server) {
4
+ server.addTool({
5
+ name: "reverse_dns_lookup",
6
+ description: "Perform reverse DNS lookups to find hostnames associated with IP addresses. Supports batch lookups of multiple IP addresses in a single query. Returns all known hostnames for each IP address, with clear indication when no hostnames are found.",
7
+ parameters: z.object({
8
+ ips: z
9
+ .array(z.string())
10
+ .describe("List of IP addresses to perform reverse DNS lookup on."),
11
+ }),
12
+ annotations: {
13
+ readOnlyHint: true,
14
+ openWorldHint: true,
15
+ },
16
+ execute: async (args) => {
17
+ const ipsString = args.ips.join(",");
18
+ const result = await queryShodan("/dns/reverse", {
19
+ ips: ipsString,
20
+ });
21
+ const formattedResult = {
22
+ "Reverse DNS Resolutions": Object.entries(result).map(([ip, hostnames]) => ({
23
+ "IP Address": ip,
24
+ Hostnames: hostnames.length > 0 ? hostnames : ["No hostnames found"],
25
+ })),
26
+ Summary: {
27
+ "Total IPs Queried": args.ips.length,
28
+ "IPs with Results": Object.keys(result).length,
29
+ "Queried IP Addresses": args.ips,
30
+ },
31
+ };
32
+ return JSON.stringify(formattedResult, null, 2);
33
+ },
34
+ });
35
+ }
@@ -0,0 +1,71 @@
1
+ import { z } from "zod";
2
+ import { queryShodan } from "../helpers.js";
3
+ export function registerShodanSearch(server) {
4
+ server.addTool({
5
+ name: "shodan_search",
6
+ description: "Search Shodan's database of internet-connected devices. Returns detailed information about matching devices including services, vulnerabilities, and geographic distribution. Supports advanced search filters and returns country-based statistics.",
7
+ parameters: z.object({
8
+ query: z.string().describe("Search query for Shodan."),
9
+ max_results: z
10
+ .number()
11
+ .optional()
12
+ .default(10)
13
+ .describe("Maximum results to return."),
14
+ }),
15
+ annotations: {
16
+ readOnlyHint: true,
17
+ openWorldHint: true,
18
+ },
19
+ execute: async (args) => {
20
+ const result = await queryShodan("/shodan/host/search", {
21
+ query: args.query,
22
+ limit: args.max_results,
23
+ });
24
+ const formattedResult = {
25
+ "Search Summary": {
26
+ Query: args.query,
27
+ "Total Results": result.total,
28
+ "Results Returned": result.matches.length,
29
+ },
30
+ "Country Distribution": result.facets?.country?.map((country) => ({
31
+ Country: country.value,
32
+ Count: country.count,
33
+ Percentage: `${((country.count / result.total) * 100).toFixed(2)}%`,
34
+ })) || [],
35
+ Matches: result.matches.map((match) => ({
36
+ "Basic Information": {
37
+ "IP Address": match.ip_str,
38
+ Organization: match.org,
39
+ ISP: match.isp,
40
+ ASN: match.asn,
41
+ "Last Update": match.timestamp,
42
+ },
43
+ Location: {
44
+ Country: match.location.country_name,
45
+ City: match.location.city || "Unknown",
46
+ Region: match.location.region_code || "Unknown",
47
+ Coordinates: `${match.location.latitude}, ${match.location.longitude}`,
48
+ },
49
+ "Service Details": {
50
+ Port: match.port,
51
+ Transport: match.transport,
52
+ Product: match.product || "Unknown",
53
+ Version: match.version || "Unknown",
54
+ CPE: match.cpe || [],
55
+ },
56
+ "Web Information": match.http
57
+ ? {
58
+ Server: match.http.server,
59
+ Title: match.http.title,
60
+ "Robots.txt": match.http.robots ? "Present" : "Not found",
61
+ Sitemap: match.http.sitemap ? "Present" : "Not found",
62
+ }
63
+ : "No HTTP information",
64
+ Hostnames: match.hostnames,
65
+ Domains: match.domains,
66
+ })),
67
+ };
68
+ return JSON.stringify(formattedResult, null, 2);
69
+ },
70
+ });
71
+ }
package/build/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@burtthecoder/mcp-shodan",
4
- "version": "1.0.16",
4
+ "version": "1.0.20",
5
5
  "description": "A Model Context Protocol server for Shodan API queries.",
6
6
  "main": "build/index.js",
7
7
  "bin": {
@@ -12,11 +12,10 @@
12
12
  "prepublishOnly": "npm run build"
13
13
  },
14
14
  "dependencies": {
15
- "@modelcontextprotocol/sdk": "^0.6.0",
16
15
  "axios": "^1.7.8",
17
16
  "dotenv": "^16.4.5",
18
- "zod": "^3.22.2",
19
- "zod-to-json-schema": "^3.23.5"
17
+ "fastmcp": "^3.33.0",
18
+ "zod": "^3.23.0"
20
19
  },
21
20
  "devDependencies": {
22
21
  "typescript": "^5.3.3",