@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/CHANGELOG.md +28 -0
- package/README.md +59 -319
- package/dist/{chunk-GTBCNGHE.mjs → chunk-OM6RHPQC.mjs} +183 -38
- package/dist/chunk-OM6RHPQC.mjs.map +1 -0
- package/dist/http.cjs +183 -38
- package/dist/http.cjs.map +1 -1
- package/dist/http.mjs +2 -2
- package/dist/stdio.cjs +183 -38
- package/dist/stdio.cjs.map +1 -1
- package/dist/stdio.mjs +183 -38
- package/dist/stdio.mjs.map +1 -1
- package/dist/tools.cjs +182 -37
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.mjs +1 -1
- package/package.json +1 -1
- package/dist/chunk-GTBCNGHE.mjs.map +0 -1
package/dist/stdio.mjs
CHANGED
|
@@ -64,15 +64,27 @@ function mapPodUpdateToV2(params) {
|
|
|
64
64
|
...compact({ name: params.name })
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
var DEFAULT_ENDPOINT_TYPE = "QUEUE";
|
|
68
|
+
var DEFAULT_SCALER_VALUE = 4;
|
|
69
|
+
function defaultScalerType(endpointType) {
|
|
70
|
+
return endpointType === "LOAD_BALANCER" ? "REQUEST_COUNT" : "QUEUE_DELAY";
|
|
71
|
+
}
|
|
72
|
+
function endpointScaling(scalerType, value) {
|
|
73
|
+
return scalerType === "REQUEST_COUNT" ? { type: "REQUEST_COUNT", requestCount: value } : { type: "QUEUE_DELAY", queueDelay: value };
|
|
70
74
|
}
|
|
71
75
|
function endpointGpuConfig(pools, count) {
|
|
72
76
|
if (!pools?.length) return void 0;
|
|
73
77
|
return compact({ pools, count });
|
|
74
78
|
}
|
|
75
|
-
function
|
|
79
|
+
function endpointWorkers(params) {
|
|
80
|
+
const workers = compact({
|
|
81
|
+
min: params.workersMin,
|
|
82
|
+
max: params.workersMax,
|
|
83
|
+
idleTimeout: params.idleTimeout
|
|
84
|
+
});
|
|
85
|
+
return Object.keys(workers).length ? workers : void 0;
|
|
86
|
+
}
|
|
87
|
+
function endpointCommonToV2(params) {
|
|
76
88
|
return compact({
|
|
77
89
|
name: params.name,
|
|
78
90
|
image: params.imageName,
|
|
@@ -82,23 +94,33 @@ function mapEndpointToV2(params) {
|
|
|
82
94
|
env: params.env,
|
|
83
95
|
registry: params.containerRegistryAuthId,
|
|
84
96
|
gpu: endpointGpuConfig(params.gpuPoolIds, params.gpuCount),
|
|
85
|
-
workers:
|
|
86
|
-
min: params.workersMin,
|
|
87
|
-
max: params.workersMax
|
|
88
|
-
}),
|
|
89
|
-
scaling: nestedOrUndefined({
|
|
90
|
-
type: params.scalerType,
|
|
91
|
-
value: params.scalerValue,
|
|
92
|
-
idleTimeout: params.idleTimeout
|
|
93
|
-
}),
|
|
97
|
+
workers: endpointWorkers(params),
|
|
94
98
|
dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
|
|
95
99
|
networkVolumes: params.networkVolumeIds?.length ? params.networkVolumeIds : void 0,
|
|
96
100
|
timeout: params.executionTimeoutMs,
|
|
97
101
|
flashboot: params.flashboot
|
|
98
102
|
});
|
|
99
103
|
}
|
|
100
|
-
|
|
101
|
-
|
|
104
|
+
function mapEndpointCreateToV2(params) {
|
|
105
|
+
const endpointType = params.endpointType ?? DEFAULT_ENDPOINT_TYPE;
|
|
106
|
+
return compact({
|
|
107
|
+
...endpointCommonToV2(params),
|
|
108
|
+
type: endpointType,
|
|
109
|
+
scaling: endpointScaling(
|
|
110
|
+
params.scalerType ?? defaultScalerType(endpointType),
|
|
111
|
+
params.scalerValue ?? DEFAULT_SCALER_VALUE
|
|
112
|
+
)
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function mapEndpointUpdateToV2(params) {
|
|
116
|
+
return compact({
|
|
117
|
+
...endpointCommonToV2(params),
|
|
118
|
+
scaling: params.scalerType ? endpointScaling(
|
|
119
|
+
params.scalerType,
|
|
120
|
+
params.scalerValue ?? DEFAULT_SCALER_VALUE
|
|
121
|
+
) : void 0
|
|
122
|
+
});
|
|
123
|
+
}
|
|
102
124
|
function mapNetworkVolumeCreateToV2(params) {
|
|
103
125
|
return compact({
|
|
104
126
|
name: params.name,
|
|
@@ -404,7 +426,7 @@ function resolveBackend(opts) {
|
|
|
404
426
|
|
|
405
427
|
// src/server.ts
|
|
406
428
|
var SERVER_NAME = "Runpod API Server";
|
|
407
|
-
var SERVER_VERSION = true ? "3.
|
|
429
|
+
var SERVER_VERSION = true ? "3.1.0" : "dev";
|
|
408
430
|
function createServer(version = SERVER_VERSION) {
|
|
409
431
|
return new McpServer({
|
|
410
432
|
name: SERVER_NAME,
|
|
@@ -581,7 +603,7 @@ async function streamLogsReply(rt, tool, params) {
|
|
|
581
603
|
}
|
|
582
604
|
|
|
583
605
|
// src/tools/runtime.ts
|
|
584
|
-
var MCP_SERVER_VERSION = true ? "3.
|
|
606
|
+
var MCP_SERVER_VERSION = true ? "3.1.0" : "dev";
|
|
585
607
|
var SESSION_ID = randomUUID();
|
|
586
608
|
function trackingHeaders(server, ctx) {
|
|
587
609
|
const info = server.server.getClientVersion();
|
|
@@ -1967,11 +1989,22 @@ function registerPodTools(server, rt) {
|
|
|
1967
1989
|
|
|
1968
1990
|
// src/tools/endpoints.ts
|
|
1969
1991
|
import { z as z8 } from "zod";
|
|
1992
|
+
var MIN_QUEUE_DELAY = 0.5;
|
|
1993
|
+
var MIN_REQUEST_COUNT = 1;
|
|
1994
|
+
var MIN_IDLE_TIMEOUT = 1;
|
|
1995
|
+
var MAX_IDLE_TIMEOUT = 3600;
|
|
1996
|
+
function scalerValueError(scalerType, scalerValue) {
|
|
1997
|
+
if (scalerType !== "REQUEST_COUNT" || scalerValue === void 0) return;
|
|
1998
|
+
if (!Number.isInteger(scalerValue) || scalerValue < MIN_REQUEST_COUNT) {
|
|
1999
|
+
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}.`;
|
|
2000
|
+
}
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
1970
2003
|
function registerEndpointTools(server, rt) {
|
|
1971
2004
|
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
1972
2005
|
server.tool(
|
|
1973
2006
|
"list-endpoints",
|
|
1974
|
-
"List your Serverless endpoints, optionally expanding template and worker details (v1 only). Paginated via limit/cursor.",
|
|
2007
|
+
"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`.",
|
|
1975
2008
|
{
|
|
1976
2009
|
...listPaginationParams,
|
|
1977
2010
|
includeTemplate: z8.boolean().optional().describe("Include template information (v1 only)"),
|
|
@@ -2006,7 +2039,7 @@ function registerEndpointTools(server, rt) {
|
|
|
2006
2039
|
);
|
|
2007
2040
|
server.tool(
|
|
2008
2041
|
"get-endpoint",
|
|
2009
|
-
"Get one Serverless endpoint by id, optionally expanding template and worker details (v1 only).",
|
|
2042
|
+
"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.",
|
|
2010
2043
|
{
|
|
2011
2044
|
endpointId: z8.string().describe("ID of the endpoint to retrieve"),
|
|
2012
2045
|
includeTemplate: z8.boolean().optional().describe("Include template information (v1 only)"),
|
|
@@ -2038,11 +2071,14 @@ function registerEndpointTools(server, rt) {
|
|
|
2038
2071
|
);
|
|
2039
2072
|
server.tool(
|
|
2040
2073
|
"create-endpoint",
|
|
2041
|
-
"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).",
|
|
2074
|
+
"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.",
|
|
2042
2075
|
{
|
|
2043
2076
|
name: z8.string().optional().describe("Name for the endpoint"),
|
|
2044
2077
|
// --- v2 inline-config fields ---
|
|
2045
2078
|
imageName: z8.string().optional().describe("Docker image (v2). Required on v2 instead of a templateId."),
|
|
2079
|
+
endpointType: z8.enum(["QUEUE", "LOAD_BALANCER"]).optional().describe(
|
|
2080
|
+
"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."
|
|
2081
|
+
),
|
|
2046
2082
|
gpuPoolIds: z8.array(z8.string()).optional().describe(
|
|
2047
2083
|
'GPU pool names (v2, required). The `pool` field from list-gpu-types, e.g. ["AMPERE_80"]. NOT GPU type ids.'
|
|
2048
2084
|
),
|
|
@@ -2062,9 +2098,15 @@ function registerEndpointTools(server, rt) {
|
|
|
2062
2098
|
// --- shared autoscaling fields ---
|
|
2063
2099
|
workersMin: z8.number().optional().describe("Minimum number of workers"),
|
|
2064
2100
|
workersMax: z8.number().optional().describe("Maximum number of workers"),
|
|
2065
|
-
scalerType: z8.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe(
|
|
2066
|
-
|
|
2067
|
-
|
|
2101
|
+
scalerType: z8.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe(
|
|
2102
|
+
"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."
|
|
2103
|
+
),
|
|
2104
|
+
scalerValue: z8.number().min(MIN_QUEUE_DELAY).optional().describe(
|
|
2105
|
+
"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."
|
|
2106
|
+
),
|
|
2107
|
+
idleTimeout: z8.number().int().min(MIN_IDLE_TIMEOUT).max(MAX_IDLE_TIMEOUT).optional().describe(
|
|
2108
|
+
"Idle timeout in seconds before scaling a worker down (1-3600). Does not apply to QUEUE endpoints scaling on REQUEST_COUNT."
|
|
2109
|
+
),
|
|
2068
2110
|
dataCenterIds: z8.array(z8.string()).optional().describe("List of preferred data centers")
|
|
2069
2111
|
},
|
|
2070
2112
|
{ title: "Create endpoint", ...WRITE },
|
|
@@ -2089,6 +2131,19 @@ function registerEndpointTools(server, rt) {
|
|
|
2089
2131
|
status: 400
|
|
2090
2132
|
});
|
|
2091
2133
|
}
|
|
2134
|
+
if (params.endpointType === "LOAD_BALANCER" && params.scalerType === "QUEUE_DELAY") {
|
|
2135
|
+
return jsonReply2({
|
|
2136
|
+
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.",
|
|
2137
|
+
status: 400
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
const scalerError = scalerValueError(
|
|
2141
|
+
params.scalerType ?? (params.endpointType === "LOAD_BALANCER" ? "REQUEST_COUNT" : void 0),
|
|
2142
|
+
params.scalerValue
|
|
2143
|
+
);
|
|
2144
|
+
if (scalerError) {
|
|
2145
|
+
return jsonReply2({ error: scalerError, status: 400 });
|
|
2146
|
+
}
|
|
2092
2147
|
const body = backend.mapCreate(params);
|
|
2093
2148
|
const result2 = await callRestUrl(
|
|
2094
2149
|
`${backend.base}${backend.list}`,
|
|
@@ -2126,15 +2181,21 @@ function registerEndpointTools(server, rt) {
|
|
|
2126
2181
|
);
|
|
2127
2182
|
server.tool(
|
|
2128
2183
|
"update-endpoint",
|
|
2129
|
-
"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.",
|
|
2184
|
+
"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.",
|
|
2130
2185
|
{
|
|
2131
2186
|
endpointId: z8.string().describe("ID of the endpoint to update"),
|
|
2132
2187
|
name: z8.string().optional().describe("New name for the endpoint"),
|
|
2133
2188
|
workersMin: z8.number().optional().describe("New minimum number of workers"),
|
|
2134
2189
|
workersMax: z8.number().optional().describe("New maximum number of workers"),
|
|
2135
|
-
idleTimeout: z8.number().optional().describe(
|
|
2136
|
-
|
|
2137
|
-
|
|
2190
|
+
idleTimeout: z8.number().int().min(MIN_IDLE_TIMEOUT).max(MAX_IDLE_TIMEOUT).optional().describe(
|
|
2191
|
+
"New idle timeout in seconds (1-3600). Does not apply to queue endpoints scaling on REQUEST_COUNT."
|
|
2192
|
+
),
|
|
2193
|
+
scalerType: z8.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe(
|
|
2194
|
+
"New autoscaler signal. Switchable on queue endpoints; load-balancing endpoints only accept REQUEST_COUNT."
|
|
2195
|
+
),
|
|
2196
|
+
scalerValue: z8.number().min(MIN_QUEUE_DELAY).optional().describe(
|
|
2197
|
+
"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."
|
|
2198
|
+
),
|
|
2138
2199
|
// --- v2-only inline-config fields ---
|
|
2139
2200
|
imageName: z8.string().optional().describe("New Docker image (v2)"),
|
|
2140
2201
|
gpuPoolIds: z8.array(z8.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
|
|
@@ -2152,13 +2213,28 @@ function registerEndpointTools(server, rt) {
|
|
|
2152
2213
|
async (params) => {
|
|
2153
2214
|
const { endpointId, ...updateParams } = params;
|
|
2154
2215
|
const backend = backendFor("endpoints");
|
|
2155
|
-
const
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
"PATCH",
|
|
2159
|
-
|
|
2216
|
+
const url = `${backend.base}${backend.get(endpointId)}`;
|
|
2217
|
+
if (backend.version !== "v2") {
|
|
2218
|
+
const body2 = backend.mapUpdate(updateParams);
|
|
2219
|
+
return jsonReply2(await callRestUrl(url, "PATCH", body2));
|
|
2220
|
+
}
|
|
2221
|
+
let scalerType = updateParams.scalerType;
|
|
2222
|
+
if (scalerType === void 0 && updateParams.scalerValue !== void 0) {
|
|
2223
|
+
const current = await callRestUrl(url);
|
|
2224
|
+
scalerType = current?.scaling?.type === "REQUEST_COUNT" ? "REQUEST_COUNT" : "QUEUE_DELAY";
|
|
2225
|
+
}
|
|
2226
|
+
const scalerError = scalerValueError(
|
|
2227
|
+
scalerType,
|
|
2228
|
+
updateParams.scalerValue
|
|
2160
2229
|
);
|
|
2161
|
-
|
|
2230
|
+
if (scalerError) {
|
|
2231
|
+
return jsonReply2({ error: scalerError, status: 400 });
|
|
2232
|
+
}
|
|
2233
|
+
const body = backend.mapUpdate({
|
|
2234
|
+
...updateParams,
|
|
2235
|
+
scalerType
|
|
2236
|
+
});
|
|
2237
|
+
return jsonReply2(await callRestUrl(url, "PATCH", body));
|
|
2162
2238
|
}
|
|
2163
2239
|
);
|
|
2164
2240
|
server.tool(
|
|
@@ -2206,7 +2282,7 @@ function registerEndpointTools(server, rt) {
|
|
|
2206
2282
|
);
|
|
2207
2283
|
server.tool(
|
|
2208
2284
|
"list-endpoint-workers",
|
|
2209
|
-
"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.",
|
|
2285
|
+
"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.",
|
|
2210
2286
|
{
|
|
2211
2287
|
...listPaginationParams,
|
|
2212
2288
|
endpointId: z8.string().describe("ID of the Serverless endpoint whose workers to list")
|
|
@@ -2232,7 +2308,7 @@ function registerEndpointTools(server, rt) {
|
|
|
2232
2308
|
);
|
|
2233
2309
|
server.tool(
|
|
2234
2310
|
"stream-worker-logs",
|
|
2235
|
-
"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).",
|
|
2311
|
+
"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).",
|
|
2236
2312
|
{
|
|
2237
2313
|
endpointId: z8.string().describe("ID of the Serverless endpoint the worker belongs to"),
|
|
2238
2314
|
workerId: z8.string().describe(
|
|
@@ -2255,8 +2331,67 @@ function registerEndpointTools(server, rt) {
|
|
|
2255
2331
|
|
|
2256
2332
|
// src/tools/jobs.ts
|
|
2257
2333
|
import { z as z9 } from "zod";
|
|
2334
|
+
var DIAGNOSIS_TTL_MS = 15e3;
|
|
2335
|
+
var MAX_DIAGNOSIS_ENTRIES = 500;
|
|
2336
|
+
var diagnosisCache = /* @__PURE__ */ new Map();
|
|
2258
2337
|
function registerJobTools(server, rt) {
|
|
2259
|
-
const { jsonReply: jsonReply2, serverlessRequest } = rt;
|
|
2338
|
+
const { jsonReply: jsonReply2, serverlessRequest, backendFor, callRestUrl } = rt;
|
|
2339
|
+
async function diagnoseQueuedJob(endpointId) {
|
|
2340
|
+
const cached = diagnosisCache.get(endpointId);
|
|
2341
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
2342
|
+
return cached.value;
|
|
2343
|
+
}
|
|
2344
|
+
diagnosisCache.delete(endpointId);
|
|
2345
|
+
try {
|
|
2346
|
+
const backend = backendFor("workers");
|
|
2347
|
+
if (backend.version !== "v2") return null;
|
|
2348
|
+
const raw = await callRestUrl(
|
|
2349
|
+
`${backend.base}/serverless/${endpointId}/workers`
|
|
2350
|
+
);
|
|
2351
|
+
const summary = raw?.summary;
|
|
2352
|
+
const workers = raw?.workers ?? [];
|
|
2353
|
+
if (!summary) return null;
|
|
2354
|
+
const remember = (value) => {
|
|
2355
|
+
if (diagnosisCache.size >= MAX_DIAGNOSIS_ENTRIES) {
|
|
2356
|
+
const oldest = diagnosisCache.keys().next().value;
|
|
2357
|
+
if (oldest !== void 0) diagnosisCache.delete(oldest);
|
|
2358
|
+
}
|
|
2359
|
+
diagnosisCache.set(endpointId, {
|
|
2360
|
+
value,
|
|
2361
|
+
expiresAt: Date.now() + DIAGNOSIS_TTL_MS
|
|
2362
|
+
});
|
|
2363
|
+
return value;
|
|
2364
|
+
};
|
|
2365
|
+
if ((summary.unhealthy ?? 0) > 0) {
|
|
2366
|
+
const badIds = workers.filter((w) => w.status === "UNHEALTHY").map((w) => w.id).join(", ");
|
|
2367
|
+
return remember({
|
|
2368
|
+
workerHealth: summary,
|
|
2369
|
+
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})` : ""}.`
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2372
|
+
if ((summary.total ?? 0) === 0) {
|
|
2373
|
+
return remember({
|
|
2374
|
+
workerHealth: summary,
|
|
2375
|
+
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."
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
if ((summary.throttled ?? 0) > 0 && (summary.running ?? 0) === 0) {
|
|
2379
|
+
return remember({
|
|
2380
|
+
workerHealth: summary,
|
|
2381
|
+
hint: "Workers exist but are throttled \u2014 the hosts are at capacity right now; the job should start when capacity frees up."
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
if ((summary.initializing ?? 0) > 0) {
|
|
2385
|
+
return remember({
|
|
2386
|
+
workerHealth: summary,
|
|
2387
|
+
hint: "A worker is initializing \u2014 likely a cold start (image pull / model load); the job should start soon."
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
return remember({ workerHealth: summary, hint: "" });
|
|
2391
|
+
} catch {
|
|
2392
|
+
return null;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2260
2395
|
const endpointIdSchema = z9.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
|
|
2261
2396
|
const jobIdSchema = z9.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
|
|
2262
2397
|
const inputSchema = z9.record(z9.unknown()).describe(
|
|
@@ -2330,7 +2465,7 @@ function registerJobTools(server, rt) {
|
|
|
2330
2465
|
);
|
|
2331
2466
|
server.tool(
|
|
2332
2467
|
"get-job-status",
|
|
2333
|
-
"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.",
|
|
2468
|
+
"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.",
|
|
2334
2469
|
{
|
|
2335
2470
|
endpointId: endpointIdSchema.describe(
|
|
2336
2471
|
"ID of the Serverless endpoint the job belongs to"
|
|
@@ -2343,6 +2478,16 @@ function registerJobTools(server, rt) {
|
|
|
2343
2478
|
params.endpointId,
|
|
2344
2479
|
`/status/${params.jobId}`
|
|
2345
2480
|
);
|
|
2481
|
+
if (result && typeof result === "object" && result.status === "IN_QUEUE") {
|
|
2482
|
+
const diagnosis = await diagnoseQueuedJob(params.endpointId);
|
|
2483
|
+
if (diagnosis) {
|
|
2484
|
+
return jsonReply2({
|
|
2485
|
+
...result,
|
|
2486
|
+
workerHealth: diagnosis.workerHealth,
|
|
2487
|
+
...diagnosis.hint ? { hint: diagnosis.hint } : {}
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2346
2491
|
return jsonReply2(result);
|
|
2347
2492
|
}
|
|
2348
2493
|
);
|
|
@@ -2444,7 +2589,7 @@ function registerJobTools(server, rt) {
|
|
|
2444
2589
|
);
|
|
2445
2590
|
server.tool(
|
|
2446
2591
|
"endpoint-health",
|
|
2447
|
-
"Get the health and operational status of a Serverless endpoint, including worker counts and job statistics.",
|
|
2592
|
+
"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.",
|
|
2448
2593
|
{
|
|
2449
2594
|
endpointId: endpointIdSchema.describe(
|
|
2450
2595
|
"ID of the Serverless endpoint to check health for"
|