55using System . Net ;
66using System . Text ;
77using System . Web ;
8+ using static System . Runtime . InteropServices . JavaScript . JSType ;
89
9- namespace ProtectedMCPClient ;
10+ Console . WriteLine ( "Protected MCP Weather Server" ) ;
11+ Console . WriteLine ( ) ;
1012
11- class Program
13+ var serverUrl = "http://localhost:7071/sse" ;
14+ var clientId = Environment . GetEnvironmentVariable ( "CLIENT_ID" ) ?? throw new Exception ( "The CLIENT_ID environment variable is not set." ) ;
15+
16+ // We can customize a shared HttpClient with a custom handler if desired
17+ var sharedHandler = new SocketsHttpHandler
18+ {
19+ PooledConnectionLifetime = TimeSpan . FromMinutes ( 2 ) ,
20+ PooledConnectionIdleTimeout = TimeSpan . FromMinutes ( 1 )
21+ } ;
22+
23+ var httpClient = new HttpClient ( sharedHandler ) ;
24+ // Create the token provider with our custom HttpClient and authorization URL handler
25+ var tokenProvider = new GenericOAuthProvider (
26+ new Uri ( serverUrl ) ,
27+ httpClient ,
28+ null , // AuthorizationHelpers will be created automatically
29+ clientId : clientId ,
30+ clientSecret : "" , // No secret needed for this client
31+ redirectUri : new Uri ( "http://localhost:1179/callback" ) ,
32+ scopes : [ $ "api://{ clientId } /weather.read"] ,
33+ logger : null ,
34+ authorizationUrlHandler : HandleAuthorizationUrlAsync
35+ ) ;
36+
37+ Console . WriteLine ( ) ;
38+ Console . WriteLine ( $ "Connecting to weather server at { serverUrl } ...") ;
39+
40+ try
1241{
13- static async Task Main ( string [ ] args )
42+ var transportOptions = new SseClientTransportOptions
1443 {
15- Console . WriteLine ( "Protected MCP Weather Server" ) ;
16- Console . WriteLine ( ) ;
44+ Endpoint = new Uri ( serverUrl ) ,
45+ Name = "Secure Weather Client"
46+ } ;
1747
18- var serverUrl = "http://localhost:7071/sse" ;
19- var clientId = Environment . GetEnvironmentVariable ( "CLIENT_ID" ) ?? throw new Exception ( "The CLIENT_ID environment variable is not set." ) ;
48+ // Create a transport with authentication support using the correct constructor parameters
49+ var transport = new SseClientTransport (
50+ transportOptions ,
51+ tokenProvider
52+ ) ;
2053
21- // We can customize a shared HttpClient with a custom handler if desired
22- var sharedHandler = new SocketsHttpHandler
23- {
24- PooledConnectionLifetime = TimeSpan . FromMinutes ( 2 ) ,
25- PooledConnectionIdleTimeout = TimeSpan . FromMinutes ( 1 )
26- } ;
54+ var client = await McpClientFactory . CreateAsync ( transport ) ;
55+
56+ var tools = await client . ListToolsAsync ( ) ;
57+ if ( tools . Count == 0 )
58+ {
59+ Console . WriteLine ( "No tools available on the server." ) ;
60+ return ;
61+ }
62+
63+ Console . WriteLine ( $ "Found { tools . Count } tools on the server.") ;
64+ Console . WriteLine ( ) ;
65+
66+ if ( tools . Any ( t => t . Name == "GetAlerts" ) )
67+ {
68+ Console . WriteLine ( "Calling GetAlerts tool..." ) ;
2769
28- var httpClient = new HttpClient ( sharedHandler ) ;
29- // Create the token provider with our custom HttpClient and authorization URL handler
30- var tokenProvider = new GenericOAuthProvider (
31- new Uri ( serverUrl ) ,
32- httpClient ,
33- null , // AuthorizationHelpers will be created automatically
34- clientId : clientId ,
35- clientSecret : "" , // No secret needed for this client
36- redirectUri : new Uri ( "http://localhost:1179/callback" ) ,
37- scopes : [ $ "api://{ clientId } /weather.read"] ,
38- logger : null ,
39- authorizationUrlHandler : HandleAuthorizationUrlAsync
70+ var result = await client . CallToolAsync (
71+ "GetAlerts" ,
72+ new Dictionary < string , object ? > { { "state" , "WA" } }
4073 ) ;
4174
75+ Console . WriteLine ( "Result: " + ( ( TextContentBlock ) result . Content [ 0 ] ) . Text ) ;
4276 Console . WriteLine ( ) ;
43- Console . WriteLine ( $ "Connecting to weather server at { serverUrl } ...") ;
44-
45- try
46- {
47- var transportOptions = new SseClientTransportOptions
48- {
49- Endpoint = new Uri ( serverUrl ) ,
50- Name = "Secure Weather Client"
51- } ;
52-
53- // Create a transport with authentication support using the correct constructor parameters
54- var transport = new SseClientTransport (
55- transportOptions ,
56- tokenProvider
57- ) ;
58-
59- var client = await McpClientFactory . CreateAsync ( transport ) ;
60-
61- var tools = await client . ListToolsAsync ( ) ;
62- if ( tools . Count == 0 )
63- {
64- Console . WriteLine ( "No tools available on the server." ) ;
65- return ;
66- }
67-
68- Console . WriteLine ( $ "Found { tools . Count } tools on the server.") ;
69- Console . WriteLine ( ) ;
70-
71- if ( tools . Any ( t => t . Name == "GetAlerts" ) )
72- {
73- Console . WriteLine ( "Calling GetAlerts tool..." ) ;
74-
75- var result = await client . CallToolAsync (
76- "GetAlerts" ,
77- new Dictionary < string , object ? > { { "state" , "WA" } }
78- ) ;
79-
80- Console . WriteLine ( "Result: " + ( ( TextContentBlock ) result . Content [ 0 ] ) . Text ) ;
81- Console . WriteLine ( ) ;
82- }
83- }
84- catch ( Exception ex )
85- {
86- Console . WriteLine ( $ "Error: { ex . Message } ") ;
87- if ( ex . InnerException != null )
88- {
89- Console . WriteLine ( $ "Inner error: { ex . InnerException . Message } ") ;
90- }
77+ }
78+ }
79+ catch ( Exception ex )
80+ {
81+ Console . WriteLine ( $ "Error: { ex . Message } ") ;
82+ if ( ex . InnerException != null )
83+ {
84+ Console . WriteLine ( $ "Inner error: { ex . InnerException . Message } ") ;
85+ }
9186
9287#if DEBUG
93- Console . WriteLine ( $ "Stack trace: { ex . StackTrace } ") ;
88+ Console . WriteLine ( $ "Stack trace: { ex . StackTrace } ") ;
9489#endif
95- }
96- Console . WriteLine ( "Press any key to exit..." ) ;
97- Console . ReadKey ( ) ;
98- }
90+ }
91+ Console . WriteLine ( "Press any key to exit..." ) ;
92+ Console . ReadKey ( ) ;
93+
94+ /// <summary>
95+ /// Handles the OAuth authorization URL by starting a local HTTP server and opening a browser.
96+ /// This implementation demonstrates how SDK consumers can provide their own authorization flow.
97+ /// </summary>
98+ /// <param name="authorizationUrl">The authorization URL to open in the browser.</param>
99+ /// <param name="redirectUri">The redirect URI where the authorization code will be sent.</param>
100+ /// <param name="cancellationToken">The cancellation token.</param>
101+ /// <returns>The authorization code extracted from the callback, or null if the operation failed.</returns>
102+ static async Task < string ? > HandleAuthorizationUrlAsync ( Uri authorizationUrl , Uri redirectUri , CancellationToken cancellationToken )
103+ {
104+ Console . WriteLine ( "Starting OAuth authorization flow..." ) ;
105+ Console . WriteLine ( $ "Opening browser to: { authorizationUrl } ") ;
106+
107+ var listenerPrefix = redirectUri . GetLeftPart ( UriPartial . Authority ) ;
108+ if ( ! listenerPrefix . EndsWith ( "/" ) ) listenerPrefix += "/" ;
109+
110+ using var listener = new HttpListener ( ) ;
111+ listener . Prefixes . Add ( listenerPrefix ) ;
99112
100- /// <summary>
101- /// Handles the OAuth authorization URL by starting a local HTTP server and opening a browser.
102- /// This implementation demonstrates how SDK consumers can provide their own authorization flow.
103- /// </summary>
104- /// <param name="authorizationUrl">The authorization URL to open in the browser.</param>
105- /// <param name="redirectUri">The redirect URI where the authorization code will be sent.</param>
106- /// <param name="cancellationToken">The cancellation token.</param>
107- /// <returns>The authorization code extracted from the callback, or null if the operation failed.</returns>
108- private static async Task < string ? > HandleAuthorizationUrlAsync ( Uri authorizationUrl , Uri redirectUri , CancellationToken cancellationToken )
113+ try
109114 {
110- Console . WriteLine ( "Starting OAuth authorization flow..." ) ;
111- Console . WriteLine ( $ "Opening browser to : { authorizationUrl } ") ;
115+ listener . Start ( ) ;
116+ Console . WriteLine ( $ "Listening for OAuth callback on : { listenerPrefix } ") ;
112117
113- var listenerPrefix = redirectUri . GetLeftPart ( UriPartial . Authority ) ;
114- if ( ! listenerPrefix . EndsWith ( "/" ) ) listenerPrefix += "/" ;
118+ OpenBrowser ( authorizationUrl ) ;
115119
116- using var listener = new HttpListener ( ) ;
117- listener . Prefixes . Add ( listenerPrefix ) ;
120+ var context = await listener . GetContextAsync ( ) ;
121+ var query = HttpUtility . ParseQueryString ( context . Request . Url ? . Query ?? string . Empty ) ;
122+ var code = query [ "code" ] ;
123+ var error = query [ "error" ] ;
118124
119- try
120- {
121- listener . Start ( ) ;
122- Console . WriteLine ( $ "Listening for OAuth callback on: { listenerPrefix } ") ;
123-
124- OpenBrowser ( authorizationUrl ) ;
125-
126- var context = await listener . GetContextAsync ( ) ;
127- var query = HttpUtility . ParseQueryString ( context . Request . Url ? . Query ?? string . Empty ) ;
128- var code = query [ "code" ] ;
129- var error = query [ "error" ] ;
130-
131- string responseHtml = "<html><body><h1>Authentication complete</h1><p>You can close this window now.</p></body></html>" ;
132- byte [ ] buffer = Encoding . UTF8 . GetBytes ( responseHtml ) ;
133- context . Response . ContentLength64 = buffer . Length ;
134- context . Response . ContentType = "text/html" ;
135- context . Response . OutputStream . Write ( buffer , 0 , buffer . Length ) ;
136- context . Response . Close ( ) ;
137-
138- if ( ! string . IsNullOrEmpty ( error ) )
139- {
140- Console . WriteLine ( $ "Auth error: { error } ") ;
141- return null ;
142- }
143-
144- if ( string . IsNullOrEmpty ( code ) )
145- {
146- Console . WriteLine ( "No authorization code received" ) ;
147- return null ;
148- }
149-
150- Console . WriteLine ( "Authorization code received successfully." ) ;
151- return code ;
152- }
153- catch ( Exception ex )
125+ string responseHtml = "<html><body><h1>Authentication complete</h1><p>You can close this window now.</p></body></html>" ;
126+ byte [ ] buffer = Encoding . UTF8 . GetBytes ( responseHtml ) ;
127+ context . Response . ContentLength64 = buffer . Length ;
128+ context . Response . ContentType = "text/html" ;
129+ context . Response . OutputStream . Write ( buffer , 0 , buffer . Length ) ;
130+ context . Response . Close ( ) ;
131+
132+ if ( ! string . IsNullOrEmpty ( error ) )
154133 {
155- Console . WriteLine ( $ "Error getting auth code : { ex . Message } ") ;
134+ Console . WriteLine ( $ "Auth error : { error } ") ;
156135 return null ;
157136 }
158- finally
137+
138+ if ( string . IsNullOrEmpty ( code ) )
159139 {
160- if ( listener . IsListening ) listener . Stop ( ) ;
140+ Console . WriteLine ( "No authorization code received" ) ;
141+ return null ;
161142 }
143+
144+ Console . WriteLine ( "Authorization code received successfully." ) ;
145+ return code ;
146+ }
147+ catch ( Exception ex )
148+ {
149+ Console . WriteLine ( $ "Error getting auth code: { ex . Message } ") ;
150+ return null ;
151+ }
152+ finally
153+ {
154+ if ( listener . IsListening ) listener . Stop ( ) ;
162155 }
156+ }
163157
164- /// <summary>
165- /// Opens the specified URL in the default browser.
166- /// </summary>
167- /// <param name="url">The URL to open.</param>
168- private static void OpenBrowser ( Uri url )
158+ /// <summary>
159+ /// Opens the specified URL in the default browser.
160+ /// </summary>
161+ /// <param name="url">The URL to open.</param>
162+ static void OpenBrowser ( Uri url )
163+ {
164+ try
169165 {
170- try
171- {
172- var psi = new ProcessStartInfo
173- {
174- FileName = url . ToString ( ) ,
175- UseShellExecute = true
176- } ;
177- Process . Start ( psi ) ;
178- }
179- catch ( Exception ex )
166+ var psi = new ProcessStartInfo
180167 {
181- Console . WriteLine ( $ "Error opening browser. { ex . Message } ") ;
182- }
168+ FileName = url . ToString ( ) ,
169+ UseShellExecute = true
170+ } ;
171+ Process . Start ( psi ) ;
172+ }
173+ catch ( Exception ex )
174+ {
175+ Console . WriteLine ( $ "Error opening browser. { ex . Message } ") ;
183176 }
184177}
0 commit comments