Skip to content

Commit 0810950

Browse files
author
Lasim
committed
feat(backend): enhance pagination handling and logging for registry sync
1 parent 3e761a3 commit 0810950

File tree

2 files changed

+131
-7
lines changed

2 files changed

+131
-7
lines changed

services/backend/src/services/registrySyncService.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,14 @@ export class RegistrySyncService {
259259

260260
const { servers, metadata } = batchResult.data;
261261

262+
// Log the actual metadata structure to debug pagination issues
263+
this.logger.debug({
264+
pageNumber,
265+
metadataKeys: Object.keys(metadata || {}),
266+
metadataStructure: JSON.stringify(metadata, null, 2),
267+
operation: 'registry_api_metadata'
268+
}, 'Registry API metadata structure');
269+
262270
// Extract server data and filter for isLatest === true (CRITICAL: keep this filter!)
263271
const serverData = servers
264272
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -279,7 +287,7 @@ export class RegistrySyncService {
279287
pageNumber,
280288
rawBatchSize: servers.length,
281289
afterIsLatestFilter: serverData.length,
282-
cursor,
290+
currentCursor: cursor,
283291
}, 'Fetched and filtered page from official registry');
284292

285293
// Filter this page against database (only check these 50 servers)
@@ -296,9 +304,36 @@ export class RegistrySyncService {
296304
// Add new servers from this page to accumulated list
297305
accumulatedNewServers.push(...newServersInPage);
298306

299-
// Check for next page
300-
cursor = metadata.next_cursor;
301-
hasMore = !!cursor;
307+
// Check for next page - try multiple possible field names
308+
// Official MCP Registry might use different naming conventions
309+
cursor = metadata.next_cursor || metadata.nextCursor || metadata.cursor || metadata.next;
310+
311+
// Determine if there are more pages
312+
// Stop if: no cursor OR we fetched fewer servers than requested (end of data)
313+
const shouldContinue = !!cursor && servers.length > 0;
314+
hasMore = shouldContinue;
315+
316+
this.logger.debug({
317+
pageNumber,
318+
nextCursor: cursor,
319+
serversInBatch: servers.length,
320+
hasMore,
321+
metadataCount: metadata.count,
322+
metadataTotal: metadata.total,
323+
accumulated: accumulatedNewServers.length,
324+
targetMax: maxServers,
325+
operation: 'pagination_check'
326+
}, 'Pagination status after page fetch');
327+
328+
// If we have no cursor but metadata suggests more servers exist, log a warning
329+
if (!cursor && metadata.total && metadata.count < metadata.total) {
330+
this.logger.warn({
331+
pageNumber,
332+
metadataCount: metadata.count,
333+
metadataTotal: metadata.total,
334+
operation: 'pagination_cursor_missing'
335+
}, 'API indicates more servers exist but no cursor provided - possible API issue');
336+
}
302337

303338
// Rate limiting for registry API
304339
if (hasMore) {

services/backend/src/services/transforms/officialRegistryTransforms.ts

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export interface OfficialEnvironmentVariable {
4646
*/
4747
export interface OfficialTransport {
4848
type: 'stdio' | 'streamable-http' | 'sse';
49+
command?: string; // Optional in official registry
50+
args?: string[]; // Optional in official registry
4951
}
5052

5153
/**
@@ -176,6 +178,62 @@ export function createSlug(officialName: string): string {
176178
// TRANSPORT TYPE DERIVATION
177179
// =============================================================================
178180

181+
/**
182+
* Infer command and args for STDIO packages based on registry type
183+
*
184+
* The official MCP registry doesn't always provide command/args in the transport object.
185+
* We need to infer them based on the package's registryType and identifier.
186+
*
187+
* @param pkg - Official package data
188+
* @returns Inferred transport with command and args
189+
*/
190+
export function inferStdioTransport(pkg: OfficialPackage): {
191+
command: string;
192+
args: string[];
193+
} {
194+
// If transport already has command and args, use them
195+
if (pkg.transport.command && pkg.transport.args) {
196+
return {
197+
command: pkg.transport.command,
198+
args: pkg.transport.args
199+
};
200+
}
201+
202+
// Otherwise, infer based on registryType
203+
const identifier = pkg.identifier;
204+
205+
switch (pkg.registryType.toLowerCase()) {
206+
case 'npm':
207+
// NPM packages: npx -y <package-name>
208+
return {
209+
command: 'npx',
210+
args: ['-y', identifier]
211+
};
212+
213+
case 'pypi':
214+
// PyPI packages: uvx <package-name>
215+
// Alternative: python -m <package-name>
216+
return {
217+
command: 'uvx',
218+
args: [identifier]
219+
};
220+
221+
case 'docker':
222+
// Docker images: docker run <image-name>
223+
return {
224+
command: 'docker',
225+
args: ['run', identifier]
226+
};
227+
228+
default:
229+
// Fallback to npx for unknown types
230+
return {
231+
command: 'npx',
232+
args: [identifier]
233+
};
234+
}
235+
}
236+
179237
/**
180238
* Derive DeployStack transport_type from official packages/remotes
181239
*
@@ -365,6 +423,8 @@ export function mapHeadersToThreeTier(
365423
/**
366424
* Map official runtime/package arguments to DeployStack's 3-tier args system
367425
*
426+
* IMPORTANT: For STDIO packages, we need to infer command/args if not provided
427+
*
368428
* @param packages - Official packages array
369429
* @returns DeployStack ConfigurationSchema args configuration
370430
*/
@@ -382,6 +442,29 @@ export function mapArgumentsToThreeTier(
382442
// Extract runtime and package arguments
383443
const pkg = packages[0];
384444

445+
// For STDIO packages, infer transport command/args if not provided
446+
if (pkg.transport.type === 'stdio') {
447+
const inferredTransport = inferStdioTransport(pkg);
448+
449+
// Add inferred args as template args (locked)
450+
for (const arg of inferredTransport.args) {
451+
templateArgs.push({
452+
value: arg,
453+
locked: true,
454+
description: `Static argument: ${arg}`
455+
});
456+
}
457+
458+
// Store command in the package for later use
459+
// This will be used when constructing the packages array
460+
if (!pkg.transport.command) {
461+
pkg.transport.command = inferredTransport.command;
462+
}
463+
if (!pkg.transport.args) {
464+
pkg.transport.args = inferredTransport.args;
465+
}
466+
}
467+
385468
// Package arguments go to template (locked)
386469
if (pkg.packageArguments && pkg.packageArguments.length > 0) {
387470
for (const arg of pkg.packageArguments) {
@@ -435,12 +518,17 @@ export async function transformOfficialToDeployStack(
435518
fetchGitHubMetadata?: boolean;
436519
}
437520
): Promise<Partial<CreateGlobalServerRequest>> {
521+
// IMPORTANT: Create a deep copy of packages to avoid mutating the original
522+
// We'll be adding inferred command/args to the packages during transformation
523+
const packagesCopy = officialServer.packages ? JSON.parse(JSON.stringify(officialServer.packages)) : undefined;
524+
438525
// Extract 3-tier configurations from packages (env vars + args)
439526
const envConfig = mapEnvironmentVariablesToThreeTier(
440-
officialServer.packages?.[0]?.environmentVariables || []
527+
packagesCopy?.[0]?.environmentVariables || []
441528
);
442529

443-
const argsConfig = mapArgumentsToThreeTier(officialServer.packages);
530+
// This will infer and add command/args to packagesCopy if missing
531+
const argsConfig = mapArgumentsToThreeTier(packagesCopy);
444532

445533
// Extract 3-tier configurations from remotes (headers)
446534
// Merge headers from all remotes (in case there are multiple endpoints)
@@ -500,7 +588,8 @@ export async function transformOfficialToDeployStack(
500588
website_url: officialServer.websiteUrl,
501589

502590
// Official format storage (will be JSON stringified by create-global.ts)
503-
packages: officialServer.packages,
591+
// Use packagesCopy which now has inferred command/args
592+
packages: packagesCopy,
504593
remotes: officialServer.remotes,
505594

506595
// Derived DeployStack fields

0 commit comments

Comments
 (0)