#
tokens: 13457/50000 10/10 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .env.example
├── .github
│   └── workflows
│       └── npm-publish.yml
├── .gitignore
├── .npmignore
├── Dockerfile
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│   └── index.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | node_modules/
2 | build/
3 | *.log
4 | .env
5 | .env.local
6 | .env.*.local
7 | 
```

--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------

```
 1 | # Source files (since we publish the build)
 2 | src/
 3 | 
 4 | # Development files
 5 | .github/
 6 | tsconfig.json
 7 | .env*
 8 | *.log
 9 | 
10 | # Don't ignore the build directory
11 | !build/
12 | 
```

--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------

```
1 | # Required: Your Shodan API key from https://account.shodan.io/
2 | SHODAN_API_KEY=your_api_key_here
3 | 
4 | # Optional: Debug logging level for MCP protocol
5 | # DEBUG=mcp:*
6 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Shodan MCP Server
  2 | 
  3 | [![smithery badge](https://smithery.ai/badge/@burtthecoder/mcp-shodan)](https://smithery.ai/server/@burtthecoder/mcp-shodan)
  4 | 
  5 | A Model Context Protocol (MCP) server for querying the [Shodan API](https://shodan.io) and [Shodan CVEDB](https://cvedb.shodan.io). This server provides comprehensive access to Shodan's network intelligence and security services, including IP reconnaissance, DNS operations, vulnerability tracking, and device discovery. All tools provide structured, formatted output for easy analysis and integration.
  6 | 
  7 | <a href="https://glama.ai/mcp/servers/79uakvikcj"><img width="380" height="200" src="https://glama.ai/mcp/servers/79uakvikcj/badge" /></a>
  8 | 
  9 | ## Quick Start (Recommended)
 10 | 
 11 | ### Installing via Smithery
 12 | 
 13 | To install Shodan Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@burtthecoder/mcp-shodan):
 14 | 
 15 | ```bash
 16 | npx -y @smithery/cli install @burtthecoder/mcp-shodan --client claude
 17 | ```
 18 | 
 19 | ### Installing Manually
 20 | 1. Install the server globally via npm:
 21 | ```bash
 22 | npm install -g @burtthecoder/mcp-shodan
 23 | ```
 24 | 
 25 | 2. Add to your Claude Desktop configuration file:
 26 | ```json
 27 | {
 28 |   "mcpServers": {
 29 |     "shodan": {
 30 |       "command": "mcp-shodan",
 31 |       "env": {
 32 |         "SHODAN_API_KEY": "your-shodan-api-key"
 33 |       }
 34 |     }
 35 |   }
 36 | }
 37 | ```
 38 | 
 39 | Configuration file location:
 40 | - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
 41 | - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
 42 | 
 43 | 3. Restart Claude Desktop
 44 | 
 45 | ## Alternative Setup (From Source)
 46 | 
 47 | If you prefer to run from source or need to modify the code:
 48 | 
 49 | 1. Clone and build:
 50 | ```bash
 51 | git clone https://github.com/BurtTheCoder/mcp-shodan.git
 52 | cd mcp-shodan
 53 | npm install
 54 | npm run build
 55 | ```
 56 | 
 57 | 2. Add to your Claude Desktop configuration:
 58 | ```json
 59 | {
 60 |   "mcpServers": {
 61 |     "shodan": {
 62 |       "command": "node",
 63 |       "args": ["/absolute/path/to/mcp-shodan/build/index.js"],
 64 |       "env": {
 65 |         "SHODAN_API_KEY": "your-shodan-api-key"
 66 |       }
 67 |     }
 68 |   }
 69 | }
 70 | ```
 71 | 
 72 | ## Features
 73 | 
 74 | - **Network Reconnaissance**: Query detailed information about IP addresses, including open ports, services, and vulnerabilities
 75 | - **DNS Operations**: Forward and reverse DNS lookups for domains and IP addresses
 76 | - **Vulnerability Intelligence**: Access to Shodan's CVEDB for detailed vulnerability information, CPE lookups, and product-specific CVE tracking
 77 | - **Device Discovery**: Search Shodan's database of internet-connected devices with advanced filtering
 78 | 
 79 | ## Tools
 80 | 
 81 | ### 1. IP Lookup Tool
 82 | - Name: `ip_lookup`
 83 | - Description: Retrieve comprehensive information about an IP address, including geolocation, open ports, running services, SSL certificates, hostnames, and cloud provider details if available
 84 | - Parameters:
 85 |   * `ip` (required): IP address to lookup
 86 | - Returns:
 87 |   * IP Information (address, organization, ISP, ASN)
 88 |   * Location (country, city, coordinates)
 89 |   * Services (ports, protocols, banners)
 90 |   * Cloud Provider details (if available)
 91 |   * Associated hostnames and domains
 92 |   * Tags
 93 | 
 94 | ### 2. Shodan Search Tool
 95 | - Name: `shodan_search`
 96 | - Description: Search Shodan's database of internet-connected devices
 97 | - Parameters:
 98 |   * `query` (required): Shodan search query
 99 |   * `max_results` (optional, default: 10): Number of results to return
100 | - Returns:
101 |   * Search summary with total results
102 |   * Country-based distribution statistics
103 |   * Detailed device information including:
104 |     - Basic information (IP, organization, ISP)
105 |     - Location data
106 |     - Service details
107 |     - Web server information
108 |     - Associated hostnames and domains
109 | 
110 | ### 3. CVE Lookup Tool
111 | - Name: `cve_lookup`
112 | - Description: Query detailed vulnerability information from Shodan's CVEDB
113 | - Parameters:
114 |   * `cve` (required): CVE identifier in format CVE-YYYY-NNNNN (e.g., CVE-2021-44228)
115 | - Returns:
116 |   * Basic Information (ID, published date, summary)
117 |   * Severity Scores:
118 |     - CVSS v2 and v3 with severity levels
119 |     - EPSS probability and ranking
120 |   * Impact Assessment:
121 |     - KEV status
122 |     - Proposed mitigations
123 |     - Ransomware associations
124 |   * Affected products (CPEs)
125 |   * References
126 | 
127 | ### 4. DNS Lookup Tool
128 | - Name: `dns_lookup`
129 | - Description: Resolve domain names to IP addresses using Shodan's DNS service
130 | - Parameters:
131 |   * `hostnames` (required): Array of hostnames to resolve
132 | - Returns:
133 |   * DNS resolutions mapping hostnames to IPs
134 |   * Summary of total lookups and queried hostnames
135 | 
136 | ### 5. Reverse DNS Lookup Tool
137 | - Name: `reverse_dns_lookup`
138 | - Description: Perform reverse DNS lookups to find hostnames associated with IP addresses
139 | - Parameters:
140 |   * `ips` (required): Array of IP addresses to lookup
141 | - Returns:
142 |   * Reverse DNS resolutions mapping IPs to hostnames
143 |   * Summary of total lookups and results
144 | 
145 | ### 6. CPE Lookup Tool
146 | - Name: `cpe_lookup`
147 | - Description: Search for Common Platform Enumeration (CPE) entries by product name
148 | - Parameters:
149 |   * `product` (required): Name of the product to search for
150 |   * `count` (optional, default: false): If true, returns only the count of matching CPEs
151 |   * `skip` (optional, default: 0): Number of CPEs to skip (for pagination)
152 |   * `limit` (optional, default: 1000): Maximum number of CPEs to return
153 | - Returns:
154 |   * When count is true: Total number of matching CPEs
155 |   * When count is false: List of CPEs with pagination details
156 | 
157 | ### 7. CVEs by Product Tool
158 | - Name: `cves_by_product`
159 | - Description: Search for vulnerabilities affecting specific products or CPEs
160 | - Parameters:
161 |   * `cpe23` (optional): CPE 2.3 identifier (format: cpe:2.3:part:vendor:product:version)
162 |   * `product` (optional): Name of the product to search for CVEs
163 |   * `count` (optional, default: false): If true, returns only the count of matching CVEs
164 |   * `is_kev` (optional, default: false): If true, returns only CVEs with KEV flag set
165 |   * `sort_by_epss` (optional, default: false): If true, sorts CVEs by EPSS score
166 |   * `skip` (optional, default: 0): Number of CVEs to skip (for pagination)
167 |   * `limit` (optional, default: 1000): Maximum number of CVEs to return
168 |   * `start_date` (optional): Start date for filtering CVEs (format: YYYY-MM-DDTHH:MM:SS)
169 |   * `end_date` (optional): End date for filtering CVEs (format: YYYY-MM-DDTHH:MM:SS)
170 | - Notes:
171 |   * Must provide either cpe23 or product, but not both
172 |   * Date filtering uses published time of CVEs
173 | - Returns:
174 |   * Query information
175 |   * Results summary with pagination details
176 |   * Detailed vulnerability information including:
177 |     - Basic information
178 |     - Severity scores
179 |     - Impact assessments
180 |     - References
181 | 
182 | ## Requirements
183 | 
184 | - Node.js (v18 or later)
185 | - A valid [Shodan API Key](https://account.shodan.io/)
186 | 
187 | ## Troubleshooting
188 | 
189 | ### API Key Issues
190 | 
191 | If you see API key related errors (e.g., "Request failed with status code 401"):
192 | 
193 | 1. Verify your API key:
194 |    - Must be a valid Shodan API key from your [account settings](https://account.shodan.io/)
195 |    - Ensure the key has sufficient credits/permissions for the operation
196 |    - Check for extra spaces or quotes around the key in the configuration
197 |    - Verify the key is correctly set in the SHODAN_API_KEY environment variable
198 | 
199 | 2. Common Error Codes:
200 |    - 401 Unauthorized: Invalid API key or missing authentication
201 |    - 402 Payment Required: Out of query credits
202 |    - 429 Too Many Requests: Rate limit exceeded
203 | 
204 | 3. Configuration Steps:
205 |    a. Get your API key from [Shodan Account](https://account.shodan.io/)
206 |    b. Add it to your configuration file:
207 |       ```json
208 |       {
209 |         "mcpServers": {
210 |           "shodan": {
211 |             "command": "mcp-shodan",
212 |             "env": {
213 |               "SHODAN_API_KEY": "your-actual-api-key-here"
214 |             }
215 |           }
216 |         }
217 |       }
218 |       ```
219 |    c. Save the config file
220 |    d. Restart Claude Desktop
221 | 
222 | 4. Testing Your Key:
223 |    - Try a simple query first (e.g., dns_lookup for "google.com")
224 |    - Check your [Shodan account dashboard](https://account.shodan.io/) for credit status
225 |    - Verify the key works directly with curl:
226 |      ```bash
227 |      curl "https://api.shodan.io/dns/resolve?hostnames=google.com&key=your-api-key"
228 |      ```
229 | 
230 | ### Module Loading Issues
231 | 
232 | If you see module loading errors:
233 | 1. For global installation: Use the simple configuration shown in Quick Start
234 | 2. For source installation: Ensure you're using Node.js v18 or later
235 | 
236 | ## Development
237 | 
238 | To run in development mode with hot reloading:
239 | ```bash
240 | npm run dev
241 | ```
242 | 
243 | ## Error Handling
244 | 
245 | The server includes comprehensive error handling for:
246 | - Invalid API keys
247 | - Rate limiting
248 | - Network errors
249 | - Invalid input parameters
250 | - Invalid CVE formats
251 | - Invalid CPE lookup parameters
252 | - Invalid date formats
253 | - Mutually exclusive parameter validation
254 | 
255 | ## Version History
256 | 
257 | - v1.0.12: Added reverse DNS lookup and improved output formatting
258 | - v1.0.7: Added CVEs by Product search functionality and renamed vulnerabilities tool to cve_lookup
259 | - v1.0.6: Added CVEDB integration for enhanced CVE lookups and CPE search functionality
260 | - v1.0.0: Initial release with core functionality
261 | 
262 | ## Contributing
263 | 
264 | 1. Fork the repository
265 | 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
266 | 3. Commit your changes (`git commit -m 'Add amazing feature'`)
267 | 4. Push to the branch (`git push origin feature/amazing-feature`)
268 | 5. Open a Pull Request
269 | 
270 | ## License
271 | 
272 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
273 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |     "compilerOptions": {
 3 |       "target": "ES2022",
 4 |       "module": "Node16",
 5 |       "moduleResolution": "Node16",
 6 |       "outDir": "./build",
 7 |       "rootDir": "./src",
 8 |       "strict": true,
 9 |       "esModuleInterop": true,
10 |       "skipLibCheck": true,
11 |       "forceConsistentCasingInFileNames": true
12 |     },
13 |     "include": ["src/**/*"],
14 |     "exclude": ["node_modules"]
15 |   }
16 |   
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     required:
 9 |       - shodanApiKey
10 |     properties:
11 |       shodanApiKey:
12 |         type: string
13 |         description: The API key for the Shodan API.
14 |   commandFunction:
15 |     # A function that produces the CLI command to start the MCP on stdio.
16 |     |-
17 |     config => ({ command: 'node', args: ['build/index.js'], env: { SHODAN_API_KEY: config.shodanApiKey } })
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
 2 | # Start from the official Node.js image with the desired version
 3 | FROM node:18-alpine AS builder
 4 | 
 5 | # Set the working directory inside the Docker image
 6 | WORKDIR /app
 7 | 
 8 | # Copy the package.json and package-lock.json to the working directory
 9 | COPY package*.json ./
10 | 
11 | # Install dependencies
12 | RUN npm install
13 | 
14 | # Copy the source code to the working directory
15 | COPY ./src ./src
16 | COPY tsconfig.json ./
17 | 
18 | # Run the build command to compile TypeScript to JavaScript
19 | RUN npm run build
20 | 
21 | # Now create the production image, based on a smaller image
22 | FROM node:18-alpine
23 | 
24 | # Set the working directory inside the Docker image
25 | WORKDIR /app
26 | 
27 | # Copy the compiled JavaScript files from the builder stage
28 | COPY --from=builder /app/build /app/build
29 | COPY --from=builder /app/package.json /app/package.json
30 | COPY --from=builder /app/package-lock.json /app/package-lock.json
31 | 
32 | # Install only production dependencies
33 | RUN npm ci --omit=dev
34 | 
35 | # Set environment variable for Shodan API Key
36 | ENV SHODAN_API_KEY=your-shodan-api-key
37 | 
38 | # Command to run the MCP server
39 | ENTRYPOINT ["node", "build/index.js"]
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "type": "module",
 3 |   "name": "@burtthecoder/mcp-shodan",
 4 |   "version": "1.0.16",
 5 |   "description": "A Model Context Protocol server for Shodan API queries.",
 6 |   "main": "build/index.js",
 7 |   "bin": {
 8 |     "mcp-shodan": "build/index.js"
 9 |   },
10 |   "scripts": {
11 |     "build": "tsc && chmod +x build/index.js",
12 |     "prepublishOnly": "npm run build"
13 |   },
14 |   "dependencies": {
15 |     "@modelcontextprotocol/sdk": "^0.6.0",
16 |     "axios": "^1.7.8",
17 |     "dotenv": "^16.4.5",
18 |     "zod": "^3.22.2",
19 |     "zod-to-json-schema": "^3.23.5"
20 |   },
21 |   "devDependencies": {
22 |     "typescript": "^5.3.3",
23 |     "@types/node": "^20.11.24"
24 |   },
25 |   "files": [
26 |     "build",
27 |     "README.md",
28 |     "LICENSE"
29 |   ],
30 |   "author": "BurtTheCoder",
31 |   "license": "MIT",
32 |   "repository": {
33 |     "type": "git",
34 |     "url": "git+https://github.com/BurtTheCoder/mcp-shodan.git"
35 |   },
36 |   "keywords": [
37 |     "mcp",
38 |     "shodan",
39 |     "mcp-shodan",
40 |     "cybersecurity",
41 |     "agents",
42 |     "network-reconnaissance",
43 |     "security-tools",
44 |     "shodan-api",
45 |     "shodan-integraton"
46 |   ],
47 |   "bugs": {
48 |     "url": "https://github.com/BurtTheCoder/mcp-shodan/issues"
49 |   },
50 |   "homepage": "https://github.com/BurtTheCoder/mcp-shodan#readme"
51 | }
52 | 
```

--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Publish to NPM
 2 | 
 3 | on:
 4 |   push:
 5 |     branches:
 6 |       - main
 7 |   release:
 8 |     types: [created]
 9 | 
10 | jobs:
11 |   build-and-publish:
12 |     runs-on: ubuntu-latest
13 |     permissions:
14 |       contents: write
15 |     steps:
16 |       - uses: actions/checkout@v4
17 |         with:
18 |           fetch-depth: 0
19 |           token: ${{ secrets.GITHUB_TOKEN }}
20 |       
21 |       - name: Setup Node.js
22 |         uses: actions/setup-node@v4
23 |         with:
24 |           node-version: '18'
25 |           registry-url: 'https://registry.npmjs.org'
26 |           
27 |       - name: Configure Git
28 |         run: |
29 |           git config --local user.email "[email protected]"
30 |           git config --local user.name "GitHub Action"
31 |         
32 |       - name: Install dependencies
33 |         run: npm ci
34 |         
35 |       - name: Bump version
36 |         if: github.event_name == 'push' && !contains(github.event.head_commit.message, '[skip ci]')
37 |         run: |
38 |           npm version patch -m "Bump version to %s [skip ci]"
39 |           git push
40 |           git push --tags
41 |         
42 |       - name: Build
43 |         run: npm run build
44 |         
45 |       - name: Publish to NPM
46 |         if: success()
47 |         run: npm publish --access public
48 |         env:
49 |           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
50 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  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 {
  5 |   CallToolRequestSchema,
  6 |   ListToolsRequestSchema,
  7 |   InitializeRequestSchema,
  8 | } from "@modelcontextprotocol/sdk/types.js";
  9 | import axios from "axios";
 10 | import dotenv from "dotenv";
 11 | import { z } from "zod";
 12 | import { zodToJsonSchema } from "zod-to-json-schema";
 13 | import fs from "fs";
 14 | import path from "path";
 15 | import os from "os";
 16 | 
 17 | // Shodan API Response Types
 18 | interface DnsResponse {
 19 |   [hostname: string]: string;  // Maps hostname to IP address
 20 | }
 21 | 
 22 | interface ReverseDnsResponse {
 23 |   [ip: string]: string[];  // Maps IP address to array of hostnames
 24 | }
 25 | 
 26 | interface SearchLocation {
 27 |   city: string | null;
 28 |   region_code: string | null;
 29 |   area_code: number | null;
 30 |   longitude: number;
 31 |   latitude: number;
 32 |   country_code: string;
 33 |   country_name: string;
 34 | }
 35 | 
 36 | interface SearchMatch {
 37 |   product?: string;
 38 |   hash: number;
 39 |   ip: number;
 40 |   ip_str: string;
 41 |   org: string;
 42 |   isp: string;
 43 |   transport: string;
 44 |   cpe?: string[];
 45 |   version?: string;
 46 |   hostnames: string[];
 47 |   domains: string[];
 48 |   location: SearchLocation;
 49 |   timestamp: string;
 50 |   port: number;
 51 |   data: string;
 52 |   asn: string;
 53 |   http?: {
 54 |     server?: string;
 55 |     title?: string;
 56 |     robots?: string | null;
 57 |     sitemap?: string | null;
 58 |   };
 59 | }
 60 | 
 61 | interface SearchResponse {
 62 |   matches: SearchMatch[];
 63 |   facets: {
 64 |     country?: Array<{
 65 |       count: number;
 66 |       value: string;
 67 |     }>;
 68 |   };
 69 |   total: number;
 70 | }
 71 | 
 72 | interface ShodanService {
 73 |   port: number;
 74 |   transport: string;
 75 |   data?: string;
 76 |   http?: {
 77 |     server?: string;
 78 |     title?: string;
 79 |   };
 80 |   cloud?: {
 81 |     provider: string;
 82 |     service: string;
 83 |     region: string;
 84 |   };
 85 | }
 86 | 
 87 | interface CveResponse {
 88 |   cve_id: string;
 89 |   summary: string;
 90 |   cvss: number;
 91 |   cvss_version: number;
 92 |   cvss_v2: number;
 93 |   cvss_v3: number;
 94 |   epss: number;
 95 |   ranking_epss: number;
 96 |   kev: boolean;
 97 |   propose_action: string;
 98 |   ransomware_campaign: string;
 99 |   references: string[];
100 |   published_time: string;
101 |   cpes: string[];
102 | }
103 | 
104 | interface ShodanHostResponse {
105 |   ip_str: string;
106 |   org: string;
107 |   isp: string;
108 |   asn: string;
109 |   last_update: string;
110 |   country_name: string;
111 |   city: string;
112 |   latitude: number;
113 |   longitude: number;
114 |   region_code: string;
115 |   ports: number[];
116 |   data: ShodanService[];
117 |   hostnames: string[];
118 |   domains: string[];
119 |   tags: string[];
120 | }
121 | 
122 | dotenv.config();
123 | 
124 | const logFilePath = path.join(os.tmpdir(), "mcp-shodan-server.log");
125 | const SHODAN_API_KEY = process.env.SHODAN_API_KEY;
126 | if (!SHODAN_API_KEY) {
127 |   throw new Error("SHODAN_API_KEY environment variable is required.");
128 | }
129 | 
130 | const API_BASE_URL = "https://api.shodan.io";
131 | const CVEDB_API_URL = "https://cvedb.shodan.io";
132 | 
133 | // Logging Helper Function
134 | function logToFile(message: string) {
135 |   try {
136 |     const timestamp = new Date().toISOString();
137 |     const formattedMessage = `[${timestamp}] ${message}\n`;
138 |     fs.appendFileSync(logFilePath, formattedMessage, "utf8");
139 |     console.error(formattedMessage.trim()); // Use stderr for logging to avoid interfering with stdout
140 |   } catch (error) {
141 |     console.error(`Failed to write to log file: ${error}`);
142 |   }
143 | }
144 | 
145 | // Tool Schemas
146 | const IpLookupArgsSchema = z.object({
147 |   ip: z.string().describe("The IP address to query."),
148 | });
149 | 
150 | const ShodanSearchArgsSchema = z.object({
151 |   query: z.string().describe("Search query for Shodan."),
152 |   max_results: z
153 |     .number()
154 |     .optional()
155 |     .default(10)
156 |     .describe("Maximum results to return."),
157 | });
158 | 
159 | const CVELookupArgsSchema = z.object({
160 |   cve: z.string()
161 |     .regex(/^CVE-\d{4}-\d{4,}$/i, "Must be a valid CVE ID format (e.g., CVE-2021-44228)")
162 |     .describe("The CVE identifier to query (format: CVE-YYYY-NNNNN)."),
163 | });
164 | 
165 | const DnsLookupArgsSchema = z.object({
166 |   hostnames: z.array(z.string()).describe("List of hostnames to resolve."),
167 | });
168 | 
169 | const ReverseDnsLookupArgsSchema = z.object({
170 |   ips: z.array(z.string()).describe("List of IP addresses to perform reverse DNS lookup on."),
171 | });
172 | 
173 | const CpeLookupArgsSchema = z.object({
174 |   product: z.string().describe("The name of the product to search for CPEs."),
175 |   count: z.boolean().optional().default(false).describe("If true, returns only the count of matching CPEs."),
176 |   skip: z.number().optional().default(0).describe("Number of CPEs to skip (for pagination)."),
177 |   limit: z.number().optional().default(1000).describe("Maximum number of CPEs to return (max 1000)."),
178 | });
179 | 
180 | const CVEsByProductArgsSchema = z.object({
181 |   cpe23: z.string().optional().describe("The CPE version 2.3 identifier (format: cpe:2.3:part:vendor:product:version)."),
182 |   product: z.string().optional().describe("The name of the product to search for CVEs."),
183 |   count: z.boolean().optional().default(false).describe("If true, returns only the count of matching CVEs."),
184 |   is_kev: z.boolean().optional().default(false).describe("If true, returns only CVEs with the KEV flag set."),
185 |   sort_by_epss: z.boolean().optional().default(false).describe("If true, sorts CVEs by EPSS score in descending order."),
186 |   skip: z.number().optional().default(0).describe("Number of CVEs to skip (for pagination)."),
187 |   limit: z.number().optional().default(1000).describe("Maximum number of CVEs to return (max 1000)."),
188 |   start_date: z.string().optional().describe("Start date for filtering CVEs (format: YYYY-MM-DDTHH:MM:SS)."),
189 |   end_date: z.string().optional().describe("End date for filtering CVEs (format: YYYY-MM-DDTHH:MM:SS).")
190 | }).refine(
191 |   data => !(data.cpe23 && data.product),
192 |   { message: "Cannot specify both cpe23 and product. Use only one." }
193 | ).refine(
194 |   data => data.cpe23 || data.product,
195 |   { message: "Must specify either cpe23 or product." }
196 | );
197 | 
198 | // Helper Function to Query Shodan API
199 | async function queryShodan(endpoint: string, params: Record<string, any>) {
200 |   try {
201 |     const response = await axios.get(`${API_BASE_URL}${endpoint}`, {
202 |       params: { ...params, key: SHODAN_API_KEY },
203 |       timeout: 10000,
204 |     });
205 |     return response.data;
206 |   } catch (error: any) {
207 |     const errorMessage = error.response?.data?.error || error.message;
208 |     logToFile(`Shodan API error: ${errorMessage}`);
209 |     throw new Error(`Shodan API error: ${errorMessage}`);
210 |   }
211 | }
212 | 
213 | // Helper Function for CVE lookups using CVEDB
214 | async function queryCVEDB(cveId: string) {
215 |   try {
216 |     logToFile(`Querying CVEDB for: ${cveId}`);
217 |     const response = await axios.get(`${CVEDB_API_URL}/cve/${cveId}`);
218 |     return response.data;
219 |   } catch (error: any) {
220 |     if (error.response?.status === 422) {
221 |       throw new Error(`Invalid CVE ID format: ${cveId}`);
222 |     }
223 |     if (error.response?.status === 404) {
224 |       throw new Error(`CVE not found: ${cveId}`);
225 |     }
226 |     throw new Error(`CVEDB API error: ${error.message}`);
227 |   }
228 | }
229 | 
230 | // Helper Function for CPE lookups using CVEDB
231 | async function queryCPEDB(params: {
232 |   product: string;
233 |   count?: boolean;
234 |   skip?: number;
235 |   limit?: number;
236 | }) {
237 |   try {
238 |     logToFile(`Querying CVEDB for CPEs with params: ${JSON.stringify(params)}`);
239 |     const response = await axios.get(`${CVEDB_API_URL}/cpes`, { params });
240 |     return response.data;
241 |   } catch (error: any) {
242 |     if (error.response?.status === 422) {
243 |       throw new Error(`Invalid parameters: ${error.response.data?.detail || error.message}`);
244 |     }
245 |     throw new Error(`CVEDB API error: ${error.message}`);
246 |   }
247 | }
248 | 
249 | // Helper Function for CVEs by product/CPE lookups using CVEDB
250 | async function queryCVEsByProduct(params: {
251 |   cpe23?: string;
252 |   product?: string;
253 |   count?: boolean;
254 |   is_kev?: boolean;
255 |   sort_by_epss?: boolean;
256 |   skip?: number;
257 |   limit?: number;
258 |   start_date?: string;
259 |   end_date?: string;
260 | }) {
261 |   try {
262 |     logToFile(`Querying CVEDB for CVEs with params: ${JSON.stringify(params)}`);
263 |     const response = await axios.get(`${CVEDB_API_URL}/cves`, { params });
264 |     return response.data;
265 |   } catch (error: any) {
266 |     if (error.response?.status === 422) {
267 |       throw new Error(`Invalid parameters: ${error.response.data?.detail || error.message}`);
268 |     }
269 |     throw new Error(`CVEDB API error: ${error.message}`);
270 |   }
271 | }
272 | 
273 | // Server Setup
274 | const server = new Server(
275 |   {
276 |     name: "shodan-mcp",
277 |     version: "1.0.0",
278 |   },
279 |   {
280 |     capabilities: {
281 |       tools: {
282 |         listChanged: true,
283 |       },
284 |     },
285 |   }
286 | );
287 | 
288 | // Handle Initialization
289 | server.setRequestHandler(InitializeRequestSchema, async (request) => {
290 |   logToFile("Received initialize request.");
291 |   return {
292 |     protocolVersion: "2024-11-05",
293 |     capabilities: {
294 |       tools: {
295 |         listChanged: true,
296 |       },
297 |     },
298 |     serverInfo: {
299 |       name: "shodan-mcp",
300 |       version: "1.0.0",
301 |     },
302 |     instructions: `This MCP server provides comprehensive access to Shodan's network intelligence and security services:
303 | 
304 | - Network Reconnaissance: Query detailed information about IP addresses, including open ports, services, and vulnerabilities
305 | - DNS Operations: Forward and reverse DNS lookups for domains and IP addresses
306 | - Vulnerability Intelligence: Access to Shodan's CVEDB for detailed vulnerability information, CPE lookups, and product-specific CVE tracking
307 | - Device Discovery: Search Shodan's database of internet-connected devices with advanced filtering
308 | 
309 | Each tool provides structured, formatted output for easy analysis and integration.`,
310 |   };
311 | });
312 | 
313 | // Register Tools
314 | server.setRequestHandler(ListToolsRequestSchema, async () => {
315 |   const tools = [
316 |     {
317 |       name: "ip_lookup",
318 |       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.",
319 |       inputSchema: zodToJsonSchema(IpLookupArgsSchema),
320 |     },
321 |     {
322 |       name: "shodan_search",
323 |       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.",
324 |       inputSchema: zodToJsonSchema(ShodanSearchArgsSchema),
325 |     },
326 |     {
327 |       name: "cve_lookup",
328 |       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).",
329 |       inputSchema: zodToJsonSchema(CVELookupArgsSchema),
330 |     },
331 |     {
332 |       name: "dns_lookup",
333 |       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.",
334 |       inputSchema: zodToJsonSchema(DnsLookupArgsSchema),
335 |     },
336 |     {
337 |       name: "cpe_lookup",
338 |       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.",
339 |       inputSchema: zodToJsonSchema(CpeLookupArgsSchema),
340 |     },
341 |     {
342 |       name: "cves_by_product",
343 |       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.",
344 |       inputSchema: zodToJsonSchema(CVEsByProductArgsSchema),
345 |     },
346 |     {
347 |       name: "reverse_dns_lookup",
348 |       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.",
349 |       inputSchema: zodToJsonSchema(ReverseDnsLookupArgsSchema),
350 |     },
351 |   ];
352 | 
353 |   logToFile("Registered tools.");
354 |   return { tools };
355 | });
356 | 
357 | // Handle Tool Calls
358 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
359 |   logToFile(`Tool called: ${request.params.name}`);
360 | 
361 |   try {
362 |     const { name, arguments: args } = request.params;
363 | 
364 |     switch (name) {
365 |       case "ip_lookup": {
366 |         const parsedIpArgs = IpLookupArgsSchema.safeParse(args);
367 |         if (!parsedIpArgs.success) {
368 |           throw new Error("Invalid ip_lookup arguments");
369 |         }
370 |         const result = await queryShodan(`/shodan/host/${parsedIpArgs.data.ip}`, {});
371 |         
372 |         // Format the response in a user-friendly way
373 |         const formattedResult = {
374 |           "IP Information": {
375 |             "IP Address": result.ip_str,
376 |             "Organization": result.org,
377 |             "ISP": result.isp,
378 |             "ASN": result.asn,
379 |             "Last Update": result.last_update
380 |           },
381 |           "Location": {
382 |             "Country": result.country_name,
383 |             "City": result.city,
384 |             "Coordinates": `${result.latitude}, ${result.longitude}`,
385 |             "Region": result.region_code
386 |           },
387 |           "Services": result.ports.map((port: number) => {
388 |             const service = result.data.find((d: ShodanService) => d.port === port);
389 |             return {
390 |               "Port": port,
391 |               "Protocol": service?.transport || "unknown",
392 |               "Service": service?.data?.trim() || "No banner",
393 |               ...(service?.http ? {
394 |                 "HTTP": {
395 |                   "Server": service.http.server,
396 |                   "Title": service.http.title,
397 |                 }
398 |               } : {})
399 |             };
400 |           }),
401 |           "Cloud Provider": result.data[0]?.cloud ? {
402 |             "Provider": result.data[0].cloud.provider,
403 |             "Service": result.data[0].cloud.service,
404 |             "Region": result.data[0].cloud.region
405 |           } : "Not detected",
406 |           "Hostnames": result.hostnames || [],
407 |           "Domains": result.domains || [],
408 |           "Tags": result.tags || []
409 |         };
410 | 
411 |         return {
412 |           content: [
413 |             {
414 |               type: "text",
415 |               text: JSON.stringify(formattedResult, null, 2),
416 |             },
417 |           ],
418 |         };
419 |       }
420 | 
421 |       case "shodan_search": {
422 |         const parsedSearchArgs = ShodanSearchArgsSchema.safeParse(args);
423 |         if (!parsedSearchArgs.success) {
424 |           throw new Error("Invalid search arguments");
425 |         }
426 |         const result: SearchResponse = await queryShodan("/shodan/host/search", {
427 |           query: parsedSearchArgs.data.query,
428 |           limit: parsedSearchArgs.data.max_results,
429 |         });
430 | 
431 |         // Format the response in a user-friendly way
432 |         const formattedResult = {
433 |           "Search Summary": {
434 |             "Query": parsedSearchArgs.data.query,
435 |             "Total Results": result.total,
436 |             "Results Returned": result.matches.length
437 |           },
438 |           "Country Distribution": result.facets?.country?.map(country => ({
439 |             "Country": country.value,
440 |             "Count": country.count,
441 |             "Percentage": `${((country.count / result.total) * 100).toFixed(2)}%`
442 |           })) || [],
443 |           "Matches": result.matches.map(match => ({
444 |             "Basic Information": {
445 |               "IP Address": match.ip_str,
446 |               "Organization": match.org,
447 |               "ISP": match.isp,
448 |               "ASN": match.asn,
449 |               "Last Update": match.timestamp
450 |             },
451 |             "Location": {
452 |               "Country": match.location.country_name,
453 |               "City": match.location.city || "Unknown",
454 |               "Region": match.location.region_code || "Unknown",
455 |               "Coordinates": `${match.location.latitude}, ${match.location.longitude}`
456 |             },
457 |             "Service Details": {
458 |               "Port": match.port,
459 |               "Transport": match.transport,
460 |               "Product": match.product || "Unknown",
461 |               "Version": match.version || "Unknown",
462 |               "CPE": match.cpe || []
463 |             },
464 |             "Web Information": match.http ? {
465 |               "Server": match.http.server,
466 |               "Title": match.http.title,
467 |               "Robots.txt": match.http.robots ? "Present" : "Not found",
468 |               "Sitemap": match.http.sitemap ? "Present" : "Not found"
469 |             } : "No HTTP information",
470 |             "Hostnames": match.hostnames,
471 |             "Domains": match.domains
472 |           }))
473 |         };
474 | 
475 |         return {
476 |           content: [
477 |             {
478 |               type: "text",
479 |               text: JSON.stringify(formattedResult, null, 2),
480 |             },
481 |           ],
482 |         };
483 |       }
484 | 
485 |       case "cve_lookup": {
486 |         const parsedCveArgs = CVELookupArgsSchema.safeParse(args);
487 |         if (!parsedCveArgs.success) {
488 |           throw new Error("Invalid CVE format. Please use format: CVE-YYYY-NNNNN (e.g., CVE-2021-44228)");
489 |         }
490 | 
491 |         const cveId = parsedCveArgs.data.cve.toUpperCase();
492 |         logToFile(`Looking up CVE: ${cveId}`);
493 |         
494 |         try {
495 |           const result = await queryCVEDB(cveId);
496 | 
497 |           // Helper function to format CVSS score severity
498 |           const getCvssSeverity = (score: number) => {
499 |             if (score >= 9.0) return "Critical";
500 |             if (score >= 7.0) return "High";
501 |             if (score >= 4.0) return "Medium";
502 |             if (score >= 0.1) return "Low";
503 |             return "None";
504 |           };
505 | 
506 |           // Format the response in a user-friendly way
507 |           const formattedResult = {
508 |             "Basic Information": {
509 |               "CVE ID": result.cve_id,
510 |               "Published": new Date(result.published_time).toLocaleString(),
511 |               "Summary": result.summary
512 |             },
513 |             "Severity Scores": {
514 |               "CVSS v3": result.cvss_v3 ? {
515 |                 "Score": result.cvss_v3,
516 |                 "Severity": getCvssSeverity(result.cvss_v3)
517 |               } : "Not available",
518 |               "CVSS v2": result.cvss_v2 ? {
519 |                 "Score": result.cvss_v2,
520 |                 "Severity": getCvssSeverity(result.cvss_v2)
521 |               } : "Not available",
522 |               "EPSS": result.epss ? {
523 |                 "Score": `${(result.epss * 100).toFixed(2)}%`,
524 |                 "Ranking": `Top ${(result.ranking_epss * 100).toFixed(2)}%`
525 |               } : "Not available"
526 |             },
527 |             "Impact Assessment": {
528 |               "Known Exploited Vulnerability": result.kev ? "Yes" : "No",
529 |               "Proposed Action": result.propose_action || "No specific action proposed",
530 |               "Ransomware Campaign": result.ransomware_campaign || "No known ransomware campaigns"
531 |             },
532 |             "Affected Products": result.cpes?.length > 0 ? result.cpes : ["No specific products listed"],
533 |             "Additional Information": {
534 |               "References": result.references?.length > 0 ? result.references : ["No references provided"]
535 |             }
536 |           };
537 | 
538 |           return {
539 |             content: [
540 |               {
541 |                 type: "text",
542 |                 text: JSON.stringify(formattedResult, null, 2),
543 |               },
544 |             ],
545 |           };
546 |         } catch (error: any) {
547 |           return {
548 |             content: [
549 |               {
550 |                 type: "text",
551 |                 text: error.message,
552 |               },
553 |             ],
554 |             isError: true,
555 |           };
556 |         }
557 |       }
558 | 
559 |       case "dns_lookup": {
560 |         const parsedDnsArgs = DnsLookupArgsSchema.safeParse(args);
561 |         if (!parsedDnsArgs.success) {
562 |           throw new Error("Invalid dns_lookup arguments");
563 |         }
564 |         
565 |         // Join hostnames with commas for the API request
566 |         const hostnamesString = parsedDnsArgs.data.hostnames.join(",");
567 |         
568 |         const result: DnsResponse = await queryShodan("/dns/resolve", {
569 |           hostnames: hostnamesString
570 |         });
571 | 
572 |         // Format the response in a user-friendly way
573 |         const formattedResult = {
574 |           "DNS Resolutions": Object.entries(result).map(([hostname, ip]) => ({
575 |             "Hostname": hostname,
576 |             "IP Address": ip
577 |           })),
578 |           "Summary": {
579 |             "Total Lookups": Object.keys(result).length,
580 |             "Queried Hostnames": parsedDnsArgs.data.hostnames
581 |           }
582 |         };
583 |         
584 |         return {
585 |           content: [
586 |             {
587 |               type: "text",
588 |               text: JSON.stringify(formattedResult, null, 2)
589 |             },
590 |           ],
591 |         };
592 |       }
593 | 
594 |       case "cpe_lookup": {
595 |         const parsedCpeArgs = CpeLookupArgsSchema.safeParse(args);
596 |         if (!parsedCpeArgs.success) {
597 |           throw new Error("Invalid cpe_lookup arguments");
598 |         }
599 | 
600 |         try {
601 |           const result = await queryCPEDB({
602 |             product: parsedCpeArgs.data.product,
603 |             count: parsedCpeArgs.data.count,
604 |             skip: parsedCpeArgs.data.skip,
605 |             limit: parsedCpeArgs.data.limit
606 |           });
607 | 
608 |           // Format the response based on whether it's a count request or full CPE list
609 |           const formattedResult = parsedCpeArgs.data.count
610 |             ? { total_cpes: result.total }
611 |             : {
612 |                 cpes: result.cpes,
613 |                 skip: parsedCpeArgs.data.skip,
614 |                 limit: parsedCpeArgs.data.limit,
615 |                 total_returned: result.cpes.length
616 |               };
617 | 
618 |           return {
619 |             content: [
620 |               {
621 |                 type: "text",
622 |                 text: JSON.stringify(formattedResult, null, 2),
623 |               },
624 |             ],
625 |           };
626 |         } catch (error: any) {
627 |           return {
628 |             content: [
629 |               {
630 |                 type: "text",
631 |                 text: error.message,
632 |               },
633 |             ],
634 |             isError: true,
635 |           };
636 |         }
637 |       }
638 | 
639 |       case "cves_by_product": {
640 |         const parsedArgs = CVEsByProductArgsSchema.safeParse(args);
641 |         if (!parsedArgs.success) {
642 |           throw new Error("Invalid arguments. Must provide either cpe23 or product name, but not both.");
643 |         }
644 | 
645 |         try {
646 |           const result = await queryCVEsByProduct({
647 |             cpe23: parsedArgs.data.cpe23,
648 |             product: parsedArgs.data.product,
649 |             count: parsedArgs.data.count,
650 |             is_kev: parsedArgs.data.is_kev,
651 |             sort_by_epss: parsedArgs.data.sort_by_epss,
652 |             skip: parsedArgs.data.skip,
653 |             limit: parsedArgs.data.limit,
654 |             start_date: parsedArgs.data.start_date,
655 |             end_date: parsedArgs.data.end_date
656 |           });
657 | 
658 |           // Helper function to format CVSS score severity
659 |           const getCvssSeverity = (score: number) => {
660 |             if (score >= 9.0) return "Critical";
661 |             if (score >= 7.0) return "High";
662 |             if (score >= 4.0) return "Medium";
663 |             if (score >= 0.1) return "Low";
664 |             return "None";
665 |           };
666 | 
667 |           // Format the response based on whether it's a count request or full CVE list
668 |           const formattedResult = parsedArgs.data.count
669 |             ? {
670 |                 "Query Information": {
671 |                   "Product": parsedArgs.data.product || "N/A",
672 |                   "CPE 2.3": parsedArgs.data.cpe23 || "N/A",
673 |                   "KEV Only": parsedArgs.data.is_kev ? "Yes" : "No",
674 |                   "Sort by EPSS": parsedArgs.data.sort_by_epss ? "Yes" : "No"
675 |                 },
676 |                 "Results": {
677 |                   "Total CVEs Found": result.total
678 |                 }
679 |               }
680 |             : {
681 |                 "Query Information": {
682 |                   "Product": parsedArgs.data.product || "N/A",
683 |                   "CPE 2.3": parsedArgs.data.cpe23 || "N/A",
684 |                   "KEV Only": parsedArgs.data.is_kev ? "Yes" : "No",
685 |                   "Sort by EPSS": parsedArgs.data.sort_by_epss ? "Yes" : "No",
686 |                   "Date Range": parsedArgs.data.start_date ? 
687 |                     `${parsedArgs.data.start_date} to ${parsedArgs.data.end_date || 'now'}` : 
688 |                     "All dates"
689 |                 },
690 |                 "Results Summary": {
691 |                   "Total CVEs Found": result.total,
692 |                   "CVEs Returned": result.cves.length,
693 |                   "Page": `${Math.floor(parsedArgs.data.skip! / parsedArgs.data.limit!) + 1}`,
694 |                   "CVEs per Page": parsedArgs.data.limit
695 |                 },
696 |                 "Vulnerabilities": result.cves.map((cve: CveResponse) => ({
697 |                   "Basic Information": {
698 |                     "CVE ID": cve.cve_id,
699 |                     "Published": new Date(cve.published_time).toLocaleString(),
700 |                     "Summary": cve.summary
701 |                   },
702 |                   "Severity Scores": {
703 |                     "CVSS v3": cve.cvss_v3 ? {
704 |                       "Score": cve.cvss_v3,
705 |                       "Severity": getCvssSeverity(cve.cvss_v3)
706 |                     } : "Not available",
707 |                     "CVSS v2": cve.cvss_v2 ? {
708 |                       "Score": cve.cvss_v2,
709 |                       "Severity": getCvssSeverity(cve.cvss_v2)
710 |                     } : "Not available",
711 |                     "EPSS": cve.epss ? {
712 |                       "Score": `${(cve.epss * 100).toFixed(2)}%`,
713 |                       "Ranking": `Top ${(cve.ranking_epss * 100).toFixed(2)}%`
714 |                     } : "Not available"
715 |                   },
716 |                   "Impact Assessment": {
717 |                     "Known Exploited Vulnerability": cve.kev ? "Yes" : "No",
718 |                     "Proposed Action": cve.propose_action || "No specific action proposed",
719 |                     "Ransomware Campaign": cve.ransomware_campaign || "No known ransomware campaigns"
720 |                   },
721 |                   "References": cve.references?.length > 0 ? cve.references : ["No references provided"]
722 |                 }))
723 |               };
724 | 
725 |           return {
726 |             content: [
727 |               {
728 |                 type: "text",
729 |                 text: JSON.stringify(formattedResult, null, 2),
730 |               },
731 |             ],
732 |           };
733 |         } catch (error: any) {
734 |           return {
735 |             content: [
736 |               {
737 |                 type: "text",
738 |                 text: error.message,
739 |               },
740 |             ],
741 |             isError: true,
742 |           };
743 |         }
744 |       }
745 | 
746 |       case "reverse_dns_lookup": {
747 |         const parsedArgs = ReverseDnsLookupArgsSchema.safeParse(args);
748 |         if (!parsedArgs.success) {
749 |           throw new Error("Invalid reverse_dns_lookup arguments");
750 |         }
751 |         
752 |         // Join IPs with commas for the API request
753 |         const ipsString = parsedArgs.data.ips.join(",");
754 |         
755 |         const result: ReverseDnsResponse = await queryShodan("/dns/reverse", {
756 |           ips: ipsString
757 |         });
758 | 
759 |         // Format the response in a user-friendly way
760 |         const formattedResult = {
761 |           "Reverse DNS Resolutions": Object.entries(result).map(([ip, hostnames]) => ({
762 |             "IP Address": ip,
763 |             "Hostnames": hostnames.length > 0 ? hostnames : ["No hostnames found"]
764 |           })),
765 |           "Summary": {
766 |             "Total IPs Queried": parsedArgs.data.ips.length,
767 |             "IPs with Results": Object.keys(result).length,
768 |             "Queried IP Addresses": parsedArgs.data.ips
769 |           }
770 |         };
771 | 
772 |         return {
773 |           content: [
774 |             {
775 |               type: "text",
776 |               text: JSON.stringify(formattedResult, null, 2)
777 |             },
778 |           ],
779 |         };
780 |       }
781 | 
782 |       default:
783 |         throw new Error(`Unknown tool: ${name}`);
784 |     }
785 |   } catch (error: any) {
786 |     const errorMessage = error instanceof Error ? error.message : String(error);
787 |     logToFile(`Error handling tool call: ${errorMessage}`);
788 |     return {
789 |       content: [
790 |         {
791 |           type: "text",
792 |           text: `Error: ${errorMessage}`,
793 |         },
794 |       ],
795 |       isError: true,
796 |     };
797 |   }
798 | });
799 | 
800 | // Start the Server
801 | async function runServer() {
802 |   logToFile("Starting Shodan MCP Server...");
803 | 
804 |   try {
805 |     const transport = new StdioServerTransport();
806 |     await server.connect(transport);
807 |     logToFile("Shodan MCP Server is running.");
808 |   } catch (error: any) {
809 |     logToFile(`Error connecting server: ${error.message}`);
810 |     process.exit(1);
811 |   }
812 | }
813 | 
814 | // Handle process events
815 | process.on('uncaughtException', (error) => {
816 |   logToFile(`Uncaught exception: ${error.message}`);
817 |   process.exit(1);
818 | });
819 | 
820 | process.on('unhandledRejection', (reason) => {
821 |   logToFile(`Unhandled rejection: ${reason}`);
822 |   process.exit(1);
823 | });
824 | 
825 | runServer().catch((error: any) => {
826 |   logToFile(`Fatal error: ${error.message}`);
827 |   process.exit(1);
828 | });
829 | 
```