# 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:
--------------------------------------------------------------------------------
```
node_modules/
build/
*.log
.env
.env.local
.env.*.local
```
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
```
# Source files (since we publish the build)
src/
# Development files
.github/
tsconfig.json
.env*
*.log
# Don't ignore the build directory
!build/
```
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
# Required: Your Shodan API key from https://account.shodan.io/
SHODAN_API_KEY=your_api_key_here
# Optional: Debug logging level for MCP protocol
# DEBUG=mcp:*
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Shodan MCP Server
[](https://smithery.ai/server/@burtthecoder/mcp-shodan)
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.
<a href="https://glama.ai/mcp/servers/79uakvikcj"><img width="380" height="200" src="https://glama.ai/mcp/servers/79uakvikcj/badge" /></a>
## Quick Start (Recommended)
### Installing via Smithery
To install Shodan Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@burtthecoder/mcp-shodan):
```bash
npx -y @smithery/cli install @burtthecoder/mcp-shodan --client claude
```
### Installing Manually
1. Install the server globally via npm:
```bash
npm install -g @burtthecoder/mcp-shodan
```
2. Add to your Claude Desktop configuration file:
```json
{
"mcpServers": {
"shodan": {
"command": "mcp-shodan",
"env": {
"SHODAN_API_KEY": "your-shodan-api-key"
}
}
}
}
```
Configuration file location:
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
3. Restart Claude Desktop
## Alternative Setup (From Source)
If you prefer to run from source or need to modify the code:
1. Clone and build:
```bash
git clone https://github.com/BurtTheCoder/mcp-shodan.git
cd mcp-shodan
npm install
npm run build
```
2. Add to your Claude Desktop configuration:
```json
{
"mcpServers": {
"shodan": {
"command": "node",
"args": ["/absolute/path/to/mcp-shodan/build/index.js"],
"env": {
"SHODAN_API_KEY": "your-shodan-api-key"
}
}
}
}
```
## Features
- **Network Reconnaissance**: Query detailed information about IP addresses, including open ports, services, and vulnerabilities
- **DNS Operations**: Forward and reverse DNS lookups for domains and IP addresses
- **Vulnerability Intelligence**: Access to Shodan's CVEDB for detailed vulnerability information, CPE lookups, and product-specific CVE tracking
- **Device Discovery**: Search Shodan's database of internet-connected devices with advanced filtering
## Tools
### 1. IP Lookup Tool
- Name: `ip_lookup`
- Description: Retrieve comprehensive information about an IP address, including geolocation, open ports, running services, SSL certificates, hostnames, and cloud provider details if available
- Parameters:
* `ip` (required): IP address to lookup
- Returns:
* IP Information (address, organization, ISP, ASN)
* Location (country, city, coordinates)
* Services (ports, protocols, banners)
* Cloud Provider details (if available)
* Associated hostnames and domains
* Tags
### 2. Shodan Search Tool
- Name: `shodan_search`
- Description: Search Shodan's database of internet-connected devices
- Parameters:
* `query` (required): Shodan search query
* `max_results` (optional, default: 10): Number of results to return
- Returns:
* Search summary with total results
* Country-based distribution statistics
* Detailed device information including:
- Basic information (IP, organization, ISP)
- Location data
- Service details
- Web server information
- Associated hostnames and domains
### 3. CVE Lookup Tool
- Name: `cve_lookup`
- Description: Query detailed vulnerability information from Shodan's CVEDB
- Parameters:
* `cve` (required): CVE identifier in format CVE-YYYY-NNNNN (e.g., CVE-2021-44228)
- Returns:
* Basic Information (ID, published date, summary)
* Severity Scores:
- CVSS v2 and v3 with severity levels
- EPSS probability and ranking
* Impact Assessment:
- KEV status
- Proposed mitigations
- Ransomware associations
* Affected products (CPEs)
* References
### 4. DNS Lookup Tool
- Name: `dns_lookup`
- Description: Resolve domain names to IP addresses using Shodan's DNS service
- Parameters:
* `hostnames` (required): Array of hostnames to resolve
- Returns:
* DNS resolutions mapping hostnames to IPs
* Summary of total lookups and queried hostnames
### 5. Reverse DNS Lookup Tool
- Name: `reverse_dns_lookup`
- Description: Perform reverse DNS lookups to find hostnames associated with IP addresses
- Parameters:
* `ips` (required): Array of IP addresses to lookup
- Returns:
* Reverse DNS resolutions mapping IPs to hostnames
* Summary of total lookups and results
### 6. CPE Lookup Tool
- Name: `cpe_lookup`
- Description: Search for Common Platform Enumeration (CPE) entries by product name
- Parameters:
* `product` (required): Name of the product to search for
* `count` (optional, default: false): If true, returns only the count of matching CPEs
* `skip` (optional, default: 0): Number of CPEs to skip (for pagination)
* `limit` (optional, default: 1000): Maximum number of CPEs to return
- Returns:
* When count is true: Total number of matching CPEs
* When count is false: List of CPEs with pagination details
### 7. CVEs by Product Tool
- Name: `cves_by_product`
- Description: Search for vulnerabilities affecting specific products or CPEs
- Parameters:
* `cpe23` (optional): CPE 2.3 identifier (format: cpe:2.3:part:vendor:product:version)
* `product` (optional): Name of the product to search for CVEs
* `count` (optional, default: false): If true, returns only the count of matching CVEs
* `is_kev` (optional, default: false): If true, returns only CVEs with KEV flag set
* `sort_by_epss` (optional, default: false): If true, sorts CVEs by EPSS score
* `skip` (optional, default: 0): Number of CVEs to skip (for pagination)
* `limit` (optional, default: 1000): Maximum number of CVEs to return
* `start_date` (optional): Start date for filtering CVEs (format: YYYY-MM-DDTHH:MM:SS)
* `end_date` (optional): End date for filtering CVEs (format: YYYY-MM-DDTHH:MM:SS)
- Notes:
* Must provide either cpe23 or product, but not both
* Date filtering uses published time of CVEs
- Returns:
* Query information
* Results summary with pagination details
* Detailed vulnerability information including:
- Basic information
- Severity scores
- Impact assessments
- References
## Requirements
- Node.js (v18 or later)
- A valid [Shodan API Key](https://account.shodan.io/)
## Troubleshooting
### API Key Issues
If you see API key related errors (e.g., "Request failed with status code 401"):
1. Verify your API key:
- Must be a valid Shodan API key from your [account settings](https://account.shodan.io/)
- Ensure the key has sufficient credits/permissions for the operation
- Check for extra spaces or quotes around the key in the configuration
- Verify the key is correctly set in the SHODAN_API_KEY environment variable
2. Common Error Codes:
- 401 Unauthorized: Invalid API key or missing authentication
- 402 Payment Required: Out of query credits
- 429 Too Many Requests: Rate limit exceeded
3. Configuration Steps:
a. Get your API key from [Shodan Account](https://account.shodan.io/)
b. Add it to your configuration file:
```json
{
"mcpServers": {
"shodan": {
"command": "mcp-shodan",
"env": {
"SHODAN_API_KEY": "your-actual-api-key-here"
}
}
}
}
```
c. Save the config file
d. Restart Claude Desktop
4. Testing Your Key:
- Try a simple query first (e.g., dns_lookup for "google.com")
- Check your [Shodan account dashboard](https://account.shodan.io/) for credit status
- Verify the key works directly with curl:
```bash
curl "https://api.shodan.io/dns/resolve?hostnames=google.com&key=your-api-key"
```
### Module Loading Issues
If you see module loading errors:
1. For global installation: Use the simple configuration shown in Quick Start
2. For source installation: Ensure you're using Node.js v18 or later
## Development
To run in development mode with hot reloading:
```bash
npm run dev
```
## Error Handling
The server includes comprehensive error handling for:
- Invalid API keys
- Rate limiting
- Network errors
- Invalid input parameters
- Invalid CVE formats
- Invalid CPE lookup parameters
- Invalid date formats
- Mutually exclusive parameter validation
## Version History
- v1.0.12: Added reverse DNS lookup and improved output formatting
- v1.0.7: Added CVEs by Product search functionality and renamed vulnerabilities tool to cve_lookup
- v1.0.6: Added CVEDB integration for enhanced CVE lookups and CPE search functionality
- v1.0.0: Initial release with core functionality
## Contributing
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
startCommand:
type: stdio
configSchema:
# JSON Schema defining the configuration options for the MCP.
type: object
required:
- shodanApiKey
properties:
shodanApiKey:
type: string
description: The API key for the Shodan API.
commandFunction:
# A function that produces the CLI command to start the MCP on stdio.
|-
config => ({ command: 'node', args: ['build/index.js'], env: { SHODAN_API_KEY: config.shodanApiKey } })
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
# Start from the official Node.js image with the desired version
FROM node:18-alpine AS builder
# Set the working directory inside the Docker image
WORKDIR /app
# Copy the package.json and package-lock.json to the working directory
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the source code to the working directory
COPY ./src ./src
COPY tsconfig.json ./
# Run the build command to compile TypeScript to JavaScript
RUN npm run build
# Now create the production image, based on a smaller image
FROM node:18-alpine
# Set the working directory inside the Docker image
WORKDIR /app
# Copy the compiled JavaScript files from the builder stage
COPY --from=builder /app/build /app/build
COPY --from=builder /app/package.json /app/package.json
COPY --from=builder /app/package-lock.json /app/package-lock.json
# Install only production dependencies
RUN npm ci --omit=dev
# Set environment variable for Shodan API Key
ENV SHODAN_API_KEY=your-shodan-api-key
# Command to run the MCP server
ENTRYPOINT ["node", "build/index.js"]
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"type": "module",
"name": "@burtthecoder/mcp-shodan",
"version": "1.0.16",
"description": "A Model Context Protocol server for Shodan API queries.",
"main": "build/index.js",
"bin": {
"mcp-shodan": "build/index.js"
},
"scripts": {
"build": "tsc && chmod +x build/index.js",
"prepublishOnly": "npm run build"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^0.6.0",
"axios": "^1.7.8",
"dotenv": "^16.4.5",
"zod": "^3.22.2",
"zod-to-json-schema": "^3.23.5"
},
"devDependencies": {
"typescript": "^5.3.3",
"@types/node": "^20.11.24"
},
"files": [
"build",
"README.md",
"LICENSE"
],
"author": "BurtTheCoder",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/BurtTheCoder/mcp-shodan.git"
},
"keywords": [
"mcp",
"shodan",
"mcp-shodan",
"cybersecurity",
"agents",
"network-reconnaissance",
"security-tools",
"shodan-api",
"shodan-integraton"
],
"bugs": {
"url": "https://github.com/BurtTheCoder/mcp-shodan/issues"
},
"homepage": "https://github.com/BurtTheCoder/mcp-shodan#readme"
}
```
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
```yaml
name: Publish to NPM
on:
push:
branches:
- main
release:
types: [created]
jobs:
build-and-publish:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org'
- name: Configure Git
run: |
git config --local user.email "[email protected]"
git config --local user.name "GitHub Action"
- name: Install dependencies
run: npm ci
- name: Bump version
if: github.event_name == 'push' && !contains(github.event.head_commit.message, '[skip ci]')
run: |
npm version patch -m "Bump version to %s [skip ci]"
git push
git push --tags
- name: Build
run: npm run build
- name: Publish to NPM
if: success()
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
InitializeRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import axios from "axios";
import dotenv from "dotenv";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import fs from "fs";
import path from "path";
import os from "os";
// Shodan API Response Types
interface DnsResponse {
[hostname: string]: string; // Maps hostname to IP address
}
interface ReverseDnsResponse {
[ip: string]: string[]; // Maps IP address to array of hostnames
}
interface SearchLocation {
city: string | null;
region_code: string | null;
area_code: number | null;
longitude: number;
latitude: number;
country_code: string;
country_name: string;
}
interface SearchMatch {
product?: string;
hash: number;
ip: number;
ip_str: string;
org: string;
isp: string;
transport: string;
cpe?: string[];
version?: string;
hostnames: string[];
domains: string[];
location: SearchLocation;
timestamp: string;
port: number;
data: string;
asn: string;
http?: {
server?: string;
title?: string;
robots?: string | null;
sitemap?: string | null;
};
}
interface SearchResponse {
matches: SearchMatch[];
facets: {
country?: Array<{
count: number;
value: string;
}>;
};
total: number;
}
interface ShodanService {
port: number;
transport: string;
data?: string;
http?: {
server?: string;
title?: string;
};
cloud?: {
provider: string;
service: string;
region: string;
};
}
interface CveResponse {
cve_id: string;
summary: string;
cvss: number;
cvss_version: number;
cvss_v2: number;
cvss_v3: number;
epss: number;
ranking_epss: number;
kev: boolean;
propose_action: string;
ransomware_campaign: string;
references: string[];
published_time: string;
cpes: string[];
}
interface ShodanHostResponse {
ip_str: string;
org: string;
isp: string;
asn: string;
last_update: string;
country_name: string;
city: string;
latitude: number;
longitude: number;
region_code: string;
ports: number[];
data: ShodanService[];
hostnames: string[];
domains: string[];
tags: string[];
}
dotenv.config();
const logFilePath = path.join(os.tmpdir(), "mcp-shodan-server.log");
const SHODAN_API_KEY = process.env.SHODAN_API_KEY;
if (!SHODAN_API_KEY) {
throw new Error("SHODAN_API_KEY environment variable is required.");
}
const API_BASE_URL = "https://api.shodan.io";
const CVEDB_API_URL = "https://cvedb.shodan.io";
// Logging Helper Function
function logToFile(message: string) {
try {
const timestamp = new Date().toISOString();
const formattedMessage = `[${timestamp}] ${message}\n`;
fs.appendFileSync(logFilePath, formattedMessage, "utf8");
console.error(formattedMessage.trim()); // Use stderr for logging to avoid interfering with stdout
} catch (error) {
console.error(`Failed to write to log file: ${error}`);
}
}
// Tool Schemas
const IpLookupArgsSchema = z.object({
ip: z.string().describe("The IP address to query."),
});
const ShodanSearchArgsSchema = z.object({
query: z.string().describe("Search query for Shodan."),
max_results: z
.number()
.optional()
.default(10)
.describe("Maximum results to return."),
});
const CVELookupArgsSchema = z.object({
cve: z.string()
.regex(/^CVE-\d{4}-\d{4,}$/i, "Must be a valid CVE ID format (e.g., CVE-2021-44228)")
.describe("The CVE identifier to query (format: CVE-YYYY-NNNNN)."),
});
const DnsLookupArgsSchema = z.object({
hostnames: z.array(z.string()).describe("List of hostnames to resolve."),
});
const ReverseDnsLookupArgsSchema = z.object({
ips: z.array(z.string()).describe("List of IP addresses to perform reverse DNS lookup on."),
});
const CpeLookupArgsSchema = z.object({
product: z.string().describe("The name of the product to search for CPEs."),
count: z.boolean().optional().default(false).describe("If true, returns only the count of matching CPEs."),
skip: z.number().optional().default(0).describe("Number of CPEs to skip (for pagination)."),
limit: z.number().optional().default(1000).describe("Maximum number of CPEs to return (max 1000)."),
});
const CVEsByProductArgsSchema = z.object({
cpe23: z.string().optional().describe("The CPE version 2.3 identifier (format: cpe:2.3:part:vendor:product:version)."),
product: z.string().optional().describe("The name of the product to search for CVEs."),
count: z.boolean().optional().default(false).describe("If true, returns only the count of matching CVEs."),
is_kev: z.boolean().optional().default(false).describe("If true, returns only CVEs with the KEV flag set."),
sort_by_epss: z.boolean().optional().default(false).describe("If true, sorts CVEs by EPSS score in descending order."),
skip: z.number().optional().default(0).describe("Number of CVEs to skip (for pagination)."),
limit: z.number().optional().default(1000).describe("Maximum number of CVEs to return (max 1000)."),
start_date: z.string().optional().describe("Start date for filtering CVEs (format: YYYY-MM-DDTHH:MM:SS)."),
end_date: z.string().optional().describe("End date for filtering CVEs (format: YYYY-MM-DDTHH:MM:SS).")
}).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." }
);
// Helper Function to Query Shodan API
async function queryShodan(endpoint: string, params: Record<string, any>) {
try {
const response = await axios.get(`${API_BASE_URL}${endpoint}`, {
params: { ...params, key: SHODAN_API_KEY },
timeout: 10000,
});
return response.data;
} catch (error: any) {
const errorMessage = error.response?.data?.error || error.message;
logToFile(`Shodan API error: ${errorMessage}`);
throw new Error(`Shodan API error: ${errorMessage}`);
}
}
// Helper Function for CVE lookups using CVEDB
async function queryCVEDB(cveId: string) {
try {
logToFile(`Querying CVEDB for: ${cveId}`);
const response = await axios.get(`${CVEDB_API_URL}/cve/${cveId}`);
return response.data;
} catch (error: any) {
if (error.response?.status === 422) {
throw new Error(`Invalid CVE ID format: ${cveId}`);
}
if (error.response?.status === 404) {
throw new Error(`CVE not found: ${cveId}`);
}
throw new Error(`CVEDB API error: ${error.message}`);
}
}
// Helper Function for CPE lookups using CVEDB
async function queryCPEDB(params: {
product: string;
count?: boolean;
skip?: number;
limit?: number;
}) {
try {
logToFile(`Querying CVEDB for CPEs with params: ${JSON.stringify(params)}`);
const response = await axios.get(`${CVEDB_API_URL}/cpes`, { params });
return response.data;
} catch (error: any) {
if (error.response?.status === 422) {
throw new Error(`Invalid parameters: ${error.response.data?.detail || error.message}`);
}
throw new Error(`CVEDB API error: ${error.message}`);
}
}
// Helper Function for CVEs by product/CPE lookups using CVEDB
async function queryCVEsByProduct(params: {
cpe23?: string;
product?: string;
count?: boolean;
is_kev?: boolean;
sort_by_epss?: boolean;
skip?: number;
limit?: number;
start_date?: string;
end_date?: string;
}) {
try {
logToFile(`Querying CVEDB for CVEs with params: ${JSON.stringify(params)}`);
const response = await axios.get(`${CVEDB_API_URL}/cves`, { params });
return response.data;
} catch (error: any) {
if (error.response?.status === 422) {
throw new Error(`Invalid parameters: ${error.response.data?.detail || error.message}`);
}
throw new Error(`CVEDB API error: ${error.message}`);
}
}
// Server Setup
const server = new Server(
{
name: "shodan-mcp",
version: "1.0.0",
},
{
capabilities: {
tools: {
listChanged: true,
},
},
}
);
// Handle Initialization
server.setRequestHandler(InitializeRequestSchema, async (request) => {
logToFile("Received initialize request.");
return {
protocolVersion: "2024-11-05",
capabilities: {
tools: {
listChanged: true,
},
},
serverInfo: {
name: "shodan-mcp",
version: "1.0.0",
},
instructions: `This MCP server provides comprehensive access to Shodan's network intelligence and security services:
- Network Reconnaissance: Query detailed information about IP addresses, including open ports, services, and vulnerabilities
- DNS Operations: Forward and reverse DNS lookups for domains and IP addresses
- Vulnerability Intelligence: Access to Shodan's CVEDB for detailed vulnerability information, CPE lookups, and product-specific CVE tracking
- Device Discovery: Search Shodan's database of internet-connected devices with advanced filtering
Each tool provides structured, formatted output for easy analysis and integration.`,
};
});
// Register Tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools = [
{
name: "ip_lookup",
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.",
inputSchema: zodToJsonSchema(IpLookupArgsSchema),
},
{
name: "shodan_search",
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.",
inputSchema: zodToJsonSchema(ShodanSearchArgsSchema),
},
{
name: "cve_lookup",
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).",
inputSchema: zodToJsonSchema(CVELookupArgsSchema),
},
{
name: "dns_lookup",
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.",
inputSchema: zodToJsonSchema(DnsLookupArgsSchema),
},
{
name: "cpe_lookup",
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.",
inputSchema: zodToJsonSchema(CpeLookupArgsSchema),
},
{
name: "cves_by_product",
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.",
inputSchema: zodToJsonSchema(CVEsByProductArgsSchema),
},
{
name: "reverse_dns_lookup",
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.",
inputSchema: zodToJsonSchema(ReverseDnsLookupArgsSchema),
},
];
logToFile("Registered tools.");
return { tools };
});
// Handle Tool Calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
logToFile(`Tool called: ${request.params.name}`);
try {
const { name, arguments: args } = request.params;
switch (name) {
case "ip_lookup": {
const parsedIpArgs = IpLookupArgsSchema.safeParse(args);
if (!parsedIpArgs.success) {
throw new Error("Invalid ip_lookup arguments");
}
const result = await queryShodan(`/shodan/host/${parsedIpArgs.data.ip}`, {});
// Format the response in a user-friendly way
const formattedResult = {
"IP Information": {
"IP Address": result.ip_str,
"Organization": result.org,
"ISP": result.isp,
"ASN": result.asn,
"Last Update": result.last_update
},
"Location": {
"Country": result.country_name,
"City": result.city,
"Coordinates": `${result.latitude}, ${result.longitude}`,
"Region": result.region_code
},
"Services": result.ports.map((port: number) => {
const service = result.data.find((d: ShodanService) => d.port === port);
return {
"Port": port,
"Protocol": service?.transport || "unknown",
"Service": service?.data?.trim() || "No banner",
...(service?.http ? {
"HTTP": {
"Server": service.http.server,
"Title": service.http.title,
}
} : {})
};
}),
"Cloud Provider": result.data[0]?.cloud ? {
"Provider": result.data[0].cloud.provider,
"Service": result.data[0].cloud.service,
"Region": result.data[0].cloud.region
} : "Not detected",
"Hostnames": result.hostnames || [],
"Domains": result.domains || [],
"Tags": result.tags || []
};
return {
content: [
{
type: "text",
text: JSON.stringify(formattedResult, null, 2),
},
],
};
}
case "shodan_search": {
const parsedSearchArgs = ShodanSearchArgsSchema.safeParse(args);
if (!parsedSearchArgs.success) {
throw new Error("Invalid search arguments");
}
const result: SearchResponse = await queryShodan("/shodan/host/search", {
query: parsedSearchArgs.data.query,
limit: parsedSearchArgs.data.max_results,
});
// Format the response in a user-friendly way
const formattedResult = {
"Search Summary": {
"Query": parsedSearchArgs.data.query,
"Total Results": result.total,
"Results Returned": result.matches.length
},
"Country Distribution": result.facets?.country?.map(country => ({
"Country": country.value,
"Count": country.count,
"Percentage": `${((country.count / result.total) * 100).toFixed(2)}%`
})) || [],
"Matches": result.matches.map(match => ({
"Basic Information": {
"IP Address": match.ip_str,
"Organization": match.org,
"ISP": match.isp,
"ASN": match.asn,
"Last Update": match.timestamp
},
"Location": {
"Country": match.location.country_name,
"City": match.location.city || "Unknown",
"Region": match.location.region_code || "Unknown",
"Coordinates": `${match.location.latitude}, ${match.location.longitude}`
},
"Service Details": {
"Port": match.port,
"Transport": match.transport,
"Product": match.product || "Unknown",
"Version": match.version || "Unknown",
"CPE": match.cpe || []
},
"Web Information": match.http ? {
"Server": match.http.server,
"Title": match.http.title,
"Robots.txt": match.http.robots ? "Present" : "Not found",
"Sitemap": match.http.sitemap ? "Present" : "Not found"
} : "No HTTP information",
"Hostnames": match.hostnames,
"Domains": match.domains
}))
};
return {
content: [
{
type: "text",
text: JSON.stringify(formattedResult, null, 2),
},
],
};
}
case "cve_lookup": {
const parsedCveArgs = CVELookupArgsSchema.safeParse(args);
if (!parsedCveArgs.success) {
throw new Error("Invalid CVE format. Please use format: CVE-YYYY-NNNNN (e.g., CVE-2021-44228)");
}
const cveId = parsedCveArgs.data.cve.toUpperCase();
logToFile(`Looking up CVE: ${cveId}`);
try {
const result = await queryCVEDB(cveId);
// Helper function to format CVSS score severity
const getCvssSeverity = (score: number) => {
if (score >= 9.0) return "Critical";
if (score >= 7.0) return "High";
if (score >= 4.0) return "Medium";
if (score >= 0.1) return "Low";
return "None";
};
// Format the response in a user-friendly way
const formattedResult = {
"Basic Information": {
"CVE ID": result.cve_id,
"Published": new Date(result.published_time).toLocaleString(),
"Summary": result.summary
},
"Severity Scores": {
"CVSS v3": result.cvss_v3 ? {
"Score": result.cvss_v3,
"Severity": getCvssSeverity(result.cvss_v3)
} : "Not available",
"CVSS v2": result.cvss_v2 ? {
"Score": result.cvss_v2,
"Severity": getCvssSeverity(result.cvss_v2)
} : "Not available",
"EPSS": result.epss ? {
"Score": `${(result.epss * 100).toFixed(2)}%`,
"Ranking": `Top ${(result.ranking_epss * 100).toFixed(2)}%`
} : "Not available"
},
"Impact Assessment": {
"Known Exploited Vulnerability": result.kev ? "Yes" : "No",
"Proposed Action": result.propose_action || "No specific action proposed",
"Ransomware Campaign": result.ransomware_campaign || "No known ransomware campaigns"
},
"Affected Products": result.cpes?.length > 0 ? result.cpes : ["No specific products listed"],
"Additional Information": {
"References": result.references?.length > 0 ? result.references : ["No references provided"]
}
};
return {
content: [
{
type: "text",
text: JSON.stringify(formattedResult, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: "text",
text: error.message,
},
],
isError: true,
};
}
}
case "dns_lookup": {
const parsedDnsArgs = DnsLookupArgsSchema.safeParse(args);
if (!parsedDnsArgs.success) {
throw new Error("Invalid dns_lookup arguments");
}
// Join hostnames with commas for the API request
const hostnamesString = parsedDnsArgs.data.hostnames.join(",");
const result: DnsResponse = await queryShodan("/dns/resolve", {
hostnames: hostnamesString
});
// Format the response in a user-friendly way
const formattedResult = {
"DNS Resolutions": Object.entries(result).map(([hostname, ip]) => ({
"Hostname": hostname,
"IP Address": ip
})),
"Summary": {
"Total Lookups": Object.keys(result).length,
"Queried Hostnames": parsedDnsArgs.data.hostnames
}
};
return {
content: [
{
type: "text",
text: JSON.stringify(formattedResult, null, 2)
},
],
};
}
case "cpe_lookup": {
const parsedCpeArgs = CpeLookupArgsSchema.safeParse(args);
if (!parsedCpeArgs.success) {
throw new Error("Invalid cpe_lookup arguments");
}
try {
const result = await queryCPEDB({
product: parsedCpeArgs.data.product,
count: parsedCpeArgs.data.count,
skip: parsedCpeArgs.data.skip,
limit: parsedCpeArgs.data.limit
});
// Format the response based on whether it's a count request or full CPE list
const formattedResult = parsedCpeArgs.data.count
? { total_cpes: result.total }
: {
cpes: result.cpes,
skip: parsedCpeArgs.data.skip,
limit: parsedCpeArgs.data.limit,
total_returned: result.cpes.length
};
return {
content: [
{
type: "text",
text: JSON.stringify(formattedResult, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: "text",
text: error.message,
},
],
isError: true,
};
}
}
case "cves_by_product": {
const parsedArgs = CVEsByProductArgsSchema.safeParse(args);
if (!parsedArgs.success) {
throw new Error("Invalid arguments. Must provide either cpe23 or product name, but not both.");
}
try {
const result = await queryCVEsByProduct({
cpe23: parsedArgs.data.cpe23,
product: parsedArgs.data.product,
count: parsedArgs.data.count,
is_kev: parsedArgs.data.is_kev,
sort_by_epss: parsedArgs.data.sort_by_epss,
skip: parsedArgs.data.skip,
limit: parsedArgs.data.limit,
start_date: parsedArgs.data.start_date,
end_date: parsedArgs.data.end_date
});
// Helper function to format CVSS score severity
const getCvssSeverity = (score: number) => {
if (score >= 9.0) return "Critical";
if (score >= 7.0) return "High";
if (score >= 4.0) return "Medium";
if (score >= 0.1) return "Low";
return "None";
};
// Format the response based on whether it's a count request or full CVE list
const formattedResult = parsedArgs.data.count
? {
"Query Information": {
"Product": parsedArgs.data.product || "N/A",
"CPE 2.3": parsedArgs.data.cpe23 || "N/A",
"KEV Only": parsedArgs.data.is_kev ? "Yes" : "No",
"Sort by EPSS": parsedArgs.data.sort_by_epss ? "Yes" : "No"
},
"Results": {
"Total CVEs Found": result.total
}
}
: {
"Query Information": {
"Product": parsedArgs.data.product || "N/A",
"CPE 2.3": parsedArgs.data.cpe23 || "N/A",
"KEV Only": parsedArgs.data.is_kev ? "Yes" : "No",
"Sort by EPSS": parsedArgs.data.sort_by_epss ? "Yes" : "No",
"Date Range": parsedArgs.data.start_date ?
`${parsedArgs.data.start_date} to ${parsedArgs.data.end_date || 'now'}` :
"All dates"
},
"Results Summary": {
"Total CVEs Found": result.total,
"CVEs Returned": result.cves.length,
"Page": `${Math.floor(parsedArgs.data.skip! / parsedArgs.data.limit!) + 1}`,
"CVEs per Page": parsedArgs.data.limit
},
"Vulnerabilities": result.cves.map((cve: CveResponse) => ({
"Basic Information": {
"CVE ID": cve.cve_id,
"Published": new Date(cve.published_time).toLocaleString(),
"Summary": cve.summary
},
"Severity Scores": {
"CVSS v3": cve.cvss_v3 ? {
"Score": cve.cvss_v3,
"Severity": getCvssSeverity(cve.cvss_v3)
} : "Not available",
"CVSS v2": cve.cvss_v2 ? {
"Score": cve.cvss_v2,
"Severity": getCvssSeverity(cve.cvss_v2)
} : "Not available",
"EPSS": cve.epss ? {
"Score": `${(cve.epss * 100).toFixed(2)}%`,
"Ranking": `Top ${(cve.ranking_epss * 100).toFixed(2)}%`
} : "Not available"
},
"Impact Assessment": {
"Known Exploited Vulnerability": cve.kev ? "Yes" : "No",
"Proposed Action": cve.propose_action || "No specific action proposed",
"Ransomware Campaign": cve.ransomware_campaign || "No known ransomware campaigns"
},
"References": cve.references?.length > 0 ? cve.references : ["No references provided"]
}))
};
return {
content: [
{
type: "text",
text: JSON.stringify(formattedResult, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: "text",
text: error.message,
},
],
isError: true,
};
}
}
case "reverse_dns_lookup": {
const parsedArgs = ReverseDnsLookupArgsSchema.safeParse(args);
if (!parsedArgs.success) {
throw new Error("Invalid reverse_dns_lookup arguments");
}
// Join IPs with commas for the API request
const ipsString = parsedArgs.data.ips.join(",");
const result: ReverseDnsResponse = await queryShodan("/dns/reverse", {
ips: ipsString
});
// Format the response in a user-friendly way
const formattedResult = {
"Reverse DNS Resolutions": Object.entries(result).map(([ip, hostnames]) => ({
"IP Address": ip,
"Hostnames": hostnames.length > 0 ? hostnames : ["No hostnames found"]
})),
"Summary": {
"Total IPs Queried": parsedArgs.data.ips.length,
"IPs with Results": Object.keys(result).length,
"Queried IP Addresses": parsedArgs.data.ips
}
};
return {
content: [
{
type: "text",
text: JSON.stringify(formattedResult, null, 2)
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error: any) {
const errorMessage = error instanceof Error ? error.message : String(error);
logToFile(`Error handling tool call: ${errorMessage}`);
return {
content: [
{
type: "text",
text: `Error: ${errorMessage}`,
},
],
isError: true,
};
}
});
// Start the Server
async function runServer() {
logToFile("Starting Shodan MCP Server...");
try {
const transport = new StdioServerTransport();
await server.connect(transport);
logToFile("Shodan MCP Server is running.");
} catch (error: any) {
logToFile(`Error connecting server: ${error.message}`);
process.exit(1);
}
}
// Handle process events
process.on('uncaughtException', (error) => {
logToFile(`Uncaught exception: ${error.message}`);
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
logToFile(`Unhandled rejection: ${reason}`);
process.exit(1);
});
runServer().catch((error: any) => {
logToFile(`Fatal error: ${error.message}`);
process.exit(1);
});
```