mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
Add prompt management functionality to MCP server (#281)
This commit is contained in:
126
QWEN.md
Normal file
126
QWEN.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# MCPHub Project Overview
|
||||||
|
|
||||||
|
## Project Summary
|
||||||
|
|
||||||
|
MCPHub is a centralized hub server for managing multiple Model Context Protocol (MCP) servers. It allows organizing these servers into flexible Streamable HTTP (SSE) endpoints, supporting access to all servers, individual servers, or logical server groups. It provides a web dashboard for monitoring and managing servers, along with features like authentication, group-based access control, and Smart Routing using vector semantic search.
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- **Language:** TypeScript (Node.js)
|
||||||
|
- **Framework:** Express
|
||||||
|
- **Key Libraries:**
|
||||||
|
- `@modelcontextprotocol/sdk`: Core library for MCP interactions.
|
||||||
|
- `typeorm`: ORM for database interactions.
|
||||||
|
- `pg` & `pgvector`: PostgreSQL database and vector support.
|
||||||
|
- `jsonwebtoken` & `bcryptjs`: Authentication (JWT) and password hashing.
|
||||||
|
- `openai`: For embedding generation in Smart Routing.
|
||||||
|
- Various utility and validation libraries (e.g., `dotenv`, `express-validator`, `uuid`).
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- **Framework:** React (via Vite)
|
||||||
|
- **Language:** TypeScript
|
||||||
|
- **UI Library:** Tailwind CSS
|
||||||
|
- **Routing:** `react-router-dom`
|
||||||
|
- **Internationalization:** `i18next`
|
||||||
|
- **Component Structure:** Modular components and pages within `frontend/src`.
|
||||||
|
|
||||||
|
### Infrastructure
|
||||||
|
- **Build Tool:** `pnpm` (package manager and script runner).
|
||||||
|
- **Containerization:** Docker (`Dockerfile` provided).
|
||||||
|
- **Process Management:** Not explicitly defined in core files, but likely managed by Docker or host system.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
- **MCP Server Management:** Configure, start, stop, and monitor multiple upstream MCP servers via `stdio`, `SSE`, or `Streamable HTTP` protocols.
|
||||||
|
- **Centralized Dashboard:** Web UI for server status, group management, user administration, and logs.
|
||||||
|
- **Flexible Endpoints:**
|
||||||
|
- Global MCP/SSE endpoint (`/mcp`, `/sse`) for all enabled servers.
|
||||||
|
- Group-based endpoints (`/mcp/{group}`, `/sse/{group}`).
|
||||||
|
- Server-specific endpoints (`/mcp/{server}`, `/sse/{server}`).
|
||||||
|
- Smart Routing endpoint (`/mcp/$smart`, `/sse/$smart`) using vector search.
|
||||||
|
- **Authentication & Authorization:** JWT-based user authentication with role-based access control (admin/user).
|
||||||
|
- **Group Management:** Logical grouping of servers for targeted access and permission control.
|
||||||
|
- **Smart Routing (Experimental):** Uses pgvector and OpenAI embeddings to semantically search and find relevant tools across all connected servers.
|
||||||
|
- **Configuration:** Managed via `mcp_settings.json`.
|
||||||
|
- **Logging:** Server logs are captured and viewable via the dashboard.
|
||||||
|
- **Marketplace Integration:** Access to a marketplace of MCP servers (`servers.json`).
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
C:\code\mcphub\
|
||||||
|
├───src\ # Backend source code (TypeScript)
|
||||||
|
├───frontend\ # Frontend source code (React/TypeScript)
|
||||||
|
│ ├───src\
|
||||||
|
│ ├───components\ # Reusable UI components
|
||||||
|
│ ├───pages\ # Top-level page components
|
||||||
|
│ ├───contexts\ # React contexts (Auth, Theme, Toast)
|
||||||
|
│ ├───layouts\ # Page layouts
|
||||||
|
│ ├───utils\ # Frontend utilities
|
||||||
|
│ └───...
|
||||||
|
├───dist\ # Compiled backend output
|
||||||
|
├───frontend\dist\ # Compiled frontend output
|
||||||
|
├───tests\ # Backend tests
|
||||||
|
├───docs\ # Documentation
|
||||||
|
├───scripts\ # Utility scripts
|
||||||
|
├───bin\ # CLI entry points
|
||||||
|
├───assets\ # Static assets (e.g., images for README)
|
||||||
|
├───.github\ # GitHub workflows
|
||||||
|
├───.vscode\ # VS Code settings
|
||||||
|
├───mcp_settings.json # Main configuration file for MCP servers and users
|
||||||
|
├───servers.json # Marketplace server definitions
|
||||||
|
├───package.json # Node.js project definition, dependencies, and scripts
|
||||||
|
├───pnpm-lock.yaml # Dependency lock file
|
||||||
|
├───tsconfig.json # TypeScript compiler configuration (Backend)
|
||||||
|
├───README.md # Project documentation
|
||||||
|
├───Dockerfile # Docker image definition
|
||||||
|
└───...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building and Running
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Node.js (>=18.0.0 or >=20.0.0)
|
||||||
|
- pnpm
|
||||||
|
- Python 3.13 (for some upstream servers and uvx)
|
||||||
|
- Docker (optional, for containerized deployment)
|
||||||
|
- PostgreSQL with pgvector (optional, for Smart Routing)
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
1. Clone the repository.
|
||||||
|
2. Install dependencies: `pnpm install`.
|
||||||
|
3. Start development servers: `pnpm dev`.
|
||||||
|
- This runs `pnpm backend:dev` (Node.js with `tsx watch`) and `pnpm frontend:dev` (Vite dev server) concurrently.
|
||||||
|
- Access the dashboard at `http://localhost:5173` (Vite default) or the configured port/path.
|
||||||
|
|
||||||
|
### Production Build
|
||||||
|
1. Install dependencies: `pnpm install`.
|
||||||
|
2. Build the project: `pnpm build`.
|
||||||
|
- This runs `pnpm backend:build` (TypeScript compilation to `dist/`) and `pnpm frontend:build` (Vite build to `frontend/dist/`).
|
||||||
|
3. Start the production server: `pnpm start`.
|
||||||
|
- This runs `node dist/index.js`.
|
||||||
|
|
||||||
|
### Docker Deployment
|
||||||
|
- Pull the image: `docker pull samanhappy/mcphub`.
|
||||||
|
- Run with default settings: `docker run -p 3000:3000 samanhappy/mcphub`.
|
||||||
|
- Run with custom config: `docker run -p 3000:3000 -v ./mcp_settings.json:/app/mcp_settings.json -v ./data:/app/data samanhappy/mcphub`.
|
||||||
|
- Access the dashboard at `http://localhost:3000`.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The main configuration file is `mcp_settings.json`. It defines:
|
||||||
|
- `mcpServers`: A map of server configurations (command, args, env, URL, etc.).
|
||||||
|
- `users`: A list of user accounts (username, hashed password, admin status).
|
||||||
|
- `groups`: A map of server groups.
|
||||||
|
- `systemConfig`: System-wide settings (e.g., proxy, registry, installation options).
|
||||||
|
|
||||||
|
## Development Conventions
|
||||||
|
|
||||||
|
- **Language:** TypeScript for both backend and frontend.
|
||||||
|
- **Backend Style:** Modular structure with clear separation of concerns (controllers, services, models, middlewares, routes, config, utils).
|
||||||
|
- **Frontend Style:** Component-based React architecture with contexts for state management.
|
||||||
|
- **Database:** TypeORM with PostgreSQL is used, leveraging decorators for entity definition.
|
||||||
|
- **Testing:** Uses `jest` for backend testing.
|
||||||
|
- **Linting/Formatting:** Uses `eslint` and `prettier`.
|
||||||
|
- **Scripts:** Defined in `package.json` under the `scripts` section for common tasks (dev, build, start, test, lint, format).
|
||||||
@@ -4,6 +4,7 @@ import { Server } from '@/types'
|
|||||||
import { ChevronDown, ChevronRight, AlertCircle, Copy, Check } from 'lucide-react'
|
import { ChevronDown, ChevronRight, AlertCircle, Copy, Check } from 'lucide-react'
|
||||||
import { StatusBadge } from '@/components/ui/Badge'
|
import { StatusBadge } from '@/components/ui/Badge'
|
||||||
import ToolCard from '@/components/ui/ToolCard'
|
import ToolCard from '@/components/ui/ToolCard'
|
||||||
|
import PromptCard from '@/components/ui/PromptCard'
|
||||||
import DeleteDialog from '@/components/ui/DeleteDialog'
|
import DeleteDialog from '@/components/ui/DeleteDialog'
|
||||||
import { useToast } from '@/contexts/ToastContext'
|
import { useToast } from '@/contexts/ToastContext'
|
||||||
|
|
||||||
@@ -107,7 +108,6 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
|||||||
try {
|
try {
|
||||||
const { toggleTool } = await import('@/services/toolService')
|
const { toggleTool } = await import('@/services/toolService')
|
||||||
const result = await toggleTool(server.name, toolName, enabled)
|
const result = await toggleTool(server.name, toolName, enabled)
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
showToast(
|
showToast(
|
||||||
t(enabled ? 'tool.enableSuccess' : 'tool.disableSuccess', { name: toolName }),
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`bg-white shadow rounded-lg p-6 mb-6 page-card transition-all duration-200 ${server.enabled === false ? 'opacity-60' : ''}`}>
|
<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>
|
<span>{server.tools?.length || 0} {t('server.tools')}</span>
|
||||||
</div>
|
</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 && (
|
{server.error && (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<div
|
||||||
@@ -236,15 +267,35 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isExpanded && server.tools && (
|
{isExpanded && (
|
||||||
<div className="mt-6">
|
<>
|
||||||
<h6 className={`font-medium ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'} mb-4`}>{t('server.tools')}</h6>
|
{server.tools && (
|
||||||
<div className="space-y-4">
|
<div className="mt-6">
|
||||||
{server.tools.map((tool, index) => (
|
<h6 className={`font-medium ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'} mb-4`}>{t('server.tools')}</h6>
|
||||||
<ToolCard key={index} server={server.name} tool={tool} onToggle={handleToolToggle} />
|
<div className="space-y-4">
|
||||||
))}
|
{server.tools.map((tool, index) => (
|
||||||
</div>
|
<ToolCard key={index} server={server.name} tool={tool} onToggle={handleToolToggle} />
|
||||||
</div>
|
))}
|
||||||
|
</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>
|
</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">
|
<th scope="col" className="px-6 py-5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
{t('server.tools')}
|
{t('server.tools')}
|
||||||
</th>
|
</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">
|
<th scope="col" className="px-6 py-5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
{t('server.enabled')}
|
{t('server.enabled')}
|
||||||
</th>
|
</th>
|
||||||
@@ -163,6 +166,9 @@ const DashboardPage: React.FC = () => {
|
|||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
{server.tools?.length || 0}
|
{server.tools?.length || 0}
|
||||||
</td>
|
</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">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
{server.enabled !== false ? (
|
{server.enabled !== false ? (
|
||||||
<span className="text-green-600">✓</span>
|
<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;
|
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
|
// Server config types
|
||||||
export interface ServerConfig {
|
export interface ServerConfig {
|
||||||
type?: 'stdio' | 'sse' | 'streamable-http' | 'openapi';
|
type?: 'stdio' | 'sse' | 'streamable-http' | 'openapi';
|
||||||
@@ -101,6 +115,7 @@ export interface ServerConfig {
|
|||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
tools?: Record<string, { enabled: boolean; description?: string }>; // Tool-specific configurations with enable/disable state and custom descriptions
|
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?: {
|
options?: {
|
||||||
timeout?: number; // Request timeout in milliseconds
|
timeout?: number; // Request timeout in milliseconds
|
||||||
resetTimeoutOnProgress?: boolean; // Reset timeout on progress notifications
|
resetTimeoutOnProgress?: boolean; // Reset timeout on progress notifications
|
||||||
@@ -153,6 +168,7 @@ export interface Server {
|
|||||||
status: ServerStatus;
|
status: ServerStatus;
|
||||||
error?: string;
|
error?: string;
|
||||||
tools?: Tool[];
|
tools?: Tool[];
|
||||||
|
prompts?: Prompt[];
|
||||||
config?: ServerConfig;
|
config?: ServerConfig;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,7 @@
|
|||||||
"deleteWarning": "Deleting server '{{name}}' will remove it and all its data. This action cannot be undone.",
|
"deleteWarning": "Deleting server '{{name}}' will remove it and all its data. This action cannot be undone.",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"tools": "Tools",
|
"tools": "Tools",
|
||||||
|
"prompts": "Prompts",
|
||||||
"name": "Server Name",
|
"name": "Server Name",
|
||||||
"url": "Server URL",
|
"url": "Server URL",
|
||||||
"apiKey": "API Key",
|
"apiKey": "API Key",
|
||||||
@@ -414,6 +415,23 @@
|
|||||||
"addItem": "Add {{key}} item",
|
"addItem": "Add {{key}} item",
|
||||||
"enterKey": "Enter {{key}}"
|
"enterKey": "Enter {{key}}"
|
||||||
},
|
},
|
||||||
|
"prompt": {
|
||||||
|
"run": "Get",
|
||||||
|
"running": "Getting...",
|
||||||
|
"result": "Prompt Result",
|
||||||
|
"error": "Prompt Error",
|
||||||
|
"execution": "Prompt Execution",
|
||||||
|
"successful": "Successful",
|
||||||
|
"failed": "Failed",
|
||||||
|
"errorDetails": "Error Details:",
|
||||||
|
"noContent": "Prompt executed successfully but returned no content.",
|
||||||
|
"unknownError": "Unknown error occurred",
|
||||||
|
"jsonResponse": "JSON Response:",
|
||||||
|
"description": "Description",
|
||||||
|
"messages": "Messages",
|
||||||
|
"noDescription": "No description available",
|
||||||
|
"runPromptWithName": "Get Prompt: {{name}}"
|
||||||
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"enableGlobalRoute": "Enable Global Route",
|
"enableGlobalRoute": "Enable Global Route",
|
||||||
"enableGlobalRouteDescription": "Allow connections to /sse endpoint without specifying a group ID",
|
"enableGlobalRouteDescription": "Allow connections to /sse endpoint without specifying a group ID",
|
||||||
|
|||||||
@@ -80,6 +80,7 @@
|
|||||||
"deleteWarning": "删除服务器 '{{name}}' 将会移除该服务器及其所有数据。此操作无法撤销。",
|
"deleteWarning": "删除服务器 '{{name}}' 将会移除该服务器及其所有数据。此操作无法撤销。",
|
||||||
"status": "状态",
|
"status": "状态",
|
||||||
"tools": "工具",
|
"tools": "工具",
|
||||||
|
"prompts": "提示词",
|
||||||
"name": "服务器名称",
|
"name": "服务器名称",
|
||||||
"url": "服务器 URL",
|
"url": "服务器 URL",
|
||||||
"apiKey": "API 密钥",
|
"apiKey": "API 密钥",
|
||||||
@@ -415,6 +416,23 @@
|
|||||||
"addItem": "添加 {{key}} 项目",
|
"addItem": "添加 {{key}} 项目",
|
||||||
"enterKey": "输入 {{key}}"
|
"enterKey": "输入 {{key}}"
|
||||||
},
|
},
|
||||||
|
"prompt": {
|
||||||
|
"run": "获取",
|
||||||
|
"running": "获取中...",
|
||||||
|
"result": "提示词结果",
|
||||||
|
"error": "提示词错误",
|
||||||
|
"execution": "提示词执行",
|
||||||
|
"successful": "成功",
|
||||||
|
"failed": "失败",
|
||||||
|
"errorDetails": "错误详情:",
|
||||||
|
"noContent": "提示词执行成功但未返回内容。",
|
||||||
|
"unknownError": "发生未知错误",
|
||||||
|
"jsonResponse": "JSON 响应:",
|
||||||
|
"description": "描述",
|
||||||
|
"messages": "消息",
|
||||||
|
"noDescription": "无描述信息",
|
||||||
|
"runPromptWithName": "获取提示词: {{name}}"
|
||||||
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"enableGlobalRoute": "启用全局路由",
|
"enableGlobalRoute": "启用全局路由",
|
||||||
"enableGlobalRouteDescription": "允许不指定组 ID 就连接到 /sse 端点",
|
"enableGlobalRouteDescription": "允许不指定组 ID 就连接到 /sse 端点",
|
||||||
|
|||||||
45
src/controllers/promptController.ts
Normal file
45
src/controllers/promptController.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { ApiResponse } from '../types/index.js';
|
||||||
|
import { handleGetPromptRequest } from '../services/mcpService.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific prompt by server and prompt name
|
||||||
|
*/
|
||||||
|
export const getPrompt = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { serverName, promptName } = req.params;
|
||||||
|
if (!serverName || !promptName) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'serverName and promptName are required',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promptArgs = {
|
||||||
|
params: req.body as { [key: string]: any }
|
||||||
|
};
|
||||||
|
const result = await handleGetPromptRequest(promptArgs, serverName);
|
||||||
|
if (result.isError) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Failed to get prompt',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: ApiResponse = {
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json(response);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting prompt:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Failed to get prompt',
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -739,3 +739,131 @@ export const updateSystemConfig = (req: Request, res: Response): void => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Toggle prompt status for a specific server
|
||||||
|
export const togglePrompt = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { serverName, promptName } = req.params;
|
||||||
|
const { enabled } = req.body;
|
||||||
|
|
||||||
|
if (!serverName || !promptName) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Server name and prompt name are required',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof enabled !== 'boolean') {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Enabled status must be a boolean',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = loadSettings();
|
||||||
|
if (!settings.mcpServers[serverName]) {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Server not found',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize prompts config if it doesn't exist
|
||||||
|
if (!settings.mcpServers[serverName].prompts) {
|
||||||
|
settings.mcpServers[serverName].prompts = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the prompt's enabled state
|
||||||
|
settings.mcpServers[serverName].prompts![promptName] = { enabled };
|
||||||
|
|
||||||
|
if (!saveSettings(settings)) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Failed to save settings',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify that tools have changed (as prompts are part of the tool listing)
|
||||||
|
notifyToolChanged();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: `Prompt ${promptName} ${enabled ? 'enabled' : 'disabled'} successfully`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update prompt description for a specific server
|
||||||
|
export const updatePromptDescription = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { serverName, promptName } = req.params;
|
||||||
|
const { description } = req.body;
|
||||||
|
|
||||||
|
if (!serverName || !promptName) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Server name and prompt name are required',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof description !== 'string') {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Description must be a string',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = loadSettings();
|
||||||
|
if (!settings.mcpServers[serverName]) {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Server not found',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize prompts config if it doesn't exist
|
||||||
|
if (!settings.mcpServers[serverName].prompts) {
|
||||||
|
settings.mcpServers[serverName].prompts = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the prompt's description
|
||||||
|
if (!settings.mcpServers[serverName].prompts![promptName]) {
|
||||||
|
settings.mcpServers[serverName].prompts![promptName] = { enabled: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.mcpServers[serverName].prompts![promptName].description = description;
|
||||||
|
|
||||||
|
if (!saveSettings(settings)) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Failed to save settings',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify that tools have changed (as prompts are part of the tool listing)
|
||||||
|
notifyToolChanged();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: `Prompt ${promptName} description updated successfully`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
toggleServer,
|
toggleServer,
|
||||||
toggleTool,
|
toggleTool,
|
||||||
updateToolDescription,
|
updateToolDescription,
|
||||||
|
togglePrompt,
|
||||||
|
updatePromptDescription,
|
||||||
updateSystemConfig,
|
updateSystemConfig,
|
||||||
} from '../controllers/serverController.js';
|
} from '../controllers/serverController.js';
|
||||||
import {
|
import {
|
||||||
@@ -58,6 +60,7 @@ import { login, register, getCurrentUser, changePassword } from '../controllers/
|
|||||||
import { getAllLogs, clearLogs, streamLogs } from '../controllers/logController.js';
|
import { getAllLogs, clearLogs, streamLogs } from '../controllers/logController.js';
|
||||||
import { getRuntimeConfig, getPublicConfig } from '../controllers/configController.js';
|
import { getRuntimeConfig, getPublicConfig } from '../controllers/configController.js';
|
||||||
import { callTool } from '../controllers/toolController.js';
|
import { callTool } from '../controllers/toolController.js';
|
||||||
|
import { getPrompt } from '../controllers/promptController.js';
|
||||||
import { uploadDxtFile, uploadMiddleware } from '../controllers/dxtController.js';
|
import { uploadDxtFile, uploadMiddleware } from '../controllers/dxtController.js';
|
||||||
import { healthCheck } from '../controllers/healthController.js';
|
import { healthCheck } from '../controllers/healthController.js';
|
||||||
import { auth } from '../middlewares/auth.js';
|
import { auth } from '../middlewares/auth.js';
|
||||||
@@ -77,6 +80,8 @@ export const initRoutes = (app: express.Application): void => {
|
|||||||
router.post('/servers/:name/toggle', toggleServer);
|
router.post('/servers/:name/toggle', toggleServer);
|
||||||
router.post('/servers/:serverName/tools/:toolName/toggle', toggleTool);
|
router.post('/servers/:serverName/tools/:toolName/toggle', toggleTool);
|
||||||
router.put('/servers/:serverName/tools/:toolName/description', updateToolDescription);
|
router.put('/servers/:serverName/tools/:toolName/description', updateToolDescription);
|
||||||
|
router.post('/servers/:serverName/prompts/:promptName/toggle', togglePrompt);
|
||||||
|
router.put('/servers/:serverName/prompts/:promptName/description', updatePromptDescription);
|
||||||
router.put('/system-config', updateSystemConfig);
|
router.put('/system-config', updateSystemConfig);
|
||||||
|
|
||||||
// Group management routes
|
// Group management routes
|
||||||
@@ -106,6 +111,9 @@ export const initRoutes = (app: express.Application): void => {
|
|||||||
// Tool management routes
|
// Tool management routes
|
||||||
router.post('/tools/call/:server', callTool);
|
router.post('/tools/call/:server', callTool);
|
||||||
|
|
||||||
|
// Prompt management routes
|
||||||
|
router.post('/mcp/:serverName/prompts/:promptName', getPrompt);
|
||||||
|
|
||||||
// DXT upload routes
|
// DXT upload routes
|
||||||
router.post('/dxt/upload', uploadMiddleware, uploadDxtFile);
|
router.post('/dxt/upload', uploadMiddleware, uploadDxtFile);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
ListPromptsRequestSchema,
|
||||||
|
GetPromptRequestSchema,
|
||||||
|
ServerCapabilities,
|
||||||
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||||
import { ServerInfo, ServerConfig, ToolInfo } from '../types/index.js';
|
import { ServerInfo, ServerConfig, Tool } from '../types/index.js';
|
||||||
import { loadSettings, saveSettings, expandEnvVars, replaceEnvVars } from '../config/index.js';
|
import { loadSettings, saveSettings, expandEnvVars, replaceEnvVars } from '../config/index.js';
|
||||||
import config from '../config/index.js';
|
import config from '../config/index.js';
|
||||||
import { getGroup } from './sseService.js';
|
import { getGroup } from './sseService.js';
|
||||||
@@ -343,6 +349,7 @@ export const initializeClientsFromSettings = async (
|
|||||||
status: 'disconnected',
|
status: 'disconnected',
|
||||||
error: null,
|
error: null,
|
||||||
tools: [],
|
tools: [],
|
||||||
|
prompts: [],
|
||||||
createTime: Date.now(),
|
createTime: Date.now(),
|
||||||
enabled: false,
|
enabled: false,
|
||||||
});
|
});
|
||||||
@@ -376,6 +383,7 @@ export const initializeClientsFromSettings = async (
|
|||||||
status: 'disconnected',
|
status: 'disconnected',
|
||||||
error: 'Missing OpenAPI specification URL or schema',
|
error: 'Missing OpenAPI specification URL or schema',
|
||||||
tools: [],
|
tools: [],
|
||||||
|
prompts: [],
|
||||||
createTime: Date.now(),
|
createTime: Date.now(),
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
@@ -388,6 +396,7 @@ export const initializeClientsFromSettings = async (
|
|||||||
status: 'connecting',
|
status: 'connecting',
|
||||||
error: null,
|
error: null,
|
||||||
tools: [],
|
tools: [],
|
||||||
|
prompts: [],
|
||||||
createTime: Date.now(),
|
createTime: Date.now(),
|
||||||
enabled: conf.enabled === undefined ? true : conf.enabled,
|
enabled: conf.enabled === undefined ? true : conf.enabled,
|
||||||
};
|
};
|
||||||
@@ -404,7 +413,7 @@ export const initializeClientsFromSettings = async (
|
|||||||
|
|
||||||
// Convert OpenAPI tools to MCP tool format
|
// Convert OpenAPI tools to MCP tool format
|
||||||
const openApiTools = openApiClient.getTools();
|
const openApiTools = openApiClient.getTools();
|
||||||
const mcpTools: ToolInfo[] = openApiTools.map((tool) => ({
|
const mcpTools: Tool[] = openApiTools.map((tool) => ({
|
||||||
name: `${name}-${tool.name}`,
|
name: `${name}-${tool.name}`,
|
||||||
description: tool.description,
|
description: tool.description,
|
||||||
inputSchema: cleanInputSchema(tool.inputSchema),
|
inputSchema: cleanInputSchema(tool.inputSchema),
|
||||||
@@ -469,6 +478,7 @@ export const initializeClientsFromSettings = async (
|
|||||||
status: 'connecting',
|
status: 'connecting',
|
||||||
error: null,
|
error: null,
|
||||||
tools: [],
|
tools: [],
|
||||||
|
prompts: [],
|
||||||
client,
|
client,
|
||||||
transport,
|
transport,
|
||||||
options: requestOptions,
|
options: requestOptions,
|
||||||
@@ -480,32 +490,63 @@ export const initializeClientsFromSettings = async (
|
|||||||
.connect(transport, initRequestOptions || requestOptions)
|
.connect(transport, initRequestOptions || requestOptions)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log(`Successfully connected client for server: ${name}`);
|
console.log(`Successfully connected client for server: ${name}`);
|
||||||
client
|
const capabilities: ServerCapabilities | undefined = client.getServerCapabilities();
|
||||||
.listTools({}, initRequestOptions || requestOptions)
|
console.log(`Server capabilities: ${JSON.stringify(capabilities)}`);
|
||||||
.then((tools) => {
|
|
||||||
console.log(`Successfully listed ${tools.tools.length} tools for server: ${name}`);
|
|
||||||
|
|
||||||
serverInfo.tools = tools.tools.map((tool) => ({
|
let dataError: Error | null = null;
|
||||||
name: `${name}-${tool.name}`,
|
if (capabilities?.tools) {
|
||||||
description: tool.description || '',
|
client
|
||||||
inputSchema: cleanInputSchema(tool.inputSchema || {}),
|
.listTools({}, initRequestOptions || requestOptions)
|
||||||
}));
|
.then((tools) => {
|
||||||
serverInfo.status = 'connected';
|
console.log(`Successfully listed ${tools.tools.length} tools for server: ${name}`);
|
||||||
serverInfo.error = null;
|
serverInfo.tools = tools.tools.map((tool) => ({
|
||||||
|
name: `${name}-${tool.name}`,
|
||||||
|
description: tool.description || '',
|
||||||
|
inputSchema: cleanInputSchema(tool.inputSchema || {}),
|
||||||
|
}));
|
||||||
|
// Save tools as vector embeddings for search
|
||||||
|
saveToolsAsVectorEmbeddings(name, serverInfo.tools);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(
|
||||||
|
`Failed to list tools for server ${name} by error: ${error} with stack: ${error.stack}`,
|
||||||
|
);
|
||||||
|
dataError = error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Set up keep-alive ping for SSE connections
|
if (capabilities?.prompts) {
|
||||||
setupKeepAlive(serverInfo, conf);
|
client
|
||||||
|
.listPrompts({}, initRequestOptions || requestOptions)
|
||||||
|
.then((prompts) => {
|
||||||
|
console.log(
|
||||||
|
`Successfully listed ${prompts.prompts.length} prompts for server: ${name}`,
|
||||||
|
);
|
||||||
|
serverInfo.prompts = prompts.prompts.map((prompt) => ({
|
||||||
|
name: `${name}-${prompt.name}`,
|
||||||
|
title: prompt.title,
|
||||||
|
description: prompt.description,
|
||||||
|
arguments: prompt.arguments,
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(
|
||||||
|
`Failed to list prompts for server ${name} by error: ${error} with stack: ${error.stack}`,
|
||||||
|
);
|
||||||
|
dataError = error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Save tools as vector embeddings for search
|
if (!dataError) {
|
||||||
saveToolsAsVectorEmbeddings(name, serverInfo.tools);
|
serverInfo.status = 'connected';
|
||||||
})
|
serverInfo.error = null;
|
||||||
.catch((error) => {
|
|
||||||
console.error(
|
// Set up keep-alive ping for SSE connections
|
||||||
`Failed to list tools for server ${name} by error: ${error} with stack: ${error.stack}`,
|
setupKeepAlive(serverInfo, conf);
|
||||||
);
|
} else {
|
||||||
serverInfo.status = 'disconnected';
|
serverInfo.status = 'disconnected';
|
||||||
serverInfo.error = `Failed to list tools: ${error.stack} `;
|
serverInfo.error = `Failed to list data: ${dataError} `;
|
||||||
});
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -532,7 +573,7 @@ export const getServersInfo = (): Omit<ServerInfo, 'client' | 'transport'>[] =>
|
|||||||
const filterServerInfos: ServerInfo[] = dataService.filterData
|
const filterServerInfos: ServerInfo[] = dataService.filterData
|
||||||
? dataService.filterData(serverInfos)
|
? dataService.filterData(serverInfos)
|
||||||
: serverInfos;
|
: serverInfos;
|
||||||
const infos = filterServerInfos.map(({ name, status, tools, createTime, error }) => {
|
const infos = filterServerInfos.map(({ name, status, tools, prompts, createTime, error }) => {
|
||||||
const serverConfig = settings.mcpServers[name];
|
const serverConfig = settings.mcpServers[name];
|
||||||
const enabled = serverConfig ? serverConfig.enabled !== false : true;
|
const enabled = serverConfig ? serverConfig.enabled !== false : true;
|
||||||
|
|
||||||
@@ -546,11 +587,21 @@ export const getServersInfo = (): Omit<ServerInfo, 'client' | 'transport'>[] =>
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const promptsWithEnabled = prompts.map((prompt) => {
|
||||||
|
const promptConfig = serverConfig?.prompts?.[prompt.name];
|
||||||
|
return {
|
||||||
|
...prompt,
|
||||||
|
description: promptConfig?.description || prompt.description, // Use custom description if available
|
||||||
|
enabled: promptConfig?.enabled !== false, // Default to true if not explicitly disabled
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
status,
|
status,
|
||||||
error,
|
error,
|
||||||
tools: toolsWithEnabled,
|
tools: toolsWithEnabled,
|
||||||
|
prompts: promptsWithEnabled,
|
||||||
createTime,
|
createTime,
|
||||||
enabled,
|
enabled,
|
||||||
};
|
};
|
||||||
@@ -568,7 +619,7 @@ const getServerByName = (name: string): ServerInfo | undefined => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Filter tools by server configuration
|
// Filter tools by server configuration
|
||||||
const filterToolsByConfig = (serverName: string, tools: ToolInfo[]): ToolInfo[] => {
|
const filterToolsByConfig = (serverName: string, tools: Tool[]): Tool[] => {
|
||||||
const settings = loadSettings();
|
const settings = loadSettings();
|
||||||
const serverConfig = settings.mcpServers[serverName];
|
const serverConfig = settings.mcpServers[serverName];
|
||||||
|
|
||||||
@@ -948,7 +999,7 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
|||||||
if (tool.name) {
|
if (tool.name) {
|
||||||
const serverName = searchResults.find((r) => r.toolName === tool.name)?.serverName;
|
const serverName = searchResults.find((r) => r.toolName === tool.name)?.serverName;
|
||||||
if (serverName) {
|
if (serverName) {
|
||||||
const enabledTools = filterToolsByConfig(serverName, [tool as ToolInfo]);
|
const enabledTools = filterToolsByConfig(serverName, [tool as Tool]);
|
||||||
return enabledTools.length > 0;
|
return enabledTools.length > 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1139,6 +1190,119 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const handleGetPromptRequest = async (request: any, extra: any) => {
|
||||||
|
try {
|
||||||
|
const { name, arguments: promptArgs } = request.params;
|
||||||
|
let server: ServerInfo | undefined;
|
||||||
|
if (extra && extra.server) {
|
||||||
|
server = getServerByName(extra.server);
|
||||||
|
} else {
|
||||||
|
// Find the first server that has this tool
|
||||||
|
server = serverInfos.find(
|
||||||
|
(serverInfo) =>
|
||||||
|
serverInfo.status === 'connected' &&
|
||||||
|
serverInfo.enabled !== false &&
|
||||||
|
serverInfo.prompts.find((prompt) => prompt.name === name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!server) {
|
||||||
|
throw new Error(`Server not found: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove server prefix from prompt name if present
|
||||||
|
const cleanPromptName = name.startsWith(`${server.name}-`)
|
||||||
|
? name.replace(`${server.name}-`, '')
|
||||||
|
: name;
|
||||||
|
|
||||||
|
const promptParams = {
|
||||||
|
name: cleanPromptName || '',
|
||||||
|
arguments: promptArgs,
|
||||||
|
};
|
||||||
|
// Log the final promptParams
|
||||||
|
console.log(`Calling getPrompt with params: ${JSON.stringify(promptParams)}`);
|
||||||
|
const prompt = await server.client?.getPrompt(promptParams);
|
||||||
|
console.log(`Received prompt: ${JSON.stringify(prompt)}`);
|
||||||
|
if (!prompt) {
|
||||||
|
throw new Error(`Prompt not found: ${cleanPromptName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return prompt;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error handling GetPromptRequest: ${error}`);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `Error: ${error}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleListPromptsRequest = async (_: any, extra: any) => {
|
||||||
|
const sessionId = extra.sessionId || '';
|
||||||
|
const group = getGroup(sessionId);
|
||||||
|
console.log(`Handling ListPromptsRequest for group: ${group}`);
|
||||||
|
|
||||||
|
const allServerInfos = getDataService()
|
||||||
|
.filterData(serverInfos)
|
||||||
|
.filter((serverInfo) => {
|
||||||
|
if (serverInfo.enabled === false) return false;
|
||||||
|
if (!group) return true;
|
||||||
|
const serversInGroup = getServersInGroup(group);
|
||||||
|
if (!serversInGroup || serversInGroup.length === 0) return serverInfo.name === group;
|
||||||
|
return serversInGroup.includes(serverInfo.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
const allPrompts: any[] = [];
|
||||||
|
for (const serverInfo of allServerInfos) {
|
||||||
|
if (serverInfo.prompts && serverInfo.prompts.length > 0) {
|
||||||
|
// Filter prompts based on server configuration
|
||||||
|
const settings = loadSettings();
|
||||||
|
const serverConfig = settings.mcpServers[serverInfo.name];
|
||||||
|
|
||||||
|
let enabledPrompts = serverInfo.prompts;
|
||||||
|
if (serverConfig && serverConfig.prompts) {
|
||||||
|
enabledPrompts = serverInfo.prompts.filter((prompt: any) => {
|
||||||
|
const promptConfig = serverConfig.prompts?.[prompt.name];
|
||||||
|
// If prompt is not in config, it's enabled by default
|
||||||
|
return promptConfig?.enabled !== false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is a group request, apply group-level prompt filtering
|
||||||
|
if (group) {
|
||||||
|
const serverConfigInGroup = getServerConfigInGroup(group, serverInfo.name);
|
||||||
|
if (
|
||||||
|
serverConfigInGroup &&
|
||||||
|
serverConfigInGroup.tools !== 'all' &&
|
||||||
|
Array.isArray(serverConfigInGroup.tools)
|
||||||
|
) {
|
||||||
|
// Note: Group config uses 'tools' field but we're filtering prompts here
|
||||||
|
// This might be a design decision to control access at the server level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply custom descriptions from server configuration
|
||||||
|
const promptsWithCustomDescriptions = enabledPrompts.map((prompt: any) => {
|
||||||
|
const promptConfig = serverConfig?.prompts?.[prompt.name];
|
||||||
|
return {
|
||||||
|
...prompt,
|
||||||
|
description: promptConfig?.description || prompt.description, // Use custom description if available
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
allPrompts.push(...promptsWithCustomDescriptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
prompts: allPrompts,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// Create McpServer instance
|
// Create McpServer instance
|
||||||
export const createMcpServer = (name: string, version: string, group?: string): Server => {
|
export const createMcpServer = (name: string, version: string, group?: string): Server => {
|
||||||
// Determine server name based on routing type
|
// Determine server name based on routing type
|
||||||
@@ -1157,8 +1321,13 @@ export const createMcpServer = (name: string, version: string, group?: string):
|
|||||||
}
|
}
|
||||||
// If no group, use default name (global routing)
|
// If no group, use default name (global routing)
|
||||||
|
|
||||||
const server = new Server({ name: serverName, version }, { capabilities: { tools: {} } });
|
const server = new Server(
|
||||||
|
{ name: serverName, version },
|
||||||
|
{ capabilities: { tools: {}, prompts: {}, resources: {} } },
|
||||||
|
);
|
||||||
server.setRequestHandler(ListToolsRequestSchema, handleListToolsRequest);
|
server.setRequestHandler(ListToolsRequestSchema, handleListToolsRequest);
|
||||||
server.setRequestHandler(CallToolRequestSchema, handleCallToolRequest);
|
server.setRequestHandler(CallToolRequestSchema, handleCallToolRequest);
|
||||||
|
server.setRequestHandler(GetPromptRequestSchema, handleGetPromptRequest);
|
||||||
|
server.setRequestHandler(ListPromptsRequestSchema, handleListPromptsRequest);
|
||||||
return server;
|
return server;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getRepositoryFactory } from '../db/index.js';
|
import { getRepositoryFactory } from '../db/index.js';
|
||||||
import { VectorEmbeddingRepository } from '../db/repositories/index.js';
|
import { VectorEmbeddingRepository } from '../db/repositories/index.js';
|
||||||
import { ToolInfo } from '../types/index.js';
|
import { Tool } from '../types/index.js';
|
||||||
import { getAppDataSource, initializeDatabase } from '../db/connection.js';
|
import { getAppDataSource, initializeDatabase } from '../db/connection.js';
|
||||||
import { getSmartRoutingConfig } from '../utils/smartRouting.js';
|
import { getSmartRoutingConfig } from '../utils/smartRouting.js';
|
||||||
import OpenAI from 'openai';
|
import OpenAI from 'openai';
|
||||||
@@ -190,7 +190,7 @@ function generateFallbackEmbedding(text: string): number[] {
|
|||||||
*/
|
*/
|
||||||
export const saveToolsAsVectorEmbeddings = async (
|
export const saveToolsAsVectorEmbeddings = async (
|
||||||
serverName: string,
|
serverName: string,
|
||||||
tools: ToolInfo[],
|
tools: Tool[],
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
if (tools.length === 0) {
|
if (tools.length === 0) {
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ export interface ServerConfig {
|
|||||||
owner?: string; // Owner of the server, defaults to 'admin' user
|
owner?: string; // Owner of the server, defaults to 'admin' user
|
||||||
keepAliveInterval?: number; // Keep-alive ping interval in milliseconds (default: 60000ms for SSE servers)
|
keepAliveInterval?: number; // Keep-alive ping interval in milliseconds (default: 60000ms for SSE servers)
|
||||||
tools?: Record<string, { enabled: boolean; description?: string }>; // Tool-specific configurations with enable/disable state and custom descriptions
|
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?: Partial<Pick<RequestOptions, 'timeout' | 'resetTimeoutOnProgress' | 'maxTotalTimeout'>>; // MCP request options configuration
|
options?: Partial<Pick<RequestOptions, 'timeout' | 'resetTimeoutOnProgress' | 'maxTotalTimeout'>>; // MCP request options configuration
|
||||||
// OpenAPI specific configuration
|
// OpenAPI specific configuration
|
||||||
openapi?: {
|
openapi?: {
|
||||||
@@ -226,7 +227,8 @@ export interface ServerInfo {
|
|||||||
owner?: string; // Owner of the server, defaults to 'admin' user
|
owner?: string; // Owner of the server, defaults to 'admin' user
|
||||||
status: 'connected' | 'connecting' | 'disconnected'; // Current connection status
|
status: 'connected' | 'connecting' | 'disconnected'; // Current connection status
|
||||||
error: string | null; // Error message if any
|
error: string | null; // Error message if any
|
||||||
tools: ToolInfo[]; // List of tools available on the server
|
tools: Tool[]; // List of tools available on the server
|
||||||
|
prompts: Prompt[]; // List of prompts available on the server
|
||||||
client?: Client; // Client instance for communication (MCP clients)
|
client?: Client; // Client instance for communication (MCP clients)
|
||||||
transport?: SSEClientTransport | StdioClientTransport | StreamableHTTPClientTransport; // Transport mechanism used
|
transport?: SSEClientTransport | StdioClientTransport | StreamableHTTPClientTransport; // Transport mechanism used
|
||||||
openApiClient?: any; // OpenAPI client instance for openapi type servers
|
openApiClient?: any; // OpenAPI client instance for openapi type servers
|
||||||
@@ -237,13 +239,27 @@ export interface ServerInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Details about a tool available on the server
|
// Details about a tool available on the server
|
||||||
export interface ToolInfo {
|
export interface Tool {
|
||||||
name: string; // Name of the tool
|
name: string; // Name of the tool
|
||||||
description: string; // Brief description of the tool
|
description: string; // Brief description of the tool
|
||||||
inputSchema: Record<string, unknown>; // Input schema for the tool
|
inputSchema: Record<string, unknown>; // Input schema for the tool
|
||||||
enabled?: boolean; // Whether the tool is enabled (optional, defaults to true)
|
enabled?: boolean; // Whether the tool is enabled (optional, defaults to true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Prompt {
|
||||||
|
name: string; // Name of the prompt
|
||||||
|
title?: string; // Title of the prompt
|
||||||
|
description?: string; // Brief description of the prompt
|
||||||
|
arguments?: PromptArgument[]; // Input schema for the prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PromptArgument {
|
||||||
|
name: string; // Name of the argument
|
||||||
|
title?: string; // Title of the argument
|
||||||
|
description?: string; // Brief description of the argument
|
||||||
|
required?: boolean; // Whether the argument is required
|
||||||
|
}
|
||||||
|
|
||||||
// Standardized API response structure
|
// Standardized API response structure
|
||||||
export interface ApiResponse<T = unknown> {
|
export interface ApiResponse<T = unknown> {
|
||||||
success: boolean; // Indicates if the operation was successful
|
success: boolean; // Indicates if the operation was successful
|
||||||
|
|||||||
Reference in New Issue
Block a user