rapidoc
Version:
RapiDoc - Open API spec viewer with built in console
167 lines (151 loc) • 5.1 kB
JavaScript
/**
* MCP (Model Context Protocol) helpers shared by the manifest builder and the project writer.
*/
import { envNameFor } from "./runtime.js";
export { envNameFor };
// Auth and transport headers never exposed as tool arguments
export const AUTH_TRANSPORT_HEADERS = new Set([
"authorization",
"cookie",
"set-cookie",
"x-api-key",
"api-key",
"apikey",
"api_key",
"user-agent",
"host",
"content-length",
"accept",
"accept-encoding",
"connection",
]);
/**
* Reads the `mcp` attribute: export is off for an empty value, `false`, `none` or a list that
* contains one of them (the old kill-switch), and on for anything else, including the old
* language names.
*
* @param {string|string[]|boolean} val
* @returns {boolean}
*/
export function parseMcpEnabled(val) {
if (!val || val === "false") return false;
const items = Array.isArray(val) ? val : String(val).split(",");
const normalized = items.map((s) => String(s).trim().toLowerCase()).filter(Boolean);
if (!normalized.length) return false;
return !normalized.some((s) => s === "none" || s === "false");
}
/**
* Converts camelCase, PascalCase, kebab-case, or arbitrary text into clean snake_case.
*/
export function toSnakeCase(str) {
if (!str) return "tool";
return str
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
.replace(/[^a-zA-Z0-9_]/g, "_")
.replace(/_+/g, "_")
.replace(/^_|_$/g, "")
.toLowerCase();
}
/**
* Sanitizes markdown/HTML descriptions into clean, concise single or multi-line agent instructions.
*/
export function sanitizeDescription(text, maxLength = 300) {
if (!text || typeof text !== "string") return "";
const cleaned = text
.replace(/<[^>]*>/g, "") // remove HTML tags
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // [link](url) -> link
.replace(/[`*_~#<>]/g, "") // strip markdown markers, preserve sentence punctuation
.replace(/\s+/g, " ")
.trim();
if (cleaned.length <= maxLength) return cleaned;
return cleaned.slice(0, maxLength - 3) + "...";
}
/**
* Derives a clean snake_case tool name from HTTP method and path when operationId is missing.
* e.g., GET /customers/{customerId}/orders -> get_customers_by_customer_id_orders
*/
export function deriveToolName(method, path) {
const parts = path
.split("/")
.filter(Boolean)
.map((seg) => {
if (seg.startsWith("{") && seg.endsWith("}")) {
return `by_${seg.slice(1, -1)}`;
}
return seg;
});
return toSnakeCase(`${method}_${parts.join("_")}`);
}
/**
* Estimates the prompt context a manifest's tool definitions cost an agent on every turn.
*
* @param {object} manifest from buildMcpManifest()
* @returns {{ totalTools: number, totalResources: number, estimatedTokens: number,
* consequentialCount: number, warnings: string[] }} `consequentialCount` is the number of
* tools that are not read-only
*/
export function estimateTokenCost(manifest) {
const tools = manifest?.tools || [];
const resources = manifest?.resources || [];
let estimatedTokens = 0;
let consequentialCount = 0;
for (const t of tools) {
// Name, description and the JSON Schema: ~4 chars per token, plus the envelope
estimatedTokens += Math.ceil(
(t.name.length + (t.description || "").length + JSON.stringify(t.inputSchema || {}).length) /
4,
);
estimatedTokens += 25;
if (!t.annotations?.readOnlyHint) consequentialCount++;
}
for (const r of resources) {
estimatedTokens +=
Math.ceil((r.name.length + (r.description || "").length + r.uri.length) / 4) + 20;
}
const warnings = [];
if (estimatedTokens > 5000) {
warnings.push(
`Context overhead (~${estimatedTokens.toLocaleString()} tokens/turn) is high. Consider hiding unused operations with x-internal: true.`,
);
}
if (tools.length > 30) {
warnings.push(
`${tools.length} tools exceeds the recommended limit of 30 tools per agent to prevent model paralysis.`,
);
}
return {
totalTools: tools.length,
totalResources: resources.length,
estimatedTokens,
consequentialCount,
warnings,
};
}
/**
* Generates Claude Desktop and Cursor JSON configuration snippets for the exported project.
*
* @param {string} serverName
* @param {object} [options]
* @param {string[]} [options.schemes] security scheme ids; one `env` entry each (`API_KEY` when none)
*/
export function generateHostConfigs(serverName, options = {}) {
const safeName = toSnakeCase(serverName || "api_mcp_server");
const schemes = options.schemes?.length ? options.schemes : [];
const env = {};
for (const id of schemes) env[envNameFor(id)] = `your-${toSnakeCase(id).replace(/_/g, "-")}`;
if (!schemes.length) env.API_KEY = "your-api-key-here";
const claudeConfig = {
mcpServers: {
[safeName]: {
command: "npx",
args: ["-y", "tsx", "server.ts"],
env,
},
},
};
const cursorConfig = claudeConfig;
return {
claudeDesktopConfig: JSON.stringify(claudeConfig, null, 2),
cursorConfig: JSON.stringify(cursorConfig, null, 2),
};
}