lib/code-mode-docs.ts
Rama main · réplica de lectura del repositorio original.
import type { McpServer, OpenApiEndpoint } from "./types";
export type SearchDocsDetail = "compact" | "schema" | "examples" | "auth";
type SchemaPropertyDoc = {
name: string;
type: string;
required: boolean;
location?: string;
source_name?: string;
description?: string;
enum?: unknown[];
format?: string;
};
const DETAIL_MODES: SearchDocsDetail[] = ["compact", "schema", "examples", "auth"];
const PUBLIC_READ_METHODS = new Set(["GET", "HEAD", "OPTIONS", "BROWSER"]);
const SENSITIVE_KEY_PATTERN = /(^|_|\b)(api_?key|access_?token|authorization|bearer|client_?secret|credential|password|refresh_?token|secret|signature|token)($|_|\b)/i;
const BEARER_PATTERN = /Bearer\s+[A-Za-z0-9._~+/=-]+/gi;
const ASSIGNMENT_SECRET_PATTERN = /\b(api_?key|access_?token|authorization|bearer|client_?secret|password|refresh_?token|secret|token)=([^&\s"'`]+)/gi;
const STOP_WORDS = new Set([
"a",
"an",
"and",
"api",
"by",
"for",
"from",
"get",
"in",
"of",
"on",
"or",
"the",
"to",
"with",
]);
export function camelCase(value: string) {
const cleaned = value
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
.replace(/[^a-zA-Z0-9]+/g, " ")
.trim()
.split(/\s+/)
.filter(Boolean);
if (cleaned.length === 0) return "api";
return cleaned
.map((part, index) => {
const lower = part.toLowerCase();
return index === 0 ? lower : lower.charAt(0).toUpperCase() + lower.slice(1);
})
.join("");
}
export function endpointId(endpoint: OpenApiEndpoint) {
return endpoint.tool_name || endpoint.operation_id || `${endpoint.method} ${endpoint.path}`;
}
export function sdkResource(endpoint: OpenApiEndpoint) {
return camelCase(endpoint.resource || endpoint.tags?.[0] || endpoint.path.split("/").find((part) => part && !part.startsWith("{")) || "api");
}
export function sdkMethod(endpoint: OpenApiEndpoint) {
if (endpoint.operation_id) return camelCase(endpoint.operation_id);
const verb = endpoint.operation_kind === "read"
? endpoint.path.includes("{") ? "get" : "list"
: endpoint.operation_kind === "destructive"
? "delete"
: endpoint.method.toUpperCase() === "POST"
? "create"
: "update";
const leaf = endpoint.path
.split("/")
.filter((part) => part && !part.startsWith("{"))
.pop() || "resource";
return camelCase(`${verb} ${leaf}`);
}
function truncate(value: string | null | undefined, max = 240) {
if (!value) return null;
const normalized = redactText(value).replace(/\s+/g, " ").trim();
if (normalized.length <= max) return normalized;
return `${normalized.slice(0, Math.max(0, max - 1)).trim()}...`;
}
function tokenize(value: unknown) {
if (typeof value !== "string") return [];
return value
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
.toLowerCase()
.split(/[^a-z0-9]+/)
.map((token) => token.trim())
.filter((token) => token.length > 1 && !STOP_WORDS.has(token));
}
function stringValue(value: unknown) {
return typeof value === "string" ? value : "";
}
function redactText(value: string) {
return value
.replace(BEARER_PATTERN, "Bearer [redacted]")
.replace(ASSIGNMENT_SECRET_PATTERN, (_match, key) => `${key}=[redacted]`);
}
function redactSensitive<T>(value: T, seen = new WeakSet<object>()): T {
if (typeof value === "string") return redactText(value) as T;
if (!value || typeof value !== "object") return value;
if (seen.has(value)) return "[redacted]" as T;
seen.add(value);
if (Array.isArray(value)) {
return value.map((item) => redactSensitive(item, seen)) as T;
}
const output: Record<string, unknown> = {};
for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {
output[key] = SENSITIVE_KEY_PATTERN.test(key) ? "[redacted]" : redactSensitive(nested, seen);
}
return output as T;
}
function endpointVisibility(value: unknown) {
return value === "public" || value === "private" ? value : null;
}
function record(value: unknown) {
return value && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
: {};
}
function endpointToolVisibility(server: McpServer, endpoint: OpenApiEndpoint) {
const ids = new Set([
endpointId(endpoint),
endpoint.tool_name,
endpoint.operation_id,
].filter((value): value is string => typeof value === "string" && value.length > 0));
const tools = Array.isArray(server.tools_json) ? server.tools_json : [];
for (const tool of tools) {
const matches = ids.has(tool.name) || Boolean(
tool.method
&& tool.path
&& tool.method.toUpperCase() === endpoint.method.toUpperCase()
&& tool.path === endpoint.path
);
if (!matches) continue;
const visibility = endpointVisibility(tool.visibility)
?? endpointVisibility(tool.x_astrail?.visibility)
?? endpointVisibility(record(tool.metadata).visibility);
if (visibility) return visibility;
}
return null;
}
function endpointHasSecurityRequirement(endpoint: OpenApiEndpoint) {
if (endpoint.requires_auth === true) return true;
const security = endpoint.security_requirements ?? endpoint.security;
if (!security) return false;
if (Array.isArray(security)) return security.length > 0;
if (typeof security === "object") return Object.keys(security).length > 0;
return Boolean(security);
}
function endpointOperationKind(endpoint: OpenApiEndpoint) {
if (endpoint.operation_kind) return endpoint.operation_kind;
const method = endpoint.method.toUpperCase();
if (PUBLIC_READ_METHODS.has(method)) return "read";
if (method === "DELETE") return "destructive";
return "write";
}
function isPublicDocsEndpoint(server: McpServer, endpoint: OpenApiEndpoint) {
if (endpointVisibility(endpoint.visibility) === "private") return false;
if (endpointToolVisibility(server, endpoint) === "private") return false;
if (endpointHasSecurityRequirement(endpoint)) return false;
return endpointOperationKind(endpoint) === "read" && PUBLIC_READ_METHODS.has(endpoint.method.toUpperCase());
}
export function visibleDocsEndpoints(server: McpServer) {
const endpoints = (server.endpoint_map ?? [])
.filter((endpoint) => !["ASTRAIL_META", "ASTRAIL_CODE"].includes(endpoint.method.toUpperCase()));
return server.is_public ? endpoints.filter((endpoint) => isPublicDocsEndpoint(server, endpoint)) : endpoints;
}
function schemaType(schema: Record<string, unknown>) {
if (typeof schema.type === "string") return schema.type;
if (schema.$ref) return "ref";
if (Array.isArray(schema.enum)) return "enum";
if (schema.anyOf || schema.oneOf) return "union";
return "unknown";
}
export function exampleArgumentsFromSchema(schema: unknown) {
if (!schema || typeof schema !== "object") return {};
const record = schema as Record<string, unknown>;
const properties = record.properties && typeof record.properties === "object"
? record.properties as Record<string, unknown>
: {};
const required = Array.isArray(record.required) ? record.required : [];
const args: Record<string, unknown> = {};
for (const [name, property] of Object.entries(properties).slice(0, 8)) {
const prop = property && typeof property === "object" ? property as Record<string, unknown> : {};
if (!required.includes(name) && Object.keys(args).length >= 3) continue;
if (prop.example !== undefined) args[name] = prop.example;
else if (Array.isArray(prop.enum) && prop.enum.length > 0) args[name] = prop.enum[0];
else if (prop.default !== undefined) args[name] = prop.default;
else if (prop.type === "integer" || prop.type === "number") args[name] = 1;
else if (prop.type === "boolean") args[name] = true;
else if (prop.type === "array") args[name] = [];
else if (prop.type === "object") args[name] = {};
else args[name] = name.toLowerCase().includes("id") ? "example_id" : "example";
args[name] = SENSITIVE_KEY_PATTERN.test(name) ? "[redacted]" : redactSensitive(args[name]);
}
return args;
}
export function schemaProperties(schema: unknown) {
if (!schema || typeof schema !== "object") return [] as SchemaPropertyDoc[];
const record = schema as Record<string, unknown>;
const properties = record.properties && typeof record.properties === "object"
? record.properties as Record<string, unknown>
: {};
const required = Array.isArray(record.required) ? record.required : [];
return Object.entries(properties).slice(0, 16).map(([name, property]) => {
const prop = property && typeof property === "object" ? property as Record<string, unknown> : {};
return {
name,
type: schemaType(prop),
required: required.includes(name),
location: stringValue(prop["x-astrail-in"]) || undefined,
source_name: stringValue(prop["x-astrail-name"]) || undefined,
description: truncate(redactText(stringValue(prop.description)), 160) ?? undefined,
enum: Array.isArray(prop.enum) ? redactSensitive(prop.enum.slice(0, 8)) : undefined,
format: stringValue(prop.format) || undefined,
};
});
}
function collectParameterNames(endpoint: OpenApiEndpoint) {
const params = Array.isArray(endpoint.parameters) ? endpoint.parameters : [];
return params
.map((param) => param && typeof param === "object" ? (param as Record<string, unknown>).name : null)
.filter((name): name is string => typeof name === "string")
.map((name) => name.toLowerCase());
}
export function paginationHint(endpoint: OpenApiEndpoint) {
const names = collectParameterNames(endpoint);
if (names.some((name) => ["cursor", "starting_after", "ending_before", "next_cursor", "page_token"].includes(name))) {
return { type: "cursor", fields: names.filter((name) => name.includes("cursor") || name.includes("token") || name.includes("after") || name.includes("before")) };
}
if (names.some((name) => ["page", "per_page", "page_size"].includes(name))) {
return { type: "page", fields: names.filter((name) => name.includes("page")) };
}
if (names.some((name) => ["offset", "limit"].includes(name))) {
return { type: "offset", fields: names.filter((name) => ["offset", "limit"].includes(name)) };
}
return null;
}
export function securitySchemeNames(value: unknown) {
const names = new Set<string>();
const collect = (item: unknown) => {
if (!item) return;
if (typeof item === "string") {
names.add(item);
return;
}
if (Array.isArray(item)) {
for (const child of item) collect(child);
return;
}
if (typeof item === "object") {
for (const [key, child] of Object.entries(item as Record<string, unknown>)) {
names.add(key);
if (Array.isArray(child) && child.every((value) => typeof value === "string")) continue;
collect(child);
}
}
};
collect(value);
return Array.from(names).filter(Boolean).sort();
}
export function authSummary(endpoint: OpenApiEndpoint) {
const security = endpoint.security_requirements ?? endpoint.security ?? null;
const schemes = securitySchemeNames(security);
return {
required: Boolean(endpoint.requires_auth),
schemes,
note: endpoint.requires_auth
? "Provider credentials must be configured in Astrail before execute can call this upstream endpoint."
: "No upstream auth requirement was declared for this endpoint.",
};
}
function responseHints(endpoint: OpenApiEndpoint) {
const hints = Array.isArray(endpoint.response_hints) ? endpoint.response_hints : [];
return hints.slice(0, 4).map((hint) => {
const record = hint && typeof hint === "object" ? hint as Record<string, unknown> : {};
return {
status: stringValue(record.status),
description: truncate(redactText(stringValue(record.description)), 140),
content_types: Array.isArray(record.content_types) ? record.content_types.slice(0, 4) : undefined,
schema_hint: redactSensitive(record.schema_hint ?? undefined),
};
});
}
function endpointExamples(endpoint: OpenApiEndpoint) {
const resource = sdkResource(endpoint);
const method = sdkMethod(endpoint);
const exampleArgs = exampleArgumentsFromSchema(endpoint.input_schema);
const argJson = JSON.stringify(exampleArgs, null, 2);
const call = `client.${resource}.${method}(${argJson})`;
const isListLikeRead = endpoint.operation_kind === "read" && (method.toLowerCase().startsWith("list") || !endpoint.path.includes("{"));
return {
arguments: exampleArgs,
typescript: `const result = await ${call};`,
iterable_typescript: isListLikeRead
? `const results = [];\nfor await (const item of ${call}) {\n results.push(item);\n}\nreturn results;`
: null,
};
}
export function endpointDocsCorpus(endpoint: OpenApiEndpoint) {
const resource = sdkResource(endpoint);
const method = sdkMethod(endpoint);
const args = schemaProperties(endpoint.input_schema);
const auth = authSummary(endpoint);
const pagination = paginationHint(endpoint);
const sdkMethodName = `client.${resource}.${method}`;
const required_arguments = args.filter((arg) => arg.required).map((arg) => arg.name);
const searchable = [
sdkMethodName,
endpointId(endpoint),
endpoint.method,
endpoint.path,
endpoint.operation_id,
endpoint.summary,
endpoint.description,
endpoint.resource,
endpoint.operation_kind,
...(endpoint.tags ?? []),
...args.flatMap((arg) => [arg.name, arg.source_name, arg.location, arg.description, arg.type]),
...auth.schemes,
pagination?.type,
].filter(Boolean).join(" ");
return {
sdk_method: sdkMethodName,
endpoint_id: endpointId(endpoint),
method: endpoint.method,
path: endpoint.path,
resource,
operation: endpoint.operation_kind,
title: endpoint.summary || `${endpoint.method} ${endpoint.path}`,
searchable_text: truncate(searchable, 2000) ?? "",
required_arguments,
argument_count: args.length,
auth,
pagination,
response_hints: responseHints(endpoint),
examples: endpointExamples(endpoint),
};
}
export function sdkDocForEndpoint(endpoint: OpenApiEndpoint, detail: SearchDocsDetail = "compact", score?: number, matchedTerms?: string[]) {
const corpus = endpointDocsCorpus(endpoint);
const args = schemaProperties(endpoint.input_schema);
const compactDoc = {
sdk_method: corpus.sdk_method,
endpoint_id: corpus.endpoint_id,
http: `${endpoint.method} ${endpoint.path}`,
method: endpoint.method,
path: endpoint.path,
resource: corpus.resource,
operation: corpus.operation,
summary: truncate(endpoint.summary, 180),
description: truncate(endpoint.description, 320),
requires_auth: corpus.auth.required,
auth_schemes: corpus.auth.schemes,
tags: endpoint.tags ?? [],
required_fields: corpus.required_arguments,
arguments: args,
pagination: corpus.pagination,
response_hints: corpus.response_hints,
example: corpus.examples.typescript,
iterable_example: corpus.examples.iterable_typescript,
score,
matched_terms: matchedTerms,
execution_notes: [
"Use only the returned client.resource.method shape with execute.",
"Astrail statically compiles supported SDK calls to endpoint-map execution; arbitrary JavaScript is not evaluated.",
corpus.operation === "destructive" || corpus.operation === "write" ? "Ask the user to confirm intent before write or destructive operations." : null,
corpus.pagination ? `Pagination appears ${corpus.pagination.type}-based; pass the documented pagination fields when continuing a list.` : null,
].filter(Boolean),
};
if (detail === "schema") {
return redactSensitive({
...compactDoc,
input_schema: redactSensitive(endpoint.input_schema ?? { type: "object", properties: {} }),
request_body_schema: redactSensitive(endpoint.request_body_schema ?? null),
parameters: redactSensitive(Array.isArray(endpoint.parameters) ? endpoint.parameters.slice(0, 20) : []),
});
}
if (detail === "examples") {
return redactSensitive({
...compactDoc,
examples: corpus.examples,
});
}
if (detail === "auth") {
return redactSensitive({
...compactDoc,
auth: {
...corpus.auth,
schemes: [...corpus.auth.schemes],
security: redactSensitive(endpoint.security_requirements ?? endpoint.security ?? null),
},
});
}
return redactSensitive(compactDoc);
}
function weightedSearchFields(endpoint: OpenApiEndpoint) {
const corpus = endpointDocsCorpus(endpoint);
const args = schemaProperties(endpoint.input_schema);
return [
{ weight: 18, text: corpus.sdk_method },
{ weight: 16, text: endpoint.operation_id },
{ weight: 14, text: endpoint.summary },
{ weight: 12, text: `${endpoint.method} ${endpoint.path}` },
{ weight: 10, text: corpus.resource },
{ weight: 8, text: endpoint.description },
{ weight: 7, text: (endpoint.tags ?? []).join(" ") },
{ weight: 6, text: args.map((arg) => `${arg.name} ${arg.source_name ?? ""} ${arg.description ?? ""}`).join(" ") },
{ weight: 4, text: responseHints(endpoint).map((hint) => `${hint.status} ${hint.description ?? ""}`).join(" ") },
{ weight: 3, text: authSummary(endpoint).schemes.join(" ") },
{ weight: 2, text: corpus.searchable_text },
];
}
function scoreEndpoint(endpoint: OpenApiEndpoint, query: string) {
const tokens = tokenize(query);
if (tokens.length === 0) return { score: 1, matchedTerms: [] };
const phrase = query.toLowerCase().trim();
let score = 0;
const matched = new Set<string>();
for (const field of weightedSearchFields(endpoint)) {
const text = stringValue(field.text).toLowerCase();
if (!text) continue;
if (phrase.length > 2 && text.includes(phrase)) {
score += field.weight * 3;
}
const fieldTokens = new Set(tokenize(text));
for (const token of tokens) {
if (fieldTokens.has(token)) {
score += field.weight;
matched.add(token);
} else if (text.includes(token)) {
score += field.weight / 2;
matched.add(token);
}
}
}
if (tokens.length > 0 && matched.size === tokens.length) score += 20;
if (endpoint.operation_kind === "read" && tokens.some((token) => ["list", "search", "find", "retrieve", "fetch"].includes(token))) score += 4;
return { score, matchedTerms: Array.from(matched) };
}
function normalizeDetailMode(value: unknown): SearchDocsDetail {
return DETAIL_MODES.includes(value as SearchDocsDetail) ? value as SearchDocsDetail : "compact";
}
export function searchDocs(server: McpServer, args: Record<string, unknown>) {
const query = typeof args.query === "string" ? args.query.trim() : "";
const resource = typeof args.resource === "string" ? camelCase(args.resource).toLowerCase() : "";
const operation = typeof args.operation === "string" ? args.operation.toLowerCase().trim() : "";
const detail = normalizeDetailMode(args.detail);
const limit = Math.max(1, Math.min(Number(args.limit ?? 8) || 8, 20));
const allDocsEndpoints = (server.endpoint_map ?? [])
.filter((endpoint) => !["ASTRAIL_META", "ASTRAIL_CODE"].includes(endpoint.method.toUpperCase()));
const visibleEndpoints = visibleDocsEndpoints(server);
const scored = visibleEndpoints
.map((endpoint) => {
const doc = endpointDocsCorpus(endpoint);
const ranking = scoreEndpoint(endpoint, query);
return { endpoint, doc, ...ranking };
})
.filter((item) => {
if (query && item.score <= 0) return false;
if (resource && item.doc.resource.toLowerCase() !== resource) return false;
if (operation && item.doc.operation !== operation) return false;
return true;
})
.sort((a, b) => b.score - a.score || a.doc.sdk_method.localeCompare(b.doc.sdk_method));
return {
status: "success",
server: server.name,
mode: "astrail_code_mode",
detail,
total_matches: scored.length,
returned: Math.min(scored.length, limit),
docs_corpus: {
version: "2026-06-23",
total_endpoints: allDocsEndpoints.length,
searched_endpoints: visibleEndpoints.length,
visibility_filter: server.is_public
? "public servers expose only read endpoints without upstream auth and without visibility=private"
: "private servers expose the authenticated endpoint map",
fields: [
"sdk_method",
"http",
"summary",
"arguments",
"required_fields",
"auth",
"pagination",
"response_hints",
"examples",
],
ranking: "weighted token scoring over SDK method, operationId, summary, path, parameters, tags, auth, and response hints",
},
docs: scored.slice(0, limit).map((item) => sdkDocForEndpoint(item.endpoint, detail, Math.round(item.score * 100) / 100, item.matchedTerms)),
execute_contract: {
supported_call_shapes: [
"await client.resource.method({ jsonCompatible: true })",
"for await (const item of client.resource.list({ jsonCompatible: true })) { ... }",
],
batching: "Independent read calls in one execute request are compiled and run in parallel.",
note: "Astrail statically compiles SDK-style calls to endpoint-map execution. It does not eval arbitrary JavaScript.",
},
};
}