@runpod/mcp-server 4.3.0 → 4.4.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.d.cts CHANGED
@@ -26,6 +26,11 @@ interface RpcSummary {
26
26
  client?: string;
27
27
  }
28
28
 
29
+ /**
30
+ * Extract the bearer token from an HTTP request's Authorization header.
31
+ * Returns null if the header is missing or malformed.
32
+ */
33
+ declare function extractBearerToken(req: IncomingMessage): string | null;
29
34
  declare function getBaseUrl(req: {
30
35
  headers: {
31
36
  host?: string;
@@ -59,4 +64,4 @@ declare function summarizeRpc(messages: JSONRPCMessage[]): RpcSummary;
59
64
  */
60
65
  declare function handleMcpRequest(req: IncomingMessage, res: ServerResponse, opts?: HandleMcpRequestOptions): Promise<void>;
61
66
 
62
- export { bodyIsCheckable, defaultCredentialChecker, getBaseUrl, handleMcpRequest, parseMcpMessages, prefersJsonResponse, requestLogSampleRate, summarizeRpc };
67
+ export { bodyIsCheckable, defaultCredentialChecker, extractBearerToken, getBaseUrl, handleMcpRequest, parseMcpMessages, prefersJsonResponse, requestLogSampleRate, summarizeRpc };
package/dist/http.d.ts CHANGED
@@ -26,6 +26,11 @@ interface RpcSummary {
26
26
  client?: string;
27
27
  }
28
28
 
29
+ /**
30
+ * Extract the bearer token from an HTTP request's Authorization header.
31
+ * Returns null if the header is missing or malformed.
32
+ */
33
+ declare function extractBearerToken(req: IncomingMessage): string | null;
29
34
  declare function getBaseUrl(req: {
30
35
  headers: {
31
36
  host?: string;
@@ -59,4 +64,4 @@ declare function summarizeRpc(messages: JSONRPCMessage[]): RpcSummary;
59
64
  */
60
65
  declare function handleMcpRequest(req: IncomingMessage, res: ServerResponse, opts?: HandleMcpRequestOptions): Promise<void>;
61
66
 
62
- export { bodyIsCheckable, defaultCredentialChecker, getBaseUrl, handleMcpRequest, parseMcpMessages, prefersJsonResponse, requestLogSampleRate, summarizeRpc };
67
+ export { bodyIsCheckable, defaultCredentialChecker, extractBearerToken, getBaseUrl, handleMcpRequest, parseMcpMessages, prefersJsonResponse, requestLogSampleRate, summarizeRpc };
package/dist/http.mjs CHANGED
@@ -5695,6 +5695,8 @@ var listHubRepos = {
5695
5695
  };
5696
5696
  var deployHubRepo = {
5697
5697
  name: "deploy-hub-repo",
5698
+ // Two sequential upstream calls: together they can outlast a client's request timeout.
5699
+ holdsResponse: true,
5698
5700
  annotations: write,
5699
5701
  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).",
5700
5702
  inputSchema: {
@@ -6183,6 +6185,7 @@ var runEndpoint = {
6183
6185
  };
6184
6186
  var runsyncEndpoint = {
6185
6187
  name: "runsync-endpoint",
6188
+ holdsResponse: true,
6186
6189
  annotations: write,
6187
6190
  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)."),
6188
6191
  inputSchema: {
@@ -6227,6 +6230,7 @@ var runsyncEndpoint = {
6227
6230
  };
6228
6231
  var getJobStatus = {
6229
6232
  name: "get-job-status",
6233
+ holdsResponse: true,
6230
6234
  annotations: readOnly,
6231
6235
  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.",
6232
6236
  inputSchema: {
@@ -6275,6 +6279,7 @@ var getJobStatus = {
6275
6279
  };
6276
6280
  var streamJob = {
6277
6281
  name: "stream-job",
6282
+ holdsResponse: true,
6278
6283
  annotations: readOnly,
6279
6284
  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.`,
6280
6285
  inputSchema: {
@@ -6507,6 +6512,9 @@ function publicGraphqlBase(env) {
6507
6512
  function authedGraphqlBase(env) {
6508
6513
  return pick(env.RUNPOD_AUTHED_GRAPHQL_URL, "https://api.runpod.io/graphql");
6509
6514
  }
6515
+ function knowledgeSearchBase(env) {
6516
+ return pick(env.KNOWLEDGE_SEARCH_URL, "") || void 0;
6517
+ }
6510
6518
 
6511
6519
  // node_modules/.pnpm/@runpod+typescript-api-sdk@0.3.0/node_modules/@runpod/typescript-api-sdk/dist/index.js
6512
6520
  var PATH_PARAM_RE = /\{[^{}]+\}/g;
@@ -7912,6 +7920,7 @@ function logParams(args) {
7912
7920
  }
7913
7921
  var streamPodLogs = {
7914
7922
  name: "stream-pod-logs",
7923
+ holdsResponse: true,
7915
7924
  annotations: readOnly,
7916
7925
  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.",
7917
7926
  inputSchema: {
@@ -7934,6 +7943,7 @@ var streamPodLogs = {
7934
7943
  };
7935
7944
  var streamWorkerLogs = {
7936
7945
  name: "stream-worker-logs",
7946
+ holdsResponse: true,
7937
7947
  annotations: readOnly,
7938
7948
  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.",
7939
7949
  inputSchema: {
@@ -8527,7 +8537,7 @@ function words2(name) {
8527
8537
 
8528
8538
  // src/server.ts
8529
8539
  var SERVER_NAME = "Runpod API Server";
8530
- var SERVER_VERSION = true ? "4.3.0" : "dev";
8540
+ var SERVER_VERSION = true ? "4.4.0" : "dev";
8531
8541
 
8532
8542
  // src/specgen/analytics.ts
8533
8543
  import { createHmac } from "crypto";
@@ -8857,6 +8867,177 @@ function createAlpTools(opts) {
8857
8867
  return [reportFeedback, saveToJournal, askQuestion, readJournalTool];
8858
8868
  }
8859
8869
 
8870
+ // src/search/knowledge.ts
8871
+ var SEARCH_NAMESPACES = ["docs", "knowledge", "blog", "site"];
8872
+ var MAX_QUERY_CHARS = 2e3;
8873
+ var MAX_SEARCH_LIMIT = 20;
8874
+ var ATTEMPTS = 7;
8875
+ var BASE_DELAY_MS = 250;
8876
+ var ATTEMPT_TIMEOUT_MS = 1e4;
8877
+ var RETRY_DEADLINE_MS = 3e4;
8878
+ var PLATFORM_LIMIT = /TooManyConcurrentRequests|Too many concurrent requests/;
8879
+ var AT_CAPACITY = {
8880
+ status: 503,
8881
+ body: {
8882
+ error: "Runpod knowledge search is at capacity.",
8883
+ hint: "Retry the same call in a few seconds."
8884
+ }
8885
+ };
8886
+ var UNREACHABLE = {
8887
+ status: 504,
8888
+ body: {
8889
+ error: "Runpod knowledge search did not respond.",
8890
+ hint: "Retry the same call once."
8891
+ }
8892
+ };
8893
+ function searchConfigured(env = process.env) {
8894
+ return Boolean(knowledgeSearchBase(env) && env.RUNPOD_ASSISTANT_API_KEY);
8895
+ }
8896
+ function parseSearchRequest(raw) {
8897
+ if (typeof raw.query !== "string" || !raw.query.trim()) {
8898
+ return { error: "query must be a non-empty string." };
8899
+ }
8900
+ if (raw.query.length > MAX_QUERY_CHARS) {
8901
+ return { error: `query exceeds ${MAX_QUERY_CHARS} characters.` };
8902
+ }
8903
+ const request = { query: raw.query };
8904
+ if (raw.namespaces !== void 0) {
8905
+ if (!Array.isArray(raw.namespaces) || raw.namespaces.length === 0 || !raw.namespaces.every(
8906
+ (ns) => SEARCH_NAMESPACES.includes(ns)
8907
+ )) {
8908
+ return {
8909
+ error: `namespaces must be a non-empty list of: ${SEARCH_NAMESPACES.join(", ")}.`
8910
+ };
8911
+ }
8912
+ request.namespaces = raw.namespaces;
8913
+ }
8914
+ if (raw.limit !== void 0) {
8915
+ if (typeof raw.limit !== "number" || !Number.isInteger(raw.limit) || raw.limit < 1 || raw.limit > MAX_SEARCH_LIMIT) {
8916
+ return {
8917
+ error: `limit must be an integer from 1 to ${MAX_SEARCH_LIMIT}.`
8918
+ };
8919
+ }
8920
+ request.limit = raw.limit;
8921
+ }
8922
+ return { request };
8923
+ }
8924
+ async function searchKnowledge(request, opts = {}) {
8925
+ const env = opts.env ?? process.env;
8926
+ const doFetch = opts.fetch ?? fetch;
8927
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
8928
+ const now = opts.now ?? Date.now;
8929
+ if (!searchConfigured(env)) {
8930
+ return {
8931
+ status: 503,
8932
+ body: { error: "Knowledge search is not configured on this server." }
8933
+ };
8934
+ }
8935
+ const url = `${knowledgeSearchBase(env)}/v1/search`;
8936
+ const init = {
8937
+ method: "POST",
8938
+ headers: {
8939
+ Authorization: `Bearer ${env.RUNPOD_ASSISTANT_API_KEY}`,
8940
+ "Content-Type": "application/json"
8941
+ },
8942
+ body: JSON.stringify({ ...request, source: "runpod-mcp" })
8943
+ };
8944
+ const startedAt = now();
8945
+ for (let attempt = 1; ; attempt++) {
8946
+ try {
8947
+ const res = await doFetch(url, {
8948
+ ...init,
8949
+ signal: AbortSignal.timeout(ATTEMPT_TIMEOUT_MS)
8950
+ });
8951
+ if (res.ok) {
8952
+ const data = await res.json().catch(() => null);
8953
+ if (!data) {
8954
+ console.warn("knowledge_search_failed status=200 invalid_json");
8955
+ return {
8956
+ status: 502,
8957
+ body: { error: "Runpod knowledge search failed." }
8958
+ };
8959
+ }
8960
+ return {
8961
+ status: 200,
8962
+ body: {
8963
+ text: String(data.text ?? ""),
8964
+ totalResults: Number(data.totalResults ?? 0),
8965
+ failedNamespaces: Array.isArray(data.failedNamespaces) ? data.failedNamespaces : []
8966
+ }
8967
+ };
8968
+ }
8969
+ const text = await res.text();
8970
+ const retryable = res.status === 503 || res.status === 429 || PLATFORM_LIMIT.test(text);
8971
+ if (!retryable) {
8972
+ console.warn(`knowledge_search_failed status=${res.status}`);
8973
+ return res.status === 400 ? { status: 400, body: { error: errorField(text) } } : { status: 502, body: { error: "Runpod knowledge search failed." } };
8974
+ }
8975
+ } catch (error) {
8976
+ console.warn(
8977
+ `knowledge_search_unreachable ${error instanceof Error ? error.name : "error"}`
8978
+ );
8979
+ return UNREACHABLE;
8980
+ }
8981
+ const delay = Math.random() * BASE_DELAY_MS * 2 ** (attempt - 1);
8982
+ if (attempt >= ATTEMPTS || now() - startedAt + delay > RETRY_DEADLINE_MS) {
8983
+ return AT_CAPACITY;
8984
+ }
8985
+ await sleep2(delay);
8986
+ }
8987
+ }
8988
+ function errorField(text) {
8989
+ try {
8990
+ const parsed = JSON.parse(text);
8991
+ if (typeof parsed.error === "string") return parsed.error;
8992
+ } catch {
8993
+ }
8994
+ return "Invalid search request.";
8995
+ }
8996
+
8997
+ // src/specgen/tools/search.ts
8998
+ function createSearchDocsTool(search) {
8999
+ return {
9000
+ name: "search-docs",
9001
+ annotations: readOnly,
9002
+ recordedArguments: ["limit"],
9003
+ 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.',
9004
+ inputSchema: {
9005
+ type: "object",
9006
+ properties: {
9007
+ query: {
9008
+ type: "string",
9009
+ maxLength: MAX_QUERY_CHARS,
9010
+ description: "The question, in natural language."
9011
+ },
9012
+ namespaces: {
9013
+ type: "array",
9014
+ items: { type: "string", enum: [...SEARCH_NAMESPACES] },
9015
+ minItems: 1,
9016
+ description: "Sources to search: docs (docs.runpod.io), knowledge (curated support answers), blog, site (runpod.io pages). Default: docs, knowledge, blog."
9017
+ },
9018
+ limit: {
9019
+ type: "integer",
9020
+ minimum: 1,
9021
+ maximum: MAX_SEARCH_LIMIT,
9022
+ description: "Most passages to return. Default 8."
9023
+ }
9024
+ },
9025
+ required: ["query"],
9026
+ additionalProperties: false
9027
+ },
9028
+ handler: async (ctx, args) => {
9029
+ const parsed = parseSearchRequest(args);
9030
+ if ("error" in parsed) return badRequest(parsed.error);
9031
+ const outcome = await search(parsed.request, ctx.apiKey);
9032
+ return {
9033
+ ok: outcome.status === 200,
9034
+ status: outcome.status,
9035
+ payload: outcome.body
9036
+ };
9037
+ }
9038
+ };
9039
+ }
9040
+
8860
9041
  // src/specgen/ops.ts
8861
9042
  import { createHash, randomBytes } from "crypto";
8862
9043
  var SALT = randomBytes(16).toString("hex");
@@ -8881,6 +9062,9 @@ var curatedTools = [
8881
9062
  listPublicEndpoints,
8882
9063
  ...knowledgeTools
8883
9064
  ];
9065
+ var responseHoldingTools = new Set(
9066
+ curatedTools.filter((tool) => tool.holdsResponse).map((tool) => tool.name)
9067
+ );
8884
9068
  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.
8885
9069
 
8886
9070
  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.
@@ -8904,9 +9088,10 @@ CONTRIBUTING BACK IS OPTIONAL. Four tools feed Runpod's agent-learning pipeline,
8904
9088
  function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
8905
9089
  const rateLimiter = opts.rateLimiter ?? noopRateLimiter;
8906
9090
  const caller = callerId(ctx.apiKey);
8907
- const configuredCuratedTools = opts.alp ? [...curatedTools, ...createAlpTools(opts.alp)] : curatedTools;
8908
9091
  const servedCuratedTools = [
8909
- ...configuredCuratedTools,
9092
+ ...curatedTools,
9093
+ ...opts.search ? [createSearchDocsTool(opts.search)] : [],
9094
+ ...opts.alp ? createAlpTools(opts.alp) : [],
8910
9095
  createDescribeTool(() => [...servedCuratedTools, ...generatedTools])
8911
9096
  ];
8912
9097
  const callShapeOnlyTools = new Set(
@@ -9531,9 +9716,12 @@ function earlyCredentialCheckSkip(req) {
9531
9716
  if (credentialCheckMayBeWrongEnvironment()) return "skip_env_mismatch";
9532
9717
  return null;
9533
9718
  }
9534
- var isToolCall = (message) => CallToolRequestSchema2.safeParse(message).success;
9719
+ var callsResponseHoldingTool = (message) => {
9720
+ const call = CallToolRequestSchema2.safeParse(message);
9721
+ return call.success && responseHoldingTools.has(call.data.params.name);
9722
+ };
9535
9723
  function prefersJsonResponse(messages) {
9536
- return messages.length > 0 && !messages.some(isToolCall);
9724
+ return messages.length > 0 && !messages.some(callsResponseHoldingTool);
9537
9725
  }
9538
9726
  var DEFAULT_REQUEST_LOG_SAMPLE_RATE = 0.1;
9539
9727
  function requestLogSampleRate(env = process.env) {
@@ -9718,6 +9906,9 @@ async function handleMcpRequest(req, res, opts = {}) {
9718
9906
  harnessSource: "user_agent"
9719
9907
  }
9720
9908
  } : {},
9909
+ // search-docs: listed only where the deployment holds the assistant's
9910
+ // service key; the search runs in-process with it.
9911
+ ...searchConfigured() ? { search: (request) => searchKnowledge(request) } : {},
9721
9912
  ...notFoundLimiter ? {
9722
9913
  rateLimiter: notFoundLimiter.rateLimiter,
9723
9914
  onToolResult: notFoundLimiter.onToolResult
@@ -9746,6 +9937,7 @@ async function handleMcpRequest(req, res, opts = {}) {
9746
9937
  export {
9747
9938
  bodyIsCheckable,
9748
9939
  defaultCredentialChecker,
9940
+ extractBearerToken,
9749
9941
  getBaseUrl,
9750
9942
  handleMcpRequest,
9751
9943
  parseMcpMessages,