# Directory Structure ``` ├── .cursor │ └── mcp.json ├── .gitignore ├── build │ └── index.js ├── Dockerfile ├── LICENSE ├── package.json ├── README.md ├── smithery.yaml ├── src │ └── index.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` /node_modules /package-lock.json .env ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # RunPod MCP Server [](https://smithery.ai/server/@runpod/runpod-mcp-ts) This Model Context Protocol (MCP) server enables you to interact with the RunPod REST API through Claude or other MCP-compatible clients. ## Features The server provides tools for managing: - **Pods**: Create, list, get details, update, start, stop, and delete pods - **Endpoints**: Create, list, get details, update, and delete serverless endpoints - **Templates**: Create, list, get details, update, and delete templates - **Network Volumes**: Create, list, get details, update, and delete network volumes - **Container Registry Authentications**: Create, list, get details, and delete authentications ## Setup ### Prerequisites - Node.js 18 or higher - A RunPod account and API key - Claude for Desktop or another MCP-compatible client ### Installing via Smithery To install runpod-mcp-ts for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@runpod/runpod-mcp-ts): ```bash npx -y @smithery/cli install @runpod/runpod-mcp-ts --client claude ``` ### Installation 1. Clone the repository 2. Install dependencies: ``` npm install ``` 3. Build the server: ``` npm run build ``` ### Configuration Set your RunPod API key as an environment variable: ```bash # Linux/macOS export RUNPOD_API_KEY=your_api_key_here # Windows (Command Prompt) set RUNPOD_API_KEY=your_api_key_here # Windows (PowerShell) $env:RUNPOD_API_KEY="your_api_key_here" ``` You can get your API key from the [RunPod console](https://www.runpod.io/console/user/settings). ### Running the Server Start the server: ```bash npm start ``` ## Setting up with Claude for Desktop 1. Open Claude for Desktop 2. Edit the config file: `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows) 3. Add the server configuration: ```json { "mcpServers": { "runpod": { "command": "node", "args": ["/path/to/runpod-mcp-server/build/index.js"], "env": { "RUNPOD_API_KEY": "your_api_key_here" } } } } ``` Make sure to replace the `"args": ["/path/to/runpod-mcp-server/build/index.js"]` with the path to the build folder in the repository. 4. Restart Claude for Desktop ## Usage Examples Here are some examples of how to use the server with Claude: ### List all pods ``` Can you list all my RunPod pods? ``` ### Create a new pod ``` Create a new RunPod pod with the following specifications: - Name: test-pod - Image: runpod/pytorch:2.1.0-py3.10-cuda11.8.0-devel-ubuntu22.04 - GPU Type: NVIDIA GeForce RTX 4090 - GPU Count: 1 ``` ### Create a serverless endpoint ``` Create a RunPod serverless endpoint with the following configuration: - Name: my-endpoint - Template ID: 30zmvf89kd - Minimum workers: 0 - Maximum workers: 3 ``` ## Security Considerations This server requires your RunPod API key, which grants full access to your RunPod account. For security: - Never share your API key - Be cautious about what operations you perform - Consider setting up a separate API key with limited permissions - Don't use this in a production environment without proper security measures ## License MIT ``` -------------------------------------------------------------------------------- /.cursor/mcp.json: -------------------------------------------------------------------------------- ```json { "mcpServers": { "runpod": { "command": "node", "args": ["your/path/to/runpod-mcp-ts/build/index.js"], "env": { "RUNPOD_API_KEY": "your_api_key_here" } } } } ``` -------------------------------------------------------------------------------- /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"] } ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile FROM node:lts-alpine # Set working directory WORKDIR /app # Copy package.json and package-lock.json if available COPY package*.json ./ # Install dependencies RUN npm install --ignore-scripts # Copy the rest of the application COPY . . # Build the project RUN npm run build # Expose any necessary port if required (not specified, so leaving out) # Start the server CMD ["npm", "start"] ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "runpod-mcp-server", "version": "1.0.0", "description": "MCP server for interacting with RunPod API", "type": "module", "main": "build/index.js", "scripts": { "build": "tsc", "start": "node build/index.js", "dev": "tsx index.ts" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.7.0", "node-fetch": "^3.3.2", "zod": "^3.22.4" }, "devDependencies": { "@types/node": "^20.10.5", "tsx": "^4.7.0", "typescript": "^5.3.3" } } ``` -------------------------------------------------------------------------------- /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: - runpodApiKey properties: runpodApiKey: type: string description: Your RunPod API key commandFunction: # A JS function that produces the CLI command based on the given config to start the MCP on stdio. |- (config) => ({ command: 'node', args: ['build/index.js'], env: { RUNPOD_API_KEY: config.runpodApiKey } }) exampleConfig: runpodApiKey: your_dummy_runpod_api_key_here ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { z } from "zod" import fetch from "node-fetch" // Base URL for RunPod API const API_BASE_URL = "https://rest.runpod.io/v1" // Get API key from environment variable const API_KEY = process.env.RUNPOD_API_KEY if (!API_KEY) { console.error("RUNPOD_API_KEY environment variable is required") process.exit(1) } // Create an MCP server const server = new McpServer({ name: "RunPod API Server", version: "1.0.0", capabilities: { resources: {}, tools: {}, }, }) // Helper function to make authenticated API requests to RunPod async function runpodRequest( endpoint: string, method: string = "GET", body?: any ) { const url = `${API_BASE_URL}${endpoint}` const headers = { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json", } const options: any = { method, headers, } if (body && (method === "POST" || method === "PATCH")) { options.body = JSON.stringify(body) } try { const response = await fetch(url, options) if (!response.ok) { const errorText = await response.text() throw new Error(`RunPod API Error: ${response.status} - ${errorText}`) } // Some endpoints might not return JSON const contentType = response.headers.get("content-type") if (contentType && contentType.includes("application/json")) { return await response.json() } return { success: true, status: response.status } } catch (error) { console.error("Error calling RunPod API:", error) throw error } } // ============== POD MANAGEMENT TOOLS ============== // List Pods server.tool( "list-pods", { computeType: z .enum(["GPU", "CPU"]) .optional() .describe("Filter to only GPU or only CPU Pods"), gpuTypeId: z .array(z.string()) .optional() .describe("Filter to Pods with any of the listed GPU types"), dataCenterId: z .array(z.string()) .optional() .describe("Filter to Pods in any of the provided data centers"), name: z .string() .optional() .describe("Filter to Pods with the provided name"), includeMachine: z .boolean() .optional() .describe("Include information about the machine"), includeNetworkVolume: z .boolean() .optional() .describe("Include information about attached network volumes"), }, async (params) => { // Construct query parameters const queryParams = new URLSearchParams() if (params.computeType) queryParams.append("computeType", params.computeType) if (params.gpuTypeId) params.gpuTypeId.forEach((type) => queryParams.append("gpuTypeId", type)) if (params.dataCenterId) params.dataCenterId.forEach((dc) => queryParams.append("dataCenterId", dc) ) if (params.name) queryParams.append("name", params.name) if (params.includeMachine) queryParams.append("includeMachine", params.includeMachine.toString()) if (params.includeNetworkVolume) queryParams.append( "includeNetworkVolume", params.includeNetworkVolume.toString() ) const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "" const result = await runpodRequest(`/pods${queryString}`) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // Get Pod Details server.tool( "get-pod", { podId: z.string().describe("ID of the pod to retrieve"), includeMachine: z .boolean() .optional() .describe("Include information about the machine"), includeNetworkVolume: z .boolean() .optional() .describe("Include information about attached network volumes"), }, async (params) => { // Construct query parameters const queryParams = new URLSearchParams() if (params.includeMachine) queryParams.append("includeMachine", params.includeMachine.toString()) if (params.includeNetworkVolume) queryParams.append( "includeNetworkVolume", params.includeNetworkVolume.toString() ) const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "" const result = await runpodRequest(`/pods/${params.podId}${queryString}`) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // Create Pod server.tool( "create-pod", { name: z.string().optional().describe("Name for the pod"), imageName: z.string().describe("Docker image to use"), cloudType: z .enum(["SECURE", "COMMUNITY"]) .optional() .describe("SECURE or COMMUNITY cloud"), gpuTypeIds: z .array(z.string()) .optional() .describe("List of acceptable GPU types"), gpuCount: z.number().optional().describe("Number of GPUs"), containerDiskInGb: z .number() .optional() .describe("Container disk size in GB"), volumeInGb: z.number().optional().describe("Volume size in GB"), volumeMountPath: z.string().optional().describe("Path to mount the volume"), ports: z .array(z.string()) .optional() .describe("Ports to expose (e.g., '8888/http', '22/tcp')"), env: z.record(z.string()).optional().describe("Environment variables"), dataCenterIds: z .array(z.string()) .optional() .describe("List of data centers"), }, async (params) => { const result = await runpodRequest("/pods", "POST", params) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // Update Pod server.tool( "update-pod", { podId: z.string().describe("ID of the pod to update"), name: z.string().optional().describe("New name for the pod"), imageName: z.string().optional().describe("New Docker image"), containerDiskInGb: z .number() .optional() .describe("New container disk size in GB"), volumeInGb: z.number().optional().describe("New volume size in GB"), volumeMountPath: z .string() .optional() .describe("New path to mount the volume"), ports: z.array(z.string()).optional().describe("New ports to expose"), env: z.record(z.string()).optional().describe("New environment variables"), }, async (params) => { const { podId, ...updateParams } = params const result = await runpodRequest(`/pods/${podId}`, "PATCH", updateParams) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // Start Pod server.tool( "start-pod", { podId: z.string().describe("ID of the pod to start"), }, async (params) => { const result = await runpodRequest(`/pods/${params.podId}/start`, "POST") return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // Stop Pod server.tool( "stop-pod", { podId: z.string().describe("ID of the pod to stop"), }, async (params) => { const result = await runpodRequest(`/pods/${params.podId}/stop`, "POST") return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // Delete Pod server.tool( "delete-pod", { podId: z.string().describe("ID of the pod to delete"), }, async (params) => { const result = await runpodRequest(`/pods/${params.podId}`, "DELETE") return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // ============== ENDPOINT MANAGEMENT TOOLS ============== // List Endpoints server.tool( "list-endpoints", { includeTemplate: z .boolean() .optional() .describe("Include template information"), includeWorkers: z .boolean() .optional() .describe("Include information about workers"), }, async (params) => { // Construct query parameters const queryParams = new URLSearchParams() if (params.includeTemplate) queryParams.append("includeTemplate", params.includeTemplate.toString()) if (params.includeWorkers) queryParams.append("includeWorkers", params.includeWorkers.toString()) const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "" const result = await runpodRequest(`/endpoints${queryString}`) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // Get Endpoint Details server.tool( "get-endpoint", { endpointId: z.string().describe("ID of the endpoint to retrieve"), includeTemplate: z .boolean() .optional() .describe("Include template information"), includeWorkers: z .boolean() .optional() .describe("Include information about workers"), }, async (params) => { // Construct query parameters const queryParams = new URLSearchParams() if (params.includeTemplate) queryParams.append("includeTemplate", params.includeTemplate.toString()) if (params.includeWorkers) queryParams.append("includeWorkers", params.includeWorkers.toString()) const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "" const result = await runpodRequest( `/endpoints/${params.endpointId}${queryString}` ) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // Create Endpoint server.tool( "create-endpoint", { name: z.string().optional().describe("Name for the endpoint"), templateId: z.string().describe("Template ID to use"), computeType: z .enum(["GPU", "CPU"]) .optional() .describe("GPU or CPU endpoint"), gpuTypeIds: z .array(z.string()) .optional() .describe("List of acceptable GPU types"), gpuCount: z.number().optional().describe("Number of GPUs per worker"), workersMin: z.number().optional().describe("Minimum number of workers"), workersMax: z.number().optional().describe("Maximum number of workers"), dataCenterIds: z .array(z.string()) .optional() .describe("List of data centers"), }, async (params) => { const result = await runpodRequest("/endpoints", "POST", params) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // Update Endpoint server.tool( "update-endpoint", { endpointId: z.string().describe("ID of the endpoint to update"), name: z.string().optional().describe("New name for the endpoint"), workersMin: z.number().optional().describe("New minimum number of workers"), workersMax: z.number().optional().describe("New maximum number of workers"), idleTimeout: z.number().optional().describe("New idle timeout in seconds"), scalerType: z .enum(["QUEUE_DELAY", "REQUEST_COUNT"]) .optional() .describe("Scaler type"), scalerValue: z.number().optional().describe("Scaler value"), }, async (params) => { const { endpointId, ...updateParams } = params const result = await runpodRequest( `/endpoints/${endpointId}`, "PATCH", updateParams ) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // Delete Endpoint server.tool( "delete-endpoint", { endpointId: z.string().describe("ID of the endpoint to delete"), }, async (params) => { const result = await runpodRequest( `/endpoints/${params.endpointId}`, "DELETE" ) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // ============== TEMPLATE MANAGEMENT TOOLS ============== // List Templates server.tool("list-templates", {}, async () => { const result = await runpodRequest("/templates") return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } }) // Get Template Details server.tool( "get-template", { templateId: z.string().describe("ID of the template to retrieve"), }, async (params) => { const result = await runpodRequest(`/templates/${params.templateId}`) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // Create Template server.tool( "create-template", { name: z.string().describe("Name for the template"), imageName: z.string().describe("Docker image to use"), isServerless: z .boolean() .optional() .describe("Is this a serverless template"), ports: z.array(z.string()).optional().describe("Ports to expose"), dockerEntrypoint: z .array(z.string()) .optional() .describe("Docker entrypoint commands"), dockerStartCmd: z .array(z.string()) .optional() .describe("Docker start commands"), env: z.record(z.string()).optional().describe("Environment variables"), containerDiskInGb: z .number() .optional() .describe("Container disk size in GB"), volumeInGb: z.number().optional().describe("Volume size in GB"), volumeMountPath: z.string().optional().describe("Path to mount the volume"), readme: z.string().optional().describe("README content in markdown format"), }, async (params) => { const result = await runpodRequest("/templates", "POST", params) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // Update Template server.tool( "update-template", { templateId: z.string().describe("ID of the template to update"), name: z.string().optional().describe("New name for the template"), imageName: z.string().optional().describe("New Docker image"), ports: z.array(z.string()).optional().describe("New ports to expose"), env: z.record(z.string()).optional().describe("New environment variables"), readme: z .string() .optional() .describe("New README content in markdown format"), }, async (params) => { const { templateId, ...updateParams } = params const result = await runpodRequest( `/templates/${templateId}`, "PATCH", updateParams ) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // Delete Template server.tool( "delete-template", { templateId: z.string().describe("ID of the template to delete"), }, async (params) => { const result = await runpodRequest( `/templates/${params.templateId}`, "DELETE" ) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // ============== NETWORK VOLUME MANAGEMENT TOOLS ============== // List Network Volumes server.tool("list-network-volumes", {}, async () => { const result = await runpodRequest("/networkvolumes") return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } }) // Get Network Volume Details server.tool( "get-network-volume", { networkVolumeId: z .string() .describe("ID of the network volume to retrieve"), }, async (params) => { const result = await runpodRequest( `/networkvolumes/${params.networkVolumeId}` ) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // Create Network Volume server.tool( "create-network-volume", { name: z.string().describe("Name for the network volume"), size: z.number().describe("Size in GB (1-4000)"), dataCenterId: z.string().describe("Data center ID"), }, async (params) => { const result = await runpodRequest("/networkvolumes", "POST", params) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // Update Network Volume server.tool( "update-network-volume", { networkVolumeId: z.string().describe("ID of the network volume to update"), name: z.string().optional().describe("New name for the network volume"), size: z .number() .optional() .describe("New size in GB (must be larger than current)"), }, async (params) => { const { networkVolumeId, ...updateParams } = params const result = await runpodRequest( `/networkvolumes/${networkVolumeId}`, "PATCH", updateParams ) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // Delete Network Volume server.tool( "delete-network-volume", { networkVolumeId: z.string().describe("ID of the network volume to delete"), }, async (params) => { const result = await runpodRequest( `/networkvolumes/${params.networkVolumeId}`, "DELETE" ) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // ============== CONTAINER REGISTRY AUTH TOOLS ============== // List Container Registry Auths server.tool("list-container-registry-auths", {}, async () => { const result = await runpodRequest("/containerregistryauth") return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } }) // Get Container Registry Auth Details server.tool( "get-container-registry-auth", { containerRegistryAuthId: z .string() .describe("ID of the container registry auth to retrieve"), }, async (params) => { const result = await runpodRequest( `/containerregistryauth/${params.containerRegistryAuthId}` ) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // Create Container Registry Auth server.tool( "create-container-registry-auth", { name: z.string().describe("Name for the container registry auth"), username: z.string().describe("Registry username"), password: z.string().describe("Registry password"), }, async (params) => { const result = await runpodRequest("/containerregistryauth", "POST", params) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // Delete Container Registry Auth server.tool( "delete-container-registry-auth", { containerRegistryAuthId: z .string() .describe("ID of the container registry auth to delete"), }, async (params) => { const result = await runpodRequest( `/containerregistryauth/${params.containerRegistryAuthId}`, "DELETE" ) return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], } } ) // Start receiving messages on stdin and sending messages on stdout const transport = new StdioServerTransport() server.connect(transport) ```