@runpod/mcp-server 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/http.mjs CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  restV2Base,
6
6
  restVersionSummary,
7
7
  serverlessBase
8
- } from "./chunk-GTBCNGHE.mjs";
8
+ } from "./chunk-OM6RHPQC.mjs";
9
9
 
10
10
  // src/http.ts
11
11
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -13,7 +13,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
13
13
  // src/server.ts
14
14
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15
15
  var SERVER_NAME = "Runpod API Server";
16
- var SERVER_VERSION = true ? "3.0.0" : "dev";
16
+ var SERVER_VERSION = true ? "3.1.0" : "dev";
17
17
  function createServer(version = SERVER_VERSION) {
18
18
  return new McpServer({
19
19
  name: SERVER_NAME,
package/dist/stdio.cjs CHANGED
@@ -506,15 +506,27 @@ function mapPodUpdateToV2(params) {
506
506
  ...compact({ name: params.name })
507
507
  };
508
508
  }
509
- function nestedOrUndefined(obj) {
510
- const c = compact(obj);
511
- return Object.keys(c).length ? c : void 0;
509
+ var DEFAULT_ENDPOINT_TYPE = "QUEUE";
510
+ var DEFAULT_SCALER_VALUE = 4;
511
+ function defaultScalerType(endpointType) {
512
+ return endpointType === "LOAD_BALANCER" ? "REQUEST_COUNT" : "QUEUE_DELAY";
513
+ }
514
+ function endpointScaling(scalerType, value) {
515
+ return scalerType === "REQUEST_COUNT" ? { type: "REQUEST_COUNT", requestCount: value } : { type: "QUEUE_DELAY", queueDelay: value };
512
516
  }
513
517
  function endpointGpuConfig(pools, count) {
514
518
  if (!pools?.length) return void 0;
515
519
  return compact({ pools, count });
516
520
  }
517
- function mapEndpointToV2(params) {
521
+ function endpointWorkers(params) {
522
+ const workers = compact({
523
+ min: params.workersMin,
524
+ max: params.workersMax,
525
+ idleTimeout: params.idleTimeout
526
+ });
527
+ return Object.keys(workers).length ? workers : void 0;
528
+ }
529
+ function endpointCommonToV2(params) {
518
530
  return compact({
519
531
  name: params.name,
520
532
  image: params.imageName,
@@ -524,23 +536,33 @@ function mapEndpointToV2(params) {
524
536
  env: params.env,
525
537
  registry: params.containerRegistryAuthId,
526
538
  gpu: endpointGpuConfig(params.gpuPoolIds, params.gpuCount),
527
- workers: nestedOrUndefined({
528
- min: params.workersMin,
529
- max: params.workersMax
530
- }),
531
- scaling: nestedOrUndefined({
532
- type: params.scalerType,
533
- value: params.scalerValue,
534
- idleTimeout: params.idleTimeout
535
- }),
539
+ workers: endpointWorkers(params),
536
540
  dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
537
541
  networkVolumes: params.networkVolumeIds?.length ? params.networkVolumeIds : void 0,
538
542
  timeout: params.executionTimeoutMs,
539
543
  flashboot: params.flashboot
540
544
  });
541
545
  }
542
- var mapEndpointCreateToV2 = mapEndpointToV2;
543
- var mapEndpointUpdateToV2 = mapEndpointToV2;
546
+ function mapEndpointCreateToV2(params) {
547
+ const endpointType = params.endpointType ?? DEFAULT_ENDPOINT_TYPE;
548
+ return compact({
549
+ ...endpointCommonToV2(params),
550
+ type: endpointType,
551
+ scaling: endpointScaling(
552
+ params.scalerType ?? defaultScalerType(endpointType),
553
+ params.scalerValue ?? DEFAULT_SCALER_VALUE
554
+ )
555
+ });
556
+ }
557
+ function mapEndpointUpdateToV2(params) {
558
+ return compact({
559
+ ...endpointCommonToV2(params),
560
+ scaling: params.scalerType ? endpointScaling(
561
+ params.scalerType,
562
+ params.scalerValue ?? DEFAULT_SCALER_VALUE
563
+ ) : void 0
564
+ });
565
+ }
544
566
  function mapNetworkVolumeCreateToV2(params) {
545
567
  return compact({
546
568
  name: params.name,
@@ -846,7 +868,7 @@ function resolveBackend(opts) {
846
868
 
847
869
  // src/server.ts
848
870
  var SERVER_NAME = "Runpod API Server";
849
- var SERVER_VERSION = true ? "3.0.0" : "dev";
871
+ var SERVER_VERSION = true ? "3.1.0" : "dev";
850
872
  function createServer(version = SERVER_VERSION) {
851
873
  return new import_mcp.McpServer({
852
874
  name: SERVER_NAME,
@@ -1023,7 +1045,7 @@ async function streamLogsReply(rt, tool, params) {
1023
1045
  }
1024
1046
 
1025
1047
  // src/tools/runtime.ts
1026
- var MCP_SERVER_VERSION = true ? "3.0.0" : "dev";
1048
+ var MCP_SERVER_VERSION = true ? "3.1.0" : "dev";
1027
1049
  var SESSION_ID = (0, import_node_crypto.randomUUID)();
1028
1050
  function trackingHeaders(server, ctx) {
1029
1051
  const info = server.server.getClientVersion();
@@ -2409,11 +2431,22 @@ function registerPodTools(server, rt) {
2409
2431
 
2410
2432
  // src/tools/endpoints.ts
2411
2433
  var import_zod8 = require("zod");
2434
+ var MIN_QUEUE_DELAY = 0.5;
2435
+ var MIN_REQUEST_COUNT = 1;
2436
+ var MIN_IDLE_TIMEOUT = 1;
2437
+ var MAX_IDLE_TIMEOUT = 3600;
2438
+ function scalerValueError(scalerType, scalerValue) {
2439
+ if (scalerType !== "REQUEST_COUNT" || scalerValue === void 0) return;
2440
+ if (!Number.isInteger(scalerValue) || scalerValue < MIN_REQUEST_COUNT) {
2441
+ return `REQUEST_COUNT scales on whole in-flight requests per worker, so scalerValue must be an integer >= ${MIN_REQUEST_COUNT} (got ${scalerValue}). For a fractional target use scalerType QUEUE_DELAY, which takes seconds >= ${MIN_QUEUE_DELAY}.`;
2442
+ }
2443
+ return;
2444
+ }
2412
2445
  function registerEndpointTools(server, rt) {
2413
2446
  const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
2414
2447
  server.tool(
2415
2448
  "list-endpoints",
2416
- "List your Serverless endpoints, optionally expanding template and worker details (v1 only). Paginated via limit/cursor.",
2449
+ "List your Serverless endpoints, optionally expanding template and worker details (v1 only). Paginated via limit/cursor. On v2 each endpoint carries its request routing in `type` and the URLs to call it in `requestUrls`.",
2417
2450
  {
2418
2451
  ...listPaginationParams,
2419
2452
  includeTemplate: import_zod8.z.boolean().optional().describe("Include template information (v1 only)"),
@@ -2448,7 +2481,7 @@ function registerEndpointTools(server, rt) {
2448
2481
  );
2449
2482
  server.tool(
2450
2483
  "get-endpoint",
2451
- "Get one Serverless endpoint by id, optionally expanding template and worker details (v1 only).",
2484
+ "Get one Serverless endpoint by id, optionally expanding template and worker details (v1 only). On v2 the reply carries `type` (queue-based vs load-balancing routing) and `requestUrls` \u2014 the run/runsync/status/... URLs for a queue endpoint, or the base + health URLs for a load-balancing one. Read those rather than constructing endpoint URLs by hand.",
2452
2485
  {
2453
2486
  endpointId: import_zod8.z.string().describe("ID of the endpoint to retrieve"),
2454
2487
  includeTemplate: import_zod8.z.boolean().optional().describe("Include template information (v1 only)"),
@@ -2480,11 +2513,14 @@ function registerEndpointTools(server, rt) {
2480
2513
  );
2481
2514
  server.tool(
2482
2515
  "create-endpoint",
2483
- "Create a Serverless endpoint. On v2 (default), pass an inline config: imageName + gpuPoolIds (GPU pool names from list-gpu-types \u2014 the `pool` field, e.g. AMPERE_80/ADA_24) plus optional workers/scaling/disk/env. On v1, pass a templateId instead. Worker min/max set autoscaling bounds (min 0 = scale to zero).",
2516
+ "Create a Serverless endpoint. On v2 (default), pass an inline config: imageName + gpuPoolIds (GPU pool names from list-gpu-types \u2014 the `pool` field, e.g. AMPERE_80/ADA_24) plus optional workers/scaling/disk/env. On v1, pass a templateId instead. Worker min/max set autoscaling bounds (min 0 = scale to zero). Use endpointType to pick queue-based (default) or load-balancing request routing; the response `requestUrls` carries the URLs to call the endpoint with.",
2484
2517
  {
2485
2518
  name: import_zod8.z.string().optional().describe("Name for the endpoint"),
2486
2519
  // --- v2 inline-config fields ---
2487
2520
  imageName: import_zod8.z.string().optional().describe("Docker image (v2). Required on v2 instead of a templateId."),
2521
+ endpointType: import_zod8.z.enum(["QUEUE", "LOAD_BALANCER"]).optional().describe(
2522
+ "How requests reach the workers (v2). QUEUE (default) = submit jobs through the managed queue (run/runsync/status/...). LOAD_BALANCER = send HTTP requests straight to worker-defined paths, and forces scalerType REQUEST_COUNT. Fixed at creation \u2014 update-endpoint cannot change it."
2523
+ ),
2488
2524
  gpuPoolIds: import_zod8.z.array(import_zod8.z.string()).optional().describe(
2489
2525
  'GPU pool names (v2, required). The `pool` field from list-gpu-types, e.g. ["AMPERE_80"]. NOT GPU type ids.'
2490
2526
  ),
@@ -2504,9 +2540,15 @@ function registerEndpointTools(server, rt) {
2504
2540
  // --- shared autoscaling fields ---
2505
2541
  workersMin: import_zod8.z.number().optional().describe("Minimum number of workers"),
2506
2542
  workersMax: import_zod8.z.number().optional().describe("Maximum number of workers"),
2507
- scalerType: import_zod8.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type"),
2508
- scalerValue: import_zod8.z.number().optional().describe("Autoscaler target value"),
2509
- idleTimeout: import_zod8.z.number().optional().describe("Idle timeout in seconds before scaling a worker down"),
2543
+ scalerType: import_zod8.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe(
2544
+ "Autoscaler signal. QUEUE_DELAY scales on how long requests wait in the queue (queue endpoints only); REQUEST_COUNT scales on in-flight requests per worker. Defaults to QUEUE_DELAY for QUEUE endpoints and REQUEST_COUNT for LOAD_BALANCER ones."
2545
+ ),
2546
+ scalerValue: import_zod8.z.number().min(MIN_QUEUE_DELAY).optional().describe(
2547
+ "Autoscaler target for the chosen scalerType \u2014 seconds of queue delay (min 0.5) or in-flight requests per worker (integer, min 1). Defaults to 4."
2548
+ ),
2549
+ idleTimeout: import_zod8.z.number().int().min(MIN_IDLE_TIMEOUT).max(MAX_IDLE_TIMEOUT).optional().describe(
2550
+ "Idle timeout in seconds before scaling a worker down (1-3600). Does not apply to QUEUE endpoints scaling on REQUEST_COUNT."
2551
+ ),
2510
2552
  dataCenterIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("List of preferred data centers")
2511
2553
  },
2512
2554
  { title: "Create endpoint", ...WRITE },
@@ -2531,6 +2573,19 @@ function registerEndpointTools(server, rt) {
2531
2573
  status: 400
2532
2574
  });
2533
2575
  }
2576
+ if (params.endpointType === "LOAD_BALANCER" && params.scalerType === "QUEUE_DELAY") {
2577
+ return jsonReply2({
2578
+ error: "LOAD_BALANCER endpoints have no request queue, so they cannot scale on QUEUE_DELAY. Use scalerType REQUEST_COUNT (the default for this endpoint type), or create a QUEUE endpoint instead.",
2579
+ status: 400
2580
+ });
2581
+ }
2582
+ const scalerError = scalerValueError(
2583
+ params.scalerType ?? (params.endpointType === "LOAD_BALANCER" ? "REQUEST_COUNT" : void 0),
2584
+ params.scalerValue
2585
+ );
2586
+ if (scalerError) {
2587
+ return jsonReply2({ error: scalerError, status: 400 });
2588
+ }
2534
2589
  const body = backend.mapCreate(params);
2535
2590
  const result2 = await callRestUrl(
2536
2591
  `${backend.base}${backend.list}`,
@@ -2568,15 +2623,21 @@ function registerEndpointTools(server, rt) {
2568
2623
  );
2569
2624
  server.tool(
2570
2625
  "update-endpoint",
2571
- "Update a Serverless endpoint's config. On v2 you can change image/disk/env/ports/registry/workers/scaling/networkVolumes/timeout/flashboot; on v1, scaling fields (worker min/max, idle timeout, scaler type/value, name). Only provided fields change.",
2626
+ "Update a Serverless endpoint's config. On v2 you can change image/disk/env/ports/registry/workers/scaling/networkVolumes/timeout/flashboot; on v1, scaling fields (worker min/max, idle timeout, scaler type/value, name). Only provided fields change. An endpoint's request routing (queue vs load balancer) is fixed at creation and cannot be changed here \u2014 recreate the endpoint instead.",
2572
2627
  {
2573
2628
  endpointId: import_zod8.z.string().describe("ID of the endpoint to update"),
2574
2629
  name: import_zod8.z.string().optional().describe("New name for the endpoint"),
2575
2630
  workersMin: import_zod8.z.number().optional().describe("New minimum number of workers"),
2576
2631
  workersMax: import_zod8.z.number().optional().describe("New maximum number of workers"),
2577
- idleTimeout: import_zod8.z.number().optional().describe("New idle timeout in seconds"),
2578
- scalerType: import_zod8.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Scaler type"),
2579
- scalerValue: import_zod8.z.number().optional().describe("Scaler value"),
2632
+ idleTimeout: import_zod8.z.number().int().min(MIN_IDLE_TIMEOUT).max(MAX_IDLE_TIMEOUT).optional().describe(
2633
+ "New idle timeout in seconds (1-3600). Does not apply to queue endpoints scaling on REQUEST_COUNT."
2634
+ ),
2635
+ scalerType: import_zod8.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe(
2636
+ "New autoscaler signal. Switchable on queue endpoints; load-balancing endpoints only accept REQUEST_COUNT."
2637
+ ),
2638
+ scalerValue: import_zod8.z.number().min(MIN_QUEUE_DELAY).optional().describe(
2639
+ "New autoscaler target \u2014 seconds of queue delay (min 0.5) or in-flight requests per worker (integer, min 1). Applies to the endpoint's current scalerType unless you also pass a new one."
2640
+ ),
2580
2641
  // --- v2-only inline-config fields ---
2581
2642
  imageName: import_zod8.z.string().optional().describe("New Docker image (v2)"),
2582
2643
  gpuPoolIds: import_zod8.z.array(import_zod8.z.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
@@ -2594,13 +2655,28 @@ function registerEndpointTools(server, rt) {
2594
2655
  async (params) => {
2595
2656
  const { endpointId, ...updateParams } = params;
2596
2657
  const backend = backendFor("endpoints");
2597
- const body = backend.mapUpdate(updateParams);
2598
- const result = await callRestUrl(
2599
- `${backend.base}${backend.get(endpointId)}`,
2600
- "PATCH",
2601
- body
2658
+ const url = `${backend.base}${backend.get(endpointId)}`;
2659
+ if (backend.version !== "v2") {
2660
+ const body2 = backend.mapUpdate(updateParams);
2661
+ return jsonReply2(await callRestUrl(url, "PATCH", body2));
2662
+ }
2663
+ let scalerType = updateParams.scalerType;
2664
+ if (scalerType === void 0 && updateParams.scalerValue !== void 0) {
2665
+ const current = await callRestUrl(url);
2666
+ scalerType = current?.scaling?.type === "REQUEST_COUNT" ? "REQUEST_COUNT" : "QUEUE_DELAY";
2667
+ }
2668
+ const scalerError = scalerValueError(
2669
+ scalerType,
2670
+ updateParams.scalerValue
2602
2671
  );
2603
- return jsonReply2(result);
2672
+ if (scalerError) {
2673
+ return jsonReply2({ error: scalerError, status: 400 });
2674
+ }
2675
+ const body = backend.mapUpdate({
2676
+ ...updateParams,
2677
+ scalerType
2678
+ });
2679
+ return jsonReply2(await callRestUrl(url, "PATCH", body));
2604
2680
  }
2605
2681
  );
2606
2682
  server.tool(
@@ -2648,7 +2724,7 @@ function registerEndpointTools(server, rt) {
2648
2724
  );
2649
2725
  server.tool(
2650
2726
  "list-endpoint-workers",
2651
- "List the workers backing a Serverless endpoint, with their status and an aggregate summary. v2-only \u2014 returns a 501 notice on the v1 API. Paginated via limit/cursor.",
2727
+ "List the workers backing a Serverless endpoint, with their status and an aggregate summary. v2-only \u2014 returns a 501 notice on the v1 API. An UNHEALTHY worker usually means the container is crash-looping (repeated starts that exit before the handler runs) \u2014 jobs on the endpoint will sit IN_QUEUE even though this is a worker failure, not a capacity shortage; inspect it with stream-worker-logs. Zero workers while jobs queue means the endpoint is waiting for GPU capacity. Paginated via limit/cursor.",
2652
2728
  {
2653
2729
  ...listPaginationParams,
2654
2730
  endpointId: import_zod8.z.string().describe("ID of the Serverless endpoint whose workers to list")
@@ -2674,7 +2750,7 @@ function registerEndpointTools(server, rt) {
2674
2750
  );
2675
2751
  server.tool(
2676
2752
  "stream-worker-logs",
2677
- "Fetch a bounded snapshot of a serverless worker's live logs (container and/or system) via Server-Sent Events. v2-only \u2014 returns a 501 notice on the v1 API. Get the workerId from list-endpoint-workers. Reads for up to maxWaitMs (default 5s) and returns the collected log lines; use `tail` to backfill recent lines first. Large output is truncated (see the `truncated` flag).",
2753
+ "Fetch a bounded snapshot of a serverless worker's live logs (container and/or system) via Server-Sent Events. v2-only \u2014 returns a 501 notice on the v1 API. Get the workerId from list-endpoint-workers. Use this to diagnose UNHEALTHY workers: repeated `start container` system lines with no container output is the crash-loop signature (the container exits before the handler runs). Reads for up to maxWaitMs (default 5s) and returns the collected log lines; use `tail` to backfill recent lines first. Large output is truncated (see the `truncated` flag).",
2678
2754
  {
2679
2755
  endpointId: import_zod8.z.string().describe("ID of the Serverless endpoint the worker belongs to"),
2680
2756
  workerId: import_zod8.z.string().describe(
@@ -2697,8 +2773,67 @@ function registerEndpointTools(server, rt) {
2697
2773
 
2698
2774
  // src/tools/jobs.ts
2699
2775
  var import_zod9 = require("zod");
2776
+ var DIAGNOSIS_TTL_MS = 15e3;
2777
+ var MAX_DIAGNOSIS_ENTRIES = 500;
2778
+ var diagnosisCache = /* @__PURE__ */ new Map();
2700
2779
  function registerJobTools(server, rt) {
2701
- const { jsonReply: jsonReply2, serverlessRequest } = rt;
2780
+ const { jsonReply: jsonReply2, serverlessRequest, backendFor, callRestUrl } = rt;
2781
+ async function diagnoseQueuedJob(endpointId) {
2782
+ const cached = diagnosisCache.get(endpointId);
2783
+ if (cached && cached.expiresAt > Date.now()) {
2784
+ return cached.value;
2785
+ }
2786
+ diagnosisCache.delete(endpointId);
2787
+ try {
2788
+ const backend = backendFor("workers");
2789
+ if (backend.version !== "v2") return null;
2790
+ const raw = await callRestUrl(
2791
+ `${backend.base}/serverless/${endpointId}/workers`
2792
+ );
2793
+ const summary = raw?.summary;
2794
+ const workers = raw?.workers ?? [];
2795
+ if (!summary) return null;
2796
+ const remember = (value) => {
2797
+ if (diagnosisCache.size >= MAX_DIAGNOSIS_ENTRIES) {
2798
+ const oldest = diagnosisCache.keys().next().value;
2799
+ if (oldest !== void 0) diagnosisCache.delete(oldest);
2800
+ }
2801
+ diagnosisCache.set(endpointId, {
2802
+ value,
2803
+ expiresAt: Date.now() + DIAGNOSIS_TTL_MS
2804
+ });
2805
+ return value;
2806
+ };
2807
+ if ((summary.unhealthy ?? 0) > 0) {
2808
+ const badIds = workers.filter((w) => w.status === "UNHEALTHY").map((w) => w.id).join(", ");
2809
+ return remember({
2810
+ workerHealth: summary,
2811
+ hint: `${summary.unhealthy} of ${summary.total} worker(s) on this endpoint are UNHEALTHY \u2014 the container is likely crash-looping (repeated starts that exit before the handler runs). The job can stay IN_QUEUE indefinitely; this is a worker failure, NOT a capacity shortage. Inspect the logs with stream-worker-logs${badIds ? ` (workerId: ${badIds})` : ""}.`
2812
+ });
2813
+ }
2814
+ if ((summary.total ?? 0) === 0) {
2815
+ return remember({
2816
+ workerHealth: summary,
2817
+ hint: "No workers are scheduled for this endpoint yet \u2014 it may simply be scaling up from zero (normal for the first seconds of a cold start), or it is waiting for GPU capacity / its GPU-CUDA constraints exclude the currently-available hosts. If this persists, check availability with list-gpu-types or widen the gpuIds/CUDA settings."
2818
+ });
2819
+ }
2820
+ if ((summary.throttled ?? 0) > 0 && (summary.running ?? 0) === 0) {
2821
+ return remember({
2822
+ workerHealth: summary,
2823
+ hint: "Workers exist but are throttled \u2014 the hosts are at capacity right now; the job should start when capacity frees up."
2824
+ });
2825
+ }
2826
+ if ((summary.initializing ?? 0) > 0) {
2827
+ return remember({
2828
+ workerHealth: summary,
2829
+ hint: "A worker is initializing \u2014 likely a cold start (image pull / model load); the job should start soon."
2830
+ });
2831
+ }
2832
+ return remember({ workerHealth: summary, hint: "" });
2833
+ } catch {
2834
+ return null;
2835
+ }
2836
+ }
2702
2837
  const endpointIdSchema = import_zod9.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
2703
2838
  const jobIdSchema = import_zod9.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
2704
2839
  const inputSchema = import_zod9.z.record(import_zod9.z.unknown()).describe(
@@ -2772,7 +2907,7 @@ function registerJobTools(server, rt) {
2772
2907
  );
2773
2908
  server.tool(
2774
2909
  "get-job-status",
2775
- "Check the status of an asynchronous Serverless job. Returns the current status and output when complete. Job statuses: IN_QUEUE, IN_PROGRESS, COMPLETED, FAILED, CANCELLED, TIMED_OUT.",
2910
+ "Check the status of an asynchronous Serverless job. Returns the current status and output when complete. Job statuses: IN_QUEUE, IN_PROGRESS, COMPLETED, FAILED, CANCELLED, TIMED_OUT. IMPORTANT: a job stuck IN_QUEUE does not necessarily mean a capacity shortage \u2014 a worker may have spun up and crash-looped (the platform marks it UNHEALTHY while the job stays queued). When the job is IN_QUEUE this tool attaches a workerHealth summary and a hint; to dig deeper, call list-endpoint-workers (look for UNHEALTHY) and stream-worker-logs.",
2776
2911
  {
2777
2912
  endpointId: endpointIdSchema.describe(
2778
2913
  "ID of the Serverless endpoint the job belongs to"
@@ -2785,6 +2920,16 @@ function registerJobTools(server, rt) {
2785
2920
  params.endpointId,
2786
2921
  `/status/${params.jobId}`
2787
2922
  );
2923
+ if (result && typeof result === "object" && result.status === "IN_QUEUE") {
2924
+ const diagnosis = await diagnoseQueuedJob(params.endpointId);
2925
+ if (diagnosis) {
2926
+ return jsonReply2({
2927
+ ...result,
2928
+ workerHealth: diagnosis.workerHealth,
2929
+ ...diagnosis.hint ? { hint: diagnosis.hint } : {}
2930
+ });
2931
+ }
2932
+ }
2788
2933
  return jsonReply2(result);
2789
2934
  }
2790
2935
  );
@@ -2886,7 +3031,7 @@ function registerJobTools(server, rt) {
2886
3031
  );
2887
3032
  server.tool(
2888
3033
  "endpoint-health",
2889
- "Get the health and operational status of a Serverless endpoint, including worker counts and job statistics.",
3034
+ "Get the health and operational status of a Serverless endpoint, including worker counts and job statistics. Use this when jobs are stuck IN_QUEUE: unhealthy workers mean a crash-looping container (a worker failure \u2014 inspect with stream-worker-logs), while zero workers means the endpoint is waiting for GPU capacity.",
2890
3035
  {
2891
3036
  endpointId: endpointIdSchema.describe(
2892
3037
  "ID of the Serverless endpoint to check health for"