@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/CHANGELOG.md +10 -0
- package/README.md +16 -0
- package/dist/{chunk-DDX42UTT.mjs → chunk-4GF3CMSH.mjs} +6 -2
- package/dist/{chunk-DDX42UTT.mjs.map → chunk-4GF3CMSH.mjs.map} +1 -1
- package/dist/http.cjs +198 -5
- package/dist/http.cjs.map +1 -1
- package/dist/http.d.cts +6 -1
- package/dist/http.d.ts +6 -1
- package/dist/http.mjs +197 -5
- package/dist/http.mjs.map +1 -1
- package/dist/stdio.cjs +131 -5
- package/dist/stdio.cjs.map +1 -1
- package/dist/stdio.mjs +127 -6
- package/dist/stdio.mjs.map +1 -1
- package/dist/{wizard-L25AYBNQ.mjs → wizard-KKTBTVXE.mjs} +4 -3
- package/dist/wizard-KKTBTVXE.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/wizard-L25AYBNQ.mjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @runpod/mcp-server
|
|
2
2
|
|
|
3
|
+
## 4.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- a58f9f1: New `search-docs` tool: searches Runpod documentation, curated support knowledge, the Runpod blog and runpod.io through the runpod-assistant knowledge index, and returns ranked passages with source URLs. The hosted server searches with its own service key and retries the index's capacity rejections with jittered backoff. Local installs call the hosted `POST /api/search` with the user's API key.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 4b53720: Hosted server: `tools/call` now answers as `application/json` too, so the CDN compresses tool results; only tools that hold their response open (`runsync-endpoint`, `get-job-status`, `stream-job`, `stream-pod-logs`, `stream-worker-logs`, `deploy-hub-repo`) keep SSE, declared with `CuratedTool.holdsResponse`.
|
|
12
|
+
|
|
3
13
|
## 4.3.0
|
|
4
14
|
|
|
5
15
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -84,6 +84,22 @@ An OAuth-capable client starts the "Sign in with Runpod" flow automatically on f
|
|
|
84
84
|
|
|
85
85
|
> Prefer your own API key over OAuth? Append `--header "Authorization: Bearer YOUR_API_KEY"` to the `claude mcp add` command (or add a `headers` block in the JSON). The server forwards that key to the Runpod API directly.
|
|
86
86
|
|
|
87
|
+
### If sign-in fails with HTTP 403
|
|
88
|
+
|
|
89
|
+
A `403 Forbidden` response with the header `X-Vercel-Mitigated: deny` means the hosted server's firewall has temporarily blocked your IP address, usually after a burst of unauthenticated requests from an MCP client. This happens most often with Codex, which sends many unauthenticated requests while it signs in. The block covers every path, including `/register` and requests that carry an API key, and lifts on its own within an hour. Wait for it to expire, then use one of these options so the next connection doesn't trip it again.
|
|
90
|
+
|
|
91
|
+
You can send your API key as a header instead of signing in with OAuth, as described above. Authenticated requests don't count toward the limit. In Codex, add the key to the server entry in `~/.codex/config.toml`:
|
|
92
|
+
|
|
93
|
+
```toml
|
|
94
|
+
[mcp_servers.runpod]
|
|
95
|
+
url = "https://mcp.getrunpod.io/"
|
|
96
|
+
bearer_token_env_var = "RUNPOD_API_KEY"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Then export `RUNPOD_API_KEY` in the shell that launches Codex.
|
|
100
|
+
|
|
101
|
+
You can also [run the server locally](#run-locally-with-npx). The local server calls the Runpod API directly, so the hosted firewall only affects its `search-docs` tool, which still searches through the hosted server.
|
|
102
|
+
|
|
87
103
|
### Usage analytics
|
|
88
104
|
|
|
89
105
|
The hosted server records one anonymous event per tool call (tool name, status, duration, transport) and per guide resource read (guide URI, or `unknown` on a miss), plus one event when it starts blocking a 404 loop (tool name and block length). It never records your API key or any resource data, and drops tool arguments except on the guide and concept tools (`list-guides`, `read-guide`, `lookup-concept`, `search-concepts`), where it keeps the guide id, concept name or search words, plus the kind of guide read. Your identity is an irreversible salted hash. To opt out, send the header `X-Runpod-Analytics: off` — e.g. append `--header "X-Runpod-Analytics: off"` to the `claude mcp add` command. Running locally, nothing is ever sent.
|
|
@@ -16,11 +16,15 @@ function publicGraphqlBase(env) {
|
|
|
16
16
|
function authedGraphqlBase(env) {
|
|
17
17
|
return pick(env.RUNPOD_AUTHED_GRAPHQL_URL, "https://api.runpod.io/graphql");
|
|
18
18
|
}
|
|
19
|
+
function hostedMcpBase(env) {
|
|
20
|
+
return pick(env.RUNPOD_MCP_URL, "https://mcp.getrunpod.io");
|
|
21
|
+
}
|
|
19
22
|
|
|
20
23
|
export {
|
|
21
24
|
sdkBase,
|
|
22
25
|
serverlessBase,
|
|
23
26
|
publicGraphqlBase,
|
|
24
|
-
authedGraphqlBase
|
|
27
|
+
authedGraphqlBase,
|
|
28
|
+
hostedMcpBase
|
|
25
29
|
};
|
|
26
|
-
//# sourceMappingURL=chunk-
|
|
30
|
+
//# sourceMappingURL=chunk-4GF3CMSH.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/_shared/hosts.ts"],"sourcesContent":["// Env-resolved API base URLs, extracted from the retired dual-version backend\n// resolver (backend.ts) when the spec-generated surface became the only one.\n// The credential preflight compares only active SDK and runtime destinations.\n\nexport type Env = Record<string, string | undefined>;\n\n// '' reads as unset everywhere: the wrong-environment guard already strips\n// empty vars before comparing, and every consumer (the credential checker's\n// probe URL, the GraphQL/log clients) must resolve identically or the\n// guard's \"checker and guard cannot disagree\" invariant breaks.\nfunction pick(value: string | undefined, fallback: string): string {\n return value?.trim().replace(/\\/+$/, '') || fallback;\n}\n\n// Generated paths include /v2, so the SDK base is the API origin.\nexport function sdkBase(env: Env): string {\n return pick(env.RUNPOD_API_BASE_URL, 'https://api.runpod.io');\n}\nexport function serverlessBase(env: Env): string {\n return pick(env.RUNPOD_SERVERLESS_API_URL, 'https://api.runpod.ai/v2');\n}\nexport function publicGraphqlBase(env: Env): string {\n return pick(env.RUNPOD_PUBLIC_GRAPHQL_URL, 'https://api.runpod.io/graphql');\n}\n// A separate var from publicGraphqlBase despite the identical default:\n// RUNPOD_PUBLIC_GRAPHQL_URL is the documented credential-free discovery\n// override and gets pointed at stubs freely, so routing authed calls through it\n// would turn \"point this anywhere\" into \"send the caller's API key there\".\nexport function authedGraphqlBase(env: Env): string {\n return pick(env.RUNPOD_AUTHED_GRAPHQL_URL, 'https://api.runpod.io/graphql');\n}\n"],"mappings":";;;AAUA,SAAS,KAAK,OAA2B,UAA0B;AACjE,SAAO,OAAO,KAAK,EAAE,QAAQ,QAAQ,EAAE,KAAK;AAC9C;AAGO,SAAS,QAAQ,KAAkB;AACxC,SAAO,KAAK,IAAI,qBAAqB,uBAAuB;AAC9D;AACO,SAAS,eAAe,KAAkB;AAC/C,SAAO,KAAK,IAAI,2BAA2B,0BAA0B;AACvE;AACO,SAAS,kBAAkB,KAAkB;AAClD,SAAO,KAAK,IAAI,2BAA2B,+BAA+B;AAC5E;AAKO,SAAS,kBAAkB,KAAkB;AAClD,SAAO,KAAK,IAAI,2BAA2B,+BAA+B;AAC5E;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/_shared/hosts.ts"],"sourcesContent":["// Env-resolved API base URLs, extracted from the retired dual-version backend\n// resolver (backend.ts) when the spec-generated surface became the only one.\n// The credential preflight compares only active SDK and runtime destinations.\n\nexport type Env = Record<string, string | undefined>;\n\n// '' reads as unset everywhere: the wrong-environment guard already strips\n// empty vars before comparing, and every consumer (the credential checker's\n// probe URL, the GraphQL/log clients) must resolve identically or the\n// guard's \"checker and guard cannot disagree\" invariant breaks.\nfunction pick(value: string | undefined, fallback: string): string {\n return value?.trim().replace(/\\/+$/, '') || fallback;\n}\n\n// Generated paths include /v2, so the SDK base is the API origin.\nexport function sdkBase(env: Env): string {\n return pick(env.RUNPOD_API_BASE_URL, 'https://api.runpod.io');\n}\nexport function serverlessBase(env: Env): string {\n return pick(env.RUNPOD_SERVERLESS_API_URL, 'https://api.runpod.ai/v2');\n}\nexport function publicGraphqlBase(env: Env): string {\n return pick(env.RUNPOD_PUBLIC_GRAPHQL_URL, 'https://api.runpod.io/graphql');\n}\n// A separate var from publicGraphqlBase despite the identical default:\n// RUNPOD_PUBLIC_GRAPHQL_URL is the documented credential-free discovery\n// override and gets pointed at stubs freely, so routing authed calls through it\n// would turn \"point this anywhere\" into \"send the caller's API key there\".\nexport function authedGraphqlBase(env: Env): string {\n return pick(env.RUNPOD_AUTHED_GRAPHQL_URL, 'https://api.runpod.io/graphql');\n}\n/** The hosted MCP server: install target, and stdio's search-docs backend. */\nexport function hostedMcpBase(env: Env): string {\n return pick(env.RUNPOD_MCP_URL, 'https://mcp.getrunpod.io');\n}\n/** The runpod-assistant Convex site serving /v1/search. No default: unset\n * means search is not configured. */\nexport function knowledgeSearchBase(env: Env): string | undefined {\n return pick(env.KNOWLEDGE_SEARCH_URL, '') || undefined;\n}\n"],"mappings":";;;AAUA,SAAS,KAAK,OAA2B,UAA0B;AACjE,SAAO,OAAO,KAAK,EAAE,QAAQ,QAAQ,EAAE,KAAK;AAC9C;AAGO,SAAS,QAAQ,KAAkB;AACxC,SAAO,KAAK,IAAI,qBAAqB,uBAAuB;AAC9D;AACO,SAAS,eAAe,KAAkB;AAC/C,SAAO,KAAK,IAAI,2BAA2B,0BAA0B;AACvE;AACO,SAAS,kBAAkB,KAAkB;AAClD,SAAO,KAAK,IAAI,2BAA2B,+BAA+B;AAC5E;AAKO,SAAS,kBAAkB,KAAkB;AAClD,SAAO,KAAK,IAAI,2BAA2B,+BAA+B;AAC5E;AAEO,SAAS,cAAc,KAAkB;AAC9C,SAAO,KAAK,IAAI,gBAAgB,0BAA0B;AAC5D;","names":[]}
|
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,
|
|
@@ -5716,6 +5717,8 @@ var listHubRepos = {
|
|
|
5716
5717
|
};
|
|
5717
5718
|
var deployHubRepo = {
|
|
5718
5719
|
name: "deploy-hub-repo",
|
|
5720
|
+
// Two sequential upstream calls: together they can outlast a client's request timeout.
|
|
5721
|
+
holdsResponse: true,
|
|
5719
5722
|
annotations: write,
|
|
5720
5723
|
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
5724
|
inputSchema: {
|
|
@@ -6204,6 +6207,7 @@ var runEndpoint = {
|
|
|
6204
6207
|
};
|
|
6205
6208
|
var runsyncEndpoint = {
|
|
6206
6209
|
name: "runsync-endpoint",
|
|
6210
|
+
holdsResponse: true,
|
|
6207
6211
|
annotations: write,
|
|
6208
6212
|
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
6213
|
inputSchema: {
|
|
@@ -6248,6 +6252,7 @@ var runsyncEndpoint = {
|
|
|
6248
6252
|
};
|
|
6249
6253
|
var getJobStatus = {
|
|
6250
6254
|
name: "get-job-status",
|
|
6255
|
+
holdsResponse: true,
|
|
6251
6256
|
annotations: readOnly,
|
|
6252
6257
|
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
6258
|
inputSchema: {
|
|
@@ -6296,6 +6301,7 @@ var getJobStatus = {
|
|
|
6296
6301
|
};
|
|
6297
6302
|
var streamJob = {
|
|
6298
6303
|
name: "stream-job",
|
|
6304
|
+
holdsResponse: true,
|
|
6299
6305
|
annotations: readOnly,
|
|
6300
6306
|
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
6307
|
inputSchema: {
|
|
@@ -6528,6 +6534,9 @@ function publicGraphqlBase(env) {
|
|
|
6528
6534
|
function authedGraphqlBase(env) {
|
|
6529
6535
|
return pick(env.RUNPOD_AUTHED_GRAPHQL_URL, "https://api.runpod.io/graphql");
|
|
6530
6536
|
}
|
|
6537
|
+
function knowledgeSearchBase(env) {
|
|
6538
|
+
return pick(env.KNOWLEDGE_SEARCH_URL, "") || void 0;
|
|
6539
|
+
}
|
|
6531
6540
|
|
|
6532
6541
|
// node_modules/.pnpm/@runpod+typescript-api-sdk@0.3.0/node_modules/@runpod/typescript-api-sdk/dist/index.js
|
|
6533
6542
|
var PATH_PARAM_RE = /\{[^{}]+\}/g;
|
|
@@ -7933,6 +7942,7 @@ function logParams(args) {
|
|
|
7933
7942
|
}
|
|
7934
7943
|
var streamPodLogs = {
|
|
7935
7944
|
name: "stream-pod-logs",
|
|
7945
|
+
holdsResponse: true,
|
|
7936
7946
|
annotations: readOnly,
|
|
7937
7947
|
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
7948
|
inputSchema: {
|
|
@@ -7955,6 +7965,7 @@ var streamPodLogs = {
|
|
|
7955
7965
|
};
|
|
7956
7966
|
var streamWorkerLogs = {
|
|
7957
7967
|
name: "stream-worker-logs",
|
|
7968
|
+
holdsResponse: true,
|
|
7958
7969
|
annotations: readOnly,
|
|
7959
7970
|
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
7971
|
inputSchema: {
|
|
@@ -8546,7 +8557,7 @@ function words2(name) {
|
|
|
8546
8557
|
|
|
8547
8558
|
// src/server.ts
|
|
8548
8559
|
var SERVER_NAME = "Runpod API Server";
|
|
8549
|
-
var SERVER_VERSION = true ? "4.
|
|
8560
|
+
var SERVER_VERSION = true ? "4.4.0" : "dev";
|
|
8550
8561
|
|
|
8551
8562
|
// src/specgen/analytics.ts
|
|
8552
8563
|
var import_node_crypto = require("crypto");
|
|
@@ -8876,6 +8887,177 @@ function createAlpTools(opts) {
|
|
|
8876
8887
|
return [reportFeedback, saveToJournal, askQuestion, readJournalTool];
|
|
8877
8888
|
}
|
|
8878
8889
|
|
|
8890
|
+
// src/search/knowledge.ts
|
|
8891
|
+
var SEARCH_NAMESPACES = ["docs", "knowledge", "blog", "site"];
|
|
8892
|
+
var MAX_QUERY_CHARS = 2e3;
|
|
8893
|
+
var MAX_SEARCH_LIMIT = 20;
|
|
8894
|
+
var ATTEMPTS = 7;
|
|
8895
|
+
var BASE_DELAY_MS = 250;
|
|
8896
|
+
var ATTEMPT_TIMEOUT_MS = 1e4;
|
|
8897
|
+
var RETRY_DEADLINE_MS = 3e4;
|
|
8898
|
+
var PLATFORM_LIMIT = /TooManyConcurrentRequests|Too many concurrent requests/;
|
|
8899
|
+
var AT_CAPACITY = {
|
|
8900
|
+
status: 503,
|
|
8901
|
+
body: {
|
|
8902
|
+
error: "Runpod knowledge search is at capacity.",
|
|
8903
|
+
hint: "Retry the same call in a few seconds."
|
|
8904
|
+
}
|
|
8905
|
+
};
|
|
8906
|
+
var UNREACHABLE = {
|
|
8907
|
+
status: 504,
|
|
8908
|
+
body: {
|
|
8909
|
+
error: "Runpod knowledge search did not respond.",
|
|
8910
|
+
hint: "Retry the same call once."
|
|
8911
|
+
}
|
|
8912
|
+
};
|
|
8913
|
+
function searchConfigured(env = process.env) {
|
|
8914
|
+
return Boolean(knowledgeSearchBase(env) && env.RUNPOD_ASSISTANT_API_KEY);
|
|
8915
|
+
}
|
|
8916
|
+
function parseSearchRequest(raw) {
|
|
8917
|
+
if (typeof raw.query !== "string" || !raw.query.trim()) {
|
|
8918
|
+
return { error: "query must be a non-empty string." };
|
|
8919
|
+
}
|
|
8920
|
+
if (raw.query.length > MAX_QUERY_CHARS) {
|
|
8921
|
+
return { error: `query exceeds ${MAX_QUERY_CHARS} characters.` };
|
|
8922
|
+
}
|
|
8923
|
+
const request = { query: raw.query };
|
|
8924
|
+
if (raw.namespaces !== void 0) {
|
|
8925
|
+
if (!Array.isArray(raw.namespaces) || raw.namespaces.length === 0 || !raw.namespaces.every(
|
|
8926
|
+
(ns) => SEARCH_NAMESPACES.includes(ns)
|
|
8927
|
+
)) {
|
|
8928
|
+
return {
|
|
8929
|
+
error: `namespaces must be a non-empty list of: ${SEARCH_NAMESPACES.join(", ")}.`
|
|
8930
|
+
};
|
|
8931
|
+
}
|
|
8932
|
+
request.namespaces = raw.namespaces;
|
|
8933
|
+
}
|
|
8934
|
+
if (raw.limit !== void 0) {
|
|
8935
|
+
if (typeof raw.limit !== "number" || !Number.isInteger(raw.limit) || raw.limit < 1 || raw.limit > MAX_SEARCH_LIMIT) {
|
|
8936
|
+
return {
|
|
8937
|
+
error: `limit must be an integer from 1 to ${MAX_SEARCH_LIMIT}.`
|
|
8938
|
+
};
|
|
8939
|
+
}
|
|
8940
|
+
request.limit = raw.limit;
|
|
8941
|
+
}
|
|
8942
|
+
return { request };
|
|
8943
|
+
}
|
|
8944
|
+
async function searchKnowledge(request, opts = {}) {
|
|
8945
|
+
const env = opts.env ?? process.env;
|
|
8946
|
+
const doFetch = opts.fetch ?? fetch;
|
|
8947
|
+
const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
8948
|
+
const now = opts.now ?? Date.now;
|
|
8949
|
+
if (!searchConfigured(env)) {
|
|
8950
|
+
return {
|
|
8951
|
+
status: 503,
|
|
8952
|
+
body: { error: "Knowledge search is not configured on this server." }
|
|
8953
|
+
};
|
|
8954
|
+
}
|
|
8955
|
+
const url = `${knowledgeSearchBase(env)}/v1/search`;
|
|
8956
|
+
const init = {
|
|
8957
|
+
method: "POST",
|
|
8958
|
+
headers: {
|
|
8959
|
+
Authorization: `Bearer ${env.RUNPOD_ASSISTANT_API_KEY}`,
|
|
8960
|
+
"Content-Type": "application/json"
|
|
8961
|
+
},
|
|
8962
|
+
body: JSON.stringify({ ...request, source: "runpod-mcp" })
|
|
8963
|
+
};
|
|
8964
|
+
const startedAt = now();
|
|
8965
|
+
for (let attempt = 1; ; attempt++) {
|
|
8966
|
+
try {
|
|
8967
|
+
const res = await doFetch(url, {
|
|
8968
|
+
...init,
|
|
8969
|
+
signal: AbortSignal.timeout(ATTEMPT_TIMEOUT_MS)
|
|
8970
|
+
});
|
|
8971
|
+
if (res.ok) {
|
|
8972
|
+
const data = await res.json().catch(() => null);
|
|
8973
|
+
if (!data) {
|
|
8974
|
+
console.warn("knowledge_search_failed status=200 invalid_json");
|
|
8975
|
+
return {
|
|
8976
|
+
status: 502,
|
|
8977
|
+
body: { error: "Runpod knowledge search failed." }
|
|
8978
|
+
};
|
|
8979
|
+
}
|
|
8980
|
+
return {
|
|
8981
|
+
status: 200,
|
|
8982
|
+
body: {
|
|
8983
|
+
text: String(data.text ?? ""),
|
|
8984
|
+
totalResults: Number(data.totalResults ?? 0),
|
|
8985
|
+
failedNamespaces: Array.isArray(data.failedNamespaces) ? data.failedNamespaces : []
|
|
8986
|
+
}
|
|
8987
|
+
};
|
|
8988
|
+
}
|
|
8989
|
+
const text = await res.text();
|
|
8990
|
+
const retryable = res.status === 503 || res.status === 429 || PLATFORM_LIMIT.test(text);
|
|
8991
|
+
if (!retryable) {
|
|
8992
|
+
console.warn(`knowledge_search_failed status=${res.status}`);
|
|
8993
|
+
return res.status === 400 ? { status: 400, body: { error: errorField(text) } } : { status: 502, body: { error: "Runpod knowledge search failed." } };
|
|
8994
|
+
}
|
|
8995
|
+
} catch (error) {
|
|
8996
|
+
console.warn(
|
|
8997
|
+
`knowledge_search_unreachable ${error instanceof Error ? error.name : "error"}`
|
|
8998
|
+
);
|
|
8999
|
+
return UNREACHABLE;
|
|
9000
|
+
}
|
|
9001
|
+
const delay = Math.random() * BASE_DELAY_MS * 2 ** (attempt - 1);
|
|
9002
|
+
if (attempt >= ATTEMPTS || now() - startedAt + delay > RETRY_DEADLINE_MS) {
|
|
9003
|
+
return AT_CAPACITY;
|
|
9004
|
+
}
|
|
9005
|
+
await sleep2(delay);
|
|
9006
|
+
}
|
|
9007
|
+
}
|
|
9008
|
+
function errorField(text) {
|
|
9009
|
+
try {
|
|
9010
|
+
const parsed = JSON.parse(text);
|
|
9011
|
+
if (typeof parsed.error === "string") return parsed.error;
|
|
9012
|
+
} catch {
|
|
9013
|
+
}
|
|
9014
|
+
return "Invalid search request.";
|
|
9015
|
+
}
|
|
9016
|
+
|
|
9017
|
+
// src/specgen/tools/search.ts
|
|
9018
|
+
function createSearchDocsTool(search) {
|
|
9019
|
+
return {
|
|
9020
|
+
name: "search-docs",
|
|
9021
|
+
annotations: readOnly,
|
|
9022
|
+
recordedArguments: ["limit"],
|
|
9023
|
+
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.',
|
|
9024
|
+
inputSchema: {
|
|
9025
|
+
type: "object",
|
|
9026
|
+
properties: {
|
|
9027
|
+
query: {
|
|
9028
|
+
type: "string",
|
|
9029
|
+
maxLength: MAX_QUERY_CHARS,
|
|
9030
|
+
description: "The question, in natural language."
|
|
9031
|
+
},
|
|
9032
|
+
namespaces: {
|
|
9033
|
+
type: "array",
|
|
9034
|
+
items: { type: "string", enum: [...SEARCH_NAMESPACES] },
|
|
9035
|
+
minItems: 1,
|
|
9036
|
+
description: "Sources to search: docs (docs.runpod.io), knowledge (curated support answers), blog, site (runpod.io pages). Default: docs, knowledge, blog."
|
|
9037
|
+
},
|
|
9038
|
+
limit: {
|
|
9039
|
+
type: "integer",
|
|
9040
|
+
minimum: 1,
|
|
9041
|
+
maximum: MAX_SEARCH_LIMIT,
|
|
9042
|
+
description: "Most passages to return. Default 8."
|
|
9043
|
+
}
|
|
9044
|
+
},
|
|
9045
|
+
required: ["query"],
|
|
9046
|
+
additionalProperties: false
|
|
9047
|
+
},
|
|
9048
|
+
handler: async (ctx, args) => {
|
|
9049
|
+
const parsed = parseSearchRequest(args);
|
|
9050
|
+
if ("error" in parsed) return badRequest(parsed.error);
|
|
9051
|
+
const outcome = await search(parsed.request, ctx.apiKey);
|
|
9052
|
+
return {
|
|
9053
|
+
ok: outcome.status === 200,
|
|
9054
|
+
status: outcome.status,
|
|
9055
|
+
payload: outcome.body
|
|
9056
|
+
};
|
|
9057
|
+
}
|
|
9058
|
+
};
|
|
9059
|
+
}
|
|
9060
|
+
|
|
8879
9061
|
// src/specgen/ops.ts
|
|
8880
9062
|
var import_node_crypto2 = require("crypto");
|
|
8881
9063
|
var SALT = (0, import_node_crypto2.randomBytes)(16).toString("hex");
|
|
@@ -8900,6 +9082,9 @@ var curatedTools = [
|
|
|
8900
9082
|
listPublicEndpoints,
|
|
8901
9083
|
...knowledgeTools
|
|
8902
9084
|
];
|
|
9085
|
+
var responseHoldingTools = new Set(
|
|
9086
|
+
curatedTools.filter((tool) => tool.holdsResponse).map((tool) => tool.name)
|
|
9087
|
+
);
|
|
8903
9088
|
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
9089
|
|
|
8905
9090
|
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.
|
|
@@ -8923,9 +9108,10 @@ CONTRIBUTING BACK IS OPTIONAL. Four tools feed Runpod's agent-learning pipeline,
|
|
|
8923
9108
|
function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
|
|
8924
9109
|
const rateLimiter = opts.rateLimiter ?? noopRateLimiter;
|
|
8925
9110
|
const caller = callerId(ctx.apiKey);
|
|
8926
|
-
const configuredCuratedTools = opts.alp ? [...curatedTools, ...createAlpTools(opts.alp)] : curatedTools;
|
|
8927
9111
|
const servedCuratedTools = [
|
|
8928
|
-
...
|
|
9112
|
+
...curatedTools,
|
|
9113
|
+
...opts.search ? [createSearchDocsTool(opts.search)] : [],
|
|
9114
|
+
...opts.alp ? createAlpTools(opts.alp) : [],
|
|
8929
9115
|
createDescribeTool(() => [...servedCuratedTools, ...generatedTools])
|
|
8930
9116
|
];
|
|
8931
9117
|
const callShapeOnlyTools = new Set(
|
|
@@ -9550,9 +9736,12 @@ function earlyCredentialCheckSkip(req) {
|
|
|
9550
9736
|
if (credentialCheckMayBeWrongEnvironment()) return "skip_env_mismatch";
|
|
9551
9737
|
return null;
|
|
9552
9738
|
}
|
|
9553
|
-
var
|
|
9739
|
+
var callsResponseHoldingTool = (message) => {
|
|
9740
|
+
const call = import_types2.CallToolRequestSchema.safeParse(message);
|
|
9741
|
+
return call.success && responseHoldingTools.has(call.data.params.name);
|
|
9742
|
+
};
|
|
9554
9743
|
function prefersJsonResponse(messages) {
|
|
9555
|
-
return messages.length > 0 && !messages.some(
|
|
9744
|
+
return messages.length > 0 && !messages.some(callsResponseHoldingTool);
|
|
9556
9745
|
}
|
|
9557
9746
|
var DEFAULT_REQUEST_LOG_SAMPLE_RATE = 0.1;
|
|
9558
9747
|
function requestLogSampleRate(env = process.env) {
|
|
@@ -9737,6 +9926,9 @@ async function handleMcpRequest(req, res, opts = {}) {
|
|
|
9737
9926
|
harnessSource: "user_agent"
|
|
9738
9927
|
}
|
|
9739
9928
|
} : {},
|
|
9929
|
+
// search-docs: listed only where the deployment holds the assistant's
|
|
9930
|
+
// service key; the search runs in-process with it.
|
|
9931
|
+
...searchConfigured() ? { search: (request) => searchKnowledge(request) } : {},
|
|
9740
9932
|
...notFoundLimiter ? {
|
|
9741
9933
|
rateLimiter: notFoundLimiter.rateLimiter,
|
|
9742
9934
|
onToolResult: notFoundLimiter.onToolResult
|
|
@@ -9766,6 +9958,7 @@ async function handleMcpRequest(req, res, opts = {}) {
|
|
|
9766
9958
|
0 && (module.exports = {
|
|
9767
9959
|
bodyIsCheckable,
|
|
9768
9960
|
defaultCredentialChecker,
|
|
9961
|
+
extractBearerToken,
|
|
9769
9962
|
getBaseUrl,
|
|
9770
9963
|
handleMcpRequest,
|
|
9771
9964
|
parseMcpMessages,
|