Skip to main content
Glama
index.ts5.41 kB
import { Context7Error } from "@error"; type CacheSetting = | "default" | "force-cache" | "no-cache" | "no-store" | "only-if-cached" | "reload" | false; export type Context7Request = { path?: string[]; /** * Request body will be serialized to json */ body?: unknown; /** * HTTP method to use * @default "POST" */ method?: "GET" | "POST"; /** * Query parameters for GET requests */ query?: Record<string, string | number | boolean | undefined>; }; export type TxtResponseHeaders = { page: number; limit: number; totalPages: number; hasNext: boolean; hasPrev: boolean; totalTokens: number; }; export type Context7Response<TResult> = { result?: TResult; headers?: TxtResponseHeaders; }; export type Requester = { request: <TResult = unknown>(req: Context7Request) => Promise<Context7Response<TResult>>; }; export type RetryConfig = | false | { /** * The number of retries to attempt before giving up. * * @default 5 */ retries?: number; /** * A backoff function receives the current retry cound and returns a number in milliseconds to wait before retrying. * * @default * ```ts * Math.exp(retryCount) * 50 * ``` */ backoff?: (retryCount: number) => number; }; export type RequesterConfig = { /** * Configure the retry behaviour in case of network errors */ retry?: RetryConfig; /** * Configure the cache behaviour * @default "no-store" */ cache?: CacheSetting; }; export type HttpClientConfig = { headers?: Record<string, string>; baseUrl: string; retry?: RetryConfig; signal?: () => AbortSignal; } & RequesterConfig; export class HttpClient implements Requester { public baseUrl: string; public headers: Record<string, string>; public readonly options: { signal?: HttpClientConfig["signal"]; cache?: CacheSetting; }; public readonly retry: { attempts: number; backoff: (retryCount: number) => number; }; public constructor(config: HttpClientConfig) { this.options = { cache: config.cache, signal: config.signal, }; this.baseUrl = config.baseUrl.replace(/\/$/, ""); this.headers = { "Content-Type": "application/json", ...config.headers, }; this.retry = typeof config?.retry === "boolean" && config?.retry === false ? { attempts: 1, backoff: () => 0, } : { attempts: config?.retry?.retries ?? 5, backoff: config?.retry?.backoff ?? ((retryCount) => Math.exp(retryCount) * 50), }; } public async request<TResult>(req: Context7Request): Promise<Context7Response<TResult>> { const method = req.method || "POST"; let url = [this.baseUrl, ...(req.path ?? [])].join("/"); if (method === "GET" && req.query) { const queryParams = new URLSearchParams(); Object.entries(req.query).forEach(([key, value]) => { if (value !== undefined) { queryParams.append(key, String(value)); } }); const queryString = queryParams.toString(); if (queryString) { url += `?${queryString}`; } } const requestOptions = { cache: this.options.cache, method, headers: this.headers, body: req.body ? JSON.stringify(req.body) : undefined, keepalive: true, signal: this.options.signal?.(), }; let res: Response | null = null; let error: Error | null = null; for (let i = 0; i <= this.retry.attempts; i++) { try { res = await fetch(url, requestOptions as RequestInit); break; } catch (error_) { if (requestOptions.signal?.aborted) { throw error_; } error = error_ as Error; if (i < this.retry.attempts) { await new Promise((r) => setTimeout(r, this.retry.backoff(i))); } } } if (!res) { throw error ?? new Error("Exhausted all retries"); } if (!res.ok) { const errorBody = (await res.json()) as { error?: string; message?: string }; throw new Context7Error(errorBody.error || errorBody.message || res.statusText); } const contentType = res.headers.get("content-type"); if (contentType?.includes("application/json")) { const body = await res.json(); return { result: body as TResult }; } else { const text = await res.text(); const headers = this.extractTxtResponseHeaders(res.headers); return { result: text as TResult, headers }; } } private extractTxtResponseHeaders(headers: Headers): TxtResponseHeaders | undefined { const page = headers.get("x-context7-page"); const limit = headers.get("x-context7-limit"); const totalPages = headers.get("x-context7-total-pages"); const hasNext = headers.get("x-context7-has-next"); const hasPrev = headers.get("x-context7-has-prev"); const totalTokens = headers.get("x-context7-total-tokens"); if (!page || !limit || !totalPages || !hasNext || !hasPrev || !totalTokens) { return undefined; } return { page: parseInt(page, 10), limit: parseInt(limit, 10), totalPages: parseInt(totalPages, 10), hasNext: hasNext === "true", hasPrev: hasPrev === "true", totalTokens: parseInt(totalTokens, 10), }; } }

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/upstash/context7-mcp'

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