diff --git a/src/mcp-server/tools/pipeshubSources.ts b/src/mcp-server/tools/pipeshubSources.ts index 9d022c7..ca79348 100644 --- a/src/mcp-server/tools/pipeshubSources.ts +++ b/src/mcp-server/tools/pipeshubSources.ts @@ -1,6 +1,7 @@ import * as z from "zod"; import { knowledgeHubGetKnowledgeHubRootNodes } from "../../funcs/knowledgeHubGetKnowledgeHubRootNodes.js"; import { aiModelsProvidersGetAvailableModelsByType } from "../../funcs/aiModelsProvidersGetAvailableModelsByType.js"; +import { APIError } from "../../models/errors/apierror.js"; import { ToolDefinition } from "../tools.js"; import { errorResult, jsonResult, readJson } from "./_helpers.js"; @@ -26,14 +27,13 @@ Returns up to three sections: \`pipeshub_search\`'s \`apps\` filter. - \`llmModels\` — chat / generation models. Each item's \`modelKey\` is the value to pass on \`pipeshub_chat\` / \`pipeshub_search\` as - \`modelKey\`. Pick \`isDefault: true\` unless the user asks for a - specific model. -- \`embeddingModels\` — vector embedding models (only fetched when - explicitly requested via \`include\`). + \`modelKey\`. Pick \`isDefault: true\` unless specified. Missing + \`config:read\` returns \`[]\` plus \`llmModelsRestricted: true\`. +- \`embeddingModels\` — vector embedding models (fetched only via + \`include\`). Same restriction applies. -Call this once at the start of a session and cache the result — -sources and models change infrequently. \`sources\` and \`llmModels\` -are returned by default; pass \`include\` to override.`, +Call once per session and cache — sources/models change rarely. +\`sources\`/\`llmModels\` return by default; pass \`include\` to override.`, scopes: ["read"], annotations: { title: "List PipesHub sources and AI models", @@ -78,7 +78,22 @@ are returned by default; pass \`include\` to override.`, const [r] = await aiModelsProvidersGetAvailableModelsByType(client, { modelType, }, { fetchOptions }).$inspect(); - if (!r.ok) return errorResult(`${key}: ${r.error.message}`); + if (!r.ok) { + // `llmModels`/`embeddingModels` are gated by config:read, which a + // caller's token may not have. That denial degrades silently to an + // empty array rather than a 403. Reporting the distinction keeps + // "nothing configured" from looking the same as "not authorized to + // see it" (see issue #2942). + const status = r.error instanceof APIError + ? r.error.httpMeta.response.status + : undefined; + if (status === 403) { + result[key] = []; + result[`${key}Restricted`] = true; + continue; + } + return errorResult(`${key}: ${r.error.message}`); + } const parsed = await readJson<{ models?: any[] }>(r.value, "Model listing"); if (!parsed.ok) return parsed.result; result[key] = (parsed.value.models ?? []).map((m: any) => ({ @@ -93,4 +108,4 @@ are returned by default; pass \`include\` to override.`, return jsonResult(result); }, -}; +}; \ No newline at end of file