{"record":{"id":"5bb3a536d19b3e39","repo":"abhigyanpatwari/GitNexus","slug":"source-must-be-a-positive-integer","errorCode":null,"errorMessage":"${source} must be a positive integer.","messagePattern":"(.+?) must be a positive integer\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"gitnexus/src/mcp/output-budget.ts","lineNumber":12,"sourceCode":"const BUDGETED_TOOLS = new Set(['query', 'context', 'impact']);\n\nexport const MCP_TOKEN_ESTIMATE_BYTES = 4;\nexport const MCP_TRUNCATION_MARKER = '\\n…';\n\nfunction parsePositiveInteger(value: unknown, source: string): number {\n  if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return value;\n  if (typeof value === 'string' && /^[1-9]\\d*$/.test(value.trim())) {\n    const parsed = Number(value.trim());\n    if (Number.isSafeInteger(parsed)) return parsed;\n  }\n  throw new Error(`${source} must be a positive integer.`);\n}\n\nexport function resolveMcpMaxTokens(\n  toolName: string,\n  args: Record<string, unknown> | undefined,\n  env: NodeJS.ProcessEnv = process.env,\n): number | undefined {\n  if (!BUDGETED_TOOLS.has(toolName)) return undefined;\n  if (args?.maxTokens !== undefined) return parsePositiveInteger(args.maxTokens, 'maxTokens');\n\n  const configured = env.GITNEXUS_MCP_DEFAULT_MAX_TOKENS;\n  if (configured === undefined || configured.trim() === '') return undefined;\n  return parsePositiveInteger(configured, 'GITNEXUS_MCP_DEFAULT_MAX_TOKENS');\n}\n\nfunction utf8Prefix(text: string, maxBytes: number): string {\n  let bytes = 0;\n  const codePoints: string[] = [];","sourceCodeStart":1,"sourceCodeEnd":30,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/aac7515d2a8c50a1f8f923c6fb77218b333560d6/gitnexus/src/mcp/output-budget.ts#L1-L30","documentation":"Thrown by parsePositiveInteger in the MCP output-budget module when a token budget value is not a usable positive integer. It guards the maxTokens argument accepted by the budgeted MCP tools (query, context, impact) and the GITNEXUS_MCP_DEFAULT_MAX_TOKENS environment variable. A value passes only if it is a number that is a safe integer > 0, or a string matching ^[1-9]\\d*$ that parses to a safe integer (whitespace is trimmed first). Anything else — 0, negatives, floats, '1e3', 'abc', '007', values above Number.MAX_SAFE_INTEGER — throws immediately, before any tool backend runs.","triggerScenarios":"Calling the MCP 'query', 'context', or 'impact' tool with maxTokens: 0, maxTokens: -100, maxTokens: 5000.5, maxTokens: '1e3', or maxTokens: 'abc'. Or starting/serving MCP with env GITNEXUS_MCP_DEFAULT_MAX_TOKENS='-1', '10.5', 'true', or '1e4' while any budgeted tool is invoked without an explicit maxTokens argument.","commonSituations":"An AI client computes maxTokens from a model context size and passes 0 or a float (e.g. ctx.window / 1000). A .env file sets GITNEXUS_MCP_DEFAULT_MAX_TOKENS with quotes or units ('4096 tokens', '\"4096\"'). Copying a permissive validator that accepts '1e3' style scientific notation, which the strict ^[1-9]\\d*$ regex deliberately rejects.","solutions":["Pass maxTokens as a plain positive integer (number or digit-only string), e.g. 4096 — no decimals, no scientific notation, no leading zeros, and at most Number.MAX_SAFE_INTEGER.","If you do not want a budget, omit the maxTokens argument entirely instead of passing 0.","Check and fix GITNEXUS_MCP_DEFAULT_MAX_TOKENS in the server environment (unset it or set it to a plain positive integer like 4096).","If the value comes from another system, coerce and validate it client-side before the MCP call (Number.isSafeInteger check)."],"exampleFix":"// before\nconst out = await callTool('query', { search_query: 'auth flow', maxTokens: tokens }); // tokens = 0 or 4096.5\n\n// after\nconst tokens = Number.isSafeInteger(raw) && raw > 0 ? raw : 4096;\nconst out = await callTool('query', { search_query: 'auth flow', maxTokens: tokens });","handlingStrategy":"validation","validationCode":"function toMaxTokens(v: unknown): number | undefined {\n  if (v === undefined) return undefined;\n  if (typeof v === 'number' && Number.isSafeInteger(v) && v > 0) return v;\n  if (typeof v === 'string' && /^[1-9]\\d*$/.test(v.trim())) {\n    const n = Number(v.trim());\n    if (Number.isSafeInteger(n)) return n;\n  }\n  return undefined; // omit the arg rather than send an invalid one\n}\nconst args = { search_query: 'auth' } as Record<string, unknown>;\nconst mt = toMaxTokens(userBudget);\nif (mt !== undefined) args.maxTokens = mt;","typeGuard":"const isPositiveIntArg = (v: unknown): v is number | string =>\n  (typeof v === 'number' && Number.isSafeInteger(v) && v > 0) ||\n  (typeof v === 'string' && /^[1-9]\\d*$/.test(v.trim()) && Number.isSafeInteger(Number(v.trim())));","tryCatchPattern":"try {\n  await client.callTool({ name: 'query', arguments });\n} catch (e) {\n  if (e instanceof Error && /must be a positive integer/.test(e.message)) {\n    // deterministic input error: fix the value, do not retry\n    throw new Error(`Bad maxTokens: ${JSON.stringify(arguments['maxTokens'])}`);\n  }\n  throw e;\n}","preventionTips":["Never derive maxTokens with division unless you Math.floor and clamp to >= 1.","Treat 0 as 'omit the budget' client-side; the server rejects it.","Check GITNEXUS_MCP_DEFAULT_MAX_TOKENS in the server environment with the same regex before deploying."],"tags":["mcp","maxtokens","validation","configuration","environment-variables"],"backgroundTag":"invalid-maxtokens-value","analyzedSha":"aac7515d2a8c50a1f8f923c6fb77218b333560d6","analyzedAt":"2026-08-20T23:29:22.980Z","contentChangedAt":"2026-08-20T23:29:22.980Z","schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}