@runpod/mcp-server 2.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 +97 -2
- package/README.md +59 -309
- package/dist/chunk-OM6RHPQC.mjs +3023 -0
- package/dist/chunk-OM6RHPQC.mjs.map +1 -0
- package/dist/http.cjs +1621 -489
- package/dist/http.cjs.map +1 -1
- package/dist/http.d.cts +35 -2
- package/dist/http.d.ts +35 -2
- package/dist/http.mjs +247 -17
- package/dist/http.mjs.map +1 -1
- package/dist/stdio.cjs +1420 -516
- package/dist/stdio.cjs.map +1 -1
- package/dist/stdio.mjs +1420 -516
- package/dist/stdio.mjs.map +1 -1
- package/dist/tools.cjs +1293 -399
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +14 -0
- package/dist/tools.d.ts +14 -0
- package/dist/tools.mjs +1 -1
- package/package.json +1 -1
- package/dist/chunk-ZP3TCVXG.mjs +0 -2116
- package/dist/chunk-ZP3TCVXG.mjs.map +0 -1
package/dist/http.cjs
CHANGED
|
@@ -30,6 +30,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/http.ts
|
|
31
31
|
var http_exports = {};
|
|
32
32
|
__export(http_exports, {
|
|
33
|
+
bodyIsCheckable: () => bodyIsCheckable,
|
|
34
|
+
buildToolContext: () => buildToolContext,
|
|
35
|
+
defaultCredentialChecker: () => defaultCredentialChecker,
|
|
33
36
|
handleMcpRequest: () => handleMcpRequest,
|
|
34
37
|
registerTools: () => registerTools
|
|
35
38
|
});
|
|
@@ -38,81 +41,6 @@ var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamable
|
|
|
38
41
|
|
|
39
42
|
// src/server.ts
|
|
40
43
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
41
|
-
var SERVER_NAME = "Runpod API Server";
|
|
42
|
-
var SERVER_VERSION = true ? "2.0.0" : "dev";
|
|
43
|
-
function createServer(version = SERVER_VERSION) {
|
|
44
|
-
return new import_mcp.McpServer({
|
|
45
|
-
name: SERVER_NAME,
|
|
46
|
-
version,
|
|
47
|
-
capabilities: {
|
|
48
|
-
resources: {},
|
|
49
|
-
tools: {}
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// src/tools/runtime.ts
|
|
55
|
-
var import_node_fetch = __toESM(require("node-fetch"), 1);
|
|
56
|
-
var import_node_crypto = require("crypto");
|
|
57
|
-
|
|
58
|
-
// src/_shared/http.ts
|
|
59
|
-
var HttpError = class extends Error {
|
|
60
|
-
constructor(prefix, status, body) {
|
|
61
|
-
super(`${prefix}: ${status} - ${body}`);
|
|
62
|
-
this.name = "HttpError";
|
|
63
|
-
this.status = status;
|
|
64
|
-
this.body = body;
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
function isJsonContentType(contentType) {
|
|
68
|
-
if (!contentType) return false;
|
|
69
|
-
const ct = contentType.toLowerCase();
|
|
70
|
-
return ct.includes("application/json") || ct.includes("+json");
|
|
71
|
-
}
|
|
72
|
-
function methodSendsBody(method) {
|
|
73
|
-
return method === "POST" || method === "PATCH" || method === "PUT";
|
|
74
|
-
}
|
|
75
|
-
function buildRequestHeaders(apiKey, tracking) {
|
|
76
|
-
return {
|
|
77
|
-
Authorization: `Bearer ${apiKey}`,
|
|
78
|
-
"Content-Type": "application/json",
|
|
79
|
-
...tracking()
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
function createHttpClient(deps) {
|
|
83
|
-
return async function request(url, method = "GET", body) {
|
|
84
|
-
const init = {
|
|
85
|
-
method,
|
|
86
|
-
headers: buildRequestHeaders(deps.apiKey, deps.tracking)
|
|
87
|
-
};
|
|
88
|
-
if (body && methodSendsBody(method)) {
|
|
89
|
-
init.body = JSON.stringify(body);
|
|
90
|
-
}
|
|
91
|
-
const response = await deps.fetch(url, init);
|
|
92
|
-
if (!response.ok) {
|
|
93
|
-
throw new HttpError(
|
|
94
|
-
deps.errorPrefix,
|
|
95
|
-
response.status,
|
|
96
|
-
await response.text()
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// src/_shared/tracking.ts
|
|
104
|
-
function sanitizeUaToken(value) {
|
|
105
|
-
return value.replace(/[()<>@,;:\\"/[\]?={}\s]/g, "_").slice(0, 64);
|
|
106
|
-
}
|
|
107
|
-
function buildTrackingHeaders(input) {
|
|
108
|
-
const name = sanitizeUaToken(input.clientName || "unknown");
|
|
109
|
-
const version = sanitizeUaToken(input.clientVersion || "unknown");
|
|
110
|
-
const userAgent = `runpod-mcp-server/${input.serverVersion} (caller=mcp; client=${name}; client_version=${version}; transport=${input.transport})`;
|
|
111
|
-
return {
|
|
112
|
-
"User-Agent": userAgent,
|
|
113
|
-
"X-Runpod-Session-Id": input.sessionId
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
44
|
|
|
117
45
|
// src/_shared/mappers.ts
|
|
118
46
|
function compact(obj) {
|
|
@@ -126,6 +54,9 @@ function persistentMount(size, path) {
|
|
|
126
54
|
if (size === void 0 || path === void 0) return void 0;
|
|
127
55
|
return { persistent: { size, path } };
|
|
128
56
|
}
|
|
57
|
+
function joinStartCmd(cmd) {
|
|
58
|
+
return cmd && cmd.length ? cmd.join(" ") : void 0;
|
|
59
|
+
}
|
|
129
60
|
function containerConfigToV2(p) {
|
|
130
61
|
return compact({
|
|
131
62
|
image: p.imageName,
|
|
@@ -144,6 +75,13 @@ function mapPodCreateToV2(params) {
|
|
|
144
75
|
return compact({
|
|
145
76
|
...containerConfigToV2(params),
|
|
146
77
|
name: params.name,
|
|
78
|
+
// Private-image registry credential (v2 ContainerConfig `registry`). When
|
|
79
|
+
// deploying from a template, this spreads over the template's registry, so
|
|
80
|
+
// passing it overrides the inherited credential. An empty string is an
|
|
81
|
+
// explicit opt-out: emit `registry: null` (v2 accepts null) to CLEAR a
|
|
82
|
+
// template's inherited credential. Undefined is dropped by compact, so an
|
|
83
|
+
// omitted param leaves the template's registry in place.
|
|
84
|
+
registry: params.containerRegistryAuthId === "" ? null : params.containerRegistryAuthId,
|
|
147
85
|
cloud: params.cloudType,
|
|
148
86
|
// v2 CreatePodRequest takes `dataCenterIds` as an ARRAY (preferred data
|
|
149
87
|
// centers; omit/empty = scheduler chooses). Pass the v1 list straight
|
|
@@ -161,15 +99,27 @@ function mapPodUpdateToV2(params) {
|
|
|
161
99
|
...compact({ name: params.name })
|
|
162
100
|
};
|
|
163
101
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
102
|
+
var DEFAULT_ENDPOINT_TYPE = "QUEUE";
|
|
103
|
+
var DEFAULT_SCALER_VALUE = 4;
|
|
104
|
+
function defaultScalerType(endpointType) {
|
|
105
|
+
return endpointType === "LOAD_BALANCER" ? "REQUEST_COUNT" : "QUEUE_DELAY";
|
|
106
|
+
}
|
|
107
|
+
function endpointScaling(scalerType, value) {
|
|
108
|
+
return scalerType === "REQUEST_COUNT" ? { type: "REQUEST_COUNT", requestCount: value } : { type: "QUEUE_DELAY", queueDelay: value };
|
|
167
109
|
}
|
|
168
110
|
function endpointGpuConfig(pools, count) {
|
|
169
111
|
if (!pools?.length) return void 0;
|
|
170
112
|
return compact({ pools, count });
|
|
171
113
|
}
|
|
172
|
-
function
|
|
114
|
+
function endpointWorkers(params) {
|
|
115
|
+
const workers = compact({
|
|
116
|
+
min: params.workersMin,
|
|
117
|
+
max: params.workersMax,
|
|
118
|
+
idleTimeout: params.idleTimeout
|
|
119
|
+
});
|
|
120
|
+
return Object.keys(workers).length ? workers : void 0;
|
|
121
|
+
}
|
|
122
|
+
function endpointCommonToV2(params) {
|
|
173
123
|
return compact({
|
|
174
124
|
name: params.name,
|
|
175
125
|
image: params.imageName,
|
|
@@ -179,28 +129,39 @@ function mapEndpointToV2(params) {
|
|
|
179
129
|
env: params.env,
|
|
180
130
|
registry: params.containerRegistryAuthId,
|
|
181
131
|
gpu: endpointGpuConfig(params.gpuPoolIds, params.gpuCount),
|
|
182
|
-
workers:
|
|
183
|
-
min: params.workersMin,
|
|
184
|
-
max: params.workersMax
|
|
185
|
-
}),
|
|
186
|
-
scaling: nestedOrUndefined({
|
|
187
|
-
type: params.scalerType,
|
|
188
|
-
value: params.scalerValue,
|
|
189
|
-
idleTimeout: params.idleTimeout
|
|
190
|
-
}),
|
|
132
|
+
workers: endpointWorkers(params),
|
|
191
133
|
dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
|
|
192
134
|
networkVolumes: params.networkVolumeIds?.length ? params.networkVolumeIds : void 0,
|
|
193
135
|
timeout: params.executionTimeoutMs,
|
|
194
136
|
flashboot: params.flashboot
|
|
195
137
|
});
|
|
196
138
|
}
|
|
197
|
-
|
|
198
|
-
|
|
139
|
+
function mapEndpointCreateToV2(params) {
|
|
140
|
+
const endpointType = params.endpointType ?? DEFAULT_ENDPOINT_TYPE;
|
|
141
|
+
return compact({
|
|
142
|
+
...endpointCommonToV2(params),
|
|
143
|
+
type: endpointType,
|
|
144
|
+
scaling: endpointScaling(
|
|
145
|
+
params.scalerType ?? defaultScalerType(endpointType),
|
|
146
|
+
params.scalerValue ?? DEFAULT_SCALER_VALUE
|
|
147
|
+
)
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function mapEndpointUpdateToV2(params) {
|
|
151
|
+
return compact({
|
|
152
|
+
...endpointCommonToV2(params),
|
|
153
|
+
scaling: params.scalerType ? endpointScaling(
|
|
154
|
+
params.scalerType,
|
|
155
|
+
params.scalerValue ?? DEFAULT_SCALER_VALUE
|
|
156
|
+
) : void 0
|
|
157
|
+
});
|
|
158
|
+
}
|
|
199
159
|
function mapNetworkVolumeCreateToV2(params) {
|
|
200
160
|
return compact({
|
|
201
161
|
name: params.name,
|
|
202
162
|
size: params.size,
|
|
203
|
-
dataCenter: params.dataCenterId
|
|
163
|
+
dataCenter: params.dataCenterId,
|
|
164
|
+
type: params.volumeType
|
|
204
165
|
});
|
|
205
166
|
}
|
|
206
167
|
function mapTemplateCreateToV2(params) {
|
|
@@ -209,18 +170,39 @@ function mapTemplateCreateToV2(params) {
|
|
|
209
170
|
...compact({
|
|
210
171
|
name: params.name,
|
|
211
172
|
serverless: params.isServerless,
|
|
212
|
-
registry: params.containerRegistryAuthId
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
173
|
+
registry: params.containerRegistryAuthId,
|
|
174
|
+
args: joinStartCmd(params.dockerStartCmd),
|
|
175
|
+
// No longer required by v2 (it defaults to NVIDIA server-side), so an
|
|
176
|
+
// unset category is dropped by compact instead of being forced here.
|
|
177
|
+
category: params.category
|
|
178
|
+
})
|
|
216
179
|
};
|
|
217
180
|
}
|
|
181
|
+
function podBodyFromTemplate(template) {
|
|
182
|
+
const out = {};
|
|
183
|
+
const keys = [
|
|
184
|
+
"name",
|
|
185
|
+
"image",
|
|
186
|
+
"args",
|
|
187
|
+
"disk",
|
|
188
|
+
"ports",
|
|
189
|
+
"env",
|
|
190
|
+
"mounts",
|
|
191
|
+
"registry"
|
|
192
|
+
];
|
|
193
|
+
for (const key of keys) {
|
|
194
|
+
const value = template[key];
|
|
195
|
+
if (value !== void 0 && value !== null) out[key] = value;
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
218
199
|
function mapTemplateUpdateToV2(params) {
|
|
219
200
|
return {
|
|
220
201
|
...containerConfigToV2(params),
|
|
221
202
|
...compact({
|
|
222
203
|
name: params.name,
|
|
223
|
-
registry: params.containerRegistryAuthId
|
|
204
|
+
registry: params.containerRegistryAuthId,
|
|
205
|
+
args: joinStartCmd(params.dockerStartCmd)
|
|
224
206
|
})
|
|
225
207
|
};
|
|
226
208
|
}
|
|
@@ -237,7 +219,6 @@ var V2_UNWRAP_KEY = {
|
|
|
237
219
|
dataCenters: "dataCenters",
|
|
238
220
|
endpoints: "endpoints",
|
|
239
221
|
jobs: "jobs",
|
|
240
|
-
tags: "tags",
|
|
241
222
|
workers: "workers",
|
|
242
223
|
billing: "records"
|
|
243
224
|
};
|
|
@@ -251,7 +232,6 @@ var RESOURCE_ENV_KEY = {
|
|
|
251
232
|
dataCenters: "DATA_CENTERS",
|
|
252
233
|
endpoints: "ENDPOINTS",
|
|
253
234
|
jobs: "JOBS",
|
|
254
|
-
tags: "TAGS",
|
|
255
235
|
workers: "WORKERS",
|
|
256
236
|
billing: "BILLING"
|
|
257
237
|
};
|
|
@@ -267,6 +247,9 @@ function serverlessBase(env) {
|
|
|
267
247
|
function publicGraphqlBase(env) {
|
|
268
248
|
return env.RUNPOD_PUBLIC_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
|
|
269
249
|
}
|
|
250
|
+
function authedGraphqlBase(env) {
|
|
251
|
+
return env.RUNPOD_AUTHED_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
|
|
252
|
+
}
|
|
270
253
|
function unwrapList(resource, version, raw) {
|
|
271
254
|
if (version === "v1") return Array.isArray(raw) ? raw : [];
|
|
272
255
|
if (raw === null || typeof raw !== "object") return [];
|
|
@@ -285,6 +268,14 @@ function resolveVersion(opts) {
|
|
|
285
268
|
}
|
|
286
269
|
return "v1";
|
|
287
270
|
}
|
|
271
|
+
function restVersionSummary(env) {
|
|
272
|
+
const raw = (env.RUNPOD_REST_VERSION ?? "").trim().toLowerCase();
|
|
273
|
+
const base = raw ? `RUNPOD_REST_VERSION=${raw}` : "RUNPOD_REST_VERSION unset (default v2)";
|
|
274
|
+
const overrides = Object.keys(env).filter(
|
|
275
|
+
(k) => k.startsWith("RUNPOD_REST_VERSION_") && (env[k] ?? "").trim() !== ""
|
|
276
|
+
).length;
|
|
277
|
+
return overrides > 0 ? `${base}, +${overrides} per-resource` : base;
|
|
278
|
+
}
|
|
288
279
|
var identity = (body) => body;
|
|
289
280
|
var V1_REST_PATHS = {
|
|
290
281
|
pods: { list: "/pods", get: (id) => `/pods/${id}` },
|
|
@@ -305,7 +296,6 @@ var CATALOG = /* @__PURE__ */ new Set([
|
|
|
305
296
|
"dataCenters"
|
|
306
297
|
]);
|
|
307
298
|
var V2_ONLY = /* @__PURE__ */ new Set([
|
|
308
|
-
"tags",
|
|
309
299
|
"workers",
|
|
310
300
|
"billing"
|
|
311
301
|
]);
|
|
@@ -323,7 +313,6 @@ var V2_REST_PATHS = {
|
|
|
323
313
|
list: "/catalog/datacenters",
|
|
324
314
|
get: (id) => `/catalog/datacenters/${id}`
|
|
325
315
|
},
|
|
326
|
-
tags: { list: "/tags", get: (id) => `/tags/${id}` },
|
|
327
316
|
// Endpoints live under /v2/serverless on v2 (NOT /v2/endpoints). The v2 list
|
|
328
317
|
// response wraps rows under `endpoints` (see V2_UNWRAP_KEY), so the existing
|
|
329
318
|
// unwrap key stays correct.
|
|
@@ -420,8 +409,186 @@ function resolveBackend(opts) {
|
|
|
420
409
|
return version === "v2" ? buildV2Backend(resource, env, unwrap) : buildV1Backend(resource, env, unwrap);
|
|
421
410
|
}
|
|
422
411
|
|
|
412
|
+
// src/server.ts
|
|
413
|
+
var SERVER_NAME = "Runpod API Server";
|
|
414
|
+
var SERVER_VERSION = true ? "3.1.0" : "dev";
|
|
415
|
+
function createServer(version = SERVER_VERSION) {
|
|
416
|
+
return new import_mcp.McpServer({
|
|
417
|
+
name: SERVER_NAME,
|
|
418
|
+
version: `${version} [${restVersionSummary(process.env)}]`,
|
|
419
|
+
capabilities: {
|
|
420
|
+
resources: {},
|
|
421
|
+
tools: {}
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
423
426
|
// src/tools/runtime.ts
|
|
424
|
-
var
|
|
427
|
+
var import_node_fetch = __toESM(require("node-fetch"), 1);
|
|
428
|
+
var import_node_crypto = require("crypto");
|
|
429
|
+
|
|
430
|
+
// src/_shared/http.ts
|
|
431
|
+
var HttpError = class extends Error {
|
|
432
|
+
constructor(prefix, status, body) {
|
|
433
|
+
const hint = status === 401 ? " (the Runpod API rejected the credential \u2014 the API key may be expired or revoked; re-authenticate or update RUNPOD_API_KEY)" : "";
|
|
434
|
+
super(`${prefix}: ${status} - ${body}${hint}`);
|
|
435
|
+
this.name = "HttpError";
|
|
436
|
+
this.status = status;
|
|
437
|
+
this.body = body;
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
function isJsonContentType(contentType) {
|
|
441
|
+
if (!contentType) return false;
|
|
442
|
+
const ct = contentType.toLowerCase();
|
|
443
|
+
return ct.includes("application/json") || ct.includes("+json");
|
|
444
|
+
}
|
|
445
|
+
function methodSendsBody(method) {
|
|
446
|
+
return method === "POST" || method === "PATCH" || method === "PUT";
|
|
447
|
+
}
|
|
448
|
+
function buildRequestHeaders(apiKey, tracking) {
|
|
449
|
+
return {
|
|
450
|
+
Authorization: `Bearer ${apiKey}`,
|
|
451
|
+
"Content-Type": "application/json",
|
|
452
|
+
...tracking()
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function createHttpClient(deps) {
|
|
456
|
+
return async function request(url, method = "GET", body) {
|
|
457
|
+
const init = {
|
|
458
|
+
method,
|
|
459
|
+
headers: buildRequestHeaders(deps.apiKey, deps.tracking)
|
|
460
|
+
};
|
|
461
|
+
if (body && methodSendsBody(method)) {
|
|
462
|
+
init.body = JSON.stringify(body);
|
|
463
|
+
}
|
|
464
|
+
const response = await deps.fetch(url, init);
|
|
465
|
+
if (!response.ok) {
|
|
466
|
+
throw new HttpError(
|
|
467
|
+
deps.errorPrefix,
|
|
468
|
+
response.status,
|
|
469
|
+
await response.text()
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/_shared/tracking.ts
|
|
477
|
+
function sanitizeUaToken(value) {
|
|
478
|
+
return value.replace(/[()<>@,;:\\"/[\]?={}\s]/g, "_").slice(0, 64);
|
|
479
|
+
}
|
|
480
|
+
function buildTrackingHeaders(input) {
|
|
481
|
+
const name = sanitizeUaToken(input.clientName || "unknown");
|
|
482
|
+
const version = sanitizeUaToken(input.clientVersion || "unknown");
|
|
483
|
+
const userAgent = `runpod-mcp-server/${input.serverVersion} (caller=mcp; client=${name}; client_version=${version}; transport=${input.transport})`;
|
|
484
|
+
return {
|
|
485
|
+
"User-Agent": userAgent,
|
|
486
|
+
"X-Runpod-Session-Id": input.sessionId
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/tools/logs.ts
|
|
491
|
+
var import_zod = require("zod");
|
|
492
|
+
function parseLogSse(raw) {
|
|
493
|
+
const items = [];
|
|
494
|
+
for (const block of raw.split(/\r?\n\r?\n/)) {
|
|
495
|
+
const dataLines = block.split(/\r?\n/).filter((l) => l.startsWith("data:"));
|
|
496
|
+
if (!dataLines.length) continue;
|
|
497
|
+
const payload = dataLines.map((l) => l.slice(5).replace(/^ /, "")).join("\n");
|
|
498
|
+
if (!payload.trim()) continue;
|
|
499
|
+
try {
|
|
500
|
+
const parsed = JSON.parse(payload);
|
|
501
|
+
items.push(
|
|
502
|
+
parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { raw: payload }
|
|
503
|
+
);
|
|
504
|
+
} catch {
|
|
505
|
+
items.push({ raw: payload });
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return items;
|
|
509
|
+
}
|
|
510
|
+
var LOG_STREAM_DEFAULT_WAIT_MS = 5e3;
|
|
511
|
+
var LOG_STREAM_MAX_BYTES = 256 * 1024;
|
|
512
|
+
async function readSseSnapshot(fetchImpl, url, headers, opts) {
|
|
513
|
+
const controller = new AbortController();
|
|
514
|
+
const timer = setTimeout(() => controller.abort(), opts.maxWaitMs);
|
|
515
|
+
const chunks = [];
|
|
516
|
+
let bytes = 0;
|
|
517
|
+
let truncated = false;
|
|
518
|
+
try {
|
|
519
|
+
const res = await fetchImpl(url, {
|
|
520
|
+
method: "GET",
|
|
521
|
+
headers,
|
|
522
|
+
signal: controller.signal
|
|
523
|
+
});
|
|
524
|
+
if (!res.ok) {
|
|
525
|
+
throw new HttpError(
|
|
526
|
+
"Runpod API Error",
|
|
527
|
+
res.status,
|
|
528
|
+
await res.text().catch(() => "")
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
for await (const chunk of res.body ?? []) {
|
|
532
|
+
chunks.push(chunk);
|
|
533
|
+
bytes += chunk.length;
|
|
534
|
+
if (bytes >= opts.maxBytes) {
|
|
535
|
+
truncated = true;
|
|
536
|
+
controller.abort();
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
} catch (err) {
|
|
541
|
+
if (!(err instanceof Error && err.name === "AbortError")) throw err;
|
|
542
|
+
} finally {
|
|
543
|
+
clearTimeout(timer);
|
|
544
|
+
}
|
|
545
|
+
return { raw: Buffer.concat(chunks).toString("utf8"), truncated };
|
|
546
|
+
}
|
|
547
|
+
var logStreamParams = {
|
|
548
|
+
source: import_zod.z.enum(["container", "system", "both"]).optional().describe("Which log source to read (default: both)"),
|
|
549
|
+
tail: import_zod.z.number().int().min(0).max(5e3).optional().describe(
|
|
550
|
+
"Historical lines to backfill before live output (API default 100, max 5000; 0 = live only). Ignored when `since` is set."
|
|
551
|
+
),
|
|
552
|
+
since: import_zod.z.string().optional().describe("RFC3339 timestamp to resume from; when set, `tail` is ignored."),
|
|
553
|
+
maxWaitMs: import_zod.z.number().int().min(500).max(3e4).optional().describe("How long to read the stream, in ms (default 5000, max 30000)")
|
|
554
|
+
};
|
|
555
|
+
async function collectLogSnapshot(streamSse, logsUrl, params) {
|
|
556
|
+
const qs = new URLSearchParams();
|
|
557
|
+
if (params.source && params.source !== "both")
|
|
558
|
+
qs.append("source", params.source);
|
|
559
|
+
if (params.tail !== void 0) qs.append("tail", String(params.tail));
|
|
560
|
+
if (params.since) qs.append("since", params.since);
|
|
561
|
+
const query = qs.toString() ? `?${qs}` : "";
|
|
562
|
+
const { raw, truncated } = await streamSse(`${logsUrl}${query}`, {
|
|
563
|
+
maxWaitMs: params.maxWaitMs ?? LOG_STREAM_DEFAULT_WAIT_MS,
|
|
564
|
+
maxBytes: LOG_STREAM_MAX_BYTES
|
|
565
|
+
});
|
|
566
|
+
const items = parseLogSse(raw);
|
|
567
|
+
if (truncated) items.pop();
|
|
568
|
+
return { items, count: items.length, truncated };
|
|
569
|
+
}
|
|
570
|
+
async function streamLogsReply(rt, tool, params) {
|
|
571
|
+
const backend = rt.backendFor(tool.resource);
|
|
572
|
+
if (backend.version === "v1") {
|
|
573
|
+
return rt.jsonReply({
|
|
574
|
+
error: `${tool.name} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
|
|
575
|
+
status: 501
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
try {
|
|
579
|
+
return rt.jsonReply(
|
|
580
|
+
await collectLogSnapshot(rt.streamSse, tool.logsUrl(backend), params)
|
|
581
|
+
);
|
|
582
|
+
} catch (error) {
|
|
583
|
+
if (error instanceof HttpError) {
|
|
584
|
+
return rt.jsonReply({ error: error.message, status: error.status });
|
|
585
|
+
}
|
|
586
|
+
throw error;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/tools/runtime.ts
|
|
591
|
+
var MCP_SERVER_VERSION = true ? "3.1.0" : "dev";
|
|
425
592
|
var SESSION_ID = (0, import_node_crypto.randomUUID)();
|
|
426
593
|
function trackingHeaders(server, ctx) {
|
|
427
594
|
const info = server.server.getClientVersion();
|
|
@@ -458,14 +625,18 @@ var DESTRUCTIVE = {
|
|
|
458
625
|
idempotentHint: true,
|
|
459
626
|
openWorldHint: true
|
|
460
627
|
};
|
|
461
|
-
async function graphqlRequest(query, tracking, fetchImpl, url) {
|
|
628
|
+
async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
462
629
|
const response = await fetchImpl(url, {
|
|
463
630
|
method: "POST",
|
|
464
631
|
headers: {
|
|
465
632
|
"Content-Type": "application/json",
|
|
633
|
+
...options?.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {},
|
|
466
634
|
...tracking()
|
|
467
635
|
},
|
|
468
|
-
body: JSON.stringify({
|
|
636
|
+
body: JSON.stringify({
|
|
637
|
+
query,
|
|
638
|
+
...options?.variables ? { variables: options.variables } : {}
|
|
639
|
+
})
|
|
469
640
|
});
|
|
470
641
|
const result = await response.json();
|
|
471
642
|
if (result.errors && result.errors.length > 0) {
|
|
@@ -480,7 +651,17 @@ async function graphqlRequest(query, tracking, fetchImpl, url) {
|
|
|
480
651
|
}
|
|
481
652
|
function createToolRuntime(server, ctx, deps = {}) {
|
|
482
653
|
const tracking = () => trackingHeaders(server, ctx);
|
|
483
|
-
|
|
654
|
+
function observeUnauthorized(fn) {
|
|
655
|
+
const notify = ctx.onUnauthorized;
|
|
656
|
+
if (!notify) return fn;
|
|
657
|
+
return async (...args) => {
|
|
658
|
+
const response = await fn(...args);
|
|
659
|
+
if (response.status === 401) notify();
|
|
660
|
+
return response;
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
const rawFetch = deps.fetch ?? defaultFetch;
|
|
664
|
+
const httpFetch = observeUnauthorized(rawFetch);
|
|
484
665
|
const v1Client = createHttpClient({
|
|
485
666
|
apiKey: ctx.apiKey,
|
|
486
667
|
fetch: httpFetch,
|
|
@@ -515,47 +696,19 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
515
696
|
"Error calling Runpod API:",
|
|
516
697
|
() => restClient(url, method, body)
|
|
517
698
|
);
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
if (!res.ok) {
|
|
532
|
-
throw new HttpError(
|
|
533
|
-
"Runpod API Error",
|
|
534
|
-
res.status,
|
|
535
|
-
await res.text().catch(() => "")
|
|
536
|
-
);
|
|
537
|
-
}
|
|
538
|
-
const chunks = [];
|
|
539
|
-
let bytes = 0;
|
|
540
|
-
let truncated = false;
|
|
541
|
-
try {
|
|
542
|
-
for await (const chunk of res.body) {
|
|
543
|
-
chunks.push(chunk);
|
|
544
|
-
bytes += chunk.length;
|
|
545
|
-
if (bytes >= opts.maxBytes) {
|
|
546
|
-
truncated = true;
|
|
547
|
-
controller.abort();
|
|
548
|
-
break;
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
} catch (err) {
|
|
552
|
-
if (!(err instanceof Error && err.name === "AbortError")) throw err;
|
|
553
|
-
}
|
|
554
|
-
return { raw: Buffer.concat(chunks).toString("utf8"), truncated };
|
|
555
|
-
} finally {
|
|
556
|
-
clearTimeout(timer);
|
|
557
|
-
}
|
|
558
|
-
});
|
|
699
|
+
const sseFetch = observeUnauthorized(
|
|
700
|
+
deps.sseFetch ?? import_node_fetch.default
|
|
701
|
+
);
|
|
702
|
+
const streamSse = deps.streamSse ?? ((url, opts) => readSseSnapshot(
|
|
703
|
+
sseFetch,
|
|
704
|
+
url,
|
|
705
|
+
{
|
|
706
|
+
Authorization: `Bearer ${ctx.apiKey}`,
|
|
707
|
+
Accept: "text/event-stream",
|
|
708
|
+
...tracking()
|
|
709
|
+
},
|
|
710
|
+
opts
|
|
711
|
+
));
|
|
559
712
|
const backendFor = (resource) => resolveBackend({
|
|
560
713
|
resource,
|
|
561
714
|
env: process.env,
|
|
@@ -577,9 +730,18 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
577
730
|
graphql: (query) => graphqlRequest(
|
|
578
731
|
query,
|
|
579
732
|
tracking,
|
|
580
|
-
httpFetch
|
|
733
|
+
// rawFetch, not httpFetch: this call carries no credential (see above).
|
|
734
|
+
rawFetch,
|
|
581
735
|
publicGraphqlBase(process.env)
|
|
582
736
|
),
|
|
737
|
+
graphqlAuthed: (query, variables) => graphqlRequest(
|
|
738
|
+
query,
|
|
739
|
+
tracking,
|
|
740
|
+
httpFetch,
|
|
741
|
+
// NOT publicGraphqlBase: this call carries the caller's API key.
|
|
742
|
+
authedGraphqlBase(process.env),
|
|
743
|
+
{ variables, apiKey: ctx.apiKey }
|
|
744
|
+
),
|
|
583
745
|
runpodRequest,
|
|
584
746
|
serverlessRequest,
|
|
585
747
|
callRestUrl,
|
|
@@ -591,17 +753,17 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
591
753
|
}
|
|
592
754
|
|
|
593
755
|
// src/tools/catalog.ts
|
|
594
|
-
var
|
|
756
|
+
var import_zod3 = require("zod");
|
|
595
757
|
|
|
596
758
|
// src/pagination.ts
|
|
597
|
-
var
|
|
759
|
+
var import_zod2 = require("zod");
|
|
598
760
|
var DEFAULT_LIST_LIMIT = 20;
|
|
599
761
|
var MAX_LIST_LIMIT = 100;
|
|
600
762
|
var listPaginationParams = {
|
|
601
|
-
limit:
|
|
763
|
+
limit: import_zod2.z.number().int().positive().max(MAX_LIST_LIMIT).optional().describe(
|
|
602
764
|
`Maximum number of items to return (default ${DEFAULT_LIST_LIMIT}, max ${MAX_LIST_LIMIT}). Use the returned nextCursor to fetch the next page.`
|
|
603
765
|
),
|
|
604
|
-
cursor:
|
|
766
|
+
cursor: import_zod2.z.string().optional().describe(
|
|
605
767
|
"Opaque pagination cursor from a previous response (nextCursor). Omit to start from the beginning."
|
|
606
768
|
)
|
|
607
769
|
};
|
|
@@ -659,25 +821,32 @@ function registerCatalogTools(server, rt) {
|
|
|
659
821
|
const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
|
|
660
822
|
server.tool(
|
|
661
823
|
"list-gpu-types",
|
|
662
|
-
"List available GPU types with stock/pricing and capability filters (minimum VRAM, secure/community cloud, name search). Use this to discover valid gpuTypeIds before creating a pod or endpoint.
|
|
824
|
+
"List available GPU types with stock/pricing and capability filters (minimum VRAM, secure/community cloud, name search). Use this to discover valid gpuTypeIds before creating a pod or endpoint. By default the full catalog is returned: each result includes an `availability` summary (HIGH/MEDIUM/LOW/NONE) and results are sorted with the most-available GPUs first, but nothing is hidden. Set includeUnavailable:false to drop out-of-stock GPUs and list only deployable ones (set includeAvailability:false to skip the stock lookup entirely). For per-datacenter availability (to pick a dataCenterIds), use get-gpu-type.",
|
|
663
825
|
{
|
|
664
826
|
...listPaginationParams,
|
|
665
|
-
minMemoryGb:
|
|
666
|
-
secureCloudOnly:
|
|
667
|
-
communityCloudOnly:
|
|
668
|
-
searchTerm:
|
|
827
|
+
minMemoryGb: import_zod3.z.number().optional().describe("Filter to GPUs with at least this much VRAM in GB"),
|
|
828
|
+
secureCloudOnly: import_zod3.z.boolean().optional().describe("Filter to only GPUs available in secure cloud"),
|
|
829
|
+
communityCloudOnly: import_zod3.z.boolean().optional().describe("Filter to only GPUs available in community cloud"),
|
|
830
|
+
searchTerm: import_zod3.z.string().optional().describe(
|
|
669
831
|
"Search term to filter GPUs by name (e.g., 'A100', 'RTX 4090')"
|
|
670
832
|
),
|
|
671
|
-
includeUnavailable:
|
|
672
|
-
"
|
|
833
|
+
includeUnavailable: import_zod3.z.boolean().optional().describe(
|
|
834
|
+
"Out-of-stock GPUs are included by default (annotated availability:NONE and sorted last). Set false to hide them and list only currently-deployable GPUs."
|
|
835
|
+
),
|
|
836
|
+
includeAvailability: import_zod3.z.boolean().optional().describe(
|
|
837
|
+
"Request realtime stock and annotate each GPU with an availability summary (HIGH/MEDIUM/LOW/NONE). Default true. Set false to skip the availability lookup \u2014 then out-of-stock GPUs cannot be filtered."
|
|
673
838
|
)
|
|
674
839
|
},
|
|
675
840
|
{ title: "List GPU types", ...READ_ONLY },
|
|
676
841
|
async (params) => {
|
|
677
842
|
const backend = backendFor("gpus");
|
|
678
843
|
if (backend.version === "v2") {
|
|
679
|
-
const
|
|
844
|
+
const wantAvailability = params.includeAvailability !== false;
|
|
845
|
+
const raw = await callRestUrl(
|
|
846
|
+
`${backend.base}${backend.list}${wantAvailability ? "?include=AVAILABILITY" : ""}`
|
|
847
|
+
);
|
|
680
848
|
let gpus = backend.unwrap(raw);
|
|
849
|
+
gpus = gpus.filter((g) => g.id !== "unknown");
|
|
681
850
|
if (params.minMemoryGb !== void 0)
|
|
682
851
|
gpus = gpus.filter(
|
|
683
852
|
(g) => Number(g.memory ?? 0) >= params.minMemoryGb
|
|
@@ -690,6 +859,18 @@ function registerCatalogTools(server, rt) {
|
|
|
690
859
|
(g) => String(g.id ?? "").toLowerCase().includes(t) || String(g.name ?? "").toLowerCase().includes(t)
|
|
691
860
|
);
|
|
692
861
|
}
|
|
862
|
+
if (params.includeUnavailable === false)
|
|
863
|
+
gpus = gpus.filter((g) => g.availability !== "NONE");
|
|
864
|
+
const rank = {
|
|
865
|
+
HIGH: 3,
|
|
866
|
+
MEDIUM: 2,
|
|
867
|
+
LOW: 1,
|
|
868
|
+
NONE: 0
|
|
869
|
+
};
|
|
870
|
+
gpus.sort(
|
|
871
|
+
(a, b) => (rank[String(b.availability)] ?? 0) - (rank[String(a.availability)] ?? 0)
|
|
872
|
+
);
|
|
873
|
+
gpus = gpus.map(({ dataCenters: _dataCenters, ...rest }) => rest);
|
|
693
874
|
return capListResult(gpus, {
|
|
694
875
|
limit: params.limit,
|
|
695
876
|
cursor: params.cursor
|
|
@@ -719,7 +900,7 @@ function registerCatalogTools(server, rt) {
|
|
|
719
900
|
return !!status && status !== "Out";
|
|
720
901
|
};
|
|
721
902
|
let gpuTypes = data.gpuTypes.filter((gpu) => gpu.id !== "unknown");
|
|
722
|
-
if (
|
|
903
|
+
if (params.includeUnavailable === false) {
|
|
723
904
|
gpuTypes = gpuTypes.filter(isAvailable);
|
|
724
905
|
}
|
|
725
906
|
if (params.minMemoryGb) {
|
|
@@ -765,7 +946,7 @@ function registerCatalogTools(server, rt) {
|
|
|
765
946
|
"List Runpod data centers (id, name, region/location). Use this to discover valid dataCenterIds for placing pods, endpoints, or network volumes.",
|
|
766
947
|
{
|
|
767
948
|
...listPaginationParams,
|
|
768
|
-
region:
|
|
949
|
+
region: import_zod3.z.string().optional().describe(
|
|
769
950
|
"Filter by region/location (e.g., 'United States', 'Europe', 'Canada')"
|
|
770
951
|
)
|
|
771
952
|
},
|
|
@@ -830,9 +1011,12 @@ function registerCatalogTools(server, rt) {
|
|
|
830
1011
|
);
|
|
831
1012
|
server.tool(
|
|
832
1013
|
"get-gpu-type",
|
|
833
|
-
"Get details for a single GPU type by id. v2-only \u2014 returns a 501 notice on the v1 API (use list-gpu-types there).",
|
|
1014
|
+
"Get details for a single GPU type by id, including per-datacenter availability. v2-only \u2014 returns a 501 notice on the v1 API (use list-gpu-types there). Use the returned dataCenters[].availability to pick a dataCenterIds with stock before creating a pod.",
|
|
834
1015
|
{
|
|
835
|
-
gpuTypeId:
|
|
1016
|
+
gpuTypeId: import_zod3.z.string().describe("ID of the GPU type to retrieve"),
|
|
1017
|
+
includeAvailability: import_zod3.z.boolean().optional().describe(
|
|
1018
|
+
"Include realtime per-datacenter availability (HIGH/MEDIUM/LOW/NONE). Default true."
|
|
1019
|
+
)
|
|
836
1020
|
},
|
|
837
1021
|
{ title: "Get GPU type", ...READ_ONLY },
|
|
838
1022
|
async (params) => {
|
|
@@ -843,9 +1027,9 @@ function registerCatalogTools(server, rt) {
|
|
|
843
1027
|
status: 501
|
|
844
1028
|
});
|
|
845
1029
|
}
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
);
|
|
1030
|
+
const path = backend.get(encodeURIComponent(params.gpuTypeId));
|
|
1031
|
+
const query = params.includeAvailability === false ? "" : "?include=AVAILABILITY";
|
|
1032
|
+
const result = await callRestUrl(`${backend.base}${path}${query}`);
|
|
849
1033
|
return jsonReply2(result);
|
|
850
1034
|
}
|
|
851
1035
|
);
|
|
@@ -853,7 +1037,7 @@ function registerCatalogTools(server, rt) {
|
|
|
853
1037
|
"get-cpu-type",
|
|
854
1038
|
"Get details for a single CPU flavor type by id. v2-only \u2014 returns a 501 notice on the v1 API (use list-cpu-types there).",
|
|
855
1039
|
{
|
|
856
|
-
cpuTypeId:
|
|
1040
|
+
cpuTypeId: import_zod3.z.string().describe("ID of the CPU type to retrieve")
|
|
857
1041
|
},
|
|
858
1042
|
{ title: "Get CPU type", ...READ_ONLY },
|
|
859
1043
|
async (params) => {
|
|
@@ -874,7 +1058,7 @@ function registerCatalogTools(server, rt) {
|
|
|
874
1058
|
"get-data-center",
|
|
875
1059
|
"Get details for a single data center by id. v2-only \u2014 returns a 501 notice on the v1 API (use list-data-centers there).",
|
|
876
1060
|
{
|
|
877
|
-
dataCenterId:
|
|
1061
|
+
dataCenterId: import_zod3.z.string().describe("ID of the data center to retrieve")
|
|
878
1062
|
},
|
|
879
1063
|
{ title: "Get data center", ...READ_ONLY },
|
|
880
1064
|
async (params) => {
|
|
@@ -885,16 +1069,587 @@ function registerCatalogTools(server, rt) {
|
|
|
885
1069
|
status: 501
|
|
886
1070
|
});
|
|
887
1071
|
}
|
|
888
|
-
const result = await callRestUrl(
|
|
889
|
-
`${backend.base}${backend.get(params.dataCenterId)}`
|
|
1072
|
+
const result = await callRestUrl(
|
|
1073
|
+
`${backend.base}${backend.get(params.dataCenterId)}`
|
|
1074
|
+
);
|
|
1075
|
+
return jsonReply2(result);
|
|
1076
|
+
}
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// src/tools/hub.ts
|
|
1081
|
+
var import_zod4 = require("zod");
|
|
1082
|
+
function parseReleaseConfig(config) {
|
|
1083
|
+
if (!config) return void 0;
|
|
1084
|
+
try {
|
|
1085
|
+
return JSON.parse(config);
|
|
1086
|
+
} catch {
|
|
1087
|
+
return config;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
function listingsQuery(withConfig) {
|
|
1091
|
+
return `
|
|
1092
|
+
query {
|
|
1093
|
+
listings(input: {}) {
|
|
1094
|
+
id
|
|
1095
|
+
repoId
|
|
1096
|
+
title
|
|
1097
|
+
description
|
|
1098
|
+
repoName
|
|
1099
|
+
repoOwner
|
|
1100
|
+
createdAt
|
|
1101
|
+
updatedAt
|
|
1102
|
+
views
|
|
1103
|
+
stars
|
|
1104
|
+
deploys
|
|
1105
|
+
language
|
|
1106
|
+
category
|
|
1107
|
+
tags
|
|
1108
|
+
type
|
|
1109
|
+
listedRelease {
|
|
1110
|
+
id
|
|
1111
|
+
name
|
|
1112
|
+
tagName
|
|
1113
|
+
createdAt${withConfig ? "\n config" : ""}
|
|
1114
|
+
build {
|
|
1115
|
+
id
|
|
1116
|
+
imageName
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
`;
|
|
1122
|
+
}
|
|
1123
|
+
function serializeBoolean(value, input) {
|
|
1124
|
+
if (value === "true") return input.trueValue ?? "true";
|
|
1125
|
+
if (value === "false") return input.falseValue ?? "false";
|
|
1126
|
+
return value;
|
|
1127
|
+
}
|
|
1128
|
+
function buildHubEnv(config, overrides) {
|
|
1129
|
+
const env = [];
|
|
1130
|
+
const missingRequired = [];
|
|
1131
|
+
const remaining = { ...overrides };
|
|
1132
|
+
for (const entry of config.env ?? []) {
|
|
1133
|
+
const input = entry.input ?? {};
|
|
1134
|
+
const override = entry.key in remaining ? remaining[entry.key] : void 0;
|
|
1135
|
+
delete remaining[entry.key];
|
|
1136
|
+
if (override !== void 0 && override !== "") {
|
|
1137
|
+
env.push({
|
|
1138
|
+
key: entry.key,
|
|
1139
|
+
value: input.type === "boolean" ? serializeBoolean(override, input) : override
|
|
1140
|
+
});
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
if (input.default !== void 0 && input.default !== null) {
|
|
1144
|
+
env.push({
|
|
1145
|
+
key: entry.key,
|
|
1146
|
+
value: typeof input.default === "boolean" ? input.default ? input.trueValue ?? "true" : input.falseValue ?? "false" : String(input.default)
|
|
1147
|
+
});
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
if (input.required) {
|
|
1151
|
+
missingRequired.push(entry.key);
|
|
1152
|
+
env.push({ key: entry.key, value: "" });
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
for (const [key, value] of Object.entries(remaining)) {
|
|
1157
|
+
env.push({ key, value });
|
|
1158
|
+
}
|
|
1159
|
+
return { env, missingRequired };
|
|
1160
|
+
}
|
|
1161
|
+
function minCudaVersion(versions) {
|
|
1162
|
+
if (!versions || versions.length === 0) return null;
|
|
1163
|
+
const compare = (a, b) => {
|
|
1164
|
+
const pa = a.split(".").map(Number);
|
|
1165
|
+
const pb = b.split(".").map(Number);
|
|
1166
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
1167
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
1168
|
+
if (d !== 0) return d;
|
|
1169
|
+
}
|
|
1170
|
+
return 0;
|
|
1171
|
+
};
|
|
1172
|
+
return [...versions].sort(compare)[0];
|
|
1173
|
+
}
|
|
1174
|
+
function randomSuffix() {
|
|
1175
|
+
return Math.random().toString(36).slice(2, 8);
|
|
1176
|
+
}
|
|
1177
|
+
function registerHubTools(server, rt) {
|
|
1178
|
+
const { graphql, graphqlAuthed, jsonReply: jsonReply2 } = rt;
|
|
1179
|
+
server.tool(
|
|
1180
|
+
"list-hub-repos",
|
|
1181
|
+
"List repos published to the Runpod Hub (prebuilt Serverless workers and Pod templates, e.g. vLLM, ComfyUI). Public catalog \u2014 no auth required. Each result includes the currently listed release with its hubReleaseId and prebuilt image name. Results are sorted by deploy count (most popular first). Set includeConfig:true to also return the release config (hardware requirements and environment-variable schema) \u2014 it is large, so prefer requesting it for a single repo via the repoOwner/searchTerm filters.",
|
|
1182
|
+
{
|
|
1183
|
+
...listPaginationParams,
|
|
1184
|
+
searchTerm: import_zod4.z.string().optional().describe(
|
|
1185
|
+
"Case-insensitive search across title, description, repo name, owner, and tags (e.g. 'vllm', 'comfyui', 'fine-tuning')"
|
|
1186
|
+
),
|
|
1187
|
+
category: import_zod4.z.string().optional().describe(
|
|
1188
|
+
"Filter by category (e.g. 'language', 'image', 'audio', 'video', 'embedding')"
|
|
1189
|
+
),
|
|
1190
|
+
type: import_zod4.z.enum(["SERVERLESS", "POD"]).optional().describe(
|
|
1191
|
+
"Filter by listing type: SERVERLESS (endpoint workers) or POD (pod templates)"
|
|
1192
|
+
),
|
|
1193
|
+
repoOwner: import_zod4.z.string().optional().describe(
|
|
1194
|
+
"Filter to listings from this GitHub owner/org (e.g. 'runpod-workers', 'axolotl-ai-cloud')"
|
|
1195
|
+
),
|
|
1196
|
+
includeConfig: import_zod4.z.boolean().optional().describe(
|
|
1197
|
+
"Include the listed release config (hardware requirements + env-var input schema, parsed from .runpod/hub.json). Large \u2014 off by default."
|
|
1198
|
+
)
|
|
1199
|
+
},
|
|
1200
|
+
{ title: "List Hub repos", ...READ_ONLY },
|
|
1201
|
+
async (params) => {
|
|
1202
|
+
const data = await graphql(
|
|
1203
|
+
listingsQuery(params.includeConfig === true)
|
|
1204
|
+
);
|
|
1205
|
+
let listings = data.listings;
|
|
1206
|
+
if (params.type) {
|
|
1207
|
+
listings = listings.filter(
|
|
1208
|
+
(l) => (l.type ?? "").toUpperCase() === params.type
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
if (params.category) {
|
|
1212
|
+
const term = params.category.toLowerCase();
|
|
1213
|
+
listings = listings.filter(
|
|
1214
|
+
(l) => (l.category ?? "").toLowerCase() === term
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
if (params.repoOwner) {
|
|
1218
|
+
const term = params.repoOwner.toLowerCase();
|
|
1219
|
+
listings = listings.filter(
|
|
1220
|
+
(l) => (l.repoOwner ?? "").toLowerCase() === term
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
if (params.searchTerm) {
|
|
1224
|
+
const term = params.searchTerm.toLowerCase();
|
|
1225
|
+
listings = listings.filter(
|
|
1226
|
+
(l) => [
|
|
1227
|
+
l.title,
|
|
1228
|
+
l.description,
|
|
1229
|
+
l.repoName,
|
|
1230
|
+
l.repoOwner,
|
|
1231
|
+
...l.tags ?? []
|
|
1232
|
+
].some((field) => (field ?? "").toLowerCase().includes(term))
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
listings = [...listings].sort(
|
|
1236
|
+
(a, b) => (b.deploys ?? 0) - (a.deploys ?? 0) || a.id.localeCompare(b.id)
|
|
1237
|
+
);
|
|
1238
|
+
const result = listings.map((l) => ({
|
|
1239
|
+
id: l.id,
|
|
1240
|
+
repo: `${l.repoOwner}/${l.repoName}`,
|
|
1241
|
+
title: l.title,
|
|
1242
|
+
description: l.description,
|
|
1243
|
+
type: l.type,
|
|
1244
|
+
category: l.category,
|
|
1245
|
+
tags: l.tags,
|
|
1246
|
+
language: l.language,
|
|
1247
|
+
stars: l.stars,
|
|
1248
|
+
deploys: l.deploys,
|
|
1249
|
+
views: l.views,
|
|
1250
|
+
githubUrl: `https://github.com/${l.repoOwner}/${l.repoName}`,
|
|
1251
|
+
hubUrl: `https://console.runpod.io/hub/${l.repoOwner}/${l.repoName}`,
|
|
1252
|
+
createdAt: l.createdAt,
|
|
1253
|
+
updatedAt: l.updatedAt,
|
|
1254
|
+
listedRelease: l.listedRelease ? {
|
|
1255
|
+
hubReleaseId: l.listedRelease.id,
|
|
1256
|
+
name: l.listedRelease.name,
|
|
1257
|
+
tagName: l.listedRelease.tagName,
|
|
1258
|
+
createdAt: l.listedRelease.createdAt,
|
|
1259
|
+
imageName: l.listedRelease.build?.imageName,
|
|
1260
|
+
...params.includeConfig ? { config: parseReleaseConfig(l.listedRelease.config) } : {}
|
|
1261
|
+
} : null
|
|
1262
|
+
}));
|
|
1263
|
+
return capListResult(result, {
|
|
1264
|
+
limit: params.limit,
|
|
1265
|
+
cursor: params.cursor
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
);
|
|
1269
|
+
server.tool(
|
|
1270
|
+
"deploy-hub-repo",
|
|
1271
|
+
"Deploy a Runpod Hub repo's listed release as a new Serverless endpoint (the same as clicking Deploy on the Hub). Identify the repo by `repo` (\"owner/name\" from list-hub-repos) or by `hubReleaseId`. The release supplies the prebuilt image, container disk, CUDA constraints, and env-var defaults; pass `env` to override or fill in values (required keys without a default must be provided \u2014 check list-hub-repos with includeConfig:true for the schema). GPU selection comes from the release config when it specifies one; otherwise pass gpuIds (GPU pool names, e.g. 'ADA_24' or 'ADA_80_PRO,AMPERE_80'). Uses the authenticated GraphQL API (no REST home for Hub deploys yet), so it works the same on v1 and v2.",
|
|
1272
|
+
{
|
|
1273
|
+
repo: import_zod4.z.string().optional().describe(
|
|
1274
|
+
`The Hub repo to deploy as "owner/name" (e.g. 'runpod-workers/worker-comfyui'). Deploys its currently listed release. Provide this or hubReleaseId.`
|
|
1275
|
+
),
|
|
1276
|
+
hubReleaseId: import_zod4.z.string().optional().describe(
|
|
1277
|
+
"The Hub release ID to deploy (from list-hub-repos listedRelease.hubReleaseId). Provide this or repo."
|
|
1278
|
+
),
|
|
1279
|
+
name: import_zod4.z.string().optional().describe(
|
|
1280
|
+
"Name for the new endpoint. Defaults to '<listing title> <release tag>'."
|
|
1281
|
+
),
|
|
1282
|
+
env: import_zod4.z.record(import_zod4.z.string()).optional().describe(
|
|
1283
|
+
"Environment variable overrides, merged over the release config defaults. Keys not in the release schema are passed through as-is."
|
|
1284
|
+
),
|
|
1285
|
+
gpuIds: import_zod4.z.string().optional().describe(
|
|
1286
|
+
"Comma-separated GPU pool names for workers (e.g. 'ADA_24' or 'ADA_80_PRO,AMPERE_80'). To pin specific GPU SKUs within a pool, append exclusions prefixed with '-' using GPU type ids from list-gpu-types (e.g. 'AMPERE_16,-NVIDIA RTX 2000 Ada Generation,-NVIDIA RTX A4500' allows only the pool's remaining SKUs). Defaults to the release config's gpuIds; required when the config does not specify one."
|
|
1287
|
+
),
|
|
1288
|
+
gpuCount: import_zod4.z.number().int().positive().optional().describe("GPUs per worker. Defaults to the release config (or 1)."),
|
|
1289
|
+
containerDiskInGb: import_zod4.z.number().int().positive().optional().describe("Container disk size in GB. Defaults to the release config."),
|
|
1290
|
+
workersMin: import_zod4.z.number().int().min(0).optional().describe("Minimum workers (default 0 \u2014 scale to zero)."),
|
|
1291
|
+
workersMax: import_zod4.z.number().int().positive().optional().describe("Maximum workers (default: account default)."),
|
|
1292
|
+
idleTimeout: import_zod4.z.number().int().positive().optional().describe("Seconds a worker idles before scaling down (default 5)."),
|
|
1293
|
+
scalerType: import_zod4.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type (default QUEUE_DELAY)."),
|
|
1294
|
+
scalerValue: import_zod4.z.number().optional().describe("Autoscaler target value (default 4)."),
|
|
1295
|
+
executionTimeoutMs: import_zod4.z.number().int().positive().optional().describe("Per-job execution timeout in ms (default 600000)."),
|
|
1296
|
+
flashboot: import_zod4.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (default FLASHBOOT).")
|
|
1297
|
+
},
|
|
1298
|
+
{ title: "Deploy Hub repo", ...WRITE },
|
|
1299
|
+
async (params) => {
|
|
1300
|
+
if (!params.repo && !params.hubReleaseId) {
|
|
1301
|
+
return jsonReply2({
|
|
1302
|
+
error: 'Provide either repo ("owner/name") or hubReleaseId. Use list-hub-repos to discover both.'
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
const catalog = await graphql(listingsQuery(true));
|
|
1306
|
+
let listing;
|
|
1307
|
+
if (params.hubReleaseId) {
|
|
1308
|
+
listing = catalog.listings.find(
|
|
1309
|
+
(l) => l.listedRelease?.id === params.hubReleaseId
|
|
1310
|
+
);
|
|
1311
|
+
} else {
|
|
1312
|
+
const repoKey = params.repo.toLowerCase();
|
|
1313
|
+
listing = catalog.listings.find(
|
|
1314
|
+
(l) => `${l.repoOwner}/${l.repoName}`.toLowerCase() === repoKey
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
if (!listing) {
|
|
1318
|
+
return jsonReply2({
|
|
1319
|
+
error: `No Hub listing found for ${params.hubReleaseId ? `hubReleaseId "${params.hubReleaseId}" (only each repo's currently listed release is deployable)` : `repo "${params.repo}"`}. Use list-hub-repos to see the catalog.`
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
const release = listing.listedRelease;
|
|
1323
|
+
if (!release?.build?.imageName) {
|
|
1324
|
+
return jsonReply2({
|
|
1325
|
+
error: `Hub repo ${listing.repoOwner}/${listing.repoName} has no listed release with a built image, so it cannot be deployed.`
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
if ((listing.type ?? "").toUpperCase() !== "SERVERLESS") {
|
|
1329
|
+
return jsonReply2({
|
|
1330
|
+
error: `Hub repo ${listing.repoOwner}/${listing.repoName} is a ${listing.type} listing \u2014 only SERVERLESS listings deploy as endpoints.`
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
const config = parseReleaseConfig(release.config) ?? {};
|
|
1334
|
+
const gpuIds = params.gpuIds ?? config.gpuIds;
|
|
1335
|
+
if (!gpuIds) {
|
|
1336
|
+
return jsonReply2({
|
|
1337
|
+
error: 'This release does not specify a GPU pool \u2014 pass gpuIds (comma-separated pool names, e.g. "ADA_80_PRO,AMPERE_80"; see the pool field on list-gpu-types).'
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
const { env, missingRequired } = buildHubEnv(config, params.env ?? {});
|
|
1341
|
+
if (missingRequired.length > 0) {
|
|
1342
|
+
return jsonReply2({
|
|
1343
|
+
error: `Missing required environment variables for this release: ${missingRequired.join(
|
|
1344
|
+
", "
|
|
1345
|
+
)}. Pass them via the env parameter.`
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
const endpointName = params.name ?? `${listing.title} ${release.tagName}`;
|
|
1349
|
+
const minCuda = minCudaVersion(config.allowedCudaVersions);
|
|
1350
|
+
const input = {
|
|
1351
|
+
name: endpointName,
|
|
1352
|
+
hubReleaseId: release.id,
|
|
1353
|
+
type: "QB",
|
|
1354
|
+
gpuIds,
|
|
1355
|
+
gpuCount: params.gpuCount ?? config.gpuCount ?? 1,
|
|
1356
|
+
workersMin: params.workersMin ?? 0,
|
|
1357
|
+
workersMax: params.workersMax ?? null,
|
|
1358
|
+
idleTimeout: params.idleTimeout ?? 5,
|
|
1359
|
+
scalerType: params.scalerType ?? "QUEUE_DELAY",
|
|
1360
|
+
scalerValue: params.scalerValue ?? 4,
|
|
1361
|
+
executionTimeoutMs: params.executionTimeoutMs ?? 6e5,
|
|
1362
|
+
flashBootType: params.flashboot ?? "FLASHBOOT",
|
|
1363
|
+
locations: null,
|
|
1364
|
+
networkVolumeIds: null,
|
|
1365
|
+
compliance: [],
|
|
1366
|
+
modelReferences: [],
|
|
1367
|
+
...minCuda ? {
|
|
1368
|
+
minCudaVersion: minCuda,
|
|
1369
|
+
allowedCudaVersions: config.allowedCudaVersions.join(",")
|
|
1370
|
+
} : {},
|
|
1371
|
+
template: {
|
|
1372
|
+
name: `${endpointName}__template__${randomSuffix()}`,
|
|
1373
|
+
imageName: release.build.imageName,
|
|
1374
|
+
containerDiskInGb: params.containerDiskInGb ?? config.containerDiskInGb ?? 20,
|
|
1375
|
+
containerRegistryAuthId: "",
|
|
1376
|
+
dockerArgs: "",
|
|
1377
|
+
startScript: "",
|
|
1378
|
+
ports: "",
|
|
1379
|
+
env
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
const data = await graphqlAuthed(
|
|
1383
|
+
`
|
|
1384
|
+
mutation saveEndpoint($input: EndpointInput!) {
|
|
1385
|
+
saveEndpoint(input: $input) {
|
|
1386
|
+
id
|
|
1387
|
+
name
|
|
1388
|
+
gpuIds
|
|
1389
|
+
gpuCount
|
|
1390
|
+
workersMin
|
|
1391
|
+
workersMax
|
|
1392
|
+
idleTimeout
|
|
1393
|
+
scalerType
|
|
1394
|
+
scalerValue
|
|
1395
|
+
flashBootType
|
|
1396
|
+
templateId
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
`,
|
|
1400
|
+
{ input }
|
|
1401
|
+
);
|
|
1402
|
+
return jsonReply2({
|
|
1403
|
+
endpoint: data.saveEndpoint,
|
|
1404
|
+
deployed: {
|
|
1405
|
+
repo: `${listing.repoOwner}/${listing.repoName}`,
|
|
1406
|
+
release: release.tagName,
|
|
1407
|
+
hubReleaseId: release.id,
|
|
1408
|
+
imageName: release.build.imageName
|
|
1409
|
+
},
|
|
1410
|
+
note: `Endpoint created. Submit jobs with run-endpoint/runsync-endpoint using endpointId "${data.saveEndpoint.id}".`
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// src/tools/public-endpoints.ts
|
|
1417
|
+
var import_zod5 = require("zod");
|
|
1418
|
+
function parseMetadata(metadata) {
|
|
1419
|
+
if (!metadata) return {};
|
|
1420
|
+
try {
|
|
1421
|
+
const parsed = JSON.parse(metadata);
|
|
1422
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1423
|
+
} catch {
|
|
1424
|
+
return {};
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
function registerPublicEndpointTools(server, rt) {
|
|
1428
|
+
const { graphql } = rt;
|
|
1429
|
+
server.tool(
|
|
1430
|
+
"list-public-endpoints",
|
|
1431
|
+
"List Runpod Public Endpoints \u2014 managed, pay-per-use model APIs (text, image, video, audio) that require no deployment. Public catalog, no auth required. Each result includes the endpointId to call with run-endpoint/runsync-endpoint (or via https://api.runpod.ai/v2/{endpointId}), the model name, modality, owner, and pricing. Only live endpoints are returned by default; set includeOffline:true to also list ones that are not currently live.",
|
|
1432
|
+
{
|
|
1433
|
+
...listPaginationParams,
|
|
1434
|
+
searchTerm: import_zod5.z.string().optional().describe(
|
|
1435
|
+
"Case-insensitive search across display name, model name, endpoint id, description, owner, and tag (e.g. 'kimi', 'video', 'flux')"
|
|
1436
|
+
),
|
|
1437
|
+
modality: import_zod5.z.string().optional().describe(
|
|
1438
|
+
"Filter by modality/category (the metadata source field, e.g. 'language', 'image', 'video', 'audio')"
|
|
1439
|
+
),
|
|
1440
|
+
owner: import_zod5.z.string().optional().describe(
|
|
1441
|
+
"Filter by model owner (e.g. 'moonshot', 'minimax', 'google')"
|
|
1442
|
+
),
|
|
1443
|
+
includeOffline: import_zod5.z.boolean().optional().describe(
|
|
1444
|
+
"Include endpoints that are not currently live (isLive:false). Default false \u2014 only live endpoints are listed."
|
|
1445
|
+
)
|
|
1446
|
+
},
|
|
1447
|
+
{ title: "List public endpoints", ...READ_ONLY },
|
|
1448
|
+
async (params) => {
|
|
1449
|
+
const data = await graphql(`
|
|
1450
|
+
query {
|
|
1451
|
+
allAiApiPublicConfigs {
|
|
1452
|
+
id
|
|
1453
|
+
aiApiId
|
|
1454
|
+
modelName
|
|
1455
|
+
displayName
|
|
1456
|
+
description
|
|
1457
|
+
metadata
|
|
1458
|
+
isLive
|
|
1459
|
+
createdAt
|
|
1460
|
+
updatedAt
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
`);
|
|
1464
|
+
let configs = data.allAiApiPublicConfigs.map((c) => ({
|
|
1465
|
+
config: c,
|
|
1466
|
+
meta: parseMetadata(c.metadata)
|
|
1467
|
+
}));
|
|
1468
|
+
if (!params.includeOffline) {
|
|
1469
|
+
configs = configs.filter(({ config }) => config.isLive);
|
|
1470
|
+
}
|
|
1471
|
+
if (params.modality) {
|
|
1472
|
+
const term = params.modality.toLowerCase();
|
|
1473
|
+
configs = configs.filter(
|
|
1474
|
+
({ meta }) => (meta.source ?? "").toLowerCase() === term
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
if (params.owner) {
|
|
1478
|
+
const term = params.owner.toLowerCase();
|
|
1479
|
+
configs = configs.filter(
|
|
1480
|
+
({ meta }) => (meta.owner ?? "").toLowerCase() === term
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
if (params.searchTerm) {
|
|
1484
|
+
const term = params.searchTerm.toLowerCase();
|
|
1485
|
+
configs = configs.filter(
|
|
1486
|
+
({ config, meta }) => [
|
|
1487
|
+
config.displayName,
|
|
1488
|
+
config.modelName,
|
|
1489
|
+
config.aiApiId,
|
|
1490
|
+
config.description,
|
|
1491
|
+
meta.owner,
|
|
1492
|
+
meta.tag,
|
|
1493
|
+
meta.source
|
|
1494
|
+
].some((field) => (field ?? "").toLowerCase().includes(term))
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
configs = [...configs].sort(
|
|
1498
|
+
(a, b) => a.config.displayName.localeCompare(b.config.displayName)
|
|
1499
|
+
);
|
|
1500
|
+
const result = configs.map(({ config, meta }) => ({
|
|
1501
|
+
endpointId: config.aiApiId,
|
|
1502
|
+
displayName: config.displayName,
|
|
1503
|
+
modelName: config.modelName,
|
|
1504
|
+
description: config.description,
|
|
1505
|
+
modality: meta.source,
|
|
1506
|
+
tag: meta.tag,
|
|
1507
|
+
owner: meta.owner,
|
|
1508
|
+
pricing: meta.priceString,
|
|
1509
|
+
isLive: config.isLive,
|
|
1510
|
+
baseUrl: `https://api.runpod.ai/v2/${config.aiApiId}`,
|
|
1511
|
+
createdAt: config.createdAt,
|
|
1512
|
+
updatedAt: config.updatedAt
|
|
1513
|
+
}));
|
|
1514
|
+
return capListResult(result, {
|
|
1515
|
+
limit: params.limit,
|
|
1516
|
+
cursor: params.cursor
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// src/tools/endpoint-gpus.ts
|
|
1523
|
+
var import_zod6 = require("zod");
|
|
1524
|
+
function registerEndpointGpuTools(server, rt) {
|
|
1525
|
+
const { graphqlAuthed, jsonReply: jsonReply2 } = rt;
|
|
1526
|
+
server.tool(
|
|
1527
|
+
"set-endpoint-gpus",
|
|
1528
|
+
"Set which GPUs a Serverless endpoint's workers run on \u2014 including pinning specific GPU SKUs, which create-endpoint/update-endpoint cannot express. Provide either a raw gpuIds string, or pools plus optional excludeGpuTypeIds (GPU type ids from list-gpu-types) and the exclusion string is built for you: a pool allows every SKU in it, and excluding all but one SKU pins that SKU exactly (e.g. pools:['AMPERE_16'], excludeGpuTypeIds:['NVIDIA RTX 2000 Ada Generation','NVIDIA RTX 4000 Ada Generation','NVIDIA RTX A4500'] pins RTX A4000). All other endpoint settings (workers, scaling, timeouts, template) are read first and preserved. Works on any Serverless endpoint via the authenticated GraphQL API.",
|
|
1529
|
+
{
|
|
1530
|
+
endpointId: import_zod6.z.string().describe("ID of the Serverless endpoint to update"),
|
|
1531
|
+
gpuIds: import_zod6.z.string().optional().describe(
|
|
1532
|
+
"Raw gpuIds string, e.g. 'ADA_24' or 'AMPERE_16,-NVIDIA RTX A4500'. Takes precedence over pools/excludeGpuTypeIds."
|
|
1533
|
+
),
|
|
1534
|
+
pools: import_zod6.z.array(import_zod6.z.string()).optional().describe(
|
|
1535
|
+
"GPU pool names workers may use (e.g. ['ADA_80_PRO','AMPERE_80']). The pool field from list-gpu-types."
|
|
1536
|
+
),
|
|
1537
|
+
excludeGpuTypeIds: import_zod6.z.array(import_zod6.z.string()).optional().describe(
|
|
1538
|
+
"GPU type ids to exclude from the allowed pools (e.g. ['NVIDIA H100 NVL']). Use with pools to pin specific SKUs."
|
|
1539
|
+
),
|
|
1540
|
+
gpuCount: import_zod6.z.number().int().positive().optional().describe("GPUs per worker. Omit to keep the current value."),
|
|
1541
|
+
minCudaVersion: import_zod6.z.string().optional().describe(
|
|
1542
|
+
"Minimum host CUDA version workers may run on (e.g. '12.4'). Omit to keep the current value."
|
|
1543
|
+
),
|
|
1544
|
+
allowedCudaVersions: import_zod6.z.string().optional().describe(
|
|
1545
|
+
"Comma-separated allowed host CUDA versions (e.g. '12.8,12.7,12.6'). Omit to keep the current value. CUDA compatibility is part of GPU selection \u2014 a narrow list can leave an endpoint unable to schedule workers."
|
|
1546
|
+
)
|
|
1547
|
+
},
|
|
1548
|
+
{ title: "Set endpoint GPUs", ...WRITE, idempotentHint: true },
|
|
1549
|
+
async (params) => {
|
|
1550
|
+
const gpuIds = params.gpuIds ?? (params.pools && params.pools.length > 0 ? [
|
|
1551
|
+
...params.pools,
|
|
1552
|
+
...(params.excludeGpuTypeIds ?? []).map((id) => `-${id}`)
|
|
1553
|
+
].join(",") : void 0);
|
|
1554
|
+
if (!gpuIds) {
|
|
1555
|
+
return jsonReply2({
|
|
1556
|
+
error: "Provide gpuIds (raw string) or pools (with optional excludeGpuTypeIds). See list-gpu-types for pool names and GPU type ids."
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
const data = await graphqlAuthed(`
|
|
1560
|
+
query {
|
|
1561
|
+
myself {
|
|
1562
|
+
endpoints {
|
|
1563
|
+
id
|
|
1564
|
+
name
|
|
1565
|
+
gpuIds
|
|
1566
|
+
gpuCount
|
|
1567
|
+
workersMin
|
|
1568
|
+
workersMax
|
|
1569
|
+
idleTimeout
|
|
1570
|
+
scalerType
|
|
1571
|
+
scalerValue
|
|
1572
|
+
executionTimeoutMs
|
|
1573
|
+
flashBootType
|
|
1574
|
+
type
|
|
1575
|
+
locations
|
|
1576
|
+
templateId
|
|
1577
|
+
allowedCudaVersions
|
|
1578
|
+
minCudaVersion
|
|
1579
|
+
compliance
|
|
1580
|
+
modelReferences
|
|
1581
|
+
networkVolumeIds {
|
|
1582
|
+
networkVolumeId
|
|
1583
|
+
dataCenterId
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
`);
|
|
1589
|
+
const current = data.myself.endpoints.find(
|
|
1590
|
+
(e) => e.id === params.endpointId
|
|
1591
|
+
);
|
|
1592
|
+
if (!current) {
|
|
1593
|
+
return jsonReply2({
|
|
1594
|
+
error: `No Serverless endpoint found with id "${params.endpointId}". Use list-endpoints to see your endpoints.`
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
const input = {
|
|
1598
|
+
id: current.id,
|
|
1599
|
+
name: current.name,
|
|
1600
|
+
gpuIds,
|
|
1601
|
+
gpuCount: params.gpuCount ?? current.gpuCount,
|
|
1602
|
+
workersMin: current.workersMin,
|
|
1603
|
+
workersMax: current.workersMax,
|
|
1604
|
+
idleTimeout: current.idleTimeout,
|
|
1605
|
+
scalerType: current.scalerType,
|
|
1606
|
+
scalerValue: current.scalerValue,
|
|
1607
|
+
executionTimeoutMs: current.executionTimeoutMs,
|
|
1608
|
+
flashBootType: current.flashBootType,
|
|
1609
|
+
type: current.type,
|
|
1610
|
+
locations: current.locations,
|
|
1611
|
+
networkVolumeIds: current.networkVolumeIds && current.networkVolumeIds.length > 0 ? (
|
|
1612
|
+
// Drop dataCenterId: NetworkVolumeIdsInput takes networkVolumeId
|
|
1613
|
+
// ONLY, and the read shape is rejected outright.
|
|
1614
|
+
current.networkVolumeIds.map((v) => ({
|
|
1615
|
+
networkVolumeId: v.networkVolumeId
|
|
1616
|
+
}))
|
|
1617
|
+
) : null
|
|
1618
|
+
};
|
|
1619
|
+
for (const [key, value] of Object.entries({
|
|
1620
|
+
templateId: current.templateId,
|
|
1621
|
+
allowedCudaVersions: params.allowedCudaVersions ?? current.allowedCudaVersions,
|
|
1622
|
+
minCudaVersion: params.minCudaVersion ?? current.minCudaVersion,
|
|
1623
|
+
compliance: current.compliance,
|
|
1624
|
+
modelReferences: current.modelReferences
|
|
1625
|
+
})) {
|
|
1626
|
+
if (value !== null && value !== void 0) input[key] = value;
|
|
1627
|
+
}
|
|
1628
|
+
const result = await graphqlAuthed(
|
|
1629
|
+
`
|
|
1630
|
+
mutation saveEndpoint($input: EndpointInput!) {
|
|
1631
|
+
saveEndpoint(input: $input) {
|
|
1632
|
+
id
|
|
1633
|
+
name
|
|
1634
|
+
gpuIds
|
|
1635
|
+
gpuCount
|
|
1636
|
+
workersMin
|
|
1637
|
+
workersMax
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
`,
|
|
1641
|
+
{ input }
|
|
890
1642
|
);
|
|
891
|
-
return jsonReply2(
|
|
1643
|
+
return jsonReply2({
|
|
1644
|
+
endpoint: result.saveEndpoint,
|
|
1645
|
+
previousGpuIds: current.gpuIds
|
|
1646
|
+
});
|
|
892
1647
|
}
|
|
893
1648
|
);
|
|
894
1649
|
}
|
|
895
1650
|
|
|
896
1651
|
// src/tools/pods.ts
|
|
897
|
-
var
|
|
1652
|
+
var import_zod7 = require("zod");
|
|
898
1653
|
function registerPodTools(server, rt) {
|
|
899
1654
|
const { jsonReply: jsonReply2, callRestUrl, backendFor, podAction, env } = rt;
|
|
900
1655
|
server.tool(
|
|
@@ -902,12 +1657,12 @@ function registerPodTools(server, rt) {
|
|
|
902
1657
|
"List your pods (running and stopped) with optional filters \u2014 compute type, GPU type, data center, name \u2014 plus machine/network-volume expansion. Paginated via limit/cursor.",
|
|
903
1658
|
{
|
|
904
1659
|
...listPaginationParams,
|
|
905
|
-
computeType:
|
|
906
|
-
gpuTypeId:
|
|
907
|
-
dataCenterId:
|
|
908
|
-
name:
|
|
909
|
-
includeMachine:
|
|
910
|
-
includeNetworkVolume:
|
|
1660
|
+
computeType: import_zod7.z.enum(["GPU", "CPU"]).optional().describe("Filter to only GPU or only CPU Pods"),
|
|
1661
|
+
gpuTypeId: import_zod7.z.array(import_zod7.z.string()).optional().describe("Filter to Pods with any of the listed GPU types"),
|
|
1662
|
+
dataCenterId: import_zod7.z.array(import_zod7.z.string()).optional().describe("Filter to Pods in any of the provided data centers"),
|
|
1663
|
+
name: import_zod7.z.string().optional().describe("Filter to Pods with the provided name"),
|
|
1664
|
+
includeMachine: import_zod7.z.boolean().optional().describe("Include information about the machine"),
|
|
1665
|
+
includeNetworkVolume: import_zod7.z.boolean().optional().describe("Include information about attached network volumes")
|
|
911
1666
|
},
|
|
912
1667
|
{ title: "List pods", ...READ_ONLY },
|
|
913
1668
|
async (params) => {
|
|
@@ -951,9 +1706,9 @@ function registerPodTools(server, rt) {
|
|
|
951
1706
|
"get-pod",
|
|
952
1707
|
"Get full details for one pod by id, optionally expanding machine and attached network-volume info.",
|
|
953
1708
|
{
|
|
954
|
-
podId:
|
|
955
|
-
includeMachine:
|
|
956
|
-
includeNetworkVolume:
|
|
1709
|
+
podId: import_zod7.z.string().describe("ID of the pod to retrieve"),
|
|
1710
|
+
includeMachine: import_zod7.z.boolean().optional().describe("Include information about the machine"),
|
|
1711
|
+
includeNetworkVolume: import_zod7.z.boolean().optional().describe("Include information about attached network volumes")
|
|
957
1712
|
},
|
|
958
1713
|
{ title: "Get pod", ...READ_ONLY },
|
|
959
1714
|
async (params) => {
|
|
@@ -981,26 +1736,44 @@ function registerPodTools(server, rt) {
|
|
|
981
1736
|
);
|
|
982
1737
|
server.tool(
|
|
983
1738
|
"create-pod",
|
|
984
|
-
'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). If the user
|
|
1739
|
+
'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). Pass either imageName or templateId (templateId deploys from an existing template \u2014 its name, image, start command, ports, env, disk, volume, and registry credential are used as defaults, and any field you also pass explicitly replaces the template value for that field, e.g. passing env replaces the whole env set rather than merging; pass containerRegistryAuthId to override or supply a private-image credential, or an empty string to deploy with none). If the user specifies neither an image nor a template, recommend the "Runpod Pytorch 2.8.0" image (runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404) as the default \u2014 it has the most up-to-date CUDA and PyTorch versions.',
|
|
985
1740
|
{
|
|
986
|
-
name:
|
|
987
|
-
imageName:
|
|
988
|
-
|
|
1741
|
+
name: import_zod7.z.string().optional().describe("Name for the pod"),
|
|
1742
|
+
imageName: import_zod7.z.string().optional().describe("Docker image to use. Optional when templateId is provided."),
|
|
1743
|
+
templateId: import_zod7.z.string().optional().describe(
|
|
1744
|
+
"Deploy from an existing template (see list-templates / get-template). The template supplies name, image, start command, ports, env, disk, volume, and registry credential as defaults; any field you also pass replaces the template value for that field (whole-field, not a merge). Override the registry credential with containerRegistryAuthId. Requires the v2 REST API. Still pass gpuTypeIds or computeType to choose the compute \u2014 a template does not pin the pod compute."
|
|
1745
|
+
),
|
|
1746
|
+
computeType: import_zod7.z.enum(["GPU", "CPU"]).optional().describe(
|
|
989
1747
|
"Pod type. On v2, required when not passing gpuTypeIds. 'CPU' is served by the v1 API for now (v2 has no CPU pods yet)."
|
|
990
1748
|
),
|
|
991
|
-
cloudType:
|
|
992
|
-
gpuTypeIds:
|
|
993
|
-
gpuCount:
|
|
994
|
-
containerDiskInGb:
|
|
995
|
-
volumeInGb:
|
|
996
|
-
volumeMountPath:
|
|
997
|
-
ports:
|
|
998
|
-
env:
|
|
999
|
-
|
|
1749
|
+
cloudType: import_zod7.z.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
|
|
1750
|
+
gpuTypeIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("List of acceptable GPU types"),
|
|
1751
|
+
gpuCount: import_zod7.z.number().optional().describe("Number of GPUs"),
|
|
1752
|
+
containerDiskInGb: import_zod7.z.number().optional().describe("Container disk size in GB"),
|
|
1753
|
+
volumeInGb: import_zod7.z.number().optional().describe("Volume size in GB"),
|
|
1754
|
+
volumeMountPath: import_zod7.z.string().optional().describe("Path to mount the volume"),
|
|
1755
|
+
ports: import_zod7.z.array(import_zod7.z.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
|
|
1756
|
+
env: import_zod7.z.record(import_zod7.z.string()).optional().describe("Environment variables"),
|
|
1757
|
+
containerRegistryAuthId: import_zod7.z.string().optional().describe(
|
|
1758
|
+
"Container registry credential ID for pulling a private image (see list-container-registry-auths). When deploying from a templateId, this overrides the credential the template carries; pass an empty string to deploy with NO credential (clears a template\u2019s inherited registry)."
|
|
1759
|
+
),
|
|
1760
|
+
dataCenterIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("List of data centers")
|
|
1000
1761
|
},
|
|
1001
1762
|
{ title: "Create pod", ...WRITE },
|
|
1002
1763
|
async (params) => {
|
|
1003
1764
|
const backend = backendFor("pods");
|
|
1765
|
+
if (params.templateId && backend.version !== "v2") {
|
|
1766
|
+
return jsonReply2({
|
|
1767
|
+
error: "Deploying a pod from a templateId is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or pass imageName instead.",
|
|
1768
|
+
status: 501
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
if (!params.imageName && !params.templateId) {
|
|
1772
|
+
return jsonReply2({
|
|
1773
|
+
error: "Provide imageName, or templateId to deploy from an existing template.",
|
|
1774
|
+
status: 400
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1004
1777
|
const hasGpuTypes = (params.gpuTypeIds?.length ?? 0) > 0;
|
|
1005
1778
|
const hasGpuCount = (params.gpuCount ?? 0) > 0;
|
|
1006
1779
|
const wantsCpu = params.computeType === "CPU";
|
|
@@ -1025,6 +1798,12 @@ function registerPodTools(server, rt) {
|
|
|
1025
1798
|
});
|
|
1026
1799
|
}
|
|
1027
1800
|
if (wantsCpu) {
|
|
1801
|
+
if (params.templateId) {
|
|
1802
|
+
return jsonReply2({
|
|
1803
|
+
error: "Template-based deploy is currently supported only for GPU pods on the v2 API. Create the CPU pod with an explicit imageName instead.",
|
|
1804
|
+
status: 400
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1028
1807
|
try {
|
|
1029
1808
|
const result = await callRestUrl(
|
|
1030
1809
|
`${restV1Base(env)}/pods`,
|
|
@@ -1049,7 +1828,24 @@ function registerPodTools(server, rt) {
|
|
|
1049
1828
|
}
|
|
1050
1829
|
}
|
|
1051
1830
|
}
|
|
1052
|
-
|
|
1831
|
+
let body = backend.mapCreate(params);
|
|
1832
|
+
if (params.templateId) {
|
|
1833
|
+
let template;
|
|
1834
|
+
try {
|
|
1835
|
+
template = await callRestUrl(
|
|
1836
|
+
`${restV2Base(env)}/templates/${params.templateId}`
|
|
1837
|
+
);
|
|
1838
|
+
} catch (error) {
|
|
1839
|
+
if (error instanceof HttpError) {
|
|
1840
|
+
return jsonReply2({
|
|
1841
|
+
error: `Failed to load template ${params.templateId}: ${error.message}`,
|
|
1842
|
+
status: error.status
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
throw error;
|
|
1846
|
+
}
|
|
1847
|
+
body = { ...podBodyFromTemplate(template), ...body };
|
|
1848
|
+
}
|
|
1053
1849
|
try {
|
|
1054
1850
|
const result = await callRestUrl(
|
|
1055
1851
|
`${backend.base}${backend.list}`,
|
|
@@ -1064,11 +1860,8 @@ function registerPodTools(server, rt) {
|
|
|
1064
1860
|
}
|
|
1065
1861
|
return jsonReply2(result);
|
|
1066
1862
|
} catch (error) {
|
|
1067
|
-
if (error instanceof HttpError
|
|
1068
|
-
return jsonReply2({
|
|
1069
|
-
error: "CPU pods are not yet supported on the v2 REST API. Use a GPU pod, or create the CPU pod via the v1 API (set RUNPOD_REST_VERSION=v1).",
|
|
1070
|
-
status: 501
|
|
1071
|
-
});
|
|
1863
|
+
if (error instanceof HttpError) {
|
|
1864
|
+
return jsonReply2({ error: error.message, status: error.status });
|
|
1072
1865
|
}
|
|
1073
1866
|
throw error;
|
|
1074
1867
|
}
|
|
@@ -1078,14 +1871,14 @@ function registerPodTools(server, rt) {
|
|
|
1078
1871
|
"update-pod",
|
|
1079
1872
|
"Update a pod's mutable fields (name, image, container disk, volume, ports, env). Only the fields you provide change.",
|
|
1080
1873
|
{
|
|
1081
|
-
podId:
|
|
1082
|
-
name:
|
|
1083
|
-
imageName:
|
|
1084
|
-
containerDiskInGb:
|
|
1085
|
-
volumeInGb:
|
|
1086
|
-
volumeMountPath:
|
|
1087
|
-
ports:
|
|
1088
|
-
env:
|
|
1874
|
+
podId: import_zod7.z.string().describe("ID of the pod to update"),
|
|
1875
|
+
name: import_zod7.z.string().optional().describe("New name for the pod"),
|
|
1876
|
+
imageName: import_zod7.z.string().optional().describe("New Docker image"),
|
|
1877
|
+
containerDiskInGb: import_zod7.z.number().optional().describe("New container disk size in GB"),
|
|
1878
|
+
volumeInGb: import_zod7.z.number().optional().describe("New volume size in GB"),
|
|
1879
|
+
volumeMountPath: import_zod7.z.string().optional().describe("New path to mount the volume"),
|
|
1880
|
+
ports: import_zod7.z.array(import_zod7.z.string()).optional().describe("New ports to expose"),
|
|
1881
|
+
env: import_zod7.z.record(import_zod7.z.string()).optional().describe("New environment variables")
|
|
1089
1882
|
},
|
|
1090
1883
|
{ title: "Update pod", ...WRITE, idempotentHint: true },
|
|
1091
1884
|
async (params) => {
|
|
@@ -1104,7 +1897,7 @@ function registerPodTools(server, rt) {
|
|
|
1104
1897
|
"start-pod",
|
|
1105
1898
|
"Start a stopped pod (resumes the same pod and its billing).",
|
|
1106
1899
|
{
|
|
1107
|
-
podId:
|
|
1900
|
+
podId: import_zod7.z.string().describe("ID of the pod to start")
|
|
1108
1901
|
},
|
|
1109
1902
|
{ title: "Start pod", ...WRITE, idempotentHint: true },
|
|
1110
1903
|
async (params) => {
|
|
@@ -1116,7 +1909,7 @@ function registerPodTools(server, rt) {
|
|
|
1116
1909
|
"stop-pod",
|
|
1117
1910
|
"Stop a running pod. The pod and its disk persist; GPU billing stops.",
|
|
1118
1911
|
{
|
|
1119
|
-
podId:
|
|
1912
|
+
podId: import_zod7.z.string().describe("ID of the pod to stop")
|
|
1120
1913
|
},
|
|
1121
1914
|
{ title: "Stop pod", ...WRITE, idempotentHint: true },
|
|
1122
1915
|
async (params) => {
|
|
@@ -1128,7 +1921,7 @@ function registerPodTools(server, rt) {
|
|
|
1128
1921
|
"restart-pod",
|
|
1129
1922
|
"Restart a running pod (single PodAction call). v2-only \u2014 on the v1 API returns a 501 notice (stop then start the pod instead).",
|
|
1130
1923
|
{
|
|
1131
|
-
podId:
|
|
1924
|
+
podId: import_zod7.z.string().describe("ID of the pod to restart")
|
|
1132
1925
|
},
|
|
1133
1926
|
{ title: "Restart pod", ...WRITE },
|
|
1134
1927
|
async (params) => {
|
|
@@ -1143,11 +1936,29 @@ function registerPodTools(server, rt) {
|
|
|
1143
1936
|
return jsonReply2(result);
|
|
1144
1937
|
}
|
|
1145
1938
|
);
|
|
1939
|
+
server.tool(
|
|
1940
|
+
"stream-pod-logs",
|
|
1941
|
+
"Fetch a bounded snapshot of a pod's live logs (container and/or system) via Server-Sent Events. v2-only \u2014 returns a 501 notice on the v1 API. 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).",
|
|
1942
|
+
{
|
|
1943
|
+
podId: import_zod7.z.string().describe("ID of the pod whose logs to stream"),
|
|
1944
|
+
...logStreamParams
|
|
1945
|
+
},
|
|
1946
|
+
{ title: "Stream pod logs", ...READ_ONLY },
|
|
1947
|
+
(params) => streamLogsReply(
|
|
1948
|
+
rt,
|
|
1949
|
+
{
|
|
1950
|
+
name: "stream-pod-logs",
|
|
1951
|
+
resource: "pods",
|
|
1952
|
+
logsUrl: (backend) => `${backend.base}${backend.get(encodeURIComponent(params.podId))}/logs`
|
|
1953
|
+
},
|
|
1954
|
+
params
|
|
1955
|
+
)
|
|
1956
|
+
);
|
|
1146
1957
|
server.tool(
|
|
1147
1958
|
"delete-pod",
|
|
1148
1959
|
"Permanently delete a pod and its data. This cannot be undone.",
|
|
1149
1960
|
{
|
|
1150
|
-
podId:
|
|
1961
|
+
podId: import_zod7.z.string().describe("ID of the pod to delete")
|
|
1151
1962
|
},
|
|
1152
1963
|
{ title: "Delete pod", ...DESTRUCTIVE },
|
|
1153
1964
|
async (params) => {
|
|
@@ -1162,16 +1973,27 @@ function registerPodTools(server, rt) {
|
|
|
1162
1973
|
}
|
|
1163
1974
|
|
|
1164
1975
|
// src/tools/endpoints.ts
|
|
1165
|
-
var
|
|
1976
|
+
var import_zod8 = require("zod");
|
|
1977
|
+
var MIN_QUEUE_DELAY = 0.5;
|
|
1978
|
+
var MIN_REQUEST_COUNT = 1;
|
|
1979
|
+
var MIN_IDLE_TIMEOUT = 1;
|
|
1980
|
+
var MAX_IDLE_TIMEOUT = 3600;
|
|
1981
|
+
function scalerValueError(scalerType, scalerValue) {
|
|
1982
|
+
if (scalerType !== "REQUEST_COUNT" || scalerValue === void 0) return;
|
|
1983
|
+
if (!Number.isInteger(scalerValue) || scalerValue < MIN_REQUEST_COUNT) {
|
|
1984
|
+
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}.`;
|
|
1985
|
+
}
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1166
1988
|
function registerEndpointTools(server, rt) {
|
|
1167
1989
|
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
1168
1990
|
server.tool(
|
|
1169
1991
|
"list-endpoints",
|
|
1170
|
-
"List your Serverless endpoints, optionally expanding template and worker details (v1 only). Paginated via limit/cursor.",
|
|
1992
|
+
"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`.",
|
|
1171
1993
|
{
|
|
1172
1994
|
...listPaginationParams,
|
|
1173
|
-
includeTemplate:
|
|
1174
|
-
includeWorkers:
|
|
1995
|
+
includeTemplate: import_zod8.z.boolean().optional().describe("Include template information (v1 only)"),
|
|
1996
|
+
includeWorkers: import_zod8.z.boolean().optional().describe("Include information about workers (v1 only)")
|
|
1175
1997
|
},
|
|
1176
1998
|
{ title: "List endpoints", ...READ_ONLY },
|
|
1177
1999
|
async (params) => {
|
|
@@ -1202,11 +2024,11 @@ function registerEndpointTools(server, rt) {
|
|
|
1202
2024
|
);
|
|
1203
2025
|
server.tool(
|
|
1204
2026
|
"get-endpoint",
|
|
1205
|
-
"Get one Serverless endpoint by id, optionally expanding template and worker details (v1 only).",
|
|
2027
|
+
"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.",
|
|
1206
2028
|
{
|
|
1207
|
-
endpointId:
|
|
1208
|
-
includeTemplate:
|
|
1209
|
-
includeWorkers:
|
|
2029
|
+
endpointId: import_zod8.z.string().describe("ID of the endpoint to retrieve"),
|
|
2030
|
+
includeTemplate: import_zod8.z.boolean().optional().describe("Include template information (v1 only)"),
|
|
2031
|
+
includeWorkers: import_zod8.z.boolean().optional().describe("Include information about workers (v1 only)")
|
|
1210
2032
|
},
|
|
1211
2033
|
{ title: "Get endpoint", ...READ_ONLY },
|
|
1212
2034
|
async (params) => {
|
|
@@ -1234,34 +2056,43 @@ function registerEndpointTools(server, rt) {
|
|
|
1234
2056
|
);
|
|
1235
2057
|
server.tool(
|
|
1236
2058
|
"create-endpoint",
|
|
1237
|
-
"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).",
|
|
2059
|
+
"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.",
|
|
1238
2060
|
{
|
|
1239
|
-
name:
|
|
2061
|
+
name: import_zod8.z.string().optional().describe("Name for the endpoint"),
|
|
1240
2062
|
// --- v2 inline-config fields ---
|
|
1241
|
-
imageName:
|
|
1242
|
-
|
|
2063
|
+
imageName: import_zod8.z.string().optional().describe("Docker image (v2). Required on v2 instead of a templateId."),
|
|
2064
|
+
endpointType: import_zod8.z.enum(["QUEUE", "LOAD_BALANCER"]).optional().describe(
|
|
2065
|
+
"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."
|
|
2066
|
+
),
|
|
2067
|
+
gpuPoolIds: import_zod8.z.array(import_zod8.z.string()).optional().describe(
|
|
1243
2068
|
'GPU pool names (v2, required). The `pool` field from list-gpu-types, e.g. ["AMPERE_80"]. NOT GPU type ids.'
|
|
1244
2069
|
),
|
|
1245
|
-
gpuCount:
|
|
1246
|
-
args:
|
|
1247
|
-
containerDiskInGb:
|
|
1248
|
-
ports:
|
|
1249
|
-
env:
|
|
1250
|
-
containerRegistryAuthId:
|
|
1251
|
-
networkVolumeIds:
|
|
1252
|
-
executionTimeoutMs:
|
|
1253
|
-
flashboot:
|
|
2070
|
+
gpuCount: import_zod8.z.number().optional().describe("GPUs per worker (v2)"),
|
|
2071
|
+
args: import_zod8.z.string().optional().describe("Container start command/args (v2)"),
|
|
2072
|
+
containerDiskInGb: import_zod8.z.number().optional().describe("Container disk size in GB (v2)"),
|
|
2073
|
+
ports: import_zod8.z.array(import_zod8.z.string()).optional().describe("Ports to expose (v2), e.g. ['8000/http']"),
|
|
2074
|
+
env: import_zod8.z.record(import_zod8.z.string()).optional().describe("Environment variables (v2)"),
|
|
2075
|
+
containerRegistryAuthId: import_zod8.z.string().optional().describe("Container registry auth id for a private image (v2)"),
|
|
2076
|
+
networkVolumeIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("Network volume ids to attach (v2)"),
|
|
2077
|
+
executionTimeoutMs: import_zod8.z.number().optional().describe("Per-job execution timeout in ms (v2)"),
|
|
2078
|
+
flashboot: import_zod8.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (v2)"),
|
|
1254
2079
|
// --- v1 template-based fields ---
|
|
1255
|
-
templateId:
|
|
1256
|
-
computeType:
|
|
1257
|
-
gpuTypeIds:
|
|
2080
|
+
templateId: import_zod8.z.string().optional().describe("Template ID (v1). Required on v1."),
|
|
2081
|
+
computeType: import_zod8.z.enum(["GPU", "CPU"]).optional().describe("GPU or CPU endpoint (v1)"),
|
|
2082
|
+
gpuTypeIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("List of acceptable GPU types (v1)"),
|
|
1258
2083
|
// --- shared autoscaling fields ---
|
|
1259
|
-
workersMin:
|
|
1260
|
-
workersMax:
|
|
1261
|
-
scalerType:
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
2084
|
+
workersMin: import_zod8.z.number().optional().describe("Minimum number of workers"),
|
|
2085
|
+
workersMax: import_zod8.z.number().optional().describe("Maximum number of workers"),
|
|
2086
|
+
scalerType: import_zod8.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe(
|
|
2087
|
+
"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."
|
|
2088
|
+
),
|
|
2089
|
+
scalerValue: import_zod8.z.number().min(MIN_QUEUE_DELAY).optional().describe(
|
|
2090
|
+
"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."
|
|
2091
|
+
),
|
|
2092
|
+
idleTimeout: import_zod8.z.number().int().min(MIN_IDLE_TIMEOUT).max(MAX_IDLE_TIMEOUT).optional().describe(
|
|
2093
|
+
"Idle timeout in seconds before scaling a worker down (1-3600). Does not apply to QUEUE endpoints scaling on REQUEST_COUNT."
|
|
2094
|
+
),
|
|
2095
|
+
dataCenterIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("List of preferred data centers")
|
|
1265
2096
|
},
|
|
1266
2097
|
{ title: "Create endpoint", ...WRITE },
|
|
1267
2098
|
async (params) => {
|
|
@@ -1285,6 +2116,19 @@ function registerEndpointTools(server, rt) {
|
|
|
1285
2116
|
status: 400
|
|
1286
2117
|
});
|
|
1287
2118
|
}
|
|
2119
|
+
if (params.endpointType === "LOAD_BALANCER" && params.scalerType === "QUEUE_DELAY") {
|
|
2120
|
+
return jsonReply2({
|
|
2121
|
+
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.",
|
|
2122
|
+
status: 400
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
const scalerError = scalerValueError(
|
|
2126
|
+
params.scalerType ?? (params.endpointType === "LOAD_BALANCER" ? "REQUEST_COUNT" : void 0),
|
|
2127
|
+
params.scalerValue
|
|
2128
|
+
);
|
|
2129
|
+
if (scalerError) {
|
|
2130
|
+
return jsonReply2({ error: scalerError, status: 400 });
|
|
2131
|
+
}
|
|
1288
2132
|
const body = backend.mapCreate(params);
|
|
1289
2133
|
const result2 = await callRestUrl(
|
|
1290
2134
|
`${backend.base}${backend.list}`,
|
|
@@ -1322,46 +2166,67 @@ function registerEndpointTools(server, rt) {
|
|
|
1322
2166
|
);
|
|
1323
2167
|
server.tool(
|
|
1324
2168
|
"update-endpoint",
|
|
1325
|
-
"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.",
|
|
2169
|
+
"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.",
|
|
1326
2170
|
{
|
|
1327
|
-
endpointId:
|
|
1328
|
-
name:
|
|
1329
|
-
workersMin:
|
|
1330
|
-
workersMax:
|
|
1331
|
-
idleTimeout:
|
|
1332
|
-
|
|
1333
|
-
|
|
2171
|
+
endpointId: import_zod8.z.string().describe("ID of the endpoint to update"),
|
|
2172
|
+
name: import_zod8.z.string().optional().describe("New name for the endpoint"),
|
|
2173
|
+
workersMin: import_zod8.z.number().optional().describe("New minimum number of workers"),
|
|
2174
|
+
workersMax: import_zod8.z.number().optional().describe("New maximum number of workers"),
|
|
2175
|
+
idleTimeout: import_zod8.z.number().int().min(MIN_IDLE_TIMEOUT).max(MAX_IDLE_TIMEOUT).optional().describe(
|
|
2176
|
+
"New idle timeout in seconds (1-3600). Does not apply to queue endpoints scaling on REQUEST_COUNT."
|
|
2177
|
+
),
|
|
2178
|
+
scalerType: import_zod8.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe(
|
|
2179
|
+
"New autoscaler signal. Switchable on queue endpoints; load-balancing endpoints only accept REQUEST_COUNT."
|
|
2180
|
+
),
|
|
2181
|
+
scalerValue: import_zod8.z.number().min(MIN_QUEUE_DELAY).optional().describe(
|
|
2182
|
+
"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."
|
|
2183
|
+
),
|
|
1334
2184
|
// --- v2-only inline-config fields ---
|
|
1335
|
-
imageName:
|
|
1336
|
-
gpuPoolIds:
|
|
1337
|
-
gpuCount:
|
|
1338
|
-
args:
|
|
1339
|
-
containerDiskInGb:
|
|
1340
|
-
ports:
|
|
1341
|
-
env:
|
|
1342
|
-
containerRegistryAuthId:
|
|
1343
|
-
networkVolumeIds:
|
|
1344
|
-
executionTimeoutMs:
|
|
1345
|
-
flashboot:
|
|
2185
|
+
imageName: import_zod8.z.string().optional().describe("New Docker image (v2)"),
|
|
2186
|
+
gpuPoolIds: import_zod8.z.array(import_zod8.z.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
|
|
2187
|
+
gpuCount: import_zod8.z.number().optional().describe("New GPUs per worker (v2)"),
|
|
2188
|
+
args: import_zod8.z.string().optional().describe("New container args (v2)"),
|
|
2189
|
+
containerDiskInGb: import_zod8.z.number().optional().describe("New container disk size in GB (v2)"),
|
|
2190
|
+
ports: import_zod8.z.array(import_zod8.z.string()).optional().describe("New ports (v2)"),
|
|
2191
|
+
env: import_zod8.z.record(import_zod8.z.string()).optional().describe("New environment variables (v2)"),
|
|
2192
|
+
containerRegistryAuthId: import_zod8.z.string().optional().describe("New container registry auth id (v2)"),
|
|
2193
|
+
networkVolumeIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("New network volume ids (v2)"),
|
|
2194
|
+
executionTimeoutMs: import_zod8.z.number().optional().describe("New per-job execution timeout in ms (v2)"),
|
|
2195
|
+
flashboot: import_zod8.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("New FlashBoot mode (v2)")
|
|
1346
2196
|
},
|
|
1347
2197
|
{ title: "Update endpoint", ...WRITE, idempotentHint: true },
|
|
1348
2198
|
async (params) => {
|
|
1349
2199
|
const { endpointId, ...updateParams } = params;
|
|
1350
2200
|
const backend = backendFor("endpoints");
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
"PATCH",
|
|
1355
|
-
|
|
2201
|
+
const url = `${backend.base}${backend.get(endpointId)}`;
|
|
2202
|
+
if (backend.version !== "v2") {
|
|
2203
|
+
const body2 = backend.mapUpdate(updateParams);
|
|
2204
|
+
return jsonReply2(await callRestUrl(url, "PATCH", body2));
|
|
2205
|
+
}
|
|
2206
|
+
let scalerType = updateParams.scalerType;
|
|
2207
|
+
if (scalerType === void 0 && updateParams.scalerValue !== void 0) {
|
|
2208
|
+
const current = await callRestUrl(url);
|
|
2209
|
+
scalerType = current?.scaling?.type === "REQUEST_COUNT" ? "REQUEST_COUNT" : "QUEUE_DELAY";
|
|
2210
|
+
}
|
|
2211
|
+
const scalerError = scalerValueError(
|
|
2212
|
+
scalerType,
|
|
2213
|
+
updateParams.scalerValue
|
|
1356
2214
|
);
|
|
1357
|
-
|
|
2215
|
+
if (scalerError) {
|
|
2216
|
+
return jsonReply2({ error: scalerError, status: 400 });
|
|
2217
|
+
}
|
|
2218
|
+
const body = backend.mapUpdate({
|
|
2219
|
+
...updateParams,
|
|
2220
|
+
scalerType
|
|
2221
|
+
});
|
|
2222
|
+
return jsonReply2(await callRestUrl(url, "PATCH", body));
|
|
1358
2223
|
}
|
|
1359
2224
|
);
|
|
1360
2225
|
server.tool(
|
|
1361
2226
|
"delete-endpoint",
|
|
1362
2227
|
"Permanently delete a Serverless endpoint. This cannot be undone.",
|
|
1363
2228
|
{
|
|
1364
|
-
endpointId:
|
|
2229
|
+
endpointId: import_zod8.z.string().describe("ID of the endpoint to delete")
|
|
1365
2230
|
},
|
|
1366
2231
|
{ title: "Delete endpoint", ...DESTRUCTIVE },
|
|
1367
2232
|
async (params) => {
|
|
@@ -1373,12 +2238,39 @@ function registerEndpointTools(server, rt) {
|
|
|
1373
2238
|
return jsonReply2(result);
|
|
1374
2239
|
}
|
|
1375
2240
|
);
|
|
2241
|
+
server.tool(
|
|
2242
|
+
"list-endpoint-releases",
|
|
2243
|
+
"List a Serverless endpoint's release history and current rollout status (workers on the latest version). v2-only \u2014 returns a 501 notice on the v1 API. Paginated via limit/cursor.",
|
|
2244
|
+
{
|
|
2245
|
+
...listPaginationParams,
|
|
2246
|
+
endpointId: import_zod8.z.string().describe("ID of the Serverless endpoint whose releases to list")
|
|
2247
|
+
},
|
|
2248
|
+
{ title: "List endpoint releases", ...READ_ONLY },
|
|
2249
|
+
async (params) => {
|
|
2250
|
+
const backend = backendFor("endpoints");
|
|
2251
|
+
if (backend.version === "v1") {
|
|
2252
|
+
return jsonReply2({
|
|
2253
|
+
error: "list-endpoint-releases is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
2254
|
+
status: 501
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
const raw = await callRestUrl(
|
|
2258
|
+
`${backend.base}/serverless/${params.endpointId}/releases`
|
|
2259
|
+
);
|
|
2260
|
+
const releases = Array.isArray(raw?.releases) ? raw.releases : [];
|
|
2261
|
+
return capListResult(
|
|
2262
|
+
releases,
|
|
2263
|
+
{ limit: params.limit, cursor: params.cursor },
|
|
2264
|
+
{ rollout: raw?.rollout, endpointVersion: raw?.endpointVersion }
|
|
2265
|
+
);
|
|
2266
|
+
}
|
|
2267
|
+
);
|
|
1376
2268
|
server.tool(
|
|
1377
2269
|
"list-endpoint-workers",
|
|
1378
|
-
"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.",
|
|
2270
|
+
"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.",
|
|
1379
2271
|
{
|
|
1380
2272
|
...listPaginationParams,
|
|
1381
|
-
endpointId:
|
|
2273
|
+
endpointId: import_zod8.z.string().describe("ID of the Serverless endpoint whose workers to list")
|
|
1382
2274
|
},
|
|
1383
2275
|
{ title: "List endpoint workers", ...READ_ONLY },
|
|
1384
2276
|
async (params) => {
|
|
@@ -1399,30 +2291,110 @@ function registerEndpointTools(server, rt) {
|
|
|
1399
2291
|
);
|
|
1400
2292
|
}
|
|
1401
2293
|
);
|
|
2294
|
+
server.tool(
|
|
2295
|
+
"stream-worker-logs",
|
|
2296
|
+
"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).",
|
|
2297
|
+
{
|
|
2298
|
+
endpointId: import_zod8.z.string().describe("ID of the Serverless endpoint the worker belongs to"),
|
|
2299
|
+
workerId: import_zod8.z.string().describe(
|
|
2300
|
+
"ID of the worker whose logs to stream (from list-endpoint-workers)"
|
|
2301
|
+
),
|
|
2302
|
+
...logStreamParams
|
|
2303
|
+
},
|
|
2304
|
+
{ title: "Stream worker logs", ...READ_ONLY },
|
|
2305
|
+
(params) => streamLogsReply(
|
|
2306
|
+
rt,
|
|
2307
|
+
{
|
|
2308
|
+
name: "stream-worker-logs",
|
|
2309
|
+
resource: "workers",
|
|
2310
|
+
logsUrl: (backend) => `${backend.base}/serverless/${encodeURIComponent(params.endpointId)}/workers/${encodeURIComponent(params.workerId)}/logs`
|
|
2311
|
+
},
|
|
2312
|
+
params
|
|
2313
|
+
)
|
|
2314
|
+
);
|
|
1402
2315
|
}
|
|
1403
2316
|
|
|
1404
2317
|
// src/tools/jobs.ts
|
|
1405
|
-
var
|
|
2318
|
+
var import_zod9 = require("zod");
|
|
2319
|
+
var DIAGNOSIS_TTL_MS = 15e3;
|
|
2320
|
+
var MAX_DIAGNOSIS_ENTRIES = 500;
|
|
2321
|
+
var diagnosisCache = /* @__PURE__ */ new Map();
|
|
1406
2322
|
function registerJobTools(server, rt) {
|
|
1407
|
-
const { jsonReply: jsonReply2, serverlessRequest } = rt;
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
2323
|
+
const { jsonReply: jsonReply2, serverlessRequest, backendFor, callRestUrl } = rt;
|
|
2324
|
+
async function diagnoseQueuedJob(endpointId) {
|
|
2325
|
+
const cached = diagnosisCache.get(endpointId);
|
|
2326
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
2327
|
+
return cached.value;
|
|
2328
|
+
}
|
|
2329
|
+
diagnosisCache.delete(endpointId);
|
|
2330
|
+
try {
|
|
2331
|
+
const backend = backendFor("workers");
|
|
2332
|
+
if (backend.version !== "v2") return null;
|
|
2333
|
+
const raw = await callRestUrl(
|
|
2334
|
+
`${backend.base}/serverless/${endpointId}/workers`
|
|
2335
|
+
);
|
|
2336
|
+
const summary = raw?.summary;
|
|
2337
|
+
const workers = raw?.workers ?? [];
|
|
2338
|
+
if (!summary) return null;
|
|
2339
|
+
const remember = (value) => {
|
|
2340
|
+
if (diagnosisCache.size >= MAX_DIAGNOSIS_ENTRIES) {
|
|
2341
|
+
const oldest = diagnosisCache.keys().next().value;
|
|
2342
|
+
if (oldest !== void 0) diagnosisCache.delete(oldest);
|
|
2343
|
+
}
|
|
2344
|
+
diagnosisCache.set(endpointId, {
|
|
2345
|
+
value,
|
|
2346
|
+
expiresAt: Date.now() + DIAGNOSIS_TTL_MS
|
|
2347
|
+
});
|
|
2348
|
+
return value;
|
|
2349
|
+
};
|
|
2350
|
+
if ((summary.unhealthy ?? 0) > 0) {
|
|
2351
|
+
const badIds = workers.filter((w) => w.status === "UNHEALTHY").map((w) => w.id).join(", ");
|
|
2352
|
+
return remember({
|
|
2353
|
+
workerHealth: summary,
|
|
2354
|
+
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})` : ""}.`
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
if ((summary.total ?? 0) === 0) {
|
|
2358
|
+
return remember({
|
|
2359
|
+
workerHealth: summary,
|
|
2360
|
+
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."
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
if ((summary.throttled ?? 0) > 0 && (summary.running ?? 0) === 0) {
|
|
2364
|
+
return remember({
|
|
2365
|
+
workerHealth: summary,
|
|
2366
|
+
hint: "Workers exist but are throttled \u2014 the hosts are at capacity right now; the job should start when capacity frees up."
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
if ((summary.initializing ?? 0) > 0) {
|
|
2370
|
+
return remember({
|
|
2371
|
+
workerHealth: summary,
|
|
2372
|
+
hint: "A worker is initializing \u2014 likely a cold start (image pull / model load); the job should start soon."
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
return remember({ workerHealth: summary, hint: "" });
|
|
2376
|
+
} catch {
|
|
2377
|
+
return null;
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
const endpointIdSchema = import_zod9.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
|
|
2381
|
+
const jobIdSchema = import_zod9.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
|
|
2382
|
+
const inputSchema = import_zod9.z.record(import_zod9.z.unknown()).describe(
|
|
1411
2383
|
"Input payload for the worker handler. The expected fields depend on the deployed model or worker."
|
|
1412
2384
|
);
|
|
1413
|
-
const webhookSchema =
|
|
2385
|
+
const webhookSchema = import_zod9.z.string().url().optional().describe(
|
|
1414
2386
|
"Webhook URL to receive job completion notifications instead of polling"
|
|
1415
2387
|
);
|
|
1416
|
-
const policySchema =
|
|
1417
|
-
executionTimeout:
|
|
1418
|
-
lowPriority:
|
|
1419
|
-
ttl:
|
|
2388
|
+
const policySchema = import_zod9.z.object({
|
|
2389
|
+
executionTimeout: import_zod9.z.number().optional().describe("Maximum execution time in milliseconds"),
|
|
2390
|
+
lowPriority: import_zod9.z.boolean().optional().describe("Submit as a low-priority job"),
|
|
2391
|
+
ttl: import_zod9.z.number().optional().describe("Time-to-live for the job result in milliseconds")
|
|
1420
2392
|
}).optional().describe("Execution policy options");
|
|
1421
|
-
const s3ConfigSchema =
|
|
1422
|
-
accessId:
|
|
1423
|
-
accessSecret:
|
|
1424
|
-
bucketName:
|
|
1425
|
-
endpointUrl:
|
|
2393
|
+
const s3ConfigSchema = import_zod9.z.object({
|
|
2394
|
+
accessId: import_zod9.z.string().describe("S3 access key ID"),
|
|
2395
|
+
accessSecret: import_zod9.z.string().describe("S3 secret access key"),
|
|
2396
|
+
bucketName: import_zod9.z.string().describe("S3 bucket name"),
|
|
2397
|
+
endpointUrl: import_zod9.z.string().describe("S3 endpoint URL")
|
|
1426
2398
|
}).optional().describe("S3-compatible storage config for large outputs");
|
|
1427
2399
|
server.tool(
|
|
1428
2400
|
"run-endpoint",
|
|
@@ -1456,7 +2428,7 @@ function registerJobTools(server, rt) {
|
|
|
1456
2428
|
"ID of the Serverless endpoint to run synchronously"
|
|
1457
2429
|
),
|
|
1458
2430
|
input: inputSchema,
|
|
1459
|
-
wait:
|
|
2431
|
+
wait: import_zod9.z.number().min(1e3).max(3e5).optional().describe(
|
|
1460
2432
|
"How long in milliseconds the server should wait for a result before returning a job ID to poll (1000\u2013300000). Defaults to 90000 (90 seconds)."
|
|
1461
2433
|
),
|
|
1462
2434
|
webhook: webhookSchema,
|
|
@@ -1478,7 +2450,7 @@ function registerJobTools(server, rt) {
|
|
|
1478
2450
|
);
|
|
1479
2451
|
server.tool(
|
|
1480
2452
|
"get-job-status",
|
|
1481
|
-
"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.",
|
|
2453
|
+
"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.",
|
|
1482
2454
|
{
|
|
1483
2455
|
endpointId: endpointIdSchema.describe(
|
|
1484
2456
|
"ID of the Serverless endpoint the job belongs to"
|
|
@@ -1491,6 +2463,16 @@ function registerJobTools(server, rt) {
|
|
|
1491
2463
|
params.endpointId,
|
|
1492
2464
|
`/status/${params.jobId}`
|
|
1493
2465
|
);
|
|
2466
|
+
if (result && typeof result === "object" && result.status === "IN_QUEUE") {
|
|
2467
|
+
const diagnosis = await diagnoseQueuedJob(params.endpointId);
|
|
2468
|
+
if (diagnosis) {
|
|
2469
|
+
return jsonReply2({
|
|
2470
|
+
...result,
|
|
2471
|
+
workerHealth: diagnosis.workerHealth,
|
|
2472
|
+
...diagnosis.hint ? { hint: diagnosis.hint } : {}
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
1494
2476
|
return jsonReply2(result);
|
|
1495
2477
|
}
|
|
1496
2478
|
);
|
|
@@ -1592,7 +2574,7 @@ function registerJobTools(server, rt) {
|
|
|
1592
2574
|
);
|
|
1593
2575
|
server.tool(
|
|
1594
2576
|
"endpoint-health",
|
|
1595
|
-
"Get the health and operational status of a Serverless endpoint, including worker counts and job statistics.",
|
|
2577
|
+
"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.",
|
|
1596
2578
|
{
|
|
1597
2579
|
endpointId: endpointIdSchema.describe(
|
|
1598
2580
|
"ID of the Serverless endpoint to check health for"
|
|
@@ -1625,7 +2607,7 @@ function registerJobTools(server, rt) {
|
|
|
1625
2607
|
}
|
|
1626
2608
|
|
|
1627
2609
|
// src/tools/templates.ts
|
|
1628
|
-
var
|
|
2610
|
+
var import_zod10 = require("zod");
|
|
1629
2611
|
function registerTemplateTools(server, rt) {
|
|
1630
2612
|
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
1631
2613
|
server.tool(
|
|
@@ -1633,9 +2615,9 @@ function registerTemplateTools(server, rt) {
|
|
|
1633
2615
|
`List available templates. By default returns only the user's own templates. Use includeRunpodTemplates to also include official Runpod templates. The recommended default template for new pods is "Runpod Pytorch 2.8.0" (ID: runpod-torch-v280) \u2014 it has the latest CUDA and PyTorch versions.`,
|
|
1634
2616
|
{
|
|
1635
2617
|
...listPaginationParams,
|
|
1636
|
-
includeRunpodTemplates:
|
|
1637
|
-
includePublicTemplates:
|
|
1638
|
-
includeEndpointBoundTemplates:
|
|
2618
|
+
includeRunpodTemplates: import_zod10.z.boolean().optional().describe("Include official Runpod templates in the response"),
|
|
2619
|
+
includePublicTemplates: import_zod10.z.boolean().optional().describe("Include community-made public templates in the response"),
|
|
2620
|
+
includeEndpointBoundTemplates: import_zod10.z.boolean().optional().describe(
|
|
1639
2621
|
"Include templates bound to Serverless endpoints in the response"
|
|
1640
2622
|
)
|
|
1641
2623
|
},
|
|
@@ -1666,7 +2648,7 @@ function registerTemplateTools(server, rt) {
|
|
|
1666
2648
|
"get-template",
|
|
1667
2649
|
"Get one template by id (image, env, ports, disk/volume config).",
|
|
1668
2650
|
{
|
|
1669
|
-
templateId:
|
|
2651
|
+
templateId: import_zod10.z.string().describe("ID of the template to retrieve")
|
|
1670
2652
|
},
|
|
1671
2653
|
{ title: "Get template", ...READ_ONLY },
|
|
1672
2654
|
async (params) => {
|
|
@@ -1679,23 +2661,27 @@ function registerTemplateTools(server, rt) {
|
|
|
1679
2661
|
);
|
|
1680
2662
|
server.tool(
|
|
1681
2663
|
"create-template",
|
|
1682
|
-
"Create a reusable pod/endpoint template (image, ports, env, disk/volume).
|
|
2664
|
+
"Create a reusable pod/endpoint template (image, ports, env, disk/volume). Category is optional and defaults to NVIDIA.",
|
|
1683
2665
|
{
|
|
1684
|
-
name:
|
|
1685
|
-
imageName:
|
|
1686
|
-
isServerless:
|
|
1687
|
-
ports:
|
|
1688
|
-
dockerEntrypoint:
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
2666
|
+
name: import_zod10.z.string().describe("Name for the template"),
|
|
2667
|
+
imageName: import_zod10.z.string().describe("Docker image to use"),
|
|
2668
|
+
isServerless: import_zod10.z.boolean().optional().describe("Is this a serverless template"),
|
|
2669
|
+
ports: import_zod10.z.array(import_zod10.z.string()).optional().describe("Ports to expose"),
|
|
2670
|
+
dockerEntrypoint: import_zod10.z.array(import_zod10.z.string()).optional().describe(
|
|
2671
|
+
"Docker entrypoint commands. Note: not persisted on the v2 REST API, which has no separate entrypoint field (only a start command)."
|
|
2672
|
+
),
|
|
2673
|
+
dockerStartCmd: import_zod10.z.array(import_zod10.z.string()).optional().describe(
|
|
2674
|
+
'Container start command. On v2 this is stored as the template\'s `args` string; a multi-element array is joined with spaces (e.g. ["python","-u","app.py"] \u2192 "python -u app.py"). The join is lossy: an element that itself contains spaces is NOT preserved as one argument \u2014 e.g. ["sh","-c","echo hi"] becomes "sh -c echo hi", so pass a shell command as a single pre-quoted element if that distinction matters. Omit or pass an empty array to leave the command unset (never overwrites with "").'
|
|
2675
|
+
),
|
|
2676
|
+
env: import_zod10.z.record(import_zod10.z.string()).optional().describe("Environment variables"),
|
|
2677
|
+
containerDiskInGb: import_zod10.z.number().optional().describe("Container disk size in GB"),
|
|
2678
|
+
volumeInGb: import_zod10.z.number().optional().describe("Volume size in GB"),
|
|
2679
|
+
volumeMountPath: import_zod10.z.string().optional().describe("Path to mount the volume"),
|
|
2680
|
+
readme: import_zod10.z.string().optional().describe("README content in markdown format"),
|
|
2681
|
+
category: import_zod10.z.enum(["CPU", "NVIDIA", "AMD"]).optional().describe(
|
|
2682
|
+
"How the template is grouped and filtered in the Runpod console \u2014 it does NOT affect hardware selection, scheduling, or billing. Optional; defaults to NVIDIA. Set CPU or AMD only to file the template correctly in the console."
|
|
1697
2683
|
),
|
|
1698
|
-
containerRegistryAuthId:
|
|
2684
|
+
containerRegistryAuthId: import_zod10.z.string().optional().describe(
|
|
1699
2685
|
"ID of a container registry credential (from create-container-registry-auth / list-container-registry-auths) used to pull a private image. Required when imageName points at a private registry; without it serverless workers fail the image pull."
|
|
1700
2686
|
)
|
|
1701
2687
|
},
|
|
@@ -1713,15 +2699,18 @@ function registerTemplateTools(server, rt) {
|
|
|
1713
2699
|
);
|
|
1714
2700
|
server.tool(
|
|
1715
2701
|
"update-template",
|
|
1716
|
-
"Update a template's mutable fields (name, image, ports, env,
|
|
2702
|
+
"Update a template's mutable fields (name, image, ports, env, start command, registry auth). Only provided fields change; omitted fields are left untouched.",
|
|
1717
2703
|
{
|
|
1718
|
-
templateId:
|
|
1719
|
-
name:
|
|
1720
|
-
imageName:
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
2704
|
+
templateId: import_zod10.z.string().describe("ID of the template to update"),
|
|
2705
|
+
name: import_zod10.z.string().optional().describe("New name for the template"),
|
|
2706
|
+
imageName: import_zod10.z.string().optional().describe("New Docker image"),
|
|
2707
|
+
dockerStartCmd: import_zod10.z.array(import_zod10.z.string()).optional().describe(
|
|
2708
|
+
"New container start command. On v2 stored as the template's `args` string; a multi-element array is joined with spaces (lossy \u2014 an element containing spaces is split into separate tokens). Note: this can only set a command, not clear one \u2014 an empty or absent array is ignored so the existing command is preserved."
|
|
2709
|
+
),
|
|
2710
|
+
ports: import_zod10.z.array(import_zod10.z.string()).optional().describe("New ports to expose"),
|
|
2711
|
+
env: import_zod10.z.record(import_zod10.z.string()).optional().describe("New environment variables"),
|
|
2712
|
+
readme: import_zod10.z.string().optional().describe("New README content in markdown format"),
|
|
2713
|
+
containerRegistryAuthId: import_zod10.z.string().optional().describe(
|
|
1725
2714
|
"ID of a container registry credential (from create-container-registry-auth / list-container-registry-auths) used to pull a private image. Set this to attach a credential to a template referencing a private registry."
|
|
1726
2715
|
)
|
|
1727
2716
|
},
|
|
@@ -1742,7 +2731,7 @@ function registerTemplateTools(server, rt) {
|
|
|
1742
2731
|
"delete-template",
|
|
1743
2732
|
"Permanently delete a template. This cannot be undone.",
|
|
1744
2733
|
{
|
|
1745
|
-
templateId:
|
|
2734
|
+
templateId: import_zod10.z.string().describe("ID of the template to delete")
|
|
1746
2735
|
},
|
|
1747
2736
|
{ title: "Delete template", ...DESTRUCTIVE },
|
|
1748
2737
|
async (params) => {
|
|
@@ -1757,7 +2746,7 @@ function registerTemplateTools(server, rt) {
|
|
|
1757
2746
|
}
|
|
1758
2747
|
|
|
1759
2748
|
// src/tools/network-volumes.ts
|
|
1760
|
-
var
|
|
2749
|
+
var import_zod11 = require("zod");
|
|
1761
2750
|
function registerNetworkVolumeTools(server, rt) {
|
|
1762
2751
|
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
1763
2752
|
server.tool(
|
|
@@ -1778,7 +2767,7 @@ function registerNetworkVolumeTools(server, rt) {
|
|
|
1778
2767
|
"get-network-volume",
|
|
1779
2768
|
"Get one network volume by id (name, size, data center).",
|
|
1780
2769
|
{
|
|
1781
|
-
networkVolumeId:
|
|
2770
|
+
networkVolumeId: import_zod11.z.string().describe("ID of the network volume to retrieve")
|
|
1782
2771
|
},
|
|
1783
2772
|
{ title: "Get network volume", ...READ_ONLY },
|
|
1784
2773
|
async (params) => {
|
|
@@ -1791,11 +2780,17 @@ function registerNetworkVolumeTools(server, rt) {
|
|
|
1791
2780
|
);
|
|
1792
2781
|
server.tool(
|
|
1793
2782
|
"create-network-volume",
|
|
1794
|
-
"Create a persistent network volume in a data center (size
|
|
2783
|
+
"Create a persistent network volume in a data center (size 10\u20134096 GB) that pods can mount. Pass volumeType to pick a storage tier; omit it to get the data center default.",
|
|
1795
2784
|
{
|
|
1796
|
-
name:
|
|
1797
|
-
|
|
1798
|
-
|
|
2785
|
+
name: import_zod11.z.string().describe("Name for the network volume"),
|
|
2786
|
+
// Bounds mirror the v2 spec (integer, minimum 10, maximum 4096). Enforced
|
|
2787
|
+
// here as well as described, so an out-of-range value fails locally
|
|
2788
|
+
// instead of costing an API roundtrip.
|
|
2789
|
+
size: import_zod11.z.number().int().min(10).max(4096).describe("Size in GB (10-4096)"),
|
|
2790
|
+
dataCenterId: import_zod11.z.string().describe("Data center ID (see list-data-centers)"),
|
|
2791
|
+
volumeType: import_zod11.z.enum(["STANDARD", "HIGH_PERFORMANCE"]).optional().describe(
|
|
2792
|
+
"Storage tier. Omit for the data center's default (primary) tier. HIGH_PERFORMANCE provisions an HPS volume. Immutable after creation \u2014 update-network-volume cannot change it."
|
|
2793
|
+
)
|
|
1799
2794
|
},
|
|
1800
2795
|
{ title: "Create network volume", ...WRITE },
|
|
1801
2796
|
async (params) => {
|
|
@@ -1813,9 +2808,11 @@ function registerNetworkVolumeTools(server, rt) {
|
|
|
1813
2808
|
"update-network-volume",
|
|
1814
2809
|
"Update a network volume (rename, or grow size \u2014 the new size must exceed the current size). Only provided fields change.",
|
|
1815
2810
|
{
|
|
1816
|
-
networkVolumeId:
|
|
1817
|
-
name:
|
|
1818
|
-
|
|
2811
|
+
networkVolumeId: import_zod11.z.string().describe("ID of the network volume to update"),
|
|
2812
|
+
name: import_zod11.z.string().optional().describe("New name for the network volume"),
|
|
2813
|
+
// Same absolute bounds as create. "Larger than current" stays server-side —
|
|
2814
|
+
// we do not hold the current size here, so only the range is checked.
|
|
2815
|
+
size: import_zod11.z.number().int().min(10).max(4096).optional().describe("New size in GB, 10-4096 (must be larger than current)")
|
|
1819
2816
|
},
|
|
1820
2817
|
{ title: "Update network volume", ...WRITE, idempotentHint: true },
|
|
1821
2818
|
async (params) => {
|
|
@@ -1834,7 +2831,7 @@ function registerNetworkVolumeTools(server, rt) {
|
|
|
1834
2831
|
"delete-network-volume",
|
|
1835
2832
|
"Permanently delete a network volume and its data. This cannot be undone.",
|
|
1836
2833
|
{
|
|
1837
|
-
networkVolumeId:
|
|
2834
|
+
networkVolumeId: import_zod11.z.string().describe("ID of the network volume to delete")
|
|
1838
2835
|
},
|
|
1839
2836
|
{ title: "Delete network volume", ...DESTRUCTIVE },
|
|
1840
2837
|
async (params) => {
|
|
@@ -1849,9 +2846,14 @@ function registerNetworkVolumeTools(server, rt) {
|
|
|
1849
2846
|
}
|
|
1850
2847
|
|
|
1851
2848
|
// src/tools/registries.ts
|
|
1852
|
-
var
|
|
2849
|
+
var import_zod12 = require("zod");
|
|
2850
|
+
var DELEGATIONS = "/delegations";
|
|
1853
2851
|
function registerRegistryTools(server, rt) {
|
|
1854
2852
|
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
2853
|
+
const delegationsV2OnlyNotice = (tool) => ({
|
|
2854
|
+
error: `${tool} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
|
|
2855
|
+
status: 501
|
|
2856
|
+
});
|
|
1855
2857
|
server.tool(
|
|
1856
2858
|
"list-container-registry-auths",
|
|
1857
2859
|
"List your saved container-registry credentials (private image-pull auth). Paginated via limit/cursor.",
|
|
@@ -1870,7 +2872,7 @@ function registerRegistryTools(server, rt) {
|
|
|
1870
2872
|
"get-container-registry-auth",
|
|
1871
2873
|
"Get one saved container-registry credential by id (the secret/password is not returned).",
|
|
1872
2874
|
{
|
|
1873
|
-
containerRegistryAuthId:
|
|
2875
|
+
containerRegistryAuthId: import_zod12.z.string().describe("ID of the container registry auth to retrieve")
|
|
1874
2876
|
},
|
|
1875
2877
|
{ title: "Get container registry auth", ...READ_ONLY },
|
|
1876
2878
|
async (params) => {
|
|
@@ -1885,9 +2887,9 @@ function registerRegistryTools(server, rt) {
|
|
|
1885
2887
|
"create-container-registry-auth",
|
|
1886
2888
|
"Save container-registry credentials (username + password/token) so pods and endpoints can pull private images.",
|
|
1887
2889
|
{
|
|
1888
|
-
name:
|
|
1889
|
-
username:
|
|
1890
|
-
password:
|
|
2890
|
+
name: import_zod12.z.string().describe("Name for the container registry auth"),
|
|
2891
|
+
username: import_zod12.z.string().describe("Registry username"),
|
|
2892
|
+
password: import_zod12.z.string().describe("Registry password or access token (stored as a secret)")
|
|
1891
2893
|
},
|
|
1892
2894
|
{ title: "Create container registry auth", ...WRITE },
|
|
1893
2895
|
async (params) => {
|
|
@@ -1905,7 +2907,7 @@ function registerRegistryTools(server, rt) {
|
|
|
1905
2907
|
"delete-container-registry-auth",
|
|
1906
2908
|
"Permanently delete a saved container-registry credential. This cannot be undone.",
|
|
1907
2909
|
{
|
|
1908
|
-
containerRegistryAuthId:
|
|
2910
|
+
containerRegistryAuthId: import_zod12.z.string().describe("ID of the container registry auth to delete")
|
|
1909
2911
|
},
|
|
1910
2912
|
{ title: "Delete container registry auth", ...DESTRUCTIVE },
|
|
1911
2913
|
async (params) => {
|
|
@@ -1917,166 +2919,69 @@ function registerRegistryTools(server, rt) {
|
|
|
1917
2919
|
return jsonReply2(result);
|
|
1918
2920
|
}
|
|
1919
2921
|
);
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
// src/tools/tags.ts
|
|
1923
|
-
var import_zod9 = require("zod");
|
|
1924
|
-
var TAG_RESOURCE_TYPES = [
|
|
1925
|
-
"POD",
|
|
1926
|
-
"NETWORK_VOLUME",
|
|
1927
|
-
"CLUSTER",
|
|
1928
|
-
"SERVERLESS_ENDPOINT"
|
|
1929
|
-
];
|
|
1930
|
-
var taggedResourceSchema = import_zod9.z.object({
|
|
1931
|
-
id: import_zod9.z.string().describe("Resource identifier (e.g. a pod or endpoint id)"),
|
|
1932
|
-
type: import_zod9.z.enum(TAG_RESOURCE_TYPES).describe("Type of resource to associate with the tag")
|
|
1933
|
-
});
|
|
1934
|
-
function v2OnlyNotice(tool) {
|
|
1935
|
-
return {
|
|
1936
|
-
error: `${tool} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
|
|
1937
|
-
status: 501
|
|
1938
|
-
};
|
|
1939
|
-
}
|
|
1940
|
-
function registerTagTools(server, rt) {
|
|
1941
|
-
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
1942
2922
|
server.tool(
|
|
1943
|
-
"list-
|
|
1944
|
-
"List your
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
includeResources: import_zod9.z.boolean().optional().describe("Include the resources attached to each tag in the response")
|
|
1948
|
-
},
|
|
1949
|
-
{ title: "List tags", ...READ_ONLY },
|
|
2923
|
+
"list-registry-delegations",
|
|
2924
|
+
"List your AWS ECR delegations \u2014 ECR repositories Runpod has been granted scoped pull access to, so private ECR images can be pulled without storing AWS credentials. Each entry includes the `dockerRegistryUri` to pull from. v2-only \u2014 returns a 501 notice on the v1 API. Paginated via limit/cursor.",
|
|
2925
|
+
listPaginationParams,
|
|
2926
|
+
{ title: "List registry delegations", ...READ_ONLY },
|
|
1950
2927
|
async (params) => {
|
|
1951
|
-
const backend = backendFor("
|
|
1952
|
-
if (backend.version === "v1")
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
2928
|
+
const backend = backendFor("registries");
|
|
2929
|
+
if (backend.version === "v1") {
|
|
2930
|
+
return jsonReply2(delegationsV2OnlyNotice("list-registry-delegations"));
|
|
2931
|
+
}
|
|
2932
|
+
const raw = await callRestUrl(
|
|
2933
|
+
`${backend.base}${backend.list}${DELEGATIONS}`
|
|
2934
|
+
);
|
|
2935
|
+
const delegations = Array.isArray(raw?.delegations) ? raw.delegations : [];
|
|
2936
|
+
return capListResult(delegations, {
|
|
1956
2937
|
limit: params.limit,
|
|
1957
2938
|
cursor: params.cursor
|
|
1958
2939
|
});
|
|
1959
2940
|
}
|
|
1960
2941
|
);
|
|
1961
2942
|
server.tool(
|
|
1962
|
-
"
|
|
1963
|
-
"
|
|
1964
|
-
{
|
|
1965
|
-
tagId: import_zod9.z.string().describe("ID of the tag to retrieve"),
|
|
1966
|
-
includeResources: import_zod9.z.boolean().optional().describe("Include the resources attached to this tag")
|
|
1967
|
-
},
|
|
1968
|
-
{ title: "Get tag", ...READ_ONLY },
|
|
1969
|
-
async (params) => {
|
|
1970
|
-
const backend = backendFor("tags");
|
|
1971
|
-
if (backend.version === "v1") return jsonReply2(v2OnlyNotice("get-tag"));
|
|
1972
|
-
const query = params.includeResources ? "?include=resources" : "";
|
|
1973
|
-
const result = await callRestUrl(
|
|
1974
|
-
`${backend.base}${backend.get(params.tagId)}${query}`
|
|
1975
|
-
);
|
|
1976
|
-
return jsonReply2(result);
|
|
1977
|
-
}
|
|
1978
|
-
);
|
|
1979
|
-
server.tool(
|
|
1980
|
-
"create-tag",
|
|
1981
|
-
"Create a key/value tag, optionally attaching resources at creation. v2-only \u2014 returns a 501 notice on the v1 API.",
|
|
2943
|
+
"create-registry-delegation",
|
|
2944
|
+
"Register an AWS ECR repository so Runpod can pull private images from it without stored credentials. Pass the repository ARN; the reply carries the `dockerRegistryUri`, resolved repository/tag, and AWS region. v2-only \u2014 returns a 501 notice on the v1 API. For a non-ECR private registry use create-container-registry-auth instead.",
|
|
1982
2945
|
{
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
2946
|
+
resource: import_zod12.z.string().min(1).describe(
|
|
2947
|
+
'ECR resource ARN, e.g. "arn:aws:ecr:us-east-2:123456789012:repository/my-org/my-image"'
|
|
2948
|
+
),
|
|
2949
|
+
name: import_zod12.z.string().optional().describe("Optional human-readable name for the delegation")
|
|
1986
2950
|
},
|
|
1987
|
-
{ title: "Create
|
|
2951
|
+
{ title: "Create registry delegation", ...WRITE },
|
|
1988
2952
|
async (params) => {
|
|
1989
|
-
const backend = backendFor("
|
|
1990
|
-
if (backend.version === "v1")
|
|
1991
|
-
return jsonReply2(
|
|
2953
|
+
const backend = backendFor("registries");
|
|
2954
|
+
if (backend.version === "v1") {
|
|
2955
|
+
return jsonReply2(delegationsV2OnlyNotice("create-registry-delegation"));
|
|
2956
|
+
}
|
|
2957
|
+
const body = { resource: params.resource };
|
|
2958
|
+
if (params.name !== void 0) body.name = params.name;
|
|
1992
2959
|
const result = await callRestUrl(
|
|
1993
|
-
`${backend.base}${backend.list}`,
|
|
2960
|
+
`${backend.base}${backend.list}${DELEGATIONS}`,
|
|
1994
2961
|
"POST",
|
|
1995
|
-
|
|
2962
|
+
body
|
|
1996
2963
|
);
|
|
1997
2964
|
return jsonReply2(result);
|
|
1998
2965
|
}
|
|
1999
2966
|
);
|
|
2000
2967
|
server.tool(
|
|
2001
|
-
"
|
|
2002
|
-
"
|
|
2968
|
+
"delete-registry-delegation",
|
|
2969
|
+
"Revoke an AWS ECR delegation, withdrawing Runpod's pull access to that repository. Existing workers keep running; future pulls fail. v2-only \u2014 returns a 501 notice on the v1 API. This cannot be undone.",
|
|
2003
2970
|
{
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
value: import_zod9.z.string().min(1).optional().describe("New tag value"),
|
|
2007
|
-
resources: import_zod9.z.array(taggedResourceSchema).optional().describe(
|
|
2008
|
-
"Replace the tag's resource associations with exactly these. Omit to leave unchanged; pass [] to detach all."
|
|
2971
|
+
delegationId: import_zod12.z.string().describe(
|
|
2972
|
+
"ID of the delegation to revoke (from list-registry-delegations)"
|
|
2009
2973
|
)
|
|
2010
2974
|
},
|
|
2011
|
-
|
|
2012
|
-
// retried-after-partial-failure update is not guaranteed to be a no-op.
|
|
2013
|
-
{ title: "Update tag", ...WRITE },
|
|
2014
|
-
async (params) => {
|
|
2015
|
-
const { tagId, ...updateParams } = params;
|
|
2016
|
-
const backend = backendFor("tags");
|
|
2017
|
-
if (backend.version === "v1")
|
|
2018
|
-
return jsonReply2(v2OnlyNotice("update-tag"));
|
|
2019
|
-
const result = await callRestUrl(
|
|
2020
|
-
`${backend.base}${backend.get(tagId)}`,
|
|
2021
|
-
"PATCH",
|
|
2022
|
-
updateParams
|
|
2023
|
-
);
|
|
2024
|
-
return jsonReply2(result);
|
|
2025
|
-
}
|
|
2026
|
-
);
|
|
2027
|
-
server.tool(
|
|
2028
|
-
"delete-tag",
|
|
2029
|
-
"Permanently delete a tag (removes it from all resources it is attached to). This cannot be undone. v2-only \u2014 returns a 501 notice on the v1 API.",
|
|
2030
|
-
{
|
|
2031
|
-
tagId: import_zod9.z.string().describe("ID of the tag to delete")
|
|
2032
|
-
},
|
|
2033
|
-
{ title: "Delete tag", ...DESTRUCTIVE },
|
|
2034
|
-
async (params) => {
|
|
2035
|
-
const backend = backendFor("tags");
|
|
2036
|
-
if (backend.version === "v1")
|
|
2037
|
-
return jsonReply2(v2OnlyNotice("delete-tag"));
|
|
2038
|
-
const result = await callRestUrl(
|
|
2039
|
-
`${backend.base}${backend.get(params.tagId)}`,
|
|
2040
|
-
"DELETE"
|
|
2041
|
-
);
|
|
2042
|
-
return jsonReply2(result);
|
|
2043
|
-
}
|
|
2044
|
-
);
|
|
2045
|
-
server.tool(
|
|
2046
|
-
"attach-tag",
|
|
2047
|
-
"Attach a tag to a resource (pod, network volume, cluster, or serverless endpoint). Idempotent \u2014 attaching an already-associated resource is a no-op. v2-only \u2014 returns a 501 notice on the v1 API.",
|
|
2048
|
-
{
|
|
2049
|
-
tagId: import_zod9.z.string().describe("ID of the tag"),
|
|
2050
|
-
resourceType: import_zod9.z.enum(TAG_RESOURCE_TYPES).describe("Type of resource to attach the tag to"),
|
|
2051
|
-
resourceId: import_zod9.z.string().describe("ID of the resource to attach the tag to")
|
|
2052
|
-
},
|
|
2053
|
-
{ title: "Attach tag to resource", ...WRITE, idempotentHint: true },
|
|
2054
|
-
async (params) => {
|
|
2055
|
-
const backend = backendFor("tags");
|
|
2056
|
-
if (backend.version === "v1")
|
|
2057
|
-
return jsonReply2(v2OnlyNotice("attach-tag"));
|
|
2058
|
-
const result = await callRestUrl(
|
|
2059
|
-
`${backend.base}/tags/${encodeURIComponent(params.tagId)}/resources/${params.resourceType}/${encodeURIComponent(params.resourceId)}`,
|
|
2060
|
-
"PUT"
|
|
2061
|
-
);
|
|
2062
|
-
return jsonReply2(result);
|
|
2063
|
-
}
|
|
2064
|
-
);
|
|
2065
|
-
server.tool(
|
|
2066
|
-
"detach-tag",
|
|
2067
|
-
"Detach a tag from a resource (removes the association only \u2014 the resource and tag are not deleted). Idempotent \u2014 detaching a resource that is not associated is a no-op. v2-only \u2014 returns a 501 notice on the v1 API.",
|
|
2068
|
-
{
|
|
2069
|
-
tagId: import_zod9.z.string().describe("ID of the tag"),
|
|
2070
|
-
resourceType: import_zod9.z.enum(TAG_RESOURCE_TYPES).describe("Type of resource to detach the tag from"),
|
|
2071
|
-
resourceId: import_zod9.z.string().describe("ID of the resource to detach the tag from")
|
|
2072
|
-
},
|
|
2073
|
-
{ title: "Detach tag from resource", ...WRITE, idempotentHint: true },
|
|
2975
|
+
{ title: "Revoke registry delegation", ...DESTRUCTIVE },
|
|
2074
2976
|
async (params) => {
|
|
2075
|
-
const backend = backendFor("
|
|
2076
|
-
if (backend.version === "v1")
|
|
2077
|
-
return jsonReply2(
|
|
2977
|
+
const backend = backendFor("registries");
|
|
2978
|
+
if (backend.version === "v1") {
|
|
2979
|
+
return jsonReply2(delegationsV2OnlyNotice("delete-registry-delegation"));
|
|
2980
|
+
}
|
|
2078
2981
|
const result = await callRestUrl(
|
|
2079
|
-
`${backend.base}
|
|
2982
|
+
`${backend.base}${backend.list}${DELEGATIONS}/${encodeURIComponent(
|
|
2983
|
+
params.delegationId
|
|
2984
|
+
)}`,
|
|
2080
2985
|
"DELETE"
|
|
2081
2986
|
);
|
|
2082
2987
|
return jsonReply2(result);
|
|
@@ -2085,7 +2990,7 @@ function registerTagTools(server, rt) {
|
|
|
2085
2990
|
}
|
|
2086
2991
|
|
|
2087
2992
|
// src/tools/billing.ts
|
|
2088
|
-
var
|
|
2993
|
+
var import_zod13 = require("zod");
|
|
2089
2994
|
var BILLING_SCOPE_PATH = {
|
|
2090
2995
|
all: "/billing",
|
|
2091
2996
|
pods: "/billing/pods",
|
|
@@ -2101,7 +3006,7 @@ function registerBillingTools(server, rt) {
|
|
|
2101
3006
|
"Get time-bucketed Runpod spend history, either aggregated across all resources or broken down by resource type (pods, serverless, endpoints, network volumes, clusters). v2-only \u2014 returns a 501 notice on the v1 API. Records are capped via limit/cursor; narrow the window with startTime/endTime or lastN.",
|
|
2102
3007
|
{
|
|
2103
3008
|
...listPaginationParams,
|
|
2104
|
-
scope:
|
|
3009
|
+
scope: import_zod13.z.enum([
|
|
2105
3010
|
"all",
|
|
2106
3011
|
"pods",
|
|
2107
3012
|
"serverless",
|
|
@@ -2111,14 +3016,14 @@ function registerBillingTools(server, rt) {
|
|
|
2111
3016
|
]).optional().describe(
|
|
2112
3017
|
'Which billing breakdown to fetch. "all" (default) is the aggregate across every resource.'
|
|
2113
3018
|
),
|
|
2114
|
-
startTime:
|
|
3019
|
+
startTime: import_zod13.z.string().optional().describe(
|
|
2115
3020
|
"Start of the period (RFC 3339, e.g. 2026-05-01T00:00:00Z). Defaults to 30 days ago. Mutually exclusive with lastN."
|
|
2116
3021
|
),
|
|
2117
|
-
endTime:
|
|
3022
|
+
endTime: import_zod13.z.string().optional().describe(
|
|
2118
3023
|
"End of the period (RFC 3339), exclusive. Defaults to now. Mutually exclusive with lastN."
|
|
2119
3024
|
),
|
|
2120
|
-
bucketSize:
|
|
2121
|
-
lastN:
|
|
3025
|
+
bucketSize: import_zod13.z.enum(["hour", "day", "week", "month", "year"]).optional().describe("Length of each time bucket. Defaults to day."),
|
|
3026
|
+
lastN: import_zod13.z.number().int().positive().optional().describe(
|
|
2122
3027
|
"Return the last N buckets of bucketSize, ending with the current one. Mutually exclusive with startTime/endTime."
|
|
2123
3028
|
)
|
|
2124
3029
|
},
|
|
@@ -2153,16 +3058,119 @@ function registerBillingTools(server, rt) {
|
|
|
2153
3058
|
function registerTools(server, ctx, deps = {}) {
|
|
2154
3059
|
const rt = createToolRuntime(server, ctx, deps);
|
|
2155
3060
|
registerCatalogTools(server, rt);
|
|
3061
|
+
registerHubTools(server, rt);
|
|
3062
|
+
registerPublicEndpointTools(server, rt);
|
|
2156
3063
|
registerPodTools(server, rt);
|
|
2157
3064
|
registerEndpointTools(server, rt);
|
|
3065
|
+
registerEndpointGpuTools(server, rt);
|
|
2158
3066
|
registerJobTools(server, rt);
|
|
2159
3067
|
registerTemplateTools(server, rt);
|
|
2160
3068
|
registerNetworkVolumeTools(server, rt);
|
|
2161
3069
|
registerRegistryTools(server, rt);
|
|
2162
|
-
registerTagTools(server, rt);
|
|
2163
3070
|
registerBillingTools(server, rt);
|
|
2164
3071
|
}
|
|
2165
3072
|
|
|
3073
|
+
// src/_shared/credential-check.ts
|
|
3074
|
+
var import_node_crypto2 = require("crypto");
|
|
3075
|
+
var VALID_TTL_MS = 6e4;
|
|
3076
|
+
var INVALID_TTL_MS = 3e4;
|
|
3077
|
+
var REQUEST_TIMEOUT_MS = 4e3;
|
|
3078
|
+
var MAX_CACHE_ENTRIES = 1e3;
|
|
3079
|
+
function hashToken(token) {
|
|
3080
|
+
return (0, import_node_crypto2.createHash)("sha256").update(token).digest("hex");
|
|
3081
|
+
}
|
|
3082
|
+
function createCredentialChecker(deps) {
|
|
3083
|
+
const now = deps.now ?? (() => performance.now());
|
|
3084
|
+
const validTtl = deps.validTtlMs ?? VALID_TTL_MS;
|
|
3085
|
+
const invalidTtl = deps.invalidTtlMs ?? INVALID_TTL_MS;
|
|
3086
|
+
const timeoutMs = deps.timeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
3087
|
+
const maxEntries = Math.max(1, deps.maxEntries ?? MAX_CACHE_ENTRIES);
|
|
3088
|
+
const entries = /* @__PURE__ */ new Map();
|
|
3089
|
+
async function fetchVerdict(token) {
|
|
3090
|
+
try {
|
|
3091
|
+
const response = await deps.fetch(deps.url(), {
|
|
3092
|
+
method: "POST",
|
|
3093
|
+
headers: {
|
|
3094
|
+
"Content-Type": "application/json",
|
|
3095
|
+
Authorization: `Bearer ${token}`
|
|
3096
|
+
},
|
|
3097
|
+
body: JSON.stringify({ query: "query { myself { id } }" }),
|
|
3098
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
3099
|
+
});
|
|
3100
|
+
if (response.status === 401) {
|
|
3101
|
+
return {
|
|
3102
|
+
status: "invalid",
|
|
3103
|
+
reason: "The Runpod API rejected the credential (it may have been revoked)."
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
if (!response.ok) return { status: "unknown" };
|
|
3107
|
+
const result = await response.json();
|
|
3108
|
+
if (result?.data?.myself?.id) return { status: "valid" };
|
|
3109
|
+
if (Array.isArray(result?.errors) && result.errors.length > 0) {
|
|
3110
|
+
return { status: "unknown" };
|
|
3111
|
+
}
|
|
3112
|
+
if (result?.data && result.data.myself === null) {
|
|
3113
|
+
return {
|
|
3114
|
+
status: "invalid",
|
|
3115
|
+
reason: "The credential does not resolve to a Runpod account."
|
|
3116
|
+
};
|
|
3117
|
+
}
|
|
3118
|
+
return { status: "unknown" };
|
|
3119
|
+
} catch {
|
|
3120
|
+
return { status: "unknown" };
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
function evictIfFull() {
|
|
3124
|
+
while (entries.size >= maxEntries) {
|
|
3125
|
+
let victim;
|
|
3126
|
+
for (const [k, e] of entries) {
|
|
3127
|
+
if (!e.promise) {
|
|
3128
|
+
victim = k;
|
|
3129
|
+
break;
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
if (victim === void 0) return;
|
|
3133
|
+
entries.delete(victim);
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
async function verify(token) {
|
|
3137
|
+
const key = hashToken(token);
|
|
3138
|
+
const existing = entries.get(key);
|
|
3139
|
+
if (existing?.verdict) {
|
|
3140
|
+
if (existing.expiresAt > now()) {
|
|
3141
|
+
entries.delete(key);
|
|
3142
|
+
entries.set(key, existing);
|
|
3143
|
+
return existing.verdict;
|
|
3144
|
+
}
|
|
3145
|
+
entries.delete(key);
|
|
3146
|
+
} else if (existing?.promise) {
|
|
3147
|
+
return existing.promise;
|
|
3148
|
+
}
|
|
3149
|
+
const entry = { expiresAt: 0 };
|
|
3150
|
+
entry.promise = fetchVerdict(token).then((verdict) => {
|
|
3151
|
+
if (entries.get(key) === entry) {
|
|
3152
|
+
if (verdict.status !== "unknown") {
|
|
3153
|
+
entry.verdict = verdict;
|
|
3154
|
+
entry.expiresAt = now() + (verdict.status === "valid" ? validTtl : invalidTtl);
|
|
3155
|
+
entry.promise = void 0;
|
|
3156
|
+
} else {
|
|
3157
|
+
entries.delete(key);
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
return verdict;
|
|
3161
|
+
});
|
|
3162
|
+
evictIfFull();
|
|
3163
|
+
entries.set(key, entry);
|
|
3164
|
+
return entry.promise;
|
|
3165
|
+
}
|
|
3166
|
+
return {
|
|
3167
|
+
verify,
|
|
3168
|
+
invalidate: (token) => {
|
|
3169
|
+
entries.delete(hashToken(token));
|
|
3170
|
+
}
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
3173
|
+
|
|
2166
3174
|
// src/http.ts
|
|
2167
3175
|
function extractBearerToken(req) {
|
|
2168
3176
|
const authHeader = req.headers.authorization;
|
|
@@ -2179,16 +3187,111 @@ function getBaseUrl(req) {
|
|
|
2179
3187
|
}
|
|
2180
3188
|
return `${proto}://${host}`;
|
|
2181
3189
|
}
|
|
2182
|
-
function writeUnauthorized(req, res, error) {
|
|
2183
|
-
const
|
|
2184
|
-
|
|
2185
|
-
|
|
3190
|
+
function writeUnauthorized(req, res, error, invalidToken = false) {
|
|
3191
|
+
const challenge = [
|
|
3192
|
+
'Bearer realm="mcp"',
|
|
3193
|
+
...invalidToken ? [
|
|
3194
|
+
'error="invalid_token"',
|
|
3195
|
+
'error_description="The credential was rejected; re-authenticate."'
|
|
3196
|
+
] : [],
|
|
3197
|
+
`resource_metadata="${getBaseUrl(
|
|
2186
3198
|
req
|
|
2187
3199
|
)}/.well-known/oauth-protected-resource"`
|
|
2188
|
-
|
|
2189
|
-
res.writeHead(401,
|
|
3200
|
+
].join(", ");
|
|
3201
|
+
res.writeHead(401, {
|
|
3202
|
+
"Content-Type": "application/json",
|
|
3203
|
+
"WWW-Authenticate": challenge
|
|
3204
|
+
});
|
|
2190
3205
|
res.end(JSON.stringify({ error }));
|
|
2191
3206
|
}
|
|
3207
|
+
function authGraphqlUrl(env) {
|
|
3208
|
+
return authedGraphqlBase(env);
|
|
3209
|
+
}
|
|
3210
|
+
var defaultCredentialChecker = createCredentialChecker({
|
|
3211
|
+
fetch: (url, init) => fetch(url, init),
|
|
3212
|
+
url: () => authGraphqlUrl(process.env)
|
|
3213
|
+
});
|
|
3214
|
+
var sameHost = (a, b) => normalizeUrl(a) === normalizeUrl(b);
|
|
3215
|
+
function normalizeUrl(raw) {
|
|
3216
|
+
try {
|
|
3217
|
+
const u = new URL(raw);
|
|
3218
|
+
return `${u.protocol}//${u.host.toLowerCase()}${u.pathname.replace(/\/+$/, "")}`;
|
|
3219
|
+
} catch {
|
|
3220
|
+
return raw.trim().toLowerCase().replace(/\/+$/, "");
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
function credentialCheckMayBeWrongEnvironment(env = process.env) {
|
|
3224
|
+
const envWithoutEmpties = Object.fromEntries(
|
|
3225
|
+
Object.entries(env).filter(([, v]) => v !== "")
|
|
3226
|
+
);
|
|
3227
|
+
const restMoved = !sameHost(restV1Base(envWithoutEmpties), restV1Base({})) || !sameHost(restV2Base(envWithoutEmpties), restV2Base({})) || !sameHost(serverlessBase(envWithoutEmpties), serverlessBase({}));
|
|
3228
|
+
const graphqlMoved = !sameHost(authGraphqlUrl(env), authGraphqlUrl({}));
|
|
3229
|
+
return restMoved !== graphqlMoved;
|
|
3230
|
+
}
|
|
3231
|
+
function looksLikeMcpMessage(value) {
|
|
3232
|
+
if (typeof value !== "object" || value === null) return false;
|
|
3233
|
+
const msg = value;
|
|
3234
|
+
return msg.jsonrpc === "2.0" && typeof msg.method === "string";
|
|
3235
|
+
}
|
|
3236
|
+
function bodyIsCheckable(body) {
|
|
3237
|
+
const messages = Array.isArray(body) ? body : [body];
|
|
3238
|
+
return messages.some(looksLikeMcpMessage);
|
|
3239
|
+
}
|
|
3240
|
+
function isJsonContentType2(req) {
|
|
3241
|
+
const ct = req.headers["content-type"];
|
|
3242
|
+
if (!ct) return false;
|
|
3243
|
+
return ct.toLowerCase().includes("application/json");
|
|
3244
|
+
}
|
|
3245
|
+
var MAX_BODY_BYTES = 4 * 1024 * 1024;
|
|
3246
|
+
var BODY_TOO_LARGE = Symbol("body_too_large");
|
|
3247
|
+
async function readJsonBody(req) {
|
|
3248
|
+
const existing = req.body;
|
|
3249
|
+
if (existing !== void 0) return existing;
|
|
3250
|
+
const declared = Number(req.headers["content-length"]);
|
|
3251
|
+
if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
|
|
3252
|
+
return BODY_TOO_LARGE;
|
|
3253
|
+
}
|
|
3254
|
+
try {
|
|
3255
|
+
const chunks = [];
|
|
3256
|
+
let total = 0;
|
|
3257
|
+
for await (const chunk of req) {
|
|
3258
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
3259
|
+
total += buf.length;
|
|
3260
|
+
if (total > MAX_BODY_BYTES) return BODY_TOO_LARGE;
|
|
3261
|
+
chunks.push(buf);
|
|
3262
|
+
}
|
|
3263
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
3264
|
+
return raw ? JSON.parse(raw) : void 0;
|
|
3265
|
+
} catch {
|
|
3266
|
+
return void 0;
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
function earlyCredentialCheckSkip(req) {
|
|
3270
|
+
if (process.env.MCP_SKIP_CREDENTIAL_CHECK === "true") return "skip_env_var";
|
|
3271
|
+
if (req.method !== "POST") return "skip_not_post";
|
|
3272
|
+
if (!isJsonContentType2(req)) return "skip_not_json";
|
|
3273
|
+
if (credentialCheckMayBeWrongEnvironment()) return "skip_env_mismatch";
|
|
3274
|
+
return null;
|
|
3275
|
+
}
|
|
3276
|
+
function buildToolContext(req, bearerToken, opts = {}) {
|
|
3277
|
+
const drop = opts.invalidateCredential ?? defaultCredentialChecker.invalidate;
|
|
3278
|
+
return {
|
|
3279
|
+
apiKey: bearerToken,
|
|
3280
|
+
transport: "http",
|
|
3281
|
+
// A tools/call is a separate request from `initialize` in stateless HTTP, so
|
|
3282
|
+
// the per-request server never sees the MCP `clientInfo`; fall back to the
|
|
3283
|
+
// inbound User-Agent so caller-tracking still attributes the client.
|
|
3284
|
+
clientUserAgent: req.headers["user-agent"],
|
|
3285
|
+
serverVersion: opts.serverVersion,
|
|
3286
|
+
// A tool call that 401s proves a cached "valid" wrong before its TTL. Drop it
|
|
3287
|
+
// so the next request re-checks and can emit the 401 that triggers re-auth.
|
|
3288
|
+
onUnauthorized: () => drop(bearerToken)
|
|
3289
|
+
};
|
|
3290
|
+
}
|
|
3291
|
+
function logCredentialCheck(outcome) {
|
|
3292
|
+
if (outcome === "pass" && process.env.MCP_VERBOSE_LOGS !== "true") return;
|
|
3293
|
+
console.log("credential_check", { outcome });
|
|
3294
|
+
}
|
|
2192
3295
|
async function handleMcpRequest(req, res, opts = {}) {
|
|
2193
3296
|
const bearerToken = extractBearerToken(req);
|
|
2194
3297
|
console.log("mcp_request", {
|
|
@@ -2204,13 +3307,39 @@ async function handleMcpRequest(req, res, opts = {}) {
|
|
|
2204
3307
|
);
|
|
2205
3308
|
return;
|
|
2206
3309
|
}
|
|
3310
|
+
let body = req.body;
|
|
3311
|
+
const early = earlyCredentialCheckSkip(req);
|
|
3312
|
+
if (early) {
|
|
3313
|
+
logCredentialCheck(early);
|
|
3314
|
+
} else {
|
|
3315
|
+
const read = await readJsonBody(req);
|
|
3316
|
+
if (read === BODY_TOO_LARGE) {
|
|
3317
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
3318
|
+
res.end(JSON.stringify({ error: "Request body too large." }));
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
body = read;
|
|
3322
|
+
if (!bodyIsCheckable(body)) {
|
|
3323
|
+
logCredentialCheck("skip_not_mcp_body");
|
|
3324
|
+
} else {
|
|
3325
|
+
const verify = opts.verifyCredential ?? defaultCredentialChecker.verify;
|
|
3326
|
+
const verdict = await verify(bearerToken);
|
|
3327
|
+
logCredentialCheck(
|
|
3328
|
+
verdict.status === "invalid" ? "reject" : verdict.status === "valid" ? "pass" : "fail_open"
|
|
3329
|
+
);
|
|
3330
|
+
if (verdict.status === "invalid") {
|
|
3331
|
+
writeUnauthorized(
|
|
3332
|
+
req,
|
|
3333
|
+
res,
|
|
3334
|
+
`${verdict.reason} Re-authenticate to continue.`,
|
|
3335
|
+
true
|
|
3336
|
+
);
|
|
3337
|
+
return;
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
2207
3341
|
const server = createServer(opts.serverVersion);
|
|
2208
|
-
registerTools(server,
|
|
2209
|
-
apiKey: bearerToken,
|
|
2210
|
-
transport: "http",
|
|
2211
|
-
clientUserAgent: req.headers["user-agent"],
|
|
2212
|
-
serverVersion: opts.serverVersion
|
|
2213
|
-
});
|
|
3342
|
+
registerTools(server, buildToolContext(req, bearerToken, opts));
|
|
2214
3343
|
const transport = new import_streamableHttp.StreamableHTTPServerTransport({
|
|
2215
3344
|
sessionIdGenerator: void 0
|
|
2216
3345
|
// stateless — no session persistence
|
|
@@ -2222,10 +3351,13 @@ async function handleMcpRequest(req, res, opts = {}) {
|
|
|
2222
3351
|
});
|
|
2223
3352
|
});
|
|
2224
3353
|
await server.connect(transport);
|
|
2225
|
-
await transport.handleRequest(req, res,
|
|
3354
|
+
await transport.handleRequest(req, res, body);
|
|
2226
3355
|
}
|
|
2227
3356
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2228
3357
|
0 && (module.exports = {
|
|
3358
|
+
bodyIsCheckable,
|
|
3359
|
+
buildToolContext,
|
|
3360
|
+
defaultCredentialChecker,
|
|
2229
3361
|
handleMcpRequest,
|
|
2230
3362
|
registerTools
|
|
2231
3363
|
});
|