@runpod/mcp-server 3.2.0 → 3.3.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 +66 -0
- package/dist/{chunk-WGK7D2YR.mjs → chunk-ICUS4KGX.mjs} +345 -98
- package/dist/chunk-ICUS4KGX.mjs.map +1 -0
- package/dist/http.cjs +362 -98
- package/dist/http.cjs.map +1 -1
- package/dist/http.mjs +19 -2
- package/dist/http.mjs.map +1 -1
- package/dist/stdio.cjs +957 -204
- package/dist/stdio.cjs.map +1 -1
- package/dist/stdio.mjs +346 -99
- package/dist/stdio.mjs.map +1 -1
- package/dist/tools.cjs +344 -97
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +10 -1
- package/dist/tools.d.ts +10 -1
- package/dist/tools.mjs +1 -1
- package/dist/wizard-FBLOZ6YM.mjs +901 -0
- package/dist/wizard-FBLOZ6YM.mjs.map +1 -0
- package/package.json +2 -2
- package/dist/chunk-WGK7D2YR.mjs.map +0 -1
- package/dist/wizard-37ZJSVB5.mjs +0 -402
- package/dist/wizard-37ZJSVB5.mjs.map +0 -1
package/dist/http.cjs
CHANGED
|
@@ -69,7 +69,7 @@ function containerConfigToV2(p) {
|
|
|
69
69
|
function gpuConfigToV2(gpuTypeIds, gpuCount) {
|
|
70
70
|
const id = gpuTypeIds?.[0];
|
|
71
71
|
if (id === void 0) return void 0;
|
|
72
|
-
return
|
|
72
|
+
return { id, count: gpuCount ?? 1 };
|
|
73
73
|
}
|
|
74
74
|
function mapPodCreateToV2(params) {
|
|
75
75
|
return compact({
|
|
@@ -196,6 +196,42 @@ function podBodyFromTemplate(template) {
|
|
|
196
196
|
}
|
|
197
197
|
return out;
|
|
198
198
|
}
|
|
199
|
+
var SSH_PUBLIC_KEY_LINE = /^(ssh-[a-z0-9-]+|ecdsa-[a-z0-9-]+|sk-[a-z0-9@.-]+)\s+\S+/i;
|
|
200
|
+
var PRIVATE_KEY_MARKER = /private[-\s]?key|PuTTY-User-Key-File/i;
|
|
201
|
+
function normalizeSshPublicKey(input) {
|
|
202
|
+
if (PRIVATE_KEY_MARKER.test(input))
|
|
203
|
+
return {
|
|
204
|
+
ok: false,
|
|
205
|
+
error: 'sshPublicKey looks like a PRIVATE key. Pass the PUBLIC key instead \u2014 the contents of your ~/.ssh/<key>.pub file (e.g. "ssh-ed25519 AAAA\u2026").'
|
|
206
|
+
};
|
|
207
|
+
const lines = input.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
208
|
+
if (lines.length === 0)
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
error: "sshPublicKey is empty. Pass the contents of your ~/.ssh/<key>.pub file, or omit the parameter entirely to create the pod without SSH."
|
|
212
|
+
};
|
|
213
|
+
if (lines.some((line) => !SSH_PUBLIC_KEY_LINE.test(line)))
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
error: 'sshPublicKey is not an SSH public key. Expected the CONTENTS of your ~/.ssh/<key>.pub file \u2014 a line like "ssh-ed25519 AAAAC3Nza\u2026 you@host" (ssh-*, ecdsa-*, or sk-* followed by the base64 key) \u2014 not a file path, a SHA256 fingerprint, or a bare base64 blob. Newline-separate multiple keys.'
|
|
217
|
+
};
|
|
218
|
+
return { ok: true, key: lines.join("\n") };
|
|
219
|
+
}
|
|
220
|
+
function applySshPublicKey(body, sshPublicKey) {
|
|
221
|
+
const ports = Array.isArray(body.ports) ? body.ports : [];
|
|
222
|
+
const hasSshPort = ports.some((p) => /^\s*22\/tcp\s*$/i.test(String(p)));
|
|
223
|
+
const env = { ...body.env ?? {} };
|
|
224
|
+
const key = sshPublicKey.trim();
|
|
225
|
+
if (!env.PUBLIC_KEY) env.PUBLIC_KEY = key;
|
|
226
|
+
else if (!env.PUBLIC_KEY.includes(key))
|
|
227
|
+
env.PUBLIC_KEY = `${env.PUBLIC_KEY}
|
|
228
|
+
${key}`;
|
|
229
|
+
return {
|
|
230
|
+
...body,
|
|
231
|
+
ports: hasSshPort ? ports : [...ports, "22/tcp"],
|
|
232
|
+
env
|
|
233
|
+
};
|
|
234
|
+
}
|
|
199
235
|
function mapTemplateUpdateToV2(params) {
|
|
200
236
|
return {
|
|
201
237
|
...containerConfigToV2(params),
|
|
@@ -411,7 +447,7 @@ function resolveBackend(opts) {
|
|
|
411
447
|
|
|
412
448
|
// src/server.ts
|
|
413
449
|
var SERVER_NAME = "Runpod API Server";
|
|
414
|
-
var SERVER_VERSION = true ? "3.
|
|
450
|
+
var SERVER_VERSION = true ? "3.3.0" : "dev";
|
|
415
451
|
var SERVER_INSTRUCTIONS = `These tools cover common Runpod operations, but they are curated projections: the underlying APIs expose more fields and parameters than the tools surface.
|
|
416
452
|
|
|
417
453
|
When you need data or capability beyond these tools, consult the full API contracts directly. The REST v2 API lives at https://api.runpod.io/v2 \u2014 the same host these tools call \u2014 and is machine-readable at https://api.runpod.io/v2/openapi.json (fetch and search it to enumerate every path, parameter, and response schema). REST v1 at https://rest.runpod.io/v1 is deprecated; prefer v2. The REST version these tools are actually configured for is reported in the serverInfo.version string of the initialize response, e.g. "[RUNPOD_REST_VERSION=v2]". The GraphQL API at https://api.runpod.io/graphql may expose data the REST API does not yet cover; its schema is documented at https://graphql-spec.runpod.io/, and introspection is disabled in production, so use that reference rather than __schema queries. Both APIs take Bearer auth with a Runpod API key: use the key this client is already configured with \u2014 do not search the environment or filesystem for a credential.`;
|
|
@@ -586,6 +622,28 @@ var HttpError = class extends Error {
|
|
|
586
622
|
this.body = body;
|
|
587
623
|
}
|
|
588
624
|
};
|
|
625
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
626
|
+
var MIN_REQUEST_TIMEOUT_MS = 1e3;
|
|
627
|
+
var RequestTimeoutError = class extends Error {
|
|
628
|
+
constructor(prefix, timeoutMs, method) {
|
|
629
|
+
const write = method !== "GET" && method !== "HEAD";
|
|
630
|
+
super(
|
|
631
|
+
`${prefix}: no response after ${timeoutMs}ms for ${method} (the API may be overloaded, or still be working on it). ${write ? "This request may have SUCCEEDED upstream \u2014 do not retry blindly. Check with the matching list-/get- tool first and retry only if it did not take effect." : "This was a read and changed nothing, so retrying is safe."}`
|
|
632
|
+
);
|
|
633
|
+
this.name = "RequestTimeoutError";
|
|
634
|
+
this.timeoutMs = timeoutMs;
|
|
635
|
+
this.method = method;
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
async function withRequestTimeout(errorPrefix, timeoutMs, method, run) {
|
|
639
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
640
|
+
try {
|
|
641
|
+
return await run(signal);
|
|
642
|
+
} catch (error) {
|
|
643
|
+
if (error instanceof HttpError || !signal.aborted) throw error;
|
|
644
|
+
throw new RequestTimeoutError(errorPrefix, timeoutMs, method);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
589
647
|
function isJsonContentType(contentType) {
|
|
590
648
|
if (!contentType) return false;
|
|
591
649
|
const ct = contentType.toLowerCase();
|
|
@@ -601,28 +659,49 @@ function buildRequestHeaders(apiKey, tracking) {
|
|
|
601
659
|
...tracking()
|
|
602
660
|
};
|
|
603
661
|
}
|
|
662
|
+
function resolveCeiling(ceiling) {
|
|
663
|
+
if (ceiling === void 0) return Infinity;
|
|
664
|
+
if (typeof ceiling !== "function") return ceiling;
|
|
665
|
+
return Math.max(ceiling(), MIN_REQUEST_TIMEOUT_MS);
|
|
666
|
+
}
|
|
667
|
+
function clampTimeout(requestedMs, ceiling, defaultMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
668
|
+
return Math.min(requestedMs ?? defaultMs, resolveCeiling(ceiling));
|
|
669
|
+
}
|
|
604
670
|
function createHttpClient(deps) {
|
|
605
|
-
|
|
606
|
-
|
|
671
|
+
const defaultTimeoutMs = deps.defaultTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
672
|
+
return async function request(url, method = "GET", body, options) {
|
|
673
|
+
const timeoutMs = clampTimeout(
|
|
674
|
+
options?.timeoutMs,
|
|
675
|
+
deps.maxTimeoutMs,
|
|
676
|
+
defaultTimeoutMs
|
|
677
|
+
);
|
|
678
|
+
return withRequestTimeout(
|
|
679
|
+
deps.errorPrefix,
|
|
680
|
+
timeoutMs,
|
|
607
681
|
method,
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
response.
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
682
|
+
async (signal) => {
|
|
683
|
+
const init = {
|
|
684
|
+
method,
|
|
685
|
+
headers: buildRequestHeaders(deps.apiKey, deps.tracking),
|
|
686
|
+
signal
|
|
687
|
+
};
|
|
688
|
+
if (body && methodSendsBody(method)) {
|
|
689
|
+
init.body = JSON.stringify(body);
|
|
690
|
+
}
|
|
691
|
+
const response = await deps.fetch(url, init);
|
|
692
|
+
if (!response.ok) {
|
|
693
|
+
const hint = response.status === 429 ? rateLimitHint(
|
|
694
|
+
response.headers.get("ratelimit"),
|
|
695
|
+
response.headers.get("retry-after")
|
|
696
|
+
) : response.status === 401 ? EXPIRED_CREDENTIAL_HINT : void 0;
|
|
697
|
+
const text = await response.text().catch(
|
|
698
|
+
() => "<body unavailable: the connection closed while reading it>"
|
|
699
|
+
);
|
|
700
|
+
throw new HttpError(deps.errorPrefix, response.status, text, hint);
|
|
701
|
+
}
|
|
702
|
+
return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
|
|
703
|
+
}
|
|
704
|
+
);
|
|
626
705
|
};
|
|
627
706
|
}
|
|
628
707
|
|
|
@@ -749,7 +828,7 @@ async function streamLogsReply(rt, tool, params) {
|
|
|
749
828
|
}
|
|
750
829
|
|
|
751
830
|
// src/tools/runtime.ts
|
|
752
|
-
var MCP_SERVER_VERSION = true ? "3.
|
|
831
|
+
var MCP_SERVER_VERSION = true ? "3.3.0" : "dev";
|
|
753
832
|
var SESSION_ID = (0, import_node_crypto.randomUUID)();
|
|
754
833
|
function trackingHeaders(server, ctx) {
|
|
755
834
|
const info = server.server.getClientVersion();
|
|
@@ -764,6 +843,11 @@ function trackingHeaders(server, ctx) {
|
|
|
764
843
|
sessionId: SESSION_ID
|
|
765
844
|
});
|
|
766
845
|
}
|
|
846
|
+
var HTTP_TRANSPORT_BUDGET_MS = 5e4;
|
|
847
|
+
function invocationBudget(totalMs) {
|
|
848
|
+
const startedAt = Date.now();
|
|
849
|
+
return () => totalMs - (Date.now() - startedAt);
|
|
850
|
+
}
|
|
767
851
|
var defaultFetch = import_node_fetch.default;
|
|
768
852
|
async function withApiErrorLog(label, call) {
|
|
769
853
|
try {
|
|
@@ -787,6 +871,23 @@ var DESTRUCTIVE = {
|
|
|
787
871
|
openWorldHint: true
|
|
788
872
|
};
|
|
789
873
|
async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
874
|
+
return withRequestTimeout(
|
|
875
|
+
"Runpod GraphQL Error",
|
|
876
|
+
clampTimeout(options?.timeoutMs, options?.maxTimeoutMs),
|
|
877
|
+
// GraphQL is POST on the wire whatever it carries, but the retry advice keys
|
|
878
|
+
// off the method, and telling a caller its list-gpu-types "may have
|
|
879
|
+
// SUCCEEDED upstream — check with the matching list-/get- tool first" points
|
|
880
|
+
// it back at the tool that just failed. The operation keyword is what
|
|
881
|
+
// actually says whether anything could have been written.
|
|
882
|
+
isGraphqlMutation(query) ? "POST" : "GET",
|
|
883
|
+
(signal) => runGraphql(query, tracking, fetchImpl, url, signal, options)
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
function isGraphqlMutation(query) {
|
|
887
|
+
const firstMeaningful = query.split("\n").map((line) => line.trim()).find((line) => line.length > 0 && !line.startsWith("#"));
|
|
888
|
+
return /^mutation\b/.test(firstMeaningful ?? "");
|
|
889
|
+
}
|
|
890
|
+
async function runGraphql(query, tracking, fetchImpl, url, signal, options) {
|
|
790
891
|
const response = await fetchImpl(url, {
|
|
791
892
|
method: "POST",
|
|
792
893
|
headers: {
|
|
@@ -797,7 +898,8 @@ async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
|
797
898
|
body: JSON.stringify({
|
|
798
899
|
query,
|
|
799
900
|
...options?.variables ? { variables: options.variables } : {}
|
|
800
|
-
})
|
|
901
|
+
}),
|
|
902
|
+
signal
|
|
801
903
|
});
|
|
802
904
|
if (!response.ok) {
|
|
803
905
|
const bodyText = await response.text();
|
|
@@ -853,39 +955,53 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
853
955
|
}
|
|
854
956
|
const rawFetch = deps.fetch ?? defaultFetch;
|
|
855
957
|
const httpFetch = observeUnauthorized(rawFetch);
|
|
958
|
+
const remainingHttpBudgetMs = invocationBudget(HTTP_TRANSPORT_BUDGET_MS);
|
|
959
|
+
const timeouts = {
|
|
960
|
+
defaultTimeoutMs: deps.defaultTimeoutMs,
|
|
961
|
+
maxTimeoutMs: deps.maxTimeoutMs ?? (ctx.transport === "http" ? remainingHttpBudgetMs : void 0)
|
|
962
|
+
};
|
|
856
963
|
const v1Client = createHttpClient({
|
|
857
964
|
apiKey: ctx.apiKey,
|
|
858
965
|
fetch: httpFetch,
|
|
859
966
|
tracking,
|
|
860
|
-
errorPrefix: "Runpod API Error"
|
|
967
|
+
errorPrefix: "Runpod API Error",
|
|
968
|
+
...timeouts
|
|
861
969
|
});
|
|
862
|
-
const runpodRequest = (endpoint, method = "GET", body) => withApiErrorLog(
|
|
970
|
+
const runpodRequest = (endpoint, method = "GET", body, options) => withApiErrorLog(
|
|
863
971
|
"Error calling Runpod API:",
|
|
864
|
-
() => v1Client(
|
|
972
|
+
() => v1Client(
|
|
973
|
+
`${restV1Base(process.env)}${endpoint}`,
|
|
974
|
+
method,
|
|
975
|
+
body,
|
|
976
|
+
options
|
|
977
|
+
)
|
|
865
978
|
);
|
|
866
979
|
const serverlessClient = createHttpClient({
|
|
867
980
|
apiKey: ctx.apiKey,
|
|
868
981
|
fetch: httpFetch,
|
|
869
982
|
tracking,
|
|
870
|
-
errorPrefix: "Runpod Serverless API Error"
|
|
983
|
+
errorPrefix: "Runpod Serverless API Error",
|
|
984
|
+
...timeouts
|
|
871
985
|
});
|
|
872
|
-
const serverlessRequest = (endpointId, path, method = "GET", body) => withApiErrorLog(
|
|
986
|
+
const serverlessRequest = (endpointId, path, method = "GET", body, options) => withApiErrorLog(
|
|
873
987
|
"Error calling Runpod Serverless API:",
|
|
874
988
|
() => serverlessClient(
|
|
875
989
|
`${serverlessBase(process.env)}/${endpointId}${path}`,
|
|
876
990
|
method,
|
|
877
|
-
body
|
|
991
|
+
body,
|
|
992
|
+
options
|
|
878
993
|
)
|
|
879
994
|
);
|
|
880
995
|
const restClient = createHttpClient({
|
|
881
996
|
apiKey: ctx.apiKey,
|
|
882
997
|
fetch: httpFetch,
|
|
883
998
|
tracking,
|
|
884
|
-
errorPrefix: "Runpod API Error"
|
|
999
|
+
errorPrefix: "Runpod API Error",
|
|
1000
|
+
...timeouts
|
|
885
1001
|
});
|
|
886
|
-
const callRestUrl = (url, method = "GET", body) => withApiErrorLog(
|
|
1002
|
+
const callRestUrl = (url, method = "GET", body, options) => withApiErrorLog(
|
|
887
1003
|
"Error calling Runpod API:",
|
|
888
|
-
() => restClient(url, method, body)
|
|
1004
|
+
() => restClient(url, method, body, options)
|
|
889
1005
|
);
|
|
890
1006
|
const sseFetch = observeUnauthorized(
|
|
891
1007
|
deps.sseFetch ?? import_node_fetch.default
|
|
@@ -918,12 +1034,18 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
918
1034
|
};
|
|
919
1035
|
return {
|
|
920
1036
|
jsonReply,
|
|
1037
|
+
// No GraphQL caller overrides a deadline, but both are still capped by the
|
|
1038
|
+
// shared budget: deploy-hub-repo spends one of each back to back.
|
|
921
1039
|
graphql: (query) => graphqlRequest(
|
|
922
1040
|
query,
|
|
923
1041
|
tracking,
|
|
924
1042
|
// rawFetch, not httpFetch: this call carries no credential (see above).
|
|
925
1043
|
rawFetch,
|
|
926
|
-
publicGraphqlBase(process.env)
|
|
1044
|
+
publicGraphqlBase(process.env),
|
|
1045
|
+
{
|
|
1046
|
+
timeoutMs: deps.defaultTimeoutMs,
|
|
1047
|
+
maxTimeoutMs: timeouts.maxTimeoutMs
|
|
1048
|
+
}
|
|
927
1049
|
),
|
|
928
1050
|
graphqlAuthed: (query, variables) => graphqlRequest(
|
|
929
1051
|
query,
|
|
@@ -931,7 +1053,12 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
931
1053
|
httpFetch,
|
|
932
1054
|
// NOT publicGraphqlBase: this call carries the caller's API key.
|
|
933
1055
|
authedGraphqlBase(process.env),
|
|
934
|
-
{
|
|
1056
|
+
{
|
|
1057
|
+
variables,
|
|
1058
|
+
apiKey: ctx.apiKey,
|
|
1059
|
+
timeoutMs: deps.defaultTimeoutMs,
|
|
1060
|
+
maxTimeoutMs: timeouts.maxTimeoutMs
|
|
1061
|
+
}
|
|
935
1062
|
),
|
|
936
1063
|
runpodRequest,
|
|
937
1064
|
serverlessRequest,
|
|
@@ -939,7 +1066,8 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
939
1066
|
backendFor,
|
|
940
1067
|
streamSse,
|
|
941
1068
|
podAction,
|
|
942
|
-
env: process.env
|
|
1069
|
+
env: process.env,
|
|
1070
|
+
transport: ctx.transport
|
|
943
1071
|
};
|
|
944
1072
|
}
|
|
945
1073
|
|
|
@@ -1008,6 +1136,7 @@ function capListResult(result, options, extra) {
|
|
|
1008
1136
|
}
|
|
1009
1137
|
|
|
1010
1138
|
// src/tools/catalog.ts
|
|
1139
|
+
var GPU_PRODUCTS = /* @__PURE__ */ new Set(["POD", "CLUSTER", "SERVERLESS"]);
|
|
1011
1140
|
function registerCatalogTools(server, rt) {
|
|
1012
1141
|
const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
|
|
1013
1142
|
server.tool(
|
|
@@ -1026,6 +1155,9 @@ function registerCatalogTools(server, rt) {
|
|
|
1026
1155
|
),
|
|
1027
1156
|
includeAvailability: import_zod3.z.boolean().optional().describe(
|
|
1028
1157
|
"Request realtime stock and annotate each GPU with an availability summary (HIGH/MEDIUM/LOW/NONE). Default true. Set false to skip the availability lookup \u2014 then out-of-stock GPUs cannot be filtered."
|
|
1158
|
+
),
|
|
1159
|
+
product: import_zod3.z.enum(["POD", "CLUSTER", "SERVERLESS"]).optional().describe(
|
|
1160
|
+
"Product context for the availability lookup \u2014 the same GPU can be scarce for Pods and plentiful for Serverless. Default POD. Use SERVERLESS when picking a GPU for an endpoint, CLUSTER for Instant Clusters. Ignored when includeAvailability is false."
|
|
1029
1161
|
)
|
|
1030
1162
|
},
|
|
1031
1163
|
{ title: "List GPU types", ...READ_ONLY },
|
|
@@ -1033,8 +1165,9 @@ function registerCatalogTools(server, rt) {
|
|
|
1033
1165
|
const backend = backendFor("gpus");
|
|
1034
1166
|
if (backend.version === "v2") {
|
|
1035
1167
|
const wantAvailability = params.includeAvailability !== false;
|
|
1168
|
+
const product = GPU_PRODUCTS.has(String(params.product)) ? String(params.product) : "POD";
|
|
1036
1169
|
const raw = await callRestUrl(
|
|
1037
|
-
`${backend.base}${backend.list}${wantAvailability ?
|
|
1170
|
+
`${backend.base}${backend.list}${wantAvailability ? `?include=AVAILABILITY&product=${product}` : ""}`
|
|
1038
1171
|
);
|
|
1039
1172
|
let gpus = backend.unwrap(raw);
|
|
1040
1173
|
gpus = gpus.filter((g) => g.id !== "unknown");
|
|
@@ -1207,6 +1340,9 @@ function registerCatalogTools(server, rt) {
|
|
|
1207
1340
|
gpuTypeId: import_zod3.z.string().describe("ID of the GPU type to retrieve"),
|
|
1208
1341
|
includeAvailability: import_zod3.z.boolean().optional().describe(
|
|
1209
1342
|
"Include realtime per-datacenter availability (HIGH/MEDIUM/LOW/NONE). Default true."
|
|
1343
|
+
),
|
|
1344
|
+
product: import_zod3.z.enum(["POD", "CLUSTER", "SERVERLESS"]).optional().describe(
|
|
1345
|
+
"Product context for the availability lookup \u2014 stock differs by product. Default POD. Use SERVERLESS when picking a GPU for an endpoint, CLUSTER for Instant Clusters. Ignored when includeAvailability is false."
|
|
1210
1346
|
)
|
|
1211
1347
|
},
|
|
1212
1348
|
{ title: "Get GPU type", ...READ_ONLY },
|
|
@@ -1219,7 +1355,8 @@ function registerCatalogTools(server, rt) {
|
|
|
1219
1355
|
});
|
|
1220
1356
|
}
|
|
1221
1357
|
const path = backend.get(encodeURIComponent(params.gpuTypeId));
|
|
1222
|
-
const
|
|
1358
|
+
const product = GPU_PRODUCTS.has(String(params.product)) ? String(params.product) : "POD";
|
|
1359
|
+
const query = params.includeAvailability === false ? "" : `?include=AVAILABILITY&product=${product}`;
|
|
1223
1360
|
const result = await callRestUrl(`${backend.base}${path}${query}`);
|
|
1224
1361
|
return jsonReply2(result);
|
|
1225
1362
|
}
|
|
@@ -2099,7 +2236,7 @@ function registerPodTools(server, rt) {
|
|
|
2099
2236
|
);
|
|
2100
2237
|
server.tool(
|
|
2101
2238
|
"create-pod",
|
|
2102
|
-
'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). Pass either imageName or templateId (templateId deploys from an existing template \u2014 its name, image, start command, ports, env, disk, volume, and registry credential are used as defaults, and any field you also pass explicitly replaces the template value for that field, e.g. passing env replaces the whole env set rather than merging; pass containerRegistryAuthId to override or supply a private-image credential, or an empty string to deploy with none). If the user specifies neither an image nor a template, recommend the "Runpod Pytorch 2.8.0" image (runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404) as the default \u2014 it has the most up-to-date CUDA and PyTorch versions.',
|
|
2239
|
+
'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). Pass either imageName or templateId (templateId deploys from an existing template \u2014 its name, image, start command, ports, env, disk, volume, and registry credential are used as defaults, and any field you also pass explicitly replaces the template value for that field, e.g. passing env replaces the whole env set rather than merging; pass containerRegistryAuthId to override or supply a private-image credential, or an empty string to deploy with none). For full SSH access (including SCP/SFTP/rsync), pass sshPublicKey \u2014 it sets the PUBLIC_KEY env var and exposes 22/tcp, which official Runpod images turn into a running sshd with the key authorized. If the user specifies neither an image nor a template, recommend the "Runpod Pytorch 2.8.0" image (runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404) as the default \u2014 it has the most up-to-date CUDA and PyTorch versions.',
|
|
2103
2240
|
{
|
|
2104
2241
|
name: import_zod7.z.string().optional().describe(
|
|
2105
2242
|
"Name for the pod. Required on v2 unless deploying from a templateId (the template supplies its name as the default)."
|
|
@@ -2113,12 +2250,18 @@ function registerPodTools(server, rt) {
|
|
|
2113
2250
|
),
|
|
2114
2251
|
cloudType: import_zod7.z.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
|
|
2115
2252
|
gpuTypeIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("List of acceptable GPU types"),
|
|
2116
|
-
|
|
2253
|
+
// v2 GpuConfig.count is `integer, minimum: 1`. Without these bounds a 0,
|
|
2254
|
+
// a negative, or a fractional count reached the wire as-is and came back
|
|
2255
|
+
// as a raw 422; reject it here with a message that names the field.
|
|
2256
|
+
gpuCount: import_zod7.z.number().int().min(1).optional().describe("Number of GPUs (minimum 1)"),
|
|
2117
2257
|
containerDiskInGb: import_zod7.z.number().optional().describe("Container disk size in GB"),
|
|
2118
2258
|
volumeInGb: import_zod7.z.number().optional().describe("Volume size in GB"),
|
|
2119
2259
|
volumeMountPath: import_zod7.z.string().optional().describe("Path to mount the volume"),
|
|
2120
2260
|
ports: import_zod7.z.array(import_zod7.z.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
|
|
2121
2261
|
env: import_zod7.z.record(import_zod7.z.string()).optional().describe("Environment variables"),
|
|
2262
|
+
sshPublicKey: import_zod7.z.string().optional().describe(
|
|
2263
|
+
'SSH PUBLIC key(s) to authorize on the pod \u2014 the CONTENTS of your ~/.ssh/<key>.pub file, i.e. a literal "ssh-ed25519 AAAAC3Nza\u2026 you@host" line, NOT a path to the file, a fingerprint, or the private key; newline-separate multiple keys. Merges the key into the PUBLIC_KEY env var and ensures 22/tcp is exposed, enabling full SSH (SCP/SFTP/rsync) on any image that honors the PUBLIC_KEY convention \u2014 all runpod/* official images do. Extends template ports/env rather than replacing them.'
|
|
2264
|
+
),
|
|
2122
2265
|
containerRegistryAuthId: import_zod7.z.string().optional().describe(
|
|
2123
2266
|
"Container registry credential ID for pulling a private image (see list-container-registry-auths). When deploying from a templateId, this overrides the credential the template carries; pass an empty string to deploy with NO credential (clears a template\u2019s inherited registry)."
|
|
2124
2267
|
),
|
|
@@ -2127,6 +2270,15 @@ function registerPodTools(server, rt) {
|
|
|
2127
2270
|
{ title: "Create pod", ...WRITE },
|
|
2128
2271
|
async (params) => {
|
|
2129
2272
|
const backend = backendFor("pods");
|
|
2273
|
+
const { sshPublicKey, ...podParams } = params;
|
|
2274
|
+
let sshKey;
|
|
2275
|
+
if (sshPublicKey !== void 0) {
|
|
2276
|
+
const validated = normalizeSshPublicKey(sshPublicKey);
|
|
2277
|
+
if (!validated.ok)
|
|
2278
|
+
return jsonReply2({ error: validated.error, status: 400 });
|
|
2279
|
+
sshKey = validated.key;
|
|
2280
|
+
}
|
|
2281
|
+
const sshNote = "sshPublicKey applied: PUBLIC_KEY env set and 22/tcp exposed. Full SSH (incl. SCP/SFTP/rsync) also requires an image that installs PUBLIC_KEY into authorized_keys and starts sshd (all runpod/* official images do) and a machine with a public IP \u2014 read the pod\u2019s portMappings for port 22 once it is running.";
|
|
2130
2282
|
if (params.templateId && backend.version !== "v2") {
|
|
2131
2283
|
return jsonReply2({
|
|
2132
2284
|
error: "Deploying a pod from a templateId is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or pass imageName instead.",
|
|
@@ -2176,13 +2328,15 @@ function registerPodTools(server, rt) {
|
|
|
2176
2328
|
});
|
|
2177
2329
|
}
|
|
2178
2330
|
try {
|
|
2331
|
+
const cpuBody = sshKey ? applySshPublicKey(podParams, sshKey) : podParams;
|
|
2179
2332
|
const result = await callRestUrl(
|
|
2180
2333
|
`${restV1Base(env)}/pods`,
|
|
2181
2334
|
"POST",
|
|
2182
|
-
|
|
2335
|
+
cpuBody
|
|
2183
2336
|
);
|
|
2184
2337
|
return jsonReply2({
|
|
2185
2338
|
...result,
|
|
2339
|
+
...sshKey ? { _ssh: sshNote } : {},
|
|
2186
2340
|
_servedBy: "v1",
|
|
2187
2341
|
_note: "CPU pod: created on the v1 API, because the v2 REST API does not support CPU pods yet."
|
|
2188
2342
|
});
|
|
@@ -2199,7 +2353,7 @@ function registerPodTools(server, rt) {
|
|
|
2199
2353
|
}
|
|
2200
2354
|
}
|
|
2201
2355
|
}
|
|
2202
|
-
let body = backend.mapCreate(
|
|
2356
|
+
let body = backend.mapCreate(podParams);
|
|
2203
2357
|
if (params.templateId) {
|
|
2204
2358
|
let template;
|
|
2205
2359
|
try {
|
|
@@ -2217,19 +2371,21 @@ function registerPodTools(server, rt) {
|
|
|
2217
2371
|
}
|
|
2218
2372
|
body = { ...podBodyFromTemplate(template), ...body };
|
|
2219
2373
|
}
|
|
2374
|
+
if (sshKey) body = applySshPublicKey(body, sshKey);
|
|
2220
2375
|
try {
|
|
2221
2376
|
const result = await callRestUrl(
|
|
2222
2377
|
`${backend.base}${backend.list}`,
|
|
2223
2378
|
"POST",
|
|
2224
2379
|
body
|
|
2225
2380
|
);
|
|
2381
|
+
const extras = {};
|
|
2382
|
+
if (sshKey) extras._ssh = sshNote;
|
|
2226
2383
|
if (backend.version === "v2" && (params.gpuTypeIds?.length ?? 0) > 1) {
|
|
2227
|
-
|
|
2228
|
-
...result,
|
|
2229
|
-
_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(", ")}.`
|
|
2230
|
-
});
|
|
2384
|
+
extras._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(", ")}.`;
|
|
2231
2385
|
}
|
|
2232
|
-
return jsonReply2(
|
|
2386
|
+
return jsonReply2(
|
|
2387
|
+
Object.keys(extras).length ? { ...result, ...extras } : result
|
|
2388
|
+
);
|
|
2233
2389
|
} catch (error) {
|
|
2234
2390
|
if (error instanceof HttpError) {
|
|
2235
2391
|
return jsonReply2({ error: error.message, status: error.status });
|
|
@@ -2690,8 +2846,108 @@ var import_zod9 = require("zod");
|
|
|
2690
2846
|
var DIAGNOSIS_TTL_MS = 15e3;
|
|
2691
2847
|
var MAX_DIAGNOSIS_ENTRIES = 500;
|
|
2692
2848
|
var diagnosisCache = /* @__PURE__ */ new Map();
|
|
2849
|
+
var HTTP_LONG_POLL_BUDGET_MS = 45e3;
|
|
2850
|
+
var STDIO_STREAM_BUDGET_MS = 5 * 60 * 1e3;
|
|
2851
|
+
var RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS = 9e4;
|
|
2852
|
+
var STREAM_UPSTREAM_DEFAULT_WAIT_MS = 1e4;
|
|
2853
|
+
var HTTP_STREAM_POLL_WAIT_MS = 1e3;
|
|
2854
|
+
var STREAM_JOB_POLL_INTERVAL_MS = 1e3;
|
|
2855
|
+
var MAX_CONSECUTIVE_STREAM_ERRORS = 5;
|
|
2856
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
2857
|
+
"COMPLETED",
|
|
2858
|
+
"FAILED",
|
|
2859
|
+
"CANCELLED",
|
|
2860
|
+
"TIMED_OUT"
|
|
2861
|
+
]);
|
|
2862
|
+
function isUnfinishedJobReply(result) {
|
|
2863
|
+
return result !== null && typeof result === "object" && !Array.isArray(result) && !("waitClamped" in result) && !TERMINAL_STATUSES.has(result.status ?? "");
|
|
2864
|
+
}
|
|
2865
|
+
function formatBudget(ms) {
|
|
2866
|
+
return ms >= 12e4 ? `${Math.round(ms / 6e4)} minutes` : `${Math.round(ms / 1e3)} seconds`;
|
|
2867
|
+
}
|
|
2868
|
+
var UPSTREAM_HOLD_SLACK_MS = 5e3;
|
|
2869
|
+
var MIN_STREAM_POLL_TIMEOUT_MS = 2e3;
|
|
2870
|
+
var QUEUED_DIAGNOSIS_TIMEOUT_MS = 5e3;
|
|
2871
|
+
function streamPollTimeoutMs(remainingMs, holdMs) {
|
|
2872
|
+
return Math.max(
|
|
2873
|
+
Math.min(remainingMs, holdMs + UPSTREAM_HOLD_SLACK_MS),
|
|
2874
|
+
MIN_STREAM_POLL_TIMEOUT_MS
|
|
2875
|
+
);
|
|
2876
|
+
}
|
|
2877
|
+
function sleep(ms) {
|
|
2878
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2879
|
+
}
|
|
2880
|
+
function streamPollPlan(hosted) {
|
|
2881
|
+
return hosted ? {
|
|
2882
|
+
budgetMs: HTTP_LONG_POLL_BUDGET_MS,
|
|
2883
|
+
holdMs: HTTP_STREAM_POLL_WAIT_MS,
|
|
2884
|
+
query: `?wait=${HTTP_STREAM_POLL_WAIT_MS}`
|
|
2885
|
+
} : {
|
|
2886
|
+
budgetMs: STDIO_STREAM_BUDGET_MS,
|
|
2887
|
+
holdMs: STREAM_UPSTREAM_DEFAULT_WAIT_MS,
|
|
2888
|
+
query: ""
|
|
2889
|
+
};
|
|
2890
|
+
}
|
|
2891
|
+
var RESUME_ADVICE = "Call stream-job again to continue collecting output, get-job-status to check the job without streaming, or stream without a budget by calling the runtime API directly (GET https://api.runpod.ai/v2/{endpointId}/stream/{jobId} with a Bearer API key).";
|
|
2892
|
+
function budgetExhaustedNote(budgetMs, lastError) {
|
|
2893
|
+
return {
|
|
2894
|
+
pollingTimedOut: true,
|
|
2895
|
+
note: `Polling stopped after ${formatBudget(
|
|
2896
|
+
budgetMs
|
|
2897
|
+
)} with the job possibly still running. ${RESUME_ADVICE}`,
|
|
2898
|
+
// Surface the trailing error (if any) instead of discarding it — the last
|
|
2899
|
+
// polls may have been failing (e.g. job expired) even though earlier ones
|
|
2900
|
+
// succeeded. Cleared on success, so this is never a stale error from
|
|
2901
|
+
// minutes of healthy streaming ago.
|
|
2902
|
+
...lastError ? { lastError } : {}
|
|
2903
|
+
};
|
|
2904
|
+
}
|
|
2905
|
+
function errorCapNote(lastError) {
|
|
2906
|
+
return {
|
|
2907
|
+
error: `Polling aborted after ${MAX_CONSECUTIVE_STREAM_ERRORS} consecutive errors: ${lastError}`,
|
|
2908
|
+
note: `Polling stopped after ${MAX_CONSECUTIVE_STREAM_ERRORS} consecutive errors with the job possibly still running. ${RESUME_ADVICE}`
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2911
|
+
async function collectJobStream(deps) {
|
|
2912
|
+
const { poll, budgetMs, holdMs } = deps;
|
|
2913
|
+
const startedAt = Date.now();
|
|
2914
|
+
const elapsed = () => Date.now() - startedAt;
|
|
2915
|
+
const chunks = [];
|
|
2916
|
+
let result = {};
|
|
2917
|
+
let consecutiveErrors = 0;
|
|
2918
|
+
let lastError;
|
|
2919
|
+
while (true) {
|
|
2920
|
+
try {
|
|
2921
|
+
const reply = await poll(
|
|
2922
|
+
streamPollTimeoutMs(budgetMs - elapsed(), holdMs)
|
|
2923
|
+
);
|
|
2924
|
+
consecutiveErrors = 0;
|
|
2925
|
+
lastError = void 0;
|
|
2926
|
+
if (Array.isArray(reply.stream)) chunks.push(...reply.stream);
|
|
2927
|
+
result = reply;
|
|
2928
|
+
if (TERMINAL_STATUSES.has(reply.status)) break;
|
|
2929
|
+
} catch (error) {
|
|
2930
|
+
consecutiveErrors++;
|
|
2931
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
2932
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_STREAM_ERRORS) {
|
|
2933
|
+
result = { ...result, ...errorCapNote(lastError) };
|
|
2934
|
+
break;
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
if (elapsed() > budgetMs) {
|
|
2938
|
+
result = { ...result, ...budgetExhaustedNote(budgetMs, lastError) };
|
|
2939
|
+
break;
|
|
2940
|
+
}
|
|
2941
|
+
await sleep(STREAM_JOB_POLL_INTERVAL_MS);
|
|
2942
|
+
}
|
|
2943
|
+
return { result, chunks };
|
|
2944
|
+
}
|
|
2693
2945
|
function registerJobTools(server, rt) {
|
|
2694
2946
|
const { jsonReply: jsonReply2, serverlessRequest, backendFor, callRestUrl } = rt;
|
|
2947
|
+
const hosted = rt.transport === "http";
|
|
2948
|
+
const streamBudget = formatBudget(
|
|
2949
|
+
hosted ? HTTP_LONG_POLL_BUDGET_MS : STDIO_STREAM_BUDGET_MS
|
|
2950
|
+
);
|
|
2695
2951
|
async function diagnoseQueuedJob(endpointId) {
|
|
2696
2952
|
const cached = diagnosisCache.get(endpointId);
|
|
2697
2953
|
if (cached && cached.expiresAt > Date.now()) {
|
|
@@ -2702,7 +2958,10 @@ function registerJobTools(server, rt) {
|
|
|
2702
2958
|
const backend = backendFor("workers");
|
|
2703
2959
|
if (backend.version !== "v2") return null;
|
|
2704
2960
|
const raw = await callRestUrl(
|
|
2705
|
-
`${backend.base}/serverless/${endpointId}/workers
|
|
2961
|
+
`${backend.base}/serverless/${endpointId}/workers`,
|
|
2962
|
+
"GET",
|
|
2963
|
+
void 0,
|
|
2964
|
+
{ timeoutMs: QUEUED_DIAGNOSIS_TIMEOUT_MS }
|
|
2706
2965
|
);
|
|
2707
2966
|
const summary = raw?.summary;
|
|
2708
2967
|
const workers = raw?.workers ?? [];
|
|
@@ -2793,14 +3052,14 @@ function registerJobTools(server, rt) {
|
|
|
2793
3052
|
);
|
|
2794
3053
|
server.tool(
|
|
2795
3054
|
"runsync-endpoint",
|
|
2796
|
-
|
|
3055
|
+
`Submit a synchronous job to a Serverless endpoint and wait for the result. Best for fast tasks: if the job outlives the wait, the response returns a job ID and a non-terminal status (IN_QUEUE or IN_PROGRESS) to poll with get-job-status. Max payload 20 MB; results expire after 1 minute. ${hosted ? `This server waits up to ${HTTP_LONG_POLL_BUDGET_MS} ms \u2014 it runs behind a 60-second gateway deadline, so that is both the default and the ceiling for the wait parameter. For longer jobs use run-endpoint + get-job-status, or POST https://api.runpod.ai/v2/{endpointId}/runsync?wait=300000 directly with a Bearer API key.` : "The wait parameter extends the server-side wait to up to 5 minutes (300000 ms); it defaults to 90000 (90 seconds)."}`,
|
|
2797
3056
|
{
|
|
2798
3057
|
endpointId: endpointIdSchema.describe(
|
|
2799
3058
|
"ID of the Serverless endpoint to run synchronously"
|
|
2800
3059
|
),
|
|
2801
3060
|
input: inputSchema,
|
|
2802
3061
|
wait: import_zod9.z.number().min(1e3).max(3e5).optional().describe(
|
|
2803
|
-
"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)."
|
|
3062
|
+
hosted ? `How long in milliseconds the server should wait for a result before returning a job ID to poll. Accepted range 1000\u2013300000, but this server clamps anything above ${HTTP_LONG_POLL_BUDGET_MS} down to ${HTTP_LONG_POLL_BUDGET_MS}, which is also the effective default.` : "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)."
|
|
2804
3063
|
),
|
|
2805
3064
|
webhook: webhookSchema,
|
|
2806
3065
|
policy: policySchema,
|
|
@@ -2809,13 +3068,36 @@ function registerJobTools(server, rt) {
|
|
|
2809
3068
|
{ title: "Run endpoint (sync)", ...WRITE },
|
|
2810
3069
|
async (params) => {
|
|
2811
3070
|
const { endpointId, wait, ...body } = params;
|
|
2812
|
-
const
|
|
3071
|
+
const effectiveWait = hosted ? Math.min(
|
|
3072
|
+
wait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS,
|
|
3073
|
+
HTTP_LONG_POLL_BUDGET_MS
|
|
3074
|
+
) : wait;
|
|
3075
|
+
const waitQuery = effectiveWait === void 0 ? "" : `?wait=${effectiveWait}`;
|
|
3076
|
+
const path = `/runsync${waitQuery}`;
|
|
2813
3077
|
const result = await serverlessRequest(
|
|
2814
3078
|
endpointId,
|
|
2815
3079
|
path,
|
|
2816
3080
|
"POST",
|
|
2817
|
-
body
|
|
3081
|
+
body,
|
|
3082
|
+
{
|
|
3083
|
+
// From the wait actually sent — on http the clamped 45s, not the
|
|
3084
|
+
// 300s the caller may have asked for.
|
|
3085
|
+
timeoutMs: (effectiveWait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS) + UPSTREAM_HOLD_SLACK_MS
|
|
3086
|
+
}
|
|
2818
3087
|
);
|
|
3088
|
+
const requestedWait = wait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS;
|
|
3089
|
+
if (hosted && requestedWait > HTTP_LONG_POLL_BUDGET_MS && isUnfinishedJobReply(result)) {
|
|
3090
|
+
return jsonReply2({
|
|
3091
|
+
...result,
|
|
3092
|
+
waitClamped: {
|
|
3093
|
+
requestedMs: requestedWait,
|
|
3094
|
+
effectiveMs: effectiveWait,
|
|
3095
|
+
reason: `This server runs behind a 60-second gateway deadline, so a non-terminal status here means the job outlived ${formatBudget(
|
|
3096
|
+
HTTP_LONG_POLL_BUDGET_MS
|
|
3097
|
+
)}, not the requested wait \u2014 poll get-job-status rather than treating it as stuck.`
|
|
3098
|
+
}
|
|
3099
|
+
});
|
|
3100
|
+
}
|
|
2819
3101
|
return jsonReply2(result);
|
|
2820
3102
|
}
|
|
2821
3103
|
);
|
|
@@ -2849,7 +3131,7 @@ function registerJobTools(server, rt) {
|
|
|
2849
3131
|
);
|
|
2850
3132
|
server.tool(
|
|
2851
3133
|
"stream-job",
|
|
2852
|
-
|
|
3134
|
+
`Retrieve streaming output from a Serverless job. The worker must support streaming output. Polls /stream/{jobId} and collects chunks until the status is COMPLETED, FAILED, CANCELLED, or TIMED_OUT, for up to ${streamBudget}${hosted ? " (this server runs behind a 60-second gateway deadline)" : ""}. If the budget expires first, returns the chunks collected so far with pollingTimedOut: true \u2014 call stream-job again to resume where it left off, or get-job-status to check without streaming.${hosted ? " To collect with no budget, poll GET https://api.runpod.ai/v2/{endpointId}/stream/{jobId} yourself with a Bearer API key until the status is terminal \u2014 each request drains only the chunks buffered so far, so a single request is not the whole output." : ""}`,
|
|
2853
3135
|
{
|
|
2854
3136
|
endpointId: endpointIdSchema.describe(
|
|
2855
3137
|
"ID of the Serverless endpoint the job belongs to"
|
|
@@ -2858,51 +3140,16 @@ function registerJobTools(server, rt) {
|
|
|
2858
3140
|
},
|
|
2859
3141
|
{ title: "Stream job", ...READ_ONLY },
|
|
2860
3142
|
async (params) => {
|
|
2861
|
-
const
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
let finalResult = {};
|
|
2872
|
-
let consecutiveErrors = 0;
|
|
2873
|
-
let lastError;
|
|
2874
|
-
const startTime = Date.now();
|
|
2875
|
-
while (true) {
|
|
2876
|
-
try {
|
|
2877
|
-
const result = await serverlessRequest(
|
|
2878
|
-
params.endpointId,
|
|
2879
|
-
`/stream/${params.jobId}`
|
|
2880
|
-
);
|
|
2881
|
-
consecutiveErrors = 0;
|
|
2882
|
-
if (Array.isArray(result.stream)) {
|
|
2883
|
-
allChunks.push(...result.stream);
|
|
2884
|
-
}
|
|
2885
|
-
finalResult = result;
|
|
2886
|
-
if (TERMINAL_STATUSES.has(result.status)) {
|
|
2887
|
-
break;
|
|
2888
|
-
}
|
|
2889
|
-
} catch (error) {
|
|
2890
|
-
consecutiveErrors++;
|
|
2891
|
-
lastError = error instanceof Error ? error.message : String(error);
|
|
2892
|
-
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
2893
|
-
finalResult.error = `Polling aborted after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${lastError}`;
|
|
2894
|
-
break;
|
|
2895
|
-
}
|
|
2896
|
-
}
|
|
2897
|
-
if (Date.now() - startTime > MAX_POLL_TIME_MS) {
|
|
2898
|
-
finalResult.pollingTimedOut = true;
|
|
2899
|
-
finalResult.note = "Polling timed out after 5 minutes. Use get-job-status to check the job later.";
|
|
2900
|
-
if (lastError) finalResult.lastError = lastError;
|
|
2901
|
-
break;
|
|
2902
|
-
}
|
|
2903
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
2904
|
-
}
|
|
2905
|
-
return jsonReply2({ ...finalResult, stream: allChunks });
|
|
3143
|
+
const plan = streamPollPlan(hosted);
|
|
3144
|
+
const streamPath = `/stream/${params.jobId}${plan.query}`;
|
|
3145
|
+
const { result, chunks } = await collectJobStream({
|
|
3146
|
+
budgetMs: plan.budgetMs,
|
|
3147
|
+
holdMs: plan.holdMs,
|
|
3148
|
+
poll: (timeoutMs) => serverlessRequest(params.endpointId, streamPath, "GET", void 0, {
|
|
3149
|
+
timeoutMs
|
|
3150
|
+
})
|
|
3151
|
+
});
|
|
3152
|
+
return jsonReply2({ ...result, stream: chunks });
|
|
2906
3153
|
}
|
|
2907
3154
|
);
|
|
2908
3155
|
server.tool(
|
|
@@ -3674,6 +3921,23 @@ async function handleMcpRequest(req, res, opts = {}) {
|
|
|
3674
3921
|
url: req.url,
|
|
3675
3922
|
hasBearerToken: !!bearerToken
|
|
3676
3923
|
});
|
|
3924
|
+
if (req.method === "GET") {
|
|
3925
|
+
res.writeHead(405, {
|
|
3926
|
+
Allow: "POST, DELETE",
|
|
3927
|
+
"Content-Type": "application/json"
|
|
3928
|
+
});
|
|
3929
|
+
res.end(
|
|
3930
|
+
JSON.stringify({
|
|
3931
|
+
jsonrpc: "2.0",
|
|
3932
|
+
error: {
|
|
3933
|
+
code: -32e3,
|
|
3934
|
+
message: "Method Not Allowed: this server does not offer a standalone SSE stream."
|
|
3935
|
+
},
|
|
3936
|
+
id: null
|
|
3937
|
+
})
|
|
3938
|
+
);
|
|
3939
|
+
return;
|
|
3940
|
+
}
|
|
3677
3941
|
if (!bearerToken) {
|
|
3678
3942
|
writeUnauthorized(
|
|
3679
3943
|
req,
|