diff --git a/.changeset/catalog-availability-product.md b/.changeset/catalog-availability-product.md deleted file mode 100644 index 57c693a..0000000 --- a/.changeset/catalog-availability-product.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@runpod/mcp-server': minor ---- - -`list-gpu-types` and `get-gpu-type` now always send `product` with their `include=AVAILABILITY` catalog requests, via a new optional `product` parameter (`POD` | `CLUSTER` | `SERVERLESS`, default `POD`). Availability is product-specific — the same GPU can be scarce for Pods and plentiful for Serverless — and the next v2 API release makes `product` required with availability (400 without it), so this keeps the availability lookups working and lets agents ask for the context they actually deploy to: pass `SERVERLESS` when picking a GPU for an endpoint, `CLUSTER` for Instant Clusters. When `includeAvailability` is false, `product` is not sent (the API also rejects `product` without `include=AVAILABILITY`). The CPU catalog tools never request availability, so they are unaffected. diff --git a/.changeset/create-pod-ssh-public-key.md b/.changeset/create-pod-ssh-public-key.md deleted file mode 100644 index f76f842..0000000 --- a/.changeset/create-pod-ssh-public-key.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@runpod/mcp-server': minor ---- - -Add an `sshPublicKey` parameter to `create-pod` for full SSH access (#73), and fix v2 GPU pod creation failing without an explicit `gpuCount`. - -The REST API has no SSH switch yet (the console and runpodctl set it via GraphQL `startSsh`), so Pods created through the MCP got a valid-looking port-22 mapping but no authorized key — direct SSH, SCP, SFTP, and rsync all failed. Passing `sshPublicKey` now merges the key into the `PUBLIC_KEY` environment variable and ensures `22/tcp` is exposed, which any image honoring the `PUBLIC_KEY` convention (all runpod/\* official images) turns into a running sshd with the key installed. Template deploys extend the template's ports and env rather than replacing them, and an existing `PUBLIC_KEY` is appended to rather than overwritten. - -The key is validated before the Pod is created, because an unusable value would otherwise produce a Pod that looks SSH-ready and is not: `22/tcp` exposed, junk in `PUBLIC_KEY`, and a reply reporting SSH as configured. Rejected with a 400: a private key in any format (PEM in any case, or a PuTTY `.ppk`, which contains neither the words "private key" nor a public key line), a file path or SHA256 fingerprint passed instead of the `.pub` file's contents, a bare base64 blob with no key type, and an empty or whitespace-only value (omit the parameter to create a Pod without SSH). Accepted keys are normalized to one key per line with CRLF stripped, since a stray carriage return corrupts the `authorized_keys` entry it lands on. Error messages never echo the rejected value, which may itself be secret. - -`create-pod` also now rejects a `gpuCount` below 1 or with a fractional part, which the v2 API answers with an opaque 422. - -Separately, the v2 pod-create mapper omitted `gpu.count` when the caller passed no `gpuCount`, trusting the spec's documented server-side default of 1. In practice the scheduler matches zero machines without it and every create fails with a misleading "no instances available" error (verified live against both v2 hosts). The mapper now always emits `count`, defaulting to 1. diff --git a/.changeset/http-long-poll-budgets.md b/.changeset/http-long-poll-budgets.md deleted file mode 100644 index 526f059..0000000 --- a/.changeset/http-long-poll-budgets.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@runpod/mcp-server': minor ---- - -Clamp long-poll tool budgets on the hosted HTTP server, which runs behind a 60-second Vercel function limit. `runsync-endpoint` waited 90 seconds by default (300 via `wait`) and `stream-job` polled for up to 5 minutes, so for a slow job the function was reaped mid-flight: the caller got a bare 504 and every chunk collected so far was discarded. - -Over HTTP, `runsync-endpoint` now sends a `wait` capped at 45000 ms — a job that outlives it comes back as a job ID plus a non-terminal status to poll with `get-job-status`. `stream-job` stops after 45 seconds over HTTP and returns what it has with `pollingTimedOut: true`; `/stream` drains what it hands out, so calling again resumes rather than replaying. Its HTTP polls also send `wait=1000`, since the server otherwise holds an empty response for 10 seconds and the budget is only checked between polls. Both tool descriptions are now written per transport, so a caller is told the one budget that applies to them and where to go for more — `run-endpoint` + `get-job-status`, or the runtime API directly. The stdio server has no deadline and is unchanged. diff --git a/.changeset/per-request-timeouts.md b/.changeset/per-request-timeouts.md deleted file mode 100644 index b54f0a9..0000000 --- a/.changeset/per-request-timeouts.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@runpod/mcp-server': minor ---- - -Give every outbound request a client-side deadline — every tool call, the OAuth handshake on the hosted server, and the install wizard's key check. node-fetch applies no timeout of its own, so a Runpod host that accepted the connection and then went silent — a wedged worker, a load balancer holding the socket — left a tool call pending forever, and on the hosted server that ended as a bare 504 when Vercel reaped the function at its 60s limit. - -Requests now abort after 30 seconds with a named `RequestTimeoutError` naming the API that went quiet, the deadline it was given, and what to do next. `runsync-endpoint` is the one call that legitimately asks the server to hold a connection open, so it derives its deadline from the `wait` it requested (the server's own 90-second default when `wait` is omitted) rather than being truncated. Successful tool output is unchanged. - -On the hosted transport the cap is a single budget for the whole invocation rather than a fresh allowance per request, because several tools make more than one call — `get-job-status` adds a queued-job diagnosis, `deploy-hub-repo` and `set-endpoint-gpus` read before they write, `update-endpoint` reads the current scaler before patching it — and two full deadlines back to back outlived the platform even with each one bounded. Each request is clamped to what is left, so a stall anywhere in a handler surfaces as the named error instead of a 504. The queued-job diagnosis, which only decorates a status that is already in hand, is bounded at 5 seconds so it cannot spend a budget the reply itself needs. - -`stream-job` bounds each poll by the wait the server was asked to hold rather than by its whole budget. A deadline set to the budget meant one wedged socket consumed the entire run in a single attempt — 45 seconds on the hosted server, five minutes on stdio — so the loop's retry path never ran and a stall returned nothing at all. Each poll now gets the hold plus a round trip, which both clears a reply already in flight and leaves the budget room to reconnect; a run that ends on the budget reports `pollingTimedOut` with the last error rather than discarding it. - -The hosted OAuth routes are bounded too. `/token` polls the flash backend for an approval, and one silent socket there hung the whole handshake until the platform reaped it — the worst place for a blank error, since the user has no credential yet to retry with. Each backend call now has its own 10-second deadline (override with `MCP_FLASH_TIMEOUT_MS`), never exceeding what is left of a 45-second poll budget, and a stall is reported as a named error naming the operation, the host and the deadline. The `runpod-mcp install` wizard's key verification gets the same treatment, so it can no longer sit at "Verifying…" indefinitely. - -A timed-out GraphQL read is also no longer described as a possible write. The advice keys off the HTTP method and GraphQL is always POST on the wire, so `list-gpu-types` timing out used to tell the agent the call may have landed and to "check with the matching list-/get- tool first" — which is the tool that just failed. Only actual mutations carry that warning now. diff --git a/.changeset/refuse-standalone-get-sse.md b/.changeset/refuse-standalone-get-sse.md deleted file mode 100644 index e8d9456..0000000 --- a/.changeset/refuse-standalone-get-sse.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@runpod/mcp-server': patch ---- - -Refuse the standalone GET SSE stream with 405. The HTTP server is stateless and never sends server-initiated messages, so the GET "listen" stream has nothing to carry — but the SDK accepted it anyway and held the response open with nothing to send until the platform's maxDuration killed it, at which point every connected client immediately re-opened it. On the hosted deployment that loop was ~1.1M hung requests per day (90% of all traffic), each ending in a 60-second timeout, and it dominated the serverless bill. GET now gets the spec's answer for a server without an SSE stream: 405 Method Not Allowed with an `Allow: POST, DELETE` header and a JSON-RPC error body, before any auth work — a 401 there would send OAuth clients into a pointless re-auth flow. Clients per the MCP spec treat the 405 as "no server-initiated messages offered" and continue POST-only; tool calls are unaffected. diff --git a/.changeset/windows-claude-detection.md b/.changeset/windows-claude-detection.md deleted file mode 100644 index dfbf090..0000000 --- a/.changeset/windows-claude-detection.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@runpod/mcp-server': patch ---- - -Fix Claude Code detection and registration in the install wizard on Windows (#56). - -Detection used the POSIX-only `command -v claude` plus three POSIX install paths, so -Claude Code was undetectable on a standard Windows install. Windows now probes -`%USERPROFILE%\.local\bin\claude.exe` (native installer) and `%APPDATA%\npm\claude.cmd` -(global npm), and registration resolves the real entrypoint from the installed package -manifest instead of running the `.cmd` shim through `cmd.exe`. Candidates are -canonicalized and confined to their standard install roots, a PATH result resolving into -the current project is rejected, and Claude Code is spawned with `PATH`, `COMSPEC`, -`PATHEXT`, `NODE_OPTIONS`, and `NODE_PATH` sanitized, so nothing a project directory can -plant receives your API key. Custom npm prefixes and redirected roaming profiles are -still not auto-detected; when Claude Code is not found, the wizard now prints the exact -`claude mcp` command to register or remove the entry yourself. - -Claude Code add and remove are now verified against its own `.claude.json` rather than -the CLI exit code, which reports success for writes it never made. Re-running the wizard -over an existing entry now says the entry was left unchanged instead of printing a bare -success, since the CLI does not update it and a rotated API key would otherwise look -applied. An entry shadowed by a local-scope one is called out with the command to clear -it. - -For the clients that own a JSON config, an edit that would leave the file unparseable — -or land in a duplicated server block the client does not read — now fails with the -original file untouched instead of reporting success, and validity is judged by the -parser each client actually ships (Claude Desktop is strict JSON, Cursor and VS Code -accept JSONC, Windsurf's tolerance is unverified and disclosed rather than assumed). -Claude Desktop on Linux and VS Code now honour `XDG_CONFIG_HOME` instead of writing -where those clients never read, an empty or relative `APPDATA`/`XDG_CONFIG_HOME` no -longer resolves a config path against the current directory, configs this wizard creates -are `0600` because they hold a plaintext API key, and a key taken from the environment is -trimmed. diff --git a/CHANGELOG.md b/CHANGELOG.md index 880e25f..b23f53e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,71 @@ # @runpod/mcp-server +## 3.3.0 + +### Minor Changes + +- a62a663: `list-gpu-types` and `get-gpu-type` now always send `product` with their `include=AVAILABILITY` catalog requests, via a new optional `product` parameter (`POD` | `CLUSTER` | `SERVERLESS`, default `POD`). Availability is product-specific — the same GPU can be scarce for Pods and plentiful for Serverless — and the next v2 API release makes `product` required with availability (400 without it), so this keeps the availability lookups working and lets agents ask for the context they actually deploy to: pass `SERVERLESS` when picking a GPU for an endpoint, `CLUSTER` for Instant Clusters. When `includeAvailability` is false, `product` is not sent (the API also rejects `product` without `include=AVAILABILITY`). The CPU catalog tools never request availability, so they are unaffected. +- 7074c87: Add an `sshPublicKey` parameter to `create-pod` for full SSH access (#73), and fix v2 GPU pod creation failing without an explicit `gpuCount`. + + The REST API has no SSH switch yet (the console and runpodctl set it via GraphQL `startSsh`), so Pods created through the MCP got a valid-looking port-22 mapping but no authorized key — direct SSH, SCP, SFTP, and rsync all failed. Passing `sshPublicKey` now merges the key into the `PUBLIC_KEY` environment variable and ensures `22/tcp` is exposed, which any image honoring the `PUBLIC_KEY` convention (all runpod/\* official images) turns into a running sshd with the key installed. Template deploys extend the template's ports and env rather than replacing them, and an existing `PUBLIC_KEY` is appended to rather than overwritten. + + The key is validated before the Pod is created, because an unusable value would otherwise produce a Pod that looks SSH-ready and is not: `22/tcp` exposed, junk in `PUBLIC_KEY`, and a reply reporting SSH as configured. Rejected with a 400: a private key in any format (PEM in any case, or a PuTTY `.ppk`, which contains neither the words "private key" nor a public key line), a file path or SHA256 fingerprint passed instead of the `.pub` file's contents, a bare base64 blob with no key type, and an empty or whitespace-only value (omit the parameter to create a Pod without SSH). Accepted keys are normalized to one key per line with CRLF stripped, since a stray carriage return corrupts the `authorized_keys` entry it lands on. Error messages never echo the rejected value, which may itself be secret. + + `create-pod` also now rejects a `gpuCount` below 1 or with a fractional part, which the v2 API answers with an opaque 422. + + Separately, the v2 pod-create mapper omitted `gpu.count` when the caller passed no `gpuCount`, trusting the spec's documented server-side default of 1. In practice the scheduler matches zero machines without it and every create fails with a misleading "no instances available" error (verified live against both v2 hosts). The mapper now always emits `count`, defaulting to 1. + +- 9176c78: Clamp long-poll tool budgets on the hosted HTTP server, which runs behind a 60-second Vercel function limit. `runsync-endpoint` waited 90 seconds by default (300 via `wait`) and `stream-job` polled for up to 5 minutes, so for a slow job the function was reaped mid-flight: the caller got a bare 504 and every chunk collected so far was discarded. + + Over HTTP, `runsync-endpoint` now sends a `wait` capped at 45000 ms — a job that outlives it comes back as a job ID plus a non-terminal status to poll with `get-job-status`. `stream-job` stops after 45 seconds over HTTP and returns what it has with `pollingTimedOut: true`; `/stream` drains what it hands out, so calling again resumes rather than replaying. Its HTTP polls also send `wait=1000`, since the server otherwise holds an empty response for 10 seconds and the budget is only checked between polls. Both tool descriptions are now written per transport, so a caller is told the one budget that applies to them and where to go for more — `run-endpoint` + `get-job-status`, or the runtime API directly. The stdio server has no deadline and is unchanged. + +- f20d987: Give every outbound request a client-side deadline — every tool call, the OAuth handshake on the hosted server, and the install wizard's key check. node-fetch applies no timeout of its own, so a Runpod host that accepted the connection and then went silent — a wedged worker, a load balancer holding the socket — left a tool call pending forever, and on the hosted server that ended as a bare 504 when Vercel reaped the function at its 60s limit. + + Requests now abort after 30 seconds with a named `RequestTimeoutError` naming the API that went quiet, the deadline it was given, and what to do next. `runsync-endpoint` is the one call that legitimately asks the server to hold a connection open, so it derives its deadline from the `wait` it requested (the server's own 90-second default when `wait` is omitted) rather than being truncated. Successful tool output is unchanged. + + On the hosted transport the cap is a single budget for the whole invocation rather than a fresh allowance per request, because several tools make more than one call — `get-job-status` adds a queued-job diagnosis, `deploy-hub-repo` and `set-endpoint-gpus` read before they write, `update-endpoint` reads the current scaler before patching it — and two full deadlines back to back outlived the platform even with each one bounded. Each request is clamped to what is left, so a stall anywhere in a handler surfaces as the named error instead of a 504. The queued-job diagnosis, which only decorates a status that is already in hand, is bounded at 5 seconds so it cannot spend a budget the reply itself needs. + + `stream-job` bounds each poll by the wait the server was asked to hold rather than by its whole budget. A deadline set to the budget meant one wedged socket consumed the entire run in a single attempt — 45 seconds on the hosted server, five minutes on stdio — so the loop's retry path never ran and a stall returned nothing at all. Each poll now gets the hold plus a round trip, which both clears a reply already in flight and leaves the budget room to reconnect; a run that ends on the budget reports `pollingTimedOut` with the last error rather than discarding it. + + The hosted OAuth routes are bounded too. `/token` polls the flash backend for an approval, and one silent socket there hung the whole handshake until the platform reaped it — the worst place for a blank error, since the user has no credential yet to retry with. Each backend call now has its own 10-second deadline (override with `MCP_FLASH_TIMEOUT_MS`), never exceeding what is left of a 45-second poll budget, and a stall is reported as a named error naming the operation, the host and the deadline. The `runpod-mcp install` wizard's key verification gets the same treatment, so it can no longer sit at "Verifying…" indefinitely. + + A timed-out GraphQL read is also no longer described as a possible write. The advice keys off the HTTP method and GraphQL is always POST on the wire, so `list-gpu-types` timing out used to tell the agent the call may have landed and to "check with the matching list-/get- tool first" — which is the tool that just failed. Only actual mutations carry that warning now. + +### Patch Changes + +- 2b6480d: Refuse the standalone GET SSE stream with 405. The HTTP server is stateless and never sends server-initiated messages, so the GET "listen" stream has nothing to carry — but the SDK accepted it anyway and held the response open with nothing to send until the platform's maxDuration killed it, at which point every connected client immediately re-opened it. On the hosted deployment that loop was ~1.1M hung requests per day (90% of all traffic), each ending in a 60-second timeout, and it dominated the serverless bill. GET now gets the spec's answer for a server without an SSE stream: 405 Method Not Allowed with an `Allow: POST, DELETE` header and a JSON-RPC error body, before any auth work — a 401 there would send OAuth clients into a pointless re-auth flow. Clients per the MCP spec treat the 405 as "no server-initiated messages offered" and continue POST-only; tool calls are unaffected. +- 674fa2c: Fix Claude Code detection and registration in the install wizard on Windows (#56). + + Detection used the POSIX-only `command -v claude` plus three POSIX install paths, so + Claude Code was undetectable on a standard Windows install. Windows now probes + `%USERPROFILE%\.local\bin\claude.exe` (native installer) and `%APPDATA%\npm\claude.cmd` + (global npm), and registration resolves the real entrypoint from the installed package + manifest instead of running the `.cmd` shim through `cmd.exe`. Candidates are + canonicalized and confined to their standard install roots, a PATH result resolving into + the current project is rejected, and Claude Code is spawned with `PATH`, `COMSPEC`, + `PATHEXT`, `NODE_OPTIONS`, and `NODE_PATH` sanitized, so nothing a project directory can + plant receives your API key. Custom npm prefixes and redirected roaming profiles are + still not auto-detected; when Claude Code is not found, the wizard now prints the exact + `claude mcp` command to register or remove the entry yourself. + + Claude Code add and remove are now verified against its own `.claude.json` rather than + the CLI exit code, which reports success for writes it never made. Re-running the wizard + over an existing entry now says the entry was left unchanged instead of printing a bare + success, since the CLI does not update it and a rotated API key would otherwise look + applied. An entry shadowed by a local-scope one is called out with the command to clear + it. + + For the clients that own a JSON config, an edit that would leave the file unparseable — + or land in a duplicated server block the client does not read — now fails with the + original file untouched instead of reporting success, and validity is judged by the + parser each client actually ships (Claude Desktop is strict JSON, Cursor and VS Code + accept JSONC, Windsurf's tolerance is unverified and disclosed rather than assumed). + Claude Desktop on Linux and VS Code now honour `XDG_CONFIG_HOME` instead of writing + where those clients never read, an empty or relative `APPDATA`/`XDG_CONFIG_HOME` no + longer resolves a config path against the current directory, configs this wizard creates + are `0600` because they hold a plaintext API key, and a key taken from the environment is + trimmed. + ## 3.2.0 ### Minor Changes diff --git a/package.json b/package.json index 1124229..a565b44 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@runpod/mcp-server", - "version": "3.2.0", + "version": "3.3.0", "description": "MCP server for interacting with Runpod API", "license": "Apache-2.0", "type": "module",