@@ -15,6 +15,7 @@ import {
15
15
Root ,
16
16
ServerNotification ,
17
17
Tool ,
18
+ LoggingLevel ,
18
19
} from "@modelcontextprotocol/sdk/types.js" ;
19
20
import React , { Suspense , useEffect , useRef , useState } from "react" ;
20
21
import { useConnection } from "./lib/hooks/useConnection" ;
@@ -44,23 +45,16 @@ import RootsTab from "./components/RootsTab";
44
45
import SamplingTab , { PendingRequest } from "./components/SamplingTab" ;
45
46
import Sidebar from "./components/Sidebar" ;
46
47
import ToolsTab from "./components/ToolsTab" ;
48
+ import { DEFAULT_INSPECTOR_CONFIG } from "./lib/constants" ;
49
+ import { InspectorConfig } from "./lib/configurationTypes" ;
47
50
48
51
const params = new URLSearchParams ( window . location . search ) ;
49
52
const PROXY_PORT = params . get ( "proxyPort" ) ?? "3000" ;
50
- const PROXY_SERVER_URL = `http://localhost:${ PROXY_PORT } ` ;
53
+ const PROXY_SERVER_URL = `http://${ window . location . hostname } :${ PROXY_PORT } ` ;
54
+ const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1" ;
51
55
52
56
const App = ( ) => {
53
57
// Handle OAuth callback route
54
- if ( window . location . pathname === "/oauth/callback" ) {
55
- const OAuthCallback = React . lazy (
56
- ( ) => import ( "./components/OAuthCallback" ) ,
57
- ) ;
58
- return (
59
- < Suspense fallback = { < div > Loading...</ div > } >
60
- < OAuthCallback />
61
- </ Suspense >
62
- ) ;
63
- }
64
58
const [ resources , setResources ] = useState < Resource [ ] > ( [ ] ) ;
65
59
const [ resourceTemplates , setResourceTemplates ] = useState <
66
60
ResourceTemplate [ ]
@@ -91,13 +85,22 @@ const App = () => {
91
85
( localStorage . getItem ( "lastTransportType" ) as "stdio" | "sse" ) || "stdio"
92
86
) ;
93
87
} ) ;
88
+ const [ logLevel , setLogLevel ] = useState < LoggingLevel > ( "debug" ) ;
94
89
const [ notifications , setNotifications ] = useState < ServerNotification [ ] > ( [ ] ) ;
95
90
const [ stdErrNotifications , setStdErrNotifications ] = useState <
96
91
StdErrNotification [ ]
97
92
> ( [ ] ) ;
98
93
const [ roots , setRoots ] = useState < Root [ ] > ( [ ] ) ;
99
94
const [ env , setEnv ] = useState < Record < string , string > > ( { } ) ;
100
95
96
+ const [ config , setConfig ] = useState < InspectorConfig > ( ( ) => {
97
+ const savedConfig = localStorage . getItem ( CONFIG_LOCAL_STORAGE_KEY ) ;
98
+ return savedConfig ? JSON . parse ( savedConfig ) : DEFAULT_INSPECTOR_CONFIG ;
99
+ } ) ;
100
+ const [ bearerToken , setBearerToken ] = useState < string > ( ( ) => {
101
+ return localStorage . getItem ( "lastBearerToken" ) || "" ;
102
+ } ) ;
103
+
101
104
const [ pendingSampleRequests , setPendingSampleRequests ] = useState <
102
105
Array <
103
106
PendingRequest & {
@@ -109,25 +112,13 @@ const App = () => {
109
112
const nextRequestId = useRef ( 0 ) ;
110
113
const rootsRef = useRef < Root [ ] > ( [ ] ) ;
111
114
112
- const handleApproveSampling = ( id : number , result : CreateMessageResult ) => {
113
- setPendingSampleRequests ( ( prev ) => {
114
- const request = prev . find ( ( r ) => r . id === id ) ;
115
- request ?. resolve ( result ) ;
116
- return prev . filter ( ( r ) => r . id !== id ) ;
117
- } ) ;
118
- } ;
119
-
120
- const handleRejectSampling = ( id : number ) => {
121
- setPendingSampleRequests ( ( prev ) => {
122
- const request = prev . find ( ( r ) => r . id === id ) ;
123
- request ?. reject ( new Error ( "Sampling request rejected" ) ) ;
124
- return prev . filter ( ( r ) => r . id !== id ) ;
125
- } ) ;
126
- } ;
127
-
128
115
const [ selectedResource , setSelectedResource ] = useState < Resource | null > (
129
116
null ,
130
117
) ;
118
+ const [ resourceSubscriptions , setResourceSubscriptions ] = useState <
119
+ Set < string >
120
+ > ( new Set < string > ( ) ) ;
121
+
131
122
const [ selectedPrompt , setSelectedPrompt ] = useState < Prompt | null > ( null ) ;
132
123
const [ selectedTool , setSelectedTool ] = useState < Tool | null > ( null ) ;
133
124
const [ nextResourceCursor , setNextResourceCursor ] = useState <
@@ -160,7 +151,9 @@ const App = () => {
160
151
args,
161
152
sseUrl,
162
153
env,
154
+ bearerToken,
163
155
proxyServerUrl : PROXY_SERVER_URL ,
156
+ requestTimeout : config . MCP_SERVER_REQUEST_TIMEOUT . value as number ,
164
157
onNotification : ( notification ) => {
165
158
setNotifications ( ( prev ) => [ ...prev , notification as ServerNotification ] ) ;
166
159
} ,
@@ -195,6 +188,14 @@ const App = () => {
195
188
localStorage . setItem ( "lastTransportType" , transportType ) ;
196
189
} , [ transportType ] ) ;
197
190
191
+ useEffect ( ( ) => {
192
+ localStorage . setItem ( "lastBearerToken" , bearerToken ) ;
193
+ } , [ bearerToken ] ) ;
194
+
195
+ useEffect ( ( ) => {
196
+ localStorage . setItem ( CONFIG_LOCAL_STORAGE_KEY , JSON . stringify ( config ) ) ;
197
+ } , [ config ] ) ;
198
+
198
199
// Auto-connect if serverUrl is provided in URL params (e.g. after OAuth callback)
199
200
useEffect ( ( ) => {
200
201
const serverUrl = params . get ( "serverUrl" ) ;
@@ -210,7 +211,7 @@ const App = () => {
210
211
// Connect to the server
211
212
connectMcpServer ( ) ;
212
213
}
213
- } , [ ] ) ;
214
+ } , [ connectMcpServer ] ) ;
214
215
215
216
useEffect ( ( ) => {
216
217
fetch ( `${ PROXY_SERVER_URL } /config` )
@@ -239,6 +240,22 @@ const App = () => {
239
240
}
240
241
} , [ ] ) ;
241
242
243
+ const handleApproveSampling = ( id : number , result : CreateMessageResult ) => {
244
+ setPendingSampleRequests ( ( prev ) => {
245
+ const request = prev . find ( ( r ) => r . id === id ) ;
246
+ request ?. resolve ( result ) ;
247
+ return prev . filter ( ( r ) => r . id !== id ) ;
248
+ } ) ;
249
+ } ;
250
+
251
+ const handleRejectSampling = ( id : number ) => {
252
+ setPendingSampleRequests ( ( prev ) => {
253
+ const request = prev . find ( ( r ) => r . id === id ) ;
254
+ request ?. reject ( new Error ( "Sampling request rejected" ) ) ;
255
+ return prev . filter ( ( r ) => r . id !== id ) ;
256
+ } ) ;
257
+ } ;
258
+
242
259
const clearError = ( tabKey : keyof typeof errors ) => {
243
260
setErrors ( ( prev ) => ( { ...prev , [ tabKey ] : null } ) ) ;
244
261
} ;
@@ -308,6 +325,38 @@ const App = () => {
308
325
setResourceContent ( JSON . stringify ( response , null , 2 ) ) ;
309
326
} ;
310
327
328
+ const subscribeToResource = async ( uri : string ) => {
329
+ if ( ! resourceSubscriptions . has ( uri ) ) {
330
+ await makeRequest (
331
+ {
332
+ method : "resources/subscribe" as const ,
333
+ params : { uri } ,
334
+ } ,
335
+ z . object ( { } ) ,
336
+ "resources" ,
337
+ ) ;
338
+ const clone = new Set ( resourceSubscriptions ) ;
339
+ clone . add ( uri ) ;
340
+ setResourceSubscriptions ( clone ) ;
341
+ }
342
+ } ;
343
+
344
+ const unsubscribeFromResource = async ( uri : string ) => {
345
+ if ( resourceSubscriptions . has ( uri ) ) {
346
+ await makeRequest (
347
+ {
348
+ method : "resources/unsubscribe" as const ,
349
+ params : { uri } ,
350
+ } ,
351
+ z . object ( { } ) ,
352
+ "resources" ,
353
+ ) ;
354
+ const clone = new Set ( resourceSubscriptions ) ;
355
+ clone . delete ( uri ) ;
356
+ setResourceSubscriptions ( clone ) ;
357
+ }
358
+ } ;
359
+
311
360
const listPrompts = async ( ) => {
312
361
const response = await makeRequest (
313
362
{
@@ -368,6 +417,28 @@ const App = () => {
368
417
await sendNotification ( { method : "notifications/roots/list_changed" } ) ;
369
418
} ;
370
419
420
+ const sendLogLevelRequest = async ( level : LoggingLevel ) => {
421
+ await makeRequest (
422
+ {
423
+ method : "logging/setLevel" as const ,
424
+ params : { level } ,
425
+ } ,
426
+ z . object ( { } ) ,
427
+ ) ;
428
+ setLogLevel ( level ) ;
429
+ } ;
430
+
431
+ if ( window . location . pathname === "/oauth/callback" ) {
432
+ const OAuthCallback = React . lazy (
433
+ ( ) => import ( "./components/OAuthCallback" ) ,
434
+ ) ;
435
+ return (
436
+ < Suspense fallback = { < div > Loading...</ div > } >
437
+ < OAuthCallback />
438
+ </ Suspense >
439
+ ) ;
440
+ }
441
+
371
442
return (
372
443
< div className = "flex h-screen bg-background" >
373
444
< Sidebar
@@ -382,8 +453,15 @@ const App = () => {
382
453
setSseUrl = { setSseUrl }
383
454
env = { env }
384
455
setEnv = { setEnv }
456
+ config = { config }
457
+ setConfig = { setConfig }
458
+ bearerToken = { bearerToken }
459
+ setBearerToken = { setBearerToken }
385
460
onConnect = { connectMcpServer }
386
461
stdErrNotifications = { stdErrNotifications }
462
+ logLevel = { logLevel }
463
+ sendLogLevelRequest = { sendLogLevelRequest }
464
+ loggingSupported = { ! ! serverCapabilities ?. logging || false }
387
465
/>
388
466
< div className = "flex-1 flex flex-col overflow-hidden" >
389
467
< div className = "flex-1 overflow-auto" >
@@ -485,6 +563,18 @@ const App = () => {
485
563
clearError ( "resources" ) ;
486
564
setSelectedResource ( resource ) ;
487
565
} }
566
+ resourceSubscriptionsSupported = {
567
+ serverCapabilities ?. resources ?. subscribe || false
568
+ }
569
+ resourceSubscriptions = { resourceSubscriptions }
570
+ subscribeToResource = { ( uri ) => {
571
+ clearError ( "resources" ) ;
572
+ subscribeToResource ( uri ) ;
573
+ } }
574
+ unsubscribeFromResource = { ( uri ) => {
575
+ clearError ( "resources" ) ;
576
+ unsubscribeFromResource ( uri ) ;
577
+ } }
488
578
handleCompletion = { handleCompletion }
489
579
completionsSupported = { completionsSupported }
490
580
resourceContent = { resourceContent }
0 commit comments