Merge pull request #778 from jesselumarie/jesselumarie/test-SEP-974
mcp: add icon + website for testing SEP-973
This commit is contained in:
commit
11e4e10c71
8 changed files with 191 additions and 29 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
57
client/src/components/IconDisplay.tsx
Normal file
57
client/src/components/IconDisplay.tsx
Normal 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;
|
||||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue