search_shodan
Query Shodan's database to find internet-connected devices and services for cybersecurity research and threat intelligence analysis.
Instructions
Search Shodan's database for devices and services
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Shodan search query (e.g., 'apache country:US') | |
| page | No | Page number for results pagination (default: 1) | |
| facets | No | List of facets to include in the search results (e.g., ['country', 'org']) | |
| max_items | No | Maximum number of items to include in arrays (default: 5) | |
| fields | No | List of fields to include in the results (e.g., ['ip_str', 'ports', 'location.country_name']) | |
| summarize | No | Whether to return a summary of the results instead of the full data (default: false) |
Implementation Reference
- src/index.ts:155-184 (handler)Core implementation of the Shodan search functionality, making API call to /shodan/host/search, applying sampling and field filtering, handling errors including paid membership requirement.async search(query: string, page: number = 1, facets: string[] = [], maxItems: number = 5, selectedFields?: string[]): Promise<any> { try { const params: any = { query, page }; if (facets.length > 0) { params.facets = facets.join(','); } const response = await this.axiosInstance.get("/shodan/host/search", { params }); return this.sampleResponse(response.data, maxItems, selectedFields); } catch (error: unknown) { if (axios.isAxiosError(error)) { if (error.response?.status === 401) { return { error: "Unauthorized: The Shodan search API requires a paid membership. Your API key does not have access to this endpoint.", message: "The search functionality requires a Shodan membership subscription with API access. Please upgrade your Shodan plan to use this feature.", status: 401 }; } throw new McpError( ErrorCode.InternalError, `Shodan API error: ${error.response?.data?.error || error.message}` ); } throw error; } }
- src/index.ts:1317-1374 (handler)MCP server tool handler for 'search_shodan': validates input parameters, calls ShodanClient.search, optionally summarizes results, formats response as MCP content.case "search_shodan": { const query = String(request.params.arguments?.query); if (!query) { throw new McpError( ErrorCode.InvalidParams, "Search query is required" ); } const page = Number(request.params.arguments?.page) || 1; const facets = Array.isArray(request.params.arguments?.facets) ? request.params.arguments?.facets.map(String) : []; const maxItems = Number(request.params.arguments?.max_items) || 5; const fields = Array.isArray(request.params.arguments?.fields) ? request.params.arguments?.fields.map(String) : undefined; const summarize = Boolean(request.params.arguments?.summarize); try { const searchResults = await shodanClient.search(query, page, facets, maxItems, fields); // Check if we got an error response from the search method if (searchResults.error && searchResults.status === 401) { return { content: [{ type: "text", text: JSON.stringify(searchResults, null, 2) }] }; } if (summarize) { const summary = shodanClient.summarizeResults(searchResults); return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] }; } return { content: [{ type: "text", text: JSON.stringify(searchResults, null, 2) }] }; } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Error searching Shodan: ${(error as Error).message}` ); } }
- src/index.ts:907-940 (schema)Input schema definition for the search_shodan tool, specifying parameters like query, page, facets, max_items, fields, and summarize option.type: "object", properties: { query: { type: "string", description: "Shodan search query (e.g., 'apache country:US')" }, page: { type: "number", description: "Page number for results pagination (default: 1)" }, facets: { type: "array", items: { type: "string" }, description: "List of facets to include in the search results (e.g., ['country', 'org'])" }, max_items: { type: "number", description: "Maximum number of items to include in arrays (default: 5)" }, fields: { type: "array", items: { type: "string" }, description: "List of fields to include in the results (e.g., ['ip_str', 'ports', 'location.country_name'])" }, summarize: { type: "boolean", description: "Whether to return a summary of the results instead of the full data (default: false)" } }, required: ["query"]
- src/index.ts:904-941 (registration)Tool registration in ListTools response: defines name, description, and input schema for search_shodan.name: "search_shodan", description: "Search Shodan's database for devices and services", inputSchema: { type: "object", properties: { query: { type: "string", description: "Shodan search query (e.g., 'apache country:US')" }, page: { type: "number", description: "Page number for results pagination (default: 1)" }, facets: { type: "array", items: { type: "string" }, description: "List of facets to include in the search results (e.g., ['country', 'org'])" }, max_items: { type: "number", description: "Maximum number of items to include in arrays (default: 5)" }, fields: { type: "array", items: { type: "string" }, description: "List of fields to include in the results (e.g., ['ip_str', 'ports', 'location.country_name'])" }, summarize: { type: "boolean", description: "Whether to return a summary of the results instead of the full data (default: false)" } }, required: ["query"] }
- src/index.ts:52-84 (helper)Helper method used by search to sample/truncate large arrays in responses (matches, data, ports) and filter to specific fields, reducing token usage.private sampleResponse(data: any, maxItems: number = 5, selectedFields?: string[]): any { if (!data) return data; // Clone the data to avoid modifying the original const result = JSON.parse(JSON.stringify(data)); // Sample matches array if it exists and is longer than maxItems if (result.matches && Array.isArray(result.matches) && result.matches.length > maxItems) { result.matches = result.matches.slice(0, maxItems); result._sample_note = `Response truncated to ${maxItems} matches. Original count: ${data.matches.length}`; } // Sample data array if it exists and is longer than maxItems if (result.data && Array.isArray(result.data) && result.data.length > maxItems) { result.data = result.data.slice(0, maxItems); result._sample_note = `Response truncated to ${maxItems} data items. Original count: ${data.data.length}`; } // Sample ports array if it exists and is longer than maxItems if (result.ports && Array.isArray(result.ports) && result.ports.length > maxItems) { result.ports = result.ports.slice(0, maxItems); if (!result._sample_note) { result._sample_note = `Ports truncated to ${maxItems} items. Original count: ${data.ports.length}`; } } // Filter fields if selectedFields is provided if (selectedFields && selectedFields.length > 0 && typeof result === 'object') { this.filterFields(result, selectedFields); } return result; }