mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-23 18:29:21 -05:00
Add prompt management functionality to MCP server (#281)
This commit is contained in:
@@ -4,6 +4,7 @@ import { Server } from '@/types'
|
||||
import { ChevronDown, ChevronRight, AlertCircle, Copy, Check } from 'lucide-react'
|
||||
import { StatusBadge } from '@/components/ui/Badge'
|
||||
import ToolCard from '@/components/ui/ToolCard'
|
||||
import PromptCard from '@/components/ui/PromptCard'
|
||||
import DeleteDialog from '@/components/ui/DeleteDialog'
|
||||
import { useToast } from '@/contexts/ToastContext'
|
||||
|
||||
@@ -107,7 +108,6 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
||||
try {
|
||||
const { toggleTool } = await import('@/services/toolService')
|
||||
const result = await toggleTool(server.name, toolName, enabled)
|
||||
|
||||
if (result.success) {
|
||||
showToast(
|
||||
t(enabled ? 'tool.enableSuccess' : 'tool.disableSuccess', { name: toolName }),
|
||||
@@ -126,6 +126,28 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
||||
}
|
||||
}
|
||||
|
||||
const handlePromptToggle = async (promptName: string, enabled: boolean) => {
|
||||
try {
|
||||
const { togglePrompt } = await import('@/services/promptService')
|
||||
const result = await togglePrompt(server.name, promptName, enabled)
|
||||
if (result.success) {
|
||||
showToast(
|
||||
t(enabled ? 'tool.enableSuccess' : 'tool.disableSuccess', { name: promptName }),
|
||||
'success'
|
||||
)
|
||||
// Trigger refresh to update the prompt's state in the UI
|
||||
if (onRefresh) {
|
||||
onRefresh()
|
||||
}
|
||||
} else {
|
||||
showToast(result.error || t('tool.toggleFailed'), 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling prompt:', error)
|
||||
showToast(t('tool.toggleFailed'), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`bg-white shadow rounded-lg p-6 mb-6 page-card transition-all duration-200 ${server.enabled === false ? 'opacity-60' : ''}`}>
|
||||
@@ -145,6 +167,15 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
||||
<span>{server.tools?.length || 0} {t('server.tools')}</span>
|
||||
</div>
|
||||
|
||||
{/* Prompt count display */}
|
||||
<div className="flex items-center px-2 py-1 bg-purple-50 text-purple-700 rounded-full text-sm btn-primary">
|
||||
<svg className="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M2 5a2 2 0 012-2h7a2 2 0 012 2v4a2 2 0 01-2 2H9l-3 3v-3H4a2 2 0 01-2-2V5z" />
|
||||
<path d="M15 7v2a4 4 0 01-4 4H9.828l-1.766 1.767c.28.149.599.233.938.233h2l3 3v-3h2a2 2 0 002-2V9a2 2 0 00-2-2h-1z" />
|
||||
</svg>
|
||||
<span>{server.prompts?.length || 0} {t('server.prompts')}</span>
|
||||
</div>
|
||||
|
||||
{server.error && (
|
||||
<div className="relative">
|
||||
<div
|
||||
@@ -236,15 +267,35 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && server.tools && (
|
||||
<div className="mt-6">
|
||||
<h6 className={`font-medium ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'} mb-4`}>{t('server.tools')}</h6>
|
||||
<div className="space-y-4">
|
||||
{server.tools.map((tool, index) => (
|
||||
<ToolCard key={index} server={server.name} tool={tool} onToggle={handleToolToggle} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<>
|
||||
{server.tools && (
|
||||
<div className="mt-6">
|
||||
<h6 className={`font-medium ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'} mb-4`}>{t('server.tools')}</h6>
|
||||
<div className="space-y-4">
|
||||
{server.tools.map((tool, index) => (
|
||||
<ToolCard key={index} server={server.name} tool={tool} onToggle={handleToolToggle} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{server.prompts && (
|
||||
<div className="mt-6">
|
||||
<h6 className={`font-medium ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'} mb-4`}>{t('server.prompts')}</h6>
|
||||
<div className="space-y-4">
|
||||
{server.prompts.map((prompt, index) => (
|
||||
<PromptCard
|
||||
key={index}
|
||||
server={server.name}
|
||||
prompt={prompt}
|
||||
onToggle={handlePromptToggle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
300
frontend/src/components/ui/PromptCard.tsx
Normal file
300
frontend/src/components/ui/PromptCard.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Prompt } from '@/types'
|
||||
import { ChevronDown, ChevronRight, Play, Loader, Edit, Check } from '@/components/icons/LucideIcons'
|
||||
import { Switch } from './ToggleGroup'
|
||||
import { getPrompt, PromptCallResult } from '@/services/promptService'
|
||||
import DynamicForm from './DynamicForm'
|
||||
import PromptResult from './PromptResult'
|
||||
|
||||
interface PromptCardProps {
|
||||
server: string
|
||||
prompt: Prompt
|
||||
onToggle?: (promptName: string, enabled: boolean) => void
|
||||
onDescriptionUpdate?: (promptName: string, description: string) => void
|
||||
}
|
||||
|
||||
const PromptCard = ({ prompt, server, onToggle, onDescriptionUpdate }: PromptCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [showRunForm, setShowRunForm] = useState(false)
|
||||
const [isRunning, setIsRunning] = useState(false)
|
||||
const [result, setResult] = useState<PromptCallResult | null>(null)
|
||||
const [isEditingDescription, setIsEditingDescription] = useState(false)
|
||||
const [customDescription, setCustomDescription] = useState(prompt.description || '')
|
||||
const descriptionInputRef = useRef<HTMLInputElement>(null)
|
||||
const descriptionTextRef = useRef<HTMLSpanElement>(null)
|
||||
const [textWidth, setTextWidth] = useState<number>(0)
|
||||
|
||||
// Focus the input when editing mode is activated
|
||||
useEffect(() => {
|
||||
if (isEditingDescription && descriptionInputRef.current) {
|
||||
descriptionInputRef.current.focus()
|
||||
// Set input width to match text width
|
||||
if (textWidth > 0) {
|
||||
descriptionInputRef.current.style.width = `${textWidth + 20}px` // Add some padding
|
||||
}
|
||||
}
|
||||
}, [isEditingDescription, textWidth])
|
||||
|
||||
// Measure text width when not editing
|
||||
useEffect(() => {
|
||||
if (!isEditingDescription && descriptionTextRef.current) {
|
||||
setTextWidth(descriptionTextRef.current.offsetWidth)
|
||||
}
|
||||
}, [isEditingDescription, customDescription])
|
||||
|
||||
// Generate a unique key for localStorage based on prompt name and server
|
||||
const getStorageKey = useCallback(() => {
|
||||
return `mcphub_prompt_form_${server ? `${server}_` : ''}${prompt.name}`
|
||||
}, [prompt.name, server])
|
||||
|
||||
// Clear form data from localStorage
|
||||
const clearStoredFormData = useCallback(() => {
|
||||
localStorage.removeItem(getStorageKey())
|
||||
}, [getStorageKey])
|
||||
|
||||
const handleToggle = (enabled: boolean) => {
|
||||
if (onToggle) {
|
||||
onToggle(prompt.name, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDescriptionEdit = () => {
|
||||
setIsEditingDescription(true)
|
||||
}
|
||||
|
||||
const handleDescriptionSave = async () => {
|
||||
// For now, we'll just update the local state
|
||||
// In a real implementation, you would call an API to update the description
|
||||
setIsEditingDescription(false)
|
||||
if (onDescriptionUpdate) {
|
||||
onDescriptionUpdate(prompt.name, customDescription)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setCustomDescription(e.target.value)
|
||||
}
|
||||
|
||||
const handleDescriptionKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleDescriptionSave()
|
||||
} else if (e.key === 'Escape') {
|
||||
setCustomDescription(prompt.description || '')
|
||||
setIsEditingDescription(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleGetPrompt = async (arguments_: Record<string, any>) => {
|
||||
setIsRunning(true)
|
||||
try {
|
||||
const result = await getPrompt({ promptName: prompt.name, arguments: arguments_ }, server)
|
||||
console.log('GetPrompt result:', result)
|
||||
setResult({
|
||||
success: result.success,
|
||||
data: result.data,
|
||||
error: result.error
|
||||
})
|
||||
// Clear form data on successful submission
|
||||
// clearStoredFormData()
|
||||
} catch (error) {
|
||||
setResult({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
})
|
||||
} finally {
|
||||
setIsRunning(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancelRun = () => {
|
||||
setShowRunForm(false)
|
||||
// Clear form data when cancelled
|
||||
clearStoredFormData()
|
||||
setResult(null)
|
||||
}
|
||||
|
||||
const handleCloseResult = () => {
|
||||
setResult(null)
|
||||
}
|
||||
|
||||
// Convert prompt arguments to ToolInputSchema format for DynamicForm
|
||||
const convertToSchema = () => {
|
||||
if (!prompt.arguments || prompt.arguments.length === 0) {
|
||||
return { type: 'object', properties: {}, required: [] }
|
||||
}
|
||||
|
||||
const properties: Record<string, any> = {}
|
||||
const required: string[] = []
|
||||
|
||||
prompt.arguments.forEach(arg => {
|
||||
properties[arg.name] = {
|
||||
type: 'string', // Default to string for prompts
|
||||
description: arg.description || ''
|
||||
}
|
||||
|
||||
if (arg.required) {
|
||||
required.push(arg.name)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
type: 'object',
|
||||
properties,
|
||||
required
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 shadow rounded-lg p-4 mb-4">
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-medium text-gray-900">
|
||||
{prompt.name.replace(server + '-', '')}
|
||||
{prompt.title && (
|
||||
<span className="ml-2 text-sm font-normal text-gray-600">
|
||||
{prompt.title}
|
||||
</span>
|
||||
)}
|
||||
<span className="ml-2 text-sm font-normal text-gray-500 inline-flex items-center">
|
||||
{isEditingDescription ? (
|
||||
<>
|
||||
<input
|
||||
ref={descriptionInputRef}
|
||||
type="text"
|
||||
className="px-2 py-1 border border-blue-300 rounded bg-white text-sm focus:outline-none form-input"
|
||||
value={customDescription}
|
||||
onChange={handleDescriptionChange}
|
||||
onKeyDown={handleDescriptionKeyDown}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
minWidth: '100px',
|
||||
width: textWidth > 0 ? `${textWidth + 20}px` : 'auto'
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="ml-2 p-1 text-green-600 hover:text-green-800 cursor-pointer transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDescriptionSave()
|
||||
}}
|
||||
>
|
||||
<Check size={16} />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span ref={descriptionTextRef}>{customDescription || t('tool.noDescription')}</span>
|
||||
<button
|
||||
className="ml-2 p-1 text-gray-500 hover:text-blue-600 cursor-pointer transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDescriptionEdit()
|
||||
}}
|
||||
>
|
||||
<Edit size={14} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div
|
||||
className="flex items-center space-x-2"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{prompt.enabled !== undefined && (
|
||||
<Switch
|
||||
checked={prompt.enabled}
|
||||
onCheckedChange={handleToggle}
|
||||
disabled={isRunning}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setIsExpanded(true) // Ensure card is expanded when showing run form
|
||||
setShowRunForm(true)
|
||||
}}
|
||||
className="flex items-center space-x-1 px-3 py-1 text-sm text-blue-600 bg-blue-50 hover:bg-blue-100 rounded-md transition-colors btn-primary"
|
||||
disabled={isRunning || !prompt.enabled}
|
||||
>
|
||||
{isRunning ? (
|
||||
<Loader size={14} className="animate-spin" />
|
||||
) : (
|
||||
<Play size={14} />
|
||||
)}
|
||||
<span>{isRunning ? t('tool.running') : t('tool.run')}</span>
|
||||
</button>
|
||||
<button className="text-gray-400 hover:text-gray-600">
|
||||
{isExpanded ? <ChevronDown size={18} /> : <ChevronRight size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="mt-4 space-y-4">
|
||||
{/* Run Form */}
|
||||
{showRunForm && (
|
||||
<div className="border border-gray-300 rounded-lg p-4">
|
||||
<DynamicForm
|
||||
schema={convertToSchema()}
|
||||
onSubmit={handleGetPrompt}
|
||||
onCancel={handleCancelRun}
|
||||
loading={isRunning}
|
||||
storageKey={getStorageKey()}
|
||||
title={t('prompt.runPromptWithName', { name: prompt.name.replace(server + '-', '') })}
|
||||
/>
|
||||
{/* Prompt Result */}
|
||||
{result && (
|
||||
<div className="mt-4">
|
||||
<PromptResult result={result} onClose={handleCloseResult} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Arguments Display (when not showing form) */}
|
||||
{!showRunForm && prompt.arguments && prompt.arguments.length > 0 && (
|
||||
<div className="bg-gray-50 rounded p-3 border border-gray-300">
|
||||
<h4 className="text-sm font-medium text-gray-900 mb-2">{t('tool.parameters')}</h4>
|
||||
<div className="space-y-2">
|
||||
{prompt.arguments.map((arg, index) => (
|
||||
<div key={index} className="flex items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center">
|
||||
<span className="font-medium text-gray-700">{arg.name}</span>
|
||||
{arg.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</div>
|
||||
{arg.description && (
|
||||
<p className="text-sm text-gray-600 mt-1">{arg.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 ml-2">
|
||||
{arg.title || ''}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Result Display (when not showing form) */}
|
||||
{!showRunForm && result && (
|
||||
<div className="mt-4">
|
||||
<PromptResult result={result} onClose={handleCloseResult} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PromptCard
|
||||
158
frontend/src/components/ui/PromptResult.tsx
Normal file
158
frontend/src/components/ui/PromptResult.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CheckCircle, XCircle, AlertCircle } from '@/components/icons/LucideIcons';
|
||||
|
||||
interface PromptResultProps {
|
||||
result: {
|
||||
success: boolean;
|
||||
data?: any;
|
||||
error?: string;
|
||||
message?: string;
|
||||
};
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const PromptResult: React.FC<PromptResultProps> = ({ result, onClose }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const renderContent = (content: any): React.ReactNode => {
|
||||
if (typeof content === 'string') {
|
||||
return (
|
||||
<div className="bg-gray-50 rounded-md p-3">
|
||||
<pre className="whitespace-pre-wrap text-sm text-gray-800 font-mono">{content}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof content === 'object' && content !== null) {
|
||||
// Handle the specific prompt data structure
|
||||
if (content.description || content.messages) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{content.description && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-900 mb-2">{t('prompt.description')}</h4>
|
||||
<div className="bg-gray-50 rounded-md p-3">
|
||||
<p className="text-sm text-gray-800">{content.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{content.messages && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-900 mb-2">{t('prompt.messages')}</h4>
|
||||
<div className="space-y-3">
|
||||
{content.messages.map((message: any, index: number) => (
|
||||
<div key={index} className="bg-gray-50 rounded-md p-3">
|
||||
<div className="flex items-center mb-2">
|
||||
<span className="inline-block w-16 text-xs font-medium text-gray-500">
|
||||
{message.role}:
|
||||
</span>
|
||||
</div>
|
||||
{typeof message.content === 'string' ? (
|
||||
<pre className="whitespace-pre-wrap text-sm text-gray-800 font-mono">
|
||||
{message.content}
|
||||
</pre>
|
||||
) : typeof message.content === 'object' && message.content.type === 'text' ? (
|
||||
<pre className="whitespace-pre-wrap text-sm text-gray-800 font-mono">
|
||||
{message.content.text}
|
||||
</pre>
|
||||
) : (
|
||||
<pre className="text-sm text-gray-800 overflow-auto">
|
||||
{JSON.stringify(message.content, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// For other structured content, try to parse as JSON
|
||||
try {
|
||||
const parsed = typeof content === 'string' ? JSON.parse(content) : content;
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50 rounded-md p-3">
|
||||
<div className="text-xs text-gray-500 mb-2">{t('prompt.jsonResponse')}</div>
|
||||
<pre className="text-sm text-gray-800 overflow-auto">{JSON.stringify(parsed, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
} catch {
|
||||
// If not valid JSON, show as string
|
||||
return (
|
||||
<div className="bg-gray-50 rounded-md p-3">
|
||||
<pre className="whitespace-pre-wrap text-sm text-gray-800 font-mono">{String(content)}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50 rounded-md p-3">
|
||||
<pre className="whitespace-pre-wrap text-sm text-gray-800 font-mono">{String(content)}</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border border-gray-300 rounded-lg bg-white shadow-sm">
|
||||
<div className="border-b border-gray-300 px-4 py-3 bg-gray-50 rounded-t-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
{result.success ? (
|
||||
<CheckCircle size={20} className="text-status-green" />
|
||||
) : (
|
||||
<XCircle size={20} className="text-status-red" />
|
||||
)}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-900">
|
||||
{t('prompt.execution')} {result.success ? t('prompt.successful') : t('prompt.failed')}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 text-sm"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
{result.success ? (
|
||||
<div>
|
||||
{result.data ? (
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 mb-3">{t('prompt.result')}</div>
|
||||
{renderContent(result.data)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-gray-500 italic">
|
||||
{t('prompt.noContent')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<AlertCircle size={16} className="text-red-500" />
|
||||
<span className="text-sm font-medium text-red-700">{t('prompt.error')}</span>
|
||||
</div>
|
||||
<div className="bg-red-50 border border-red-300 rounded-md p-3">
|
||||
<pre className="text-sm text-red-800 whitespace-pre-wrap">
|
||||
{result.error || result.message || t('prompt.unknownError')}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptResult;
|
||||
@@ -139,6 +139,9 @@ const DashboardPage: React.FC = () => {
|
||||
<th scope="col" className="px-6 py-5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
{t('server.tools')}
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
{t('server.prompts')}
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
{t('server.enabled')}
|
||||
</th>
|
||||
@@ -163,6 +166,9 @@ const DashboardPage: React.FC = () => {
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{server.tools?.length || 0}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{server.prompts?.length || 0}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{server.enabled !== false ? (
|
||||
<span className="text-green-600">✓</span>
|
||||
|
||||
144
frontend/src/services/promptService.ts
Normal file
144
frontend/src/services/promptService.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { apiPost, apiPut } from '../utils/fetchInterceptor';
|
||||
|
||||
export interface PromptCallRequest {
|
||||
promptName: string;
|
||||
arguments?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface PromptCallResult {
|
||||
success: boolean;
|
||||
data?: any;
|
||||
error?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// GetPrompt result types
|
||||
export interface GetPromptResult {
|
||||
success: boolean;
|
||||
data?: any;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a MCP prompt via the call_prompt API
|
||||
*/
|
||||
export const callPrompt = async (
|
||||
request: PromptCallRequest,
|
||||
server?: string,
|
||||
): Promise<PromptCallResult> => {
|
||||
try {
|
||||
// Construct the URL with optional server parameter
|
||||
const url = server ? `/prompts/call/${server}` : '/prompts/call';
|
||||
const response = await apiPost<any>(url, {
|
||||
promptName: request.promptName,
|
||||
arguments: request.arguments,
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: response.message || 'Prompt call failed',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error calling prompt:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const getPrompt = async (
|
||||
request: PromptCallRequest,
|
||||
server?: string,
|
||||
): Promise<GetPromptResult> => {
|
||||
try {
|
||||
const response = await apiPost(
|
||||
`/mcp/${server}/prompts/${encodeURIComponent(request.promptName)}`,
|
||||
{
|
||||
name: request.promptName,
|
||||
arguments: request.arguments,
|
||||
},
|
||||
);
|
||||
|
||||
// apiPost already returns parsed data, not a Response object
|
||||
if (!response.success) {
|
||||
throw new Error(`Failed to get prompt: ${response.message || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting prompt:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle a prompt's enabled state for a specific server
|
||||
*/
|
||||
export const togglePrompt = async (
|
||||
serverName: string,
|
||||
promptName: string,
|
||||
enabled: boolean,
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
const response = await apiPost<any>(`/servers/${serverName}/prompts/${promptName}/toggle`, {
|
||||
enabled,
|
||||
});
|
||||
|
||||
return {
|
||||
success: response.success,
|
||||
error: response.success ? undefined : response.message,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error toggling prompt:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update a prompt's description for a specific server
|
||||
*/
|
||||
export const updatePromptDescription = async (
|
||||
serverName: string,
|
||||
promptName: string,
|
||||
description: string,
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
const response = await apiPut<any>(
|
||||
`/servers/${serverName}/prompts/${promptName}/description`,
|
||||
{ description },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('mcphub_token')}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
success: response.success,
|
||||
error: response.success ? undefined : response.message,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error updating prompt description:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -91,6 +91,20 @@ export interface Tool {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
// Prompt types
|
||||
export interface Prompt {
|
||||
name: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
arguments?: Array<{
|
||||
name: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
}>;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
// Server config types
|
||||
export interface ServerConfig {
|
||||
type?: 'stdio' | 'sse' | 'streamable-http' | 'openapi';
|
||||
@@ -101,6 +115,7 @@ export interface ServerConfig {
|
||||
headers?: Record<string, string>;
|
||||
enabled?: boolean;
|
||||
tools?: Record<string, { enabled: boolean; description?: string }>; // Tool-specific configurations with enable/disable state and custom descriptions
|
||||
prompts?: Record<string, { enabled: boolean; description?: string }>; // Prompt-specific configurations with enable/disable state and custom descriptions
|
||||
options?: {
|
||||
timeout?: number; // Request timeout in milliseconds
|
||||
resetTimeoutOnProgress?: boolean; // Reset timeout on progress notifications
|
||||
@@ -153,6 +168,7 @@ export interface Server {
|
||||
status: ServerStatus;
|
||||
error?: string;
|
||||
tools?: Tool[];
|
||||
prompts?: Prompt[];
|
||||
config?: ServerConfig;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user