{"record":{"id":"724a066e0ef9d360","repo":"can1357/oh-my-pi","slug":"oauth-resource-uri-must-not-include-a-fragment","errorCode":null,"errorMessage":"OAuth resource URI must not include a fragment","messagePattern":"OAuth resource URI must not include a fragment","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/mcp/oauth-flow.ts","lineNumber":256,"sourceCode":"\t\tcallbackHostname: resolveCallbackHostname(redirectUri),\n\t\tredirectUri,\n\t\tallowPortFallback,\n\t};\n}\n\nfunction resolveResourceUri(resource: string | undefined): string | undefined {\n\tconst trimmed = resource?.trim();\n\tif (!trimmed) return undefined;\n\tif (trimmed !== resource) {\n\t\tthrow new Error(\"OAuth resource URI must not include surrounding whitespace\");\n\t}\n\n\tconst parsed = new URL(trimmed);\n\tif (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n\t\tthrow new Error(\"OAuth resource URI must use http or https\");\n\t}\n\tif (parsed.hash) {\n\t\tthrow new Error(\"OAuth resource URI must not include a fragment\");\n\t}\n\treturn trimmed;\n}\n\ninterface ResourceIndicatorFilterOptions {\n\t/** Strip any resource URL on the same origin as the authorization server. */\n\tstripSameOriginResource?: boolean;\n}\n\n/**\n * Drop a redundant fallback resource indicator relative to {@link serverUrl}.\n *\n * Provider-advertised resource indicators are authoritative even when they are\n * origin-only (`https://gateway.example.com`) or path-scoped same-origin\n * (`https://gateway.example.com/my-service/mcp`): servers can use either form\n * as the audience they require for the grant.\n *\n * Plane is stricter for OMP-synthesized fallback resources (e.g. using the","sourceCodeStart":238,"sourceCodeEnd":274,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/mcp/oauth-flow.ts#L238-L274","documentation":"RFC 8707 resource indicators must not contain a fragment; the resource is an audience identifier for the token, and a fragment would either break the provider's exact-match comparison or leak path data. resolveResourceUri parses the trimmed URL and rejects it if the URL has a hash fragment.","triggerScenarios":"Configuring the OAuth resource option with a trailing fragment, e.g. \"https://acme.example/mcp#section\" or a URL pasted from a browser that included an anchor, then constructing the flow options or accessing resolvedResource.","commonSituations":"Copying the MCP server URL straight from a browser address bar where a page anchor was present; template-generated URLs appending #fragment; confusion between resource indicators and web page URLs.","solutions":["Strip the #fragment from the configured resource URI","Use only scheme://host/path?query form — typically just the origin or base path of the MCP server","When copying URLs from a browser, remove any anchor portion before pasting into config"],"exampleFix":"// before\n\"resource\": \"https://acme.example/mcp#tools\"\n// after\n\"resource\": \"https://acme.example/mcp\"","handlingStrategy":"validation","validationCode":"const res = config.oauth?.resource;\nif (res != null && new URL(res.trim()).hash) {\n  throw new Error(`resource must not contain a fragment: '${res}'`);\n}","typeGuard":"function isFragmentFreeUrl(s) {\n  try { return !new URL(s).hash; } catch { return false; }\n}","tryCatchPattern":"try {\n  startOAuthFlow(config);\n} catch (e) {\n  if (e.message.includes('must not include a fragment')) {\n    const u = new URL(config.oauth.resource);\n    config.oauth.resource = u.origin + u.pathname;\n    startOAuthFlow(config);\n  } else throw e;\n}","preventionTips":["Never paste URLs with #anchor portions from a browser into resource config","Use only scheme://host/path form for resource indicators","Strip fragments programmatically when deriving the resource from a page URL"],"tags":["oauth","configuration","validation","rfc8707"],"backgroundTag":"invalid-resource-indicator","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}