openapi-directory-mcp
Version:
Model Context Protocol server for accessing enhanced triple-source OpenAPI directory (APIs.guru + additional APIs + custom imports)
890 lines • 36.5 kB
JavaScript
/**
* Custom spec client - filesystem-based implementation following ApiClient interface
* Provides same interface as primary/secondary clients for seamless integration
*/
import { ManifestManager } from "./manifest-manager.js";
/**
* Custom specification client that reads from local filesystem
* Implements same interface as ApiClient and SecondaryApiClient
*/
export class CustomSpecClient {
constructor(cacheManager) {
this.manifestManager = new ManifestManager();
this.cache = cacheManager;
}
async fetchWithCache(key, fetchFn, ttl) {
try {
const cached = this.cache.get(`custom:${key}`);
if (cached) {
return cached;
}
}
catch (error) {
// Continue without cache if there's an error
console.warn(`Cache error for key ${key}:`, error);
}
const result = await fetchFn();
try {
this.cache.set(`custom:${key}`, result, ttl);
}
catch (error) {
// Log but don't fail if cache write fails
console.warn(`Failed to cache result for key ${key}:`, error);
}
return result;
}
/**
* List all providers in custom specs (always returns ["custom"])
*/
async getProviders() {
return this.fetchWithCache("providers", async () => {
const specs = this.manifestManager.listSpecs();
// Custom specs always use "custom" as provider
if (specs.length > 0) {
return { data: ["custom"] };
}
return { data: [] };
});
}
/**
* List all APIs for the custom provider
*/
async getProvider(provider) {
if (provider !== "custom") {
return { data: {} };
}
return this.fetchWithCache(`provider:${provider}`, async () => {
const specs = this.manifestManager.listSpecs();
const apis = {};
for (const specEntry of specs) {
try {
const parsed = this.manifestManager.parseSpecId(specEntry.id);
if (!parsed)
continue;
const specContent = this.manifestManager.readSpecFile(parsed.name, parsed.version);
const spec = JSON.parse(specContent);
// Structure the API with version as expected by tests
apis[`custom:${parsed.name}`] = {
[parsed.version]: spec,
};
}
catch (error) {
console.warn(`Failed to load spec ${specEntry.id}: ${error}`);
}
}
return { data: apis };
});
}
/**
* List all services for custom provider (returns names of imported APIs)
*/
async getServices(provider) {
if (provider !== "custom") {
throw new Error(`Provider ${provider} not found in custom specs`);
}
return this.fetchWithCache(`services:${provider}`, async () => {
const specs = this.manifestManager.listSpecs();
const services = new Set();
for (const specEntry of specs) {
const parsed = this.manifestManager.parseSpecId(specEntry.id);
if (parsed) {
services.add(parsed.name);
}
}
return { data: Array.from(services).sort() };
});
}
/**
* Retrieve one version of a particular API (without service)
*/
async getAPI(providerOrId, api) {
let provider;
let apiName;
let version;
// Handle both single parameter (ID) and two parameter (provider, api) calls
if (api === undefined) {
// Single parameter: parse ID like "custom:test-api" or "custom:test-api:1.0.0"
const parts = providerOrId.split(":");
if (parts.length < 2 || parts[0] !== "custom") {
throw new Error(`Invalid API ID: ${providerOrId}`);
}
provider = parts[0];
apiName = parts[1] || "";
version = parts[2]; // Optional version
}
else {
// Two parameters
provider = providerOrId;
apiName = api;
}
if (provider !== "custom") {
throw new Error(`Provider ${provider} not found in custom specs`);
}
return this.fetchWithCache(`api:${provider}:${apiName}${version ? ":" + version : ""}`, async () => {
// If version specified, look for exact match
let specId;
if (version) {
specId = `custom:${apiName}:${version}`;
const spec = this.manifestManager.getSpec(specId);
if (!spec) {
throw new Error(`API not found: custom:${apiName}:${version}`);
}
}
else {
// Find latest version
const specs = this.manifestManager.listSpecs();
const matchingSpecs = specs.filter((s) => {
const parsed = this.manifestManager.parseSpecId(s.id);
return parsed && parsed.name === apiName;
});
if (matchingSpecs.length === 0) {
throw new Error(`API not found: custom:${apiName}`);
}
// Use first match (could be improved to find latest version)
const firstMatch = matchingSpecs[0];
if (!firstMatch) {
throw new Error(`API not found: custom:${apiName}`);
}
specId = firstMatch.id;
}
const parsed = this.manifestManager.parseSpecId(specId);
if (!parsed) {
throw new Error(`Invalid spec ID: ${specId}`);
}
const specContent = this.manifestManager.readSpecFile(parsed.name, parsed.version);
try {
return JSON.parse(specContent);
}
catch (error) {
throw new Error("Invalid JSON in spec file");
}
});
}
/**
* Retrieve one version of a particular API with a serviceName
* Custom specs don't use services, so this delegates to getAPI
*/
async getServiceAPI(provider, service, _api) {
// For custom specs, service is the API name and api is the version
// Just use service as the API name since custom specs are identified by name
return this.getAPI(provider, service);
}
/**
* List all APIs from custom specs with optional pagination
*/
async listAPIs(page, limit) {
// If pagination parameters are provided, return paginated response
if (page !== undefined && limit !== undefined) {
return this.listAPIsPaginated(page, limit);
}
// Otherwise return the original format
return this.fetchWithCache("all_apis", async () => {
const specs = this.manifestManager.listSpecs();
const apis = {};
for (const specEntry of specs) {
try {
const parsed = this.manifestManager.parseSpecId(specEntry.id);
if (!parsed)
continue;
const specContent = this.manifestManager.readSpecFile(parsed.name, parsed.version);
const openApiSpec = JSON.parse(specContent);
// Create ApiGuruAPI entry from manifest and OpenAPI spec
const apiGuruEntry = {
added: specEntry.imported,
preferred: parsed.version,
versions: {
[parsed.version]: {
added: specEntry.imported,
info: {
...openApiSpec.info,
"x-apisguru-categories": ["custom"],
"x-providerName": parsed.name.split(".")[0],
},
updated: specEntry.imported,
swaggerUrl: `/custom/${parsed.name}/${parsed.version}.json`,
swaggerYamlUrl: `/custom/${parsed.name}/${parsed.version}.yaml`,
openapiVer: openApiSpec.openapi || "3.0.0",
link: openApiSpec.info?.contact?.url || "",
},
},
};
apis[specEntry.id] = apiGuruEntry;
}
catch (error) {
console.warn(`Failed to load spec ${specEntry.id}: ${error}`);
}
}
return apis;
});
}
/**
* Get metrics for custom specs
*/
async getMetrics() {
return this.fetchWithCache("metrics", async () => {
const stats = this.manifestManager.getStats();
const specs = this.manifestManager.listSpecs();
// Calculate approximate endpoints by parsing specs
let totalEndpoints = 0;
for (const specEntry of specs) {
try {
const parsed = this.manifestManager.parseSpecId(specEntry.id);
if (!parsed)
continue;
const specContent = this.manifestManager.readSpecFile(parsed.name, parsed.version);
const specData = JSON.parse(specContent);
// Count endpoints from the latest version
const latestVersion = specData.versions[specData.preferred];
if (latestVersion && latestVersion.spec && latestVersion.spec.paths) {
const paths = Object.keys(latestVersion.spec.paths);
totalEndpoints += paths.reduce((count, path) => {
const methods = Object.keys(latestVersion.spec.paths[path]);
return (count +
methods.filter((m) => [
"get",
"post",
"put",
"delete",
"patch",
"head",
"options",
].includes(m.toLowerCase())).length);
}, 0);
}
}
catch (error) {
// Skip on error
}
}
return {
numSpecs: stats.totalSpecs,
numAPIs: stats.totalSpecs, // Each spec is an API
numEndpoints: totalEndpoints,
unreachable: 0, // Custom specs are always reachable (local files)
invalid: 0, // Only valid specs can be imported
unofficial: stats.totalSpecs, // All custom specs are unofficial
fixes: 0, // No fixes needed for custom specs
fixedPct: 0,
datasets: [], // No datasets for custom specs
stars: 0, // No GitHub stars for custom specs
starsPercentile: 0,
numProviders: stats.totalSpecs > 0 ? 1 : 0, // Only "custom" provider
};
});
}
/**
* Check if custom specs has a specific API
*/
async hasAPI(apiId) {
return this.manifestManager.hasSpec(apiId);
}
/**
* Check if custom specs has a specific provider
*/
async hasProvider(provider) {
if (provider !== "custom") {
return false;
}
const specs = this.manifestManager.listSpecs();
return specs.length > 0;
}
/**
* Get paginated APIs (for compatibility)
*/
async getPaginatedAPIs(page = 1, limit = 50) {
return this.fetchWithCache(`paginated_apis:${page}:${limit}`, async () => {
const specs = this.manifestManager.listSpecs();
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const paginatedSpecs = specs.slice(startIndex, endIndex);
const results = paginatedSpecs.map((spec) => {
const parsed = this.manifestManager.parseSpecId(spec.id);
return {
id: spec.id,
title: spec.title,
description: spec.description.substring(0, 200) +
(spec.description.length > 200 ? "..." : ""),
provider: "custom",
preferred: parsed?.version || "1.0.0",
categories: ["custom"],
};
});
const totalPages = Math.ceil(specs.length / limit);
return {
results,
pagination: {
page,
limit,
total_results: specs.length,
total_pages: totalPages,
has_next: page < totalPages,
has_previous: page > 1,
},
};
}, 300000);
}
/**
* Search APIs in custom specs
*/
async searchAPIs(query, pageOrProvider, limitOrPage, limitParam) {
// Parse parameters - handle both (query, page, limit) and (query, provider, page, limit)
let provider;
let page;
let limit;
if (typeof pageOrProvider === "number") {
// Called as searchAPIs(query, page, limit)
page = pageOrProvider;
limit = limitOrPage || 20;
provider = undefined;
}
else {
// Called as searchAPIs(query, provider, page, limit)
provider = pageOrProvider;
page = limitOrPage || 1;
limit = limitParam || 20;
}
// If provider specified and it's not "custom", return empty results
if (provider && provider !== "custom") {
return {
data: [],
pagination: {
page,
limit,
total: 0,
total_results: 0,
total_pages: 0,
has_next: false,
has_previous: false,
},
};
}
const cacheKey = `search:${query}:${provider || "all"}:${page}:${limit}`;
return this.fetchWithCache(cacheKey, async () => {
const specs = this.manifestManager.listSpecs();
const queryLower = query.toLowerCase();
// Filter specs that match the query
const matchingSpecs = specs.filter((spec) => {
return (spec.id.toLowerCase().includes(queryLower) ||
spec.title.toLowerCase().includes(queryLower) ||
spec.description.toLowerCase().includes(queryLower) ||
spec.name.toLowerCase().includes(queryLower));
});
// Paginate results
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const paginatedSpecs = matchingSpecs.slice(startIndex, endIndex);
const data = paginatedSpecs.map((spec) => {
const parsed = this.manifestManager.parseSpecId(spec.id);
return {
id: spec.id,
name: parsed?.name || spec.name,
title: spec.title,
description: spec.description.substring(0, 200) +
(spec.description.length > 200 ? "..." : ""),
provider: "custom",
preferred: parsed?.version || "1.0.0",
categories: ["custom"],
};
});
const totalPages = Math.ceil(matchingSpecs.length / limit);
return {
data,
pagination: {
page,
limit,
total: matchingSpecs.length,
totalPages,
total_results: matchingSpecs.length,
total_pages: totalPages,
has_next: page < totalPages,
has_previous: page > 1,
},
};
}, 300000);
}
/**
* Get manifest manager for direct access
*/
getManifestManager() {
return this.manifestManager;
}
/**
* Invalidate all custom spec caches (called when specs are imported/removed)
*/
invalidateCache() {
const deletedCount = this.cache.invalidatePattern("custom:*");
console.log(`Invalidated ${deletedCount} custom spec cache entries`);
}
/**
* Get summary information for all custom specs
*/
async getCustomSpecsSummary() {
const stats = this.manifestManager.getStats();
const specs = this.manifestManager.listSpecs();
return {
total_specs: stats.totalSpecs,
total_size: stats.totalSize,
by_format: stats.byFormat,
by_source: stats.bySource,
last_updated: stats.lastUpdated,
specs: specs.map((spec) => {
const baseSpec = {
id: spec.id,
name: spec.name,
version: spec.version,
title: spec.title,
imported: spec.imported,
size: spec.fileSize,
};
// Only add security_issues if we have a valid count
if (spec.securityScan?.summary) {
return {
...baseSpec,
security_issues: spec.securityScan.summary.critical +
spec.securityScan.summary.high +
spec.securityScan.summary.medium +
spec.securityScan.summary.low,
};
}
return baseSpec;
}),
};
}
/**
* Get endpoints for a specific custom API
*/
async getAPIEndpoints(apiId, page = 1, limit = 30, tag) {
// Parse the apiId to get name and version
const parsed = this.manifestManager.parseSpecId(apiId);
if (!parsed) {
throw new Error(`Invalid API ID format: ${apiId}`);
}
// Get the spec content
const specContent = this.manifestManager.readSpecFile(parsed.name, parsed.version);
const spec = JSON.parse(specContent);
// Check if this is a direct OpenAPI spec (without ApiGuru wrapper)
if (spec.openapi || spec.swagger) {
// This is a direct OpenAPI spec, not wrapped in ApiGuru format
if (!spec.paths) {
return {
endpoints: [],
pagination: {
page,
limit,
total_endpoints: 0,
total_pages: 0,
has_next: false,
has_previous: false,
},
};
}
// Extract endpoints directly from the OpenAPI spec
const paths = spec.paths;
const allEndpoints = [];
for (const [path, pathMethods] of Object.entries(paths)) {
for (const [method, operation] of Object.entries(pathMethods)) {
if ([
"get",
"post",
"put",
"delete",
"patch",
"head",
"options",
].includes(method.toLowerCase())) {
const op = operation;
const endpoint = {
path,
method: method.toUpperCase(),
summary: op.summary,
description: op.description,
tags: op.tags,
operationId: op.operationId,
};
// Filter by tag if provided
if (!tag || (op.tags && op.tags.includes(tag))) {
allEndpoints.push(endpoint);
}
}
}
}
// Apply pagination
const totalEndpoints = allEndpoints.length;
const totalPages = Math.ceil(totalEndpoints / limit);
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const endpoints = allEndpoints.slice(startIndex, endIndex);
return {
endpoints,
pagination: {
page,
limit,
total_endpoints: totalEndpoints,
total_pages: totalPages,
has_next: page < totalPages,
has_previous: page > 1,
},
};
}
// This is ApiGuru format - get the latest version spec
const latestVersion = spec.versions?.[spec.preferred];
if (!latestVersion || !latestVersion.spec || !latestVersion.spec.paths) {
return {
endpoints: [],
pagination: {
page,
limit,
total_endpoints: 0,
total_pages: 0,
has_next: false,
has_previous: false,
},
};
}
// Extract endpoints from the OpenAPI spec
const paths = latestVersion.spec.paths;
const allEndpoints = [];
for (const [path, pathMethods] of Object.entries(paths)) {
for (const [method, operation] of Object.entries(pathMethods)) {
if (["get", "post", "put", "delete", "patch", "head", "options"].includes(method.toLowerCase())) {
const op = operation; // Type assertion for OpenAPI operation object
const endpoint = {
path,
method: method.toUpperCase(),
summary: op.summary,
description: op.description,
tags: op.tags,
operationId: op.operationId,
};
// Filter by tag if provided
if (!tag || (op.tags && op.tags.includes(tag))) {
allEndpoints.push(endpoint);
}
}
}
}
// Apply pagination
const totalEndpoints = allEndpoints.length;
const totalPages = Math.ceil(totalEndpoints / limit);
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const endpoints = allEndpoints.slice(startIndex, endIndex);
return {
endpoints,
pagination: {
page,
limit,
total_endpoints: totalEndpoints,
total_pages: totalPages,
has_next: page < totalPages,
has_previous: page > 1,
},
};
}
/**
* Get provider statistics for custom source
*/
async getProviderStats(provider) {
if (provider !== "custom") {
throw new Error(`Provider ${provider} not found in custom specs`);
}
return this.fetchWithCache(`provider_stats:${provider}`, async () => {
const specs = this.manifestManager.listSpecs();
return {
provider,
api_count: specs.length,
source: "custom",
};
});
}
/**
* Get API summary by ID for custom source
*/
async getAPISummaryById(apiId) {
return this.fetchWithCache(`api_summary:${apiId}`, async () => {
const [, name, version] = apiId.split(":");
if (!name || !version) {
throw new Error(`Invalid custom API ID format: ${apiId}. Expected format: custom:name:version`);
}
const specContent = this.manifestManager.readSpecFile(name, version);
const api = JSON.parse(specContent);
return {
id: apiId,
info: api.info,
versions: api.versions,
preferred: api.preferred,
source: "custom",
};
});
}
/**
* Get popular APIs from custom source (returns all since custom specs are curated)
*/
async getPopularAPIs(limit) {
return this.fetchWithCache(`popular:${limit}`, async () => {
const specs = this.manifestManager.listSpecs();
return {
results: specs.slice(0, limit || 20).map((spec) => spec),
source: "custom",
};
});
}
/**
* Get recently updated APIs from custom source
*/
async getRecentlyUpdatedAPIs(limit) {
return this.fetchWithCache(`recent:${limit}`, async () => {
const specs = this.manifestManager.listSpecs();
// Sort by imported date (most recent first)
specs.sort((a, b) => new Date(b.imported).getTime() - new Date(a.imported).getTime());
return {
results: specs.slice(0, limit || 10).map((spec) => spec),
source: "custom",
};
});
}
/**
* Get OpenAPI specification for a custom API
*/
async getOpenAPISpec(url) {
// For custom specs, the URL might be a spec ID
if (url.startsWith("custom:")) {
return this.fetchWithCache(`spec:${url}`, async () => {
const [, name, version] = url.split(":");
if (!name || !version) {
throw new Error(`Invalid custom API ID format: ${url}`);
}
const specContent = this.manifestManager.readSpecFile(name, version);
return JSON.parse(specContent);
});
}
// Handle custom spec URLs in the format /custom/name/version.json
if (url.startsWith("/custom/")) {
return this.fetchWithCache(`spec:${url}`, async () => {
const pathParts = url.split("/");
if (pathParts.length < 4) {
throw new Error(`Invalid custom spec URL format: ${url}`);
}
const name = pathParts[2];
const versionFile = pathParts[3];
if (!name || !versionFile) {
throw new Error(`Invalid custom spec URL format: ${url}`);
}
const version = versionFile.replace(/\.(json|yaml)$/, "");
const specContent = this.manifestManager.readSpecFile(name, version);
return JSON.parse(specContent);
});
}
// For external URLs, throw error since custom client doesn't fetch external specs
throw new Error(`External URL not supported in custom client: ${url}`);
}
/**
* Get endpoint schema for custom source
*/
async getEndpointSchema(apiId, method, path) {
const cacheKey = `endpoint_schema:${apiId}:${method}:${path}`;
return this.fetchWithCache(cacheKey, async () => {
const [, name, version] = apiId.split(":");
if (!name || !version) {
throw new Error(`Invalid custom API ID format: ${apiId}. Expected format: custom:name:version`);
}
const specContent = this.manifestManager.readSpecFile(name, version);
const spec = JSON.parse(specContent);
// Handle both direct OpenAPI spec and ApiGuru wrapper format
let openApiSpec;
if (spec.openapi || spec.swagger) {
// Direct OpenAPI spec
openApiSpec = spec;
}
else if (spec.versions && spec.preferred) {
// ApiGuru wrapper format
const preferredVersion = spec.versions[spec.preferred];
openApiSpec = preferredVersion.spec;
}
else {
throw new Error(`Invalid spec format for: ${apiId}`);
}
const pathItem = openApiSpec.paths?.[path];
const operation = pathItem?.[method.toLowerCase()];
if (!operation) {
throw new Error(`Endpoint not found: ${method} ${path}`);
}
return {
api_id: apiId,
method: method.toUpperCase(),
path,
parameters: operation.parameters || [],
requestBody: operation.requestBody,
responses: operation.responses || {},
source: "custom",
};
});
}
/**
* Get endpoint examples for custom source
*/
async getEndpointExamples(apiId, method, path) {
const cacheKey = `endpoint_examples:${apiId}:${method}:${path}`;
return this.fetchWithCache(cacheKey, async () => {
// Validate that the endpoint exists by calling getEndpointSchema
await this.getEndpointSchema(apiId, method, path);
return {
api_id: apiId,
method: method.toUpperCase(),
path,
request_examples: [], // Custom source doesn't have examples
response_examples: [], // Custom source doesn't have examples
source: "custom",
};
});
}
/**
* Get detailed information about a specific endpoint
*/
async getEndpointDetails(apiId, method, path) {
const cacheKey = `endpoint_details:${apiId}:${method.toLowerCase()}:${path}`;
return this.fetchWithCache(cacheKey, async () => {
// Get the custom spec - extract name and version from apiId
const [, name, version] = apiId.split(":");
if (!name || !version) {
throw new Error(`Invalid custom API ID format: ${apiId}. Expected format: custom:name:version`);
}
const specEntry = this.manifestManager.getSpec(apiId);
if (!specEntry) {
throw new Error(`API not found: ${apiId}`);
}
// Load the OpenAPI spec
const specContent = this.manifestManager.readSpecFile(name, version);
const specData = JSON.parse(specContent);
// Handle both direct OpenAPI spec and ApiGuru wrapper format
let spec;
if (specData.openapi || specData.swagger) {
// Direct OpenAPI spec
spec = specData;
}
else if (specData.versions && specData.preferred) {
// ApiGuru wrapper format
const preferredVersion = specData.versions[specData.preferred];
if (!preferredVersion || !preferredVersion.spec) {
throw new Error(`OpenAPI spec not available for: ${apiId}`);
}
spec = preferredVersion.spec;
}
else {
throw new Error(`Invalid spec format for: ${apiId}`);
}
if (!spec.paths || !spec.paths[path]) {
throw new Error(`Path not found: ${path} in API: ${apiId}`);
}
const pathItem = spec.paths[path];
const operation = pathItem[method.toLowerCase()];
if (!operation) {
throw new Error(`Method ${method.toUpperCase()} not found for path: ${path} in API: ${apiId}`);
}
// Extract parameters
const parameters = [];
// Add path-level parameters
if (pathItem.parameters) {
for (const param of pathItem.parameters) {
parameters.push({
name: param.name,
in: param.in,
required: param.required || false,
type: param.schema?.type || param.type || "string",
description: param.description,
});
}
}
// Add operation-level parameters
if (operation.parameters) {
for (const param of operation.parameters) {
parameters.push({
name: param.name,
in: param.in,
required: param.required || false,
type: param.schema?.type || param.type || "string",
description: param.description,
});
}
}
// Extract responses
const responses = [];
if (operation.responses) {
for (const [code, response] of Object.entries(operation.responses)) {
const responseObj = response;
responses.push({
code,
description: responseObj.description || "",
content_types: responseObj.content
? Object.keys(responseObj.content)
: [],
});
}
}
// Extract security requirements
const security = [];
const securityRequirements = operation.security || spec.security || [];
for (const securityReq of securityRequirements) {
for (const [securityName, scopes] of Object.entries(securityReq)) {
const securityScheme = spec.components?.securitySchemes?.[securityName];
if (securityScheme) {
security.push({
type: securityScheme.type,
scopes: Array.isArray(scopes) ? scopes : [],
});
}
}
}
return {
method: method.toUpperCase(),
path,
summary: operation.summary,
description: operation.description,
operationId: operation.operationId,
tags: operation.tags || [],
deprecated: operation.deprecated || false,
parameters,
responses,
consumes: operation.consumes || spec.consumes || [],
produces: operation.produces || spec.produces || [],
...(security.length > 0 && { security }),
};
}, 600000); // Cache for 10 minutes
}
/**
* List APIs with pagination support (for testing compatibility)
*/
async listAPIsPaginated(page = 1, limit = 20) {
const cacheKey = `list:paginated:${page}:${limit}`;
return this.fetchWithCache(cacheKey, async () => {
const specs = this.manifestManager.listSpecs();
// Convert specs to API format
const apiList = specs.map((spec) => {
this.manifestManager.parseSpecId(spec.id);
return {
id: spec.id,
title: spec.title,
description: spec.description,
version: spec.version,
provider: "custom",
added: spec.imported,
preferred: spec.version,
};
});
// Apply pagination
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const paginatedData = apiList.slice(startIndex, endIndex);
return {
data: paginatedData,
pagination: {
page,
limit,
total: apiList.length,
totalPages: Math.ceil(apiList.length / limit),
},
};
});
}
}
//# sourceMappingURL=custom-spec-client.js.map