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 {
|
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}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
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,
|
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 ? (
|
||||||
|
|
|
||||||
|
|
@ -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 &&
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 ? (
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue