diff --git a/client/coverage/base.css b/client/coverage/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/client/coverage/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/client/coverage/block-navigation.js b/client/coverage/block-navigation.js new file mode 100644 index 0000000..cc12130 --- /dev/null +++ b/client/coverage/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selecter that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/client/coverage/client/bin/cli.js.html b/client/coverage/client/bin/cli.js.html new file mode 100644 index 0000000..29aaeb3 --- /dev/null +++ b/client/coverage/client/bin/cli.js.html @@ -0,0 +1,133 @@ + + + + +
++ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 | + + + + + + + + + + + + + + + + | #!/usr/bin/env node + +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; +import handler from "serve-handler"; +import http from "http"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const distPath = join(__dirname, "../dist"); + +const server = http.createServer((request, response) => { + return handler(request, response, { public: distPath }); +}); + +const port = process.env.PORT || 5173; +server.listen(port, () => {}); + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| cli.js | +
+
+ |
+ 0% | +0/16 | +0% | +0/1 | +0% | +0/1 | +0% | +0/16 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| postcss.config.js | +
+
+ |
+ 0% | +0/6 | +0% | +0/1 | +0% | +0/1 | +0% | +0/6 | +
| tailwind.config.js | +
+
+ |
+ 0% | +0/58 | +0% | +0/1 | +0% | +0/1 | +0% | +0/58 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 | + + + + + + | export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 | 1x +1x + + + + + + + + + + + + + + + + + +1x +1x + + + +1x + + + + + + + +1x + + +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x + +1x +1x +1x + +1x +16x +16x + +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +7x +16x +16x +7x +16x + +16x +16x +16x +16x + +16x +16x +16x + +16x + + + + + + +16x +16x +16x + +16x + + + + + + + +16x + + + + + + + +16x +16x +16x +16x +16x +16x + +16x +16x + +16x +16x + +16x +16x +16x + +16x +16x +16x +16x + +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x + + +16x + + +16x +1x +1x +1x +1x +1x +16x +16x + +16x + + + + + + + + + + + + + + + + + + + + + + +16x +10x +16x + +16x +8x +16x + +16x +7x +7x +7x +7x +7x +7x +7x +7x +7x +7x +7x +7x + +7x +16x + +16x +7x +16x + +16x +7x +7x +7x +16x + +16x + + + +16x + + + + + + + + + + + + +16x + + + + + + + + + + + + + + + + +16x + + + + + + + + + + + +16x + + + + + + + + + + + + +16x + + + + + + + + + + + +16x + + + + + + + + + + + + +16x + + + + + + + + + + + + + + + + + +16x + + + +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +9x +9x +9x + +9x +7x +5x +5x + +9x +9x + +9x +9x +9x + +9x +9x +9x + +9x +9x +9x + +9x +9x +9x + +9x +9x +9x + +9x +1x +1x +1x + +9x +9x +9x + +9x +9x + +9x +9x +5x +5x + +5x +5x + +4x +4x +4x +4x +4x + + + +4x + + + +4x + + + +4x + + + +4x + + + +4x +4x + + + +4x +4x +4x +4x +4x +4x +4x +4x + + + +4x + + + +4x + + + +4x +4x + + + +4x +4x +4x +4x +4x +4x +4x + + + +4x + + + +4x + + + +4x +4x + + + + +4x +4x +4x +4x +4x +4x +4x + + + + + + + +4x +4x +4x +4x +4x +4x +4x +4x +4x +4x +4x +4x + +9x +9x + +7x +7x + +7x +7x + +16x +16x +16x +16x +16x +16x + +16x +16x +16x + +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x + +16x + +1x + | import { useDraggablePane } from "./lib/hooks/useDraggablePane";
+import { useConnection } from "./lib/hooks/useConnection";
+import {
+ ClientRequest,
+ CompatibilityCallToolResult,
+ CompatibilityCallToolResultSchema,
+ CreateMessageResult,
+ EmptyResultSchema,
+ GetPromptResultSchema,
+ ListPromptsResultSchema,
+ ListResourcesResultSchema,
+ ListResourceTemplatesResultSchema,
+ ReadResourceResultSchema,
+ ListToolsResultSchema,
+ Resource,
+ ResourceTemplate,
+ Root,
+ ServerNotification,
+ Tool
+} from "@modelcontextprotocol/sdk/types.js";
+import { useEffect, useRef, useState } from "react";
+
+import { StdErrNotification } from "./lib/notificationTypes";
+
+import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import {
+ Bell,
+ Files,
+ FolderTree,
+ Hammer,
+ Hash,
+ MessageSquare,
+} from "lucide-react";
+
+import { z } from "zod";
+import "./App.css";
+import ConsoleTab from "./components/ConsoleTab";
+import HistoryAndNotifications from "./components/History";
+import PingTab from "./components/PingTab";
+import PromptsTab, { Prompt } from "./components/PromptsTab";
+import ResourcesTab from "./components/ResourcesTab";
+import RootsTab from "./components/RootsTab";
+import SamplingTab, { PendingRequest } from "./components/SamplingTab";
+import Sidebar from "./components/Sidebar";
+import ToolsTab from "./components/ToolsTab";
+
+const params = new URLSearchParams(window.location.search);
+const PROXY_PORT = params.get("proxyPort") ?? "3000";
+const PROXY_SERVER_URL = `http://localhost:${PROXY_PORT}`;
+
+const App = () => {
+ const [resources, setResources] = useState<Resource[]>([]);
+ const [resourceTemplates, setResourceTemplates] = useState<
+ ResourceTemplate[]
+ >([]);
+ const [resourceContent, setResourceContent] = useState<string>("");
+ const [prompts, setPrompts] = useState<Prompt[]>([]);
+ const [promptContent, setPromptContent] = useState<string>("");
+ const [tools, setTools] = useState<Tool[]>([]);
+ const [toolResult, setToolResult] =
+ useState<CompatibilityCallToolResult | null>(null);
+ const [errors, setErrors] = useState<Record<string, string | null>>({
+ resources: null,
+ prompts: null,
+ tools: null,
+ });
+ const [command, setCommand] = useState<string>(() => {
+ return localStorage.getItem("lastCommand") || "mcp-server-everything";
+ });
+ const [args, setArgs] = useState<string>(() => {
+ return localStorage.getItem("lastArgs") || "";
+ });
+
+ const [sseUrl, setSseUrl] = useState<string>("http://localhost:3001/sse");
+ const [transportType, setTransportType] = useState<"stdio" | "sse">("stdio");
+ const [notifications, setNotifications] = useState<ServerNotification[]>([]);
+ const [stdErrNotifications, setStdErrNotifications] = useState<
+ StdErrNotification[]
+ >([]);
+ const [roots, setRoots] = useState<Root[]>([]);
+ const [env, setEnv] = useState<Record<string, string>>({});
+
+ const [pendingSampleRequests, setPendingSampleRequests] = useState<
+ Array<
+ PendingRequest & {
+ resolve: (result: CreateMessageResult) => void;
+ reject: (error: Error) => void;
+ }
+ >
+ >([]);
+ const nextRequestId = useRef(0);
+ const rootsRef = useRef<Root[]>([]);
+
+ const handleApproveSampling = (id: number, result: CreateMessageResult) => {
+ setPendingSampleRequests((prev) => {
+ const request = prev.find((r) => r.id === id);
+ request?.resolve(result);
+ return prev.filter((r) => r.id !== id);
+ });
+ };
+
+ const handleRejectSampling = (id: number) => {
+ setPendingSampleRequests((prev) => {
+ const request = prev.find((r) => r.id === id);
+ request?.reject(new Error("Sampling request rejected"));
+ return prev.filter((r) => r.id !== id);
+ });
+ };
+
+ const [selectedResource, setSelectedResource] = useState<Resource | null>(
+ null,
+ );
+ const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
+ const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
+ const [nextResourceCursor, setNextResourceCursor] = useState<
+ string | undefined
+ >();
+ const [nextResourceTemplateCursor, setNextResourceTemplateCursor] = useState<
+ string | undefined
+ >();
+ const [nextPromptCursor, setNextPromptCursor] = useState<
+ string | undefined
+ >();
+ const [nextToolCursor, setNextToolCursor] = useState<string | undefined>();
+ const progressTokenRef = useRef(0);
+
+ const {
+ height: historyPaneHeight,
+ handleDragStart
+ } = useDraggablePane(300);
+
+ const {
+ connectionStatus,
+ serverCapabilities,
+ mcpClient,
+ requestHistory,
+ makeRequest: makeConnectionRequest,
+ sendNotification,
+ connect: connectMcpServer
+ } = useConnection({
+ transportType,
+ command,
+ args,
+ sseUrl,
+ env,
+ proxyServerUrl: PROXY_SERVER_URL,
+ onNotification: (notification) => {
+ setNotifications(prev => [...prev, notification as ServerNotification]);
+ },
+ onStdErrNotification: (notification) => {
+ setStdErrNotifications(prev => [...prev, notification as StdErrNotification]);
+ },
+ onPendingRequest: (request, resolve, reject) => {
+ setPendingSampleRequests(prev => [
+ ...prev,
+ { id: nextRequestId.current++, request, resolve, reject }
+ ]);
+ },
+ getRoots: () => rootsRef.current
+ });
+
+ const makeRequest = async <T extends z.ZodType>(
+ request: ClientRequest,
+ schema: T,
+ tabKey?: keyof typeof errors,
+ ) => {
+ try {
+ const response = await makeConnectionRequest(request, schema);
+ if (tabKey !== undefined) {
+ clearError(tabKey);
+ }
+ return response;
+ } catch (e) {
+ const errorString = (e as Error).message ?? String(e);
+ if (tabKey !== undefined) {
+ setErrors((prev) => ({
+ ...prev,
+ [tabKey]: errorString,
+ }));
+ }
+ throw e;
+ }
+ };
+
+ useEffect(() => {
+ localStorage.setItem("lastCommand", command);
+ }, [command]);
+
+ useEffect(() => {
+ localStorage.setItem("lastArgs", args);
+ }, [args]);
+
+ useEffect(() => {
+ fetch(`${PROXY_SERVER_URL}/config`)
+ .then((response) => response.json())
+ .then((data) => {
+ setEnv(data.defaultEnvironment);
+ if (data.defaultCommand) {
+ setCommand(data.defaultCommand);
+ }
+ if (data.defaultArgs) {
+ setArgs(data.defaultArgs);
+ }
+ })
+ .catch((error) =>
+ console.error("Error fetching default environment:", error),
+ );
+ }, []);
+
+ useEffect(() => {
+ rootsRef.current = roots;
+ }, [roots]);
+
+ useEffect(() => {
+ if (!window.location.hash) {
+ window.location.hash = "resources";
+ }
+ }, []);
+
+ const clearError = (tabKey: keyof typeof errors) => {
+ setErrors((prev) => ({ ...prev, [tabKey]: null }));
+ };
+
+ const listResources = async () => {
+ const response = await makeRequest(
+ {
+ method: "resources/list" as const,
+ params: nextResourceCursor ? { cursor: nextResourceCursor } : {},
+ },
+ ListResourcesResultSchema,
+ "resources",
+ );
+ setResources(resources.concat(response.resources ?? []));
+ setNextResourceCursor(response.nextCursor);
+ };
+
+ const listResourceTemplates = async () => {
+ const response = await makeRequest(
+ {
+ method: "resources/templates/list" as const,
+ params: nextResourceTemplateCursor
+ ? { cursor: nextResourceTemplateCursor }
+ : {},
+ },
+ ListResourceTemplatesResultSchema,
+ "resources",
+ );
+ setResourceTemplates(
+ resourceTemplates.concat(response.resourceTemplates ?? []),
+ );
+ setNextResourceTemplateCursor(response.nextCursor);
+ };
+
+ const readResource = async (uri: string) => {
+ const response = await makeRequest(
+ {
+ method: "resources/read" as const,
+ params: { uri },
+ },
+ ReadResourceResultSchema,
+ "resources",
+ );
+ setResourceContent(JSON.stringify(response, null, 2));
+ };
+
+ const listPrompts = async () => {
+ const response = await makeRequest(
+ {
+ method: "prompts/list" as const,
+ params: nextPromptCursor ? { cursor: nextPromptCursor } : {},
+ },
+ ListPromptsResultSchema,
+ "prompts",
+ );
+ setPrompts(response.prompts);
+ setNextPromptCursor(response.nextCursor);
+ };
+
+ const getPrompt = async (name: string, args: Record<string, string> = {}) => {
+ const response = await makeRequest(
+ {
+ method: "prompts/get" as const,
+ params: { name, arguments: args },
+ },
+ GetPromptResultSchema,
+ "prompts",
+ );
+ setPromptContent(JSON.stringify(response, null, 2));
+ };
+
+ const listTools = async () => {
+ const response = await makeRequest(
+ {
+ method: "tools/list" as const,
+ params: nextToolCursor ? { cursor: nextToolCursor } : {},
+ },
+ ListToolsResultSchema,
+ "tools",
+ );
+ setTools(response.tools);
+ setNextToolCursor(response.nextCursor);
+ };
+
+ const callTool = async (name: string, params: Record<string, unknown>) => {
+ const response = await makeRequest(
+ {
+ method: "tools/call" as const,
+ params: {
+ name,
+ arguments: params,
+ _meta: {
+ progressToken: progressTokenRef.current++,
+ },
+ },
+ },
+ CompatibilityCallToolResultSchema,
+ "tools",
+ );
+ setToolResult(response);
+ };
+
+ const handleRootsChange = async () => {
+ await sendNotification({ method: "notifications/roots/list_changed" });
+ };
+
+ return (
+ <div className="flex h-screen bg-background">
+ <Sidebar
+ connectionStatus={connectionStatus}
+ transportType={transportType}
+ setTransportType={setTransportType}
+ command={command}
+ setCommand={setCommand}
+ args={args}
+ setArgs={setArgs}
+ sseUrl={sseUrl}
+ setSseUrl={setSseUrl}
+ env={env}
+ setEnv={setEnv}
+ onConnect={connectMcpServer}
+ stdErrNotifications={stdErrNotifications}
+ />
+ <div className="flex-1 flex flex-col overflow-hidden">
+ <div className="flex-1 overflow-auto">
+ {mcpClient ? (
+ <Tabs
+ defaultValue={
+ Object.keys(serverCapabilities ?? {}).includes(window.location.hash.slice(1)) ?
+ window.location.hash.slice(1) :
+ serverCapabilities?.resources ? "resources" :
+ serverCapabilities?.prompts ? "prompts" :
+ serverCapabilities?.tools ? "tools" :
+ "ping"
+ }
+ className="w-full p-4"
+ onValueChange={(value) => (window.location.hash = value)}
+ >
+ <TabsList className="mb-4 p-0">
+ <TabsTrigger value="resources" disabled={!serverCapabilities?.resources}>
+ <Files className="w-4 h-4 mr-2" />
+ Resources
+ </TabsTrigger>
+ <TabsTrigger value="prompts" disabled={!serverCapabilities?.prompts}>
+ <MessageSquare className="w-4 h-4 mr-2" />
+ Prompts
+ </TabsTrigger>
+ <TabsTrigger value="tools" disabled={!serverCapabilities?.tools}>
+ <Hammer className="w-4 h-4 mr-2" />
+ Tools
+ </TabsTrigger>
+ <TabsTrigger value="ping">
+ <Bell className="w-4 h-4 mr-2" />
+ Ping
+ </TabsTrigger>
+ <TabsTrigger value="sampling" className="relative">
+ <Hash className="w-4 h-4 mr-2" />
+ Sampling
+ {pendingSampleRequests.length > 0 && (
+ <span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">
+ {pendingSampleRequests.length}
+ </span>
+ )}
+ </TabsTrigger>
+ <TabsTrigger value="roots">
+ <FolderTree className="w-4 h-4 mr-2" />
+ Roots
+ </TabsTrigger>
+ </TabsList>
+
+ <div className="w-full">
+ {!serverCapabilities?.resources && !serverCapabilities?.prompts && !serverCapabilities?.tools ? (
+ <div className="flex items-center justify-center p-4">
+ <p className="text-lg text-gray-500">
+ The connected server does not support any MCP capabilities
+ </p>
+ </div>
+ ) : (
+ <>
+ <ResourcesTab
+ resources={resources}
+ resourceTemplates={resourceTemplates}
+ listResources={() => {
+ clearError("resources");
+ listResources();
+ }}
+ clearResources={() => {
+ setResources([]);
+ setNextResourceCursor(undefined);
+ }}
+ listResourceTemplates={() => {
+ clearError("resources");
+ listResourceTemplates();
+ }}
+ clearResourceTemplates={() => {
+ setResourceTemplates([]);
+ setNextResourceTemplateCursor(undefined);
+ }}
+ readResource={(uri) => {
+ clearError("resources");
+ readResource(uri);
+ }}
+ selectedResource={selectedResource}
+ setSelectedResource={(resource) => {
+ clearError("resources");
+ setSelectedResource(resource);
+ }}
+ resourceContent={resourceContent}
+ nextCursor={nextResourceCursor}
+ nextTemplateCursor={nextResourceTemplateCursor}
+ error={errors.resources}
+ />
+ <PromptsTab
+ prompts={prompts}
+ listPrompts={() => {
+ clearError("prompts");
+ listPrompts();
+ }}
+ clearPrompts={() => {
+ setPrompts([]);
+ setNextPromptCursor(undefined);
+ }}
+ getPrompt={(name, args) => {
+ clearError("prompts");
+ getPrompt(name, args);
+ }}
+ selectedPrompt={selectedPrompt}
+ setSelectedPrompt={(prompt) => {
+ clearError("prompts");
+ setSelectedPrompt(prompt);
+ }}
+ promptContent={promptContent}
+ nextCursor={nextPromptCursor}
+ error={errors.prompts}
+ />
+ <ToolsTab
+ tools={tools}
+ listTools={() => {
+ clearError("tools");
+ listTools();
+ }}
+ clearTools={() => {
+ setTools([]);
+ setNextToolCursor(undefined);
+ }}
+ callTool={(name, params) => {
+ clearError("tools");
+ callTool(name, params);
+ }}
+ selectedTool={selectedTool}
+ setSelectedTool={(tool) => {
+ clearError("tools");
+ setSelectedTool(tool);
+ setToolResult(null);
+ }}
+ toolResult={toolResult}
+ nextCursor={nextToolCursor}
+ error={errors.tools}
+ />
+ <ConsoleTab />
+ <PingTab
+ onPingClick={() => {
+ void makeRequest(
+ {
+ method: "ping" as const,
+ },
+ EmptyResultSchema,
+ );
+ }}
+ />
+ <SamplingTab
+ pendingRequests={pendingSampleRequests}
+ onApprove={handleApproveSampling}
+ onReject={handleRejectSampling}
+ />
+ <RootsTab
+ roots={roots}
+ setRoots={setRoots}
+ onRootsChange={handleRootsChange}
+ />
+ </>
+ )}
+ </div>
+ </Tabs>
+ ) : (
+ <div className="flex items-center justify-center h-full">
+ <p className="text-lg text-gray-500">
+ Connect to an MCP server to start inspecting
+ </p>
+ </div>
+ )}
+ </div>
+ <div
+ className="relative border-t border-border"
+ style={{
+ height: `${historyPaneHeight}px`,
+ }}
+ >
+ <div
+ className="absolute w-full h-4 -top-2 cursor-row-resize flex items-center justify-center hover:bg-accent/50"
+ onMouseDown={handleDragStart}
+ >
+ <div className="w-8 h-1 rounded-full bg-border" />
+ </div>
+ <div className="h-full overflow-auto">
+ <HistoryAndNotifications
+ requestHistory={requestHistory}
+ serverNotifications={notifications}
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+};
+
+export default App;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 | 1x + +1x +4x +4x +4x + +4x +4x + + +1x + | import { TabsContent } from "@/components/ui/tabs";
+
+const ConsoleTab = () => (
+ <TabsContent value="console" className="h-96">
+ <div className="bg-gray-900 text-gray-100 p-4 rounded-lg h-full font-mono text-sm overflow-auto">
+ <div className="opacity-50">Welcome to MCP Client Console</div>
+ {/* Console output would go here */}
+ </div>
+ </TabsContent>
+);
+
+export default ConsoleTab;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 | 1x +1x +1x + +1x +21x +21x +21x + + +21x +21x + +21x +21x + +21x + +21x +1x +1x + +21x +1x +1x + +21x + + + +21x +21x +21x +21x +21x +18x + +3x +3x +3x +3x +3x +6x +6x +6x + +6x +6x +6x +1x + + +6x +6x +6x +6x +6x +6x +1x +5x +6x +6x +6x +1x +1x +1x +1x + +1x +1x +1x +1x + +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x + +1x +1x +1x +1x + +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x + +1x + +6x +3x +3x + +21x +21x +21x +21x +19x + +2x +2x +2x +2x +2x +4x +4x +4x + +4x +4x +4x + +4x +4x +4x +4x +4x +4x +4x +1x +1x +1x + +1x +1x +1x + + +1x + +1x +1x +1x +1x +1x +1x +1x + +4x +2x +2x + +21x +21x + +21x + +1x + | import { ServerNotification } from "@modelcontextprotocol/sdk/types.js";
+import { Copy } from "lucide-react";
+import { useState } from "react";
+
+const HistoryAndNotifications = ({
+ requestHistory,
+ serverNotifications,
+}: {
+ requestHistory: Array<{ request: string; response?: string }>;
+ serverNotifications: ServerNotification[];
+}) => {
+ const [expandedRequests, setExpandedRequests] = useState<{
+ [key: number]: boolean;
+ }>({});
+ const [expandedNotifications, setExpandedNotifications] = useState<{
+ [key: number]: boolean;
+ }>({});
+
+ const toggleRequestExpansion = (index: number) => {
+ setExpandedRequests((prev) => ({ ...prev, [index]: !prev[index] }));
+ };
+
+ const toggleNotificationExpansion = (index: number) => {
+ setExpandedNotifications((prev) => ({ ...prev, [index]: !prev[index] }));
+ };
+
+ const copyToClipboard = (text: string) => {
+ navigator.clipboard.writeText(text);
+ };
+
+ return (
+ <div className="bg-card overflow-hidden flex h-full">
+ <div className="flex-1 overflow-y-auto p-4 border-r">
+ <h2 className="text-lg font-semibold mb-4">History</h2>
+ {requestHistory.length === 0 ? (
+ <p className="text-sm text-gray-500 italic">No history yet</p>
+ ) : (
+ <ul className="space-y-3">
+ {requestHistory
+ .slice()
+ .reverse()
+ .map((request, index) => (
+ <li
+ key={index}
+ className="text-sm text-foreground bg-secondary p-2 rounded"
+ >
+ <div
+ className="flex justify-between items-center cursor-pointer"
+ onClick={() =>
+ toggleRequestExpansion(requestHistory.length - 1 - index)
+ }
+ >
+ <span className="font-mono">
+ {requestHistory.length - index}.{" "}
+ {JSON.parse(request.request).method}
+ </span>
+ <span>
+ {expandedRequests[requestHistory.length - 1 - index]
+ ? "â–¼"
+ : "â–¶"}
+ </span>
+ </div>
+ {expandedRequests[requestHistory.length - 1 - index] && (
+ <>
+ <div className="mt-2">
+ <div className="flex justify-between items-center mb-1">
+ <span className="font-semibold text-blue-600">
+ Request:
+ </span>
+ <button
+ onClick={() => copyToClipboard(request.request)}
+ className="text-blue-500 hover:text-blue-700"
+ >
+ <Copy size={16} />
+ </button>
+ </div>
+ <pre className="whitespace-pre-wrap break-words bg-background p-2 rounded">
+ {JSON.stringify(JSON.parse(request.request), null, 2)}
+ </pre>
+ </div>
+ {request.response && (
+ <div className="mt-2">
+ <div className="flex justify-between items-center mb-1">
+ <span className="font-semibold text-green-600">
+ Response:
+ </span>
+ <button
+ onClick={() => copyToClipboard(request.response!)}
+ className="text-blue-500 hover:text-blue-700"
+ >
+ <Copy size={16} />
+ </button>
+ </div>
+ <pre className="whitespace-pre-wrap break-words bg-background p-2 rounded">
+ {JSON.stringify(
+ JSON.parse(request.response),
+ null,
+ 2,
+ )}
+ </pre>
+ </div>
+ )}
+ </>
+ )}
+ </li>
+ ))}
+ </ul>
+ )}
+ </div>
+ <div className="flex-1 overflow-y-auto p-4">
+ <h2 className="text-lg font-semibold mb-4">Server Notifications</h2>
+ {serverNotifications.length === 0 ? (
+ <p className="text-sm text-gray-500 italic">No notifications yet</p>
+ ) : (
+ <ul className="space-y-3">
+ {serverNotifications
+ .slice()
+ .reverse()
+ .map((notification, index) => (
+ <li
+ key={index}
+ className="text-sm text-foreground bg-secondary p-2 rounded"
+ >
+ <div
+ className="flex justify-between items-center cursor-pointer"
+ onClick={() => toggleNotificationExpansion(index)}
+ >
+ <span className="font-mono">
+ {serverNotifications.length - index}.{" "}
+ {notification.method}
+ </span>
+ <span>{expandedNotifications[index] ? "â–¼" : "â–¶"}</span>
+ </div>
+ {expandedNotifications[index] && (
+ <div className="mt-2">
+ <div className="flex justify-between items-center mb-1">
+ <span className="font-semibold text-purple-600">
+ Details:
+ </span>
+ <button
+ onClick={() =>
+ copyToClipboard(JSON.stringify(notification))
+ }
+ className="text-blue-500 hover:text-blue-700"
+ >
+ <Copy size={16} />
+ </button>
+ </div>
+ <pre className="whitespace-pre-wrap break-words bg-background p-2 rounded">
+ {JSON.stringify(notification, null, 2)}
+ </pre>
+ </div>
+ )}
+ </li>
+ ))}
+ </ul>
+ )}
+ </div>
+ </div>
+ );
+};
+
+export default HistoryAndNotifications;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 | 1x + + + + + + + + + + + + +1x +44x +44x +44x +44x +44x +44x +44x +44x +44x +44x +44x +44x +44x +44x +44x +44x +44x +44x +44x + +44x +44x +44x +44x +44x +44x +44x +44x + +44x +44x +44x +74x +74x +74x +74x + +74x +74x +44x +44x +44x +44x + + +1x + | import { Button } from "./ui/button";
+
+type ListPaneProps<T> = {
+ items: T[];
+ listItems: () => void;
+ clearItems: () => void;
+ setSelectedItem: (item: T) => void;
+ renderItem: (item: T) => React.ReactNode;
+ title: string;
+ buttonText: string;
+ isButtonDisabled?: boolean;
+};
+
+const ListPane = <T extends object>({
+ items,
+ listItems,
+ clearItems,
+ setSelectedItem,
+ renderItem,
+ title,
+ buttonText,
+ isButtonDisabled,
+}: ListPaneProps<T>) => (
+ <div className="bg-card rounded-lg shadow">
+ <div className="p-4 border-b border-gray-200 dark:border-gray-700">
+ <h3 className="font-semibold dark:text-white">{title}</h3>
+ </div>
+ <div className="p-4">
+ <Button
+ variant="outline"
+ className="w-full mb-4"
+ onClick={listItems}
+ disabled={isButtonDisabled}
+ >
+ {buttonText}
+ </Button>
+ <Button
+ variant="outline"
+ className="w-full mb-4"
+ onClick={clearItems}
+ disabled={items.length === 0}
+ >
+ Clear
+ </Button>
+ <div className="space-y-2 overflow-y-auto max-h-96">
+ {items.map((item, index) => (
+ <div
+ key={index}
+ className="flex items-center p-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer"
+ onClick={() => setSelectedItem(item)}
+ >
+ {renderItem(item)}
+ </div>
+ ))}
+ </div>
+ </div>
+ </div>
+);
+
+export default ListPane;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 | 1x +1x + +1x +9x +9x +9x +9x +9x +9x + +9x + +9x +9x +9x +9x + +9x + +1x + | import { TabsContent } from "@/components/ui/tabs";
+import { Button } from "@/components/ui/button";
+
+const PingTab = ({ onPingClick }: { onPingClick: () => void }) => {
+ return (
+ <TabsContent value="ping" className="grid grid-cols-2 gap-4">
+ <div className="col-span-2 flex justify-center items-center">
+ <Button
+ onClick={onPingClick}
+ className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white font-bold py-6 px-12 rounded-full shadow-lg transform transition duration-300 hover:scale-110 focus:outline-none focus:ring-4 focus:ring-purple-300 animate-pulse"
+ >
+ <span className="text-3xl mr-2">🚀</span>
+ MEGA PING
+ <span className="text-3xl ml-2">💥</span>
+ </Button>
+ </div>
+ </TabsContent>
+ );
+};
+
+export default PingTab;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 | 1x +1x +1x +1x +1x +1x + +1x +1x +1x + + + + + + + + + + + +1x +10x +10x +10x +10x +10x +10x +10x +10x +10x +10x + + + + + + + + + +10x +10x + +10x +1x +1x + +10x +1x +1x +1x +1x + +10x +10x +10x +10x +10x +10x +10x + + + +10x +12x +12x +12x +12x + +10x +10x +10x +10x + +10x +10x +10x +10x +10x +10x +10x +10x +1x +1x +1x +1x +1x +9x +4x +4x +4x +4x +4x + +4x +8x +8x +8x +8x +8x +8x +8x +1x + +8x +8x +8x +8x +8x +4x + +8x + +8x +4x +4x + +4x +4x +1x +1x +1x +1x +1x + +4x + +5x +5x + +5x +5x + +10x +10x +10x + +10x + +1x + | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { TabsContent } from "@/components/ui/tabs";
+import { Textarea } from "@/components/ui/textarea";
+import { ListPromptsResult } from "@modelcontextprotocol/sdk/types.js";
+import { AlertCircle } from "lucide-react";
+import { useState } from "react";
+import ListPane from "./ListPane";
+
+export type Prompt = {
+ name: string;
+ description?: string;
+ arguments?: {
+ name: string;
+ description?: string;
+ required?: boolean;
+ }[];
+};
+
+const PromptsTab = ({
+ prompts,
+ listPrompts,
+ clearPrompts,
+ getPrompt,
+ selectedPrompt,
+ setSelectedPrompt,
+ promptContent,
+ nextCursor,
+ error,
+}: {
+ prompts: Prompt[];
+ listPrompts: () => void;
+ clearPrompts: () => void;
+ getPrompt: (name: string, args: Record<string, string>) => void;
+ selectedPrompt: Prompt | null;
+ setSelectedPrompt: (prompt: Prompt) => void;
+ promptContent: string;
+ nextCursor: ListPromptsResult["nextCursor"];
+ error: string | null;
+}) => {
+ const [promptArgs, setPromptArgs] = useState<Record<string, string>>({});
+
+ const handleInputChange = (argName: string, value: string) => {
+ setPromptArgs((prev) => ({ ...prev, [argName]: value }));
+ };
+
+ const handleGetPrompt = () => {
+ if (selectedPrompt) {
+ getPrompt(selectedPrompt.name, promptArgs);
+ }
+ };
+
+ return (
+ <TabsContent value="prompts" className="grid grid-cols-2 gap-4">
+ <ListPane
+ items={prompts}
+ listItems={listPrompts}
+ clearItems={clearPrompts}
+ setSelectedItem={(prompt) => {
+ setSelectedPrompt(prompt);
+ setPromptArgs({});
+ }}
+ renderItem={(prompt) => (
+ <>
+ <span className="flex-1">{prompt.name}</span>
+ <span className="text-sm text-gray-500">{prompt.description}</span>
+ </>
+ )}
+ title="Prompts"
+ buttonText={nextCursor ? "List More Prompts" : "List Prompts"}
+ isButtonDisabled={!nextCursor && prompts.length > 0}
+ />
+
+ <div className="bg-card rounded-lg shadow">
+ <div className="p-4 border-b border-gray-200">
+ <h3 className="font-semibold">
+ {selectedPrompt ? selectedPrompt.name : "Select a prompt"}
+ </h3>
+ </div>
+ <div className="p-4">
+ {error ? (
+ <Alert variant="destructive">
+ <AlertCircle className="h-4 w-4" />
+ <AlertTitle>Error</AlertTitle>
+ <AlertDescription>{error}</AlertDescription>
+ </Alert>
+ ) : selectedPrompt ? (
+ <div className="space-y-4">
+ {selectedPrompt.description && (
+ <p className="text-sm text-gray-600">
+ {selectedPrompt.description}
+ </p>
+ )}
+ {selectedPrompt.arguments?.map((arg) => (
+ <div key={arg.name}>
+ <Label htmlFor={arg.name}>{arg.name}</Label>
+ <Input
+ id={arg.name}
+ placeholder={`Enter ${arg.name}`}
+ value={promptArgs[arg.name] || ""}
+ onChange={(e) =>
+ handleInputChange(arg.name, e.target.value)
+ }
+ />
+ {arg.description && (
+ <p className="text-xs text-gray-500 mt-1">
+ {arg.description}
+ {arg.required && (
+ <span className="text-xs mt-1 ml-1">(Required)</span>
+ )}
+ </p>
+ )}
+ </div>
+ ))}
+ <Button onClick={handleGetPrompt} className="w-full">
+ Get Prompt
+ </Button>
+ {promptContent && (
+ <Textarea
+ value={promptContent}
+ readOnly
+ className="h-64 font-mono"
+ />
+ )}
+ </div>
+ ) : (
+ <Alert>
+ <AlertDescription>
+ Select a prompt from the list to view and use it
+ </AlertDescription>
+ </Alert>
+ )}
+ </div>
+ </div>
+ </TabsContent>
+ );
+};
+
+export default PromptsTab;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 | 1x +1x +1x +1x + + + + + + +1x +1x +1x + +1x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x + + + + + + + + + + + + + +16x +16x +16x +16x +16x +16x + +16x +1x +1x +1x +1x +1x +1x +1x +1x + +16x +1x +1x +1x +1x + +1x +1x +1x + +16x +16x +16x +16x +16x +16x +16x + + + + +16x +24x +24x +24x +24x +24x +24x +24x + +16x +16x +16x +16x + +16x +16x +16x +16x +16x +2x +2x +2x +2x +16x +24x +24x +24x +24x +24x +24x +24x + +16x +16x +16x + +16x +16x + +16x +16x +16x +16x +16x + +16x +2x +14x +4x +10x +16x +16x +2x +2x +2x +2x + +2x + +2x + +16x +16x +16x +1x +1x +1x +1x +1x +15x +2x +2x +2x +13x +4x +4x +4x +4x +4x +4x +4x +8x +8x +8x +8x +8x +8x + +8x +8x +8x +8x +8x +8x +2x +2x +2x +2x + +8x +8x +8x + +4x +4x +4x +4x +4x + +4x +4x + +9x +9x + +9x +9x + +16x +16x +16x + +16x + +1x + | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { TabsContent } from "@/components/ui/tabs";
+import {
+ ListResourcesResult,
+ Resource,
+ ResourceTemplate,
+ ListResourceTemplatesResult,
+} from "@modelcontextprotocol/sdk/types.js";
+import { AlertCircle, ChevronRight, FileText, RefreshCw } from "lucide-react";
+import ListPane from "./ListPane";
+import { useState } from "react";
+
+const ResourcesTab = ({
+ resources,
+ resourceTemplates,
+ listResources,
+ clearResources,
+ listResourceTemplates,
+ clearResourceTemplates,
+ readResource,
+ selectedResource,
+ setSelectedResource,
+ resourceContent,
+ nextCursor,
+ nextTemplateCursor,
+ error,
+}: {
+ resources: Resource[];
+ resourceTemplates: ResourceTemplate[];
+ listResources: () => void;
+ clearResources: () => void;
+ listResourceTemplates: () => void;
+ clearResourceTemplates: () => void;
+ readResource: (uri: string) => void;
+ selectedResource: Resource | null;
+ setSelectedResource: (resource: Resource | null) => void;
+ resourceContent: string;
+ nextCursor: ListResourcesResult["nextCursor"];
+ nextTemplateCursor: ListResourceTemplatesResult["nextCursor"];
+ error: string | null;
+}) => {
+ const [selectedTemplate, setSelectedTemplate] =
+ useState<ResourceTemplate | null>(null);
+ const [templateValues, setTemplateValues] = useState<Record<string, string>>(
+ {},
+ );
+
+ const fillTemplate = (
+ template: string,
+ values: Record<string, string>,
+ ): string => {
+ return template.replace(
+ /{([^}]+)}/g,
+ (_, key) => values[key] || `{${key}}`,
+ );
+ };
+
+ const handleReadTemplateResource = () => {
+ if (selectedTemplate) {
+ const uri = fillTemplate(selectedTemplate.uriTemplate, templateValues);
+ readResource(uri);
+ setSelectedTemplate(null);
+ // We don't have the full Resource object here, so we create a partial one
+ setSelectedResource({ uri, name: uri } as Resource);
+ }
+ };
+
+ return (
+ <TabsContent value="resources" className="grid grid-cols-3 gap-4">
+ <ListPane
+ items={resources}
+ listItems={listResources}
+ clearItems={clearResources}
+ setSelectedItem={(resource) => {
+ setSelectedResource(resource);
+ readResource(resource.uri);
+ setSelectedTemplate(null);
+ }}
+ renderItem={(resource) => (
+ <div className="flex items-center w-full">
+ <FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
+ <span className="flex-1 truncate" title={resource.uri.toString()}>
+ {resource.name}
+ </span>
+ <ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400" />
+ </div>
+ )}
+ title="Resources"
+ buttonText={nextCursor ? "List More Resources" : "List Resources"}
+ isButtonDisabled={!nextCursor && resources.length > 0}
+ />
+
+ <ListPane
+ items={resourceTemplates}
+ listItems={listResourceTemplates}
+ clearItems={clearResourceTemplates}
+ setSelectedItem={(template) => {
+ setSelectedTemplate(template);
+ setSelectedResource(null);
+ setTemplateValues({});
+ }}
+ renderItem={(template) => (
+ <div className="flex items-center w-full">
+ <FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
+ <span className="flex-1 truncate" title={template.uriTemplate}>
+ {template.name}
+ </span>
+ <ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400" />
+ </div>
+ )}
+ title="Resource Templates"
+ buttonText={
+ nextTemplateCursor ? "List More Templates" : "List Templates"
+ }
+ isButtonDisabled={!nextTemplateCursor && resourceTemplates.length > 0}
+ />
+
+ <div className="bg-card rounded-lg shadow">
+ <div className="p-4 border-b border-gray-200 flex justify-between items-center">
+ <h3
+ className="font-semibold truncate"
+ title={selectedResource?.name || selectedTemplate?.name}
+ >
+ {selectedResource
+ ? selectedResource.name
+ : selectedTemplate
+ ? selectedTemplate.name
+ : "Select a resource or template"}
+ </h3>
+ {selectedResource && (
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => readResource(selectedResource.uri)}
+ >
+ <RefreshCw className="w-4 h-4 mr-2" />
+ Refresh
+ </Button>
+ )}
+ </div>
+ <div className="p-4">
+ {error ? (
+ <Alert variant="destructive">
+ <AlertCircle className="h-4 w-4" />
+ <AlertTitle>Error</AlertTitle>
+ <AlertDescription>{error}</AlertDescription>
+ </Alert>
+ ) : selectedResource ? (
+ <pre className="bg-gray-50 dark:bg-gray-800 p-4 rounded text-sm overflow-auto max-h-96 whitespace-pre-wrap break-words text-gray-900 dark:text-gray-100">
+ {resourceContent}
+ </pre>
+ ) : selectedTemplate ? (
+ <div className="space-y-4">
+ <p className="text-sm text-gray-600">
+ {selectedTemplate.description}
+ </p>
+ {selectedTemplate.uriTemplate
+ .match(/{([^}]+)}/g)
+ ?.map((param) => {
+ const key = param.slice(1, -1);
+ return (
+ <div key={key}>
+ <label
+ htmlFor={key}
+ className="block text-sm font-medium text-gray-700"
+ >
+ {key}
+ </label>
+ <Input
+ id={key}
+ value={templateValues[key] || ""}
+ onChange={(e) =>
+ setTemplateValues({
+ ...templateValues,
+ [key]: e.target.value,
+ })
+ }
+ className="mt-1"
+ />
+ </div>
+ );
+ })}
+ <Button
+ onClick={handleReadTemplateResource}
+ disabled={Object.keys(templateValues).length === 0}
+ >
+ Read Resource
+ </Button>
+ </div>
+ ) : (
+ <Alert>
+ <AlertDescription>
+ Select a resource or template from the list to view its contents
+ </AlertDescription>
+ </Alert>
+ )}
+ </div>
+ </div>
+ </TabsContent>
+ );
+};
+
+export default ResourcesTab;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 | 1x +1x +1x +1x + +1x + +1x +9x +9x +9x +9x + + + +9x +9x +1x +1x + +9x +1x +1x + +9x +1x +1x +2x +1x +1x +1x + +9x +1x +1x + +9x +9x +9x +9x + +9x +9x + +9x +10x +10x +10x +10x +10x +10x +10x +10x +10x +10x +10x +10x + +10x +10x +10x +9x + +9x +9x +9x + +9x +9x +9x + +9x +9x +9x + +9x + +1x + | import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { TabsContent } from "@/components/ui/tabs";
+import { Root } from "@modelcontextprotocol/sdk/types.js";
+import { Plus, Minus, Save } from "lucide-react";
+
+const RootsTab = ({
+ roots,
+ setRoots,
+ onRootsChange,
+}: {
+ roots: Root[];
+ setRoots: React.Dispatch<React.SetStateAction<Root[]>>;
+ onRootsChange: () => void;
+}) => {
+ const addRoot = () => {
+ setRoots((currentRoots) => [...currentRoots, { uri: "file://", name: "" }]);
+ };
+
+ const removeRoot = (index: number) => {
+ setRoots((currentRoots) => currentRoots.filter((_, i) => i !== index));
+ };
+
+ const updateRoot = (index: number, field: keyof Root, value: string) => {
+ setRoots((currentRoots) =>
+ currentRoots.map((root, i) =>
+ i === index ? { ...root, [field]: value } : root,
+ ),
+ );
+ };
+
+ const handleSave = () => {
+ onRootsChange();
+ };
+
+ return (
+ <TabsContent value="roots" className="space-y-4">
+ <Alert>
+ <AlertDescription>
+ Configure the root directories that the server can access
+ </AlertDescription>
+ </Alert>
+
+ {roots.map((root, index) => (
+ <div key={index} className="flex gap-2 items-center">
+ <Input
+ placeholder="file:// URI"
+ value={root.uri}
+ onChange={(e) => updateRoot(index, "uri", e.target.value)}
+ className="flex-1"
+ />
+ <Button
+ variant="destructive"
+ size="sm"
+ onClick={() => removeRoot(index)}
+ aria-label="Remove root"
+ >
+ <Minus className="h-4 w-4" />
+ </Button>
+ </div>
+ ))}
+
+ <div className="flex gap-2">
+ <Button variant="outline" onClick={addRoot}>
+ <Plus className="h-4 w-4 mr-2" />
+ Add Root
+ </Button>
+ <Button onClick={handleSave}>
+ <Save className="h-4 w-4 mr-2" />
+ Save Changes
+ </Button>
+ </div>
+ </TabsContent>
+ );
+};
+
+export default RootsTab;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 | 1x +1x +1x + + + + + + + + + + + + + + + + +1x +10x + +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x + +10x +10x +10x +10x + + +10x +10x +10x +10x +10x +10x +10x +10x +10x +10x +10x +10x + +10x +10x +10x +10x +10x +5x + +10x +10x + +10x + +1x + | import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Button } from "@/components/ui/button";
+import { TabsContent } from "@/components/ui/tabs";
+import {
+ CreateMessageRequest,
+ CreateMessageResult,
+} from "@modelcontextprotocol/sdk/types.js";
+
+export type PendingRequest = {
+ id: number;
+ request: CreateMessageRequest;
+};
+
+export type Props = {
+ pendingRequests: PendingRequest[];
+ onApprove: (id: number, result: CreateMessageResult) => void;
+ onReject: (id: number) => void;
+};
+
+const SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => {
+ const handleApprove = (id: number) => {
+ // For now, just return a stub response
+ onApprove(id, {
+ model: "stub-model",
+ stopReason: "endTurn",
+ role: "assistant",
+ content: {
+ type: "text",
+ text: "This is a stub response.",
+ },
+ });
+ };
+
+ return (
+ <TabsContent value="sampling" className="h-96">
+ <Alert>
+ <AlertDescription>
+ When the server requests LLM sampling, requests will appear here for
+ approval.
+ </AlertDescription>
+ </Alert>
+ <div className="mt-4 space-y-4">
+ <h3 className="text-lg font-semibold">Recent Requests</h3>
+ {pendingRequests.map((request) => (
+ <div key={request.id} className="p-4 border rounded-lg space-y-4">
+ <pre className="bg-gray-50 p-2 rounded">
+ {JSON.stringify(request.request, null, 2)}
+ </pre>
+ <div className="flex space-x-2">
+ <Button onClick={() => handleApprove(request.id)}>Approve</Button>
+ <Button variant="outline" onClick={() => onReject(request.id)}>
+ Reject
+ </Button>
+ </div>
+ </div>
+ ))}
+ {pendingRequests.length === 0 && (
+ <p className="text-gray-500">No pending requests</p>
+ )}
+ </div>
+ </TabsContent>
+ );
+};
+
+export default SamplingTab;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 | 1x +1x +1x +1x + + + + + + +1x + + +1x +1x + + + + + + + + + + + + + + + + + +1x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x + +16x +16x +16x +16x +16x +16x +16x +16x +16x + +16x +16x +16x +16x +16x +16x +16x + + + +16x +16x +16x +16x +16x +16x +16x +16x +16x + +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x + + + + + + + + + + + +16x +16x +16x +16x +16x +16x + +16x + + +16x +16x + +16x +16x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +16x + + +16x +16x +16x + +16x + +16x +16x +16x +16x +9x +7x + +7x +16x +16x +16x +16x +9x +7x + +7x +16x +16x +16x + + + + + + + + + + + + + + + + + + +16x +16x +16x +16x +16x +16x +16x +16x + + + +16x +16x +16x +16x +16x +16x +16x +16x +16x + +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x +16x + +16x + +1x + | import { useState } from "react";
+import { Play, ChevronDown, ChevronRight, CircleHelp, Bug, Github } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { StdErrNotification } from "@/lib/notificationTypes";
+
+import useTheme from "../lib/useTheme";
+import { version } from "../../../package.json";
+
+interface SidebarProps {
+ connectionStatus: "disconnected" | "connected" | "error";
+ transportType: "stdio" | "sse";
+ setTransportType: (type: "stdio" | "sse") => void;
+ command: string;
+ setCommand: (command: string) => void;
+ args: string;
+ setArgs: (args: string) => void;
+ sseUrl: string;
+ setSseUrl: (url: string) => void;
+ env: Record<string, string>;
+ setEnv: (env: Record<string, string>) => void;
+ onConnect: () => void;
+ stdErrNotifications: StdErrNotification[];
+}
+
+const Sidebar = ({
+ connectionStatus,
+ transportType,
+ setTransportType,
+ command,
+ setCommand,
+ args,
+ setArgs,
+ sseUrl,
+ setSseUrl,
+ env,
+ setEnv,
+ onConnect,
+ stdErrNotifications,
+}: SidebarProps) => {
+ const [theme, setTheme] = useTheme();
+ const [showEnvVars, setShowEnvVars] = useState(false);
+
+ return (
+ <div className="w-80 bg-card border-r border-border flex flex-col h-full">
+ <div className="flex items-center justify-between p-4 border-b border-gray-200">
+ <div className="flex items-center">
+ <h1 className="ml-2 text-lg font-semibold">
+ MCP Inspector v{version}
+ </h1>
+ </div>
+ </div>
+
+ <div className="p-4 flex-1 overflow-auto">
+ <div className="space-y-4">
+ <div className="space-y-2">
+ <label className="text-sm font-medium">Transport Type</label>
+ <Select
+ value={transportType}
+ onValueChange={(value: "stdio" | "sse") =>
+ setTransportType(value)
+ }
+ >
+ <SelectTrigger>
+ <SelectValue placeholder="Select transport type" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="stdio">STDIO</SelectItem>
+ <SelectItem value="sse">SSE</SelectItem>
+ </SelectContent>
+ </Select>
+ </div>
+
+ {transportType === "stdio" ? (
+ <>
+ <div className="space-y-2">
+ <label className="text-sm font-medium">Command</label>
+ <Input
+ placeholder="Command"
+ value={command}
+ onChange={(e) => setCommand(e.target.value)}
+ className="font-mono"
+ />
+ </div>
+ <div className="space-y-2">
+ <label className="text-sm font-medium">Arguments</label>
+ <Input
+ placeholder="Arguments (space-separated)"
+ value={args}
+ onChange={(e) => setArgs(e.target.value)}
+ className="font-mono"
+ />
+ </div>
+ </>
+ ) : (
+ <div className="space-y-2">
+ <label className="text-sm font-medium">URL</label>
+ <Input
+ placeholder="URL"
+ value={sseUrl}
+ onChange={(e) => setSseUrl(e.target.value)}
+ className="font-mono"
+ />
+ </div>
+ )}
+ {transportType === "stdio" && (
+ <div className="space-y-2">
+ <Button
+ variant="outline"
+ onClick={() => setShowEnvVars(!showEnvVars)}
+ className="flex items-center w-full"
+ >
+ {showEnvVars ? (
+ <ChevronDown className="w-4 h-4 mr-2" />
+ ) : (
+ <ChevronRight className="w-4 h-4 mr-2" />
+ )}
+ Environment Variables
+ </Button>
+ {showEnvVars && (
+ <div className="space-y-2">
+ {Object.entries(env).map(([key, value], idx) => (
+ <div key={idx} className="grid grid-cols-[1fr,auto] gap-2">
+ <div className="space-y-1">
+ <Input
+ placeholder="Key"
+ value={key}
+ onChange={(e) => {
+ const newEnv = { ...env };
+ delete newEnv[key];
+ newEnv[e.target.value] = value;
+ setEnv(newEnv);
+ }}
+ className="font-mono"
+ />
+ <Input
+ placeholder="Value"
+ value={value}
+ onChange={(e) => {
+ const newEnv = { ...env };
+ newEnv[key] = e.target.value;
+ setEnv(newEnv);
+ }}
+ className="font-mono"
+ />
+ </div>
+ <Button
+ variant="destructive"
+ onClick={() => {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { [key]: removed, ...rest } = env;
+ setEnv(rest);
+ }}
+ >
+ Remove
+ </Button>
+ </div>
+ ))}
+ <Button
+ variant="outline"
+ onClick={() => {
+ const newEnv = { ...env };
+ newEnv[""] = "";
+ setEnv(newEnv);
+ }}
+ >
+ Add Environment Variable
+ </Button>
+ </div>
+ )}
+ </div>
+ )}
+
+ <div className="space-y-2">
+ <Button className="w-full" onClick={onConnect}>
+ <Play className="w-4 h-4 mr-2" />
+ Connect
+ </Button>
+
+ <div className="flex items-center justify-center space-x-2 mb-4">
+ <div
+ className={`w-2 h-2 rounded-full ${
+ connectionStatus === "connected"
+ ? "bg-green-500"
+ : connectionStatus === "error"
+ ? "bg-red-500"
+ : "bg-gray-500"
+ }`}
+ />
+ <span className="text-sm text-gray-600">
+ {connectionStatus === "connected"
+ ? "Connected"
+ : connectionStatus === "error"
+ ? "Connection Error"
+ : "Disconnected"}
+ </span>
+ </div>
+ {stdErrNotifications.length > 0 && (
+ <>
+ <div className="mt-4 border-t border-gray-200 pt-4">
+ <h3 className="text-sm font-medium">
+ Error output from MCP server
+ </h3>
+ <div className="mt-2 max-h-80 overflow-y-auto">
+ {stdErrNotifications.map((notification, index) => (
+ <div
+ key={index}
+ className="text-sm text-red-500 font-mono py-2 border-b border-gray-200 last:border-b-0"
+ >
+ {notification.params.content}
+ </div>
+ ))}
+ </div>
+ </div>
+ </>
+ )}
+ </div>
+ </div>
+ </div>
+ <div className="p-4 border-t">
+ <div className="flex items-center justify-between">
+ <Select
+ value={theme}
+ onValueChange={(value: string) =>
+ setTheme(value as "system" | "light" | "dark")
+ }
+ >
+ <SelectTrigger className="w-[100px]" id="theme-select">
+ <SelectValue />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="system">System</SelectItem>
+ <SelectItem value="light">Light</SelectItem>
+ <SelectItem value="dark">Dark</SelectItem>
+ </SelectContent>
+ </Select>
+
+ <div className="flex items-center space-x-2">
+ <a href="https://modelcontextprotocol.io/docs/tools/inspector" target="_blank" rel="noopener noreferrer">
+ <Button variant="ghost" title="Inspector Documentation">
+ <CircleHelp className="w-4 h-4 text-gray-800" />
+ </Button>
+ </a>
+ <a href="https://modelcontextprotocol.io/docs/tools/debugging" target="_blank" rel="noopener noreferrer">
+ <Button variant="ghost" title="Debugging Guide">
+ <Bug className="w-4 h-4 text-gray-800" />
+ </Button>
+ </a>
+ <a href="https://github.com/modelcontextprotocol/inspector" target="_blank" rel="noopener noreferrer">
+ <Button variant="ghost" title="Report bugs or contribute on GitHub">
+ <Github className="w-4 h-4 text-gray-800" />
+ </Button>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+};
+
+export default Sidebar;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 | 1x +1x +1x +1x +1x +1x + + + + +1x +1x +1x +1x + + + +1x +6x +6x +6x +6x +6x +6x +6x +6x +6x +6x + + + + + + + + + +6x +6x +6x +2x +6x + +6x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +6x +6x +6x +6x +6x +6x + + + +6x +6x + + + + + + + +6x +6x +6x +6x + +6x +6x +6x +6x +6x +6x +6x +6x + + + + + +6x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +6x +6x + +6x +6x + +6x +6x +6x + +6x + +1x + | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { TabsContent } from "@/components/ui/tabs";
+import { Textarea } from "@/components/ui/textarea";
+import {
+ ListToolsResult,
+ Tool,
+ CallToolResultSchema,
+} from "@modelcontextprotocol/sdk/types.js";
+import { AlertCircle, Send } from "lucide-react";
+import { useEffect, useState } from "react";
+import ListPane from "./ListPane";
+
+import { CompatibilityCallToolResult } from "@modelcontextprotocol/sdk/types.js";
+
+const ToolsTab = ({
+ tools,
+ listTools,
+ clearTools,
+ callTool,
+ selectedTool,
+ setSelectedTool,
+ toolResult,
+ nextCursor,
+ error,
+}: {
+ tools: Tool[];
+ listTools: () => void;
+ clearTools: () => void;
+ callTool: (name: string, params: Record<string, unknown>) => void;
+ selectedTool: Tool | null;
+ setSelectedTool: (tool: Tool | null) => void;
+ toolResult: CompatibilityCallToolResult | null;
+ nextCursor: ListToolsResult["nextCursor"];
+ error: string | null;
+}) => {
+ const [params, setParams] = useState<Record<string, unknown>>({});
+ useEffect(() => {
+ setParams({});
+ }, [selectedTool]);
+
+ const renderToolResult = () => {
+ if (!toolResult) return null;
+
+ if ("content" in toolResult) {
+ const parsedResult = CallToolResultSchema.safeParse(toolResult);
+ if (!parsedResult.success) {
+ return (
+ <>
+ <h4 className="font-semibold mb-2">Invalid Tool Result:</h4>
+ <pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64">
+ {JSON.stringify(toolResult, null, 2)}
+ </pre>
+ <h4 className="font-semibold mb-2">Errors:</h4>
+ {parsedResult.error.errors.map((error, idx) => (
+ <pre
+ key={idx}
+ className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64"
+ >
+ {JSON.stringify(error, null, 2)}
+ </pre>
+ ))}
+ </>
+ );
+ }
+ const structuredResult = parsedResult.data;
+ const isError = structuredResult.isError ?? false;
+
+ return (
+ <>
+ <h4 className="font-semibold mb-2">
+ Tool Result: {isError ? "Error" : "Success"}
+ </h4>
+ {structuredResult.content.map((item, index) => (
+ <div key={index} className="mb-2">
+ {item.type === "text" && (
+ <pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64">
+ {item.text}
+ </pre>
+ )}
+ {item.type === "image" && (
+ <img
+ src={`data:${item.mimeType};base64,${item.data}`}
+ alt="Tool result image"
+ className="max-w-full h-auto"
+ />
+ )}
+ {item.type === "resource" && (
+ <pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 whitespace-pre-wrap break-words p-4 rounded text-sm overflow-auto max-h-64">
+ {JSON.stringify(item.resource, null, 2)}
+ </pre>
+ )}
+ </div>
+ ))}
+ </>
+ );
+ } else if ("toolResult" in toolResult) {
+ return (
+ <>
+ <h4 className="font-semibold mb-2">Tool Result (Legacy):</h4>
+ <pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64">
+ {JSON.stringify(toolResult.toolResult, null, 2)}
+ </pre>
+ </>
+ );
+ }
+ };
+
+ return (
+ <TabsContent value="tools" className="grid grid-cols-2 gap-4">
+ <ListPane
+ items={tools}
+ listItems={listTools}
+ clearItems={() => {
+ clearTools();
+ setSelectedTool(null);
+ }}
+ setSelectedItem={setSelectedTool}
+ renderItem={(tool) => (
+ <>
+ <span className="flex-1">{tool.name}</span>
+ <span className="text-sm text-gray-500 text-right">
+ {tool.description}
+ </span>
+ </>
+ )}
+ title="Tools"
+ buttonText={nextCursor ? "List More Tools" : "List Tools"}
+ isButtonDisabled={!nextCursor && tools.length > 0}
+ />
+
+ <div className="bg-card rounded-lg shadow">
+ <div className="p-4 border-b border-gray-200">
+ <h3 className="font-semibold">
+ {selectedTool ? selectedTool.name : "Select a tool"}
+ </h3>
+ </div>
+ <div className="p-4">
+ {error ? (
+ <Alert variant="destructive">
+ <AlertCircle className="h-4 w-4" />
+ <AlertTitle>Error</AlertTitle>
+ <AlertDescription>{error}</AlertDescription>
+ </Alert>
+ ) : selectedTool ? (
+ <div className="space-y-4">
+ <p className="text-sm text-gray-600">
+ {selectedTool.description}
+ </p>
+ {Object.entries(selectedTool.inputSchema.properties ?? []).map(
+ ([key, value]) => (
+ <div key={key}>
+ <Label
+ htmlFor={key}
+ className="block text-sm font-medium text-gray-700"
+ >
+ {key}
+ </Label>
+ {
+ /* @ts-expect-error value type is currently unknown */
+ value.type === "string" ? (
+ <Textarea
+ id={key}
+ name={key}
+ // @ts-expect-error value type is currently unknown
+ placeholder={value.description}
+ onChange={(e) =>
+ setParams({
+ ...params,
+ [key]: e.target.value,
+ })
+ }
+ className="mt-1"
+ />
+ ) :
+ /* @ts-expect-error value type is currently unknown */
+ value.type === "object" ? (
+ <Textarea
+ id={key}
+ name={key}
+ // @ts-expect-error value type is currently unknown
+ placeholder={value.description}
+ onChange={(e) => {
+ try {
+ const parsed = JSON.parse(e.target.value);
+ setParams({
+ ...params,
+ [key]: parsed,
+ });
+ } catch (err) {
+ // If invalid JSON, store as string - will be validated on submit
+ setParams({
+ ...params,
+ [key]: e.target.value,
+ });
+ }
+ }}
+ className="mt-1"
+ />
+ ) : (
+ <Input
+ // @ts-expect-error value type is currently unknown
+ type={value.type === "number" ? "number" : "text"}
+ id={key}
+ name={key}
+ // @ts-expect-error value type is currently unknown
+ placeholder={value.description}
+ onChange={(e) =>
+ setParams({
+ ...params,
+ [key]:
+ // @ts-expect-error value type is currently unknown
+ value.type === "number"
+ ? Number(e.target.value)
+ : e.target.value,
+ })
+ }
+ className="mt-1"
+ />
+ )
+ }
+ </div>
+ ),
+ )}
+ <Button onClick={() => callTool(selectedTool.name, params)}>
+ <Send className="w-4 h-4 mr-2" />
+ Run Tool
+ </Button>
+ {toolResult && renderToolResult()}
+ </div>
+ ) : (
+ <Alert>
+ <AlertDescription>
+ Select a tool from the list to view its details and run it
+ </AlertDescription>
+ </Alert>
+ )}
+ </div>
+ </div>
+ </TabsContent>
+ );
+};
+
+export default ToolsTab;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| ConsoleTab.tsx | +
+
+ |
+ 100% | +8/8 | +100% | +1/1 | +100% | +1/1 | +100% | +8/8 | +
| History.tsx | +
+
+ |
+ 97.74% | +130/133 | +100% | +19/19 | +55.55% | +5/9 | +97.74% | +130/133 | +
| ListPane.tsx | +
+
+ |
+ 100% | +43/43 | +100% | +3/3 | +100% | +2/2 | +100% | +43/43 | +
| PingTab.tsx | +
+
+ |
+ 100% | +16/16 | +100% | +1/1 | +100% | +1/1 | +100% | +16/16 | +
| PromptsTab.tsx | +
+
+ |
+ 97.11% | +101/104 | +94.11% | +16/17 | +83.33% | +5/6 | +97.11% | +101/104 | +
| ResourcesTab.tsx | +
+
+ |
+ 97.54% | +159/163 | +89.28% | +25/28 | +88.88% | +8/9 | +97.54% | +159/163 | +
| RootsTab.tsx | +
+
+ |
+ 100% | +61/61 | +100% | +15/15 | +100% | +7/7 | +100% | +61/61 | +
| SamplingTab.tsx | +
+
+ |
+ 100% | +41/41 | +100% | +6/6 | +100% | +4/4 | +100% | +41/41 | +
| Sidebar.tsx | +
+
+ |
+ 65.43% | +142/217 | +50% | +6/12 | +25% | +2/8 | +65.43% | +142/217 | +
| ToolsTab.tsx | +
+
+ |
+ 29.38% | +57/194 | +33.33% | +2/6 | +20% | +1/5 | +29.38% | +57/194 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 | 1x + + + + +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x + +1x + + +1x +23x +23x +23x +23x +23x +23x +23x +1x + +1x + + +1x +2x +2x +2x +2x +2x +2x +1x + +1x + + +1x +23x +23x +23x +23x +23x +23x +1x + + + | import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+);
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
+>(({ className, variant, ...props }, ref) => (
+ <div
+ ref={ref}
+ role="alert"
+ className={cn(alertVariants({ variant }), className)}
+ {...props}
+ />
+));
+Alert.displayName = "Alert";
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes<HTMLHeadingElement>
+>(({ className, ...props }, ref) => (
+ <h5
+ ref={ref}
+ className={cn("mb-1 font-medium leading-none tracking-tight", className)}
+ {...props}
+ />
+));
+AlertTitle.displayName = "AlertTitle";
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes<HTMLParagraphElement>
+>(({ className, ...props }, ref) => (
+ <div
+ ref={ref}
+ className={cn("text-sm [&_p]:leading-relaxed", className)}
+ {...props}
+ />
+));
+AlertDescription.displayName = "AlertDescription";
+
+export { Alert, AlertTitle, AlertDescription };
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 | 1x + + + + + +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x + + + + + + + +1x +1x +223x +223x +223x +223x +223x +223x +223x + +223x +1x +1x + + + | import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow hover:bg-primary/90 dark:bg-gray-800 dark:text-white dark:hover:bg-gray-700",
+ destructive:
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2",
+ sm: "h-8 rounded-md px-3 text-xs",
+ lg: "h-10 rounded-md px-8",
+ icon: "h-9 w-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
+ VariantProps<typeof buttonVariants> {
+ asChild?: boolean;
+}
+
+const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+ return (
+ <Comp
+ className={cn(buttonVariants({ variant, size, className }))}
+ ref={ref}
+ {...props}
+ />
+ );
+ },
+);
+Button.displayName = "Button";
+
+export { Button, buttonVariants };
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| alert.tsx | +
+
+ |
+ 100% | +44/44 | +100% | +3/3 | +100% | +0/0 | +100% | +44/44 | +
| button.tsx | +
+
+ |
+ 100% | +42/42 | +50% | +1/2 | +100% | +0/0 | +100% | +42/42 | +
| input.tsx | +
+
+ |
+ 100% | +16/16 | +100% | +1/1 | +100% | +0/0 | +100% | +16/16 | +
| label.tsx | +
+
+ |
+ 100% | +13/13 | +100% | +1/1 | +100% | +0/0 | +100% | +13/13 | +
| select.tsx | +
+
+ |
+ 89.09% | +98/110 | +100% | +5/5 | +100% | +0/0 | +89.09% | +98/110 | +
| tabs.tsx | +
+
+ |
+ 100% | +38/38 | +100% | +3/3 | +100% | +0/0 | +100% | +38/38 | +
| textarea.tsx | +
+
+ |
+ 100% | +15/15 | +100% | +1/1 | +100% | +0/0 | +100% | +15/15 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 | 1x + + + + + + +1x +1x +58x +58x +58x +58x +58x +58x +58x +58x +58x +58x + +58x +1x +1x + + + | import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+export interface InputProps
+ extends React.InputHTMLAttributes<HTMLInputElement> {}
+
+const Input = React.forwardRef<HTMLInputElement, InputProps>(
+ ({ className, type, ...props }, ref) => {
+ return (
+ <input
+ type={type}
+ className={cn(
+ "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
+ className,
+ )}
+ ref={ref}
+ {...props}
+ />
+ );
+ },
+);
+Input.displayName = "Input";
+
+export { Input };
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 | 1x + + + + + +1x +1x +1x + +1x + + + +1x +8x +8x +8x +8x +8x +8x +1x + + + | import * as React from "react";
+import * as LabelPrimitive from "@radix-ui/react-label";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
+);
+
+const Label = React.forwardRef<
+ React.ElementRef<typeof LabelPrimitive.Root>,
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
+ VariantProps<typeof labelVariants>
+>(({ className, ...props }, ref) => (
+ <LabelPrimitive.Root
+ ref={ref}
+ className={cn(labelVariants(), className)}
+ {...props}
+ />
+));
+Label.displayName = LabelPrimitive.Root.displayName;
+
+export { Label };
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 | 1x + + + + + + + + + + +1x + +1x + +1x + +1x + + +1x +32x +32x +32x +32x +32x +32x +32x + +32x +32x +32x +32x +32x +32x +1x + +1x + + +1x +32x +32x +32x +32x +32x +32x +32x + +32x +32x +32x +1x + +1x + + +1x +32x +32x +32x +32x +32x +32x +32x + +32x +32x +32x +1x +1x + +1x + + +1x +32x +32x +32x +32x +32x +32x +32x +32x +32x +32x +32x + +32x +32x +32x +32x +32x +32x +32x + +32x +32x +32x +32x +32x +1x +1x + +1x + + +1x + + + + + + +1x + +1x + + +1x +80x +80x +80x +80x +80x +80x +80x + +80x +80x +80x +80x +80x +80x +80x +80x +1x + +1x + + +1x + + + + + + +1x + + + + + + + + + + + + + + | import * as React from "react";
+import {
+ CaretSortIcon,
+ CheckIcon,
+ ChevronDownIcon,
+ ChevronUpIcon,
+} from "@radix-ui/react-icons";
+import * as SelectPrimitive from "@radix-ui/react-select";
+
+import { cn } from "@/lib/utils";
+
+const Select = SelectPrimitive.Root;
+
+const SelectGroup = SelectPrimitive.Group;
+
+const SelectValue = SelectPrimitive.Value;
+
+const SelectTrigger = React.forwardRef<
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
+>(({ className, children, ...props }, ref) => (
+ <SelectPrimitive.Trigger
+ ref={ref}
+ className={cn(
+ "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
+ className,
+ )}
+ {...props}
+ >
+ {children}
+ <SelectPrimitive.Icon asChild>
+ <CaretSortIcon className="h-4 w-4 opacity-50" />
+ </SelectPrimitive.Icon>
+ </SelectPrimitive.Trigger>
+));
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
+
+const SelectScrollUpButton = React.forwardRef<
+ React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
+>(({ className, ...props }, ref) => (
+ <SelectPrimitive.ScrollUpButton
+ ref={ref}
+ className={cn(
+ "flex cursor-default items-center justify-center py-1",
+ className,
+ )}
+ {...props}
+ >
+ <ChevronUpIcon />
+ </SelectPrimitive.ScrollUpButton>
+));
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
+
+const SelectScrollDownButton = React.forwardRef<
+ React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
+>(({ className, ...props }, ref) => (
+ <SelectPrimitive.ScrollDownButton
+ ref={ref}
+ className={cn(
+ "flex cursor-default items-center justify-center py-1",
+ className,
+ )}
+ {...props}
+ >
+ <ChevronDownIcon />
+ </SelectPrimitive.ScrollDownButton>
+));
+SelectScrollDownButton.displayName =
+ SelectPrimitive.ScrollDownButton.displayName;
+
+const SelectContent = React.forwardRef<
+ React.ElementRef<typeof SelectPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
+>(({ className, children, position = "popper", ...props }, ref) => (
+ <SelectPrimitive.Portal>
+ <SelectPrimitive.Content
+ ref={ref}
+ className={cn(
+ "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+ position === "popper" &&
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
+ className,
+ )}
+ position={position}
+ {...props}
+ >
+ <SelectScrollUpButton />
+ <SelectPrimitive.Viewport
+ className={cn(
+ "p-1",
+ position === "popper" &&
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
+ )}
+ >
+ {children}
+ </SelectPrimitive.Viewport>
+ <SelectScrollDownButton />
+ </SelectPrimitive.Content>
+ </SelectPrimitive.Portal>
+));
+SelectContent.displayName = SelectPrimitive.Content.displayName;
+
+const SelectLabel = React.forwardRef<
+ React.ElementRef<typeof SelectPrimitive.Label>,
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
+>(({ className, ...props }, ref) => (
+ <SelectPrimitive.Label
+ ref={ref}
+ className={cn("px-2 py-1.5 text-sm font-semibold", className)}
+ {...props}
+ />
+));
+SelectLabel.displayName = SelectPrimitive.Label.displayName;
+
+const SelectItem = React.forwardRef<
+ React.ElementRef<typeof SelectPrimitive.Item>,
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
+>(({ className, children, ...props }, ref) => (
+ <SelectPrimitive.Item
+ ref={ref}
+ className={cn(
+ "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
+ className,
+ )}
+ {...props}
+ >
+ <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
+ <SelectPrimitive.ItemIndicator>
+ <CheckIcon className="h-4 w-4" />
+ </SelectPrimitive.ItemIndicator>
+ </span>
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
+ </SelectPrimitive.Item>
+));
+SelectItem.displayName = SelectPrimitive.Item.displayName;
+
+const SelectSeparator = React.forwardRef<
+ React.ElementRef<typeof SelectPrimitive.Separator>,
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
+>(({ className, ...props }, ref) => (
+ <SelectPrimitive.Separator
+ ref={ref}
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
+ {...props}
+ />
+));
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
+
+export {
+ Select,
+ SelectGroup,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectLabel,
+ SelectItem,
+ SelectSeparator,
+ SelectScrollUpButton,
+ SelectScrollDownButton,
+};
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 | 1x + + + + +1x + +1x + + +1x +9x +9x +9x +9x +9x +9x +9x +9x +9x +1x + +1x + + +1x +54x +54x +54x +54x +54x +54x +54x +54x +54x +1x + +1x + + +1x +64x +64x +64x +64x +64x +64x +64x +64x +64x +1x + + + | import * as React from "react";
+import * as TabsPrimitive from "@radix-ui/react-tabs";
+
+import { cn } from "@/lib/utils";
+
+const Tabs = TabsPrimitive.Root;
+
+const TabsList = React.forwardRef<
+ React.ElementRef<typeof TabsPrimitive.List>,
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
+>(({ className, ...props }, ref) => (
+ <TabsPrimitive.List
+ ref={ref}
+ className={cn(
+ "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
+ className,
+ )}
+ {...props}
+ />
+));
+TabsList.displayName = TabsPrimitive.List.displayName;
+
+const TabsTrigger = React.forwardRef<
+ React.ElementRef<typeof TabsPrimitive.Trigger>,
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
+>(({ className, ...props }, ref) => (
+ <TabsPrimitive.Trigger
+ ref={ref}
+ className={cn(
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-muted data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
+ className,
+ )}
+ {...props}
+ />
+));
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
+
+const TabsContent = React.forwardRef<
+ React.ElementRef<typeof TabsPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
+>(({ className, ...props }, ref) => (
+ <TabsPrimitive.Content
+ ref={ref}
+ className={cn(
+ "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
+ className,
+ )}
+ {...props}
+ />
+));
+TabsContent.displayName = TabsPrimitive.Content.displayName;
+
+export { Tabs, TabsList, TabsTrigger, TabsContent };
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 | 1x + + + + + + +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x + +1x +1x +1x + + + | import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+export interface TextareaProps
+ extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
+
+const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
+ ({ className, ...props }, ref) => {
+ return (
+ <textarea
+ className={cn(
+ "flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
+ className,
+ )}
+ ref={ref}
+ {...props}
+ />
+ );
+ },
+);
+Textarea.displayName = "Textarea";
+
+export { Textarea };
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ ++ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| useConnection.ts | +
+
+ |
+ 0% | +0/136 | +0% | +0/1 | +0% | +0/1 | +0% | +0/136 | +
| useDraggablePane.ts | +
+
+ |
+ 0% | +0/38 | +0% | +0/1 | +0% | +0/1 | +0% | +0/38 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; +import { + ClientNotification, + ClientRequest, + CreateMessageRequestSchema, + ListRootsRequestSchema, + ProgressNotificationSchema, + Request, + Result, + ServerCapabilities, +} from "@modelcontextprotocol/sdk/types.js"; +import { useState } from "react"; +import { toast } from "react-toastify"; +import { Notification, StdErrNotificationSchema } from "../notificationTypes"; +import { z } from "zod"; + +const DEFAULT_REQUEST_TIMEOUT_MSEC = 10000; + +interface UseConnectionOptions { + transportType: "stdio" | "sse"; + command: string; + args: string; + sseUrl: string; + env: Record<string, string>; + proxyServerUrl: string; + requestTimeout?: number; + onNotification?: (notification: Notification) => void; + onStdErrNotification?: (notification: Notification) => void; + onPendingRequest?: (request: any, resolve: any, reject: any) => void; + getRoots?: () => any[]; +} + +export function useConnection({ + transportType, + command, + args, + sseUrl, + env, + proxyServerUrl, + requestTimeout = DEFAULT_REQUEST_TIMEOUT_MSEC, + onNotification, + onStdErrNotification, + onPendingRequest, + getRoots, +}: UseConnectionOptions) { + const [connectionStatus, setConnectionStatus] = useState<"disconnected" | "connected" | "error">("disconnected"); + const [serverCapabilities, setServerCapabilities] = useState<ServerCapabilities | null>(null); + const [mcpClient, setMcpClient] = useState<Client | null>(null); + const [requestHistory, setRequestHistory] = useState<{ request: string; response?: string }[]>([]); + + const pushHistory = (request: object, response?: object) => { + setRequestHistory((prev) => [ + ...prev, + { + request: JSON.stringify(request), + response: response !== undefined ? JSON.stringify(response) : undefined, + }, + ]); + }; + + const makeRequest = async <T extends z.ZodType>( + request: ClientRequest, + schema: T + ) => { + if (!mcpClient) { + throw new Error("MCP client not connected"); + } + + try { + const abortController = new AbortController(); + const timeoutId = setTimeout(() => { + abortController.abort("Request timed out"); + }, requestTimeout); + + let response; + try { + response = await mcpClient.request(request, schema, { + signal: abortController.signal, + }); + pushHistory(request, response); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + pushHistory(request, { error: errorMessage }); + throw error; + } finally { + clearTimeout(timeoutId); + } + + + return response; + } catch (e: unknown) { + const errorString = (e as Error).message ?? String(e); + toast.error(errorString); + + throw e; + } + }; + + const sendNotification = async (notification: ClientNotification) => { + if (!mcpClient) { + throw new Error("MCP client not connected"); + } + + try { + await mcpClient.notification(notification); + pushHistory(notification); + } catch (e: unknown) { + toast.error((e as Error).message ?? String(e)); + throw e; + } + }; + + const connect = async () => { + try { + const client = new Client<Request, Notification, Result>( + { + name: "mcp-inspector", + version: "0.0.1", + }, + { + capabilities: { + sampling: {}, + roots: { + listChanged: true, + }, + }, + }, + ); + + const backendUrl = new URL(`${proxyServerUrl}/sse`); + + backendUrl.searchParams.append("transportType", transportType); + if (transportType === "stdio") { + backendUrl.searchParams.append("command", command); + backendUrl.searchParams.append("args", args); + backendUrl.searchParams.append("env", JSON.stringify(env)); + } else { + backendUrl.searchParams.append("url", sseUrl); + } + + const clientTransport = new SSEClientTransport(backendUrl); + + if (onNotification) { + client.setNotificationHandler(ProgressNotificationSchema, onNotification); + } + + if (onStdErrNotification) { + client.setNotificationHandler(StdErrNotificationSchema, onStdErrNotification); + } + + await client.connect(clientTransport); + + const capabilities = client.getServerCapabilities(); + setServerCapabilities(capabilities ?? null); + + if (onPendingRequest) { + client.setRequestHandler(CreateMessageRequestSchema, (request) => { + return new Promise((resolve, reject) => { + onPendingRequest(request, resolve, reject); + }); + }); + } + + if (getRoots) { + client.setRequestHandler(ListRootsRequestSchema, async () => { + return { roots: getRoots() }; + }); + } + + setMcpClient(client); + setConnectionStatus("connected"); + } catch (e) { + console.error(e); + setConnectionStatus("error"); + } + }; + + return { + connectionStatus, + serverCapabilities, + mcpClient, + requestHistory, + makeRequest, + sendNotification, + connect + }; +} |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { useCallback, useEffect, useRef, useState } from "react"; + +export function useDraggablePane(initialHeight: number) { + const [height, setHeight] = useState(initialHeight); + const [isDragging, setIsDragging] = useState(false); + const dragStartY = useRef<number>(0); + const dragStartHeight = useRef<number>(0); + + const handleDragStart = useCallback((e: React.MouseEvent) => { + setIsDragging(true); + dragStartY.current = e.clientY; + dragStartHeight.current = height; + document.body.style.userSelect = "none"; + }, [height]); + + const handleDragMove = useCallback((e: MouseEvent) => { + if (!isDragging) return; + const deltaY = dragStartY.current - e.clientY; + const newHeight = Math.max(100, Math.min(800, dragStartHeight.current + deltaY)); + setHeight(newHeight); + }, [isDragging]); + + const handleDragEnd = useCallback(() => { + setIsDragging(false); + document.body.style.userSelect = ""; + }, []); + + useEffect(() => { + if (isDragging) { + window.addEventListener("mousemove", handleDragMove); + window.addEventListener("mouseup", handleDragEnd); + return () => { + window.removeEventListener("mousemove", handleDragMove); + window.removeEventListener("mouseup", handleDragEnd); + }; + } + }, [isDragging, handleDragMove, handleDragEnd]); + + return { + height, + isDragging, + handleDragStart + }; +} |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| notificationTypes.ts | +
+
+ |
+ 0% | +0/10 | +0% | +0/1 | +0% | +0/1 | +0% | +0/10 | +
| useTheme.ts | +
+
+ |
+ 72.5% | +29/40 | +71.42% | +5/7 | +66.66% | +2/3 | +72.5% | +29/40 | +
| utils.ts | +
+
+ |
+ 100% | +4/4 | +100% | +1/1 | +100% | +1/1 | +100% | +4/4 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 | + + + + + + + + + + + + + + + + + + + | import { + NotificationSchema as BaseNotificationSchema, + ClientNotificationSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import { z } from "zod"; + +export const StdErrNotificationSchema = BaseNotificationSchema.extend({ + method: z.literal("notifications/stderr"), + params: z.object({ + content: z.string(), + }), +}); + +export const NotificationSchema = ClientNotificationSchema.or( + StdErrNotificationSchema, +); + +export type StdErrNotification = z.infer<typeof StdErrNotificationSchema>; +export type Notification = z.infer<typeof NotificationSchema>; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 | 1x + + + +1x +16x +7x +7x +16x + +16x +7x +7x +7x +7x + + + + + +7x +7x +7x + + +7x +7x +7x + + + +7x + +7x +7x +7x +16x + +16x +16x +16x + + + + + +16x +16x +16x + +1x + | import { useCallback, useEffect, useState } from "react";
+
+type Theme = "light" | "dark" | "system";
+
+const useTheme = (): [Theme, (mode: Theme) => void] => {
+ const [theme, setTheme] = useState<Theme>(() => {
+ const savedTheme = localStorage.getItem("theme") as Theme;
+ return savedTheme || "system";
+ });
+
+ useEffect(() => {
+ const darkModeMediaQuery = window.matchMedia(
+ "(prefers-color-scheme: dark)",
+ );
+ const handleDarkModeChange = (e: MediaQueryListEvent) => {
+ if (theme === "system") {
+ updateDocumentTheme(e.matches ? "dark" : "light");
+ }
+ };
+
+ const updateDocumentTheme = (newTheme: "light" | "dark") => {
+ document.documentElement.classList.toggle("dark", newTheme === "dark");
+ };
+
+ // Set initial theme based on current mode
+ if (theme === "system") {
+ updateDocumentTheme(darkModeMediaQuery.matches ? "dark" : "light");
+ } else {
+ updateDocumentTheme(theme);
+ }
+
+ darkModeMediaQuery.addEventListener("change", handleDarkModeChange);
+
+ return () => {
+ darkModeMediaQuery.removeEventListener("change", handleDarkModeChange);
+ };
+ }, [theme]);
+
+ return [
+ theme,
+ useCallback((newTheme: Theme) => {
+ setTheme(newTheme);
+ localStorage.setItem("theme", newTheme);
+ if (newTheme !== "system") {
+ document.documentElement.classList.toggle("dark", newTheme === "dark");
+ }
+ }, []),
+ ];
+};
+
+export default useTheme;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 | 1x + + +1x +705x +705x + | import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 | + + + + + + + + + + + + + | import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { ToastContainer } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; +import App from "./App.tsx"; +import "./index.css"; + +createRoot(document.getElementById("root")!).render( + <StrictMode> + <App /> + <ToastContainer /> + </StrictMode>, +); + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | /** @type {import('tailwindcss').Config} */ +import animate from "tailwindcss-animate"; +export default { + darkMode: ["class"], + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: { + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + colors: { + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + chart: { + 1: "hsl(var(--chart-1))", + 2: "hsl(var(--chart-2))", + 3: "hsl(var(--chart-3))", + 4: "hsl(var(--chart-4))", + 5: "hsl(var(--chart-5))", + }, + }, + }, + }, + plugins: [animate], +}; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| client | +
+
+ |
+ 0% | +0/64 | +0% | +0/2 | +0% | +0/2 | +0% | +0/64 | +
| client/bin | +
+
+ |
+ 0% | +0/16 | +0% | +0/1 | +0% | +0/1 | +0% | +0/16 | +
| client/src | +
+
+ |
+ 59.17% | +274/463 | +86.2% | +25/29 | +5.88% | +2/34 | +59.17% | +274/463 | +
| client/src/components | +
+
+ |
+ 77.34% | +758/980 | +87.03% | +94/108 | +69.23% | +36/52 | +77.34% | +758/980 | +
| client/src/components/ui | +
+
+ |
+ 95.68% | +266/278 | +93.75% | +15/16 | +100% | +0/0 | +95.68% | +266/278 | +
| client/src/lib | +
+
+ |
+ 61.11% | +33/54 | +66.66% | +6/9 | +60% | +3/5 | +61.11% | +33/54 | +
| client/src/lib/hooks | +
+
+ |
+ 0% | +0/174 | +0% | +0/2 | +0% | +0/2 | +0% | +0/174 | +