api_import
Import an OpenAPI or Swagger specification from a URL or local file to save endpoints and schemas for querying and testing.
Instructions
Importa un spec OpenAPI/Swagger desde una URL o archivo local (JSON o YAML). Guarda los endpoints y schemas para consulta.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| name | Yes | Nombre para identificar este API (ej: "mi-backend", "cocaxcode-api") | |
| source | Yes | URL o ruta a archivo local del spec OpenAPI (JSON o YAML, ej: http://localhost:3001/api-docs-json, ./openapi.yaml) |
Implementation Reference
- src/tools/api-spec.ts:66-176 (handler)The main handler function for the api_import tool. Accepts name (identifier for the API) and source (URL or file path to OpenAPI spec). Fetches from URL or reads local file, parses JSON/YAML, validates OpenAPI structure, calls parseOpenApiSpec, saves to storage, auto-associates with active environment, and returns a summary.
async (params) => { try { let rawDoc: Record<string, unknown> if (params.source.startsWith('http://') || params.source.startsWith('https://')) { // Fetch from URL const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), 30000) try { const response = await fetch(params.source, { signal: controller.signal }) if (!response.ok) { return { content: [ { type: 'text' as const, text: `Error: No se pudo descargar el spec. Status: ${response.status} ${response.statusText}`, }, ], isError: true, } } const text = await response.text() rawDoc = parseJsonOrYaml(text) } finally { clearTimeout(timeout) } } else { // Read from local file try { const content = await readFile(params.source, 'utf-8') rawDoc = parseJsonOrYaml(content) } catch { return { content: [ { type: 'text' as const, text: `Error: No se pudo leer el archivo '${params.source}'. Verifica que existe y es JSON o YAML válido.`, }, ], isError: true, } } } // Validate it looks like OpenAPI if (!rawDoc.openapi && !rawDoc.swagger) { return { content: [ { type: 'text' as const, text: 'Error: El documento no parece ser un spec OpenAPI/Swagger válido. Falta la propiedad "openapi" o "swagger".', }, ], isError: true, } } // Parse and save const spec = parseOpenApiSpec(rawDoc, params.name, params.source) await storage.saveSpec(spec) // Auto-associate with active environment const activeEnv = await storage.getActiveEnvironment() if (activeEnv) { await storage.setEnvironmentSpec(activeEnv, params.name) } // Build summary const tagCounts: Record<string, number> = {} for (const ep of spec.endpoints) { for (const tag of ep.tags ?? ['sin-tag']) { tagCounts[tag] = (tagCounts[tag] ?? 0) + 1 } } const tagSummary = Object.entries(tagCounts) .map(([tag, count]) => ` - ${tag}: ${count} endpoints`) .join('\n') const schemaCount = Object.keys(spec.schemas).length return { content: [ { type: 'text' as const, text: [ `API '${params.name}' importada correctamente.`, '', `Versión: ${spec.version ?? 'no especificada'}`, `Endpoints: ${spec.endpoints.length}`, `Schemas: ${schemaCount}`, '', 'Endpoints por tag:', tagSummary, '', activeEnv ? `Asociado al entorno '${activeEnv}'.` : '', 'Usa api_endpoints para ver los endpoints disponibles.', 'Usa api_endpoint_detail para ver el detalle de un endpoint específico.', ].join('\n'), }, ], } } catch (error) { const message = error instanceof Error ? error.message : String(error) return { content: [{ type: 'text' as const, text: `Error: ${message}` }], isError: true, } } }, - src/tools/api-spec.ts:50-177 (registration)Registration function registerApiSpecTools that calls server.tool('api_import', ...) with Zod schema for input validation (name: string, source: string). Called from src/server.ts line 65.
export function registerApiSpecTools(server: McpServer, storage: Storage): void { // ── api_import ── server.tool( 'api_import', 'Importa un spec OpenAPI/Swagger desde una URL o archivo local (JSON o YAML). Guarda los endpoints y schemas para consulta.', { name: z .string() .describe('Nombre para identificar este API (ej: "mi-backend", "cocaxcode-api")'), source: z .string() .describe( 'URL o ruta a archivo local del spec OpenAPI (JSON o YAML, ej: http://localhost:3001/api-docs-json, ./openapi.yaml)', ), }, async (params) => { try { let rawDoc: Record<string, unknown> if (params.source.startsWith('http://') || params.source.startsWith('https://')) { // Fetch from URL const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), 30000) try { const response = await fetch(params.source, { signal: controller.signal }) if (!response.ok) { return { content: [ { type: 'text' as const, text: `Error: No se pudo descargar el spec. Status: ${response.status} ${response.statusText}`, }, ], isError: true, } } const text = await response.text() rawDoc = parseJsonOrYaml(text) } finally { clearTimeout(timeout) } } else { // Read from local file try { const content = await readFile(params.source, 'utf-8') rawDoc = parseJsonOrYaml(content) } catch { return { content: [ { type: 'text' as const, text: `Error: No se pudo leer el archivo '${params.source}'. Verifica que existe y es JSON o YAML válido.`, }, ], isError: true, } } } // Validate it looks like OpenAPI if (!rawDoc.openapi && !rawDoc.swagger) { return { content: [ { type: 'text' as const, text: 'Error: El documento no parece ser un spec OpenAPI/Swagger válido. Falta la propiedad "openapi" o "swagger".', }, ], isError: true, } } // Parse and save const spec = parseOpenApiSpec(rawDoc, params.name, params.source) await storage.saveSpec(spec) // Auto-associate with active environment const activeEnv = await storage.getActiveEnvironment() if (activeEnv) { await storage.setEnvironmentSpec(activeEnv, params.name) } // Build summary const tagCounts: Record<string, number> = {} for (const ep of spec.endpoints) { for (const tag of ep.tags ?? ['sin-tag']) { tagCounts[tag] = (tagCounts[tag] ?? 0) + 1 } } const tagSummary = Object.entries(tagCounts) .map(([tag, count]) => ` - ${tag}: ${count} endpoints`) .join('\n') const schemaCount = Object.keys(spec.schemas).length return { content: [ { type: 'text' as const, text: [ `API '${params.name}' importada correctamente.`, '', `Versión: ${spec.version ?? 'no especificada'}`, `Endpoints: ${spec.endpoints.length}`, `Schemas: ${schemaCount}`, '', 'Endpoints por tag:', tagSummary, '', activeEnv ? `Asociado al entorno '${activeEnv}'.` : '', 'Usa api_endpoints para ver los endpoints disponibles.', 'Usa api_endpoint_detail para ver el detalle de un endpoint específico.', ].join('\n'), }, ], } } catch (error) { const message = error instanceof Error ? error.message : String(error) return { content: [{ type: 'text' as const, text: `Error: ${message}` }], isError: true, } } }, ) - src/tools/api-spec.ts:56-65 (schema)Zod input schema for api_import: 'name' string (identifier for the API) and 'source' string (URL or local file path for OpenAPI spec).
{ name: z .string() .describe('Nombre para identificar este API (ej: "mi-backend", "cocaxcode-api")'), source: z .string() .describe( 'URL o ruta a archivo local del spec OpenAPI (JSON o YAML, ej: http://localhost:3001/api-docs-json, ./openapi.yaml)', ), }, - src/lib/openapi-parser.ts:109-231 (helper)Helper function parseOpenApiSpec used by api_import to parse the raw OpenAPI document into structured ApiSpec object with endpoints, schemas, basePath, etc.
export function parseOpenApiSpec( doc: Record<string, unknown>, name: string, source: string, ): ApiSpec { const info = doc.info as Record<string, unknown> | undefined const paths = doc.paths as Record<string, Record<string, unknown>> | undefined const components = doc.components as Record<string, unknown> | undefined const rawSchemas = (components?.schemas ?? {}) as Record<string, ApiSpecSchema> // Extraer basePath de servers[].url (ej: "https://api.example.com/api/v1" → "/api/v1") const servers = doc.servers as Array<Record<string, unknown>> | undefined let basePath = '' if (servers && servers.length > 0 && typeof servers[0].url === 'string') { try { const serverUrl = new URL(servers[0].url) basePath = serverUrl.pathname.replace(/\/+$/, '') // quitar trailing slashes } catch { // Si no es una URL absoluta, podria ser un path relativo como "/api/v1" const rawUrl = servers[0].url as string if (rawUrl.startsWith('/')) { basePath = rawUrl.replace(/\/+$/, '') } } } // Resolve all schemas const schemas: Record<string, ApiSpecSchema> = {} for (const [schemaName, schema] of Object.entries(rawSchemas)) { schemas[schemaName] = resolveSchema(schema, doc, 0) ?? schema } const endpoints: ApiSpecEndpoint[] = [] if (paths) { for (const [path, pathItem] of Object.entries(paths)) { for (const [method, operation] of Object.entries(pathItem)) { const upperMethod = method.toUpperCase() as HttpMethod if (!VALID_METHODS.includes(upperMethod)) continue const op = operation as Record<string, unknown> // Parse parameters const rawParams = (op.parameters ?? []) as Array<Record<string, unknown>> const parameters = rawParams.map((p) => ({ name: p.name as string, in: p.in as 'path' | 'query' | 'header' | 'cookie', required: p.required as boolean | undefined, description: p.description as string | undefined, schema: resolveSchema(p.schema as ApiSpecSchema | undefined, doc), })) // Parse request body const rawBody = op.requestBody as Record<string, unknown> | undefined let requestBody = undefined if (rawBody) { const bodyContent = rawBody.content as Record<string, Record<string, unknown>> | undefined const resolvedContent: Record<string, { schema?: ApiSpecSchema }> = {} if (bodyContent) { for (const [contentType, mediaType] of Object.entries(bodyContent)) { resolvedContent[contentType] = { schema: resolveSchema(mediaType.schema as ApiSpecSchema | undefined, doc), } } } requestBody = { required: rawBody.required as boolean | undefined, description: rawBody.description as string | undefined, content: resolvedContent, } } // Parse responses const rawResponses = (op.responses ?? {}) as Record<string, Record<string, unknown>> const responses: Record<string, { description?: string; content?: Record<string, { schema?: ApiSpecSchema }> }> = {} for (const [statusCode, resp] of Object.entries(rawResponses)) { const respContent = resp.content as Record<string, Record<string, unknown>> | undefined const resolvedRespContent: Record<string, { schema?: ApiSpecSchema }> = {} if (respContent) { for (const [contentType, mediaType] of Object.entries(respContent)) { resolvedRespContent[contentType] = { schema: resolveSchema(mediaType.schema as ApiSpecSchema | undefined, doc), } } } responses[statusCode] = { description: resp.description as string | undefined, content: respContent ? resolvedRespContent : undefined, } } endpoints.push({ method: upperMethod, path: basePath ? `${basePath}${path}` : path, summary: op.summary as string | undefined, description: op.description as string | undefined, tags: op.tags as string[] | undefined, parameters, requestBody, responses, }) } } } const now = new Date().toISOString() return { name, source, version: info?.version as string | undefined, basePath: basePath || undefined, endpoints, schemas, importedAt: now, updatedAt: now, } }