@@ -152,8 +152,9 @@ export { client as redisClient };
152152// (one McpServer per request), so a client's declared
153153// `io.modelcontextprotocol/ui` capability from initialize is not visible to
154154// later tool calls on the same connection. The route layer records it here,
155- // keyed by the bearer token, so app-only tools can fail closed on hosts that
156- // never declared MCP Apps support.
155+ // keyed by the bearer token plus JWT session claim when available, so
156+ // refreshed access tokens from the same OAuth session keep the marker alive
157+ // while app-only tools still fail closed on hosts that never declared support.
157158const MCP_APPS_KEY_PREFIX = "mcp-apps:" ;
158159
159160function hashBearerToken ( token : string ) : string {
@@ -164,6 +165,33 @@ function hashBearerToken(token: string): string {
164165 return createHmac ( "sha256" , secretKey ) . update ( token ) . digest ( "hex" ) ;
165166}
166167
168+ function decodeJwtPayload ( token : string ) : Record < string , unknown > | null {
169+ const parts = token . split ( "." ) ;
170+ if ( parts . length !== 3 ) return null ;
171+ try {
172+ return JSON . parse (
173+ Buffer . from ( parts [ 1 ] , "base64url" ) . toString ( "utf8" ) ,
174+ ) as Record < string , unknown > ;
175+ } catch {
176+ return null ;
177+ }
178+ }
179+
180+ function mcpAppsMarkerKeys ( token : string ) : string [ ] {
181+ const keys = [ `${ MCP_APPS_KEY_PREFIX } ${ hashBearerToken ( token ) } ` ] ;
182+ const claims = decodeJwtPayload ( token ) ;
183+ const sessionId =
184+ typeof claims ?. sid === "string" && claims . sid
185+ ? claims . sid
186+ : typeof claims ?. session_id === "string" && claims . session_id
187+ ? claims . session_id
188+ : null ;
189+ if ( sessionId ) {
190+ keys . push ( `${ MCP_APPS_KEY_PREFIX } session:${ hashOpaqueToken ( sessionId ) } ` ) ;
191+ }
192+ return keys ;
193+ }
194+
167195export async function markMcpAppsClient ( {
168196 token,
169197 ttlSeconds,
@@ -172,10 +200,10 @@ export async function markMcpAppsClient({
172200 ttlSeconds : number ;
173201} ) : Promise < void > {
174202 await ensureConnected ( ) ;
175- const key = ` ${ MCP_APPS_KEY_PREFIX } ${ hashBearerToken ( token ) } ` ;
176- await withReconnect ( ( ) =>
177- client . setEx ( key , Math . max ( 60 , Math . floor ( ttlSeconds ) ) , "1" ) ,
178- ) ;
203+ const ttl = Math . max ( 60 , Math . floor ( ttlSeconds ) ) ;
204+ for ( const key of mcpAppsMarkerKeys ( token ) ) {
205+ await withReconnect ( ( ) => client . setEx ( key , ttl , "1" ) ) ;
206+ }
179207}
180208
181209/**
@@ -190,12 +218,13 @@ export async function hasMcpAppsClient({
190218 ttlSeconds : number ;
191219} ) : Promise < boolean > {
192220 await ensureConnected ( ) ;
193- const key = `${ MCP_APPS_KEY_PREFIX } ${ hashBearerToken ( token ) } ` ;
194- const value = await withReconnect ( ( ) => client . get ( key ) ) ;
195- if ( value === null ) return false ;
196- await withReconnect ( ( ) =>
197- client . expire ( key , Math . max ( 60 , Math . floor ( ttlSeconds ) ) ) ,
198- ) ;
221+ const ttl = Math . max ( 60 , Math . floor ( ttlSeconds ) ) ;
222+ const keys = mcpAppsMarkerKeys ( token ) ;
223+ const values = await withReconnect ( ( ) => client . mGet ( keys ) ) ;
224+ if ( ! values . some ( ( value ) => value !== null ) ) return false ;
225+ for ( const key of keys ) {
226+ await withReconnect ( ( ) => client . expire ( key , ttl ) ) ;
227+ }
199228 return true ;
200229}
201230
0 commit comments