@@ -3,34 +3,174 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
33import { test , expect , inject } from 'vitest'
44
55const mcpServerPort = inject ( 'mcpServerPort' )
6+ const EPIC_ME_SERVER_URL = 'http://localhost:7788'
67
7- async function setupClient ( ) {
8- const client = new Client (
9- {
10- name : 'EpicMeTester' ,
11- version : '1.0.0' ,
12- } ,
13- { capabilities : { } } ,
14- )
15-
16- const transport = new StreamableHTTPClientTransport (
17- new URL ( `http://localhost:${ mcpServerPort } /mcp` ) ,
8+ // Helper function to generate PKCE challenge
9+ function generateCodeChallenge ( ) {
10+ const codeVerifier = btoa (
11+ String . fromCharCode ( ...crypto . getRandomValues ( new Uint8Array ( 32 ) ) ) ,
1812 )
19-
20- await client . connect ( transport )
13+ . replace ( / \+ / g, '-' )
14+ . replace ( / \/ / g, '_' )
15+ . replace ( / = / g, '' )
2116
2217 return {
23- client,
24- async [ Symbol . asyncDispose ] ( ) {
25- await client . transport ?. close ( )
26- } ,
18+ codeVerifier,
19+ codeChallenge : codeVerifier , // For simplicity, using plain method
20+ codeChallengeMethod : 'plain' ,
2721 }
2822}
2923
30- test ( 'listing tools works' , async ( ) => {
31- await using setup = await setupClient ( )
32- const { client } = setup
24+ test ( 'OAuth integration flow works end-to-end' , async ( ) => {
25+ const mcpServerUrl = `http://localhost:${ mcpServerPort } `
26+
27+ // Step 0: Verify 401 response headers from initial unauthorized request
28+ const unauthorizedResponse = await fetch ( `${ mcpServerUrl } /mcp` , {
29+ method : 'POST' ,
30+ headers : { 'Content-Type' : 'application/json' } ,
31+ body : JSON . stringify ( {
32+ jsonrpc : '2.0' ,
33+ id : 1 ,
34+ method : 'tools/list' ,
35+ } ) ,
36+ } )
37+
38+ expect ( unauthorizedResponse . status , '🚨 Expected 401 status for unauthorized request' ) . toBe ( 401 )
39+
40+ const wwwAuthHeader = unauthorizedResponse . headers . get ( 'WWW-Authenticate' )
41+ expect ( wwwAuthHeader , '🚨 WWW-Authenticate header should be present' ) . toBeTruthy ( )
42+ expect ( wwwAuthHeader , '🚨 WWW-Authenticate header should contain OAuth realm' ) . toContain ( 'OAuth realm="EpicMe"' )
43+ expect ( wwwAuthHeader , '🚨 WWW-Authenticate header should contain authorization_url' ) . toContain ( 'authorization_url=' )
44+
45+ // Extract the authorization URL from the header
46+ const authUrlMatch = wwwAuthHeader ?. match ( / a u t h o r i z a t i o n _ u r l = " ( [ ^ " ] + ) " / )
47+ expect ( authUrlMatch , '🚨 Could not extract authorization URL from WWW-Authenticate header' ) . toBeTruthy ( )
48+ const authorizationUrl = authUrlMatch ! [ 1 ]
49+
50+ // Step 1: Metadata discovery
51+ // Test OAuth Authorization Server discovery
52+ const authServerDiscoveryResponse = await fetch ( `${ mcpServerUrl } /.well-known/oauth-authorization-server` )
53+ expect ( authServerDiscoveryResponse . ok , '🚨 OAuth authorization server discovery should succeed' ) . toBe ( true )
54+
55+ const authServerConfig = await authServerDiscoveryResponse . json ( )
56+ expect ( authServerConfig . authorization_endpoint , '🚨 Authorization endpoint should be present in discovery' ) . toBeTruthy ( )
57+ expect ( authServerConfig . token_endpoint , '🚨 Token endpoint should be present in discovery' ) . toBeTruthy ( )
58+
59+ // Test OAuth Protected Resource discovery
60+ const protectedResourceDiscoveryResponse = await fetch ( `${ mcpServerUrl } /.well-known/oauth-protected-resource/mcp` )
61+ expect ( protectedResourceDiscoveryResponse . ok , '🚨 OAuth protected resource discovery should succeed' ) . toBe ( true )
62+
63+ const protectedResourceConfig = await protectedResourceDiscoveryResponse . json ( )
64+ expect ( protectedResourceConfig . resource , '🚨 Resource identifier should be present' ) . toBe ( 'epicme-mcp' )
65+ expect ( protectedResourceConfig . scopes , '🚨 Scopes should be present' ) . toContain ( 'read' )
66+ expect ( protectedResourceConfig . scopes , '🚨 Scopes should contain write' ) . toContain ( 'write' )
3367
34- const result = await client . listTools ( )
35- expect ( result . tools . length ) . toBeGreaterThan ( 0 )
68+ // Step 2: Dynamic client registration
69+ const clientRegistrationResponse = await fetch ( `${ EPIC_ME_SERVER_URL } /register` , {
70+ method : 'POST' ,
71+ headers : { 'Content-Type' : 'application/json' } ,
72+ body : JSON . stringify ( {
73+ client_name : 'Test MCP Client' ,
74+ redirect_uris : [ `${ mcpServerUrl } /mcp` ] ,
75+ scope : 'read write' ,
76+ } ) ,
77+ } )
78+
79+ expect ( clientRegistrationResponse . ok , '🚨 Client registration should succeed' ) . toBe ( true )
80+ const clientRegistration = await clientRegistrationResponse . json ( )
81+ expect ( clientRegistration . client_id , '🚨 Client ID should be returned from registration' ) . toBeTruthy ( )
82+
83+ // Step 3: Preparing Authorization (getting the auth URL)
84+ const { codeVerifier, codeChallenge, codeChallengeMethod } = generateCodeChallenge ( )
85+ const state = crypto . randomUUID ( )
86+ const redirectUri = `${ mcpServerUrl } /mcp`
87+
88+ const authUrl = new URL ( authorizationUrl )
89+ const originalParams = JSON . parse ( authUrl . searchParams . get ( 'oauth_req_info' ) || '{}' )
90+
91+ expect ( originalParams . client_id , '🚨 Client ID should be present in auth URL' ) . toBeTruthy ( )
92+ expect ( originalParams . redirect_uri , '🚨 Redirect URI should be present in auth URL' ) . toBeTruthy ( )
93+ expect ( originalParams . response_type , '🚨 Response type should be code' ) . toBe ( 'code' )
94+
95+ // Step 4: Requesting the auth code programmatically
96+ const testAuthUrl = new URL ( `${ EPIC_ME_SERVER_URL } /test-auth` )
97+ // Use the registered client ID instead of the one from the auth URL
98+ testAuthUrl . searchParams . set ( 'client_id' , clientRegistration . client_id )
99+ testAuthUrl . searchParams . set ( 'redirect_uri' , redirectUri )
100+ testAuthUrl . searchParams . set ( 'response_type' , 'code' )
101+ testAuthUrl . searchParams . set ( 'code_challenge' , codeChallenge )
102+ testAuthUrl . searchParams . set ( 'code_challenge_method' , codeChallengeMethod )
103+ testAuthUrl . searchParams . set ( 'scope' , 'read write' )
104+ testAuthUrl . searchParams . set ( 'state' , state )
105+
106+ const authCodeResponse = await fetch ( testAuthUrl . toString ( ) )
107+ expect ( authCodeResponse . ok , '🚨 Auth code request should succeed' ) . toBe ( true )
108+
109+ const authResult = await authCodeResponse . json ( )
110+ expect ( authResult . redirectTo , '🚨 Redirect URL should be returned' ) . toBeTruthy ( )
111+
112+ // Step 5: Supplying the auth code (extract from redirect URL)
113+ const redirectUrl = new URL ( authResult . redirectTo )
114+ const authCode = redirectUrl . searchParams . get ( 'code' )
115+ const returnedState = redirectUrl . searchParams . get ( 'state' )
116+
117+ expect ( authCode , '🚨 Auth code should be present in redirect URL' ) . toBeTruthy ( )
118+ expect ( returnedState , '🚨 State should be returned' ) . toBe ( state )
119+
120+ // Step 6: Requesting the token
121+ const tokenParams = new URLSearchParams ( {
122+ grant_type : 'authorization_code' ,
123+ code : authCode ! ,
124+ redirect_uri : redirectUri ,
125+ client_id : clientRegistration . client_id , // Use registered client ID
126+ code_verifier : codeVerifier ,
127+ } )
128+
129+ // Add client_secret if provided during registration
130+ if ( clientRegistration . client_secret ) {
131+ tokenParams . set ( 'client_secret' , clientRegistration . client_secret )
132+ }
133+
134+ const tokenResponse = await fetch ( `${ EPIC_ME_SERVER_URL } /token` , {
135+ method : 'POST' ,
136+ headers : { 'Content-Type' : 'application/x-www-form-urlencoded' } ,
137+ body : tokenParams ,
138+ } )
139+
140+ if ( ! tokenResponse . ok ) {
141+ const errorText = await tokenResponse . text ( )
142+ console . error ( 'Token exchange failed:' , tokenResponse . status , errorText )
143+ }
144+
145+ expect ( tokenResponse . ok , '🚨 Token exchange should succeed' ) . toBe ( true )
146+ const tokenResult = await tokenResponse . json ( )
147+ expect ( tokenResult . access_token , '🚨 Access token should be returned' ) . toBeTruthy ( )
148+ expect ( tokenResult . token_type ?. toLowerCase ( ) , '🚨 Token type should be Bearer' ) . toBe ( 'bearer' )
149+
150+ // Step 7: Performing authenticated requests (listing tools)
151+ // Verify the token works by making a simple authenticated request to the MCP server
152+ // We'll test that we get past the authentication (no 401) even if we get protocol errors
153+ const authTestResponse = await fetch ( `${ mcpServerUrl } /mcp` , {
154+ method : 'POST' ,
155+ headers : {
156+ 'Content-Type' : 'application/json' ,
157+ 'Accept' : 'application/json, text/event-stream' ,
158+ Authorization : `Bearer ${ tokenResult . access_token } ` ,
159+ } ,
160+ body : JSON . stringify ( {
161+ jsonrpc : '2.0' ,
162+ id : 1 ,
163+ method : 'initialize' ,
164+ params : {
165+ protocolVersion : '2024-11-05' ,
166+ capabilities : { } ,
167+ clientInfo : {
168+ name : 'Test Client' ,
169+ version : '1.0.0' ,
170+ } ,
171+ } ,
172+ } ) ,
173+ } )
174+
175+ expect ( authTestResponse . status , '🚨 Should not get 401 Unauthorized with valid token' ) . not . toBe ( 401 )
36176} )
0 commit comments