Skip to main content
Glama
sideffect263

Nmap MCP Server

by sideffect263
README.md
# Nmap MCP Server (Smithery SDK)

This project implements an MCP-compliant (Model Context Protocol) server using the Smithery SDK. It wraps the Nmap network scanner, exposing its functionality as callable tools within the MCP framework.

## Features

- **MCP Standard Endpoint (`/mcp`)**: Interacts with the server using JSON-RPC 2.0 requests.
- **`nmapScan` Tool**: Initiates an Nmap scan on a specified target with configurable flags. Returns detailed scan results, including parsed XML output.
- **`getInfo` Tool**: Provides basic information about the Nmap service and its capabilities.
- Input validation for targets and Nmap flags to enhance security.

## Prerequisites

- [Node.js](https://nodejs.org/) (v16.x or later recommended)
- [Nmap](https://nmap.org/download.html) installed and available in your system's PATH.

## Installation

1. Clone the repository (or download the source code).
2. Navigate to the project directory:
   ```bash
   cd your-project-directory
   ```
3. Install dependencies:
   ```bash
   npm install
   ```

## Running the Server

To start the server, run:

```bash
npm start
```

By default, the server will listen on `http://localhost:5001`. You can set the `PORT` environment variable to use a different port.

## Interacting with the Server (MCP)

All interactions with the server are done via the `/mcp` endpoint using JSON-RPC 2.0. You will send `POST` requests with a `Content-Type: application/json` header.

### Calling the `nmapScan` Tool

To initiate an Nmap scan:

**Request Body (JSON):**

```json
{
  "jsonrpc": "2.0",
  "id": 1, // Or any unique request ID
  "method": "tools/call",
  "params": {
    "name": "nmapScan",
    "arguments": {
      "target": "scanme.nmap.org",
      "flags": "-A -T4" // Optional, defaults to "-T4 -p 1-1000"
    }
  }
}
```

**Example using cURL:**

```bash
curl -X POST http://localhost:5001/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "nmapScan",
      "arguments": {
        "target": "scanme.nmap.org",
        "flags": "-A"
      }
    }
  }'
```

**Expected Response Structure (Success):**

The server will respond with the results of the Nmap scan. The `content` array will contain a text object with a summary and the full JSON-parsed Nmap XML output.

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Nmap Scan Results for scanme.nmap.org\n\nScan started: ...\n... (summary of open ports) ...\nScan completed: ...\n\nFull XML Output:\n{\n  \"nmaprun\": { ... }\n}"
      }
    ]
  }
}
```

If the scan fails or inputs are invalid, the `text` field will contain an error message.

### Calling the `getInfo` Tool

To get information about the service:

**Request Body (JSON):**

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "getInfo",
    "arguments": {}
  }
}
```

**Example using cURL:**

```bash
curl -X POST http://localhost:5001/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/call",
    "params": {
      "name": "getInfo",
      "arguments": {}
    }
  }'
```

**Expected Response Structure (Success):**

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Nmap Service Information:\n- Service: Network scanning using Nmap\n- Version: 1.0.0\n- Available Tools: nmapScan, getInfo\n- Session ID: N/A\n- Supported Targets: Domain names, IP addresses, CIDR notation\n- Security: Input validation and command sanitization enabled"
      }
    ]
  }
}
```

## Deployment on Smithery

To deploy this server on Smithery, you will typically need to:

1. **Ensure Nmap is available in the Smithery execution environment.** This might involve custom Docker images or buildpacks if Nmap isn't pre-installed.
2. **Define the startup command:** Smithery will need to know how to start the application (e.g., `npm start`).
3. **Port configuration:** Smithery will expose the application, usually handling port mapping. Ensure the application listens on the port Smithery expects (often configured via the `PORT` environment variable, which this server supports via the `process.env.PORT || 5001` pattern).
4. **Package the application:** This usually means providing the source code and `package.json` (and `package-lock.json`). Smithery will then build the application, installing dependencies.

Refer to the Smithery documentation for specific deployment instructions and how to configure services that adhere to the Model Context Protocol.

## Example MCP Client

Below is an example of how to create a client to interact with this Nmap MCP server. You'll need to have the `@modelcontextprotocol/sdk` and `@smithery/sdk` packages installed in your client project.

Save the following code as `nmap_mcp_client_example.js`:

```javascript
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { createSmitheryUrl } from "@smithery/sdk/shared/config.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";

// Ensure you have your Smithery API key for the server in an environment variable
const SMITHERY_API_KEY = process.env.SMITHERY_NMAP_API_KEY || "your-smithery-api-key-here"; // Replace with your actual key or ensure env var is set
const NMAP_SERVER_URL = "https://server.smithery.ai/@sideffect263/nmap-mcp-server"; // Replace with your server URL if different

let clientInstance;
let nmapScanTool;
let getInfoTool;

