{"record":{"id":"0e164e2b9eb1346e","repo":"abhigyanpatwari/GitNexus","slug":"group-routing-is-not-available-in-gitnexus-mcp-rea","errorCode":null,"errorMessage":"Group routing is not available in GitNexus MCP read-only mode.","messagePattern":"Group routing is not available in GitNexus MCP read-only mode\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"gitnexus/src/mcp/read-only-policy.ts","lineNumber":40,"sourceCode":"\nexport function resolveMcpReadOnlyMode(env: NodeJS.ProcessEnv = process.env): boolean {\n  const value = env.GITNEXUS_MCP_READ_ONLY?.trim();\n  if (value === undefined || value === '' || value === '0') return false;\n  if (value === '1') return true;\n  throw new Error('GITNEXUS_MCP_READ_ONLY must be 0 or 1.');\n}\n\nexport function assertMcpReadOnlyToolCall(\n  toolName: string,\n  args: Record<string, unknown> | undefined,\n  readOnly: boolean,\n): void {\n  if (!readOnly) return;\n  if (!MCP_READ_ONLY_TOOLS.has(toolName) && !MCP_READ_ONLY_ALIASES.has(toolName)) {\n    throw new Error(`Tool \"${toolName}\" is not available in GitNexus MCP read-only mode.`);\n  }\n  if (typeof args?.repo === 'string' && args.repo.trim().startsWith('@')) {\n    throw new Error('Group routing is not available in GitNexus MCP read-only mode.');\n  }\n  // crossDepth/subgroup only do anything on the @group path rejected above,\n  // but rejecting them here keeps the advertised schema and the dispatch\n  // contract in agreement.\n  for (const groupOnlyArg of ['crossDepth', 'subgroup']) {\n    if (args?.[groupOnlyArg] !== undefined) {\n      throw new Error(\n        `Parameter \"${groupOnlyArg}\" is not available in GitNexus MCP read-only mode.`,\n      );\n    }\n  }\n}\n\nexport function readOnlyResourceTemplateAllowed(uriTemplate: string, readOnly: boolean): boolean {\n  return !readOnly || !/^gitnexus:\\/\\/group\\//iu.test(uriTemplate);\n}\n\nexport function assertMcpReadOnlyResource(uri: string, readOnly: boolean): void {","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/aac7515d2a8c50a1f8f923c6fb77218b333560d6/gitnexus/src/mcp/read-only-policy.ts#L22-L58","documentation":"Thrown by assertMcpReadOnlyToolCall when read-only mode is active and a tool call passes a repo argument that starts with '@' (the cross-repo group routing syntax, e.g. repo: '@my-group'). Read-only mode deliberately disables group routing, so any @-prefixed repo is rejected at dispatch before the backend sees it.","triggerScenarios":"With GITNEXUS_MCP_READ_ONLY=1, calling any allowlisted tool with arguments like { repo: '@frontend' } or { repo: ' @backend' } (leading whitespace is trimmed, so it still triggers).","commonSituations":"A client reuses group-routing arguments from a read-write deployment ('repo: \"@org\"') against a hardened read-only endpoint. Agents copy the @group example from cross-repo docs without noticing the deployment difference.","solutions":["Pass a concrete repository name or path instead of the @group specifier, e.g. repo: 'GitNexus' or repo: '/srv/repos/frontend'.","Issue one call per repository instead of one group-routed call.","If group routing is required, disable read-only mode (unset GITNEXUS_MCP_READ_ONLY or set it to 0) after reviewing the security trade-off."],"exampleFix":"// before\nawait client.callTool({ name: 'query', arguments: { search_query: 'auth', repo: '@my-group' } });\n\n// after\nfor (const repo of ['frontend', 'backend']) {\n  await client.callTool({ name: 'query', arguments: { search_query: 'auth', repo } });\n}","handlingStrategy":"validation","validationCode":"function assertRepoArg(repo: unknown, readOnly: boolean) {\n  if (readOnly && typeof repo === 'string' && repo.trim().startsWith('@')) {\n    throw new Error('Group routing (@repo) is disabled on this read-only endpoint; pass a concrete repo.');\n  }\n}","typeGuard":"const isConcreteRepo = (repo: unknown): repo is string =>\n  typeof repo === 'string' && repo.trim().length > 0 && !repo.trim().startsWith('@');","tryCatchPattern":"try {\n  await client.callTool({ name: 'query', arguments: { search_query: q, repo } });\n} catch (e) {\n  if (e instanceof Error && e.message.includes('Group routing is not available')) {\n    // fall back to per-repo calls over a known list\n    for (const r of repoList) await runQuery(q, r);\n  } else throw e;\n}","preventionTips":["Derive the repo argument from deployment config, never hard-code '@group' in shared templates.","Detect read-only mode once at startup (from server info/env) and select the argument strategy then.","Prefer explicit per-repo iteration; it works in both modes."],"tags":["mcp","read-only-mode","group-routing","authorization"],"backgroundTag":"readonly-mode-restriction","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"}