@runpod/mcp-server 1.4.0 → 2.0.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 +43 -0
- package/README.md +173 -53
- package/dist/chunk-ZP3TCVXG.mjs +2116 -0
- package/dist/chunk-ZP3TCVXG.mjs.map +1 -0
- package/dist/http.cjs +1545 -589
- package/dist/http.cjs.map +1 -1
- package/dist/http.mjs +2 -2
- package/dist/stdio.cjs +1619 -593
- package/dist/stdio.cjs.map +1 -1
- package/dist/stdio.mjs +1619 -593
- package/dist/stdio.mjs.map +1 -1
- package/dist/tools.cjs +1545 -587
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +51 -3
- package/dist/tools.d.ts +51 -3
- package/dist/tools.mjs +1 -1
- package/package.json +3 -2
- package/dist/chunk-D4AEYJ2O.mjs +0 -1160
- package/dist/chunk-D4AEYJ2O.mjs.map +0 -1
package/dist/http.cjs
CHANGED
|
@@ -39,7 +39,7 @@ var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamable
|
|
|
39
39
|
// src/server.ts
|
|
40
40
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
41
41
|
var SERVER_NAME = "Runpod API Server";
|
|
42
|
-
var SERVER_VERSION = true ? "
|
|
42
|
+
var SERVER_VERSION = true ? "2.0.0" : "dev";
|
|
43
43
|
function createServer(version = SERVER_VERSION) {
|
|
44
44
|
return new import_mcp.McpServer({
|
|
45
45
|
name: SERVER_NAME,
|
|
@@ -51,11 +51,548 @@ function createServer(version = SERVER_VERSION) {
|
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
// src/tools.ts
|
|
55
|
-
var import_zod2 = require("zod");
|
|
54
|
+
// src/tools/runtime.ts
|
|
56
55
|
var import_node_fetch = __toESM(require("node-fetch"), 1);
|
|
57
56
|
var import_node_crypto = require("crypto");
|
|
58
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
|
+
|
|
117
|
+
// src/_shared/mappers.ts
|
|
118
|
+
function compact(obj) {
|
|
119
|
+
const out = {};
|
|
120
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
121
|
+
if (v !== void 0) out[k] = v;
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
function persistentMount(size, path) {
|
|
126
|
+
if (size === void 0 || path === void 0) return void 0;
|
|
127
|
+
return { persistent: { size, path } };
|
|
128
|
+
}
|
|
129
|
+
function containerConfigToV2(p) {
|
|
130
|
+
return compact({
|
|
131
|
+
image: p.imageName,
|
|
132
|
+
disk: p.containerDiskInGb,
|
|
133
|
+
ports: p.ports,
|
|
134
|
+
env: p.env,
|
|
135
|
+
mounts: persistentMount(p.volumeInGb, p.volumeMountPath)
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function gpuConfigToV2(gpuTypeIds, gpuCount) {
|
|
139
|
+
const id = gpuTypeIds?.[0];
|
|
140
|
+
if (id === void 0) return void 0;
|
|
141
|
+
return compact({ id, count: gpuCount });
|
|
142
|
+
}
|
|
143
|
+
function mapPodCreateToV2(params) {
|
|
144
|
+
return compact({
|
|
145
|
+
...containerConfigToV2(params),
|
|
146
|
+
name: params.name,
|
|
147
|
+
cloud: params.cloudType,
|
|
148
|
+
// v2 CreatePodRequest takes `dataCenterIds` as an ARRAY (preferred data
|
|
149
|
+
// centers; omit/empty = scheduler chooses). Pass the v1 list straight
|
|
150
|
+
// through — the earlier singular `dataCenter` key did not exist on the pod
|
|
151
|
+
// schema, so placement was being silently dropped. GPU, by contrast, IS
|
|
152
|
+
// singular on v2 (`gpu.id`): gpuConfigToV2 takes the first id; the
|
|
153
|
+
// create-pod handler warns when more than one gpuTypeId was supplied.
|
|
154
|
+
dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
|
|
155
|
+
gpu: gpuConfigToV2(params.gpuTypeIds, params.gpuCount)
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function mapPodUpdateToV2(params) {
|
|
159
|
+
return {
|
|
160
|
+
...containerConfigToV2(params),
|
|
161
|
+
...compact({ name: params.name })
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function nestedOrUndefined(obj) {
|
|
165
|
+
const c = compact(obj);
|
|
166
|
+
return Object.keys(c).length ? c : void 0;
|
|
167
|
+
}
|
|
168
|
+
function endpointGpuConfig(pools, count) {
|
|
169
|
+
if (!pools?.length) return void 0;
|
|
170
|
+
return compact({ pools, count });
|
|
171
|
+
}
|
|
172
|
+
function mapEndpointToV2(params) {
|
|
173
|
+
return compact({
|
|
174
|
+
name: params.name,
|
|
175
|
+
image: params.imageName,
|
|
176
|
+
args: params.args,
|
|
177
|
+
disk: params.containerDiskInGb,
|
|
178
|
+
ports: params.ports,
|
|
179
|
+
env: params.env,
|
|
180
|
+
registry: params.containerRegistryAuthId,
|
|
181
|
+
gpu: endpointGpuConfig(params.gpuPoolIds, params.gpuCount),
|
|
182
|
+
workers: nestedOrUndefined({
|
|
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
|
+
}),
|
|
191
|
+
dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
|
|
192
|
+
networkVolumes: params.networkVolumeIds?.length ? params.networkVolumeIds : void 0,
|
|
193
|
+
timeout: params.executionTimeoutMs,
|
|
194
|
+
flashboot: params.flashboot
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
var mapEndpointCreateToV2 = mapEndpointToV2;
|
|
198
|
+
var mapEndpointUpdateToV2 = mapEndpointToV2;
|
|
199
|
+
function mapNetworkVolumeCreateToV2(params) {
|
|
200
|
+
return compact({
|
|
201
|
+
name: params.name,
|
|
202
|
+
size: params.size,
|
|
203
|
+
dataCenter: params.dataCenterId
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
function mapTemplateCreateToV2(params) {
|
|
207
|
+
return {
|
|
208
|
+
...containerConfigToV2(params),
|
|
209
|
+
...compact({
|
|
210
|
+
name: params.name,
|
|
211
|
+
serverless: params.isServerless,
|
|
212
|
+
registry: params.containerRegistryAuthId
|
|
213
|
+
}),
|
|
214
|
+
// Required by v2 — always emit, defaulting to NVIDIA.
|
|
215
|
+
category: params.category ?? "NVIDIA"
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function mapTemplateUpdateToV2(params) {
|
|
219
|
+
return {
|
|
220
|
+
...containerConfigToV2(params),
|
|
221
|
+
...compact({
|
|
222
|
+
name: params.name,
|
|
223
|
+
registry: params.containerRegistryAuthId
|
|
224
|
+
})
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/_shared/backend.ts
|
|
229
|
+
var V1_ONLY = /* @__PURE__ */ new Set(["jobs"]);
|
|
230
|
+
var V2_UNWRAP_KEY = {
|
|
231
|
+
pods: "pods",
|
|
232
|
+
templates: "templates",
|
|
233
|
+
networkVolumes: "networkVolumes",
|
|
234
|
+
registries: "registries",
|
|
235
|
+
gpus: "gpus",
|
|
236
|
+
cpus: "cpus",
|
|
237
|
+
dataCenters: "dataCenters",
|
|
238
|
+
endpoints: "endpoints",
|
|
239
|
+
jobs: "jobs",
|
|
240
|
+
tags: "tags",
|
|
241
|
+
workers: "workers",
|
|
242
|
+
billing: "records"
|
|
243
|
+
};
|
|
244
|
+
var RESOURCE_ENV_KEY = {
|
|
245
|
+
pods: "PODS",
|
|
246
|
+
templates: "TEMPLATES",
|
|
247
|
+
networkVolumes: "NETWORK_VOLUMES",
|
|
248
|
+
registries: "REGISTRIES",
|
|
249
|
+
gpus: "GPUS",
|
|
250
|
+
cpus: "CPUS",
|
|
251
|
+
dataCenters: "DATA_CENTERS",
|
|
252
|
+
endpoints: "ENDPOINTS",
|
|
253
|
+
jobs: "JOBS",
|
|
254
|
+
tags: "TAGS",
|
|
255
|
+
workers: "WORKERS",
|
|
256
|
+
billing: "BILLING"
|
|
257
|
+
};
|
|
258
|
+
function restV1Base(env) {
|
|
259
|
+
return env.RUNPOD_REST_API_URL ?? "https://rest.runpod.io/v1";
|
|
260
|
+
}
|
|
261
|
+
function restV2Base(env) {
|
|
262
|
+
return env.RUNPOD_REST_V2_API_URL ?? "https://v2-rest.runpod.io/v2";
|
|
263
|
+
}
|
|
264
|
+
function serverlessBase(env) {
|
|
265
|
+
return env.RUNPOD_SERVERLESS_API_URL ?? "https://api.runpod.ai/v2";
|
|
266
|
+
}
|
|
267
|
+
function publicGraphqlBase(env) {
|
|
268
|
+
return env.RUNPOD_PUBLIC_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
|
|
269
|
+
}
|
|
270
|
+
function unwrapList(resource, version, raw) {
|
|
271
|
+
if (version === "v1") return Array.isArray(raw) ? raw : [];
|
|
272
|
+
if (raw === null || typeof raw !== "object") return [];
|
|
273
|
+
const arr = raw[V2_UNWRAP_KEY[resource]];
|
|
274
|
+
return Array.isArray(arr) ? arr : [];
|
|
275
|
+
}
|
|
276
|
+
function resolveVersion(opts) {
|
|
277
|
+
const { resource, env, ctx, v2Available } = opts;
|
|
278
|
+
if (V1_ONLY.has(resource)) return "v1";
|
|
279
|
+
const perResource = env[`RUNPOD_REST_VERSION_${RESOURCE_ENV_KEY[resource]}`];
|
|
280
|
+
const setting = (perResource ?? env.RUNPOD_REST_VERSION ?? "v2").toLowerCase();
|
|
281
|
+
if (setting === "v2") return "v2";
|
|
282
|
+
if (setting === "auto") {
|
|
283
|
+
if (ctx.transport === "http") return "v1";
|
|
284
|
+
return v2Available === true ? "v2" : "v1";
|
|
285
|
+
}
|
|
286
|
+
return "v1";
|
|
287
|
+
}
|
|
288
|
+
var identity = (body) => body;
|
|
289
|
+
var V1_REST_PATHS = {
|
|
290
|
+
pods: { list: "/pods", get: (id) => `/pods/${id}` },
|
|
291
|
+
templates: { list: "/templates", get: (id) => `/templates/${id}` },
|
|
292
|
+
networkVolumes: {
|
|
293
|
+
list: "/networkvolumes",
|
|
294
|
+
get: (id) => `/networkvolumes/${id}`
|
|
295
|
+
},
|
|
296
|
+
registries: {
|
|
297
|
+
list: "/containerregistryauth",
|
|
298
|
+
get: (id) => `/containerregistryauth/${id}`
|
|
299
|
+
},
|
|
300
|
+
endpoints: { list: "/endpoints", get: (id) => `/endpoints/${id}` }
|
|
301
|
+
};
|
|
302
|
+
var CATALOG = /* @__PURE__ */ new Set([
|
|
303
|
+
"gpus",
|
|
304
|
+
"cpus",
|
|
305
|
+
"dataCenters"
|
|
306
|
+
]);
|
|
307
|
+
var V2_ONLY = /* @__PURE__ */ new Set([
|
|
308
|
+
"tags",
|
|
309
|
+
"workers",
|
|
310
|
+
"billing"
|
|
311
|
+
]);
|
|
312
|
+
var V2_REST_PATHS = {
|
|
313
|
+
pods: { list: "/pods", get: (id) => `/pods/${id}` },
|
|
314
|
+
templates: { list: "/templates", get: (id) => `/templates/${id}` },
|
|
315
|
+
networkVolumes: {
|
|
316
|
+
list: "/network-volumes",
|
|
317
|
+
get: (id) => `/network-volumes/${id}`
|
|
318
|
+
},
|
|
319
|
+
registries: { list: "/registries", get: (id) => `/registries/${id}` },
|
|
320
|
+
gpus: { list: "/catalog/gpus", get: (id) => `/catalog/gpus/${id}` },
|
|
321
|
+
cpus: { list: "/catalog/cpus", get: (id) => `/catalog/cpus/${id}` },
|
|
322
|
+
dataCenters: {
|
|
323
|
+
list: "/catalog/datacenters",
|
|
324
|
+
get: (id) => `/catalog/datacenters/${id}`
|
|
325
|
+
},
|
|
326
|
+
tags: { list: "/tags", get: (id) => `/tags/${id}` },
|
|
327
|
+
// Endpoints live under /v2/serverless on v2 (NOT /v2/endpoints). The v2 list
|
|
328
|
+
// response wraps rows under `endpoints` (see V2_UNWRAP_KEY), so the existing
|
|
329
|
+
// unwrap key stays correct.
|
|
330
|
+
endpoints: { list: "/serverless", get: (id) => `/serverless/${id}` },
|
|
331
|
+
// workers/billing build their own sub-paths in the tool from `base`; the
|
|
332
|
+
// list entry just satisfies buildV2Backend (which requires a paths entry).
|
|
333
|
+
workers: { list: "/serverless" },
|
|
334
|
+
billing: { list: "/billing" }
|
|
335
|
+
};
|
|
336
|
+
var V2_MAPPERS = {
|
|
337
|
+
pods: {
|
|
338
|
+
create: (b) => mapPodCreateToV2(b),
|
|
339
|
+
update: (b) => mapPodUpdateToV2(b)
|
|
340
|
+
},
|
|
341
|
+
templates: {
|
|
342
|
+
create: (b) => mapTemplateCreateToV2(b),
|
|
343
|
+
update: (b) => mapTemplateUpdateToV2(b)
|
|
344
|
+
},
|
|
345
|
+
networkVolumes: {
|
|
346
|
+
create: (b) => mapNetworkVolumeCreateToV2(
|
|
347
|
+
b
|
|
348
|
+
),
|
|
349
|
+
update: identity
|
|
350
|
+
},
|
|
351
|
+
registries: { create: identity, update: identity },
|
|
352
|
+
endpoints: {
|
|
353
|
+
create: (b) => mapEndpointCreateToV2(b),
|
|
354
|
+
update: (b) => mapEndpointUpdateToV2(b)
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
function buildV2Backend(resource, env, unwrap) {
|
|
358
|
+
const paths = V2_REST_PATHS[resource];
|
|
359
|
+
if (!paths) throw new Error(`no v2 REST paths for resource "${resource}"`);
|
|
360
|
+
const mappers = V2_MAPPERS[resource];
|
|
361
|
+
return {
|
|
362
|
+
kind: "rest",
|
|
363
|
+
version: "v2",
|
|
364
|
+
base: restV2Base(env),
|
|
365
|
+
list: paths.list,
|
|
366
|
+
get: paths.get,
|
|
367
|
+
unwrap,
|
|
368
|
+
mapCreate: mappers?.create ?? identity,
|
|
369
|
+
mapUpdate: mappers?.update ?? identity
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
function buildV1Backend(resource, env, unwrap) {
|
|
373
|
+
if (resource === "jobs") {
|
|
374
|
+
return {
|
|
375
|
+
kind: "rest",
|
|
376
|
+
version: "v1",
|
|
377
|
+
base: serverlessBase(env),
|
|
378
|
+
unwrap,
|
|
379
|
+
mapCreate: identity,
|
|
380
|
+
mapUpdate: identity
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
if (V2_ONLY.has(resource)) {
|
|
384
|
+
return {
|
|
385
|
+
kind: "rest",
|
|
386
|
+
version: "v1",
|
|
387
|
+
base: restV1Base(env),
|
|
388
|
+
unwrap,
|
|
389
|
+
mapCreate: identity,
|
|
390
|
+
mapUpdate: identity
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
if (CATALOG.has(resource)) {
|
|
394
|
+
return {
|
|
395
|
+
kind: "graphql",
|
|
396
|
+
version: "v1",
|
|
397
|
+
base: publicGraphqlBase(env),
|
|
398
|
+
unwrap,
|
|
399
|
+
mapCreate: identity,
|
|
400
|
+
mapUpdate: identity
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
const paths = V1_REST_PATHS[resource];
|
|
404
|
+
if (!paths) throw new Error(`no v1 REST paths for resource "${resource}"`);
|
|
405
|
+
return {
|
|
406
|
+
kind: "rest",
|
|
407
|
+
version: "v1",
|
|
408
|
+
base: restV1Base(env),
|
|
409
|
+
list: paths.list,
|
|
410
|
+
get: paths.get,
|
|
411
|
+
unwrap,
|
|
412
|
+
mapCreate: identity,
|
|
413
|
+
mapUpdate: identity
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function resolveBackend(opts) {
|
|
417
|
+
const { resource, env } = opts;
|
|
418
|
+
const version = resolveVersion(opts);
|
|
419
|
+
const unwrap = (raw) => unwrapList(resource, version, raw);
|
|
420
|
+
return version === "v2" ? buildV2Backend(resource, env, unwrap) : buildV1Backend(resource, env, unwrap);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/tools/runtime.ts
|
|
424
|
+
var MCP_SERVER_VERSION = true ? "2.0.0" : "dev";
|
|
425
|
+
var SESSION_ID = (0, import_node_crypto.randomUUID)();
|
|
426
|
+
function trackingHeaders(server, ctx) {
|
|
427
|
+
const info = server.server.getClientVersion();
|
|
428
|
+
return buildTrackingHeaders({
|
|
429
|
+
clientName: info?.name || ctx.clientUserAgent || "unknown",
|
|
430
|
+
clientVersion: info?.version || "unknown",
|
|
431
|
+
transport: ctx.transport,
|
|
432
|
+
// Prefer a server version supplied by the caller (the hosted entrypoint reads
|
|
433
|
+
// it from package.json at runtime, since tsup's build-time define does not
|
|
434
|
+
// run when Vercel compiles api/index.ts). Fall back to the build-time value.
|
|
435
|
+
serverVersion: ctx.serverVersion || MCP_SERVER_VERSION,
|
|
436
|
+
sessionId: SESSION_ID
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
var defaultFetch = import_node_fetch.default;
|
|
440
|
+
async function withApiErrorLog(label, call) {
|
|
441
|
+
try {
|
|
442
|
+
return await call();
|
|
443
|
+
} catch (error) {
|
|
444
|
+
console.error(label, error);
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function jsonReply(result) {
|
|
449
|
+
return {
|
|
450
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
var READ_ONLY = { readOnlyHint: true, openWorldHint: true };
|
|
454
|
+
var WRITE = { readOnlyHint: false, openWorldHint: true };
|
|
455
|
+
var DESTRUCTIVE = {
|
|
456
|
+
readOnlyHint: false,
|
|
457
|
+
destructiveHint: true,
|
|
458
|
+
idempotentHint: true,
|
|
459
|
+
openWorldHint: true
|
|
460
|
+
};
|
|
461
|
+
async function graphqlRequest(query, tracking, fetchImpl, url) {
|
|
462
|
+
const response = await fetchImpl(url, {
|
|
463
|
+
method: "POST",
|
|
464
|
+
headers: {
|
|
465
|
+
"Content-Type": "application/json",
|
|
466
|
+
...tracking()
|
|
467
|
+
},
|
|
468
|
+
body: JSON.stringify({ query })
|
|
469
|
+
});
|
|
470
|
+
const result = await response.json();
|
|
471
|
+
if (result.errors && result.errors.length > 0) {
|
|
472
|
+
throw new Error(
|
|
473
|
+
`GraphQL Error: ${result.errors.map((e) => e.message).join(", ")}`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
if (!result.data) {
|
|
477
|
+
throw new Error("No data returned from GraphQL query");
|
|
478
|
+
}
|
|
479
|
+
return result.data;
|
|
480
|
+
}
|
|
481
|
+
function createToolRuntime(server, ctx, deps = {}) {
|
|
482
|
+
const tracking = () => trackingHeaders(server, ctx);
|
|
483
|
+
const httpFetch = deps.fetch ?? defaultFetch;
|
|
484
|
+
const v1Client = createHttpClient({
|
|
485
|
+
apiKey: ctx.apiKey,
|
|
486
|
+
fetch: httpFetch,
|
|
487
|
+
tracking,
|
|
488
|
+
errorPrefix: "Runpod API Error"
|
|
489
|
+
});
|
|
490
|
+
const runpodRequest = (endpoint, method = "GET", body) => withApiErrorLog(
|
|
491
|
+
"Error calling Runpod API:",
|
|
492
|
+
() => v1Client(`${restV1Base(process.env)}${endpoint}`, method, body)
|
|
493
|
+
);
|
|
494
|
+
const serverlessClient = createHttpClient({
|
|
495
|
+
apiKey: ctx.apiKey,
|
|
496
|
+
fetch: httpFetch,
|
|
497
|
+
tracking,
|
|
498
|
+
errorPrefix: "Runpod Serverless API Error"
|
|
499
|
+
});
|
|
500
|
+
const serverlessRequest = (endpointId, path, method = "GET", body) => withApiErrorLog(
|
|
501
|
+
"Error calling Runpod Serverless API:",
|
|
502
|
+
() => serverlessClient(
|
|
503
|
+
`${serverlessBase(process.env)}/${endpointId}${path}`,
|
|
504
|
+
method,
|
|
505
|
+
body
|
|
506
|
+
)
|
|
507
|
+
);
|
|
508
|
+
const restClient = createHttpClient({
|
|
509
|
+
apiKey: ctx.apiKey,
|
|
510
|
+
fetch: httpFetch,
|
|
511
|
+
tracking,
|
|
512
|
+
errorPrefix: "Runpod API Error"
|
|
513
|
+
});
|
|
514
|
+
const callRestUrl = (url, method = "GET", body) => withApiErrorLog(
|
|
515
|
+
"Error calling Runpod API:",
|
|
516
|
+
() => restClient(url, method, body)
|
|
517
|
+
);
|
|
518
|
+
const streamSse = deps.streamSse ?? (async (url, opts) => {
|
|
519
|
+
const controller = new AbortController();
|
|
520
|
+
const timer = setTimeout(() => controller.abort(), opts.maxWaitMs);
|
|
521
|
+
try {
|
|
522
|
+
const res = await (0, import_node_fetch.default)(url, {
|
|
523
|
+
method: "GET",
|
|
524
|
+
headers: {
|
|
525
|
+
Authorization: `Bearer ${ctx.apiKey}`,
|
|
526
|
+
Accept: "text/event-stream",
|
|
527
|
+
...tracking()
|
|
528
|
+
},
|
|
529
|
+
signal: controller.signal
|
|
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
|
+
});
|
|
559
|
+
const backendFor = (resource) => resolveBackend({
|
|
560
|
+
resource,
|
|
561
|
+
env: process.env,
|
|
562
|
+
ctx: { transport: ctx.transport },
|
|
563
|
+
v2Available: deps.v2Available
|
|
564
|
+
});
|
|
565
|
+
const podAction = async (podId, action) => {
|
|
566
|
+
const backend = backendFor("pods");
|
|
567
|
+
const podPath = backend.get(podId);
|
|
568
|
+
if (backend.version === "v2") {
|
|
569
|
+
return callRestUrl(`${backend.base}${podPath}/action`, "POST", {
|
|
570
|
+
action
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
return callRestUrl(`${backend.base}${podPath}/${action}`, "POST");
|
|
574
|
+
};
|
|
575
|
+
return {
|
|
576
|
+
jsonReply,
|
|
577
|
+
graphql: (query) => graphqlRequest(
|
|
578
|
+
query,
|
|
579
|
+
tracking,
|
|
580
|
+
httpFetch,
|
|
581
|
+
publicGraphqlBase(process.env)
|
|
582
|
+
),
|
|
583
|
+
runpodRequest,
|
|
584
|
+
serverlessRequest,
|
|
585
|
+
callRestUrl,
|
|
586
|
+
backendFor,
|
|
587
|
+
streamSse,
|
|
588
|
+
podAction,
|
|
589
|
+
env: process.env
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/tools/catalog.ts
|
|
594
|
+
var import_zod2 = require("zod");
|
|
595
|
+
|
|
59
596
|
// src/pagination.ts
|
|
60
597
|
var import_zod = require("zod");
|
|
61
598
|
var DEFAULT_LIST_LIMIT = 20;
|
|
@@ -81,7 +618,7 @@ function decodeCursorOffset(cursor) {
|
|
|
81
618
|
function encodeCursorOffset(offset) {
|
|
82
619
|
return Buffer.from(String(offset), "utf8").toString("base64");
|
|
83
620
|
}
|
|
84
|
-
function capListResult(result, options) {
|
|
621
|
+
function capListResult(result, options, extra) {
|
|
85
622
|
const limit = Math.min(options.limit ?? DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT);
|
|
86
623
|
const offset = decodeCursorOffset(options.cursor);
|
|
87
624
|
if (!Array.isArray(result)) {
|
|
@@ -95,7 +632,11 @@ function capListResult(result, options) {
|
|
|
95
632
|
const page = result.slice(offset, offset + limit);
|
|
96
633
|
const nextOffset = offset + page.length;
|
|
97
634
|
const hasMore = nextOffset < total;
|
|
635
|
+
const { items: _i, pagination: _p, ...safeExtra } = extra ?? {};
|
|
636
|
+
void _i;
|
|
637
|
+
void _p;
|
|
98
638
|
const envelope = {
|
|
639
|
+
...safeExtra,
|
|
99
640
|
items: page,
|
|
100
641
|
pagination: {
|
|
101
642
|
total,
|
|
@@ -113,120 +654,14 @@ function capListResult(result, options) {
|
|
|
113
654
|
};
|
|
114
655
|
}
|
|
115
656
|
|
|
116
|
-
// src/tools.ts
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
var GRAPHQL_URL = process.env.RUNPOD_PUBLIC_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
|
|
120
|
-
var MCP_SERVER_VERSION = true ? "1.4.0" : "dev";
|
|
121
|
-
var SESSION_ID = (0, import_node_crypto.randomUUID)();
|
|
122
|
-
function sanitizeUaToken(value) {
|
|
123
|
-
return value.replace(/[()<>@,;:\\"/[\]?={}\s]/g, "_").slice(0, 64);
|
|
124
|
-
}
|
|
125
|
-
function trackingHeaders(server, ctx) {
|
|
126
|
-
const info = server.server.getClientVersion();
|
|
127
|
-
const fallbackName = ctx.clientUserAgent || "unknown";
|
|
128
|
-
const name = sanitizeUaToken(info?.name || fallbackName);
|
|
129
|
-
const version = sanitizeUaToken(info?.version || "unknown");
|
|
130
|
-
const serverVersion = ctx.serverVersion || MCP_SERVER_VERSION;
|
|
131
|
-
const userAgent = `runpod-mcp-server/${serverVersion} (caller=mcp; client=${name}; client_version=${version}; transport=${ctx.transport})`;
|
|
132
|
-
return {
|
|
133
|
-
"User-Agent": userAgent,
|
|
134
|
-
"X-Runpod-Session-Id": SESSION_ID
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
async function graphqlRequest(query, tracking) {
|
|
138
|
-
const response = await (0, import_node_fetch.default)(GRAPHQL_URL, {
|
|
139
|
-
method: "POST",
|
|
140
|
-
headers: {
|
|
141
|
-
"Content-Type": "application/json",
|
|
142
|
-
...tracking()
|
|
143
|
-
},
|
|
144
|
-
body: JSON.stringify({ query })
|
|
145
|
-
});
|
|
146
|
-
const result = await response.json();
|
|
147
|
-
if (result.errors && result.errors.length > 0) {
|
|
148
|
-
throw new Error(
|
|
149
|
-
`GraphQL Error: ${result.errors.map((e) => e.message).join(", ")}`
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
if (!result.data) {
|
|
153
|
-
throw new Error("No data returned from GraphQL query");
|
|
154
|
-
}
|
|
155
|
-
return result.data;
|
|
156
|
-
}
|
|
157
|
-
function createRunpodRequest(apiKey, tracking) {
|
|
158
|
-
return async function runpodRequest(endpoint, method = "GET", body) {
|
|
159
|
-
const url = `${API_BASE_URL}${endpoint}`;
|
|
160
|
-
const headers = {
|
|
161
|
-
Authorization: `Bearer ${apiKey}`,
|
|
162
|
-
"Content-Type": "application/json",
|
|
163
|
-
...tracking()
|
|
164
|
-
};
|
|
165
|
-
const options = {
|
|
166
|
-
method,
|
|
167
|
-
headers
|
|
168
|
-
};
|
|
169
|
-
if (body && (method === "POST" || method === "PATCH")) {
|
|
170
|
-
options.body = JSON.stringify(body);
|
|
171
|
-
}
|
|
172
|
-
try {
|
|
173
|
-
const response = await (0, import_node_fetch.default)(url, options);
|
|
174
|
-
if (!response.ok) {
|
|
175
|
-
const errorText = await response.text();
|
|
176
|
-
throw new Error(`Runpod API Error: ${response.status} - ${errorText}`);
|
|
177
|
-
}
|
|
178
|
-
const contentType = response.headers.get("content-type");
|
|
179
|
-
if (contentType && contentType.includes("application/json")) {
|
|
180
|
-
return await response.json();
|
|
181
|
-
}
|
|
182
|
-
return { success: true, status: response.status };
|
|
183
|
-
} catch (error) {
|
|
184
|
-
console.error("Error calling Runpod API:", error);
|
|
185
|
-
throw error;
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
function createServerlessRequest(apiKey, tracking) {
|
|
190
|
-
return async function serverlessRequest(endpointId, path, method = "GET", body) {
|
|
191
|
-
const url = `${SERVERLESS_API_BASE_URL}/${endpointId}${path}`;
|
|
192
|
-
const headers = {
|
|
193
|
-
Authorization: `Bearer ${apiKey}`,
|
|
194
|
-
"Content-Type": "application/json",
|
|
195
|
-
...tracking()
|
|
196
|
-
};
|
|
197
|
-
const options = {
|
|
198
|
-
method,
|
|
199
|
-
headers
|
|
200
|
-
};
|
|
201
|
-
if (body && (method === "POST" || method === "PATCH")) {
|
|
202
|
-
options.body = JSON.stringify(body);
|
|
203
|
-
}
|
|
204
|
-
try {
|
|
205
|
-
const response = await (0, import_node_fetch.default)(url, options);
|
|
206
|
-
if (!response.ok) {
|
|
207
|
-
const errorText = await response.text();
|
|
208
|
-
throw new Error(
|
|
209
|
-
`Runpod Serverless API Error: ${response.status} - ${errorText}`
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
const contentType = response.headers.get("content-type");
|
|
213
|
-
if (contentType && contentType.includes("application/json")) {
|
|
214
|
-
return await response.json();
|
|
215
|
-
}
|
|
216
|
-
return { success: true, status: response.status };
|
|
217
|
-
} catch (error) {
|
|
218
|
-
console.error("Error calling Runpod Serverless API:", error);
|
|
219
|
-
throw error;
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
function registerTools(server, ctx) {
|
|
224
|
-
const tracking = () => trackingHeaders(server, ctx);
|
|
225
|
-
const runpodRequest = createRunpodRequest(ctx.apiKey, tracking);
|
|
226
|
-
const serverlessRequest = createServerlessRequest(ctx.apiKey, tracking);
|
|
657
|
+
// src/tools/catalog.ts
|
|
658
|
+
function registerCatalogTools(server, rt) {
|
|
659
|
+
const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
|
|
227
660
|
server.tool(
|
|
228
661
|
"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. On v2 the realtime stock filter is a no-op.",
|
|
229
663
|
{
|
|
664
|
+
...listPaginationParams,
|
|
230
665
|
minMemoryGb: import_zod2.z.number().optional().describe("Filter to GPUs with at least this much VRAM in GB"),
|
|
231
666
|
secureCloudOnly: import_zod2.z.boolean().optional().describe("Filter to only GPUs available in secure cloud"),
|
|
232
667
|
communityCloudOnly: import_zod2.z.boolean().optional().describe("Filter to only GPUs available in community cloud"),
|
|
@@ -237,9 +672,30 @@ function registerTools(server, ctx) {
|
|
|
237
672
|
"Include GPUs that are currently out of stock. Default is false"
|
|
238
673
|
)
|
|
239
674
|
},
|
|
675
|
+
{ title: "List GPU types", ...READ_ONLY },
|
|
240
676
|
async (params) => {
|
|
241
|
-
const
|
|
242
|
-
|
|
677
|
+
const backend = backendFor("gpus");
|
|
678
|
+
if (backend.version === "v2") {
|
|
679
|
+
const raw = await callRestUrl(`${backend.base}${backend.list}`);
|
|
680
|
+
let gpus = backend.unwrap(raw);
|
|
681
|
+
if (params.minMemoryGb !== void 0)
|
|
682
|
+
gpus = gpus.filter(
|
|
683
|
+
(g) => Number(g.memory ?? 0) >= params.minMemoryGb
|
|
684
|
+
);
|
|
685
|
+
if (params.secureCloudOnly) gpus = gpus.filter((g) => g.secure);
|
|
686
|
+
if (params.communityCloudOnly) gpus = gpus.filter((g) => g.community);
|
|
687
|
+
if (params.searchTerm) {
|
|
688
|
+
const t = params.searchTerm.toLowerCase();
|
|
689
|
+
gpus = gpus.filter(
|
|
690
|
+
(g) => String(g.id ?? "").toLowerCase().includes(t) || String(g.name ?? "").toLowerCase().includes(t)
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
return capListResult(gpus, {
|
|
694
|
+
limit: params.limit,
|
|
695
|
+
cursor: params.cursor
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
const data = await graphql(`
|
|
243
699
|
query {
|
|
244
700
|
gpuTypes {
|
|
245
701
|
id
|
|
@@ -252,9 +708,7 @@ function registerTools(server, ctx) {
|
|
|
252
708
|
}
|
|
253
709
|
}
|
|
254
710
|
}
|
|
255
|
-
|
|
256
|
-
tracking
|
|
257
|
-
);
|
|
711
|
+
`);
|
|
258
712
|
const stockPriority = {
|
|
259
713
|
High: 3,
|
|
260
714
|
Medium: 2,
|
|
@@ -300,26 +754,39 @@ function registerTools(server, ctx) {
|
|
|
300
754
|
stockStatus: gpu.lowestPrice?.stockStatus || "unavailable",
|
|
301
755
|
available: isAvailable(gpu)
|
|
302
756
|
}));
|
|
303
|
-
return {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
text: JSON.stringify(result, null, 2)
|
|
308
|
-
}
|
|
309
|
-
]
|
|
310
|
-
};
|
|
757
|
+
return capListResult(result, {
|
|
758
|
+
limit: params.limit,
|
|
759
|
+
cursor: params.cursor
|
|
760
|
+
});
|
|
311
761
|
}
|
|
312
762
|
);
|
|
313
763
|
server.tool(
|
|
314
764
|
"list-data-centers",
|
|
765
|
+
"List Runpod data centers (id, name, region/location). Use this to discover valid dataCenterIds for placing pods, endpoints, or network volumes.",
|
|
315
766
|
{
|
|
767
|
+
...listPaginationParams,
|
|
316
768
|
region: import_zod2.z.string().optional().describe(
|
|
317
769
|
"Filter by region/location (e.g., 'United States', 'Europe', 'Canada')"
|
|
318
770
|
)
|
|
319
771
|
},
|
|
772
|
+
{ title: "List data centers", ...READ_ONLY },
|
|
320
773
|
async (params) => {
|
|
321
|
-
const
|
|
322
|
-
|
|
774
|
+
const backend = backendFor("dataCenters");
|
|
775
|
+
if (backend.version === "v2") {
|
|
776
|
+
const raw = await callRestUrl(`${backend.base}${backend.list}`);
|
|
777
|
+
let dcs = backend.unwrap(raw);
|
|
778
|
+
if (params.region) {
|
|
779
|
+
const t = params.region.toLowerCase();
|
|
780
|
+
dcs = dcs.filter(
|
|
781
|
+
(dc) => String(dc.region ?? "").toLowerCase().includes(t)
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
return capListResult(dcs, {
|
|
785
|
+
limit: params.limit,
|
|
786
|
+
cursor: params.cursor
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
const data = await graphql(`
|
|
323
790
|
query {
|
|
324
791
|
dataCenters {
|
|
325
792
|
id
|
|
@@ -327,9 +794,7 @@ function registerTools(server, ctx) {
|
|
|
327
794
|
location
|
|
328
795
|
}
|
|
329
796
|
}
|
|
330
|
-
|
|
331
|
-
tracking
|
|
332
|
-
);
|
|
797
|
+
`);
|
|
333
798
|
let dataCenters = data.dataCenters;
|
|
334
799
|
if (params.region) {
|
|
335
800
|
const term = params.region.toLowerCase();
|
|
@@ -337,50 +802,146 @@ function registerTools(server, ctx) {
|
|
|
337
802
|
(dc) => dc.location.toLowerCase().includes(term)
|
|
338
803
|
);
|
|
339
804
|
}
|
|
340
|
-
return {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
805
|
+
return capListResult(dataCenters, {
|
|
806
|
+
limit: params.limit,
|
|
807
|
+
cursor: params.cursor
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
);
|
|
811
|
+
server.tool(
|
|
812
|
+
"list-cpu-types",
|
|
813
|
+
"List available CPU flavor types for CPU pods/endpoints. v2-only \u2014 returns a 501 notice on the v1 API.",
|
|
814
|
+
{ ...listPaginationParams },
|
|
815
|
+
{ title: "List CPU types", ...READ_ONLY },
|
|
816
|
+
async (params) => {
|
|
817
|
+
const backend = backendFor("cpus");
|
|
818
|
+
if (backend.version === "v1") {
|
|
819
|
+
return jsonReply2({
|
|
820
|
+
error: "list-cpu-types is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
821
|
+
status: 501
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
const raw = await callRestUrl(`${backend.base}${backend.list}`);
|
|
825
|
+
return capListResult(backend.unwrap(raw), {
|
|
826
|
+
limit: params.limit,
|
|
827
|
+
cursor: params.cursor
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
);
|
|
831
|
+
server.tool(
|
|
832
|
+
"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).",
|
|
834
|
+
{
|
|
835
|
+
gpuTypeId: import_zod2.z.string().describe("ID of the GPU type to retrieve")
|
|
836
|
+
},
|
|
837
|
+
{ title: "Get GPU type", ...READ_ONLY },
|
|
838
|
+
async (params) => {
|
|
839
|
+
const backend = backendFor("gpus");
|
|
840
|
+
if (backend.version === "v1") {
|
|
841
|
+
return jsonReply2({
|
|
842
|
+
error: "get-gpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (or use list-gpu-types on v1).",
|
|
843
|
+
status: 501
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
const result = await callRestUrl(
|
|
847
|
+
`${backend.base}${backend.get(params.gpuTypeId)}`
|
|
848
|
+
);
|
|
849
|
+
return jsonReply2(result);
|
|
348
850
|
}
|
|
349
851
|
);
|
|
852
|
+
server.tool(
|
|
853
|
+
"get-cpu-type",
|
|
854
|
+
"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
|
+
{
|
|
856
|
+
cpuTypeId: import_zod2.z.string().describe("ID of the CPU type to retrieve")
|
|
857
|
+
},
|
|
858
|
+
{ title: "Get CPU type", ...READ_ONLY },
|
|
859
|
+
async (params) => {
|
|
860
|
+
const backend = backendFor("cpus");
|
|
861
|
+
if (backend.version === "v1") {
|
|
862
|
+
return jsonReply2({
|
|
863
|
+
error: "get-cpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
864
|
+
status: 501
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
const result = await callRestUrl(
|
|
868
|
+
`${backend.base}${backend.get(params.cpuTypeId)}`
|
|
869
|
+
);
|
|
870
|
+
return jsonReply2(result);
|
|
871
|
+
}
|
|
872
|
+
);
|
|
873
|
+
server.tool(
|
|
874
|
+
"get-data-center",
|
|
875
|
+
"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
|
+
{
|
|
877
|
+
dataCenterId: import_zod2.z.string().describe("ID of the data center to retrieve")
|
|
878
|
+
},
|
|
879
|
+
{ title: "Get data center", ...READ_ONLY },
|
|
880
|
+
async (params) => {
|
|
881
|
+
const backend = backendFor("dataCenters");
|
|
882
|
+
if (backend.version === "v1") {
|
|
883
|
+
return jsonReply2({
|
|
884
|
+
error: "get-data-center is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
885
|
+
status: 501
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
const result = await callRestUrl(
|
|
889
|
+
`${backend.base}${backend.get(params.dataCenterId)}`
|
|
890
|
+
);
|
|
891
|
+
return jsonReply2(result);
|
|
892
|
+
}
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// src/tools/pods.ts
|
|
897
|
+
var import_zod3 = require("zod");
|
|
898
|
+
function registerPodTools(server, rt) {
|
|
899
|
+
const { jsonReply: jsonReply2, callRestUrl, backendFor, podAction, env } = rt;
|
|
350
900
|
server.tool(
|
|
351
901
|
"list-pods",
|
|
902
|
+
"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.",
|
|
352
903
|
{
|
|
353
904
|
...listPaginationParams,
|
|
354
|
-
computeType:
|
|
355
|
-
gpuTypeId:
|
|
356
|
-
dataCenterId:
|
|
357
|
-
name:
|
|
358
|
-
includeMachine:
|
|
359
|
-
includeNetworkVolume:
|
|
905
|
+
computeType: import_zod3.z.enum(["GPU", "CPU"]).optional().describe("Filter to only GPU or only CPU Pods"),
|
|
906
|
+
gpuTypeId: import_zod3.z.array(import_zod3.z.string()).optional().describe("Filter to Pods with any of the listed GPU types"),
|
|
907
|
+
dataCenterId: import_zod3.z.array(import_zod3.z.string()).optional().describe("Filter to Pods in any of the provided data centers"),
|
|
908
|
+
name: import_zod3.z.string().optional().describe("Filter to Pods with the provided name"),
|
|
909
|
+
includeMachine: import_zod3.z.boolean().optional().describe("Include information about the machine"),
|
|
910
|
+
includeNetworkVolume: import_zod3.z.boolean().optional().describe("Include information about attached network volumes")
|
|
360
911
|
},
|
|
912
|
+
{ title: "List pods", ...READ_ONLY },
|
|
361
913
|
async (params) => {
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
params.
|
|
367
|
-
|
|
368
|
-
)
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
)
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
914
|
+
const backend = backendFor("pods");
|
|
915
|
+
let queryString = "";
|
|
916
|
+
if (backend.version === "v1") {
|
|
917
|
+
const queryParams = new URLSearchParams();
|
|
918
|
+
if (params.computeType)
|
|
919
|
+
queryParams.append("computeType", params.computeType);
|
|
920
|
+
if (params.gpuTypeId)
|
|
921
|
+
params.gpuTypeId.forEach(
|
|
922
|
+
(type) => queryParams.append("gpuTypeId", type)
|
|
923
|
+
);
|
|
924
|
+
if (params.dataCenterId)
|
|
925
|
+
params.dataCenterId.forEach(
|
|
926
|
+
(dc) => queryParams.append("dataCenterId", dc)
|
|
927
|
+
);
|
|
928
|
+
if (params.name) queryParams.append("name", params.name);
|
|
929
|
+
if (params.includeMachine)
|
|
930
|
+
queryParams.append(
|
|
931
|
+
"includeMachine",
|
|
932
|
+
params.includeMachine.toString()
|
|
933
|
+
);
|
|
934
|
+
if (params.includeNetworkVolume)
|
|
935
|
+
queryParams.append(
|
|
936
|
+
"includeNetworkVolume",
|
|
937
|
+
params.includeNetworkVolume.toString()
|
|
938
|
+
);
|
|
939
|
+
queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
|
|
940
|
+
}
|
|
941
|
+
const result = await callRestUrl(
|
|
942
|
+
`${backend.base}${backend.list}${queryString}`
|
|
943
|
+
);
|
|
944
|
+
return capListResult(backend.unwrap(result), {
|
|
384
945
|
limit: params.limit,
|
|
385
946
|
cursor: params.cursor
|
|
386
947
|
});
|
|
@@ -388,159 +949,252 @@ function registerTools(server, ctx) {
|
|
|
388
949
|
);
|
|
389
950
|
server.tool(
|
|
390
951
|
"get-pod",
|
|
952
|
+
"Get full details for one pod by id, optionally expanding machine and attached network-volume info.",
|
|
391
953
|
{
|
|
392
|
-
podId:
|
|
393
|
-
includeMachine:
|
|
394
|
-
includeNetworkVolume:
|
|
954
|
+
podId: import_zod3.z.string().describe("ID of the pod to retrieve"),
|
|
955
|
+
includeMachine: import_zod3.z.boolean().optional().describe("Include information about the machine"),
|
|
956
|
+
includeNetworkVolume: import_zod3.z.boolean().optional().describe("Include information about attached network volumes")
|
|
395
957
|
},
|
|
958
|
+
{ title: "Get pod", ...READ_ONLY },
|
|
396
959
|
async (params) => {
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
960
|
+
const backend = backendFor("pods");
|
|
961
|
+
let queryString = "";
|
|
962
|
+
if (backend.version === "v1") {
|
|
963
|
+
const queryParams = new URLSearchParams();
|
|
964
|
+
if (params.includeMachine)
|
|
965
|
+
queryParams.append(
|
|
966
|
+
"includeMachine",
|
|
967
|
+
params.includeMachine.toString()
|
|
968
|
+
);
|
|
969
|
+
if (params.includeNetworkVolume)
|
|
970
|
+
queryParams.append(
|
|
971
|
+
"includeNetworkVolume",
|
|
972
|
+
params.includeNetworkVolume.toString()
|
|
973
|
+
);
|
|
974
|
+
queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
|
|
975
|
+
}
|
|
976
|
+
const result = await callRestUrl(
|
|
977
|
+
`${backend.base}${backend.get(params.podId)}${queryString}`
|
|
978
|
+
);
|
|
979
|
+
return jsonReply2(result);
|
|
415
980
|
}
|
|
416
981
|
);
|
|
417
982
|
server.tool(
|
|
418
983
|
"create-pod",
|
|
419
|
-
'Create a new GPU/CPU pod on Runpod. If the user does not specify an image, 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.',
|
|
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 does not specify an image, 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.',
|
|
420
985
|
{
|
|
421
|
-
name:
|
|
422
|
-
imageName:
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
986
|
+
name: import_zod3.z.string().optional().describe("Name for the pod"),
|
|
987
|
+
imageName: import_zod3.z.string().describe("Docker image to use"),
|
|
988
|
+
computeType: import_zod3.z.enum(["GPU", "CPU"]).optional().describe(
|
|
989
|
+
"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
|
+
),
|
|
991
|
+
cloudType: import_zod3.z.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
|
|
992
|
+
gpuTypeIds: import_zod3.z.array(import_zod3.z.string()).optional().describe("List of acceptable GPU types"),
|
|
993
|
+
gpuCount: import_zod3.z.number().optional().describe("Number of GPUs"),
|
|
994
|
+
containerDiskInGb: import_zod3.z.number().optional().describe("Container disk size in GB"),
|
|
995
|
+
volumeInGb: import_zod3.z.number().optional().describe("Volume size in GB"),
|
|
996
|
+
volumeMountPath: import_zod3.z.string().optional().describe("Path to mount the volume"),
|
|
997
|
+
ports: import_zod3.z.array(import_zod3.z.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
|
|
998
|
+
env: import_zod3.z.record(import_zod3.z.string()).optional().describe("Environment variables"),
|
|
999
|
+
dataCenterIds: import_zod3.z.array(import_zod3.z.string()).optional().describe("List of data centers")
|
|
432
1000
|
},
|
|
1001
|
+
{ title: "Create pod", ...WRITE },
|
|
433
1002
|
async (params) => {
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
1003
|
+
const backend = backendFor("pods");
|
|
1004
|
+
const hasGpuTypes = (params.gpuTypeIds?.length ?? 0) > 0;
|
|
1005
|
+
const hasGpuCount = (params.gpuCount ?? 0) > 0;
|
|
1006
|
+
const wantsCpu = params.computeType === "CPU";
|
|
1007
|
+
const wantsGpu = hasGpuTypes || hasGpuCount || params.computeType === "GPU";
|
|
1008
|
+
if (backend.version === "v2") {
|
|
1009
|
+
if (wantsGpu && wantsCpu) {
|
|
1010
|
+
return jsonReply2({
|
|
1011
|
+
error: "Specify a GPU pod (gpuTypeIds or computeType:'GPU') OR a CPU pod (computeType:'CPU'), not both.",
|
|
1012
|
+
status: 400
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
if (!wantsGpu && !wantsCpu) {
|
|
1016
|
+
return jsonReply2({
|
|
1017
|
+
error: "No pod type specified. Pass gpuTypeIds (see list-gpu-types) for a GPU pod, or computeType:'CPU' for a CPU pod.",
|
|
1018
|
+
status: 400
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
if (wantsGpu && !hasGpuTypes) {
|
|
1022
|
+
return jsonReply2({
|
|
1023
|
+
error: "A GPU pod needs gpuTypeIds (see list-gpu-types); gpuCount or computeType alone is under-specified.",
|
|
1024
|
+
status: 400
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
if (wantsCpu) {
|
|
1028
|
+
try {
|
|
1029
|
+
const result = await callRestUrl(
|
|
1030
|
+
`${restV1Base(env)}/pods`,
|
|
1031
|
+
"POST",
|
|
1032
|
+
params
|
|
1033
|
+
);
|
|
1034
|
+
return jsonReply2({
|
|
1035
|
+
...result,
|
|
1036
|
+
_servedBy: "v1",
|
|
1037
|
+
_note: "CPU pod: created on the v1 API, because the v2 REST API does not support CPU pods yet."
|
|
1038
|
+
});
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
if (error instanceof HttpError) {
|
|
1041
|
+
return jsonReply2({
|
|
1042
|
+
error: error.message,
|
|
1043
|
+
status: error.status,
|
|
1044
|
+
_servedBy: "v1",
|
|
1045
|
+
_note: "CPU pod create was routed to the v1 API and failed."
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
throw error;
|
|
440
1049
|
}
|
|
441
|
-
|
|
442
|
-
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const body = backend.mapCreate(params);
|
|
1053
|
+
try {
|
|
1054
|
+
const result = await callRestUrl(
|
|
1055
|
+
`${backend.base}${backend.list}`,
|
|
1056
|
+
"POST",
|
|
1057
|
+
body
|
|
1058
|
+
);
|
|
1059
|
+
if (backend.version === "v2" && (params.gpuTypeIds?.length ?? 0) > 1) {
|
|
1060
|
+
return jsonReply2({
|
|
1061
|
+
...result,
|
|
1062
|
+
_warning: `v2 accepts one GPU type per pod; used "${params.gpuTypeIds[0]}" and ignored ${params.gpuTypeIds.length - 1} other(s): ${params.gpuTypeIds.slice(1).join(", ")}.`
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
return jsonReply2(result);
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
if (error instanceof HttpError && error.status === 501) {
|
|
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
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
throw error;
|
|
1074
|
+
}
|
|
443
1075
|
}
|
|
444
1076
|
);
|
|
445
1077
|
server.tool(
|
|
446
1078
|
"update-pod",
|
|
1079
|
+
"Update a pod's mutable fields (name, image, container disk, volume, ports, env). Only the fields you provide change.",
|
|
447
1080
|
{
|
|
448
|
-
podId:
|
|
449
|
-
name:
|
|
450
|
-
imageName:
|
|
451
|
-
containerDiskInGb:
|
|
452
|
-
volumeInGb:
|
|
453
|
-
volumeMountPath:
|
|
454
|
-
ports:
|
|
455
|
-
env:
|
|
1081
|
+
podId: import_zod3.z.string().describe("ID of the pod to update"),
|
|
1082
|
+
name: import_zod3.z.string().optional().describe("New name for the pod"),
|
|
1083
|
+
imageName: import_zod3.z.string().optional().describe("New Docker image"),
|
|
1084
|
+
containerDiskInGb: import_zod3.z.number().optional().describe("New container disk size in GB"),
|
|
1085
|
+
volumeInGb: import_zod3.z.number().optional().describe("New volume size in GB"),
|
|
1086
|
+
volumeMountPath: import_zod3.z.string().optional().describe("New path to mount the volume"),
|
|
1087
|
+
ports: import_zod3.z.array(import_zod3.z.string()).optional().describe("New ports to expose"),
|
|
1088
|
+
env: import_zod3.z.record(import_zod3.z.string()).optional().describe("New environment variables")
|
|
456
1089
|
},
|
|
1090
|
+
{ title: "Update pod", ...WRITE, idempotentHint: true },
|
|
457
1091
|
async (params) => {
|
|
458
1092
|
const { podId, ...updateParams } = params;
|
|
459
|
-
const
|
|
460
|
-
|
|
1093
|
+
const backend = backendFor("pods");
|
|
1094
|
+
const body = backend.mapUpdate(updateParams);
|
|
1095
|
+
const result = await callRestUrl(
|
|
1096
|
+
`${backend.base}${backend.get(podId)}`,
|
|
461
1097
|
"PATCH",
|
|
462
|
-
|
|
1098
|
+
body
|
|
463
1099
|
);
|
|
464
|
-
return
|
|
465
|
-
content: [
|
|
466
|
-
{
|
|
467
|
-
type: "text",
|
|
468
|
-
text: JSON.stringify(result, null, 2)
|
|
469
|
-
}
|
|
470
|
-
]
|
|
471
|
-
};
|
|
1100
|
+
return jsonReply2(result);
|
|
472
1101
|
}
|
|
473
1102
|
);
|
|
474
1103
|
server.tool(
|
|
475
1104
|
"start-pod",
|
|
1105
|
+
"Start a stopped pod (resumes the same pod and its billing).",
|
|
476
1106
|
{
|
|
477
|
-
podId:
|
|
1107
|
+
podId: import_zod3.z.string().describe("ID of the pod to start")
|
|
478
1108
|
},
|
|
1109
|
+
{ title: "Start pod", ...WRITE, idempotentHint: true },
|
|
479
1110
|
async (params) => {
|
|
480
|
-
const result = await
|
|
481
|
-
return
|
|
482
|
-
content: [
|
|
483
|
-
{
|
|
484
|
-
type: "text",
|
|
485
|
-
text: JSON.stringify(result, null, 2)
|
|
486
|
-
}
|
|
487
|
-
]
|
|
488
|
-
};
|
|
1111
|
+
const result = await podAction(params.podId, "start");
|
|
1112
|
+
return jsonReply2(result);
|
|
489
1113
|
}
|
|
490
1114
|
);
|
|
491
1115
|
server.tool(
|
|
492
1116
|
"stop-pod",
|
|
1117
|
+
"Stop a running pod. The pod and its disk persist; GPU billing stops.",
|
|
493
1118
|
{
|
|
494
|
-
podId:
|
|
1119
|
+
podId: import_zod3.z.string().describe("ID of the pod to stop")
|
|
495
1120
|
},
|
|
1121
|
+
{ title: "Stop pod", ...WRITE, idempotentHint: true },
|
|
496
1122
|
async (params) => {
|
|
497
|
-
const result = await
|
|
498
|
-
return
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
1123
|
+
const result = await podAction(params.podId, "stop");
|
|
1124
|
+
return jsonReply2(result);
|
|
1125
|
+
}
|
|
1126
|
+
);
|
|
1127
|
+
server.tool(
|
|
1128
|
+
"restart-pod",
|
|
1129
|
+
"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
|
+
{
|
|
1131
|
+
podId: import_zod3.z.string().describe("ID of the pod to restart")
|
|
1132
|
+
},
|
|
1133
|
+
{ title: "Restart pod", ...WRITE },
|
|
1134
|
+
async (params) => {
|
|
1135
|
+
const backend = backendFor("pods");
|
|
1136
|
+
if (backend.version === "v1") {
|
|
1137
|
+
return jsonReply2({
|
|
1138
|
+
error: "restart-pod is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (or stop then start the pod on v1).",
|
|
1139
|
+
status: 501
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
const result = await podAction(params.podId, "restart");
|
|
1143
|
+
return jsonReply2(result);
|
|
506
1144
|
}
|
|
507
1145
|
);
|
|
508
1146
|
server.tool(
|
|
509
1147
|
"delete-pod",
|
|
1148
|
+
"Permanently delete a pod and its data. This cannot be undone.",
|
|
510
1149
|
{
|
|
511
|
-
podId:
|
|
1150
|
+
podId: import_zod3.z.string().describe("ID of the pod to delete")
|
|
512
1151
|
},
|
|
1152
|
+
{ title: "Delete pod", ...DESTRUCTIVE },
|
|
513
1153
|
async (params) => {
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
521
|
-
]
|
|
522
|
-
};
|
|
1154
|
+
const backend = backendFor("pods");
|
|
1155
|
+
const result = await callRestUrl(
|
|
1156
|
+
`${backend.base}${backend.get(params.podId)}`,
|
|
1157
|
+
"DELETE"
|
|
1158
|
+
);
|
|
1159
|
+
return jsonReply2(result);
|
|
523
1160
|
}
|
|
524
1161
|
);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// src/tools/endpoints.ts
|
|
1165
|
+
var import_zod4 = require("zod");
|
|
1166
|
+
function registerEndpointTools(server, rt) {
|
|
1167
|
+
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
525
1168
|
server.tool(
|
|
526
1169
|
"list-endpoints",
|
|
1170
|
+
"List your Serverless endpoints, optionally expanding template and worker details (v1 only). Paginated via limit/cursor.",
|
|
527
1171
|
{
|
|
528
1172
|
...listPaginationParams,
|
|
529
|
-
includeTemplate:
|
|
530
|
-
includeWorkers:
|
|
1173
|
+
includeTemplate: import_zod4.z.boolean().optional().describe("Include template information (v1 only)"),
|
|
1174
|
+
includeWorkers: import_zod4.z.boolean().optional().describe("Include information about workers (v1 only)")
|
|
531
1175
|
},
|
|
1176
|
+
{ title: "List endpoints", ...READ_ONLY },
|
|
532
1177
|
async (params) => {
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
1178
|
+
const backend = backendFor("endpoints");
|
|
1179
|
+
let queryString = "";
|
|
1180
|
+
if (backend.version === "v1") {
|
|
1181
|
+
const queryParams = new URLSearchParams();
|
|
1182
|
+
if (params.includeTemplate)
|
|
1183
|
+
queryParams.append(
|
|
1184
|
+
"includeTemplate",
|
|
1185
|
+
params.includeTemplate.toString()
|
|
1186
|
+
);
|
|
1187
|
+
if (params.includeWorkers)
|
|
1188
|
+
queryParams.append(
|
|
1189
|
+
"includeWorkers",
|
|
1190
|
+
params.includeWorkers.toString()
|
|
1191
|
+
);
|
|
1192
|
+
queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
|
|
1193
|
+
}
|
|
1194
|
+
const result = await callRestUrl(
|
|
1195
|
+
`${backend.base}${backend.list}${queryString}`
|
|
1196
|
+
);
|
|
1197
|
+
return capListResult(backend.unwrap(result), {
|
|
544
1198
|
limit: params.limit,
|
|
545
1199
|
cursor: params.cursor
|
|
546
1200
|
});
|
|
@@ -548,124 +1202,227 @@ function registerTools(server, ctx) {
|
|
|
548
1202
|
);
|
|
549
1203
|
server.tool(
|
|
550
1204
|
"get-endpoint",
|
|
1205
|
+
"Get one Serverless endpoint by id, optionally expanding template and worker details (v1 only).",
|
|
551
1206
|
{
|
|
552
|
-
endpointId:
|
|
553
|
-
includeTemplate:
|
|
554
|
-
includeWorkers:
|
|
1207
|
+
endpointId: import_zod4.z.string().describe("ID of the endpoint to retrieve"),
|
|
1208
|
+
includeTemplate: import_zod4.z.boolean().optional().describe("Include template information (v1 only)"),
|
|
1209
|
+
includeWorkers: import_zod4.z.boolean().optional().describe("Include information about workers (v1 only)")
|
|
555
1210
|
},
|
|
1211
|
+
{ title: "Get endpoint", ...READ_ONLY },
|
|
556
1212
|
async (params) => {
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
1213
|
+
const backend = backendFor("endpoints");
|
|
1214
|
+
let queryString = "";
|
|
1215
|
+
if (backend.version === "v1") {
|
|
1216
|
+
const queryParams = new URLSearchParams();
|
|
1217
|
+
if (params.includeTemplate)
|
|
1218
|
+
queryParams.append(
|
|
1219
|
+
"includeTemplate",
|
|
1220
|
+
params.includeTemplate.toString()
|
|
1221
|
+
);
|
|
1222
|
+
if (params.includeWorkers)
|
|
1223
|
+
queryParams.append(
|
|
1224
|
+
"includeWorkers",
|
|
1225
|
+
params.includeWorkers.toString()
|
|
1226
|
+
);
|
|
1227
|
+
queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
|
|
1228
|
+
}
|
|
1229
|
+
const result = await callRestUrl(
|
|
1230
|
+
`${backend.base}${backend.get(params.endpointId)}${queryString}`
|
|
568
1231
|
);
|
|
569
|
-
return
|
|
570
|
-
content: [
|
|
571
|
-
{
|
|
572
|
-
type: "text",
|
|
573
|
-
text: JSON.stringify(result, null, 2)
|
|
574
|
-
}
|
|
575
|
-
]
|
|
576
|
-
};
|
|
1232
|
+
return jsonReply2(result);
|
|
577
1233
|
}
|
|
578
1234
|
);
|
|
579
1235
|
server.tool(
|
|
580
1236
|
"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).",
|
|
581
1238
|
{
|
|
582
|
-
name:
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
1239
|
+
name: import_zod4.z.string().optional().describe("Name for the endpoint"),
|
|
1240
|
+
// --- v2 inline-config fields ---
|
|
1241
|
+
imageName: import_zod4.z.string().optional().describe("Docker image (v2). Required on v2 instead of a templateId."),
|
|
1242
|
+
gpuPoolIds: import_zod4.z.array(import_zod4.z.string()).optional().describe(
|
|
1243
|
+
'GPU pool names (v2, required). The `pool` field from list-gpu-types, e.g. ["AMPERE_80"]. NOT GPU type ids.'
|
|
1244
|
+
),
|
|
1245
|
+
gpuCount: import_zod4.z.number().optional().describe("GPUs per worker (v2)"),
|
|
1246
|
+
args: import_zod4.z.string().optional().describe("Container start command/args (v2)"),
|
|
1247
|
+
containerDiskInGb: import_zod4.z.number().optional().describe("Container disk size in GB (v2)"),
|
|
1248
|
+
ports: import_zod4.z.array(import_zod4.z.string()).optional().describe("Ports to expose (v2), e.g. ['8000/http']"),
|
|
1249
|
+
env: import_zod4.z.record(import_zod4.z.string()).optional().describe("Environment variables (v2)"),
|
|
1250
|
+
containerRegistryAuthId: import_zod4.z.string().optional().describe("Container registry auth id for a private image (v2)"),
|
|
1251
|
+
networkVolumeIds: import_zod4.z.array(import_zod4.z.string()).optional().describe("Network volume ids to attach (v2)"),
|
|
1252
|
+
executionTimeoutMs: import_zod4.z.number().optional().describe("Per-job execution timeout in ms (v2)"),
|
|
1253
|
+
flashboot: import_zod4.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (v2)"),
|
|
1254
|
+
// --- v1 template-based fields ---
|
|
1255
|
+
templateId: import_zod4.z.string().optional().describe("Template ID (v1). Required on v1."),
|
|
1256
|
+
computeType: import_zod4.z.enum(["GPU", "CPU"]).optional().describe("GPU or CPU endpoint (v1)"),
|
|
1257
|
+
gpuTypeIds: import_zod4.z.array(import_zod4.z.string()).optional().describe("List of acceptable GPU types (v1)"),
|
|
1258
|
+
// --- shared autoscaling fields ---
|
|
1259
|
+
workersMin: import_zod4.z.number().optional().describe("Minimum number of workers"),
|
|
1260
|
+
workersMax: import_zod4.z.number().optional().describe("Maximum number of workers"),
|
|
1261
|
+
scalerType: import_zod4.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type"),
|
|
1262
|
+
scalerValue: import_zod4.z.number().optional().describe("Autoscaler target value"),
|
|
1263
|
+
idleTimeout: import_zod4.z.number().optional().describe("Idle timeout in seconds before scaling a worker down"),
|
|
1264
|
+
dataCenterIds: import_zod4.z.array(import_zod4.z.string()).optional().describe("List of preferred data centers")
|
|
590
1265
|
},
|
|
1266
|
+
{ title: "Create endpoint", ...WRITE },
|
|
591
1267
|
async (params) => {
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
{
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
|
|
1268
|
+
const backend = backendFor("endpoints");
|
|
1269
|
+
if (backend.version === "v2") {
|
|
1270
|
+
if (!params.name) {
|
|
1271
|
+
return jsonReply2({
|
|
1272
|
+
error: "On the v2 REST API, create-endpoint requires a name.",
|
|
1273
|
+
status: 400
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
if (!params.imageName) {
|
|
1277
|
+
return jsonReply2({
|
|
1278
|
+
error: "On the v2 REST API, create-endpoint needs an imageName (v2 endpoints are image-based, not template-based). To use a templateId, set RUNPOD_REST_VERSION=v1.",
|
|
1279
|
+
status: 400
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
if (!params.gpuPoolIds?.length) {
|
|
1283
|
+
return jsonReply2({
|
|
1284
|
+
error: 'create-endpoint needs gpuPoolIds on v2 (GPU pool names from list-gpu-types \u2014 the `pool` field, e.g. ["AMPERE_80"]).',
|
|
1285
|
+
status: 400
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
const body = backend.mapCreate(params);
|
|
1289
|
+
const result2 = await callRestUrl(
|
|
1290
|
+
`${backend.base}${backend.list}`,
|
|
1291
|
+
"POST",
|
|
1292
|
+
body
|
|
1293
|
+
);
|
|
1294
|
+
return jsonReply2(result2);
|
|
1295
|
+
}
|
|
1296
|
+
if (!params.templateId) {
|
|
1297
|
+
return jsonReply2({
|
|
1298
|
+
error: "On the v1 REST API, create-endpoint needs a templateId. (On v2 \u2014 the default \u2014 pass imageName + gpuPoolIds instead.)",
|
|
1299
|
+
status: 400
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
const v1Body = {};
|
|
1303
|
+
for (const k of [
|
|
1304
|
+
"templateId",
|
|
1305
|
+
"name",
|
|
1306
|
+
"computeType",
|
|
1307
|
+
"gpuTypeIds",
|
|
1308
|
+
"gpuCount",
|
|
1309
|
+
"workersMin",
|
|
1310
|
+
"workersMax",
|
|
1311
|
+
"dataCenterIds"
|
|
1312
|
+
]) {
|
|
1313
|
+
if (params[k] !== void 0) v1Body[k] = params[k];
|
|
1314
|
+
}
|
|
1315
|
+
const result = await callRestUrl(
|
|
1316
|
+
`${backend.base}${backend.list}`,
|
|
1317
|
+
"POST",
|
|
1318
|
+
v1Body
|
|
1319
|
+
);
|
|
1320
|
+
return jsonReply2(result);
|
|
601
1321
|
}
|
|
602
1322
|
);
|
|
603
1323
|
server.tool(
|
|
604
1324
|
"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.",
|
|
605
1326
|
{
|
|
606
|
-
endpointId:
|
|
607
|
-
name:
|
|
608
|
-
workersMin:
|
|
609
|
-
workersMax:
|
|
610
|
-
idleTimeout:
|
|
611
|
-
scalerType:
|
|
612
|
-
scalerValue:
|
|
1327
|
+
endpointId: import_zod4.z.string().describe("ID of the endpoint to update"),
|
|
1328
|
+
name: import_zod4.z.string().optional().describe("New name for the endpoint"),
|
|
1329
|
+
workersMin: import_zod4.z.number().optional().describe("New minimum number of workers"),
|
|
1330
|
+
workersMax: import_zod4.z.number().optional().describe("New maximum number of workers"),
|
|
1331
|
+
idleTimeout: import_zod4.z.number().optional().describe("New idle timeout in seconds"),
|
|
1332
|
+
scalerType: import_zod4.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Scaler type"),
|
|
1333
|
+
scalerValue: import_zod4.z.number().optional().describe("Scaler value"),
|
|
1334
|
+
// --- v2-only inline-config fields ---
|
|
1335
|
+
imageName: import_zod4.z.string().optional().describe("New Docker image (v2)"),
|
|
1336
|
+
gpuPoolIds: import_zod4.z.array(import_zod4.z.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
|
|
1337
|
+
gpuCount: import_zod4.z.number().optional().describe("New GPUs per worker (v2)"),
|
|
1338
|
+
args: import_zod4.z.string().optional().describe("New container args (v2)"),
|
|
1339
|
+
containerDiskInGb: import_zod4.z.number().optional().describe("New container disk size in GB (v2)"),
|
|
1340
|
+
ports: import_zod4.z.array(import_zod4.z.string()).optional().describe("New ports (v2)"),
|
|
1341
|
+
env: import_zod4.z.record(import_zod4.z.string()).optional().describe("New environment variables (v2)"),
|
|
1342
|
+
containerRegistryAuthId: import_zod4.z.string().optional().describe("New container registry auth id (v2)"),
|
|
1343
|
+
networkVolumeIds: import_zod4.z.array(import_zod4.z.string()).optional().describe("New network volume ids (v2)"),
|
|
1344
|
+
executionTimeoutMs: import_zod4.z.number().optional().describe("New per-job execution timeout in ms (v2)"),
|
|
1345
|
+
flashboot: import_zod4.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("New FlashBoot mode (v2)")
|
|
613
1346
|
},
|
|
1347
|
+
{ title: "Update endpoint", ...WRITE, idempotentHint: true },
|
|
614
1348
|
async (params) => {
|
|
615
1349
|
const { endpointId, ...updateParams } = params;
|
|
616
|
-
const
|
|
617
|
-
|
|
1350
|
+
const backend = backendFor("endpoints");
|
|
1351
|
+
const body = backend.mapUpdate(updateParams);
|
|
1352
|
+
const result = await callRestUrl(
|
|
1353
|
+
`${backend.base}${backend.get(endpointId)}`,
|
|
618
1354
|
"PATCH",
|
|
619
|
-
|
|
1355
|
+
body
|
|
620
1356
|
);
|
|
621
|
-
return
|
|
622
|
-
content: [
|
|
623
|
-
{
|
|
624
|
-
type: "text",
|
|
625
|
-
text: JSON.stringify(result, null, 2)
|
|
626
|
-
}
|
|
627
|
-
]
|
|
628
|
-
};
|
|
1357
|
+
return jsonReply2(result);
|
|
629
1358
|
}
|
|
630
1359
|
);
|
|
631
1360
|
server.tool(
|
|
632
1361
|
"delete-endpoint",
|
|
1362
|
+
"Permanently delete a Serverless endpoint. This cannot be undone.",
|
|
633
1363
|
{
|
|
634
|
-
endpointId:
|
|
1364
|
+
endpointId: import_zod4.z.string().describe("ID of the endpoint to delete")
|
|
635
1365
|
},
|
|
1366
|
+
{ title: "Delete endpoint", ...DESTRUCTIVE },
|
|
636
1367
|
async (params) => {
|
|
637
|
-
const
|
|
638
|
-
|
|
1368
|
+
const backend = backendFor("endpoints");
|
|
1369
|
+
const result = await callRestUrl(
|
|
1370
|
+
`${backend.base}${backend.get(params.endpointId)}`,
|
|
639
1371
|
"DELETE"
|
|
640
1372
|
);
|
|
641
|
-
return
|
|
642
|
-
content: [
|
|
643
|
-
{
|
|
644
|
-
type: "text",
|
|
645
|
-
text: JSON.stringify(result, null, 2)
|
|
646
|
-
}
|
|
647
|
-
]
|
|
648
|
-
};
|
|
1373
|
+
return jsonReply2(result);
|
|
649
1374
|
}
|
|
650
1375
|
);
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
1376
|
+
server.tool(
|
|
1377
|
+
"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.",
|
|
1379
|
+
{
|
|
1380
|
+
...listPaginationParams,
|
|
1381
|
+
endpointId: import_zod4.z.string().describe("ID of the Serverless endpoint whose workers to list")
|
|
1382
|
+
},
|
|
1383
|
+
{ title: "List endpoint workers", ...READ_ONLY },
|
|
1384
|
+
async (params) => {
|
|
1385
|
+
const backend = backendFor("workers");
|
|
1386
|
+
if (backend.version === "v1") {
|
|
1387
|
+
return jsonReply2({
|
|
1388
|
+
error: "list-endpoint-workers is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (on v1, use get-endpoint with includeWorkers).",
|
|
1389
|
+
status: 501
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
const raw = await callRestUrl(
|
|
1393
|
+
`${backend.base}/serverless/${params.endpointId}/workers`
|
|
1394
|
+
);
|
|
1395
|
+
return capListResult(
|
|
1396
|
+
backend.unwrap(raw),
|
|
1397
|
+
{ limit: params.limit, cursor: params.cursor },
|
|
1398
|
+
{ summary: raw?.summary, endpointVersion: raw?.endpointVersion }
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// src/tools/jobs.ts
|
|
1405
|
+
var import_zod5 = require("zod");
|
|
1406
|
+
function registerJobTools(server, rt) {
|
|
1407
|
+
const { jsonReply: jsonReply2, serverlessRequest } = rt;
|
|
1408
|
+
const endpointIdSchema = import_zod5.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
|
|
1409
|
+
const jobIdSchema = import_zod5.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
|
|
1410
|
+
const inputSchema = import_zod5.z.record(import_zod5.z.unknown()).describe(
|
|
654
1411
|
"Input payload for the worker handler. The expected fields depend on the deployed model or worker."
|
|
655
1412
|
);
|
|
656
|
-
const webhookSchema =
|
|
1413
|
+
const webhookSchema = import_zod5.z.string().url().optional().describe(
|
|
657
1414
|
"Webhook URL to receive job completion notifications instead of polling"
|
|
658
1415
|
);
|
|
659
|
-
const policySchema =
|
|
660
|
-
executionTimeout:
|
|
661
|
-
lowPriority:
|
|
662
|
-
ttl:
|
|
1416
|
+
const policySchema = import_zod5.z.object({
|
|
1417
|
+
executionTimeout: import_zod5.z.number().optional().describe("Maximum execution time in milliseconds"),
|
|
1418
|
+
lowPriority: import_zod5.z.boolean().optional().describe("Submit as a low-priority job"),
|
|
1419
|
+
ttl: import_zod5.z.number().optional().describe("Time-to-live for the job result in milliseconds")
|
|
663
1420
|
}).optional().describe("Execution policy options");
|
|
664
|
-
const s3ConfigSchema =
|
|
665
|
-
accessId:
|
|
666
|
-
accessSecret:
|
|
667
|
-
bucketName:
|
|
668
|
-
endpointUrl:
|
|
1421
|
+
const s3ConfigSchema = import_zod5.z.object({
|
|
1422
|
+
accessId: import_zod5.z.string().describe("S3 access key ID"),
|
|
1423
|
+
accessSecret: import_zod5.z.string().describe("S3 secret access key"),
|
|
1424
|
+
bucketName: import_zod5.z.string().describe("S3 bucket name"),
|
|
1425
|
+
endpointUrl: import_zod5.z.string().describe("S3 endpoint URL")
|
|
669
1426
|
}).optional().describe("S3-compatible storage config for large outputs");
|
|
670
1427
|
server.tool(
|
|
671
1428
|
"run-endpoint",
|
|
@@ -679,6 +1436,7 @@ function registerTools(server, ctx) {
|
|
|
679
1436
|
policy: policySchema,
|
|
680
1437
|
s3Config: s3ConfigSchema
|
|
681
1438
|
},
|
|
1439
|
+
{ title: "Run endpoint (async)", ...WRITE },
|
|
682
1440
|
async (params) => {
|
|
683
1441
|
const { endpointId, ...body } = params;
|
|
684
1442
|
const result = await serverlessRequest(
|
|
@@ -687,14 +1445,7 @@ function registerTools(server, ctx) {
|
|
|
687
1445
|
"POST",
|
|
688
1446
|
body
|
|
689
1447
|
);
|
|
690
|
-
return
|
|
691
|
-
content: [
|
|
692
|
-
{
|
|
693
|
-
type: "text",
|
|
694
|
-
text: JSON.stringify(result, null, 2)
|
|
695
|
-
}
|
|
696
|
-
]
|
|
697
|
-
};
|
|
1448
|
+
return jsonReply2(result);
|
|
698
1449
|
}
|
|
699
1450
|
);
|
|
700
1451
|
server.tool(
|
|
@@ -705,13 +1456,14 @@ function registerTools(server, ctx) {
|
|
|
705
1456
|
"ID of the Serverless endpoint to run synchronously"
|
|
706
1457
|
),
|
|
707
1458
|
input: inputSchema,
|
|
708
|
-
wait:
|
|
1459
|
+
wait: import_zod5.z.number().min(1e3).max(3e5).optional().describe(
|
|
709
1460
|
"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)."
|
|
710
1461
|
),
|
|
711
1462
|
webhook: webhookSchema,
|
|
712
1463
|
policy: policySchema,
|
|
713
1464
|
s3Config: s3ConfigSchema
|
|
714
1465
|
},
|
|
1466
|
+
{ title: "Run endpoint (sync)", ...WRITE },
|
|
715
1467
|
async (params) => {
|
|
716
1468
|
const { endpointId, wait, ...body } = params;
|
|
717
1469
|
const path = wait ? `/runsync?wait=${wait}` : "/runsync";
|
|
@@ -721,14 +1473,7 @@ function registerTools(server, ctx) {
|
|
|
721
1473
|
"POST",
|
|
722
1474
|
body
|
|
723
1475
|
);
|
|
724
|
-
return
|
|
725
|
-
content: [
|
|
726
|
-
{
|
|
727
|
-
type: "text",
|
|
728
|
-
text: JSON.stringify(result, null, 2)
|
|
729
|
-
}
|
|
730
|
-
]
|
|
731
|
-
};
|
|
1476
|
+
return jsonReply2(result);
|
|
732
1477
|
}
|
|
733
1478
|
);
|
|
734
1479
|
server.tool(
|
|
@@ -740,19 +1485,13 @@ function registerTools(server, ctx) {
|
|
|
740
1485
|
),
|
|
741
1486
|
jobId: jobIdSchema.describe("ID of the job to check")
|
|
742
1487
|
},
|
|
1488
|
+
{ title: "Get job status", ...READ_ONLY },
|
|
743
1489
|
async (params) => {
|
|
744
1490
|
const result = await serverlessRequest(
|
|
745
1491
|
params.endpointId,
|
|
746
1492
|
`/status/${params.jobId}`
|
|
747
1493
|
);
|
|
748
|
-
return
|
|
749
|
-
content: [
|
|
750
|
-
{
|
|
751
|
-
type: "text",
|
|
752
|
-
text: JSON.stringify(result, null, 2)
|
|
753
|
-
}
|
|
754
|
-
]
|
|
755
|
-
};
|
|
1494
|
+
return jsonReply2(result);
|
|
756
1495
|
}
|
|
757
1496
|
);
|
|
758
1497
|
server.tool(
|
|
@@ -764,6 +1503,7 @@ function registerTools(server, ctx) {
|
|
|
764
1503
|
),
|
|
765
1504
|
jobId: jobIdSchema.describe("ID of the job to stream results from")
|
|
766
1505
|
},
|
|
1506
|
+
{ title: "Stream job", ...READ_ONLY },
|
|
767
1507
|
async (params) => {
|
|
768
1508
|
const TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
769
1509
|
"COMPLETED",
|
|
@@ -809,18 +1549,7 @@ function registerTools(server, ctx) {
|
|
|
809
1549
|
}
|
|
810
1550
|
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
811
1551
|
}
|
|
812
|
-
return {
|
|
813
|
-
content: [
|
|
814
|
-
{
|
|
815
|
-
type: "text",
|
|
816
|
-
text: JSON.stringify(
|
|
817
|
-
{ ...finalResult, stream: allChunks },
|
|
818
|
-
null,
|
|
819
|
-
2
|
|
820
|
-
)
|
|
821
|
-
}
|
|
822
|
-
]
|
|
823
|
-
};
|
|
1552
|
+
return jsonReply2({ ...finalResult, stream: allChunks });
|
|
824
1553
|
}
|
|
825
1554
|
);
|
|
826
1555
|
server.tool(
|
|
@@ -832,20 +1561,14 @@ function registerTools(server, ctx) {
|
|
|
832
1561
|
),
|
|
833
1562
|
jobId: jobIdSchema.describe("ID of the job to cancel")
|
|
834
1563
|
},
|
|
1564
|
+
{ title: "Cancel job", ...WRITE, idempotentHint: true },
|
|
835
1565
|
async (params) => {
|
|
836
1566
|
const result = await serverlessRequest(
|
|
837
1567
|
params.endpointId,
|
|
838
1568
|
`/cancel/${params.jobId}`,
|
|
839
1569
|
"POST"
|
|
840
1570
|
);
|
|
841
|
-
return
|
|
842
|
-
content: [
|
|
843
|
-
{
|
|
844
|
-
type: "text",
|
|
845
|
-
text: JSON.stringify(result, null, 2)
|
|
846
|
-
}
|
|
847
|
-
]
|
|
848
|
-
};
|
|
1571
|
+
return jsonReply2(result);
|
|
849
1572
|
}
|
|
850
1573
|
);
|
|
851
1574
|
server.tool(
|
|
@@ -857,20 +1580,14 @@ function registerTools(server, ctx) {
|
|
|
857
1580
|
),
|
|
858
1581
|
jobId: jobIdSchema.describe("ID of the job to retry")
|
|
859
1582
|
},
|
|
1583
|
+
{ title: "Retry job", ...WRITE },
|
|
860
1584
|
async (params) => {
|
|
861
1585
|
const result = await serverlessRequest(
|
|
862
1586
|
params.endpointId,
|
|
863
1587
|
`/retry/${params.jobId}`,
|
|
864
1588
|
"POST"
|
|
865
1589
|
);
|
|
866
|
-
return
|
|
867
|
-
content: [
|
|
868
|
-
{
|
|
869
|
-
type: "text",
|
|
870
|
-
text: JSON.stringify(result, null, 2)
|
|
871
|
-
}
|
|
872
|
-
]
|
|
873
|
-
};
|
|
1590
|
+
return jsonReply2(result);
|
|
874
1591
|
}
|
|
875
1592
|
);
|
|
876
1593
|
server.tool(
|
|
@@ -881,16 +1598,10 @@ function registerTools(server, ctx) {
|
|
|
881
1598
|
"ID of the Serverless endpoint to check health for"
|
|
882
1599
|
)
|
|
883
1600
|
},
|
|
1601
|
+
{ title: "Endpoint health", ...READ_ONLY },
|
|
884
1602
|
async (params) => {
|
|
885
1603
|
const result = await serverlessRequest(params.endpointId, "/health");
|
|
886
|
-
return
|
|
887
|
-
content: [
|
|
888
|
-
{
|
|
889
|
-
type: "text",
|
|
890
|
-
text: JSON.stringify(result, null, 2)
|
|
891
|
-
}
|
|
892
|
-
]
|
|
893
|
-
};
|
|
1604
|
+
return jsonReply2(result);
|
|
894
1605
|
}
|
|
895
1606
|
);
|
|
896
1607
|
server.tool(
|
|
@@ -901,46 +1612,51 @@ function registerTools(server, ctx) {
|
|
|
901
1612
|
"ID of the Serverless endpoint to purge the queue for"
|
|
902
1613
|
)
|
|
903
1614
|
},
|
|
1615
|
+
{ title: "Purge endpoint queue", ...WRITE, idempotentHint: true },
|
|
904
1616
|
async (params) => {
|
|
905
1617
|
const result = await serverlessRequest(
|
|
906
1618
|
params.endpointId,
|
|
907
1619
|
"/purge-queue",
|
|
908
1620
|
"POST"
|
|
909
1621
|
);
|
|
910
|
-
return
|
|
911
|
-
content: [
|
|
912
|
-
{
|
|
913
|
-
type: "text",
|
|
914
|
-
text: JSON.stringify(result, null, 2)
|
|
915
|
-
}
|
|
916
|
-
]
|
|
917
|
-
};
|
|
1622
|
+
return jsonReply2(result);
|
|
918
1623
|
}
|
|
919
1624
|
);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// src/tools/templates.ts
|
|
1628
|
+
var import_zod6 = require("zod");
|
|
1629
|
+
function registerTemplateTools(server, rt) {
|
|
1630
|
+
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
920
1631
|
server.tool(
|
|
921
1632
|
"list-templates",
|
|
922
1633
|
`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.`,
|
|
923
1634
|
{
|
|
924
1635
|
...listPaginationParams,
|
|
925
|
-
includeRunpodTemplates:
|
|
926
|
-
includePublicTemplates:
|
|
927
|
-
includeEndpointBoundTemplates:
|
|
1636
|
+
includeRunpodTemplates: import_zod6.z.boolean().optional().describe("Include official Runpod templates in the response"),
|
|
1637
|
+
includePublicTemplates: import_zod6.z.boolean().optional().describe("Include community-made public templates in the response"),
|
|
1638
|
+
includeEndpointBoundTemplates: import_zod6.z.boolean().optional().describe(
|
|
928
1639
|
"Include templates bound to Serverless endpoints in the response"
|
|
929
1640
|
)
|
|
930
1641
|
},
|
|
1642
|
+
{ title: "List templates", ...READ_ONLY },
|
|
931
1643
|
async (params) => {
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1644
|
+
const backend = backendFor("templates");
|
|
1645
|
+
let query = "";
|
|
1646
|
+
if (backend.version === "v1") {
|
|
1647
|
+
const queryParams = new URLSearchParams();
|
|
1648
|
+
if (params.includeRunpodTemplates)
|
|
1649
|
+
queryParams.set("includeRunpodTemplates", "true");
|
|
1650
|
+
if (params.includePublicTemplates)
|
|
1651
|
+
queryParams.set("includePublicTemplates", "true");
|
|
1652
|
+
if (params.includeEndpointBoundTemplates)
|
|
1653
|
+
queryParams.set("includeEndpointBoundTemplates", "true");
|
|
1654
|
+
query = queryParams.toString() ? `?${queryParams.toString()}` : "";
|
|
1655
|
+
}
|
|
1656
|
+
const result = await callRestUrl(
|
|
1657
|
+
`${backend.base}${backend.list}${query}`
|
|
942
1658
|
);
|
|
943
|
-
return capListResult(result, {
|
|
1659
|
+
return capListResult(backend.unwrap(result), {
|
|
944
1660
|
limit: params.limit,
|
|
945
1661
|
cursor: params.cursor
|
|
946
1662
|
});
|
|
@@ -948,196 +1664,203 @@ function registerTools(server, ctx) {
|
|
|
948
1664
|
);
|
|
949
1665
|
server.tool(
|
|
950
1666
|
"get-template",
|
|
1667
|
+
"Get one template by id (image, env, ports, disk/volume config).",
|
|
951
1668
|
{
|
|
952
|
-
templateId:
|
|
1669
|
+
templateId: import_zod6.z.string().describe("ID of the template to retrieve")
|
|
953
1670
|
},
|
|
1671
|
+
{ title: "Get template", ...READ_ONLY },
|
|
954
1672
|
async (params) => {
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
text: JSON.stringify(result, null, 2)
|
|
961
|
-
}
|
|
962
|
-
]
|
|
963
|
-
};
|
|
1673
|
+
const backend = backendFor("templates");
|
|
1674
|
+
const result = await callRestUrl(
|
|
1675
|
+
`${backend.base}${backend.get(params.templateId)}`
|
|
1676
|
+
);
|
|
1677
|
+
return jsonReply2(result);
|
|
964
1678
|
}
|
|
965
1679
|
);
|
|
966
1680
|
server.tool(
|
|
967
1681
|
"create-template",
|
|
1682
|
+
"Create a reusable pod/endpoint template (image, ports, env, disk/volume). On v2 a category is required and defaults to NVIDIA.",
|
|
968
1683
|
{
|
|
969
|
-
name:
|
|
970
|
-
imageName:
|
|
971
|
-
isServerless:
|
|
972
|
-
ports:
|
|
973
|
-
dockerEntrypoint:
|
|
974
|
-
dockerStartCmd:
|
|
975
|
-
env:
|
|
976
|
-
containerDiskInGb:
|
|
977
|
-
volumeInGb:
|
|
978
|
-
volumeMountPath:
|
|
979
|
-
readme:
|
|
980
|
-
|
|
1684
|
+
name: import_zod6.z.string().describe("Name for the template"),
|
|
1685
|
+
imageName: import_zod6.z.string().describe("Docker image to use"),
|
|
1686
|
+
isServerless: import_zod6.z.boolean().optional().describe("Is this a serverless template"),
|
|
1687
|
+
ports: import_zod6.z.array(import_zod6.z.string()).optional().describe("Ports to expose"),
|
|
1688
|
+
dockerEntrypoint: import_zod6.z.array(import_zod6.z.string()).optional().describe("Docker entrypoint commands"),
|
|
1689
|
+
dockerStartCmd: import_zod6.z.array(import_zod6.z.string()).optional().describe("Docker start commands"),
|
|
1690
|
+
env: import_zod6.z.record(import_zod6.z.string()).optional().describe("Environment variables"),
|
|
1691
|
+
containerDiskInGb: import_zod6.z.number().optional().describe("Container disk size in GB"),
|
|
1692
|
+
volumeInGb: import_zod6.z.number().optional().describe("Volume size in GB"),
|
|
1693
|
+
volumeMountPath: import_zod6.z.string().optional().describe("Path to mount the volume"),
|
|
1694
|
+
readme: import_zod6.z.string().optional().describe("README content in markdown format"),
|
|
1695
|
+
category: import_zod6.z.enum(["CPU", "NVIDIA", "AMD"]).optional().describe(
|
|
1696
|
+
"Hardware category for the template. On v2 a category is required and defaults to NVIDIA when omitted \u2014 set CPU or AMD for non-NVIDIA templates so they are not mis-categorized."
|
|
1697
|
+
),
|
|
1698
|
+
containerRegistryAuthId: import_zod6.z.string().optional().describe(
|
|
981
1699
|
"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."
|
|
982
1700
|
)
|
|
983
1701
|
},
|
|
1702
|
+
{ title: "Create template", ...WRITE },
|
|
984
1703
|
async (params) => {
|
|
985
|
-
const
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
};
|
|
1704
|
+
const backend = backendFor("templates");
|
|
1705
|
+
const body = backend.mapCreate(params);
|
|
1706
|
+
const result = await callRestUrl(
|
|
1707
|
+
`${backend.base}${backend.list}`,
|
|
1708
|
+
"POST",
|
|
1709
|
+
body
|
|
1710
|
+
);
|
|
1711
|
+
return jsonReply2(result);
|
|
994
1712
|
}
|
|
995
1713
|
);
|
|
996
1714
|
server.tool(
|
|
997
1715
|
"update-template",
|
|
1716
|
+
"Update a template's mutable fields (name, image, ports, env, readme). Only provided fields change.",
|
|
998
1717
|
{
|
|
999
|
-
templateId:
|
|
1000
|
-
name:
|
|
1001
|
-
imageName:
|
|
1002
|
-
ports:
|
|
1003
|
-
env:
|
|
1004
|
-
readme:
|
|
1005
|
-
containerRegistryAuthId:
|
|
1718
|
+
templateId: import_zod6.z.string().describe("ID of the template to update"),
|
|
1719
|
+
name: import_zod6.z.string().optional().describe("New name for the template"),
|
|
1720
|
+
imageName: import_zod6.z.string().optional().describe("New Docker image"),
|
|
1721
|
+
ports: import_zod6.z.array(import_zod6.z.string()).optional().describe("New ports to expose"),
|
|
1722
|
+
env: import_zod6.z.record(import_zod6.z.string()).optional().describe("New environment variables"),
|
|
1723
|
+
readme: import_zod6.z.string().optional().describe("New README content in markdown format"),
|
|
1724
|
+
containerRegistryAuthId: import_zod6.z.string().optional().describe(
|
|
1006
1725
|
"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."
|
|
1007
1726
|
)
|
|
1008
1727
|
},
|
|
1728
|
+
{ title: "Update template", ...WRITE, idempotentHint: true },
|
|
1009
1729
|
async (params) => {
|
|
1010
1730
|
const { templateId, ...updateParams } = params;
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1731
|
+
const backend = backendFor("templates");
|
|
1732
|
+
const body = backend.mapUpdate(updateParams);
|
|
1733
|
+
const result = await callRestUrl(
|
|
1734
|
+
`${backend.base}${backend.get(templateId)}`,
|
|
1013
1735
|
"PATCH",
|
|
1014
|
-
|
|
1736
|
+
body
|
|
1015
1737
|
);
|
|
1016
|
-
return
|
|
1017
|
-
content: [
|
|
1018
|
-
{
|
|
1019
|
-
type: "text",
|
|
1020
|
-
text: JSON.stringify(result, null, 2)
|
|
1021
|
-
}
|
|
1022
|
-
]
|
|
1023
|
-
};
|
|
1738
|
+
return jsonReply2(result);
|
|
1024
1739
|
}
|
|
1025
1740
|
);
|
|
1026
1741
|
server.tool(
|
|
1027
1742
|
"delete-template",
|
|
1743
|
+
"Permanently delete a template. This cannot be undone.",
|
|
1028
1744
|
{
|
|
1029
|
-
templateId:
|
|
1745
|
+
templateId: import_zod6.z.string().describe("ID of the template to delete")
|
|
1030
1746
|
},
|
|
1747
|
+
{ title: "Delete template", ...DESTRUCTIVE },
|
|
1031
1748
|
async (params) => {
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1749
|
+
const backend = backendFor("templates");
|
|
1750
|
+
const result = await callRestUrl(
|
|
1751
|
+
`${backend.base}${backend.get(params.templateId)}`,
|
|
1034
1752
|
"DELETE"
|
|
1035
1753
|
);
|
|
1036
|
-
return
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1754
|
+
return jsonReply2(result);
|
|
1755
|
+
}
|
|
1756
|
+
);
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// src/tools/network-volumes.ts
|
|
1760
|
+
var import_zod7 = require("zod");
|
|
1761
|
+
function registerNetworkVolumeTools(server, rt) {
|
|
1762
|
+
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
1763
|
+
server.tool(
|
|
1764
|
+
"list-network-volumes",
|
|
1765
|
+
"List your network volumes (persistent storage attachable to pods). Paginated via limit/cursor.",
|
|
1766
|
+
listPaginationParams,
|
|
1767
|
+
{ title: "List network volumes", ...READ_ONLY },
|
|
1768
|
+
async (params) => {
|
|
1769
|
+
const backend = backendFor("networkVolumes");
|
|
1770
|
+
const result = await callRestUrl(`${backend.base}${backend.list}`);
|
|
1771
|
+
return capListResult(backend.unwrap(result), {
|
|
1772
|
+
limit: params.limit,
|
|
1773
|
+
cursor: params.cursor
|
|
1774
|
+
});
|
|
1044
1775
|
}
|
|
1045
1776
|
);
|
|
1046
|
-
server.tool("list-network-volumes", listPaginationParams, async (params) => {
|
|
1047
|
-
const result = await runpodRequest("/networkvolumes");
|
|
1048
|
-
return capListResult(result, {
|
|
1049
|
-
limit: params.limit,
|
|
1050
|
-
cursor: params.cursor
|
|
1051
|
-
});
|
|
1052
|
-
});
|
|
1053
1777
|
server.tool(
|
|
1054
1778
|
"get-network-volume",
|
|
1779
|
+
"Get one network volume by id (name, size, data center).",
|
|
1055
1780
|
{
|
|
1056
|
-
networkVolumeId:
|
|
1781
|
+
networkVolumeId: import_zod7.z.string().describe("ID of the network volume to retrieve")
|
|
1057
1782
|
},
|
|
1783
|
+
{ title: "Get network volume", ...READ_ONLY },
|
|
1058
1784
|
async (params) => {
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1785
|
+
const backend = backendFor("networkVolumes");
|
|
1786
|
+
const result = await callRestUrl(
|
|
1787
|
+
`${backend.base}${backend.get(params.networkVolumeId)}`
|
|
1061
1788
|
);
|
|
1062
|
-
return
|
|
1063
|
-
content: [
|
|
1064
|
-
{
|
|
1065
|
-
type: "text",
|
|
1066
|
-
text: JSON.stringify(result, null, 2)
|
|
1067
|
-
}
|
|
1068
|
-
]
|
|
1069
|
-
};
|
|
1789
|
+
return jsonReply2(result);
|
|
1070
1790
|
}
|
|
1071
1791
|
);
|
|
1072
1792
|
server.tool(
|
|
1073
1793
|
"create-network-volume",
|
|
1794
|
+
"Create a persistent network volume in a data center (size 1\u20134000 GB) that pods can mount.",
|
|
1074
1795
|
{
|
|
1075
|
-
name:
|
|
1076
|
-
size:
|
|
1077
|
-
dataCenterId:
|
|
1796
|
+
name: import_zod7.z.string().describe("Name for the network volume"),
|
|
1797
|
+
size: import_zod7.z.number().describe("Size in GB (1-4000)"),
|
|
1798
|
+
dataCenterId: import_zod7.z.string().describe("Data center ID (see list-data-centers)")
|
|
1078
1799
|
},
|
|
1800
|
+
{ title: "Create network volume", ...WRITE },
|
|
1079
1801
|
async (params) => {
|
|
1080
|
-
const
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
};
|
|
1802
|
+
const backend = backendFor("networkVolumes");
|
|
1803
|
+
const body = backend.mapCreate(params);
|
|
1804
|
+
const result = await callRestUrl(
|
|
1805
|
+
`${backend.base}${backend.list}`,
|
|
1806
|
+
"POST",
|
|
1807
|
+
body
|
|
1808
|
+
);
|
|
1809
|
+
return jsonReply2(result);
|
|
1089
1810
|
}
|
|
1090
1811
|
);
|
|
1091
1812
|
server.tool(
|
|
1092
1813
|
"update-network-volume",
|
|
1814
|
+
"Update a network volume (rename, or grow size \u2014 the new size must exceed the current size). Only provided fields change.",
|
|
1093
1815
|
{
|
|
1094
|
-
networkVolumeId:
|
|
1095
|
-
name:
|
|
1096
|
-
size:
|
|
1816
|
+
networkVolumeId: import_zod7.z.string().describe("ID of the network volume to update"),
|
|
1817
|
+
name: import_zod7.z.string().optional().describe("New name for the network volume"),
|
|
1818
|
+
size: import_zod7.z.number().optional().describe("New size in GB (must be larger than current)")
|
|
1097
1819
|
},
|
|
1820
|
+
{ title: "Update network volume", ...WRITE, idempotentHint: true },
|
|
1098
1821
|
async (params) => {
|
|
1099
1822
|
const { networkVolumeId, ...updateParams } = params;
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1823
|
+
const backend = backendFor("networkVolumes");
|
|
1824
|
+
const body = backend.mapUpdate(updateParams);
|
|
1825
|
+
const result = await callRestUrl(
|
|
1826
|
+
`${backend.base}${backend.get(networkVolumeId)}`,
|
|
1102
1827
|
"PATCH",
|
|
1103
|
-
|
|
1828
|
+
body
|
|
1104
1829
|
);
|
|
1105
|
-
return
|
|
1106
|
-
content: [
|
|
1107
|
-
{
|
|
1108
|
-
type: "text",
|
|
1109
|
-
text: JSON.stringify(result, null, 2)
|
|
1110
|
-
}
|
|
1111
|
-
]
|
|
1112
|
-
};
|
|
1830
|
+
return jsonReply2(result);
|
|
1113
1831
|
}
|
|
1114
1832
|
);
|
|
1115
1833
|
server.tool(
|
|
1116
1834
|
"delete-network-volume",
|
|
1835
|
+
"Permanently delete a network volume and its data. This cannot be undone.",
|
|
1117
1836
|
{
|
|
1118
|
-
networkVolumeId:
|
|
1837
|
+
networkVolumeId: import_zod7.z.string().describe("ID of the network volume to delete")
|
|
1119
1838
|
},
|
|
1839
|
+
{ title: "Delete network volume", ...DESTRUCTIVE },
|
|
1120
1840
|
async (params) => {
|
|
1121
|
-
const
|
|
1122
|
-
|
|
1841
|
+
const backend = backendFor("networkVolumes");
|
|
1842
|
+
const result = await callRestUrl(
|
|
1843
|
+
`${backend.base}${backend.get(params.networkVolumeId)}`,
|
|
1123
1844
|
"DELETE"
|
|
1124
1845
|
);
|
|
1125
|
-
return
|
|
1126
|
-
content: [
|
|
1127
|
-
{
|
|
1128
|
-
type: "text",
|
|
1129
|
-
text: JSON.stringify(result, null, 2)
|
|
1130
|
-
}
|
|
1131
|
-
]
|
|
1132
|
-
};
|
|
1846
|
+
return jsonReply2(result);
|
|
1133
1847
|
}
|
|
1134
1848
|
);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
// src/tools/registries.ts
|
|
1852
|
+
var import_zod8 = require("zod");
|
|
1853
|
+
function registerRegistryTools(server, rt) {
|
|
1854
|
+
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
1135
1855
|
server.tool(
|
|
1136
1856
|
"list-container-registry-auths",
|
|
1857
|
+
"List your saved container-registry credentials (private image-pull auth). Paginated via limit/cursor.",
|
|
1137
1858
|
listPaginationParams,
|
|
1859
|
+
{ title: "List container registry auths", ...READ_ONLY },
|
|
1138
1860
|
async (params) => {
|
|
1139
|
-
const
|
|
1140
|
-
|
|
1861
|
+
const backend = backendFor("registries");
|
|
1862
|
+
const result = await callRestUrl(`${backend.base}${backend.list}`);
|
|
1863
|
+
return capListResult(backend.unwrap(result), {
|
|
1141
1864
|
limit: params.limit,
|
|
1142
1865
|
cursor: params.cursor
|
|
1143
1866
|
});
|
|
@@ -1145,68 +1868,301 @@ function registerTools(server, ctx) {
|
|
|
1145
1868
|
);
|
|
1146
1869
|
server.tool(
|
|
1147
1870
|
"get-container-registry-auth",
|
|
1871
|
+
"Get one saved container-registry credential by id (the secret/password is not returned).",
|
|
1148
1872
|
{
|
|
1149
|
-
containerRegistryAuthId:
|
|
1873
|
+
containerRegistryAuthId: import_zod8.z.string().describe("ID of the container registry auth to retrieve")
|
|
1150
1874
|
},
|
|
1875
|
+
{ title: "Get container registry auth", ...READ_ONLY },
|
|
1151
1876
|
async (params) => {
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1877
|
+
const backend = backendFor("registries");
|
|
1878
|
+
const result = await callRestUrl(
|
|
1879
|
+
`${backend.base}${backend.get(params.containerRegistryAuthId)}`
|
|
1154
1880
|
);
|
|
1155
|
-
return
|
|
1156
|
-
content: [
|
|
1157
|
-
{
|
|
1158
|
-
type: "text",
|
|
1159
|
-
text: JSON.stringify(result, null, 2)
|
|
1160
|
-
}
|
|
1161
|
-
]
|
|
1162
|
-
};
|
|
1881
|
+
return jsonReply2(result);
|
|
1163
1882
|
}
|
|
1164
1883
|
);
|
|
1165
1884
|
server.tool(
|
|
1166
1885
|
"create-container-registry-auth",
|
|
1886
|
+
"Save container-registry credentials (username + password/token) so pods and endpoints can pull private images.",
|
|
1167
1887
|
{
|
|
1168
|
-
name:
|
|
1169
|
-
username:
|
|
1170
|
-
password:
|
|
1888
|
+
name: import_zod8.z.string().describe("Name for the container registry auth"),
|
|
1889
|
+
username: import_zod8.z.string().describe("Registry username"),
|
|
1890
|
+
password: import_zod8.z.string().describe("Registry password or access token (stored as a secret)")
|
|
1171
1891
|
},
|
|
1892
|
+
{ title: "Create container registry auth", ...WRITE },
|
|
1172
1893
|
async (params) => {
|
|
1173
|
-
const
|
|
1174
|
-
|
|
1894
|
+
const backend = backendFor("registries");
|
|
1895
|
+
const body = backend.mapCreate(params);
|
|
1896
|
+
const result = await callRestUrl(
|
|
1897
|
+
`${backend.base}${backend.list}`,
|
|
1175
1898
|
"POST",
|
|
1176
|
-
|
|
1899
|
+
body
|
|
1177
1900
|
);
|
|
1178
|
-
return
|
|
1179
|
-
content: [
|
|
1180
|
-
{
|
|
1181
|
-
type: "text",
|
|
1182
|
-
text: JSON.stringify(result, null, 2)
|
|
1183
|
-
}
|
|
1184
|
-
]
|
|
1185
|
-
};
|
|
1901
|
+
return jsonReply2(result);
|
|
1186
1902
|
}
|
|
1187
1903
|
);
|
|
1188
1904
|
server.tool(
|
|
1189
1905
|
"delete-container-registry-auth",
|
|
1906
|
+
"Permanently delete a saved container-registry credential. This cannot be undone.",
|
|
1190
1907
|
{
|
|
1191
|
-
containerRegistryAuthId:
|
|
1908
|
+
containerRegistryAuthId: import_zod8.z.string().describe("ID of the container registry auth to delete")
|
|
1192
1909
|
},
|
|
1910
|
+
{ title: "Delete container registry auth", ...DESTRUCTIVE },
|
|
1193
1911
|
async (params) => {
|
|
1194
|
-
const
|
|
1195
|
-
|
|
1912
|
+
const backend = backendFor("registries");
|
|
1913
|
+
const result = await callRestUrl(
|
|
1914
|
+
`${backend.base}${backend.get(params.containerRegistryAuthId)}`,
|
|
1196
1915
|
"DELETE"
|
|
1197
1916
|
);
|
|
1198
|
-
return
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1917
|
+
return jsonReply2(result);
|
|
1918
|
+
}
|
|
1919
|
+
);
|
|
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
|
+
server.tool(
|
|
1943
|
+
"list-tags",
|
|
1944
|
+
"List your tags (key/value labels on pods, volumes, clusters, and endpoints). v2-only \u2014 returns a 501 notice on the v1 API. Paginated via limit/cursor.",
|
|
1945
|
+
{
|
|
1946
|
+
...listPaginationParams,
|
|
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 },
|
|
1950
|
+
async (params) => {
|
|
1951
|
+
const backend = backendFor("tags");
|
|
1952
|
+
if (backend.version === "v1") return jsonReply2(v2OnlyNotice("list-tags"));
|
|
1953
|
+
const query = params.includeResources ? "?include=resources" : "";
|
|
1954
|
+
const raw = await callRestUrl(`${backend.base}${backend.list}${query}`);
|
|
1955
|
+
return capListResult(backend.unwrap(raw), {
|
|
1956
|
+
limit: params.limit,
|
|
1957
|
+
cursor: params.cursor
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
);
|
|
1961
|
+
server.tool(
|
|
1962
|
+
"get-tag",
|
|
1963
|
+
"Get one tag by id, optionally including its attached resources. v2-only \u2014 returns a 501 notice on the v1 API.",
|
|
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.",
|
|
1982
|
+
{
|
|
1983
|
+
key: import_zod9.z.string().min(1).describe('Tag key (e.g. "project")'),
|
|
1984
|
+
value: import_zod9.z.string().min(1).describe('Tag value (e.g. "gpt-training")'),
|
|
1985
|
+
resources: import_zod9.z.array(taggedResourceSchema).optional().describe("Resources to attach to the tag at creation time")
|
|
1986
|
+
},
|
|
1987
|
+
{ title: "Create tag", ...WRITE },
|
|
1988
|
+
async (params) => {
|
|
1989
|
+
const backend = backendFor("tags");
|
|
1990
|
+
if (backend.version === "v1")
|
|
1991
|
+
return jsonReply2(v2OnlyNotice("create-tag"));
|
|
1992
|
+
const result = await callRestUrl(
|
|
1993
|
+
`${backend.base}${backend.list}`,
|
|
1994
|
+
"POST",
|
|
1995
|
+
params
|
|
1996
|
+
);
|
|
1997
|
+
return jsonReply2(result);
|
|
1998
|
+
}
|
|
1999
|
+
);
|
|
2000
|
+
server.tool(
|
|
2001
|
+
"update-tag",
|
|
2002
|
+
"Update a tag (rename its key/value, or replace its resource associations). Provide at least one of key, value, or resources. Sending resources:[] removes all associations; omitting resources leaves them unchanged. Replacing resources is NOT atomic \u2014 a failed update can leave a partially-applied set, so for small association changes prefer attach-tag/detach-tag. v2-only \u2014 returns a 501 notice on the v1 API.",
|
|
2003
|
+
{
|
|
2004
|
+
tagId: import_zod9.z.string().describe("ID of the tag to update"),
|
|
2005
|
+
key: import_zod9.z.string().min(1).optional().describe("New tag key"),
|
|
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."
|
|
2009
|
+
)
|
|
2010
|
+
},
|
|
2011
|
+
// No idempotentHint: replacing `resources` is non-atomic upstream, so a
|
|
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 },
|
|
2074
|
+
async (params) => {
|
|
2075
|
+
const backend = backendFor("tags");
|
|
2076
|
+
if (backend.version === "v1")
|
|
2077
|
+
return jsonReply2(v2OnlyNotice("detach-tag"));
|
|
2078
|
+
const result = await callRestUrl(
|
|
2079
|
+
`${backend.base}/tags/${encodeURIComponent(params.tagId)}/resources/${params.resourceType}/${encodeURIComponent(params.resourceId)}`,
|
|
2080
|
+
"DELETE"
|
|
2081
|
+
);
|
|
2082
|
+
return jsonReply2(result);
|
|
1206
2083
|
}
|
|
1207
2084
|
);
|
|
1208
2085
|
}
|
|
1209
2086
|
|
|
2087
|
+
// src/tools/billing.ts
|
|
2088
|
+
var import_zod10 = require("zod");
|
|
2089
|
+
var BILLING_SCOPE_PATH = {
|
|
2090
|
+
all: "/billing",
|
|
2091
|
+
pods: "/billing/pods",
|
|
2092
|
+
serverless: "/billing/serverless",
|
|
2093
|
+
endpoints: "/billing/endpoints",
|
|
2094
|
+
networkvolumes: "/billing/networkvolumes",
|
|
2095
|
+
clusters: "/billing/clusters"
|
|
2096
|
+
};
|
|
2097
|
+
function registerBillingTools(server, rt) {
|
|
2098
|
+
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
2099
|
+
server.tool(
|
|
2100
|
+
"get-billing",
|
|
2101
|
+
"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
|
+
{
|
|
2103
|
+
...listPaginationParams,
|
|
2104
|
+
scope: import_zod10.z.enum([
|
|
2105
|
+
"all",
|
|
2106
|
+
"pods",
|
|
2107
|
+
"serverless",
|
|
2108
|
+
"endpoints",
|
|
2109
|
+
"networkvolumes",
|
|
2110
|
+
"clusters"
|
|
2111
|
+
]).optional().describe(
|
|
2112
|
+
'Which billing breakdown to fetch. "all" (default) is the aggregate across every resource.'
|
|
2113
|
+
),
|
|
2114
|
+
startTime: import_zod10.z.string().optional().describe(
|
|
2115
|
+
"Start of the period (RFC 3339, e.g. 2026-05-01T00:00:00Z). Defaults to 30 days ago. Mutually exclusive with lastN."
|
|
2116
|
+
),
|
|
2117
|
+
endTime: import_zod10.z.string().optional().describe(
|
|
2118
|
+
"End of the period (RFC 3339), exclusive. Defaults to now. Mutually exclusive with lastN."
|
|
2119
|
+
),
|
|
2120
|
+
bucketSize: import_zod10.z.enum(["hour", "day", "week", "month", "year"]).optional().describe("Length of each time bucket. Defaults to day."),
|
|
2121
|
+
lastN: import_zod10.z.number().int().positive().optional().describe(
|
|
2122
|
+
"Return the last N buckets of bucketSize, ending with the current one. Mutually exclusive with startTime/endTime."
|
|
2123
|
+
)
|
|
2124
|
+
},
|
|
2125
|
+
{ title: "Get billing history", ...READ_ONLY },
|
|
2126
|
+
async (params) => {
|
|
2127
|
+
const backend = backendFor("billing");
|
|
2128
|
+
if (backend.version === "v1") {
|
|
2129
|
+
return jsonReply2({
|
|
2130
|
+
error: "get-billing is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
2131
|
+
status: 501
|
|
2132
|
+
});
|
|
2133
|
+
}
|
|
2134
|
+
const subPath = BILLING_SCOPE_PATH[params.scope ?? "all"];
|
|
2135
|
+
const query = new URLSearchParams();
|
|
2136
|
+
if (params.startTime) query.set("startTime", params.startTime);
|
|
2137
|
+
if (params.endTime) query.set("endTime", params.endTime);
|
|
2138
|
+
if (params.bucketSize) query.set("bucketSize", params.bucketSize);
|
|
2139
|
+
if (params.lastN !== void 0 && !params.startTime && !params.endTime)
|
|
2140
|
+
query.set("lastN", String(params.lastN));
|
|
2141
|
+
const qs = query.toString() ? `?${query.toString()}` : "";
|
|
2142
|
+
const raw = await callRestUrl(`${backend.base}${subPath}${qs}`);
|
|
2143
|
+
return capListResult(
|
|
2144
|
+
backend.unwrap(raw),
|
|
2145
|
+
{ limit: params.limit, cursor: params.cursor },
|
|
2146
|
+
{ metadata: raw?.metadata }
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
// src/tools.ts
|
|
2153
|
+
function registerTools(server, ctx, deps = {}) {
|
|
2154
|
+
const rt = createToolRuntime(server, ctx, deps);
|
|
2155
|
+
registerCatalogTools(server, rt);
|
|
2156
|
+
registerPodTools(server, rt);
|
|
2157
|
+
registerEndpointTools(server, rt);
|
|
2158
|
+
registerJobTools(server, rt);
|
|
2159
|
+
registerTemplateTools(server, rt);
|
|
2160
|
+
registerNetworkVolumeTools(server, rt);
|
|
2161
|
+
registerRegistryTools(server, rt);
|
|
2162
|
+
registerTagTools(server, rt);
|
|
2163
|
+
registerBillingTools(server, rt);
|
|
2164
|
+
}
|
|
2165
|
+
|
|
1210
2166
|
// src/http.ts
|
|
1211
2167
|
function extractBearerToken(req) {
|
|
1212
2168
|
const authHeader = req.headers.authorization;
|