Merge pull request #778 from jesselumarie/jesselumarie/test-SEP-974

mcp: add icon + website for testing SEP-973
This commit is contained in:
Cliff Hall 2025-12-01 16:29:49 -05:00 • committed by GitHub
commit 11e4e10c71
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 191 additions and 29 deletions

View file

@ -300,6 +300,7 @@ const App = () => {
const {
connectionStatus,
serverCapabilities,
serverImplementation,
mcpClient,
requestHistory,
clearRequestHistory,
@ -982,6 +983,7 @@ const App = () => {
loggingSupported={!!serverCapabilities?.logging || false}
connectionType={connectionType}
setConnectionType={setConnectionType}
serverImplementation={serverImplementation}
/>
<div
onMouseDown={handleSidebarDragStart}

View file

@ -49,6 +49,7 @@ const disconnectedConnectionState = {
completionsSupported: false,
connect: jest.fn(),
disconnect: jest.fn(),
serverImplementation: null,
};
// Connected state for tests that need an active connection

View file

@ -0,0 +1,57 @@
// Define Icon type locally since it might not be exported yet
interface Icon {
src: string;
mimeType?: string;
sizes?: string[];
}
// Helper type for objects that may have icons
export interface WithIcons {
icons?: Icon[];
}
interface IconDisplayProps {
icons?: Icon[];
className?: string;
size?: "sm" | "md" | "lg";
}
const IconDisplay = ({
icons,
className = "",
size = "md",
}: IconDisplayProps) => {
if (!icons || icons.length === 0) {
return null;
}
const sizeClasses = {
sm: "w-4 h-4",
md: "w-6 h-6",
lg: "w-8 h-8",
};
const sizeClass = sizeClasses[size];
return (
<div className={`flex gap-1 ${className}`}>
{icons.map((icon, index) => (
<img
key={index}
src={icon.src}
alt=""
className={`${sizeClass} object-contain flex-shrink-0`}
style={{
imageRendering: "auto",
}}
onError={(e) => {
// Hide broken images
e.currentTarget.style.display = "none";
}}
/>
))}
</div>
);
};
export default IconDisplay;

View file

@ -9,11 +9,12 @@ import {
PromptReference,
ResourceReference,
} from "@modelcontextprotocol/sdk/types.js";
import { AlertCircle } from "lucide-react";
import { AlertCircle, ChevronRight } from "lucide-react";
import { useEffect, useState } from "react";
import ListPane from "./ListPane";
import { useCompletionState } from "@/lib/hooks/useCompletionState";
import JsonView from "./JsonView";
import IconDisplay, { WithIcons } from "./IconDisplay";
export type Prompt = {
name: string;
@ -23,6 +24,7 @@ export type Prompt = {
description?: string;
required?: boolean;
}[];
icons?: { src: string; mimeType?: string; sizes?: string[] }[];
};
const PromptsTab = ({
@ -108,11 +110,17 @@ const PromptsTab = ({
setPromptArgs({});
}}
renderItem={(prompt) => (
<div className="flex flex-col items-start">
<span className="flex-1">{prompt.name}</span>
<span className="text-sm text-gray-500 text-left">
{prompt.description}
</span>
<div className="flex items-start w-full gap-2">
<div className="flex-shrink-0 mt-1">
<IconDisplay icons={prompt.icons} size="sm" />
</div>
<div className="flex flex-col flex-1 min-w-0">
<span className="truncate">{prompt.name}</span>
<span className="text-sm text-gray-500 text-left line-clamp-2">
{prompt.description}
</span>
</div>
<ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400 mt-1" />
</div>
)}
title="Prompts"
@ -122,9 +130,17 @@ const PromptsTab = ({
<div className="bg-card border border-border rounded-lg shadow">
<div className="p-4 border-b border-gray-200 dark:border-border">
<h3 className="font-semibold">
{selectedPrompt ? selectedPrompt.name : "Select a prompt"}
</h3>
<div className="flex items-center gap-2">
{selectedPrompt && (
<IconDisplay
icons={(selectedPrompt as WithIcons).icons}
size="md"
/>
)}
<h3 className="font-semibold">
{selectedPrompt ? selectedPrompt.name : "Select a prompt"}
</h3>
</div>
</div>
<div className="p-4">
{error ? (

View file

@ -17,6 +17,7 @@ import { useEffect, useState } from "react";
import { useCompletionState } from "@/lib/hooks/useCompletionState";
import JsonView from "./JsonView";
import { UriTemplate } from "@modelcontextprotocol/sdk/shared/uriTemplate.js";
import IconDisplay, { WithIcons } from "./IconDisplay";
const ResourcesTab = ({
resources,
@ -129,7 +130,10 @@ const ResourcesTab = ({
}}
renderItem={(resource) => (
<div className="flex items-center w-full">
<FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
<IconDisplay icons={(resource as WithIcons).icons} size="sm" />
{!(resource as WithIcons).icons && (
<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>
@ -159,7 +163,10 @@ const ResourcesTab = ({
}}
renderItem={(template) => (
<div className="flex items-center w-full">
<FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
<IconDisplay icons={(template as WithIcons).icons} size="sm" />
{!(template as WithIcons).icons && (
<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>
@ -175,16 +182,26 @@ const ResourcesTab = ({
<div className="bg-card border border-border rounded-lg shadow">
<div className="p-4 border-b border-gray-200 dark:border-border 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>
<div className="flex items-center gap-2 truncate">
{(selectedResource || selectedTemplate) && (
<IconDisplay
icons={
((selectedResource || selectedTemplate) as WithIcons).icons
}
size="md"
/>
)}
<h3
className="font-semibold truncate"
title={selectedResource?.name || selectedTemplate?.name}
>
{selectedResource
? selectedResource.name
: selectedTemplate
? selectedTemplate.name
: "Select a resource or template"}
</h3>
</div>
{selectedResource && (
<div className="flex row-auto gap-1 justify-end w-2/5">
{resourceSubscriptionsSupported &&

View file

@ -14,6 +14,7 @@ import {
RefreshCwOff,
Copy,
CheckCheck,
Server,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@ -40,6 +41,7 @@ import {
import CustomHeaders from "./CustomHeaders";
import { CustomHeaders as CustomHeadersType } from "@/lib/types/customHeaders";
import { useToast } from "../lib/hooks/useToast";
import IconDisplay, { WithIcons } from "./IconDisplay";
interface SidebarProps {
connectionStatus: ConnectionStatus;
@ -71,6 +73,9 @@ interface SidebarProps {
setConfig: (config: InspectorConfig) => void;
connectionType: "direct" | "proxy";
setConnectionType: (type: "direct" | "proxy") => void;
serverImplementation?:
| (WithIcons & { name?: string; version?: string; websiteUrl?: string })
| null;
}
const Sidebar = ({
@ -102,6 +107,7 @@ const Sidebar = ({
setConfig,
connectionType,
setConnectionType,
serverImplementation,
}: SidebarProps) => {
const [theme, setTheme] = useTheme();
const [showEnvVars, setShowEnvVars] = useState(false);
@ -776,6 +782,45 @@ const Sidebar = ({
</span>
</div>
{connectionStatus === "connected" && serverImplementation && (
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg mb-4">
<div className="flex items-center gap-2 mb-1">
{(serverImplementation as WithIcons).icons &&
(serverImplementation as WithIcons).icons!.length > 0 ? (
<IconDisplay
icons={(serverImplementation as WithIcons).icons}
size="sm"
/>
) : (
<Server className="w-4 h-4 text-gray-500" />
)}
{(serverImplementation as { websiteUrl?: string })
.websiteUrl ? (
<a
href={
(serverImplementation as { websiteUrl?: string })
.websiteUrl
}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline transition-colors"
>
{serverImplementation.name || "MCP Server"}
</a>
) : (
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
{serverImplementation.name || "MCP Server"}
</span>
)}
</div>
{serverImplementation.version && (
<div className="text-xs text-gray-500 dark:text-gray-400">
Version: {serverImplementation.version}
</div>
)}
</div>
)}
{loggingSupported && connectionStatus === "connected" && (
<div className="space-y-2">
<label

View file

@ -30,6 +30,7 @@ import {
Send,
ChevronDown,
ChevronUp,
ChevronRight,
AlertCircle,
Copy,
CheckCheck,
@ -40,6 +41,7 @@ import JsonView from "./JsonView";
import ToolResults from "./ToolResults";
import { useToast } from "@/lib/hooks/useToast";
import useCopy from "@/lib/hooks/useCopy";
import IconDisplay, { WithIcons } from "./IconDisplay";
import { cn } from "@/lib/utils";
import {
META_NAME_RULES_MESSAGE,
@ -158,11 +160,17 @@ const ToolsTab = ({
}}
setSelectedItem={setSelectedTool}
renderItem={(tool) => (
<div className="flex flex-col items-start">
<span className="flex-1">{tool.name}</span>
<span className="text-sm text-gray-500 text-left line-clamp-3">
{tool.description}
</span>
<div className="flex items-start w-full gap-2">
<div className="flex-shrink-0 mt-1">
<IconDisplay icons={(tool as WithIcons).icons} size="sm" />
</div>
<div className="flex flex-col flex-1 min-w-0">
<span className="truncate">{tool.name}</span>
<span className="text-sm text-gray-500 text-left line-clamp-2">
{tool.description}
</span>
</div>
<ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400 mt-1" />
</div>
)}
title="Tools"
@ -172,9 +180,17 @@ const ToolsTab = ({
<div className="bg-card border border-border rounded-lg shadow">
<div className="p-4 border-b border-gray-200 dark:border-border">
<h3 className="font-semibold">
{selectedTool ? selectedTool.name : "Select a tool"}
</h3>
<div className="flex items-center gap-2">
{selectedTool && (
<IconDisplay
icons={(selectedTool as WithIcons).icons}
size="md"
/>
)}
<h3 className="font-semibold">
{selectedTool ? selectedTool.name : "Select a tool"}
</h3>
</div>
</div>
<div className="p-4">
{selectedTool ? (

View file

@ -30,6 +30,7 @@ import {
Progress,
LoggingLevel,
ElicitRequestSchema,
Implementation,
} from "@modelcontextprotocol/sdk/types.js";
import type {
AnySchema,
@ -86,6 +87,7 @@ interface UseConnectionOptions {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getRoots?: () => any[];
defaultLoggingLevel?: LoggingLevel;
serverImplementation?: Implementation;
metadata?: Record<string, string>;
}
@ -125,6 +127,8 @@ export function useConnection({
const [mcpProtocolVersion, setMcpProtocolVersion] = useState<string | null>(
null,
);
const [serverImplementation, setServerImplementation] =
useState<Implementation | null>(null);
useEffect(() => {
if (!oauthClientId) {
@ -726,6 +730,8 @@ export function useConnection({
setClientTransport(transport);
capabilities = client.getServerCapabilities();
const serverInfo = client.getServerVersion();
setServerImplementation(serverInfo || null);
const initializeRequest = {
method: "initialize",
};
@ -843,11 +849,13 @@ export function useConnection({
const clearRequestHistory = () => {
setRequestHistory([]);
setServerImplementation(null);
};
return {
connectionStatus,
serverCapabilities,
serverImplementation,
mcpClient,
requestHistory,
clearRequestHistory,