33 * private OpenClaw state directory with one hashed file per MCP server URL.
44 */
55import { createHash , randomUUID } from "node:crypto" ;
6- import fs from "node:fs/promises" ;
6+ import fs from "node:fs" ;
7+ import fsPromises from "node:fs/promises" ;
78import path from "node:path" ;
89import {
910 auth ,
@@ -26,6 +27,7 @@ type McpOAuthStore = {
2627 codeVerifier ?: string ;
2728 discoveryState ?: OAuthDiscoveryState ;
2829 lastAuthorizationUrl ?: string ;
30+ redirectUrl ?: string ;
2931 state ?: string ;
3032} ;
3133
@@ -59,24 +61,42 @@ function oauthStorePath(serverName: string, serverUrl: string): string {
5961
6062async function readStore ( filePath : string ) : Promise < McpOAuthStore > {
6163 try {
62- return JSON . parse ( await fs . readFile ( filePath , "utf-8" ) ) as McpOAuthStore ;
64+ return JSON . parse ( await fsPromises . readFile ( filePath , "utf-8" ) ) as McpOAuthStore ;
65+ } catch {
66+ return { } ;
67+ }
68+ }
69+
70+ function readStoreSync ( filePath : string ) : McpOAuthStore {
71+ try {
72+ return JSON . parse ( fs . readFileSync ( filePath , "utf-8" ) ) as McpOAuthStore ;
6373 } catch {
6474 return { } ;
6575 }
6676}
6777
6878async function writeStore ( filePath : string , store : McpOAuthStore ) : Promise < void > {
69- await fs . mkdir ( path . dirname ( filePath ) , { recursive : true , mode : 0o700 } ) ;
70- await fs . writeFile ( filePath , JSON . stringify ( store , null , 2 ) , { encoding : "utf-8" , mode : 0o600 } ) ;
71- await fs . chmod ( filePath , 0o600 ) . catch ( ( ) => { } ) ;
79+ await fsPromises . mkdir ( path . dirname ( filePath ) , { recursive : true , mode : 0o700 } ) ;
80+ await fsPromises . writeFile ( filePath , JSON . stringify ( store , null , 2 ) , {
81+ encoding : "utf-8" ,
82+ mode : 0o600 ,
83+ } ) ;
84+ await fsPromises . chmod ( filePath , 0o600 ) . catch ( ( ) => { } ) ;
7285}
7386
74- function resolveOAuthRedirectUrl ( config : McpOAuthConfig ) : string {
75- return normalizeOptionalString ( config . redirectUrl ) ?? LEGACY_DEFAULT_REDIRECT_URL ;
87+ function resolveOAuthRedirectUrl ( config : McpOAuthConfig , store : McpOAuthStore = { } ) : string {
88+ return (
89+ normalizeOptionalString ( config . redirectUrl ) ??
90+ normalizeOptionalString ( store . redirectUrl ) ??
91+ LEGACY_DEFAULT_REDIRECT_URL
92+ ) ;
7693}
7794
78- function buildOAuthClientMetadata ( config : McpOAuthConfig ) : OAuthClientMetadata {
79- const redirectUrl = resolveOAuthRedirectUrl ( config ) ;
95+ function buildOAuthClientMetadata (
96+ config : McpOAuthConfig ,
97+ store : McpOAuthStore = { } ,
98+ ) : OAuthClientMetadata {
99+ const redirectUrl = resolveOAuthRedirectUrl ( config , store ) ;
80100 return {
81101 client_name : "OpenClaw MCP" ,
82102 redirect_uris : [ redirectUrl ] ,
@@ -99,7 +119,6 @@ export function createMcpOAuthClientProvider(params: {
99119} ) : OAuthClientProvider {
100120 const config = params . config ?? { } ;
101121 const filePath = oauthStorePath ( params . serverName , params . serverUrl ) ;
102- const redirectUrl = resolveOAuthRedirectUrl ( config ) ;
103122 const allowAuthorizationRedirect =
104123 params . allowAuthorizationRedirect ?? Boolean ( params . onAuthorizationUrl ) ;
105124 const assertAuthorizationRedirectAllowed = ( ) => {
@@ -111,11 +130,11 @@ export function createMcpOAuthClientProvider(params: {
111130 } ;
112131 return {
113132 get redirectUrl ( ) {
114- return redirectUrl ;
133+ return resolveOAuthRedirectUrl ( config , readStoreSync ( filePath ) ) ;
115134 } ,
116135 clientMetadataUrl : normalizeOptionalString ( config . clientMetadataUrl ) ,
117136 get clientMetadata ( ) {
118- return buildOAuthClientMetadata ( config ) ;
137+ return buildOAuthClientMetadata ( config , readStoreSync ( filePath ) ) ;
119138 } ,
120139 async state ( ) {
121140 assertAuthorizationRedirectAllowed ( ) ;
@@ -188,7 +207,7 @@ export async function clearMcpOAuthCredentials(params: {
188207 serverName : string ;
189208 serverUrl : string ;
190209} ) : Promise < void > {
191- await fs . rm ( oauthStorePath ( params . serverName , params . serverUrl ) , { force : true } ) ;
210+ await fsPromises . rm ( oauthStorePath ( params . serverName , params . serverUrl ) , { force : true } ) ;
192211}
193212
194213/** Reads stored OAuth credential presence without exposing credential values. */
@@ -238,13 +257,23 @@ export async function runMcpOAuthLogin(params: {
238257 fetchFn ?: FetchLike ;
239258 onAuthorizationUrl ?: ( url : URL ) => void | Promise < void > ;
240259} ) : Promise < "authorized" | "redirect" > {
260+ const filePath = oauthStorePath ( params . serverName , params . serverUrl ) ;
261+ const store = await readStore ( filePath ) ;
262+ const loginParams = {
263+ ...params ,
264+ config : {
265+ ...params . config ,
266+ redirectUrl : normalizeOptionalString ( params . config ?. redirectUrl ) ?? store . redirectUrl ,
267+ } ,
268+ } ;
241269 try {
242- return await runMcpOAuthLoginAttempt ( params ) ;
270+ return await runMcpOAuthLoginAttempt ( loginParams ) ;
243271 } catch ( error ) {
244272 if (
245273 ! normalizeOptionalString ( params . config ?. redirectUrl ) &&
246274 isMcpOAuthRedirectRegistrationError ( error )
247275 ) {
276+ await writeStore ( filePath , { ...store , redirectUrl : LOCALHOST_REDIRECT_URL } ) ;
248277 return await runMcpOAuthLoginAttempt ( {
249278 ...params ,
250279 config : {
0 commit comments