@runpod/mcp-server 4.3.0 → 4.5.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/dist/http.cjs CHANGED
@@ -22,6 +22,7 @@ var http_exports = {};
22
22
  __export(http_exports, {
23
23
  bodyIsCheckable: () => bodyIsCheckable,
24
24
  defaultCredentialChecker: () => defaultCredentialChecker,
25
+ extractBearerToken: () => extractBearerToken,
25
26
  getBaseUrl: () => getBaseUrl,
26
27
  handleMcpRequest: () => handleMcpRequest,
27
28
  parseMcpMessages: () => parseMcpMessages,
@@ -33,6 +34,80 @@ module.exports = __toCommonJS(http_exports);
33
34
  var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
34
35
  var import_types2 = require("@modelcontextprotocol/sdk/types.js");
35
36
 
37
+ // src/alp/sink.ts
38
+ var ALREADY_SUBMITTED_NOTE = "This account has already made this one-time submission; nothing new was stored. Do not call this again on this account \u2014 continue your task.";
39
+ var ALP_REFERRAL_SOURCES = [
40
+ "searched",
41
+ "user_named",
42
+ "recommended",
43
+ "other"
44
+ ];
45
+ var ALP_REFERRAL_USE_CASES = [
46
+ "inference",
47
+ "fine_tuning",
48
+ "training",
49
+ "image_video_generation",
50
+ "dev_environment",
51
+ "batch_processing",
52
+ "other"
53
+ ];
54
+ function isSinkUrl(value) {
55
+ try {
56
+ const url = new URL(value);
57
+ return url.protocol === "https:" && url.hostname.endsWith(".convex.site") && url.pathname === "/alp/submit";
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+ function sinkRouteUrl(submitUrl, path) {
63
+ if (!isSinkUrl(submitUrl)) return null;
64
+ const url = new URL(submitUrl);
65
+ url.pathname = path;
66
+ return url.toString();
67
+ }
68
+
69
+ // src/alp/answered.ts
70
+ var ANSWERED_TIMEOUT_MS = 2500;
71
+ async function hasAnswered(identity, route, opts = {}) {
72
+ const env = opts.env ?? process.env;
73
+ const submitUrl = env.ALP_SINK_URL;
74
+ const sinkSecret = env.ALP_SINK_SECRET;
75
+ if (!identity || !submitUrl || !sinkSecret) return null;
76
+ const url = sinkRouteUrl(submitUrl, "/alp/answered");
77
+ if (!url) {
78
+ console.warn("alp_sink_misconfigured");
79
+ return null;
80
+ }
81
+ try {
82
+ const sinkFetch = opts.sinkFetch ?? fetch;
83
+ const response = await sinkFetch(url, {
84
+ method: "POST",
85
+ headers: {
86
+ "Content-Type": "application/json",
87
+ "X-ALP-Secret": sinkSecret
88
+ },
89
+ // The resolved account id and the route, nothing from any request body.
90
+ body: JSON.stringify({ identity, route }),
91
+ signal: AbortSignal.timeout(ANSWERED_TIMEOUT_MS)
92
+ });
93
+ if (!response.ok) {
94
+ console.warn("alp_answered_sink_error", { status: response.status });
95
+ return null;
96
+ }
97
+ const body = await response.json().catch(() => null);
98
+ if (body?.ok !== true || typeof body.answered !== "boolean") {
99
+ console.warn("alp_answered_sink_unrecognized", {
100
+ status: response.status
101
+ });
102
+ return null;
103
+ }
104
+ return body.answered;
105
+ } catch {
106
+ console.warn("alp_answered_sink_unreachable");
107
+ return null;
108
+ }
109
+ }
110
+
36
111
  // src/specgen/server.ts
37
112
  var import_server = require("@modelcontextprotocol/sdk/server/index.js");
38
113
  var import_types = require("@modelcontextprotocol/sdk/types.js");
@@ -358,6 +433,7 @@ function coerceScalars(value, schema, defs, depth = 0) {
358
433
  var generatedTools = [
359
434
  {
360
435
  name: "create-cluster",
436
+ title: "Create cluster",
361
437
  operationId: "createCluster",
362
438
  description: "Create an instant cluster (multiple pods with high-speed interconnect). BILLABLE from creation for every pod in the cluster: it rents compute.podCount identical pods at once, each with compute.gpuCountPerPod GPUs of compute.gpuTypeId \u2014 state the hourly price for the total GPU count (podCount * gpuCountPerPod, priced from the catalog tools) and create only after the user's explicit go on that quoted shape and total hourly price. Verify GPU stock first via the catalog tools. The compute shape and type are fixed at creation; only the name can be changed later. The container configuration applies to every pod, and templateId is the only private-image path \u2014 a bare registry property is rejected. Delete with delete-cluster when finished.",
363
439
  method: "POST",
@@ -595,6 +671,7 @@ var generatedTools = [
595
671
  },
596
672
  {
597
673
  name: "create-delegation",
674
+ title: "Create ECR delegation",
598
675
  operationId: "createDelegation",
599
676
  description: "Register an ECR delegation",
600
677
  method: "POST",
@@ -640,6 +717,7 @@ var generatedTools = [
640
717
  },
641
718
  {
642
719
  name: "create-endpoint",
720
+ title: "Create endpoint",
643
721
  operationId: "createEndpoint",
644
722
  description: "Create a serverless endpoint. BILLABLE: state the worker's hourly price (from the catalog tools) before creating. Worker hardware is one of two choices: the gpu object (gpu.pools takes serverless pool ids from the catalog, e.g. ADA_24, never display names; gpu.count sets GPUs per worker) or the cpu array of CPU flavor configurations for a CPU endpoint. type and scaling are required and fix the request-routing model. Workers cold-start on the first job: expect minutes, poll with get-job-status wait.",
645
723
  method: "POST",
@@ -1085,6 +1163,7 @@ var generatedTools = [
1085
1163
  },
1086
1164
  {
1087
1165
  name: "create-global-volume",
1166
+ title: "Create global volume",
1088
1167
  operationId: "createGlobalVolume",
1089
1168
  description: "Create a global volume. Provisions a new global volume \u2014 storage that mounts into workloads in any data center. Only a `name` is required; storage is provisioned for you. Accounts have a cap on how many global volumes they can hold; creating one past it fails with `400`. Returns `201` with the created global volume, including its assigned `id`.",
1090
1169
  method: "POST",
@@ -1126,6 +1205,7 @@ var generatedTools = [
1126
1205
  },
1127
1206
  {
1128
1207
  name: "create-network-volume",
1208
+ title: "Create network volume",
1129
1209
  operationId: "createNetworkVolume",
1130
1210
  description: "Create a network volume. Provisions a new network volume \u2014 persistent, network-attached storage that can be mounted into pods and serverless workers. Required inputs are `name`, `size` (in GB), and `dataCenter`; an optional `type` selects the storage tier and is immutable after creation. See `CreateNetworkVolumeRequest` for the size bounds and tier options. This creates a billable persistent resource that keeps incurring storage charges until it is deleted. Returns `201` with the created network volume, including its assigned `id`.",
1131
1211
  method: "POST",
@@ -1191,6 +1271,7 @@ var generatedTools = [
1191
1271
  },
1192
1272
  {
1193
1273
  name: "create-pod",
1274
+ title: "Create Pod",
1194
1275
  operationId: "createPod",
1195
1276
  description: 'Rent a GPU/CPU pod. BILLABLE from creation until stop/terminate: state the hourly price before creating. GPU selection is the gpu object: gpu.id takes a catalog GPU type id (e.g. "NVIDIA GeForce RTX 4090"), verified via list-gpu-types, and gpu.count sets the GPU count; cloud/data-center choice constrains stock.',
1196
1277
  method: "POST",
@@ -1558,6 +1639,7 @@ var generatedTools = [
1558
1639
  },
1559
1640
  {
1560
1641
  name: "create-registry",
1642
+ title: "Create registry credential",
1561
1643
  operationId: "createRegistry",
1562
1644
  description: "Create a container registry credential. Stores credentials for a private container registry. Credentials are write-only.",
1563
1645
  method: "POST",
@@ -1609,6 +1691,7 @@ var generatedTools = [
1609
1691
  },
1610
1692
  {
1611
1693
  name: "create-secret",
1694
+ title: "Create secret",
1612
1695
  operationId: "createSecret",
1613
1696
  description: "Create a secret. Stores a new account-scoped encrypted string. `name` must be unique across the account's secrets and is immutable; `value` is write-only and can never be read back through the API. Use the secret from pods, serverless endpoints, and templates by setting an environment variable's value to `{{ RUNPOD_SECRET_<name> }}` \u2014 Runpod substitutes the stored value when the pod or worker boots. Returns `201` with the created secret's metadata, or `409` when the name is already taken.",
1614
1697
  method: "POST",
@@ -1663,6 +1746,7 @@ var generatedTools = [
1663
1746
  },
1664
1747
  {
1665
1748
  name: "create-template",
1749
+ title: "Create template",
1666
1750
  operationId: "createTemplate",
1667
1751
  description: "Create a template. Creates a reusable container-configuration preset \u2014 image, disk, ports, env, registry, and mount settings \u2014 for pods and serverless endpoints. Pass its ID as `templateId` to `createPod` or `createEndpoint`, or spread its fields into the request body directly. Returns the created template.",
1668
1752
  method: "POST",
@@ -1864,6 +1948,7 @@ var generatedTools = [
1864
1948
  },
1865
1949
  {
1866
1950
  name: "delete-cluster",
1951
+ title: "Delete cluster",
1867
1952
  operationId: "deleteCluster",
1868
1953
  description: "Delete a cluster permanently: it terminates every member pod. Verify the id with get-cluster, and list-cluster-pods for what the delete takes down, before deleting; delete only clusters your own tool calls created in this conversation, or the one the user asked you to delete by id \u2014 a name found by listing is not attribution.",
1869
1954
  method: "DELETE",
@@ -1894,6 +1979,7 @@ var generatedTools = [
1894
1979
  },
1895
1980
  {
1896
1981
  name: "delete-endpoint",
1982
+ title: "Delete endpoint",
1897
1983
  operationId: "deleteEndpoint",
1898
1984
  description: "Delete a serverless endpoint permanently and immediately. Verify the id belongs to the intended endpoint (get-endpoint) before deleting; delete only endpoints your own tool calls created in this conversation, or the one the user asked you to delete by id \u2014 a name found by listing is not attribution.",
1899
1985
  method: "DELETE",
@@ -1924,6 +2010,7 @@ var generatedTools = [
1924
2010
  },
1925
2011
  {
1926
2012
  name: "delete-global-volume",
2013
+ title: "Delete global volume",
1927
2014
  operationId: "deleteGlobalVolume",
1928
2015
  description: "Delete a global volume. Permanently deletes a global volume and its stored data. Fails with `409` while the volume is still mounted: terminate every pod that mounts it (stopped pods count) and detach it from every serverless endpoint first. Nothing is detached or terminated on your behalf.",
1929
2016
  method: "DELETE",
@@ -1954,6 +2041,7 @@ var generatedTools = [
1954
2041
  },
1955
2042
  {
1956
2043
  name: "delete-network-volume",
2044
+ title: "Delete network volume",
1957
2045
  operationId: "deleteNetworkVolume",
1958
2046
  description: "Delete a network volume. Permanently deletes a network volume and releases its storage.",
1959
2047
  method: "DELETE",
@@ -1984,6 +2072,7 @@ var generatedTools = [
1984
2072
  },
1985
2073
  {
1986
2074
  name: "delete-pod",
2075
+ title: "Delete Pod",
1987
2076
  operationId: "deletePod",
1988
2077
  description: "Terminate a pod permanently \u2014 its container disk is lost. Distinguish from stopping (pod-action stop) which releases the GPU but keeps volume data. Verify the id with get-pod first; terminate only pods your own tool calls created in this conversation, or the one the user asked you to terminate by id \u2014 a name found by listing is not attribution.",
1989
2078
  method: "DELETE",
@@ -2014,6 +2103,7 @@ var generatedTools = [
2014
2103
  },
2015
2104
  {
2016
2105
  name: "delete-registry",
2106
+ title: "Delete registry credential",
2017
2107
  operationId: "deleteRegistry",
2018
2108
  description: "Delete a container registry credential. Permanently deletes a container registry credential by ID. Rejected if any pod currently uses this credential to pull its image. Templates that reference it are not part of that check \u2014 they silently lose the reference (`registry` becomes null) instead of blocking the delete.",
2019
2109
  method: "DELETE",
@@ -2043,6 +2133,7 @@ var generatedTools = [
2043
2133
  },
2044
2134
  {
2045
2135
  name: "delete-secret",
2136
+ title: "Delete secret",
2046
2137
  operationId: "deleteSecret",
2047
2138
  description: "Delete a secret permanently. Environment variables referencing its name ({{ RUNPOD_SECRET_<name> }}) stop resolving at the next pod or worker boot; running instances keep the value they started with. Verify the id with get-secret first; delete only secrets your own tool calls created in this conversation, or the one the user asked you to delete by id \u2014 a name found by listing is not attribution.",
2048
2139
  method: "DELETE",
@@ -2073,6 +2164,7 @@ var generatedTools = [
2073
2164
  },
2074
2165
  {
2075
2166
  name: "delete-template",
2167
+ title: "Delete template",
2076
2168
  operationId: "deleteTemplate",
2077
2169
  description: "Delete a template permanently. Verify with get-template first; a template referenced by a live endpoint should not be deleted.",
2078
2170
  method: "DELETE",
@@ -2102,6 +2194,7 @@ var generatedTools = [
2102
2194
  },
2103
2195
  {
2104
2196
  name: "get-cluster",
2197
+ title: "Get cluster",
2105
2198
  operationId: "getCluster",
2106
2199
  description: "Get a cluster. Returns a single cluster by ID. The pods field is an aggregate summary (total + count by status); fetch the member pods themselves from /v2/clusters/{id}/pods.",
2107
2200
  method: "GET",
@@ -2132,6 +2225,7 @@ var generatedTools = [
2132
2225
  },
2133
2226
  {
2134
2227
  name: "get-cpu-type",
2228
+ title: "Get CPU type",
2135
2229
  operationId: "getCpuType",
2136
2230
  description: "Get a CPU type. Returns a single CPU type with pricing. Availability details are included only when requested with include=AVAILABILITY, which requires `product` \u2014 stock differs by product context.",
2137
2231
  method: "GET",
@@ -2208,6 +2302,7 @@ var generatedTools = [
2208
2302
  },
2209
2303
  {
2210
2304
  name: "get-data-center",
2305
+ title: "Get data center",
2211
2306
  operationId: "getDataCenter",
2212
2307
  description: "Get a data center. Returns a single data center. Availability is included only when requested with include=GPU_AVAILABILITY or include=CPU_AVAILABILITY.",
2213
2308
  method: "GET",
@@ -2257,6 +2352,7 @@ var generatedTools = [
2257
2352
  },
2258
2353
  {
2259
2354
  name: "get-endpoint",
2355
+ title: "Get endpoint",
2260
2356
  operationId: "getEndpoint",
2261
2357
  description: "Get a serverless endpoint. Returns a single serverless endpoint by ID.",
2262
2358
  method: "GET",
@@ -2287,6 +2383,7 @@ var generatedTools = [
2287
2383
  },
2288
2384
  {
2289
2385
  name: "get-endpoint-build",
2386
+ title: "Get endpoint build",
2290
2387
  operationId: "getEndpointBuild",
2291
2388
  description: "Get a serverless endpoint build. Returns one of the endpoint's GitHub builds by id, regardless of age \u2014 no need to page through `GET /v2/serverless/{id}/builds` to reach it.",
2292
2389
  method: "GET",
@@ -2325,6 +2422,7 @@ var generatedTools = [
2325
2422
  },
2326
2423
  {
2327
2424
  name: "get-global-volume",
2425
+ title: "Get global volume",
2328
2426
  operationId: "getGlobalVolume",
2329
2427
  description: "Get a global volume. Returns a single global volume by ID.",
2330
2428
  method: "GET",
@@ -2355,6 +2453,7 @@ var generatedTools = [
2355
2453
  },
2356
2454
  {
2357
2455
  name: "get-gpu-type",
2456
+ title: "Get GPU type",
2358
2457
  operationId: "getGpuType",
2359
2458
  description: "Get a GPU type. Returns a single GPU type with pricing. Availability details are included only when requested with include=AVAILABILITY, which requires `product` \u2014 stock differs by product context.",
2360
2459
  method: "GET",
@@ -2479,6 +2578,7 @@ var generatedTools = [
2479
2578
  },
2480
2579
  {
2481
2580
  name: "get-network-volume",
2581
+ title: "Get network volume",
2482
2582
  operationId: "getNetworkVolume",
2483
2583
  description: "Get a network volume. Returns a single network volume by ID.",
2484
2584
  method: "GET",
@@ -2509,6 +2609,7 @@ var generatedTools = [
2509
2609
  },
2510
2610
  {
2511
2611
  name: "get-organization",
2612
+ title: "Get organization",
2512
2613
  operationId: "getOrganization",
2513
2614
  description: "Get your current organization. Returns the authenticated caller's current organization. Organization context comes from the caller's membership; no organization selector is required. Returns 404 when the caller does not belong to an organization, or 403 when organizations are unavailable to the caller.",
2514
2615
  method: "GET",
@@ -2528,6 +2629,7 @@ var generatedTools = [
2528
2629
  },
2529
2630
  {
2530
2631
  name: "get-pod",
2632
+ title: "Get Pod",
2531
2633
  operationId: "getPod",
2532
2634
  description: "Get a pod. Returns a single pod by ID.",
2533
2635
  method: "GET",
@@ -2558,6 +2660,7 @@ var generatedTools = [
2558
2660
  },
2559
2661
  {
2560
2662
  name: "get-registry",
2663
+ title: "Get registry credential",
2561
2664
  operationId: "getRegistry",
2562
2665
  description: "Get a container registry credential. Returns a single container registry credential by ID. `username` and `password` are never included in the response \u2014 credentials are write-only, matching `createRegistry`.",
2563
2666
  method: "GET",
@@ -2587,6 +2690,7 @@ var generatedTools = [
2587
2690
  },
2588
2691
  {
2589
2692
  name: "get-secret",
2693
+ title: "Get secret",
2590
2694
  operationId: "getSecret",
2591
2695
  description: "Get a secret. Returns a single secret's metadata by ID. The value is write-only and never returned.",
2592
2696
  method: "GET",
@@ -2617,6 +2721,7 @@ var generatedTools = [
2617
2721
  },
2618
2722
  {
2619
2723
  name: "get-ssh-keys",
2724
+ title: "Get SSH keys",
2620
2725
  operationId: "getSshKeys",
2621
2726
  description: "List registered SSH public keys. Returns the account's registered SSH public keys \u2014 the keys provisioned into pods created with `startSsh` and used to authenticate the SSH connections reported in a pod's `ssh` block.",
2622
2727
  method: "GET",
@@ -2636,6 +2741,7 @@ var generatedTools = [
2636
2741
  },
2637
2742
  {
2638
2743
  name: "get-template",
2744
+ title: "Get template",
2639
2745
  operationId: "getTemplate",
2640
2746
  description: "Get a template. Returns the full configuration of a single template by ID. Serves both templates you own and public catalog templates \u2014 everything you can read. Updates and deletes remain restricted to templates you own.",
2641
2747
  method: "GET",
@@ -2665,6 +2771,7 @@ var generatedTools = [
2665
2771
  },
2666
2772
  {
2667
2773
  name: "list-billing",
2774
+ title: "List billing",
2668
2775
  operationId: "listBilling",
2669
2776
  description: "Get aggregated billing history. Returns time-bucketed total spend across all billable Runpod resources for the authenticated user. Use startTime/endTime with bucketSize for an explicit range, or lastN with bucketSize for the most recent buckets. Each record reports one bucket's total plus pod, serverless, storage, public endpoint, and Instant Cluster cost components. The metadata block echoes the resolved query window, record count, and totals across all returned buckets.",
2670
2777
  method: "GET",
@@ -2737,6 +2844,7 @@ var generatedTools = [
2737
2844
  },
2738
2845
  {
2739
2846
  name: "list-cluster-billing",
2847
+ title: "List cluster billing",
2740
2848
  operationId: "listClusterBilling",
2741
2849
  description: "Get cluster billing history. Returns Cluster billing history for the authenticated user, split into time buckets by startTime/endTime with bucketSize or by lastN recent buckets. Use clusterId to filter to one cluster; without it, records are emitted per cluster per bucket. Each record includes GPU compute, disk, inter-node networking, and total amounts. Clusters are GPU-only, so no CPU cost component is returned.",
2742
2850
  method: "GET",
@@ -2817,6 +2925,7 @@ var generatedTools = [
2817
2925
  },
2818
2926
  {
2819
2927
  name: "list-cluster-pods",
2928
+ title: "List cluster Pods",
2820
2929
  operationId: "listClusterPods",
2821
2930
  description: "List a cluster's pods. Returns the full member pods of a cluster. The cluster summary (`GET /v2/clusters/{id}`) carries only aggregate pod counts; this endpoint returns each member as a complete Pod object.",
2822
2931
  method: "GET",
@@ -2847,6 +2956,7 @@ var generatedTools = [
2847
2956
  },
2848
2957
  {
2849
2958
  name: "list-clusters",
2959
+ title: "List clusters",
2850
2960
  operationId: "listClusters",
2851
2961
  description: "List clusters. Returns clusters owned by the authenticated user, cursor-paginated newest-first; an omitted `limit` defaults to 1000. Follow `pagination.nextCursor` until `hasNextPage` is false.",
2852
2962
  method: "GET",
@@ -2887,6 +2997,7 @@ var generatedTools = [
2887
2997
  },
2888
2998
  {
2889
2999
  name: "list-cpu-types",
3000
+ title: "List CPU types",
2890
3001
  operationId: "listCpuTypes",
2891
3002
  description: "List CPU types. Returns available CPU flavors. Availability is included only when requested with include=AVAILABILITY, which requires `product` \u2014 stock differs by product context.",
2892
3003
  method: "GET",
@@ -2955,6 +3066,7 @@ var generatedTools = [
2955
3066
  },
2956
3067
  {
2957
3068
  name: "list-data-centers",
3069
+ title: "List data centers",
2958
3070
  operationId: "listDataCenters",
2959
3071
  description: "List data centers. Returns available data center locations with region, compliance, supported network volume tiers, and global networking support. Use include=GPU_AVAILABILITY or include=CPU_AVAILABILITY to add per-resource availability arrays to each data center. The regions, networkVolumeTypes, compliance, and globalNetwork query parameters filter the list before it is returned.",
2960
3072
  method: "GET",
@@ -3082,6 +3194,7 @@ var generatedTools = [
3082
3194
  },
3083
3195
  {
3084
3196
  name: "list-delegations",
3197
+ title: "List ECR delegations",
3085
3198
  operationId: "listDelegations",
3086
3199
  description: "List all ECR delegations",
3087
3200
  method: "GET",
@@ -3101,6 +3214,7 @@ var generatedTools = [
3101
3214
  },
3102
3215
  {
3103
3216
  name: "list-endpoint-billing",
3217
+ title: "List endpoint billing",
3104
3218
  operationId: "listEndpointBilling",
3105
3219
  description: "Get public endpoint billing history. Returns Runpod public endpoint billing history for the authenticated user, split into time buckets by startTime/endTime with bucketSize or by lastN recent buckets. Each record reports the endpoint total for one bucket, and metadata echoes the resolved query window, record count, and total endpoint amount across all returned records.",
3106
3220
  method: "GET",
@@ -3173,6 +3287,7 @@ var generatedTools = [
3173
3287
  },
3174
3288
  {
3175
3289
  name: "list-endpoint-builds",
3290
+ title: "List endpoint builds",
3176
3291
  operationId: "listEndpointBuilds",
3177
3292
  description: "List serverless endpoint builds. Returns the endpoint's GitHub build history, newest first (Runpod GitHub-build integration), cursor-paginated; an omitted `limit` defaults to 100, so a bare request returns at most the 100 most recent builds. Any build can also be fetched by id via `GET /v2/serverless/{id}/builds/{buildId}`. Stream a build's logs via `/v2/serverless/{id}/builds/{buildId}/logs`.",
3178
3293
  method: "GET",
@@ -3222,6 +3337,7 @@ var generatedTools = [
3222
3337
  },
3223
3338
  {
3224
3339
  name: "list-endpoint-releases",
3340
+ title: "List endpoint releases",
3225
3341
  operationId: "listEndpointReleases",
3226
3342
  description: "List serverless endpoint releases. Returns the endpoint's release history (newest first) plus a rollout summary of how many workers are running the current version. Each release is a versioned configuration snapshot with a `diff` of what changed; build-driven releases carry a `buildId` (fetch build detail via the builds sub-routes). Releases are cursor-paginated newest-first; an omitted `limit` defaults to 1000. The rollout summary always describes the endpoint's current state, independent of the page requested.",
3227
3343
  method: "GET",
@@ -3271,6 +3387,7 @@ var generatedTools = [
3271
3387
  },
3272
3388
  {
3273
3389
  name: "list-endpoint-workers",
3390
+ title: "List endpoint workers",
3274
3391
  operationId: "listEndpointWorkers",
3275
3392
  description: "List serverless endpoint workers. Lists the active workers for a serverless endpoint. **Returns.** A `200` with a `ListEndpointWorkersResponse`: a `workers` array (one entry per active worker, each carrying its `id`, `status`, and runtime details) plus a `summary` of worker counts grouped by status. Only currently active workers are included; scaled-down workers are not returned. **How `status` is determined.** Each worker's `status` is derived by reconciling the worker pod's lifecycle status with the endpoint's live job-queue view (which workers are actively serving requests). When the job-queue view is unavailable, the response degrades gracefully: the shape is unchanged, but each `status` and the summary counts fall back to pod lifecycle alone.",
3276
3393
  method: "GET",
@@ -3301,6 +3418,7 @@ var generatedTools = [
3301
3418
  },
3302
3419
  {
3303
3420
  name: "list-global-volumes",
3421
+ title: "List global volumes",
3304
3422
  operationId: "listGlobalVolumes",
3305
3423
  description: "List global volumes. Returns global volumes owned by the authenticated user, cursor-paginated newest-first; an omitted `limit` defaults to 1000. Follow `pagination.nextCursor` until `hasNextPage` is false.",
3306
3424
  method: "GET",
@@ -3341,6 +3459,7 @@ var generatedTools = [
3341
3459
  },
3342
3460
  {
3343
3461
  name: "list-gpu-types",
3462
+ title: "List GPU types",
3344
3463
  operationId: "listGpuTypes",
3345
3464
  description: 'List GPU types. Returns available GPU types with pricing. Availability is included only when requested with include=AVAILABILITY, which requires `product` \u2014 stock differs by product context. With countryCodes, the list is narrowed to GPU types deployable in those countries, so "this geography + this chip" resolves in one read.',
3346
3465
  method: "GET",
@@ -3457,6 +3576,7 @@ var generatedTools = [
3457
3576
  },
3458
3577
  {
3459
3578
  name: "list-network-volume-billing",
3579
+ title: "List network volume billing",
3460
3580
  operationId: "listNetworkVolumeBilling",
3461
3581
  description: "Get network volume billing history. Returns network volume billing history for the authenticated user, split into time buckets by startTime/endTime with bucketSize or by lastN recent buckets. Use networkVolumeId to filter to one volume; without it, records are emitted per volume per bucket. Each record includes standard storage, high-performance storage, and total amounts, while metadata reports the resolved query, distinct volume count, and totals across the returned records.",
3462
3582
  method: "GET",
@@ -3537,6 +3657,7 @@ var generatedTools = [
3537
3657
  },
3538
3658
  {
3539
3659
  name: "list-network-volumes",
3660
+ title: "List network volumes",
3540
3661
  operationId: "listNetworkVolumes",
3541
3662
  description: "List network volumes. Returns network volumes owned by the authenticated user, cursor-paginated newest-first; an omitted `limit` defaults to 1000. Follow `pagination.nextCursor` until `hasNextPage` is false.",
3542
3663
  method: "GET",
@@ -3577,6 +3698,7 @@ var generatedTools = [
3577
3698
  },
3578
3699
  {
3579
3700
  name: "list-pod-billing",
3701
+ title: "List Pod billing",
3580
3702
  operationId: "listPodBilling",
3581
3703
  description: "Get pod billing history. Returns pod-only billing detail for the authenticated user, split into time buckets by startTime/endTime with bucketSize or by lastN recent buckets. Use podId to narrow the response to one GPU or CPU pod; without it, records are emitted per pod per bucket. Each record includes podId, GPU, CPU, disk, and total amounts, while metadata echoes the resolved query and totals across the pod records. Use listBilling when you need aggregate spend across every billable resource family.",
3582
3704
  method: "GET",
@@ -3657,6 +3779,7 @@ var generatedTools = [
3657
3779
  },
3658
3780
  {
3659
3781
  name: "list-pods",
3782
+ title: "List Pods",
3660
3783
  operationId: "listPods",
3661
3784
  description: "List pods. Returns pods owned by the authenticated user. Cluster member pods are excluded by default; set `includeClusterPods=true` to include them (each carries a non-null `cluster` membership block). Results are cursor-paginated newest-first; an omitted `limit` defaults to 1000. The cluster-member exclusion is applied before the page is cut, so a page holds `limit` pods whenever that many remain. Follow `pagination.nextCursor` until `hasNextPage` is false.",
3662
3785
  method: "GET",
@@ -3706,6 +3829,7 @@ var generatedTools = [
3706
3829
  },
3707
3830
  {
3708
3831
  name: "list-public-templates",
3832
+ title: "List public templates",
3709
3833
  operationId: "listPublicTemplates",
3710
3834
  description: "List public templates. Returns the public template catalog. `source` selects which slice: `official` (the default) is Runpod-curated templates, `verified` is community templates Runpod has verified, and `community` is everything else other users have shared publicly. Both pod and serverless templates appear \u2014 use each entry's `serverless` flag to tell them apart. `registry` is always null for templates you don't own. Your own templates (public or private) are managed under `/v2/templates`; fetch any individual template \u2014 catalog or owned \u2014 via `/v2/templates/{id}`. At most 100 templates are returned. Cursor pagination is not yet supported here; `pagination` is always the exhausted marker (`nextCursor: null`, `hasNextPage: false`).",
3711
3835
  method: "GET",
@@ -3737,6 +3861,7 @@ var generatedTools = [
3737
3861
  },
3738
3862
  {
3739
3863
  name: "list-registries",
3864
+ title: "List registry credentials",
3740
3865
  operationId: "listRegistries",
3741
3866
  description: "List container registries. Returns all container registry credentials owned by the authenticated user.",
3742
3867
  method: "GET",
@@ -3756,6 +3881,7 @@ var generatedTools = [
3756
3881
  },
3757
3882
  {
3758
3883
  name: "list-secrets",
3884
+ title: "List secrets",
3759
3885
  operationId: "listSecrets",
3760
3886
  description: "List secrets. Returns the account's secrets \u2014 encrypted strings referenced from pod, serverless, and template environment variables with the `{{ RUNPOD_SECRET_<name> }}` placeholder syntax, substituted with the secret's value when the pod or worker boots. Secret values are write-only and are never returned.",
3761
3887
  method: "GET",
@@ -3785,6 +3911,7 @@ var generatedTools = [
3785
3911
  },
3786
3912
  {
3787
3913
  name: "list-serverless-billing",
3914
+ title: "List Serverless billing",
3788
3915
  operationId: "listServerlessBilling",
3789
3916
  description: "Get serverless billing history, split into time buckets. SIZE WARNING: with defaults this emits one record per endpoint PER BUCKET \u2014 on an account with many endpoints the response overflows the tool-result limit and becomes unusable. Always pass lastN: 1 (or a bucketSize spanning the whole window) for one record per endpoint, and serverlessId when you need one endpoint. Distinct from pod billing, which covers standalone pods.",
3790
3917
  method: "GET",
@@ -3865,6 +3992,7 @@ var generatedTools = [
3865
3992
  },
3866
3993
  {
3867
3994
  name: "pod-action",
3995
+ title: "Start, stop, restart, or terminate a Pod",
3868
3996
  operationId: "podAction",
3869
3997
  description: "Trigger a pod state transition. Triggers a state transition on a pod. Send a JSON body with a single `action` field, e.g. `{ \"action\": \"stop\" }`. Valid actions: - `start` \u2014 boot a stopped pod (`EXITED` or `ERROR`) back toward `RUNNING`. - `stop` \u2014 stop a running or provisioning pod, releasing GPU/CPU compute while keeping its disk. The pod moves to `EXITED`. - `restart` \u2014 restart a `RUNNING` pod's container in place. - `terminate` \u2014 permanently delete the pod and release its resources (equivalent to `deletePod`). Which actions are valid depends on the pod's current status, and the currently permitted set is published in the pod's `actions` field: `RUNNING` allows `stop`/`restart`/`terminate`; `EXITED` and `ERROR` allow `start`/`terminate`; `PROVISIONING` and `STARTING` allow `stop`/`terminate`. `start`, `stop`, and `restart` return `200` with the updated pod. `terminate` returns `204` with no body. Requesting an action that is not valid for the pod's current status returns `409`.",
3870
3998
  method: "POST",
@@ -3914,6 +4042,7 @@ var generatedTools = [
3914
4042
  },
3915
4043
  {
3916
4044
  name: "revoke-delegation",
4045
+ title: "Revoke ECR delegation",
3917
4046
  operationId: "revokeDelegation",
3918
4047
  description: "Revoke an ECR delegation",
3919
4048
  method: "DELETE",
@@ -3943,6 +4072,7 @@ var generatedTools = [
3943
4072
  },
3944
4073
  {
3945
4074
  name: "update-cluster",
4075
+ title: "Update cluster",
3946
4076
  operationId: "updateCluster",
3947
4077
  description: "Rename a cluster. Renames a cluster. This endpoint only changes the cluster name \u2014 compute shape, type, and container configuration are fixed at creation and cannot be updated.",
3948
4078
  method: "PATCH",
@@ -3991,6 +4121,7 @@ var generatedTools = [
3991
4121
  },
3992
4122
  {
3993
4123
  name: "update-endpoint",
4124
+ title: "Update endpoint",
3994
4125
  operationId: "updateEndpoint",
3995
4126
  description: "Partially update a serverless endpoint (PATCH): only fields present in the body change, omitted ones are untouched, with one exception. DESTRUCTIVE GOTCHA: gpu.pools and gpu.excludedTypes are a single selection, so sending pools without excludedTypes clears any exclusions pinned elsewhere (the console or set-endpoint-gpus) \u2014 read the endpoint first with get-endpoint and resend excludedTypes if you still want them. An exclusion naming a type outside pools returns 400. A CUDA-only or count-only change does not need pools at all, so omit it unless you mean to replace the selection. Mutable fields: name, gpu, cpu, workers (min/max/idleTimeout), scaling (type plus queueDelay or requestCount), dataCenterIds, networkVolumes, timeout, flashboot, and the container settings (image, args, disk, ports, env, registry). cpu completely replaces a CPU endpoint's flavor selection. Compute family is immutable: gpu on a CPU endpoint, cpu on a GPU one, or both, returns 400. Effect timing differs by field \u2014 workers/scaling/timeout reach the autoscaler promptly, while container changes (image, env) cut a new release that rolls out as workers cycle, so in-flight workers keep the previous version. Track rollout with list-endpoint-releases.",
3996
4127
  method: "PATCH",
@@ -4381,6 +4512,7 @@ var generatedTools = [
4381
4512
  },
4382
4513
  {
4383
4514
  name: "update-global-volume",
4515
+ title: "Update global volume",
4384
4516
  operationId: "updateGlobalVolume",
4385
4517
  description: "Update a global volume. Updates mutable fields on a global volume. Only provided fields are changed.",
4386
4518
  method: "PATCH",
@@ -4432,6 +4564,7 @@ var generatedTools = [
4432
4564
  },
4433
4565
  {
4434
4566
  name: "update-network-volume",
4567
+ title: "Update network volume",
4435
4568
  operationId: "updateNetworkVolume",
4436
4569
  description: "Update a network volume. Updates mutable fields on a network volume. Only provided fields are changed. Note: `size` may only increase; attempts to reduce size will be rejected.",
4437
4570
  method: "PATCH",
@@ -4488,6 +4621,7 @@ var generatedTools = [
4488
4621
  },
4489
4622
  {
4490
4623
  name: "update-pod",
4624
+ title: "Update Pod",
4491
4625
  operationId: "updatePod",
4492
4626
  description: "Update a pod. Partially updates a pod's configuration. This is a PATCH: only the fields present in the body are changed, and omitted fields are left untouched. Use empty values only when you explicitly mean to clear a field (for example, set `registry` to `null` or set `ports` to `[]`). See `UpdatePodRequest` for the full body. Mutable fields: `name`, `image`, `args`, `disk`, `ports`, `env`, `registry`, `mounts`, `locked`, and `globalNetworking`. Some changes apply immediately while others (e.g. `globalNetworking`) take effect on the pod's next start/restart, as noted on the individual fields. Pods that belong to a Cluster cannot be updated here \u2014 manage them through `/v2/clusters/{id}`. Returns `200` with the full updated pod.",
4493
4627
  method: "PATCH",
@@ -4729,6 +4863,7 @@ var generatedTools = [
4729
4863
  },
4730
4864
  {
4731
4865
  name: "update-secret",
4866
+ title: "Update secret",
4732
4867
  operationId: "updateSecret",
4733
4868
  description: "Update a secret. Rotates a secret's value and/or updates its description. Only the provided fields are changed and at least one field is required; the `name` is immutable. Pods and workers receive the new value at their next boot \u2014 running instances keep the value they were started with. When both fields are sent, the value is applied first, then the description. The two updates are not atomic: if the description update fails after the value was rotated, the response is an error but the new value has already taken effect. Send the fields in separate requests when that partial outcome matters.",
4734
4869
  method: "PATCH",
@@ -4784,6 +4919,7 @@ var generatedTools = [
4784
4919
  },
4785
4920
  {
4786
4921
  name: "update-ssh-keys",
4922
+ title: "Update SSH keys",
4787
4923
  operationId: "updateSshKeys",
4788
4924
  description: "Replace registered SSH public keys. Replaces the account's full set of registered SSH public keys. Existing keys not present in the request are removed; send `[]` to remove all keys. Keys take effect for pods created afterwards with `startSsh` \u2014 running pods are not updated.",
4789
4925
  method: "PUT",
@@ -4830,6 +4966,7 @@ var generatedTools = [
4830
4966
  },
4831
4967
  {
4832
4968
  name: "update-template",
4969
+ title: "Update template",
4833
4970
  operationId: "updateTemplate",
4834
4971
  description: "Update a template. Partially updates a template. This is a PATCH: only the fields present in the body are changed; omitted fields are left untouched. See `UpdateTemplateRequest` for the full body. Mutable fields: `name`, `image`, `args`, `disk`, `ports`, `env`, `registry`, `mounts`, `serverless`, `public`, and `category`. Only the template's owner can update it (authenticated via the request's API key); public catalog templates are readable via GET but return `404` here. Returns `200` with the full updated template. Pods and endpoints already created from this template are not changed retroactively \u2014 the template is a snapshot applied at creation time.",
4835
4972
  method: "PATCH",
@@ -5128,6 +5265,43 @@ function isTimeout(error) {
5128
5265
  const cause = error.cause;
5129
5266
  return cause instanceof Error && (cause.name === "TimeoutError" || cause.name === "AbortError");
5130
5267
  }
5268
+ var UNREACHED_CODES = /* @__PURE__ */ new Set([
5269
+ "ENOTFOUND",
5270
+ "EAI_AGAIN",
5271
+ "ECONNREFUSED",
5272
+ "EHOSTUNREACH",
5273
+ "ENETUNREACH",
5274
+ "UND_ERR_CONNECT_TIMEOUT"
5275
+ ]);
5276
+ var TLS_CODE = /^(?:CERT_|ERR_TLS_|UNABLE_TO_|DEPTH_ZERO_|SELF_SIGNED_)/;
5277
+ function causeCode(cause) {
5278
+ const code = cause?.code;
5279
+ if (typeof code === "string") return code;
5280
+ const inner = cause instanceof AggregateError ? cause.errors[0] : void 0;
5281
+ const innerCode = inner?.code;
5282
+ return typeof innerCode === "string" ? innerCode : void 0;
5283
+ }
5284
+ function networkFailureResult(error) {
5285
+ if (!(error instanceof TypeError)) return void 0;
5286
+ const afterHeaders = error.message === "terminated";
5287
+ if (!afterHeaders && error.message !== "fetch failed") return void 0;
5288
+ const code = causeCode(error.cause);
5289
+ if (!code) return void 0;
5290
+ const unreached = !afterHeaders && (UNREACHED_CODES.has(code) || TLS_CODE.test(code));
5291
+ return {
5292
+ ok: false,
5293
+ status: unreached ? 503 : 502,
5294
+ payload: unreached ? {
5295
+ error: "Could not reach the Runpod API.",
5296
+ cause: code,
5297
+ hint: "The request never reached Runpod, so retrying the same call is safe: retry it once. If it fails again, the network, DNS, proxy or TLS setup on this machine is the likely cause; check https://uptime.runpod.io and report it rather than retrying further."
5298
+ } : {
5299
+ error: "The connection to the Runpod API dropped before the full response arrived.",
5300
+ cause: code,
5301
+ hint: "The request may have been applied. A read can be retried once. For a create, update, delete or job submission, check with the matching get- or list- tool whether it took effect before calling again; retrying blindly can create a duplicate billable resource."
5302
+ }
5303
+ };
5304
+ }
5131
5305
  async function runTool(body) {
5132
5306
  try {
5133
5307
  return await body();
@@ -5149,6 +5323,8 @@ async function runTool(body) {
5149
5323
  }
5150
5324
  };
5151
5325
  }
5326
+ const network = networkFailureResult(error);
5327
+ if (network) return network;
5152
5328
  throw error;
5153
5329
  }
5154
5330
  }
@@ -5219,6 +5395,7 @@ function rankByStock(rows, stats, includeUnavailable) {
5219
5395
  }
5220
5396
  var getCapacity = {
5221
5397
  name: "get-capacity",
5398
+ title: "Get GPU capacity by CUDA version",
5222
5399
  annotations: readOnly,
5223
5400
  description: "GPU capacity across host CUDA versions, as a matrix. Use this to choose an endpoint's allowedCudaVersions/minCudaVersion (or diagnose/widen a capacity-starved one) and to distinguish capacity problems from compatibility problems. Default mode is one call returning, per GPU type, overall stock plus AVAILABLE/UNAVAILABLE per host-reported CUDA version. Pass cudaVersions to deep-probe instead: one stock lookup per listed version, returning graded stock (High/Medium/Low/Out) and the lowest on-demand price per version; a probe that fails transiently reports a per-version error instead of failing the call. Nothing is hidden by default \u2014 set includeUnavailable:false to drop GPUs with no stock on any listed version. Credential-free public catalog data. Stock is live, so page cursors can shift between calls.",
5224
5401
  inputSchema: {
@@ -5420,6 +5597,7 @@ function formatGpuIds(gpu) {
5420
5597
  }
5421
5598
  var setEndpointGpus = {
5422
5599
  name: "set-endpoint-gpus",
5600
+ title: "Set endpoint GPUs",
5423
5601
  annotations: idempotentWrite,
5424
5602
  description: "Set which GPUs a Serverless endpoint's workers run on \u2014 including pinning specific GPU SKUs. Provide either a raw gpuIds string, or pools plus optional excludeGpuTypeIds (GPU type ids from list-gpu-types) and the exclusion string is built for you: a pool allows every SKU in it, and excluding all but one SKU pins that SKU exactly. Updates only GPU settings through REST v2, preserving other endpoint settings. create-endpoint/update-endpoint also support gpu.excludedTypes.",
5425
5603
  inputSchema: {
@@ -5614,6 +5792,7 @@ function randomSuffix() {
5614
5792
  }
5615
5793
  var listHubRepos = {
5616
5794
  name: "list-hub-repos",
5795
+ title: "List Hub repos",
5617
5796
  annotations: readOnly,
5618
5797
  description: "List repos published to the Runpod Hub (prebuilt Serverless workers and Pod templates, e.g. vLLM, ComfyUI). Public catalog \u2014 no auth required. Each result includes the currently listed release with its hubReleaseId and prebuilt image name. Results are sorted by deploy count (most popular first). Set includeConfig:true to also return the release config (hardware requirements and environment-variable schema) \u2014 it is large, so prefer requesting it for a single repo via the repoOwner/searchTerm filters.",
5619
5798
  inputSchema: {
@@ -5716,6 +5895,9 @@ var listHubRepos = {
5716
5895
  };
5717
5896
  var deployHubRepo = {
5718
5897
  name: "deploy-hub-repo",
5898
+ title: "Deploy Hub repo",
5899
+ // Two sequential upstream calls: together they can outlast a client's request timeout.
5900
+ holdsResponse: true,
5719
5901
  annotations: write,
5720
5902
  description: "Deploy a Runpod Hub repo's listed release as a new Serverless endpoint (the same as clicking Deploy on the Hub). Identify the repo by `repo` (\"owner/name\" from list-hub-repos) or by `hubReleaseId`. The release supplies the prebuilt image, container disk, CUDA constraints, and env-var defaults; pass `env` to override or fill in values (required keys without a default must be provided \u2014 check list-hub-repos with includeConfig:true for the schema). GPU selection comes from the release config when it specifies one; otherwise pass gpuIds (GPU pool names, e.g. 'ADA_24' or 'ADA_80_PRO,AMPERE_80'). Uses the authenticated GraphQL API (no REST home for Hub deploys yet).",
5721
5903
  inputSchema: {
@@ -6174,6 +6356,7 @@ function validateWait(wait) {
6174
6356
  }
6175
6357
  var runEndpoint = {
6176
6358
  name: "run-endpoint",
6359
+ title: "Submit Serverless job",
6177
6360
  annotations: write,
6178
6361
  description: "Submit an asynchronous job to a Serverless endpoint. Returns a job ID immediately \u2014 use get-job-status to poll for results (pass its `wait` to block through a cold start instead of tight-looping). Async results are available for 30 minutes after completion.",
6179
6362
  inputSchema: {
@@ -6204,6 +6387,8 @@ var runEndpoint = {
6204
6387
  };
6205
6388
  var runsyncEndpoint = {
6206
6389
  name: "runsync-endpoint",
6390
+ title: "Run Serverless job and wait",
6391
+ holdsResponse: true,
6207
6392
  annotations: write,
6208
6393
  description: "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. The wait parameter extends the server-side wait" + (HOSTED ? ` \u2014 this server runs behind a 60-second gateway deadline, so waits clamp to ${HTTP_LONG_POLL_BUDGET_MS} ms, which is also the effective default. For longer jobs use run-endpoint + get-job-status.` : " to up to 5 minutes (300000 ms); it defaults to 90000 (90 seconds)."),
6209
6394
  inputSchema: {
@@ -6248,6 +6433,8 @@ var runsyncEndpoint = {
6248
6433
  };
6249
6434
  var getJobStatus = {
6250
6435
  name: "get-job-status",
6436
+ title: "Get job status",
6437
+ holdsResponse: true,
6251
6438
  annotations: readOnly,
6252
6439
  description: "Check the status of a Serverless job. Returns the current status and output when complete. Job statuses: IN_QUEUE, IN_PROGRESS, COMPLETED, FAILED, CANCELLED, TIMED_OUT. Pass `wait` (milliseconds, up to " + STATUS_WAIT_MAX_MS + ") to BLOCK server-side, polling until the job reaches a terminal status or the budget expires \u2014 use it to ride out a cold start (a first job on a fresh worker pulls the image and loads the model, commonly 1\u20135+ minutes) in a single call instead of returning on the first IN_QUEUE and tight-looping the tool. Without `wait` it returns the current status immediately (a single check). If the budget expires before a terminal status, it returns the latest non-terminal status with pollingTimedOut:true \u2014 call again (with `wait`) to keep blocking. IMPORTANT: a job stuck IN_QUEUE does not necessarily mean a capacity shortage \u2014 a worker may have spun up and crash-looped. When the job is IN_QUEUE this tool attaches a workerHealth summary and a hint. To dig deeper call list-endpoint-workers, then read stream-worker-logs regardless of the status a worker reports: a container failing to start loops while its worker still shows RUNNING or THROTTLED and unhealthy stays 0, so UNHEALTHY is one symptom of a crash loop and not a test for it. The logs are the authority.",
6253
6440
  inputSchema: {
@@ -6296,6 +6483,8 @@ var getJobStatus = {
6296
6483
  };
6297
6484
  var streamJob = {
6298
6485
  name: "stream-job",
6486
+ title: "Stream job output",
6487
+ holdsResponse: true,
6299
6488
  annotations: readOnly,
6300
6489
  description: `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 ${formatBudget(STREAM_BUDGET_MS)}. 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.`,
6301
6490
  inputSchema: {
@@ -6327,6 +6516,7 @@ var streamJob = {
6327
6516
  };
6328
6517
  var cancelJob = {
6329
6518
  name: "cancel-job",
6519
+ title: "Cancel job",
6330
6520
  annotations: destructive,
6331
6521
  description: "Cancel a Serverless job that is queued or in progress.",
6332
6522
  inputSchema: {
@@ -6352,6 +6542,7 @@ var cancelJob = {
6352
6542
  };
6353
6543
  var retryJob = {
6354
6544
  name: "retry-job",
6545
+ title: "Retry job",
6355
6546
  annotations: write,
6356
6547
  description: "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.",
6357
6548
  inputSchema: {
@@ -6377,6 +6568,7 @@ var retryJob = {
6377
6568
  };
6378
6569
  var endpointHealth = {
6379
6570
  name: "endpoint-health",
6571
+ title: "Get endpoint health",
6380
6572
  annotations: readOnly,
6381
6573
  description: "Get the endpoint-level health rollup for a Serverless endpoint: worker counts by state plus job queue statistics. This is the runtime plane's own /health view and it can lag or disagree with the per-worker truth \u2014 when a job is stuck IN_QUEUE, prefer list-endpoint-workers over this rollup for per-worker state \u2014 but treat neither as the authority on crash loops, because a container failing to start loops while its worker reports RUNNING or THROTTLED and unhealthy stays 0; stream-worker-logs is what settles it, and note that get-job-status already attaches that worker summary and a hint on every IN_QUEUE result. Zero workers here means the endpoint is waiting for GPU capacity.",
6382
6574
  inputSchema: {
@@ -6397,6 +6589,7 @@ var endpointHealth = {
6397
6589
  };
6398
6590
  var purgeEndpointQueue = {
6399
6591
  name: "purge-endpoint-queue",
6592
+ title: "Purge endpoint queue",
6400
6593
  annotations: destructive,
6401
6594
  description: "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.",
6402
6595
  inputSchema: {
@@ -6433,6 +6626,7 @@ var jobTools = [
6433
6626
  // src/specgen/tools/list-endpoints.ts
6434
6627
  var listEndpoints = {
6435
6628
  name: "list-endpoints",
6629
+ title: "List endpoints",
6436
6630
  annotations: readOnly,
6437
6631
  description: "List serverless endpoints owned by the account (trimmed: id, name, image, compute config, worker counts, scaling, data centers per endpoint \u2014 env vars and request URLs are omitted; use get-endpoint for the full object). Returns a page; pass cursor=nextCursor for more.",
6438
6632
  inputSchema: {
@@ -6478,6 +6672,7 @@ var listEndpoints = {
6478
6672
  // src/specgen/tools/list-templates.ts
6479
6673
  var listTemplates = {
6480
6674
  name: "list-templates",
6675
+ title: "List templates",
6481
6676
  annotations: readOnly,
6482
6677
  description: "List templates visible to the account. Returns id, name, image, and serverless flag per template; use get-template for full detail. Returns 20 templates per page by default. To find a template, follow `pagination.nextCursor` until `hasNextPage` is false: a name missing from one page may be on a later one.",
6483
6678
  inputSchema: {
@@ -6528,6 +6723,9 @@ function publicGraphqlBase(env) {
6528
6723
  function authedGraphqlBase(env) {
6529
6724
  return pick(env.RUNPOD_AUTHED_GRAPHQL_URL, "https://api.runpod.io/graphql");
6530
6725
  }
6726
+ function knowledgeSearchBase(env) {
6727
+ return pick(env.KNOWLEDGE_SEARCH_URL, "") || void 0;
6728
+ }
6531
6729
 
6532
6730
  // node_modules/.pnpm/@runpod+typescript-api-sdk@0.3.0/node_modules/@runpod/typescript-api-sdk/dist/index.js
6533
6731
  var PATH_PARAM_RE = /\{[^{}]+\}/g;
@@ -7933,6 +8131,8 @@ function logParams(args) {
7933
8131
  }
7934
8132
  var streamPodLogs = {
7935
8133
  name: "stream-pod-logs",
8134
+ title: "Stream Pod logs",
8135
+ holdsResponse: true,
7936
8136
  annotations: readOnly,
7937
8137
  description: "Read a bounded snapshot of a Pod's logs (container and/or system source). Holds the live stream open for maxWaitMs and returns the parsed log lines; `truncated: true` means the byte cap cut the output. Use `since` to resume from a timestamp.",
7938
8138
  inputSchema: {
@@ -7955,6 +8155,8 @@ var streamPodLogs = {
7955
8155
  };
7956
8156
  var streamWorkerLogs = {
7957
8157
  name: "stream-worker-logs",
8158
+ title: "Stream worker logs",
8159
+ holdsResponse: true,
7958
8160
  annotations: readOnly,
7959
8161
  description: "Read a bounded snapshot of a Serverless worker's logs (container and/or system source). Holds the live stream open for maxWaitMs and returns the parsed log lines; `truncated: true` means the byte cap cut the output. Get worker IDs from list-endpoint-workers. Use this whenever a job stays IN_QUEUE while workers exist, whatever status they report \u2014 a crash-looping container often still shows RUNNING or THROTTLED with unhealthy 0, so these logs, not the worker status, are what identify a crash loop.",
7960
8162
  inputSchema: {
@@ -7996,6 +8198,7 @@ function parseMetadata(metadata) {
7996
8198
  }
7997
8199
  var listPublicEndpoints = {
7998
8200
  name: "list-public-endpoints",
8201
+ title: "List public endpoints",
7999
8202
  annotations: readOnly,
8000
8203
  description: "List Runpod Public Endpoints \u2014 managed, pay-per-use model APIs (text, image, video, audio) that require no deployment. Public catalog, no auth required. Each result includes the endpointId to call with run-endpoint/runsync-endpoint (or via https://api.runpod.ai/v2/{endpointId}), the model name, modality, owner, and pricing. Only live endpoints are returned by default; set includeOffline:true to also list ones that are not currently live.",
8001
8204
  inputSchema: {
@@ -8216,6 +8419,7 @@ var source = {
8216
8419
  };
8217
8420
  var listGuides = {
8218
8421
  name: "list-guides",
8422
+ title: "List guides",
8219
8423
  annotations: bundledRead,
8220
8424
  recordedArguments: ["kind", "query", "concept"],
8221
8425
  description: "List the official Runpod guides: skills (how to work with each tool), reference docs (how Runpod works), and golden paths (live-verified end-to-end walkthroughs such as a dev pod, a Whisper endpoint or a fine-tune-to-serverless pipeline). Read one with read-guide. For a golden path, mcp says whether an agent with only these MCP tools can finish it: full (follow it with tool calls), partial (use it for the plan and facts; name the step the user must run in a terminal), none (reference only). Skills and reference docs have mcp null. concepts lists the concept ids it works with (see lookup-concept). Filter with kind, query or concept.",
@@ -8252,6 +8456,7 @@ var listGuides = {
8252
8456
  };
8253
8457
  var readGuide = {
8254
8458
  name: "read-guide",
8459
+ title: "Read guide",
8255
8460
  annotations: bundledRead,
8256
8461
  recordedArguments: ["id"],
8257
8462
  recordedGuideKind: (args) => knowledge.guides.find((guide) => guide.id === String(args.id).trim())?.kind,
@@ -8291,6 +8496,7 @@ var readGuide = {
8291
8496
  };
8292
8497
  var lookupConcept = {
8293
8498
  name: "lookup-concept",
8499
+ title: "Look up a concept",
8294
8500
  annotations: bundledRead,
8295
8501
  recordedArguments: ["concept"],
8296
8502
  description: 'Look up one Runpod concept (pod, network volume, machine, worker, template, \u2026) by id, name or alias; case, plurals and small typos are tolerated, and `matched_as` shows the key used when the match was not exact. Returns its summary, fields, states, relations and rules, each rule with the public source it comes from, the concepts that point at it, and the linked guides: examples (golden paths that use it, with their mcp level) and docs (skills and reference docs that explain it). Use it for exact facts, for example "is a volume disk tied to one machine" or "what happens to a stopped pod\'s GPU".',
@@ -8344,6 +8550,7 @@ var lookupConcept = {
8344
8550
  };
8345
8551
  var searchConcepts = {
8346
8552
  name: "search-concepts",
8553
+ title: "Search concepts",
8347
8554
  annotations: bundledRead,
8348
8555
  recordedArguments: ["query", "limit"],
8349
8556
  description: 'Search the rules of the Runpod concept graph by words, for example "restart stopped pod no gpu" or "network volume data center". Returns the best-matching rules with their concept and public evidence. Follow up with lookup-concept for the full concept.',
@@ -8435,12 +8642,13 @@ function listedAnnotations(annotations) {
8435
8642
  return idempotentHint ? annotations : listed;
8436
8643
  }
8437
8644
  function listTool(tool) {
8438
- const { name, description, inputSchema } = tool;
8645
+ const { name, title, description, inputSchema } = tool;
8439
8646
  const annotations = listedAnnotations(tool.annotations);
8440
8647
  if (!tool.schemaOnDemand)
8441
- return { name, description, inputSchema, annotations };
8648
+ return { name, title, description, inputSchema, annotations };
8442
8649
  return {
8443
8650
  name,
8651
+ title,
8444
8652
  description: description + ON_DEMAND_POINTER,
8445
8653
  inputSchema: callShape(inputSchema),
8446
8654
  annotations
@@ -8492,6 +8700,7 @@ function schemaType(node, resolve) {
8492
8700
  function createDescribeTool(served) {
8493
8701
  return {
8494
8702
  name: DESCRIBE_TOOL_NAME,
8703
+ title: "Describe tool",
8495
8704
  description: "Return one tool's full definition: its description, complete argument schema (every field, nested objects, constraints) and annotations. Required before the first use of a tool whose listing says its schema is on demand; harmless for any other tool.",
8496
8705
  inputSchema: {
8497
8706
  type: "object",
@@ -8546,7 +8755,7 @@ function words2(name) {
8546
8755
 
8547
8756
  // src/server.ts
8548
8757
  var SERVER_NAME = "Runpod API Server";
8549
- var SERVER_VERSION = true ? "4.3.0" : "dev";
8758
+ var SERVER_VERSION = true ? "4.5.0" : "dev";
8550
8759
 
8551
8760
  // src/specgen/analytics.ts
8552
8761
  var import_node_crypto = require("crypto");
@@ -8646,16 +8855,6 @@ function captureResourceCall(apiKey, event) {
8646
8855
  );
8647
8856
  }
8648
8857
 
8649
- // src/alp/ingest.ts
8650
- function isSinkUrl(value) {
8651
- try {
8652
- const url = new URL(value);
8653
- return url.protocol === "https:" && url.hostname.endsWith(".convex.site") && url.pathname === "/alp/submit";
8654
- } catch {
8655
- return false;
8656
- }
8657
- }
8658
-
8659
8858
  // src/alp/read.ts
8660
8859
  var DEFAULT_JOURNAL_LIMIT = 20;
8661
8860
  var MAX_JOURNAL_LIMIT = 50;
@@ -8684,12 +8883,21 @@ async function submit(ctx, ingestUrl, route, args, extra) {
8684
8883
  tool: args.tool,
8685
8884
  workaround: args.workaround,
8686
8885
  trigger: args.trigger,
8886
+ source: args.source,
8887
+ noContext: args.noContext,
8888
+ useCase: args.useCase,
8687
8889
  ...extra
8688
8890
  }),
8689
8891
  signal: AbortSignal.timeout(1e4)
8690
8892
  });
8691
8893
  const body = await response.json().catch(() => ({}));
8692
- return { recorded: response.ok && body.recorded === true };
8894
+ return {
8895
+ recorded: response.ok && body.recorded === true,
8896
+ // The ingest's own named outcome (already_submitted on a once-per-account
8897
+ // route), carried through so the tool can say it. A string from a 200
8898
+ // only — never an upstream error body.
8899
+ ...response.ok && typeof body.reason === "string" ? { reason: body.reason } : {}
8900
+ };
8693
8901
  } catch {
8694
8902
  return { recorded: false };
8695
8903
  }
@@ -8720,6 +8928,10 @@ async function readJournal(ctx, readUrl, limit) {
8720
8928
  }
8721
8929
  }
8722
8930
  var SEVERITY = ["blocked", "degraded", "cosmetic"];
8931
+ var MODEL_TYPE_FIELD = {
8932
+ type: "string",
8933
+ description: 'Your best identification of the model you are running as, and the product running it if you know that too \u2014 for example "Claude Opus 5 in Claude Code", "GPT-5.6 via Codex", "Grok 4.6 in Cursor". Give your best guess with the word "probably" rather than leaving it empty; an approximate answer is far more useful than none. Optional.'
8934
+ };
8723
8935
  var TOOL_FIELD = {
8724
8936
  type: "string",
8725
8937
  description: "The name of the tool this is about, exactly as it appears in your tool list (for example create-pod). Omit only when no single tool is involved."
@@ -8736,10 +8948,7 @@ var alpInputSchema = (contentDescription, extra = {}) => ({
8736
8948
  description: "One short sentence: what you were trying to accomplish when this came up."
8737
8949
  },
8738
8950
  ...extra,
8739
- modelType: {
8740
- type: "string",
8741
- description: 'Your best identification of the model you are running as, and the product running it if you know that too \u2014 for example "Claude Opus 5 in Claude Code", "GPT-5.6 via Codex", "Grok 4.6 in Cursor". Give your best guess with the word "probably" rather than leaving it empty; an approximate answer is far more useful than none. Optional.'
8742
- }
8951
+ modelType: MODEL_TYPE_FIELD
8743
8952
  },
8744
8953
  required: ["content"]
8745
8954
  });
@@ -8754,6 +8963,7 @@ function createAlpTools(opts) {
8754
8963
  // is ignored, never a 400 that derails the task the agent is actually on.
8755
8964
  lenientArguments: true,
8756
8965
  name: "report_feedback",
8966
+ title: "Report feedback",
8757
8967
  annotations: write,
8758
8968
  description: "Report a problem or friction with Runpod or these tools: a wrong result, a confusing error, a docs gap, an API behavior that surprised you. The report is stored for internal review by Runpod to fix issues \u2014 nothing is returned to you and no follow-up will reach this session. One concrete observation per call; paste the exact failing request/response into content when relevant. Set severity and tool when you can \u2014 they are what lets Runpod sort a blocker from a nit without reading every report. Only for a real observation: never call this to satisfy a requirement to use a tool, and never with placeholder content. Never include API keys or secrets.",
8759
8969
  inputSchema: alpInputSchema(
@@ -8790,6 +9000,7 @@ function createAlpTools(opts) {
8790
9000
  // is ignored, never a 400 that derails the task the agent is actually on.
8791
9001
  lenientArguments: true,
8792
9002
  name: "save_to_journal",
9003
+ title: "Save to journal",
8793
9004
  annotations: write,
8794
9005
  description: "Save something you learned about using Runpod that a future session would benefit from (e.g. 'image X needs CUDA 12.8', 'endpoint type cannot be changed after create'). read_journal returns these entries only to sessions authenticated to this account; entries are not published to other accounts. Runpod stores and reviews submissions to improve agent workflows. Set trigger when you can \u2014 an entry with no stated trigger cannot be surfaced to the session that needs it. Never include API keys or secrets.",
8795
9006
  inputSchema: alpInputSchema(
@@ -8821,6 +9032,7 @@ function createAlpTools(opts) {
8821
9032
  // is ignored, never a 400 that derails the task the agent is actually on.
8822
9033
  lenientArguments: true,
8823
9034
  name: "ask_question",
9035
+ title: "Ask a question",
8824
9036
  annotations: write,
8825
9037
  description: "Record a question about Runpod that you could not answer with the available tools, skills, and docs. NO ANSWER WILL COME BACK \u2014 not now and not later in this session; do not wait, poll, or retry. Questions are collected so Runpod learns what its docs and tools fail to cover. Ask when genuinely stuck (it costs one call and improves what future agents get), then consult list-guides and read-guide and continue with your best judgment. Only for a real question: never call this to satisfy a requirement to use a tool, and never with placeholder content \u2014 if you have no Runpod question, do not call it.",
8826
9038
  inputSchema: alpInputSchema(
@@ -8846,6 +9058,7 @@ function createAlpTools(opts) {
8846
9058
  const readJournalTool = {
8847
9059
  lenientArguments: true,
8848
9060
  name: "read_journal",
9061
+ title: "Read journal",
8849
9062
  annotations: readOnly,
8850
9063
  description: "Read back this account's private journal: the lessons earlier sessions on this account saved with save_to_journal, newest first. Call it once near the start of a Runpod task to pick up what was already learned (an image's CUDA floor, a field that turned out to be immutable, a region with no stock). Entries are scoped to the account behind your API key \u2014 you cannot read any other account's journal, and there is no argument to ask for one. Only call when you have a Runpod task the entries could inform; never to satisfy a requirement to use a tool.",
8851
9064
  inputSchema: {
@@ -8873,7 +9086,233 @@ function createAlpTools(opts) {
8873
9086
  });
8874
9087
  }
8875
9088
  };
8876
- return [reportFeedback, saveToJournal, askQuestion, readJournalTool];
9089
+ const reportHowUserFoundRunpod = {
9090
+ lenientArguments: true,
9091
+ name: "report_how_user_found_runpod",
9092
+ title: "Report how the user found Runpod",
9093
+ annotations: idempotentWrite,
9094
+ description: "Tell Runpod how your user ended up on Runpod \u2014 once per account, from context you already have. Pick the useCase category for the kind of work and the source for how Runpod came up (you searched and found it, they named it, a doc or post recommended it). Do not quote or paraphrase the user's request, and do not include names, models, data, or any project details: the categories are the answer, and content is at most a short generic note in your own words. Never ask the user for this and never hold up their task for it: answer only from what you already know, and if you have no such context, set noContext to true \u2014 that is a useful answer too. Stored for review by Runpod to learn what kinds of work bring people to Runpod; nothing is returned and no follow-up will reach this session. The server keeps one answer per account: when it answers already_submitted, do not call this again on this account. Only for a real answer: never call this to satisfy a requirement to use a tool, and never with placeholder content. Never include API keys or secrets.",
9095
+ inputSchema: {
9096
+ type: "object",
9097
+ properties: {
9098
+ content: {
9099
+ type: "string",
9100
+ description: 'Optional. A short generic description of the kind of task, in your own words, for example "deploy an open-source LLM as an API". Never quote or paraphrase the user, and never include names, data, or project details. Leave it empty when useCase says enough.'
9101
+ },
9102
+ source: {
9103
+ type: "string",
9104
+ enum: [...ALP_REFERRAL_SOURCES],
9105
+ description: "Why Runpod came up: searched (you searched the web or a catalog and Runpod came up), user_named (the user named Runpod themselves), recommended (a doc, post, README or another tool pointed to Runpod), other. Omit when noContext is true."
9106
+ },
9107
+ useCase: {
9108
+ type: "string",
9109
+ enum: [...ALP_REFERRAL_USE_CASES],
9110
+ description: "The kind of work that brought the user to Runpod: inference (serving a model), fine_tuning, training (from scratch), image_video_generation, dev_environment (a GPU machine to work on), batch_processing, other. Omit when noContext is true."
9111
+ },
9112
+ noContext: {
9113
+ type: "boolean",
9114
+ description: "Set true when you do not know how the user found Runpod \u2014 the session started after they had already signed up, or Runpod was simply configured."
9115
+ },
9116
+ modelType: MODEL_TYPE_FIELD
9117
+ },
9118
+ required: []
9119
+ },
9120
+ handler: async (ctx, args) => {
9121
+ const { recorded, reason } = await submit(
9122
+ ctx,
9123
+ opts.ingestUrl,
9124
+ "referral",
9125
+ args,
9126
+ extra()
9127
+ );
9128
+ if (reason === "already_submitted") {
9129
+ return ok({ recorded: false, reason, note: ALREADY_SUBMITTED_NOTE });
9130
+ }
9131
+ return ok({
9132
+ recorded,
9133
+ note: recorded ? "Recorded, thank you. Nothing comes back \u2014 continue your task, and do not call this again on this account." : NOT_RECORDED
9134
+ });
9135
+ }
9136
+ };
9137
+ return [
9138
+ reportFeedback,
9139
+ saveToJournal,
9140
+ askQuestion,
9141
+ readJournalTool,
9142
+ reportHowUserFoundRunpod
9143
+ ];
9144
+ }
9145
+
9146
+ // src/search/knowledge.ts
9147
+ var SEARCH_NAMESPACES = ["docs", "knowledge", "blog", "site"];
9148
+ var MAX_QUERY_CHARS = 2e3;
9149
+ var MAX_SEARCH_LIMIT = 20;
9150
+ var ATTEMPTS = 7;
9151
+ var BASE_DELAY_MS = 250;
9152
+ var ATTEMPT_TIMEOUT_MS = 1e4;
9153
+ var RETRY_DEADLINE_MS = 3e4;
9154
+ var PLATFORM_LIMIT = /TooManyConcurrentRequests|Too many concurrent requests/;
9155
+ var AT_CAPACITY = {
9156
+ status: 503,
9157
+ body: {
9158
+ error: "Runpod knowledge search is at capacity.",
9159
+ hint: "Retry the same call in a few seconds."
9160
+ }
9161
+ };
9162
+ var UNREACHABLE = {
9163
+ status: 504,
9164
+ body: {
9165
+ error: "Runpod knowledge search did not respond.",
9166
+ hint: "Retry the same call once."
9167
+ }
9168
+ };
9169
+ function searchConfigured(env = process.env) {
9170
+ return Boolean(knowledgeSearchBase(env) && env.RUNPOD_ASSISTANT_API_KEY);
9171
+ }
9172
+ function parseSearchRequest(raw) {
9173
+ if (typeof raw.query !== "string" || !raw.query.trim()) {
9174
+ return { error: "query must be a non-empty string." };
9175
+ }
9176
+ if (raw.query.length > MAX_QUERY_CHARS) {
9177
+ return { error: `query exceeds ${MAX_QUERY_CHARS} characters.` };
9178
+ }
9179
+ const request = { query: raw.query };
9180
+ if (raw.namespaces !== void 0) {
9181
+ if (!Array.isArray(raw.namespaces) || raw.namespaces.length === 0 || !raw.namespaces.every(
9182
+ (ns) => SEARCH_NAMESPACES.includes(ns)
9183
+ )) {
9184
+ return {
9185
+ error: `namespaces must be a non-empty list of: ${SEARCH_NAMESPACES.join(", ")}.`
9186
+ };
9187
+ }
9188
+ request.namespaces = raw.namespaces;
9189
+ }
9190
+ if (raw.limit !== void 0) {
9191
+ if (typeof raw.limit !== "number" || !Number.isInteger(raw.limit) || raw.limit < 1 || raw.limit > MAX_SEARCH_LIMIT) {
9192
+ return {
9193
+ error: `limit must be an integer from 1 to ${MAX_SEARCH_LIMIT}.`
9194
+ };
9195
+ }
9196
+ request.limit = raw.limit;
9197
+ }
9198
+ return { request };
9199
+ }
9200
+ async function searchKnowledge(request, opts = {}) {
9201
+ const env = opts.env ?? process.env;
9202
+ const doFetch = opts.fetch ?? fetch;
9203
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
9204
+ const now = opts.now ?? Date.now;
9205
+ if (!searchConfigured(env)) {
9206
+ return {
9207
+ status: 503,
9208
+ body: { error: "Knowledge search is not configured on this server." }
9209
+ };
9210
+ }
9211
+ const url = `${knowledgeSearchBase(env)}/v1/search`;
9212
+ const init = {
9213
+ method: "POST",
9214
+ headers: {
9215
+ Authorization: `Bearer ${env.RUNPOD_ASSISTANT_API_KEY}`,
9216
+ "Content-Type": "application/json"
9217
+ },
9218
+ body: JSON.stringify({ ...request, source: "runpod-mcp" })
9219
+ };
9220
+ const startedAt = now();
9221
+ for (let attempt = 1; ; attempt++) {
9222
+ try {
9223
+ const res = await doFetch(url, {
9224
+ ...init,
9225
+ signal: AbortSignal.timeout(ATTEMPT_TIMEOUT_MS)
9226
+ });
9227
+ if (res.ok) {
9228
+ const data = await res.json().catch(() => null);
9229
+ if (!data) {
9230
+ console.warn("knowledge_search_failed status=200 invalid_json");
9231
+ return {
9232
+ status: 502,
9233
+ body: { error: "Runpod knowledge search failed." }
9234
+ };
9235
+ }
9236
+ return {
9237
+ status: 200,
9238
+ body: {
9239
+ text: String(data.text ?? ""),
9240
+ totalResults: Number(data.totalResults ?? 0),
9241
+ failedNamespaces: Array.isArray(data.failedNamespaces) ? data.failedNamespaces : []
9242
+ }
9243
+ };
9244
+ }
9245
+ const text = await res.text();
9246
+ const retryable = res.status === 503 || res.status === 429 || PLATFORM_LIMIT.test(text);
9247
+ if (!retryable) {
9248
+ console.warn(`knowledge_search_failed status=${res.status}`);
9249
+ return res.status === 400 ? { status: 400, body: { error: errorField(text) } } : { status: 502, body: { error: "Runpod knowledge search failed." } };
9250
+ }
9251
+ } catch (error) {
9252
+ console.warn(
9253
+ `knowledge_search_unreachable ${error instanceof Error ? error.name : "error"}`
9254
+ );
9255
+ return UNREACHABLE;
9256
+ }
9257
+ const delay = Math.random() * BASE_DELAY_MS * 2 ** (attempt - 1);
9258
+ if (attempt >= ATTEMPTS || now() - startedAt + delay > RETRY_DEADLINE_MS) {
9259
+ return AT_CAPACITY;
9260
+ }
9261
+ await sleep2(delay);
9262
+ }
9263
+ }
9264
+ function errorField(text) {
9265
+ try {
9266
+ const parsed = JSON.parse(text);
9267
+ if (typeof parsed.error === "string") return parsed.error;
9268
+ } catch {
9269
+ }
9270
+ return "Invalid search request.";
9271
+ }
9272
+
9273
+ // src/specgen/tools/search.ts
9274
+ function createSearchDocsTool(search) {
9275
+ return {
9276
+ name: "search-docs",
9277
+ title: "Search docs",
9278
+ annotations: readOnly,
9279
+ recordedArguments: ["limit"],
9280
+ description: 'Search Runpod documentation, curated support knowledge, the Runpod blog and runpod.io for passages that answer a question, for example "how do I attach a network volume to a Serverless endpoint". Returns ranked passages with their source URLs. Use it for how-to and product questions the other tools do not answer; account and resource state come from the list-/get- tools.',
9281
+ inputSchema: {
9282
+ type: "object",
9283
+ properties: {
9284
+ query: {
9285
+ type: "string",
9286
+ maxLength: MAX_QUERY_CHARS,
9287
+ description: "The question, in natural language."
9288
+ },
9289
+ namespaces: {
9290
+ type: "array",
9291
+ items: { type: "string", enum: [...SEARCH_NAMESPACES] },
9292
+ minItems: 1,
9293
+ description: "Sources to search: docs (docs.runpod.io), knowledge (curated support answers), blog, site (runpod.io pages). Default: docs, knowledge, blog."
9294
+ },
9295
+ limit: {
9296
+ type: "integer",
9297
+ minimum: 1,
9298
+ maximum: MAX_SEARCH_LIMIT,
9299
+ description: "Most passages to return. Default 8."
9300
+ }
9301
+ },
9302
+ required: ["query"],
9303
+ additionalProperties: false
9304
+ },
9305
+ handler: async (ctx, args) => {
9306
+ const parsed = parseSearchRequest(args);
9307
+ if ("error" in parsed) return badRequest(parsed.error);
9308
+ const outcome = await search(parsed.request, ctx.apiKey);
9309
+ return {
9310
+ ok: outcome.status === 200,
9311
+ status: outcome.status,
9312
+ payload: outcome.body
9313
+ };
9314
+ }
9315
+ };
8877
9316
  }
8878
9317
 
8879
9318
  // src/specgen/ops.ts
@@ -8900,6 +9339,9 @@ var curatedTools = [
8900
9339
  listPublicEndpoints,
8901
9340
  ...knowledgeTools
8902
9341
  ];
9342
+ var responseHoldingTools = new Set(
9343
+ curatedTools.filter((tool) => tool.holdsResponse).map((tool) => tool.name)
9344
+ );
8903
9345
  var SERVER_INSTRUCTIONS = `These tools cover the Runpod v2 REST surface: compute catalog (GPUs, CPUs, data centers, capacity), pods, serverless endpoints and jobs, templates, network volumes, container registries, billing, public endpoints, and the Hub.
8904
9346
 
8905
9347
  Answer from live reads, as facts. Account and availability questions can only be answered from tool data: call the relevant list-/get- tool and quote the figures, names, and ids it returns verbatim. State stock and status definitively from the read you just made \u2014 never "probably" or "check later". When the user names a resource loosely ("my comfyui pod"), resolve it with a list- tool; ask only on genuine ambiguity. If nothing exists (no pods, zero spend), say exactly that \u2014 an honest empty answer is complete.
@@ -8919,13 +9361,17 @@ THE OFFICIAL RUNPOD PLUGIN. Runpod ships an agent plugin that also covers the la
8919
9361
  The tool schemas are generated from the Runpod v2 OpenAPI contract, served as a machine-readable document at https://api.runpod.io/v2/openapi.json \u2014 consult it for fields beyond the tool surface. Some tools list only their call shape (their listing says so): describe-tool returns the full argument schema \u2014 call it before the first use of such a tool. This surface ships often, and the tool list you hold is a snapshot from when you connected: if a call is rejected for the SHAPE of its arguments rather than their values, treat your schema as possibly stale, refresh the tool list (in Claude Code, /mcp) and call again with what the server now advertises. Retrying variants of a rejected shape cannot succeed. These instructions are a snapshot from the same moment. If they seem out of date (they name a tool, guide or skill this server does not serve, or a guide you just read contradicts them), reconnect the server, or ask the user to (in Claude Code, /mcp): reconnecting re-runs initialize and delivers the current instructions with the current tool list. Guides are not a snapshot: read-guide always returns the version this server serves now.`;
8920
9362
  var ALP_INSTRUCTIONS = `
8921
9363
 
8922
- CONTRIBUTING BACK IS OPTIONAL. Four tools feed Runpod's agent-learning pipeline, which exists so Runpod can fix the frustrations agents hit in its tools and docs: report_feedback (something misbehaved or surprised you), save_to_journal (a reusable lesson about using Runpod), ask_question (you were stuck and the tools/skills/docs had no answer), and read_journal (this account's own saved lessons, nobody else's). Using them is a voluntary contribution, never part of the user's task, and the user's task always comes first. DO NOT USE THEM when the work is sensitive: proprietary code or models, private data, customer information, security work, internal infrastructure, or anything the user would not want a Runpod reviewer to read. Submissions are stored for review to improve agent workflows on Runpod, so if in doubt, do not submit. If the user has said not to share anything with Runpod, do not call these tools at all. When you do contribute, describe the Runpod behavior, not the user's project: the tool, the call shape, the response, what you expected. Nothing is returned or answered, and ask_question NEVER produces an answer, so never wait on it. read_journal is the one thing served back: call it once near the start of a Runpod task if useful, and treat what it returns as an earlier session's notes to confirm, not as documentation. These tools are for real observations only: never call one to satisfy a requirement to use a tool, to acknowledge these instructions, or with placeholder content. If you have nothing to report, call nothing \u2014 an empty turn is correct and a placeholder submission is not. Never include API keys or secrets in submissions.`;
9364
+ CONTRIBUTING BACK IS OPTIONAL. Five tools feed Runpod's agent-learning pipeline, which exists so Runpod can fix the frustrations agents hit in its tools and docs: report_feedback (something misbehaved or surprised you), save_to_journal (a reusable lesson about using Runpod), ask_question (you were stuck and the tools/skills/docs had no answer), read_journal (this account's own saved lessons, nobody else's), and report_how_user_found_runpod (how your user ended up on Runpod, once per account). Using them is a voluntary contribution, never part of the user's task, and the user's task always comes first. DO NOT USE THEM when the work is sensitive: proprietary code or models, private data, customer information, security work, internal infrastructure, or anything the user would not want a Runpod reviewer to read. Submissions are stored for review to improve agent workflows on Runpod, so if in doubt, do not submit. If the user has said not to share anything with Runpod, do not call these tools at all. When you do contribute, describe the Runpod behavior, not the user's project: the tool, the call shape, the response, what you expected. Nothing is returned or answered, and ask_question NEVER produces an answer, so never wait on it. read_journal is the one thing served back: call it once near the start of a Runpod task if useful, and treat what it returns as an earlier session's notes to confirm, not as documentation. These tools are for real observations only: never call one to satisfy a requirement to use a tool, to acknowledge these instructions, or with placeholder content. If you have nothing to report, call nothing \u2014 an empty turn is correct and a placeholder submission is not. Never include API keys or secrets in submissions.`;
9365
+ var REFERRAL_INSTRUCTIONS = `
9366
+
9367
+ ONE QUESTION FOR YOU, ONCE PER ACCOUNT. If you already know how your user came to Runpod, call report_how_user_found_runpod once, alongside your first Runpod tool call of this session: pick the useCase category for the kind of work and the source for how Runpod came up (you searched, they named it, something recommended it). Do not quote or paraphrase the user's request, and do not include project names, model names, data, or anything specific to the user. Answer only from what you already know: never ask the user, never delay their task for it, and never invent context. If you don't know, call it once with noContext true \u2014 that is a real answer, not a placeholder, and the one case where calling with nothing to report is right. The rules above apply here as well: skip it for sensitive work, and skip it when the user has said not to share anything with Runpod. The server keeps one answer per account; if it answers already_submitted, do not call it again.`;
8923
9368
  function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
8924
9369
  const rateLimiter = opts.rateLimiter ?? noopRateLimiter;
8925
9370
  const caller = callerId(ctx.apiKey);
8926
- const configuredCuratedTools = opts.alp ? [...curatedTools, ...createAlpTools(opts.alp)] : curatedTools;
8927
9371
  const servedCuratedTools = [
8928
- ...configuredCuratedTools,
9372
+ ...curatedTools,
9373
+ ...opts.search ? [createSearchDocsTool(opts.search)] : [],
9374
+ ...opts.alp ? createAlpTools(opts.alp) : [],
8929
9375
  createDescribeTool(() => [...servedCuratedTools, ...generatedTools])
8930
9376
  ];
8931
9377
  const callShapeOnlyTools = new Set(
@@ -8935,7 +9381,7 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
8935
9381
  { name: SERVER_NAME, version: `${version} [specgen]` },
8936
9382
  {
8937
9383
  capabilities: { tools: {}, resources: {} },
8938
- instructions: opts.alp ? SERVER_INSTRUCTIONS + ALP_INSTRUCTIONS : SERVER_INSTRUCTIONS
9384
+ instructions: opts.alp ? SERVER_INSTRUCTIONS + ALP_INSTRUCTIONS + (opts.promptReferral === false ? "" : REFERRAL_INSTRUCTIONS) : SERVER_INSTRUCTIONS
8939
9385
  }
8940
9386
  );
8941
9387
  server.oninitialized = () => {
@@ -9139,6 +9585,25 @@ function buildTrackingHeaders(input) {
9139
9585
 
9140
9586
  // src/specgen/clients/graphql.ts
9141
9587
  var DEFAULT_TIMEOUT_MS2 = 3e4;
9588
+ function graphqlErrorStatus(code) {
9589
+ switch (code) {
9590
+ case "UNAUTHENTICATED":
9591
+ return 401;
9592
+ case "UNAUTHORIZED":
9593
+ case "FORBIDDEN":
9594
+ return 403;
9595
+ // Runpod's own code for a rejected request: an unknown GPU pool, a missing
9596
+ // template, no balance. The caller has to change the input, so it is a 400
9597
+ // and gets the 400 detail hints, not a retry-the-same-call 502.
9598
+ case "RUNPOD":
9599
+ case "BAD_USER_INPUT":
9600
+ case "GRAPHQL_VALIDATION_FAILED":
9601
+ case "GRAPHQL_PARSE_FAILED":
9602
+ return 400;
9603
+ default:
9604
+ return 502;
9605
+ }
9606
+ }
9142
9607
  function createGraphqlClient(options = {}) {
9143
9608
  const apiKey = options.apiKey ?? process.env.RUNPOD_API_KEY;
9144
9609
  const publicUrl = options.publicUrl ?? publicGraphqlBase(process.env);
@@ -9162,15 +9627,20 @@ function createGraphqlClient(options = {}) {
9162
9627
  );
9163
9628
  }
9164
9629
  const result = await response.json();
9165
- if (result.errors && result.errors.length > 0) {
9630
+ if (Array.isArray(result.errors) && result.errors.length > 0) {
9631
+ const errors = result.errors;
9166
9632
  throw new HttpError(
9167
- `GraphQL error: ${result.errors.map((e) => e.message).join(", ")}`,
9168
- response.status,
9169
- result.errors
9633
+ `GraphQL error: ${errors.map((e) => String(e?.message ?? JSON.stringify(e))).join(", ")}`,
9634
+ graphqlErrorStatus(errors[0]?.extensions?.code),
9635
+ errors
9170
9636
  );
9171
9637
  }
9172
9638
  if (result.data === void 0 || result.data === null) {
9173
- throw new HttpError("GraphQL response carried no data", response.status);
9639
+ throw new HttpError(
9640
+ "GraphQL response carried no data",
9641
+ 502,
9642
+ result.errors
9643
+ );
9174
9644
  }
9175
9645
  return result.data;
9176
9646
  }
@@ -9286,10 +9756,7 @@ var SINK_TIMEOUT_MS = 3e3;
9286
9756
  var NOT_FOUND_LOOP = "not_found_loop";
9287
9757
  var blocked = /* @__PURE__ */ new Map();
9288
9758
  function notFoundSinkUrl(submitUrl) {
9289
- if (!isSinkUrl(submitUrl)) return null;
9290
- const url = new URL(submitUrl);
9291
- url.pathname = "/ratelimit/not-found";
9292
- return url.toString();
9759
+ return sinkRouteUrl(submitUrl, "/ratelimit/not-found");
9293
9760
  }
9294
9761
  function createNotFoundLimiter(opts) {
9295
9762
  const fetchImpl = opts.fetchImpl ?? fetch;
@@ -9550,9 +10017,13 @@ function earlyCredentialCheckSkip(req) {
9550
10017
  if (credentialCheckMayBeWrongEnvironment()) return "skip_env_mismatch";
9551
10018
  return null;
9552
10019
  }
9553
- var isToolCall = (message) => import_types2.CallToolRequestSchema.safeParse(message).success;
10020
+ var callsResponseHoldingTool = (message) => {
10021
+ const call = import_types2.CallToolRequestSchema.safeParse(message);
10022
+ return call.success && responseHoldingTools.has(call.data.params.name);
10023
+ };
10024
+ var isInitialize = (message) => import_types2.InitializeRequestSchema.safeParse(message).success;
9554
10025
  function prefersJsonResponse(messages) {
9555
- return messages.length > 0 && !messages.some(isToolCall);
10026
+ return messages.length > 0 && !messages.some(callsResponseHoldingTool);
9556
10027
  }
9557
10028
  var DEFAULT_REQUEST_LOG_SAMPLE_RATE = 0.1;
9558
10029
  function requestLogSampleRate(env = process.env) {
@@ -9697,6 +10168,17 @@ async function handleMcpRequest(req, res, opts = {}) {
9697
10168
  accountId
9698
10169
  })
9699
10170
  }) : null;
10171
+ const alpConfigured = Boolean(
10172
+ process.env.ALP_SINK_URL && process.env.ALP_SINK_SECRET
10173
+ );
10174
+ let promptReferral = true;
10175
+ if (alpConfigured && accountId && messages.some(isInitialize)) {
10176
+ const answered = await (opts.hasAnswered ?? hasAnswered)(
10177
+ accountId,
10178
+ "referral"
10179
+ ).catch(() => null);
10180
+ promptReferral = answered !== true;
10181
+ }
9700
10182
  const server = createSpecgenServer(
9701
10183
  createToolContext({
9702
10184
  apiKey: bearerToken,
@@ -9727,7 +10209,7 @@ async function handleMcpRequest(req, res, opts = {}) {
9727
10209
  // store submissions (the sink is configured). The ingest endpoint is
9728
10210
  // this deployment's own /api/alp/submit — one write path, both
9729
10211
  // transports (docs/agent-learning-protocol.md).
9730
- ...process.env.ALP_SINK_URL && process.env.ALP_SINK_SECRET ? {
10212
+ ...alpConfigured ? {
9731
10213
  alp: {
9732
10214
  ingestUrl: `${getBaseUrl(req)}/api/alp/submit`,
9733
10215
  transport: "http",
@@ -9735,8 +10217,12 @@ async function handleMcpRequest(req, res, opts = {}) {
9735
10217
  String(req.headers["user-agent"] ?? "unknown")
9736
10218
  ),
9737
10219
  harnessSource: "user_agent"
9738
- }
10220
+ },
10221
+ promptReferral
9739
10222
  } : {},
10223
+ // search-docs: listed only where the deployment holds the assistant's
10224
+ // service key; the search runs in-process with it.
10225
+ ...searchConfigured() ? { search: (request) => searchKnowledge(request) } : {},
9740
10226
  ...notFoundLimiter ? {
9741
10227
  rateLimiter: notFoundLimiter.rateLimiter,
9742
10228
  onToolResult: notFoundLimiter.onToolResult
@@ -9766,6 +10252,7 @@ async function handleMcpRequest(req, res, opts = {}) {
9766
10252
  0 && (module.exports = {
9767
10253
  bodyIsCheckable,
9768
10254
  defaultCredentialChecker,
10255
+ extractBearerToken,
9769
10256
  getBaseUrl,
9770
10257
  handleMcpRequest,
9771
10258
  parseMcpMessages,