@@ -20,7 +20,7 @@ import { visit } from "../src/surfaces/browser";
2020const api = composePluginApi ( [ mcpHttpPlugin ( ) ] as const ) ;
2121
2222scenario (
23- "MCP OAuth · advertised CIMD starts authorization without dynamic registration" ,
23+ "MCP OAuth · CIMD advertises refresh support and completes connection without dynamic registration" ,
2424 { timeout : 180_000 } ,
2525 Effect . scoped (
2626 Effect . gen ( function * ( ) {
@@ -30,7 +30,7 @@ scenario(
3030 const oauth = yield * OAuthTestServer ;
3131 const server = yield * serveMcpServerWithOAuth (
3232 ( ) => makeGreetingMcpServer ( { name : "cimd-connect-mcp" } ) ,
33- { path : "/mcp" } ,
33+ { path : "/mcp" , scopes : [ "read" , "offline_access" ] } ,
3434 ) ;
3535 const identity = yield * target . newIdentity ( ) ;
3636 const client = yield * makeApiClient ( api , identity ) ;
@@ -69,17 +69,68 @@ scenario(
6969 authorize ,
7070 "the popup reached the discovered authorization endpoint" ,
7171 ) . toBeDefined ( ) ;
72- const clientId = authorize ?. query [ "client_id" ] ;
73- createdClientId = clientId ;
72+ expect (
73+ ( authorize ?. query [ "scope" ] ?? "" ) . split ( " " ) ,
74+ "authorization requests the resource's offline access scope" ,
75+ ) . toContain ( "offline_access" ) ;
76+ const clientId = authorize ?. query [ "client_id" ] ?? "" ;
77+ createdClientId = clientId || undefined ;
7478 expect (
7579 clientId ,
7680 "authorization uses Executor's metadata document as client_id" ,
7781 ) . toMatch ( / ^ h t t p s ? : \/ \/ [ ^ / ] + \/ a p i \/ o a u t h \/ c l i e n t - i d - m e t a d a t a \/ .+ \. j s o n $ / ) ;
78- await popup . close ( ) ;
82+ const metadataResponse = await page . request . get ( clientId ) ;
83+ expect ( metadataResponse . status ( ) , "the client metadata document is reachable" ) . toBe (
84+ 200 ,
85+ ) ;
86+ expect (
87+ await metadataResponse . json ( ) ,
88+ "the client declares the grant required by offline_access" ,
89+ ) . toMatchObject ( {
90+ grant_types : [ "authorization_code" , "refresh_token" ] ,
91+ } ) ;
92+ expect ( authorize ) . toBeDefined ( ) ;
93+ // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- test boundary: authorization must exist before completing the flow
94+ if ( authorize === undefined ) throw new Error ( "Missing authorization request" ) ;
95+ const completed = await Effect . runPromise (
96+ oauth . completeAuthorizationCodeFlow ( { authorizationUrl : authorize . url } ) ,
97+ ) ;
98+ await popup . goto ( completed . callbackUrl ) ;
99+ await page
100+ . getByRole ( "heading" , { name : / A d d c o n n e c t i o n / } )
101+ . waitFor ( { state : "hidden" } ) ;
102+ await popup . close ( ) . catch ( ( ) => undefined ) ;
79103 } ) ;
80104 } ) ;
81105
106+ const connections = yield * client . connections . list ( { query : { integration : slug } } ) ;
107+ expect ( connections , "the OAuth callback saved the connection" ) . toHaveLength ( 1 ) ;
108+ const tools = yield * client . tools . list ( { query : { integration : slug } } ) ;
109+ expect (
110+ tools . some ( ( tool ) => tool . name === "simple_echo" ) ,
111+ "authenticated discovery finds the upstream tool" ,
112+ ) . toBe ( true ) ;
113+
114+ const invoked = yield * client . executions . execute ( {
115+ payload : {
116+ code : `return await ${ tools [ 0 ] ?. address } ({});` ,
117+ autoApprove : true ,
118+ } ,
119+ } ) ;
120+ expect ( invoked . status ) . toBe ( "completed" ) ;
121+ expect ( invoked . text , "the connected tool runs through authenticated MCP" ) . toContain (
122+ "mcp-ok" ,
123+ ) ;
124+
82125 const requests = yield * oauth . requests ;
126+ expect (
127+ requests . some (
128+ ( request ) =>
129+ request . path === "/token" &&
130+ new URLSearchParams ( request . body ) . get ( "grant_type" ) === "authorization_code" ,
131+ ) ,
132+ "the callback exchanged the code using the advertised client" ,
133+ ) . toBe ( true ) ;
83134 expect (
84135 requests . filter ( ( request ) => request . method === "POST" && request . path === "/register" ) ,
85136 "CIMD wins when the server also advertises DCR" ,
@@ -105,5 +156,12 @@ scenario(
105156 ) ,
106157 ) ;
107158 } ) ,
108- ) . pipe ( Effect . provide ( OAuthTestServer . layer ( { clientIdMetadataDocumentSupported : true } ) ) ) ,
159+ ) . pipe (
160+ Effect . provide (
161+ OAuthTestServer . layer ( {
162+ clientIdMetadataDocumentSupported : true ,
163+ scopes : [ "read" , "offline_access" ] ,
164+ } ) ,
165+ ) ,
166+ ) ,
109167) ;
0 commit comments