{"record":{"id":"df50f83ad127f918","repo":"abhigyanpatwari/GitNexus","slug":"list-repos-field-must-be-an-integer-bound","errorCode":null,"errorMessage":"list_repos: \"${field}\" must be an integer ${bound} (received ${JSON.stringify(value)})","messagePattern":"list_repos: \"(.+?)\" must be an integer (.+?) \\(received (.+?)\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"gitnexus/src/mcp/local/local-backend.ts","lineNumber":1136,"sourceCode":" * non-number, `NaN`, non-integer, `limit < 1`, `limit > maxLimit`, or\n * `offset < 0` — are REJECTED with a clear error. `limit` is bounded but NOT\n * silently clamped: an over-max value throws (symmetric with the other bounds)\n * so a client never receives a smaller page than it asked for without knowing.\n * An omitted value (only `undefined`) falls back to the default.\n */\nexport function parseListReposPagination(\n  params: { limit?: unknown; offset?: unknown } | null | undefined,\n  opts: { defaultLimit: number; maxLimit: number },\n): { limit: number; offset: number } {\n  const requireInt = (value: unknown, field: string, min: number, max?: number): number => {\n    const valid =\n      typeof value === 'number' &&\n      Number.isInteger(value) &&\n      value >= min &&\n      (max === undefined || value <= max);\n    if (!valid) {\n      const bound = max === undefined ? `>= ${min}` : `between ${min} and ${max}`;\n      throw new Error(\n        `list_repos: \"${field}\" must be an integer ${bound} (received ${JSON.stringify(value)})`,\n      );\n    }\n    return value;\n  };\n\n  let limit = opts.defaultLimit;\n  if (params?.limit !== undefined) {\n    limit = requireInt(params.limit, 'limit', 1, opts.maxLimit);\n  }\n\n  let offset = 0;\n  if (params?.offset !== undefined) {\n    offset = requireInt(params.offset, 'offset', 0);\n  }\n\n  return { limit, offset };\n}","sourceCodeStart":1118,"sourceCodeEnd":1154,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/aac7515d2a8c50a1f8f923c6fb77218b333560d6/gitnexus/src/mcp/local/local-backend.ts#L1118-L1154","documentation":"parseListReposPagination validates the list_repos tool's pagination params before use: limit and offset must be real numbers (not numeric strings), Number.isInteger, within bounds — limit between 1 and the backend's maxLimit, offset >= 0. On violation it throws a message that includes the field, the expected bound, and the JSON-stringified received value.","triggerScenarios":"Calling the MCP tool list_repos with limit: 0, a negative offset, a fractional limit like 10.5, a string such as \"10\" (sent by form-based or loosely typed clients), null, or a limit above the backend's configured maxLimit.","commonSituations":"Hand-written MCP clients that forward raw user input; JSON configs where pagination arrives as strings; SDK codegen that types params as any; test scripts poking edge values; client-side defaults computed from unclamped arithmetic (e.g. limit = total - 100 going to 0 or below).","solutions":["Send integer numbers within bounds: limit >= 1 and <= maxLimit, offset >= 0 — the error text names the exact bound you violated.","Coerce and clamp client-side before calling: Number(...), Math.trunc, Math.min/Math.max.","Omit the params entirely to receive the backend defaults (defaultLimit).","If you genuinely need more rows than maxLimit, page through results with offset instead of raising limit."],"exampleFix":"// before: string / out-of-range pagination\nawait callTool('list_repos', { limit: '50', offset: -1 });\nawait callTool('list_repos', { limit: 0 });\n\n// after: validated integers, clamped before sending\nconst limit = Math.min(Math.max(Number.parseInt(rawLimit, 10) || 20, 1), maxLimit);\nconst offset = Math.max(Number.parseInt(rawOffset, 10) || 0, 0);\nawait callTool('list_repos', { limit, offset });","handlingStrategy":"validation","validationCode":"// Clamp pagination to the backend contract before calling list_repos\nconst MAX_LIMIT = 100; // keep in sync with the backend's maxLimit\n\nfunction normalizePagination(raw: { limit?: unknown; offset?: unknown }) {\n  const limit = raw.limit === undefined ? undefined\n    : Math.min(Math.max(Number.parseInt(String(raw.limit), 10), 1), MAX_LIMIT);\n  const offset = raw.offset === undefined ? undefined\n    : Math.max(Number.parseInt(String(raw.offset), 10), 0);\n  if (Number.isNaN(limit) || Number.isNaN(offset)) throw new TypeError('pagination must be numeric');\n  return { limit, offset };\n}","typeGuard":"const isValidPagination = (p: unknown): p is { limit?: number; offset?: number } => {\n  if (typeof p !== 'object' || p === null) return false;\n  const { limit, offset } = p as { limit?: unknown; offset?: unknown };\n  return (limit === undefined || (typeof limit === 'number' && Number.isInteger(limit) && limit >= 1)) &&\n         (offset === undefined || (typeof offset === 'number' && Number.isInteger(offset) && offset >= 0));\n};","tryCatchPattern":"try {\n  return await backend.callTool('list_repos', params);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('must be an integer')) {\n    // echo the received value in the message back to the user for correction\n    throw new UserInputError(err.message);\n  }\n  throw err;\n}","preventionTips":["Coerce form/env input with parseInt + clamp before forwarding to MCP tools.","Parse the error's JSON.stringify-ed received value to show users exactly what was wrong.","Omit optional pagination fields rather than sending 0/'' defaults.","Page with offset instead of inflating limit past maxLimit."],"tags":["mcp","list-repos","pagination","input-validation","type-coercion"],"backgroundTag":"invalid-pagination-parameters","analyzedSha":"aac7515d2a8c50a1f8f923c6fb77218b333560d6","analyzedAt":"2026-08-20T23:29:22.980Z","schemaVersion":2},"datasetVersion":"2026-08-22T20:17:22.307Z"}