Skip to main content
Glama
api.ts9.01 kB
/** * Generic API executor for making authenticated Onboarded API calls. * * Takes operation details from the OpenAPI spec and executes the call * with proper authentication and parameter handling. */ import type { ApiType, EnvType, Parameter, SchemaObject } from "./spec.js"; import { describeOperation, getSpec } from "./spec.js"; import { getToken, type GetTokenOptions } from "./keychain.js"; const BASE_URLS: Record<EnvType, Record<ApiType, string>> = { prod: { v1: "https://app.onboarded.com", internal: "https://app.onboarded.com", }, staging: { v1: "https://staging.onboarded.com", internal: "https://staging.onboarded.com", }, }; export interface ApiCallInput { api: ApiType; env?: EnvType; operationId: string; params?: Record<string, unknown>; body?: unknown; profile?: string; dryRun?: boolean; } export interface ApiCallResult { success: boolean; status?: number; statusText?: string; data?: unknown; error?: string; request?: { method: string; url: string; headers: Record<string, string>; body?: unknown; }; } /** * Build the full URL for an API call, substituting path parameters. */ function buildUrl( baseUrl: string, path: string, pathParams: Record<string, unknown>, queryParams: Record<string, unknown> ): string { // Substitute path parameters let url = path; for (const [key, value] of Object.entries(pathParams)) { url = url.replace(`{${key}}`, encodeURIComponent(String(value))); } // Build full URL const fullUrl = new URL(baseUrl + url); // Add query parameters for (const [key, value] of Object.entries(queryParams)) { if (value !== undefined && value !== null) { if (Array.isArray(value)) { for (const item of value) { fullUrl.searchParams.append(key, String(item)); } } else { fullUrl.searchParams.set(key, String(value)); } } } return fullUrl.toString(); } /** * Separate parameters into path, query, and header categories. */ function categorizeParams( parameters: Parameter[], inputParams: Record<string, unknown> ): { pathParams: Record<string, unknown>; queryParams: Record<string, unknown>; headerParams: Record<string, unknown>; } { const pathParams: Record<string, unknown> = {}; const queryParams: Record<string, unknown> = {}; const headerParams: Record<string, unknown> = {}; for (const param of parameters) { const value = inputParams[param.name]; if (value === undefined) { continue; } switch (param.in) { case "path": pathParams[param.name] = value; break; case "query": queryParams[param.name] = value; break; case "header": headerParams[param.name] = value; break; } } return { pathParams, queryParams, headerParams }; } /** * Validate required parameters. */ function validateParams( parameters: Parameter[], inputParams: Record<string, unknown> ): string[] { const errors: string[] = []; for (const param of parameters) { if (param.required && inputParams[param.name] === undefined) { errors.push(`Missing required parameter: ${param.name} (${param.in})`); } } return errors; } /** * Execute an API call based on an OpenAPI operation. */ export async function executeApiCall(input: ApiCallInput): Promise<ApiCallResult> { const { api, env = "prod", operationId, params = {}, body, profile, dryRun } = input; // Get operation details from spec const opDetails = describeOperation(api, operationId, env); if (!opDetails) { return { success: false, error: `Operation '${operationId}' not found in ${api} spec (${env}). Did you run spec.sync?`, }; } const { operation, parameters } = opDetails; // Validate required parameters const validationErrors = validateParams(parameters, params); if (validationErrors.length > 0) { return { success: false, error: `Parameter validation failed:\n${validationErrors.join("\n")}`, }; } // Categorize parameters const { pathParams, queryParams, headerParams } = categorizeParams(parameters, params); // Build URL const baseUrl = BASE_URLS[env][api]; const url = buildUrl(baseUrl, operation.path, pathParams, queryParams); // Build headers const headers: Record<string, string> = { "Content-Type": "application/json", Accept: "application/json", ...Object.fromEntries( Object.entries(headerParams).map(([k, v]) => [k, String(v)]) ), }; // Get auth token const tokenOptions: GetTokenOptions = {}; if (profile) { tokenOptions.profile = profile; } const tokenResult = getToken(tokenOptions); if (!tokenResult.found) { return { success: false, error: `No auth token found. ${tokenResult.error ?? "Run 'onboarded auth login' to authenticate."}`, }; } headers["Authorization"] = `Bearer ${tokenResult.token}`; // Prepare request info for dry run const requestInfo = { method: operation.method, url, headers: { ...headers, Authorization: "Bearer [REDACTED]" }, body, }; // Dry run - return the request that would be made if (dryRun) { return { success: true, request: requestInfo, }; } // Execute the actual request try { const fetchOptions: RequestInit = { method: operation.method, headers, }; if (body && ["POST", "PUT", "PATCH"].includes(operation.method)) { fetchOptions.body = JSON.stringify(body); } const response = await fetch(url, fetchOptions); let data: unknown; const contentType = response.headers.get("content-type"); if (contentType?.includes("application/json")) { data = await response.json(); } else { data = await response.text(); } return { success: response.ok, status: response.status, statusText: response.statusText, data, request: requestInfo, error: response.ok ? undefined : `API returned ${response.status}: ${response.statusText}`, }; } catch (error) { return { success: false, error: `Request failed: ${error instanceof Error ? error.message : String(error)}`, request: requestInfo, }; } } /** * Get the base URL for an API/environment combination. */ export function getBaseUrl(api: ApiType, env: EnvType = "prod"): string { return BASE_URLS[env][api]; } /** * Generate a description of the required and optional parameters for an operation. */ export function describeOperationParams( api: ApiType, operationId: string, env: EnvType = "prod" ): string | null { const opDetails = describeOperation(api, operationId, env); if (!opDetails) { return null; } const { operation, parameters, requestBodySchema } = opDetails; const lines: string[] = []; lines.push(`# ${operation.method} ${operation.path}`); if (operation.summary) { lines.push(`## ${operation.summary}`); } if (operation.description) { lines.push(operation.description); } lines.push(""); // Parameters if (parameters.length > 0) { lines.push("## Parameters"); for (const param of parameters) { const required = param.required ? " (required)" : ""; const type = param.schema?.type ?? "any"; lines.push(`- **${param.name}**${required}: ${type}${param.description ? ` - ${param.description}` : ""}`); } lines.push(""); } // Request body if (requestBodySchema) { lines.push("## Request Body"); lines.push(formatSchema(requestBodySchema)); lines.push(""); } return lines.join("\n"); } /** * Format a schema object as a readable string. */ function formatSchema(schema: SchemaObject, indent: number = 0): string { const pad = " ".repeat(indent); if (schema.$ref) { // Handle $ref (simplified - just show the reference) const refName = schema.$ref.split("/").pop(); return `${pad}$ref: ${refName}`; } if (schema.type === "object" && schema.properties) { const lines: string[] = []; const required = new Set(schema.required ?? []); for (const [key, propSchema] of Object.entries(schema.properties)) { const isRequired = required.has(key); const reqMarker = isRequired ? " (required)" : ""; const type = propSchema.type ?? "any"; const desc = propSchema.description ? ` - ${propSchema.description}` : ""; if (propSchema.type === "object" && propSchema.properties) { lines.push(`${pad}- **${key}**${reqMarker}: object${desc}`); lines.push(formatSchema(propSchema, indent + 1)); } else if (propSchema.type === "array" && propSchema.items) { const itemType = propSchema.items.type ?? "any"; lines.push(`${pad}- **${key}**${reqMarker}: array of ${itemType}${desc}`); } else { lines.push(`${pad}- **${key}**${reqMarker}: ${type}${desc}`); } } return lines.join("\n"); } return `${pad}${schema.type ?? "any"}`; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/OnboardedInc/onboarded-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server