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