async function initializeNmapClient() {
  if (clientInstance) {
    console.log("Nmap MCP Client already initialized.");
    return clientInstance;
  }

  console.log("Initializing Nmap MCP Client...");

  try {
    const config = {}; // Add any specific config for Smithery if needed
    const serverUrl = createSmitheryUrl(
      NMAP_SERVER_URL,
      { config, apiKey: SMITHERY_API_KEY }
    );

    const transport = new StreamableHTTPClientTransport(serverUrl);

    clientInstance = new Client({
      name: "ExampleNmapClient",
      version: "1.0.0",
    });

    console.log("Connecting to Nmap MCP server...");
    await clientInstance.connect(transport);
    console.log("Successfully connected to Nmap MCP server.");

    const toolsResponse = await clientInstance.listTools();
  
    if (toolsResponse && toolsResponse.tools && Array.isArray(toolsResponse.tools)) {
      console.log(`Available tools: ${toolsResponse.tools.map((t) => t.name).join(", ")}`);
      nmapScanTool = toolsResponse.tools.find(tool => tool.name === 'nmapScan');
      getInfoTool = toolsResponse.tools.find(tool => tool.name === 'getInfo');
    } else {
      throw new Error("Could not list tools from the server.");
    }

    if (!nmapScanTool) {
      throw new Error("Nmap tool ('nmapScan') not found on the MCP server.");
    }
    console.log(`Found Nmap tool: ${nmapScanTool.name}`);

    if (getInfoTool) {
      console.log(`Found GetInfo tool: ${getInfoTool.name}`);
    } else {
      console.warn("GetInfo tool ('getInfo') not found. This might be optional.");
    }
  
    return clientInstance;
  } catch (error) {
    console.error("Failed to initialize Nmap MCP client:", error);
    clientInstance = null; // Reset on failure
    nmapScanTool = null;
    getInfoTool = null;
    throw error;
  }
}

/**
 * Invokes the 'nmapScan' tool on the MCP server.
 * @param {Object} params - Parameters for the Nmap tool.
 * @param {string} params.target - The target IP, hostname, or CIDR.
 * @param {string} params.flags - Nmap flags as a single string (e.g., "-A -T4").
 * @returns {Promise<Object>} - The result from the 'nmapScan' tool.
 */
async function invokeNmapScan(params) {
  if (!clientInstance || !nmapScanTool) {
    console.log("Client not ready. Initializing...");
    await initializeNmapClient();
    if (!clientInstance || !nmapScanTool) {
      throw new Error("Nmap MCP client is not initialized or nmapScan tool not found.");
    }
  }

  const toolInput = {
    target: params.target,
    flags: params.flags 
  };
  console.log(\`Invoking Nmap tool "\${nmapScanTool.name}" with params:\`, toolInput);

  try {
    const result = await clientInstance.callTool({
      name: nmapScanTool.name,
      arguments: toolInput
    });
    console.log("'nmapScan' tool invocation successful. Result:", JSON.stringify(result, null, 2));
    return result;
  } catch (error) {
    console.error(\`Error invoking 'nmapScan' tool for target \${params.target}:\`, error);
    throw error;
  }
}

/**
 * Invokes the 'getInfo' tool on the MCP server.
 * @returns {Promise<Object>} - The result from the 'getInfo' tool.
 */
async function invokeGetInfo() {
  if (!clientInstance || !getInfoTool) {
    console.log("Client or getInfo tool not ready. Initializing...");
    await initializeNmapClient();
    if (!clientInstance || !getInfoTool) {
      throw new Error("Nmap MCP client is not initialized or getInfo tool not found.");
    }
  }
  
  console.log(\`Invoking GetInfo tool "\${getInfoTool.name}"\`);
  try {
    const result = await clientInstance.callTool({
      name: getInfoTool.name,
      arguments: {} // getInfo usually takes no arguments
    });
    console.log("'getInfo' tool invocation successful. Result:", JSON.stringify(result, null, 2));
    return result;
  } catch (error) {
    console.error("Error invoking 'getInfo' tool:", error);
    throw error;
  }
}


// --- Example Usage ---
async function main() {
  try {
    await initializeNmapClient();

    // Example: Get server info
    console.log("\\n--- Calling getInfo ---");
    const info = await invokeGetInfo();
    if (info && info.content && info.content[0] && info.content[0].text) {
        console.log("Server Info:", info.content[0].text);
    }


    // Example: Perform an Nmap scan
    console.log("\\n--- Calling nmapScan ---");
    const scanParams = {
      target: "scanme.nmap.org", // A safe target for testing
      flags: "-T4 -F" // Example flags: Fast scan, default timing
    };
    const scanResult = await invokeNmapScan(scanParams);
    if (scanResult && scanResult.content && scanResult.content[0] && scanResult.content[0].text) {
        console.log(\`Scan results for \${scanParams.target}:\`, scanResult.content[0].text);
    }

  } catch (error) {
    console.error("\\n--- Example Script Failed ---");
    console.error("An error occurred:", error.message);
  } finally {
    if (clientInstance) {
      console.log("\\nDisconnecting client...");
      await clientInstance.disconnect();
      console.log("Client disconnected.");
    }
  }
}

// Run the example
main();

export { initializeNmapClient, invokeNmapScan, invokeGetInfo };
```

To run this example client:

1. Ensure you have Node.js installed.
2. Create a new directory for your client project.
3. Inside this directory, run `npm init -y` to create a `package.json` file.
4. Install the necessary SDKs:
   ```bash
   npm install @modelcontextprotocol/sdk @smithery/sdk
   ```
5. Save the code above as `nmap_mcp_client_example.js` in this directory.
6. Modify the `SMITHERY_NMAP_API_KEY` and `NMAP_SERVER_URL` constants in the script with your actual Smithery API key and the URL of your deployed Nmap MCP server.
7. If your `package.json` doesn't already have `"type": "module"`, add it or change the import/export syntax to CommonJS (`require`/`module.exports`). For ES Modules (as written), add:
   ```json
   // package.json
   {
     // ... other properties
     "type": "module" 
   }
   ```
8. Run the client:
   ```bash
   node nmap_mcp_client_example.js
   ```

This client will connect to your Nmap MCP server, list available tools, call `getInfo` for server information, and then call `nmapScan` to perform a scan on `scanme.nmap.org`.