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

View file

@ -49,6 +49,7 @@ const disconnectedConnectionState = {
completionsSupported: false, completionsSupported: false,
connect: jest.fn(), connect: jest.fn(),
disconnect: jest.fn(), disconnect: jest.fn(),
serverImplementation: null,
}; };
// Connected state for tests that need an active connection // 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, PromptReference,
ResourceReference, ResourceReference,
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
import { AlertCircle } from "lucide-react"; import { AlertCircle, ChevronRight } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import ListPane from "./ListPane"; import ListPane from "./ListPane";
import { useCompletionState } from "@/lib/hooks/useCompletionState"; import { useCompletionState } from "@/lib/hooks/useCompletionState";
import JsonView from "./JsonView"; import JsonView from "./JsonView";
import IconDisplay, { WithIcons } from "./IconDisplay";
export type Prompt = { export type Prompt = {
name: string; name: string;
@ -23,6 +24,7 @@ export type Prompt = {
description?: string; description?: string;
required?: boolean; required?: boolean;
}[]; }[];
icons?: { src: string; mimeType?: string; sizes?: string[] }[];
}; };
const PromptsTab = ({ const PromptsTab = ({
@ -108,11 +110,17 @@ const PromptsTab = ({
setPromptArgs({}); setPromptArgs({});
}} }}
renderItem={(prompt) => ( renderItem={(prompt) => (
<div className="flex flex-col items-start"> <div className="flex items-start w-full gap-2">
<span className="flex-1">{prompt.name}</span> <div className="flex-shrink-0 mt-1">
<span className="text-sm text-gray-500 text-left"> <IconDisplay icons={prompt.icons} size="sm" />
{prompt.description} </div>
</span> <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> </div>
)} )}
title="Prompts" title="Prompts"
@ -122,9 +130,17 @@ const PromptsTab = ({
<div className="bg-card border border-border rounded-lg shadow"> <div className="bg-card border border-border rounded-lg shadow">
<div className="p-4 border-b border-gray-200 dark:border-border"> <div className="p-4 border-b border-gray-200 dark:border-border">
<h3 className="font-semibold"> <div className="flex items-center gap-2">
{selectedPrompt ? selectedPrompt.name : "Select a prompt"} {selectedPrompt && (
</h3> <IconDisplay
icons={(selectedPrompt as WithIcons).icons}
size="md"
/>
)}
<h3 className="font-semibold">
{selectedPrompt ? selectedPrompt.name : "Select a prompt"}
</h3>
</div>
</div> </div>
<div className="p-4"> <div className="p-4">
{error ? ( {error ? (

View file

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

View file

@ -14,6 +14,7 @@ import {
RefreshCwOff, RefreshCwOff,
Copy, Copy,
CheckCheck, CheckCheck,
Server,
} from "lucide-react"; } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@ -40,6 +41,7 @@ import {
import CustomHeaders from "./CustomHeaders"; import CustomHeaders from "./CustomHeaders";
import { CustomHeaders as CustomHeadersType } from "@/lib/types/customHeaders"; import { CustomHeaders as CustomHeadersType } from "@/lib/types/customHeaders";
import { useToast } from "../lib/hooks/useToast"; import { useToast } from "../lib/hooks/useToast";
import IconDisplay, { WithIcons } from "./IconDisplay";
interface SidebarProps { interface SidebarProps {
connectionStatus: ConnectionStatus; connectionStatus: ConnectionStatus;
@ -71,6 +73,9 @@ interface SidebarProps {
setConfig: (config: InspectorConfig) => void; setConfig: (config: InspectorConfig) => void;
connectionType: "direct" | "proxy"; connectionType: "direct" | "proxy";
setConnectionType: (type: "direct" | "proxy") => void; setConnectionType: (type: "direct" | "proxy") => void;
serverImplementation?:
| (WithIcons & { name?: string; version?: string; websiteUrl?: string })
| null;
} }
const Sidebar = ({ const Sidebar = ({
@ -102,6 +107,7 @@ const Sidebar = ({
setConfig, setConfig,
connectionType, connectionType,
setConnectionType, setConnectionType,
serverImplementation,
}: SidebarProps) => { }: SidebarProps) => {
const [theme, setTheme] = useTheme(); const [theme, setTheme] = useTheme();
const [showEnvVars, setShowEnvVars] = useState(false); const [showEnvVars, setShowEnvVars] = useState(false);
@ -776,6 +782,45 @@ const Sidebar = ({
</span> </span>
</div> </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" && ( {loggingSupported && connectionStatus === "connected" && (
<div className="space-y-2"> <div className="space-y-2">
<label <label

View file

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

View file

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