mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9300814994 | ||
|
|
9952927a13 | ||
|
|
4547ae526a | ||
|
|
80b83bb029 | ||
|
|
fa2de88fea | ||
|
|
6020611f57 | ||
|
|
81c3091a5c | ||
|
|
6a8f246dff | ||
|
|
2bef1fb0bd | ||
|
|
bdb5b37cf5 | ||
|
|
cbb3b15ba2 | ||
|
|
77b423fbcc | ||
|
|
604fe4f71d | ||
|
|
907bca8aac | ||
|
|
8c58200dcc | ||
|
|
0b4dc453a5 | ||
|
|
35012f99fc | ||
|
|
22ad4f83f6 | ||
|
|
26720d9e49 | ||
|
|
a9aa4a9a08 | ||
|
|
48bcf9f5f0 | ||
|
|
f63f06d879 | ||
|
|
63b356b8d7 |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
.git
|
||||
12
Dockerfile
12
Dockerfile
@@ -2,12 +2,6 @@ FROM python:3.13-slim-bookworm AS base
|
||||
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
|
||||
# 添加 HTTP_PROXY 和 HTTPS_PROXY 环境变量
|
||||
ARG HTTP_PROXY=""
|
||||
ARG HTTPS_PROXY=""
|
||||
ENV HTTP_PROXY=$HTTP_PROXY
|
||||
ENV HTTPS_PROXY=$HTTPS_PROXY
|
||||
|
||||
RUN apt-get update && apt-get install -y curl gnupg git \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||
&& apt-get install -y nodejs \
|
||||
@@ -15,12 +9,6 @@ RUN apt-get update && apt-get install -y curl gnupg git \
|
||||
|
||||
RUN npm install -g pnpm
|
||||
|
||||
ARG REQUEST_TIMEOUT=60000
|
||||
ENV REQUEST_TIMEOUT=$REQUEST_TIMEOUT
|
||||
|
||||
ARG BASE_PATH=""
|
||||
ENV BASE_PATH=$BASE_PATH
|
||||
|
||||
ENV PNPM_HOME=/usr/local/share/pnpm
|
||||
ENV PATH=$PNPM_HOME:$PATH
|
||||
RUN mkdir -p $PNPM_HOME && \
|
||||
|
||||
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).
|
||||
@@ -6,6 +6,11 @@ MCPHub makes it easy to manage and scale multiple MCP (Model Context Protocol) s
|
||||
|
||||

|
||||
|
||||
## 🌐 Live Demo & Docs
|
||||
|
||||
- **Documentation**: [docs.mcphubx.com](https://docs.mcphubx.com/)
|
||||
- **Demo Environment**: [demo.mcphubx.com](https://demo.mcphubx.com/)
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
- **Broadened MCP Server Support**: Seamlessly integrate any MCP server with minimal configuration.
|
||||
|
||||
@@ -6,6 +6,11 @@ MCPHub 通过将多个 MCP(Model Context Protocol)服务器组织为灵活
|
||||
|
||||

|
||||
|
||||
## 🌐 在线文档与演示
|
||||
|
||||
- **文档地址**: [docs.mcphubx.com](https://docs.mcphubx.com/)
|
||||
- **演示环境**: [demo.mcphubx.com](https://demo.mcphubx.com/)
|
||||
|
||||
## 🚀 功能亮点
|
||||
|
||||
- **广泛的 MCP 服务器支持**:无缝集成任何 MCP 服务器,配置简单。
|
||||
|
||||
@@ -48,11 +48,11 @@ MCPHub 已内置多个常用 MCP 服务,如高德地图、GitHub、Slack、Fet
|
||||
|
||||

|
||||
|
||||
点击保存后,MCP Hub 将自动重启高德地图的 MCP 服务,使新配置生效。
|
||||
点击保存后,MCPHub 将自动重启高德地图的 MCP 服务,使新配置生效。
|
||||
|
||||
### 配置 MCP Hub SSE
|
||||
### 配置 MCPHub SSE
|
||||
|
||||
MCP Hub 提供了单一聚合的 MCP Server SSE 端点:`http://localhost:3000/sse`,可在任意支持 MCP 的客户端中配置使用。这里我们选择开源的 Cherry Studio 进行演示。
|
||||
MCPHub 提供了单一聚合的 MCP Server SSE 端点:`http://localhost:3000/sse`,可在任意支持 MCP 的客户端中配置使用。这里我们选择开源的 Cherry Studio 进行演示。
|
||||
|
||||

|
||||
|
||||
|
||||
147
docs/api-reference/auth.mdx
Normal file
147
docs/api-reference/auth.mdx
Normal file
@@ -0,0 +1,147 @@
|
||||
---
|
||||
title: "Authentication"
|
||||
description: "Manage users and authentication."
|
||||
---
|
||||
|
||||
import { Card, Cards } from 'mintlify';
|
||||
|
||||
<Card
|
||||
title="POST /api/auth/login"
|
||||
href="#login"
|
||||
>
|
||||
Log in to get a JWT token.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="POST /api/auth/register"
|
||||
href="#register"
|
||||
>
|
||||
Register a new user.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="GET /api/auth/user"
|
||||
href="#get-current-user"
|
||||
>
|
||||
Get the currently authenticated user.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="POST /api/auth/change-password"
|
||||
href="#change-password"
|
||||
>
|
||||
Change the password for the current user.
|
||||
</Card>
|
||||
|
||||
---
|
||||
|
||||
### Login
|
||||
|
||||
Authenticates a user and returns a JWT token along with user details.
|
||||
|
||||
- **Endpoint**: `/api/auth/login`
|
||||
- **Method**: `POST`
|
||||
- **Body**:
|
||||
- `username` (string, required): The user's username.
|
||||
- `password` (string, required): The user's password.
|
||||
- **Request Example**:
|
||||
```json
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "admin123"
|
||||
}
|
||||
```
|
||||
- **Success Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Login successful",
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"user": {
|
||||
"username": "admin",
|
||||
"isAdmin": true,
|
||||
"permissions": { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Register
|
||||
|
||||
Registers a new user and returns a JWT token.
|
||||
|
||||
- **Endpoint**: `/api/auth/register`
|
||||
- **Method**: `POST`
|
||||
- **Body**:
|
||||
- `username` (string, required): The desired username.
|
||||
- `password` (string, required): The desired password (must be at least 6 characters).
|
||||
- `isAdmin` (boolean, optional): Whether the user should have admin privileges.
|
||||
- **Request Example**:
|
||||
```json
|
||||
{
|
||||
"username": "newuser",
|
||||
"password": "password123",
|
||||
"isAdmin": false
|
||||
}
|
||||
```
|
||||
- **Success Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"user": {
|
||||
"username": "newuser",
|
||||
"isAdmin": false,
|
||||
"permissions": { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Current User
|
||||
|
||||
Retrieves the profile of the currently authenticated user.
|
||||
|
||||
- **Endpoint**: `/api/auth/user`
|
||||
- **Method**: `GET`
|
||||
- **Authentication**: Bearer Token required.
|
||||
- **Success Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"user": {
|
||||
"username": "admin",
|
||||
"isAdmin": true,
|
||||
"permissions": { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Change Password
|
||||
|
||||
Allows the authenticated user to change their password.
|
||||
|
||||
- **Endpoint**: `/api/auth/change-password`
|
||||
- **Method**: `POST`
|
||||
- **Authentication**: Bearer Token required.
|
||||
- **Body**:
|
||||
- `currentPassword` (string, required): The user's current password.
|
||||
- `newPassword` (string, required): The desired new password (must be at least 6 characters).
|
||||
- **Request Example**:
|
||||
```json
|
||||
{
|
||||
"currentPassword": "oldpassword",
|
||||
"newPassword": "newpassword123"
|
||||
}
|
||||
```
|
||||
- **Success Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Password updated successfully"
|
||||
}
|
||||
```
|
||||
111
docs/api-reference/config.mdx
Normal file
111
docs/api-reference/config.mdx
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: "Config"
|
||||
description: "Manage and retrieve system-wide configurations."
|
||||
---
|
||||
|
||||
import { Card, Cards } from 'mintlify';
|
||||
|
||||
<Card title="PUT /api/system-config" href="#update-system-config">Update the main system configuration.</Card>
|
||||
<Card title="GET /api/settings" href="#get-all-settings">Get all system settings, including servers and groups.</Card>
|
||||
<Card title="GET /config" href="#get-runtime-config">Get public runtime configuration for the frontend.</Card>
|
||||
<Card title="GET /public-config" href="#get-public-config">Get public configuration to check for auth skip.</Card>
|
||||
|
||||
---
|
||||
|
||||
### Update System Config
|
||||
|
||||
Updates various parts of the system configuration. You only need to provide the keys for the sections you want to update.
|
||||
|
||||
- **Endpoint**: `/api/system-config`
|
||||
- **Method**: `PUT`
|
||||
- **Body**: A JSON object containing one or more of the following top-level keys: `routing`, `install`, `smartRouting`, `mcpRouter`.
|
||||
|
||||
#### Routing Configuration (`routing`)
|
||||
|
||||
- `enableGlobalRoute` (boolean): Enable or disable the global `/api/mcp` route.
|
||||
- `enableGroupNameRoute` (boolean): Enable or disable group-based routing (e.g., `/api/mcp/group/:groupName`).
|
||||
- `enableBearerAuth` (boolean): Enable bearer token authentication for MCP routes.
|
||||
- `bearerAuthKey` (string): The secret key to use for bearer authentication.
|
||||
- `skipAuth` (boolean): If true, skips all authentication, making the instance public.
|
||||
|
||||
#### Install Configuration (`install`)
|
||||
|
||||
- `pythonIndexUrl` (string): The base URL of the Python Package Index (PyPI) to use for installations.
|
||||
- `npmRegistry` (string): The URL of the npm registry to use for installations.
|
||||
- `baseUrl` (string): The public base URL of this MCPHub instance.
|
||||
|
||||
#### Smart Routing Configuration (`smartRouting`)
|
||||
|
||||
- `enabled` (boolean): Enable or disable the Smart Routing feature.
|
||||
- `dbUrl` (string): The database connection URL for storing embeddings.
|
||||
- `openaiApiBaseUrl` (string): The base URL for the OpenAI-compatible API for generating embeddings.
|
||||
- `openaiApiKey` (string): The API key for the embeddings service.
|
||||
- `openaiApiEmbeddingModel` (string): The name of the embedding model to use.
|
||||
|
||||
#### MCP Router Configuration (`mcpRouter`)
|
||||
|
||||
- `apiKey` (string): The API key for the MCP Router service.
|
||||
- `referer` (string): The referer header to use for MCP Router requests.
|
||||
- `title` (string): The title to display for this instance on MCP Router.
|
||||
- `baseUrl` (string): The base URL for the MCP Router API.
|
||||
|
||||
- **Request Example**:
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"skipAuth": true
|
||||
},
|
||||
"smartRouting": {
|
||||
"enabled": true,
|
||||
"dbUrl": "postgresql://user:pass@host:port/db"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get All Settings
|
||||
|
||||
Retrieves the entire settings object for the instance, including all server configurations, groups, and system settings. This is a comprehensive dump of the `mcp_settings.json` file.
|
||||
|
||||
- **Endpoint**: `/api/settings`
|
||||
- **Method**: `GET`
|
||||
|
||||
---
|
||||
|
||||
### Get Runtime Config
|
||||
|
||||
Retrieves the essential runtime configuration required for the frontend application. This endpoint does not require authentication.
|
||||
|
||||
- **Endpoint**: `/config`
|
||||
- **Method**: `GET`
|
||||
- **Success Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"basePath": "",
|
||||
"version": "1.0.0",
|
||||
"name": "MCPHub"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Public Config
|
||||
|
||||
Retrieves public configuration, primarily to check if authentication is skipped. This allows the frontend to adjust its behavior accordingly before a user has logged in. This endpoint does not require authentication.
|
||||
|
||||
- **Endpoint**: `/public-config`
|
||||
- **Method**: `GET`
|
||||
- **Success Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"skipAuth": false,
|
||||
"permissions": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
title: 'Create Plant'
|
||||
openapi: 'POST /plants'
|
||||
---
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
title: 'Delete Plant'
|
||||
openapi: 'DELETE /plants/{id}'
|
||||
---
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
title: 'Get Plants'
|
||||
openapi: 'GET /plants'
|
||||
---
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
title: 'New Plant'
|
||||
openapi: 'WEBHOOK /plant/webhook'
|
||||
---
|
||||
212
docs/api-reference/groups.mdx
Normal file
212
docs/api-reference/groups.mdx
Normal file
@@ -0,0 +1,212 @@
|
||||
---
|
||||
title: "Groups"
|
||||
description: "Manage server groups to organize and route requests."
|
||||
---
|
||||
|
||||
import { Card, Cards } from 'mintlify';
|
||||
|
||||
<Card title="GET /api/groups" href="#get-all-groups">Get a list of all groups.</Card>
|
||||
<Card title="POST /api/groups" href="#create-a-new-group">Create a new group.</Card>
|
||||
<Card title="GET /api/groups/:id" href="#get-a-group">Get details of a specific group.</Card>
|
||||
<Card title="PUT /api/groups/:id" href="#update-a-group">Update an existing group.</Card>
|
||||
<Card title="DELETE /api/groups/:id" href="#delete-a-group">Delete a group.</Card>
|
||||
<Card title="POST /api/groups/:id/servers" href="#add-server-to-group">Add a server to a group.</Card>
|
||||
<Card title="DELETE /api/groups/:id/servers/:serverName" href="#remove-server-from-group">Remove a server from a group.</Card>
|
||||
<Card title="PUT /api/groups/:id/servers/batch" href="#batch-update-group-servers">Batch update servers in a group.</Card>
|
||||
<Card title="GET /api/groups/:id/server-configs" href="#get-group-server-configs">Get detailed server configurations in a group.</Card>
|
||||
<Card title="PUT /api/groups/:id/server-configs/:serverName/tools" href="#update-group-server-tools">Update tool selection for a server in a group.</Card>
|
||||
|
||||
---
|
||||
|
||||
### Get All Groups
|
||||
|
||||
Retrieves a list of all server groups.
|
||||
|
||||
- **Endpoint**: `/api/groups`
|
||||
- **Method**: `GET`
|
||||
- **Success Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": "group-1",
|
||||
"name": "My Group",
|
||||
"description": "A collection of servers.",
|
||||
"servers": ["server1", "server2"],
|
||||
"owner": "admin"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Create a New Group
|
||||
|
||||
Creates a new server group.
|
||||
|
||||
- **Endpoint**: `/api/groups`
|
||||
- **Method**: `POST`
|
||||
- **Body**:
|
||||
- `name` (string, required): The name of the group.
|
||||
- `description` (string, optional): A description for the group.
|
||||
- `servers` (array of strings, optional): A list of server names to include in the group.
|
||||
- **Request Example**:
|
||||
```json
|
||||
{
|
||||
"name": "My New Group",
|
||||
"description": "A description for the new group",
|
||||
"servers": ["server1", "server2"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get a Group
|
||||
|
||||
Retrieves details for a specific group by its ID or name.
|
||||
|
||||
- **Endpoint**: `/api/groups/:id`
|
||||
- **Method**: `GET`
|
||||
- **Parameters**:
|
||||
- `:id` (string, required): The ID or name of the group.
|
||||
|
||||
---
|
||||
|
||||
### Update a Group
|
||||
|
||||
Updates an existing group's name, description, or server list.
|
||||
|
||||
- **Endpoint**: `/api/groups/:id`
|
||||
- **Method**: `PUT`
|
||||
- **Parameters**:
|
||||
- `:id` (string, required): The ID or name of the group to update.
|
||||
- **Body**:
|
||||
- `name` (string, optional): The new name for the group.
|
||||
- `description` (string, optional): The new description for the group.
|
||||
- `servers` (array, optional): The new list of servers for the group. See [Batch Update Group Servers](#batch-update-group-servers) for format.
|
||||
- **Request Example**:
|
||||
```json
|
||||
{
|
||||
"name": "Updated Group Name",
|
||||
"description": "Updated description"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Delete a Group
|
||||
|
||||
Deletes a group by its ID or name.
|
||||
|
||||
- **Endpoint**: `/api/groups/:id`
|
||||
- **Method**: `DELETE`
|
||||
- **Parameters**:
|
||||
- `:id` (string, required): The ID or name of the group to delete.
|
||||
|
||||
---
|
||||
|
||||
### Add Server to Group
|
||||
|
||||
Adds a single server to a group.
|
||||
|
||||
- **Endpoint**: `/api/groups/:id/servers`
|
||||
- **Method**: `POST`
|
||||
- **Parameters**:
|
||||
- `:id` (string, required): The ID or name of the group.
|
||||
- **Body**:
|
||||
- `serverName` (string, required): The name of the server to add.
|
||||
- **Request Example**:
|
||||
```json
|
||||
{
|
||||
"serverName": "my-server"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Remove Server from Group
|
||||
|
||||
Removes a single server from a group.
|
||||
|
||||
- **Endpoint**: `/api/groups/:id/servers/:serverName`
|
||||
- **Method**: `DELETE`
|
||||
- **Parameters**:
|
||||
- `:id` (string, required): The ID or name of the group.
|
||||
- `:serverName` (string, required): The name of the server to remove.
|
||||
|
||||
---
|
||||
|
||||
### Batch Update Group Servers
|
||||
|
||||
Replaces all servers in a group with a new list. The list can be simple strings or detailed configuration objects.
|
||||
|
||||
- **Endpoint**: `/api/groups/:id/servers/batch`
|
||||
- **Method**: `PUT`
|
||||
- **Parameters**:
|
||||
- `:id` (string, required): The ID or name of the group.
|
||||
- **Body**:
|
||||
- `servers` (array, required): An array of server names (strings) or server configuration objects.
|
||||
- **Request Example (Simple)**:
|
||||
```json
|
||||
{
|
||||
"servers": ["server1", "server2"]
|
||||
}
|
||||
```
|
||||
- **Request Example (Detailed)**:
|
||||
```json
|
||||
{
|
||||
"servers": [
|
||||
{ "name": "server1", "tools": "all" },
|
||||
{ "name": "server2", "tools": ["toolA", "toolB"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Group Server Configs
|
||||
|
||||
Retrieves the detailed configuration for all servers within a group, including which tools are enabled.
|
||||
|
||||
- **Endpoint**: `/api/groups/:id/server-configs`
|
||||
- **Method**: `GET`
|
||||
- **Parameters**:
|
||||
- `:id` (string, required): The ID or name of the group.
|
||||
- **Success Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"name": "server1",
|
||||
"tools": "all"
|
||||
},
|
||||
{
|
||||
"name": "server2",
|
||||
"tools": ["toolA", "toolB"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Update Group Server Tools
|
||||
|
||||
Updates the tool selection for a specific server within a group.
|
||||
|
||||
- **Endpoint**: `/api/groups/:id/server-configs/:serverName/tools`
|
||||
- **Method**: `PUT`
|
||||
- **Parameters**:
|
||||
- `:id` (string, required): The ID or name of the group.
|
||||
- `:serverName` (string, required): The name of the server to update.
|
||||
- **Body**:
|
||||
- `tools` (string or array of strings, required): Either the string `"all"` to enable all tools, or an array of tool names to enable specifically.
|
||||
- **Request Example**:
|
||||
```json
|
||||
{
|
||||
"tools": ["toolA", "toolC"]
|
||||
}
|
||||
```
|
||||
@@ -1,33 +1,13 @@
|
||||
---
|
||||
title: 'Introduction'
|
||||
description: 'Example section for showcasing API endpoints'
|
||||
title: "Introduction"
|
||||
description: "Welcome to the MCPHub API documentation."
|
||||
---
|
||||
|
||||
<Note>
|
||||
If you're not looking to build API reference documentation, you can delete
|
||||
this section by removing the api-reference folder.
|
||||
</Note>
|
||||
The MCPHub API provides a comprehensive set of endpoints to manage your MCP servers, groups, users, and more. The API is divided into two main categories:
|
||||
|
||||
## Welcome
|
||||
- **MCP Endpoints**: These are the primary endpoints for interacting with your MCP servers. They provide a unified interface for sending requests to your servers and receiving responses in real-time.
|
||||
- **Management API**: These endpoints are used for managing the MCPHub instance itself. This includes managing servers, groups, users, and system settings.
|
||||
|
||||
There are two ways to build API documentation: [OpenAPI](https://mintlify.com/docs/api-playground/openapi/setup) and [MDX components](https://mintlify.com/docs/api-playground/mdx/configuration). For the starter kit, we are using the following OpenAPI specification.
|
||||
All API endpoints are available under the `/api` path. For example, the endpoint to get all servers is `/api/servers`.
|
||||
|
||||
<Card
|
||||
title="Plant Store Endpoints"
|
||||
icon="leaf"
|
||||
href="https://github.com/mintlify/starter/blob/main/api-reference/openapi.json"
|
||||
>
|
||||
View the OpenAPI specification file
|
||||
</Card>
|
||||
|
||||
## Authentication
|
||||
|
||||
All API endpoints are authenticated using Bearer tokens and picked up from the specification file.
|
||||
|
||||
```json
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
```
|
||||
Authentication is required for most Management API endpoints. See the [Authentication](/api-reference/auth) section for more details.
|
||||
81
docs/api-reference/logs.mdx
Normal file
81
docs/api-reference/logs.mdx
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: "Logs"
|
||||
description: "Access and manage server logs."
|
||||
---
|
||||
|
||||
import { Card, Cards } from 'mintlify';
|
||||
|
||||
<Card
|
||||
title="GET /api/logs"
|
||||
href="#get-all-logs"
|
||||
>
|
||||
Get all logs.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="DELETE /api/logs"
|
||||
href="#clear-logs"
|
||||
>
|
||||
Clear all logs.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="GET /api/logs/stream"
|
||||
href="#stream-logs"
|
||||
>
|
||||
Stream logs in real-time.
|
||||
</Card>
|
||||
|
||||
---
|
||||
|
||||
### Get All Logs
|
||||
|
||||
Retrieves all stored logs.
|
||||
|
||||
- **Endpoint**: `/api/logs`
|
||||
- **Method**: `GET`
|
||||
- **Success Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"timestamp": "2023-10-27T10:00:00.000Z",
|
||||
"level": "info",
|
||||
"message": "Server started successfully.",
|
||||
"service": "system"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Clear Logs
|
||||
|
||||
Deletes all stored logs.
|
||||
|
||||
- **Endpoint**: `/api/logs`
|
||||
- **Method**: `DELETE`
|
||||
- **Success Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Logs cleared successfully"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Stream Logs
|
||||
|
||||
Streams logs in real-time using Server-Sent Events (SSE). The connection will remain open, and new log entries will be sent as they occur.
|
||||
|
||||
- **Endpoint**: `/api/logs/stream`
|
||||
- **Method**: `GET`
|
||||
- **Response Format**: The stream sends events with a `data` field containing a JSON object. The first event has `type: 'initial'` and contains all historical logs. Subsequent events have `type: 'log'` and contain a single new log entry.
|
||||
|
||||
- **Example Event**:
|
||||
```
|
||||
data: {"type":"log","log":{"timestamp":"2023-10-27T10:00:05.000Z","level":"debug","message":"Processing request for /api/some-endpoint","service":"mcp-server"}}
|
||||
```
|
||||
33
docs/api-reference/mcp-http.mdx
Normal file
33
docs/api-reference/mcp-http.mdx
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: "MCP HTTP Endpoints"
|
||||
description: "Connect to your MCP servers using the unified HTTP endpoint."
|
||||
---
|
||||
|
||||
MCPHub provides a unified streamable HTTP interface for all your MCP servers. This allows you to send requests to any configured MCP server and receive responses in real-time.
|
||||
|
||||
### Unified Endpoint
|
||||
|
||||
This endpoint provides access to all enabled MCP servers.
|
||||
|
||||
- **Endpoint**: `http://localhost:3000/mcp`
|
||||
- **Method**: `POST`
|
||||
|
||||
### Group-Specific Endpoint
|
||||
|
||||
For targeted access to specific server groups, use the group-based HTTP endpoint.
|
||||
|
||||
- **Endpoint**: `http://localhost:3000/mcp/{group}`
|
||||
- **Method**: `POST`
|
||||
- **Parameters**:
|
||||
- `{group}`: The ID or name of the group.
|
||||
|
||||
### Server-Specific Endpoint
|
||||
|
||||
For direct access to individual servers, use the server-specific HTTP endpoint.
|
||||
|
||||
- **Endpoint**: `http://localhost:3000/mcp/{server}`
|
||||
- **Method**: `POST`
|
||||
- **Parameters**:
|
||||
- `{server}`: The name of the server.
|
||||
|
||||
> **Note**: If a server name and group name are the same, the group will take precedence.
|
||||
25
docs/api-reference/mcp-sse.mdx
Normal file
25
docs/api-reference/mcp-sse.mdx
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: "MCP SSE Endpoints (Deprecated)"
|
||||
description: "Connect to your MCP servers using the SSE endpoint."
|
||||
---
|
||||
|
||||
The SSE endpoint is deprecated and will be removed in a future version. Please use the [MCP HTTP Endpoints](/api-reference/mcp-http) instead.
|
||||
|
||||
### Unified Endpoint
|
||||
|
||||
- **Endpoint**: `http://localhost:3000/sse`
|
||||
- **Method**: `GET`
|
||||
|
||||
### Group-Specific Endpoint
|
||||
|
||||
- **Endpoint**: `http://localhost:3000/sse/{group}`
|
||||
- **Method**: `GET`
|
||||
- **Parameters**:
|
||||
- `{group}`: The ID or name of the group.
|
||||
|
||||
### Server-Specific Endpoint
|
||||
|
||||
- **Endpoint**: `http://localhost:3000/sse/{server}`
|
||||
- **Method**: `GET`
|
||||
- **Parameters**:
|
||||
- `{server}`: The name of the server.
|
||||
209
docs/api-reference/servers.mdx
Normal file
209
docs/api-reference/servers.mdx
Normal file
@@ -0,0 +1,209 @@
|
||||
---
|
||||
title: "Servers"
|
||||
description: "Manage your MCP servers."
|
||||
---
|
||||
|
||||
import { Card, Cards } from 'mintlify';
|
||||
|
||||
<Card
|
||||
title="GET /api/servers"
|
||||
href="#get-all-servers"
|
||||
>
|
||||
Get a list of all MCP servers.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="POST /api/servers"
|
||||
href="#create-a-new-server"
|
||||
>
|
||||
Create a new MCP server.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="PUT /api/servers/:name"
|
||||
href="#update-a-server"
|
||||
>
|
||||
Update an existing MCP server.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="DELETE /api/servers/:name"
|
||||
href="#delete-a-server"
|
||||
>
|
||||
Delete an MCP server.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="POST /api/servers/:name/toggle"
|
||||
href="#toggle-a-server"
|
||||
>
|
||||
Toggle the enabled state of a server.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="POST /api/servers/:serverName/tools/:toolName/toggle"
|
||||
href="#toggle-a-tool"
|
||||
>
|
||||
Toggle the enabled state of a tool.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="PUT /api/servers/:serverName/tools/:toolName/description"
|
||||
href="#update-tool-description"
|
||||
>
|
||||
Update the description of a tool.
|
||||
</Card>
|
||||
|
||||
---
|
||||
|
||||
### Get All Servers
|
||||
|
||||
Retrieves a list of all configured MCP servers, including their status and available tools.
|
||||
|
||||
- **Endpoint**: `/api/servers`
|
||||
- **Method**: `GET`
|
||||
- **Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"name": "example-server",
|
||||
"status": "connected",
|
||||
"tools": [
|
||||
{
|
||||
"name": "tool1",
|
||||
"description": "Description of tool 1"
|
||||
}
|
||||
],
|
||||
"config": {
|
||||
"type": "stdio",
|
||||
"command": "node",
|
||||
"args": ["server.js"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Create a New Server
|
||||
|
||||
Adds a new MCP server to the configuration.
|
||||
|
||||
- **Endpoint**: `/api/servers`
|
||||
- **Method**: `POST`
|
||||
- **Body**:
|
||||
```json
|
||||
{
|
||||
"name": "my-new-server",
|
||||
"config": {
|
||||
"type": "stdio",
|
||||
"command": "python",
|
||||
"args": ["-u", "my_script.py"],
|
||||
"owner": "admin"
|
||||
}
|
||||
}
|
||||
```
|
||||
- `name` (string, required): The unique name for the server.
|
||||
- `config` (object, required): The server configuration object.
|
||||
- `type` (string): `stdio`, `sse`, `streamable-http`, or `openapi`.
|
||||
- `command` (string): Command to execute for `stdio` type.
|
||||
- `args` (array of strings): Arguments for the command.
|
||||
- `url` (string): URL for `sse`, `streamable-http`, or `openapi` types.
|
||||
- `openapi` (object): OpenAPI configuration.
|
||||
- `url` (string): URL to the OpenAPI schema.
|
||||
- `schema` (object): The OpenAPI schema object itself.
|
||||
- `headers` (object): Headers to send with requests for `sse`, `streamable-http`, and `openapi` types.
|
||||
- `keepAliveInterval` (number): Keep-alive interval in milliseconds for `sse` type. Defaults to 60000.
|
||||
- `owner` (string): The owner of the server. Defaults to the current user or 'admin'.
|
||||
|
||||
---
|
||||
|
||||
### Update a Server
|
||||
|
||||
Updates the configuration of an existing MCP server.
|
||||
|
||||
- **Endpoint**: `/api/servers/:name`
|
||||
- **Method**: `PUT`
|
||||
- **Parameters**:
|
||||
- `:name` (string, required): The name of the server to update.
|
||||
- **Body**:
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"type": "stdio",
|
||||
"command": "node",
|
||||
"args": ["new_server.js"]
|
||||
}
|
||||
}
|
||||
```
|
||||
- `config` (object, required): The updated server configuration object. See "Create a New Server" for details.
|
||||
|
||||
---
|
||||
|
||||
### Delete a Server
|
||||
|
||||
Removes an MCP server from the configuration.
|
||||
|
||||
- **Endpoint**: `/api/servers/:name`
|
||||
- **Method**: `DELETE`
|
||||
- **Parameters**:
|
||||
- `:name` (string, required): The name of the server to delete.
|
||||
|
||||
---
|
||||
|
||||
### Toggle a Server
|
||||
|
||||
Enables or disables an MCP server.
|
||||
|
||||
- **Endpoint**: `/api/servers/:name/toggle`
|
||||
- **Method**: `POST`
|
||||
- **Parameters**:
|
||||
- `:name` (string, required): The name of the server to toggle.
|
||||
- **Body**:
|
||||
```json
|
||||
{
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
- `enabled` (boolean, required): `true` to enable the server, `false` to disable it.
|
||||
|
||||
---
|
||||
|
||||
### Toggle a Tool
|
||||
|
||||
Enables or disables a specific tool on a server.
|
||||
|
||||
- **Endpoint**: `/api/servers/:serverName/tools/:toolName/toggle`
|
||||
- **Method**: `POST`
|
||||
- **Parameters**:
|
||||
- `:serverName` (string, required): The name of the server.
|
||||
- `:toolName` (string, required): The name of the tool.
|
||||
- **Body**:
|
||||
```json
|
||||
{
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
- `enabled` (boolean, required): `true` to enable the tool, `false` to disable it.
|
||||
|
||||
---
|
||||
|
||||
### Update Tool Description
|
||||
|
||||
Updates the description of a specific tool.
|
||||
|
||||
- **Endpoint**: `/api/servers/:serverName/tools/:toolName/description`
|
||||
- **Method**: `PUT`
|
||||
- **Parameters**:
|
||||
- `:serverName` (string, required): The name of the server.
|
||||
- `:toolName` (string, required): The name of the tool.
|
||||
- **Body**:
|
||||
```json
|
||||
{
|
||||
"description": "New tool description"
|
||||
}
|
||||
```
|
||||
- `description` (string, required): The new description for the tool.
|
||||
29
docs/api-reference/smart-routing.mdx
Normal file
29
docs/api-reference/smart-routing.mdx
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: "Smart Routing"
|
||||
description: "Intelligent tool discovery using vector semantic search."
|
||||
---
|
||||
|
||||
Smart Routing is MCPHub's intelligent tool discovery system that uses vector semantic search to automatically find the most relevant tools for any given task.
|
||||
|
||||
### HTTP Endpoint
|
||||
|
||||
- **Endpoint**: `http://localhost:3000/mcp/$smart`
|
||||
- **Method**: `POST`
|
||||
|
||||
### SSE Endpoint (Deprecated)
|
||||
|
||||
- **Endpoint**: `http://localhost:3000/sse/$smart`
|
||||
- **Method**: `GET`
|
||||
|
||||
### How it Works
|
||||
|
||||
1. **Tool Indexing**: All MCP tools are automatically converted to vector embeddings and stored in PostgreSQL with pgvector.
|
||||
2. **Semantic Search**: User queries are converted to vectors and matched against tool embeddings using cosine similarity.
|
||||
3. **Intelligent Filtering**: Dynamic thresholds ensure relevant results without noise.
|
||||
4. **Precise Execution**: Found tools can be directly executed with proper parameter validation.
|
||||
|
||||
### Setup Requirements
|
||||
|
||||
- PostgreSQL with pgvector extension
|
||||
- OpenAI API key (or compatible embedding service)
|
||||
- Enable Smart Routing in MCPHub settings
|
||||
@@ -11,261 +11,34 @@ MCPHub uses environment variables for configuration. This guide covers all avail
|
||||
|
||||
### Server Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ----------- | ------------- | ------------------------------------------------------------- |
|
||||
| `PORT` | `3000` | Port number for the HTTP server |
|
||||
| `HOST` | `0.0.0.0` | Host address to bind the server |
|
||||
| `NODE_ENV` | `development` | Application environment (`development`, `production`, `test`) |
|
||||
| `LOG_LEVEL` | `info` | Logging level (`error`, `warn`, `info`, `debug`) |
|
||||
| Variable | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `PORT` | `3000` | Port number for the HTTP server |
|
||||
| `INIT_TIMEOUT` | `300000` | Initial timeout for the application |
|
||||
| `BASE_PATH` | `''` | The base path of the application |
|
||||
| `READONLY` | `false` | Set to `true` to enable readonly mode |
|
||||
| `MCPHUB_SETTING_PATH` | | Path to the MCPHub settings |
|
||||
| `NODE_ENV` | `development` | Application environment (`development`, `production`, `test`) |
|
||||
|
||||
```env
|
||||
PORT=3000
|
||||
HOST=0.0.0.0
|
||||
INIT_TIMEOUT=300000
|
||||
BASE_PATH=/api
|
||||
READONLY=true
|
||||
MCPHUB_SETTING_PATH=/path/to/settings
|
||||
NODE_ENV=production
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
### Database Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
| -------------- | ----------- | ---------------------------------- |
|
||||
| `DATABASE_URL` | - | PostgreSQL connection string |
|
||||
| `DB_HOST` | `localhost` | Database host |
|
||||
| `DB_PORT` | `5432` | Database port |
|
||||
| `DB_NAME` | `mcphub` | Database name |
|
||||
| `DB_USER` | `mcphub` | Database username |
|
||||
| `DB_PASSWORD` | - | Database password |
|
||||
| `DB_SSL` | `false` | Enable SSL for database connection |
|
||||
| `DB_POOL_MIN` | `2` | Minimum database pool size |
|
||||
| `DB_POOL_MAX` | `10` | Maximum database pool size |
|
||||
|
||||
```env
|
||||
# Option 1: Full connection string
|
||||
DATABASE_URL=postgresql://username:password@localhost:5432/mcphub
|
||||
|
||||
# Option 2: Individual components
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=mcphub
|
||||
DB_USER=mcphub
|
||||
DB_PASSWORD=your-password
|
||||
DB_SSL=false
|
||||
```
|
||||
|
||||
## Authentication & Security
|
||||
|
||||
### JWT Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------ | ------- | ------------------------------------------- |
|
||||
| `JWT_SECRET` | - | Secret key for JWT token signing (required) |
|
||||
| `JWT_EXPIRES_IN` | `24h` | JWT token expiration time |
|
||||
| `JWT_REFRESH_EXPIRES_IN` | `7d` | Refresh token expiration time |
|
||||
| `JWT_ALGORITHM` | `HS256` | JWT signing algorithm |
|
||||
| Variable | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `JWT_SECRET` | - | Secret key for JWT token signing (required) |
|
||||
|
||||
```env
|
||||
JWT_SECRET=your-super-secret-key-change-this-in-production
|
||||
JWT_EXPIRES_IN=24h
|
||||
JWT_REFRESH_EXPIRES_IN=7d
|
||||
```
|
||||
|
||||
### Session & Security
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------- | ------- | ------------------------------- |
|
||||
| `SESSION_SECRET` | - | Session encryption secret |
|
||||
| `BCRYPT_ROUNDS` | `12` | bcrypt hashing rounds |
|
||||
| `RATE_LIMIT_WINDOW` | `15` | Rate limiting window in minutes |
|
||||
| `RATE_LIMIT_MAX` | `100` | Maximum requests per window |
|
||||
| `CORS_ORIGIN` | `*` | Allowed CORS origins |
|
||||
|
||||
```env
|
||||
SESSION_SECRET=your-session-secret
|
||||
BCRYPT_ROUNDS=12
|
||||
RATE_LIMIT_WINDOW=15
|
||||
RATE_LIMIT_MAX=100
|
||||
CORS_ORIGIN=https://your-domain.com,https://admin.your-domain.com
|
||||
```
|
||||
|
||||
## External Services
|
||||
|
||||
### OpenAI Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------ | ------------------------ | -------------------------------- |
|
||||
| `OPENAI_API_KEY` | - | OpenAI API key for smart routing |
|
||||
| `OPENAI_MODEL` | `gpt-3.5-turbo` | OpenAI model for embeddings |
|
||||
| `OPENAI_EMBEDDING_MODEL` | `text-embedding-ada-002` | Model for vector embeddings |
|
||||
| `OPENAI_MAX_TOKENS` | `1000` | Maximum tokens per request |
|
||||
| `OPENAI_TEMPERATURE` | `0.1` | Temperature for AI responses |
|
||||
|
||||
```env
|
||||
OPENAI_API_KEY=sk-your-openai-api-key
|
||||
OPENAI_MODEL=gpt-3.5-turbo
|
||||
OPENAI_EMBEDDING_MODEL=text-embedding-ada-002
|
||||
OPENAI_MAX_TOKENS=1000
|
||||
OPENAI_TEMPERATURE=0.1
|
||||
```
|
||||
|
||||
### Redis Configuration (Optional)
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ---------------- | ----------- | ----------------------- |
|
||||
| `REDIS_URL` | - | Redis connection string |
|
||||
| `REDIS_HOST` | `localhost` | Redis host |
|
||||
| `REDIS_PORT` | `6379` | Redis port |
|
||||
| `REDIS_PASSWORD` | - | Redis password |
|
||||
| `REDIS_DB` | `0` | Redis database number |
|
||||
| `REDIS_PREFIX` | `mcphub:` | Key prefix for Redis |
|
||||
|
||||
```env
|
||||
# Option 1: Full connection string
|
||||
REDIS_URL=redis://username:password@localhost:6379/0
|
||||
|
||||
# Option 2: Individual components
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=your-redis-password
|
||||
REDIS_DB=0
|
||||
REDIS_PREFIX=mcphub:
|
||||
```
|
||||
|
||||
## MCP Server Configuration
|
||||
|
||||
### Default Settings
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------- | ------------------- | -------------------------------------------- |
|
||||
| `MCP_SETTINGS_FILE` | `mcp_settings.json` | Path to MCP settings file |
|
||||
| `MCP_SERVERS_FILE` | `servers.json` | Path to servers configuration |
|
||||
| `MCP_TIMEOUT` | `30000` | Default timeout for MCP operations (ms) |
|
||||
| `MCP_MAX_RETRIES` | `3` | Maximum retry attempts for failed operations |
|
||||
| `MCP_RESTART_DELAY` | `5000` | Delay before restarting failed servers (ms) |
|
||||
|
||||
```env
|
||||
MCP_SETTINGS_FILE=./config/mcp_settings.json
|
||||
MCP_SERVERS_FILE=./config/servers.json
|
||||
MCP_TIMEOUT=30000
|
||||
MCP_MAX_RETRIES=3
|
||||
MCP_RESTART_DELAY=5000
|
||||
```
|
||||
|
||||
### Smart Routing
|
||||
|
||||
| Variable | Default | Description |
|
||||
| --------------------------- | ------- | -------------------------------- |
|
||||
| `SMART_ROUTING_ENABLED` | `true` | Enable AI-powered smart routing |
|
||||
| `SMART_ROUTING_THRESHOLD` | `0.7` | Similarity threshold for routing |
|
||||
| `SMART_ROUTING_MAX_RESULTS` | `5` | Maximum tools to return |
|
||||
| `VECTOR_CACHE_TTL` | `3600` | Vector cache TTL in seconds |
|
||||
|
||||
```env
|
||||
SMART_ROUTING_ENABLED=true
|
||||
SMART_ROUTING_THRESHOLD=0.7
|
||||
SMART_ROUTING_MAX_RESULTS=5
|
||||
VECTOR_CACHE_TTL=3600
|
||||
```
|
||||
|
||||
## File Storage & Uploads
|
||||
|
||||
| Variable | Default | Description |
|
||||
| -------------------- | ---------------- | ----------------------------------- |
|
||||
| `UPLOAD_DIR` | `./uploads` | Directory for file uploads |
|
||||
| `MAX_FILE_SIZE` | `10485760` | Maximum file size in bytes (10MB) |
|
||||
| `ALLOWED_FILE_TYPES` | `image/*,text/*` | Allowed MIME types |
|
||||
| `STORAGE_TYPE` | `local` | Storage type (`local`, `s3`, `gcs`) |
|
||||
|
||||
```env
|
||||
UPLOAD_DIR=./data/uploads
|
||||
MAX_FILE_SIZE=10485760
|
||||
ALLOWED_FILE_TYPES=image/*,text/*,application/json
|
||||
STORAGE_TYPE=local
|
||||
```
|
||||
|
||||
### S3 Storage (Optional)
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ---------------------- | ----------- | ------------------ |
|
||||
| `S3_BUCKET` | - | S3 bucket name |
|
||||
| `S3_REGION` | `us-east-1` | S3 region |
|
||||
| `S3_ACCESS_KEY_ID` | - | S3 access key |
|
||||
| `S3_SECRET_ACCESS_KEY` | - | S3 secret key |
|
||||
| `S3_ENDPOINT` | - | Custom S3 endpoint |
|
||||
|
||||
```env
|
||||
S3_BUCKET=mcphub-uploads
|
||||
S3_REGION=us-east-1
|
||||
S3_ACCESS_KEY_ID=your-access-key
|
||||
S3_SECRET_ACCESS_KEY=your-secret-key
|
||||
```
|
||||
|
||||
## Monitoring & Logging
|
||||
|
||||
### Application Monitoring
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------ | ------- | ----------------------------- |
|
||||
| `METRICS_ENABLED` | `true` | Enable metrics collection |
|
||||
| `METRICS_PORT` | `9090` | Port for metrics endpoint |
|
||||
| `HEALTH_CHECK_INTERVAL` | `30000` | Health check interval (ms) |
|
||||
| `PERFORMANCE_MONITORING` | `false` | Enable performance monitoring |
|
||||
|
||||
```env
|
||||
METRICS_ENABLED=true
|
||||
METRICS_PORT=9090
|
||||
HEALTH_CHECK_INTERVAL=30000
|
||||
PERFORMANCE_MONITORING=true
|
||||
```
|
||||
|
||||
### Logging Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------ | ------------ | --------------------------------------- |
|
||||
| `LOG_FORMAT` | `json` | Log format (`json`, `text`) |
|
||||
| `LOG_FILE` | - | Log file path (if file logging enabled) |
|
||||
| `LOG_MAX_SIZE` | `10m` | Maximum log file size |
|
||||
| `LOG_MAX_FILES` | `5` | Maximum number of log files |
|
||||
| `LOG_DATE_PATTERN` | `YYYY-MM-DD` | Date pattern for log rotation |
|
||||
|
||||
```env
|
||||
LOG_FORMAT=json
|
||||
LOG_FILE=./logs/mcphub.log
|
||||
LOG_MAX_SIZE=10m
|
||||
LOG_MAX_FILES=5
|
||||
LOG_DATE_PATTERN=YYYY-MM-DD
|
||||
```
|
||||
|
||||
## Development & Debug
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------ | ------- | ----------------------------------- |
|
||||
| `DEBUG` | - | Debug namespaces (e.g., `mcphub:*`) |
|
||||
| `DEV_TOOLS_ENABLED` | `false` | Enable development tools |
|
||||
| `HOT_RELOAD` | `true` | Enable hot reload in development |
|
||||
| `MOCK_EXTERNAL_SERVICES` | `false` | Mock external API calls |
|
||||
|
||||
```env
|
||||
DEBUG=mcphub:*
|
||||
DEV_TOOLS_ENABLED=true
|
||||
HOT_RELOAD=true
|
||||
MOCK_EXTERNAL_SERVICES=false
|
||||
```
|
||||
|
||||
## Production Optimization
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------ | ------- | -------------------------------------- |
|
||||
| `CLUSTER_MODE` | `false` | Enable cluster mode |
|
||||
| `WORKER_PROCESSES` | `0` | Number of worker processes (0 = auto) |
|
||||
| `MEMORY_LIMIT` | - | Memory limit per process |
|
||||
| `CPU_LIMIT` | - | CPU limit per process |
|
||||
| `GC_OPTIMIZE` | `false` | Enable garbage collection optimization |
|
||||
|
||||
```env
|
||||
CLUSTER_MODE=true
|
||||
WORKER_PROCESSES=4
|
||||
MEMORY_LIMIT=512M
|
||||
GC_OPTIMIZE=true
|
||||
```
|
||||
|
||||
## Configuration Examples
|
||||
@@ -276,22 +49,9 @@ GC_OPTIMIZE=true
|
||||
# .env.development
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgresql://mcphub:password@localhost:5432/mcphub_dev
|
||||
|
||||
# Auth
|
||||
JWT_SECRET=dev-secret-key
|
||||
JWT_EXPIRES_IN=24h
|
||||
|
||||
# OpenAI (optional for development)
|
||||
# OPENAI_API_KEY=your-dev-key
|
||||
|
||||
# Debug
|
||||
DEBUG=mcphub:*
|
||||
DEV_TOOLS_ENABLED=true
|
||||
HOT_RELOAD=true
|
||||
```
|
||||
|
||||
### Production Environment
|
||||
@@ -300,30 +60,9 @@ HOT_RELOAD=true
|
||||
# .env.production
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
LOG_LEVEL=info
|
||||
LOG_FORMAT=json
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgresql://mcphub:secure-password@db.example.com:5432/mcphub
|
||||
DB_SSL=true
|
||||
DB_POOL_MAX=20
|
||||
|
||||
# Security
|
||||
JWT_SECRET=your-super-secure-production-secret
|
||||
SESSION_SECRET=your-session-secret
|
||||
BCRYPT_ROUNDS=14
|
||||
|
||||
# External Services
|
||||
OPENAI_API_KEY=your-production-openai-key
|
||||
REDIS_URL=redis://redis.example.com:6379
|
||||
|
||||
# Monitoring
|
||||
METRICS_ENABLED=true
|
||||
PERFORMANCE_MONITORING=true
|
||||
|
||||
# Optimization
|
||||
CLUSTER_MODE=true
|
||||
GC_OPTIMIZE=true
|
||||
```
|
||||
|
||||
### Docker Environment
|
||||
@@ -331,21 +70,10 @@ GC_OPTIMIZE=true
|
||||
```env
|
||||
# .env.docker
|
||||
NODE_ENV=production
|
||||
HOST=0.0.0.0
|
||||
PORT=3000
|
||||
|
||||
# Use service names for Docker networking
|
||||
DATABASE_URL=postgresql://mcphub:password@postgres:5432/mcphub
|
||||
REDIS_URL=redis://redis:6379
|
||||
|
||||
# Security
|
||||
JWT_SECRET_FILE=/run/secrets/jwt_secret
|
||||
DB_PASSWORD_FILE=/run/secrets/db_password
|
||||
|
||||
# File paths in container
|
||||
MCP_SETTINGS_FILE=/app/mcp_settings.json
|
||||
UPLOAD_DIR=/app/data/uploads
|
||||
LOG_FILE=/app/logs/mcphub.log
|
||||
```
|
||||
|
||||
## Environment Variable Loading
|
||||
@@ -364,7 +92,6 @@ MCPHub supports variable expansion:
|
||||
```env
|
||||
BASE_URL=https://api.example.com
|
||||
API_ENDPOINT=${BASE_URL}/v1
|
||||
DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
@@ -375,15 +102,3 @@ DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_N
|
||||
4. **Use environment-specific files**
|
||||
5. **Validate all environment variables** at startup
|
||||
6. **Use Docker secrets** for container deployments
|
||||
|
||||
## Validation
|
||||
|
||||
MCPHub validates environment variables at startup. Invalid configurations will prevent the application from starting with helpful error messages.
|
||||
|
||||
Required variables for production:
|
||||
|
||||
- `JWT_SECRET`
|
||||
- `DATABASE_URL` or individual DB components
|
||||
- `OPENAI_API_KEY` (if smart routing is enabled)
|
||||
|
||||
This comprehensive environment configuration ensures MCPHub can be properly configured for any deployment scenario.
|
||||
|
||||
@@ -27,10 +27,7 @@ MCPHub uses several configuration files:
|
||||
"args": ["arg1", "arg2"],
|
||||
"env": {
|
||||
"ENV_VAR": "value"
|
||||
},
|
||||
"cwd": "/working/directory",
|
||||
"timeout": 30000,
|
||||
"restart": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,8 +47,7 @@ MCPHub uses several configuration files:
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless"],
|
||||
"timeout": 60000
|
||||
"args": ["@playwright/mcp@latest", "--headless"]
|
||||
},
|
||||
"slack": {
|
||||
"command": "npx",
|
||||
@@ -79,12 +75,6 @@ MCPHub uses several configuration files:
|
||||
| Field | Type | Default | Description |
|
||||
| -------------- | ------- | --------------- | --------------------------- |
|
||||
| `env` | object | `{}` | Environment variables |
|
||||
| `cwd` | string | `process.cwd()` | Working directory |
|
||||
| `timeout` | number | `30000` | Startup timeout (ms) |
|
||||
| `restart` | boolean | `true` | Auto-restart on failure |
|
||||
| `maxRestarts` | number | `5` | Maximum restart attempts |
|
||||
| `restartDelay` | number | `5000` | Delay between restarts (ms) |
|
||||
| `stdio` | string | `pipe` | stdio configuration |
|
||||
|
||||
## Common MCP Server Examples
|
||||
|
||||
@@ -262,42 +252,14 @@ MCPHub supports environment variable substitution using `${VAR_NAME}` syntax:
|
||||
"args": ["-m", "api_server"],
|
||||
"env": {
|
||||
"API_KEY": "${API_KEY}",
|
||||
"API_URL": "${API_BASE_URL}/v1",
|
||||
"DEBUG": "${NODE_ENV:development}"
|
||||
"API_URL": "${API_BASE_URL}/v1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Default values can be specified with `${VAR_NAME:default}`:
|
||||
|
||||
```json
|
||||
{
|
||||
"timeout": "${MCP_TIMEOUT:30000}",
|
||||
"maxRestarts": "${MCP_MAX_RESTARTS:5}"
|
||||
}
|
||||
```
|
||||
|
||||
### Conditional Configuration
|
||||
|
||||
Use different configurations based on environment:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"database": {
|
||||
"command": "python",
|
||||
"args": ["-m", "db_server"],
|
||||
"env": {
|
||||
"DB_URL": "${NODE_ENV:development == 'production' ? DATABASE_URL : DEV_DATABASE_URL}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Server Scripts
|
||||
{/* ### Custom Server Scripts
|
||||
|
||||
#### Local Python Server
|
||||
|
||||
@@ -373,7 +335,7 @@ Complement `mcp_settings.json` with server metadata:
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
``` */}
|
||||
|
||||
## Group Management
|
||||
|
||||
@@ -385,25 +347,18 @@ Complement `mcp_settings.json` with server metadata:
|
||||
"production": {
|
||||
"name": "Production Tools",
|
||||
"description": "Stable production servers",
|
||||
"servers": ["fetch", "slack", "github"],
|
||||
"access": "authenticated",
|
||||
"rateLimit": {
|
||||
"requestsPerMinute": 100,
|
||||
"burstLimit": 20
|
||||
}
|
||||
"servers": ["fetch", "slack", "github"]
|
||||
},
|
||||
"experimental": {
|
||||
"name": "Experimental Features",
|
||||
"description": "Beta and experimental servers",
|
||||
"servers": ["experimental-ai", "beta-search"],
|
||||
"access": "admin",
|
||||
"enabled": false
|
||||
"servers": ["experimental-ai", "beta-search"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Access Control
|
||||
{/* ### Access Control
|
||||
|
||||
| Access Level | Description |
|
||||
| --------------- | -------------------------- |
|
||||
@@ -422,9 +377,9 @@ MCPHub supports hot reloading of configurations:
|
||||
# Reload configurations without restart
|
||||
curl -X POST http://localhost:3000/api/admin/reload-config \
|
||||
-H "Authorization: Bearer your-admin-token"
|
||||
```
|
||||
``` */}
|
||||
|
||||
### Configuration Validation
|
||||
{/* ### Configuration Validation
|
||||
|
||||
MCPHub validates configurations on startup and reload:
|
||||
|
||||
@@ -436,7 +391,7 @@ MCPHub validates configurations on startup and reload:
|
||||
"requireDocumentation": true
|
||||
}
|
||||
}
|
||||
```
|
||||
``` */}
|
||||
|
||||
## Best Practices
|
||||
|
||||
@@ -453,7 +408,7 @@ MCPHub validates configurations on startup and reload:
|
||||
}
|
||||
```
|
||||
|
||||
2. **Limit server permissions**:
|
||||
{/* 2. **Limit server permissions**:
|
||||
```json
|
||||
{
|
||||
"filesystem": {
|
||||
@@ -464,9 +419,9 @@ MCPHub validates configurations on startup and reload:
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
``` */}
|
||||
|
||||
### Performance
|
||||
{/* ### Performance
|
||||
|
||||
1. **Set appropriate timeouts**:
|
||||
|
||||
@@ -486,9 +441,9 @@ MCPHub validates configurations on startup and reload:
|
||||
"MEMORY_LIMIT": "512MB"
|
||||
}
|
||||
}
|
||||
```
|
||||
``` */}
|
||||
|
||||
### Monitoring
|
||||
{/* ### Monitoring
|
||||
|
||||
1. **Enable health checks**:
|
||||
|
||||
@@ -510,9 +465,9 @@ MCPHub validates configurations on startup and reload:
|
||||
"LOG_FORMAT": "json"
|
||||
}
|
||||
}
|
||||
```
|
||||
``` */}
|
||||
|
||||
## Troubleshooting
|
||||
{/* ## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
@@ -521,9 +476,9 @@ MCPHub validates configurations on startup and reload:
|
||||
```bash
|
||||
# Test command manually
|
||||
uvx mcp-server-fetch
|
||||
```
|
||||
``` */}
|
||||
|
||||
**Environment variables not found**: Verify `.env` file
|
||||
{/* **Environment variables not found**: Verify `.env` file
|
||||
|
||||
```bash
|
||||
# Check environment
|
||||
@@ -535,9 +490,9 @@ printenv | grep API_KEY
|
||||
```bash
|
||||
# Verify executable permissions
|
||||
ls -la /path/to/server
|
||||
```
|
||||
``` */}
|
||||
|
||||
### Debug Configuration
|
||||
{/* ### Debug Configuration
|
||||
|
||||
Enable debug mode for detailed logging:
|
||||
|
||||
@@ -550,8 +505,8 @@ Enable debug mode for detailed logging:
|
||||
"logStartup": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
``` */}
|
||||
{/*
|
||||
### Validation Errors
|
||||
|
||||
Common validation errors and solutions:
|
||||
@@ -559,6 +514,6 @@ Common validation errors and solutions:
|
||||
1. **Missing required fields**: Add `command` and `args`
|
||||
2. **Invalid timeout**: Use number, not string
|
||||
3. **Environment variable not found**: Check `.env` file
|
||||
4. **Command not found**: Verify installation and PATH
|
||||
4. **Command not found**: Verify installation and PATH */}
|
||||
|
||||
This comprehensive guide covers all aspects of configuring MCP servers in MCPHub for various use cases and environments.
|
||||
|
||||
@@ -27,9 +27,7 @@
|
||||
"pages": [
|
||||
"features/server-management",
|
||||
"features/group-management",
|
||||
"features/smart-routing",
|
||||
"features/authentication",
|
||||
"features/monitoring"
|
||||
"features/smart-routing"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -40,14 +38,6 @@
|
||||
"configuration/docker-setup",
|
||||
"configuration/nginx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Development",
|
||||
"pages": [
|
||||
"development/getting-started",
|
||||
"development/architecture",
|
||||
"development/contributing"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -67,9 +57,7 @@
|
||||
"pages": [
|
||||
"zh/features/server-management",
|
||||
"zh/features/group-management",
|
||||
"zh/features/smart-routing",
|
||||
"zh/features/authentication",
|
||||
"zh/features/monitoring"
|
||||
"zh/features/smart-routing"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -80,19 +68,11 @@
|
||||
"zh/configuration/docker-setup",
|
||||
"zh/configuration/nginx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "开发指南",
|
||||
"pages": [
|
||||
"zh/development/getting-started",
|
||||
"zh/development/architecture",
|
||||
"zh/development/contributing"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "API Reference",
|
||||
"tab": "API",
|
||||
"groups": [
|
||||
{
|
||||
"group": "MCP Endpoints",
|
||||
@@ -104,7 +84,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Management API",
|
||||
"group": "Management Endpoints",
|
||||
"pages": [
|
||||
"api-reference/servers",
|
||||
"api-reference/groups",
|
||||
@@ -114,26 +94,34 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "接口",
|
||||
"groups": [
|
||||
{
|
||||
"group": "MCP 端点",
|
||||
"pages": [
|
||||
"zh/api-reference/introduction",
|
||||
"zh/api-reference/mcp-http",
|
||||
"zh/api-reference/mcp-sse",
|
||||
"zh/api-reference/smart-routing"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "管理端点",
|
||||
"pages": [
|
||||
"zh/api-reference/servers",
|
||||
"zh/api-reference/groups",
|
||||
"zh/api-reference/auth",
|
||||
"zh/api-reference/logs",
|
||||
"zh/api-reference/config"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"global": {
|
||||
"anchors": [
|
||||
{
|
||||
"anchor": "GitHub",
|
||||
"href": "https://github.com/samanhappy/mcphub",
|
||||
"icon": "github"
|
||||
},
|
||||
{
|
||||
"anchor": "Discord",
|
||||
"href": "https://discord.gg/qMKNsn5Q",
|
||||
"icon": "discord"
|
||||
},
|
||||
{
|
||||
"anchor": "Sponsor",
|
||||
"href": "https://ko-fi.com/samanhappy",
|
||||
"icon": "heart"
|
||||
}
|
||||
]
|
||||
"anchors": []
|
||||
}
|
||||
},
|
||||
"logo": {
|
||||
@@ -144,13 +132,13 @@
|
||||
"links": [
|
||||
{
|
||||
"label": "Demo",
|
||||
"href": "http://localhost:3000"
|
||||
"href": "https://demo.mcphubx.com"
|
||||
}
|
||||
],
|
||||
"primary": {
|
||||
"type": "button",
|
||||
"label": "Get Started",
|
||||
"href": "https://docs.hubmcp.dev/quickstart"
|
||||
"href": "https://docs.mcphubx.com/quickstart"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
|
||||
@@ -30,9 +30,6 @@ Groups are named collections of MCP servers that can be accessed through dedicat
|
||||
3. **Fill Group Details**:
|
||||
|
||||
- **Name**: Unique identifier for the group
|
||||
- **Display Name**: Human-readable name
|
||||
- **Description**: Purpose and contents of the group
|
||||
- **Access Level**: Public, Private, or Restricted
|
||||
|
||||
4. **Add Servers**: Select servers to include in the group
|
||||
|
||||
@@ -46,14 +43,11 @@ curl -X POST http://localhost:3000/api/groups \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-d '{
|
||||
"name": "web-automation",
|
||||
"displayName": "Web Automation Tools",
|
||||
"description": "Browser automation and web scraping tools",
|
||||
"servers": ["playwright", "fetch"],
|
||||
"accessLevel": "public"
|
||||
"servers": ["playwright", "fetch"]
|
||||
}'
|
||||
```
|
||||
|
||||
### Via Configuration File
|
||||
{/* ### Via Configuration File
|
||||
|
||||
Define groups in your `mcp_settings.json`:
|
||||
|
||||
@@ -66,20 +60,16 @@ Define groups in your `mcp_settings.json`:
|
||||
},
|
||||
"groups": {
|
||||
"web-tools": {
|
||||
"displayName": "Web Tools",
|
||||
"description": "Web scraping and browser automation",
|
||||
"name": "web",
|
||||
"servers": ["fetch", "playwright"],
|
||||
"accessLevel": "public"
|
||||
},
|
||||
"communication": {
|
||||
"displayName": "Communication Tools",
|
||||
"description": "Messaging and collaboration tools",
|
||||
"name": "communication",
|
||||
"servers": ["slack"],
|
||||
"accessLevel": "private"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
``` */}
|
||||
|
||||
## Group Types and Use Cases
|
||||
|
||||
@@ -177,7 +167,7 @@ Define groups in your `mcp_settings.json`:
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Group Access Control
|
||||
{/* ## Group Access Control
|
||||
|
||||
### Access Levels
|
||||
|
||||
@@ -254,7 +244,7 @@ curl -X DELETE http://localhost:3000/api/groups/web-tools/members/user123 \
|
||||
# List group members
|
||||
curl http://localhost:3000/api/groups/web-tools/members \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
``` */}
|
||||
|
||||
## Group Endpoints
|
||||
|
||||
@@ -346,7 +336,7 @@ Response will only include tools from `fetch` and `playwright` servers.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Batch Server Updates
|
||||
{/* ### Batch Server Updates
|
||||
|
||||
Update multiple servers at once:
|
||||
|
||||
@@ -357,9 +347,9 @@ curl -X PUT http://localhost:3000/api/groups/web-tools/servers \
|
||||
-d '{
|
||||
"servers": ["fetch", "playwright", "selenium"]
|
||||
}'
|
||||
```
|
||||
``` */}
|
||||
|
||||
## Group Monitoring
|
||||
{/* ## Group Monitoring
|
||||
|
||||
### Group Status
|
||||
|
||||
@@ -393,9 +383,9 @@ Metrics include:
|
||||
- Request count by tool
|
||||
- Response times
|
||||
- Error rates
|
||||
- User activity
|
||||
- User activity */}
|
||||
|
||||
## Advanced Group Features
|
||||
{/* ## Advanced Group Features
|
||||
|
||||
### Nested Groups
|
||||
|
||||
@@ -474,7 +464,7 @@ Define policies for group behavior:
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
``` */}
|
||||
|
||||
## Best Practices
|
||||
|
||||
@@ -494,7 +484,7 @@ Define policies for group behavior:
|
||||
**Use Descriptive Names**: Choose names that clearly indicate the group's purpose and contents.
|
||||
</Tip>
|
||||
|
||||
### Security Considerations
|
||||
{/* ### Security Considerations
|
||||
|
||||
<Warning>
|
||||
**Principle of Least Privilege**: Only give users access to groups they actually need.
|
||||
@@ -507,7 +497,7 @@ Define policies for group behavior:
|
||||
|
||||
<Warning>
|
||||
**Regular Access Reviews**: Periodically review group memberships and remove unnecessary access.
|
||||
</Warning>
|
||||
</Warning> */}
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
|
||||
@@ -311,7 +311,7 @@ Servers can use environment variables for configuration:
|
||||
- `${VAR_NAME:-default}`: Uses default if variable not set
|
||||
- `${VAR_NAME:+value}`: Uses value if variable is set
|
||||
|
||||
### Working Directory
|
||||
{/* ### Working Directory
|
||||
|
||||
Set the working directory for server execution:
|
||||
|
||||
@@ -323,7 +323,7 @@ Set the working directory for server execution:
|
||||
"cwd": "/path/to/server/directory"
|
||||
}
|
||||
}
|
||||
```
|
||||
``` */}
|
||||
|
||||
### Command Variations
|
||||
|
||||
@@ -352,7 +352,7 @@ Different ways to specify server commands:
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Direct Python">
|
||||
{/* <Tab title="Direct Python">
|
||||
```json
|
||||
{
|
||||
"direct-python": {
|
||||
@@ -373,7 +373,7 @@ Different ways to specify server commands:
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
</Tab> */}
|
||||
</Tabs>
|
||||
|
||||
## Advanced Features
|
||||
@@ -382,12 +382,12 @@ Different ways to specify server commands:
|
||||
|
||||
MCPHub supports hot reloading of server configurations:
|
||||
|
||||
1. **Config File Changes**: Automatically detects changes to `mcp_settings.json`
|
||||
2. **Dashboard Updates**: Immediately applies changes made through the web interface
|
||||
3. **API Updates**: Real-time updates via REST API calls
|
||||
4. **Zero Downtime**: Graceful server restarts without affecting other servers
|
||||
{/* 1. **Config File Changes**: Automatically detects changes to `mcp_settings.json` */}
|
||||
1. **Dashboard Updates**: Immediately applies changes made through the web interface
|
||||
2. **API Updates**: Real-time updates via REST API calls
|
||||
3. **Zero Downtime**: Graceful server restarts without affecting other servers
|
||||
|
||||
### Resource Limits
|
||||
{/* ### Resource Limits
|
||||
|
||||
Control server resource usage:
|
||||
|
||||
@@ -403,9 +403,9 @@ Control server resource usage:
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
``` */}
|
||||
|
||||
### Dependency Management
|
||||
{/* ### Dependency Management
|
||||
|
||||
Handle server dependencies:
|
||||
|
||||
@@ -439,7 +439,7 @@ Handle server dependencies:
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
</AccordionGroup> */}
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ Smart Routing requires additional setup compared to basic MCPHub usage:
|
||||
2. **Embedding Service**: OpenAI API or compatible service
|
||||
3. **Environment Configuration**: Proper configuration variables
|
||||
|
||||
### Quick Setup
|
||||
{/* ### Quick Setup
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Docker Compose">
|
||||
@@ -265,7 +265,7 @@ EMBEDDING_BATCH_SIZE=100
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
</AccordionGroup> */}
|
||||
|
||||
## Using Smart Routing
|
||||
|
||||
@@ -287,7 +287,7 @@ Access Smart Routing through the special `$smart` endpoint:
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Basic Usage
|
||||
{/* ### Basic Usage
|
||||
|
||||
Connect your AI client to the Smart Routing endpoint and make natural language requests:
|
||||
|
||||
@@ -330,9 +330,9 @@ Response:
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
``` */}
|
||||
|
||||
### Advanced Queries
|
||||
{/* ### Advanced Queries
|
||||
|
||||
Smart Routing supports various query types:
|
||||
|
||||
@@ -405,9 +405,9 @@ Smart Routing supports various query types:
|
||||
}'
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
</AccordionGroup> */}
|
||||
|
||||
### Tool Execution
|
||||
{/* ### Tool Execution
|
||||
|
||||
Once Smart Routing finds relevant tools, you can execute them directly:
|
||||
|
||||
@@ -426,9 +426,9 @@ curl -X POST http://localhost:3000/mcp/$smart \
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
``` */}
|
||||
|
||||
## Performance Optimization
|
||||
{/* ## Performance Optimization
|
||||
|
||||
### Embedding Cache
|
||||
|
||||
@@ -585,7 +585,7 @@ curl -X POST http://localhost:3000/api/smart-routing/feedback \
|
||||
"successful": true,
|
||||
"comments": "Perfect tool for the task"
|
||||
}'
|
||||
```
|
||||
``` */}
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
title: MCPHub Documentation
|
||||
title: MCPHub
|
||||
description: 'The Unified Hub for Model Context Protocol (MCP) Servers'
|
||||
---
|
||||
|
||||
<img className="block dark:hidden" src="/images/hero-light.png" alt="Hero Light" />
|
||||
<img className="hidden dark:block" src="/images/hero-dark.png" alt="Hero Dark" />
|
||||
{/* <img className="block dark:hidden" src="/images/hero-light.png" alt="Hero Light" />
|
||||
<img className="hidden dark:block" src="/images/hero-dark.png" alt="Hero Dark" /> */}
|
||||
|
||||
# Welcome to MCPHub
|
||||
|
||||
@@ -16,12 +16,12 @@ MCPHub makes it easy to manage and scale multiple MCP (Model Context Protocol) s
|
||||
<Card title="Unified Management" icon="server" href="/features/server-management">
|
||||
Centrally manage multiple MCP servers with hot-swappable configuration
|
||||
</Card>
|
||||
<Card title="Smart Routing" icon="route" href="/features/smart-routing">
|
||||
AI-powered tool discovery using vector semantic search
|
||||
</Card>
|
||||
<Card title="Group Management" icon="users" href="/features/group-management">
|
||||
Organize servers into logical groups for streamlined access control
|
||||
</Card>
|
||||
<Card title="Smart Routing" icon="route" href="/features/smart-routing">
|
||||
AI-powered tool discovery using vector semantic search
|
||||
</Card>
|
||||
<Card title="Real-time Monitoring" icon="chart-line" href="/features/monitoring">
|
||||
Monitor server status and performance from a unified dashboard
|
||||
</Card>
|
||||
|
||||
@@ -72,7 +72,6 @@ Optional for Smart Routing:
|
||||
-p 3000:3000 \
|
||||
-e PORT=3000 \
|
||||
-e BASE_PATH="" \
|
||||
-e REQUEST_TIMEOUT=60000 \
|
||||
samanhappy/mcphub:latest
|
||||
```
|
||||
|
||||
@@ -144,12 +143,9 @@ Optional for Smart Routing:
|
||||
|
||||
# Run with custom port
|
||||
PORT=8080 mcphub
|
||||
|
||||
# Run with custom config path
|
||||
MCP_SETTINGS_PATH=/path/to/mcp_settings.json mcphub
|
||||
```
|
||||
|
||||
#### 3. Local Installation
|
||||
{/* #### 3. Local Installation
|
||||
|
||||
You can also install MCPHub locally in a project:
|
||||
|
||||
@@ -170,8 +166,7 @@ Optional for Smart Routing:
|
||||
|
||||
# Run MCPHub
|
||||
./start.sh
|
||||
```
|
||||
|
||||
``` */}
|
||||
</Tab>
|
||||
|
||||
<Tab title="Local Development">
|
||||
@@ -419,7 +414,7 @@ Smart Routing provides AI-powered tool discovery using vector semantic search.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Environment Configuration
|
||||
{/* ### Environment Configuration
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
@@ -435,13 +430,13 @@ EMBEDDING_MODEL=text-embedding-3-small
|
||||
|
||||
# Optional: Enable smart routing
|
||||
ENABLE_SMART_ROUTING=true
|
||||
```
|
||||
``` */}
|
||||
|
||||
## Verification
|
||||
|
||||
After installation, verify MCPHub is working:
|
||||
|
||||
### 1. Health Check
|
||||
{/* ### 1. Health Check
|
||||
|
||||
```bash
|
||||
curl http://localhost:3000/api/health
|
||||
@@ -455,9 +450,9 @@ Expected response:
|
||||
"version": "x.x.x",
|
||||
"uptime": 123
|
||||
}
|
||||
```
|
||||
``` */}
|
||||
|
||||
### 2. Dashboard Access
|
||||
### Dashboard Access
|
||||
|
||||
Open your browser and navigate to:
|
||||
|
||||
@@ -465,7 +460,7 @@ Open your browser and navigate to:
|
||||
http://localhost:3000
|
||||
```
|
||||
|
||||
### 3. API Test
|
||||
{/* ### 3. API Test
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/mcp \
|
||||
@@ -476,7 +471,7 @@ curl -X POST http://localhost:3000/mcp \
|
||||
"method": "tools/list",
|
||||
"params": {}
|
||||
}'
|
||||
```
|
||||
``` */}
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 1.1 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -106,12 +106,10 @@ Once your servers are configured, connect your AI clients using MCPHub endpoints
|
||||
Access all configured MCP servers: ``` http://localhost:3000/mcp ```
|
||||
</Tab>
|
||||
<Tab title="Specific Group">
|
||||
Access servers in a specific group: ``` http://localhost:3000/mcp/{group - name}
|
||||
```
|
||||
Access servers in a specific group: ``` http://localhost:3000/mcp/{groupName} ```
|
||||
</Tab>
|
||||
<Tab title="Individual Server">
|
||||
Access a single server: ``` http://localhost:3000/mcp/{server - name}
|
||||
```
|
||||
Access a single server: ``` http://localhost:3000/mcp/{serverName} ```
|
||||
</Tab>
|
||||
<Tab title="Smart Routing">
|
||||
Use AI-powered tool discovery: ``` http://localhost:3000/mcp/$smart ```
|
||||
@@ -172,7 +170,7 @@ Here are some popular MCP servers you can add:
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Verification
|
||||
{/* ## Verification
|
||||
|
||||
Test your setup by making a simple request:
|
||||
|
||||
@@ -187,7 +185,7 @@ curl -X POST http://localhost:3000/mcp \
|
||||
}'
|
||||
```
|
||||
|
||||
You should receive a list of available tools from your configured MCP servers.
|
||||
You should receive a list of available tools from your configured MCP servers. */}
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
147
docs/zh/api-reference/auth.mdx
Normal file
147
docs/zh/api-reference/auth.mdx
Normal file
@@ -0,0 +1,147 @@
|
||||
---
|
||||
title: "身份验证"
|
||||
description: "管理用户和身份验证。"
|
||||
---
|
||||
|
||||
import { Card, Cards } from 'mintlify';
|
||||
|
||||
<Card
|
||||
title="POST /api/auth/login"
|
||||
href="#login"
|
||||
>
|
||||
登录以获取 JWT 令牌。
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="POST /api/auth/register"
|
||||
href="#register"
|
||||
>
|
||||
注册一个新用户。
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="GET /api/auth/user"
|
||||
href="#get-current-user"
|
||||
>
|
||||
获取当前已验证的用户。
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="POST /api/auth/change-password"
|
||||
href="#change-password"
|
||||
>
|
||||
更改当前用户的密码。
|
||||
</Card>
|
||||
|
||||
---
|
||||
|
||||
### 登录
|
||||
|
||||
验证用户身份并返回 JWT 令牌及用户详细信息。
|
||||
|
||||
- **端点**: `/api/auth/login`
|
||||
- **方法**: `POST`
|
||||
- **正文**:
|
||||
- `username` (string, 必填): 用户名。
|
||||
- `password` (string, 必填): 用户密码。
|
||||
- **请求示例**:
|
||||
```json
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "admin123"
|
||||
}
|
||||
```
|
||||
- **成功响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "登录成功",
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"user": {
|
||||
"username": "admin",
|
||||
"isAdmin": true,
|
||||
"permissions": { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 注册
|
||||
|
||||
注册一个新用户并返回 JWT 令牌。
|
||||
|
||||
- **端点**: `/api/auth/register`
|
||||
- **方法**: `POST`
|
||||
- **正文**:
|
||||
- `username` (string, 必填): 新的用户名。
|
||||
- `password` (string, 必填): 新的用户密码 (至少6个字符)。
|
||||
- `isAdmin` (boolean, 可选): 用户是否应有管理员权限。
|
||||
- **请求示例**:
|
||||
```json
|
||||
{
|
||||
"username": "newuser",
|
||||
"password": "password123",
|
||||
"isAdmin": false
|
||||
}
|
||||
```
|
||||
- **成功响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"user": {
|
||||
"username": "newuser",
|
||||
"isAdmin": false,
|
||||
"permissions": { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 获取当前用户
|
||||
|
||||
检索当前通过身份验证的用户的个人资料。
|
||||
|
||||
- **端点**: `/api/auth/user`
|
||||
- **方法**: `GET`
|
||||
- **身份验证**: 需要承载令牌 (Bearer Token)。
|
||||
- **成功响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"user": {
|
||||
"username": "admin",
|
||||
"isAdmin": true,
|
||||
"permissions": { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 更改密码
|
||||
|
||||
允许通过身份验证的用户更改其密码。
|
||||
|
||||
- **端点**: `/api/auth/change-password`
|
||||
- **方法**: `POST`
|
||||
- **身份验证**: 需要承载令牌 (Bearer Token)。
|
||||
- **正文**:
|
||||
- `currentPassword` (string, 必填): 用户的当前密码。
|
||||
- `newPassword` (string, 必填): 新的密码 (至少6个字符)。
|
||||
- **请求示例**:
|
||||
```json
|
||||
{
|
||||
"currentPassword": "oldpassword",
|
||||
"newPassword": "newpassword123"
|
||||
}
|
||||
```
|
||||
- **成功响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "密码更新成功"
|
||||
}
|
||||
```
|
||||
111
docs/zh/api-reference/config.mdx
Normal file
111
docs/zh/api-reference/config.mdx
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: "配置"
|
||||
description: "管理和检索系统级配置。"
|
||||
---
|
||||
|
||||
import { Card, Cards } from 'mintlify';
|
||||
|
||||
<Card title="PUT /api/system-config" href="#update-system-config">更新主系统配置。</Card>
|
||||
<Card title="GET /api/settings" href="#get-all-settings">获取所有系统设置,包括服务器和群组。</Card>
|
||||
<Card title="GET /config" href="#get-runtime-config">获取前端的公共运行时配置。</Card>
|
||||
<Card title="GET /public-config" href="#get-public-config">获取公共配置以检查是否跳过身份验证。</Card>
|
||||
|
||||
---
|
||||
|
||||
### 更新系统配置
|
||||
|
||||
更新系统配置的各个部分。您只需提供要更新部分的键。
|
||||
|
||||
- **端点**: `/api/system-config`
|
||||
- **方法**: `PUT`
|
||||
- **正文**: 一个 JSON 对象,包含以下一个或多个顶级键:`routing`、`install`、`smartRouting`、`mcpRouter`。
|
||||
|
||||
#### 路由配置 (`routing`)
|
||||
|
||||
- `enableGlobalRoute` (boolean): 启用或禁用全局 `/api/mcp` 路由。
|
||||
- `enableGroupNameRoute` (boolean): 启用或禁用基于群组的路由 (例如 `/api/mcp/group/:groupName`)。
|
||||
- `enableBearerAuth` (boolean): 为 MCP 路由启用承载令牌身份验证。
|
||||
- `bearerAuthKey` (string): 用于承载身份验证的密钥。
|
||||
- `skipAuth` (boolean): 如果为 true,则跳过所有身份验证,使实例公开。
|
||||
|
||||
#### 安装配置 (`install`)
|
||||
|
||||
- `pythonIndexUrl` (string): 用于安装的 Python 包索引 (PyPI) 的基础 URL。
|
||||
- `npmRegistry` (string): 用于安装的 npm 注册表 URL。
|
||||
- `baseUrl` (string): 此 MCPHub 实例的公共基础 URL。
|
||||
|
||||
#### 智能路由配置 (`smartRouting`)
|
||||
|
||||
- `enabled` (boolean): 启用或禁用智能路由功能。
|
||||
- `dbUrl` (string): 用于存储嵌入的数据库连接 URL。
|
||||
- `openaiApiBaseUrl` (string): 用于生成嵌入的 OpenAI 兼容 API 的基础 URL。
|
||||
- `openaiApiKey` (string): 嵌入服务的 API 密钥。
|
||||
- `openaiApiEmbeddingModel` (string): 要使用的嵌入模型的名称。
|
||||
|
||||
#### MCP 路由器配置 (`mcpRouter`)
|
||||
|
||||
- `apiKey` (string): MCP 路由器服务的 API 密钥。
|
||||
- `referer` (string): 用于 MCP 路由器请求的 referer 头。
|
||||
- `title` (string): 在 MCP 路由器上显示的此实例的标题。
|
||||
- `baseUrl` (string): MCP 路由器 API 的基础 URL。
|
||||
|
||||
- **请求示例**:
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"skipAuth": true
|
||||
},
|
||||
"smartRouting": {
|
||||
"enabled": true,
|
||||
"dbUrl": "postgresql://user:pass@host:port/db"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 获取所有设置
|
||||
|
||||
检索实例的整个设置对象,包括所有服务器配置、群组和系统设置。这是 `mcp_settings.json` 文件的完整转储。
|
||||
|
||||
- **端点**: `/api/settings`
|
||||
- **方法**: `GET`
|
||||
|
||||
---
|
||||
|
||||
### 获取运行时配置
|
||||
|
||||
检索前端应用程序所需的基本运行时配置。此端点不需要身份验证。
|
||||
|
||||
- **端点**: `/config`
|
||||
- **方法**: `GET`
|
||||
- **成功响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"basePath": "",
|
||||
"version": "1.0.0",
|
||||
"name": "MCPHub"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 获取公共配置
|
||||
|
||||
检索公共配置,主要用于检查是否跳过身份验证。这允许前端在用户登录前相应地调整其行为。此端点不需要身份验证。
|
||||
|
||||
- **端点**: `/public-config`
|
||||
- **方法**: `GET`
|
||||
- **成功响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"skipAuth": false,
|
||||
"permissions": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,572 +0,0 @@
|
||||
---
|
||||
title: '创建资源'
|
||||
description: '创建新的 MCP 服务器、用户和组'
|
||||
---
|
||||
|
||||
## 创建服务器
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
POST /api/servers
|
||||
```
|
||||
|
||||
### 请求
|
||||
|
||||
#### 请求头
|
||||
|
||||
```http
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer YOUR_JWT_TOKEN
|
||||
```
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "文件系统服务器",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"],
|
||||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"DEBUG": "mcp:*",
|
||||
"MAX_FILES": "1000"
|
||||
},
|
||||
"cwd": "/app/workspace",
|
||||
"timeout": 30000,
|
||||
"retries": 3,
|
||||
"enabled": true,
|
||||
"description": "提供文件系统访问的 MCP 服务器",
|
||||
"tags": ["filesystem", "production"],
|
||||
"healthCheck": {
|
||||
"enabled": true,
|
||||
"interval": 30000,
|
||||
"timeout": 5000,
|
||||
"retries": 3,
|
||||
"endpoint": "/health"
|
||||
},
|
||||
"resources": {
|
||||
"memory": {
|
||||
"limit": "512MB",
|
||||
"warning": "400MB"
|
||||
},
|
||||
"cpu": {
|
||||
"limit": "50%"
|
||||
}
|
||||
},
|
||||
"logging": {
|
||||
"level": "info",
|
||||
"file": "/var/log/mcphub/server.log",
|
||||
"maxSize": "100MB",
|
||||
"maxFiles": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 必填字段
|
||||
|
||||
- `name` (string): 服务器唯一名称
|
||||
- `command` (string): 执行命令
|
||||
- `args` (array): 命令参数数组
|
||||
|
||||
#### 可选字段
|
||||
|
||||
- `env` (object): 环境变量键值对
|
||||
- `cwd` (string): 工作目录
|
||||
- `timeout` (number): 超时时间(毫秒)
|
||||
- `retries` (number): 重试次数
|
||||
- `enabled` (boolean): 是否启用(默认 true)
|
||||
- `description` (string): 服务器描述
|
||||
- `tags` (array): 标签数组
|
||||
- `healthCheck` (object): 健康检查配置
|
||||
- `resources` (object): 资源限制配置
|
||||
- `logging` (object): 日志配置
|
||||
|
||||
### 响应
|
||||
|
||||
#### 成功响应 (201 Created)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "server-abc123",
|
||||
"name": "文件系统服务器",
|
||||
"status": "stopped",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"],
|
||||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"DEBUG": "mcp:*",
|
||||
"MAX_FILES": "1000"
|
||||
},
|
||||
"cwd": "/app/workspace",
|
||||
"timeout": 30000,
|
||||
"retries": 3,
|
||||
"enabled": true,
|
||||
"description": "提供文件系统访问的 MCP 服务器",
|
||||
"tags": ["filesystem", "production"],
|
||||
"healthCheck": {
|
||||
"enabled": true,
|
||||
"interval": 30000,
|
||||
"timeout": 5000,
|
||||
"retries": 3,
|
||||
"endpoint": "/health",
|
||||
"status": "unknown"
|
||||
},
|
||||
"resources": {
|
||||
"memory": {
|
||||
"limit": "512MB",
|
||||
"warning": "400MB",
|
||||
"current": "0MB"
|
||||
},
|
||||
"cpu": {
|
||||
"limit": "50%",
|
||||
"current": "0%"
|
||||
}
|
||||
},
|
||||
"logging": {
|
||||
"level": "info",
|
||||
"file": "/var/log/mcphub/server.log",
|
||||
"maxSize": "100MB",
|
||||
"maxFiles": 5,
|
||||
"currentSize": "0MB"
|
||||
},
|
||||
"createdAt": "2024-01-01T12:00:00Z",
|
||||
"updatedAt": "2024-01-01T12:00:00Z",
|
||||
"createdBy": "user123"
|
||||
},
|
||||
"message": "服务器创建成功"
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
**400 Bad Request - 参数错误**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "请求数据验证失败",
|
||||
"details": [
|
||||
{
|
||||
"field": "name",
|
||||
"message": "服务器名称不能为空"
|
||||
},
|
||||
{
|
||||
"field": "command",
|
||||
"message": "执行命令不能为空"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**409 Conflict - 名称冲突**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "RESOURCE_CONFLICT",
|
||||
"message": "服务器名称已存在",
|
||||
"details": {
|
||||
"field": "name",
|
||||
"value": "文件系统服务器",
|
||||
"conflictingResourceId": "server-xyz789"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 示例
|
||||
|
||||
#### cURL
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/servers \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-d '{
|
||||
"name": "文件系统服务器",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"],
|
||||
"env": {
|
||||
"NODE_ENV": "production"
|
||||
},
|
||||
"description": "生产环境文件系统服务器"
|
||||
}'
|
||||
```
|
||||
|
||||
#### JavaScript
|
||||
|
||||
```javascript
|
||||
const response = await fetch('/api/servers', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: '文件系统服务器',
|
||||
command: 'npx',
|
||||
args: ['-y', '@modelcontextprotocol/server-filesystem', '/data'],
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
},
|
||||
description: '生产环境文件系统服务器',
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
console.log('服务器创建成功:', result.data);
|
||||
} else {
|
||||
console.error('创建失败:', result.error);
|
||||
}
|
||||
```
|
||||
|
||||
#### Python
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
response = requests.post(
|
||||
'http://localhost:3000/api/servers',
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': f'Bearer {token}'
|
||||
},
|
||||
json={
|
||||
'name': '文件系统服务器',
|
||||
'command': 'npx',
|
||||
'args': ['-y', '@modelcontextprotocol/server-filesystem', '/data'],
|
||||
'env': {
|
||||
'NODE_ENV': 'production'
|
||||
},
|
||||
'description': '生产环境文件系统服务器'
|
||||
}
|
||||
)
|
||||
|
||||
if response.status_code == 201:
|
||||
result = response.json()
|
||||
print('服务器创建成功:', result['data'])
|
||||
else:
|
||||
error = response.json()
|
||||
print('创建失败:', error['error'])
|
||||
```
|
||||
|
||||
## 创建用户
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
POST /api/users
|
||||
```
|
||||
|
||||
### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "newuser",
|
||||
"email": "user@example.com",
|
||||
"password": "SecurePassword123!",
|
||||
"role": "user",
|
||||
"groups": ["dev-team", "qa-team"],
|
||||
"profile": {
|
||||
"firstName": "张",
|
||||
"lastName": "三",
|
||||
"department": "开发部",
|
||||
"title": "软件工程师",
|
||||
"phone": "+86-138-0013-8000",
|
||||
"location": "北京"
|
||||
},
|
||||
"preferences": {
|
||||
"language": "zh-CN",
|
||||
"timezone": "Asia/Shanghai",
|
||||
"notifications": {
|
||||
"email": true,
|
||||
"slack": false,
|
||||
"browser": true
|
||||
}
|
||||
},
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
### 响应 (201 Created)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "user-abc123",
|
||||
"username": "newuser",
|
||||
"email": "user@example.com",
|
||||
"role": "user",
|
||||
"groups": [
|
||||
{
|
||||
"id": "dev-team",
|
||||
"name": "开发团队",
|
||||
"role": "member"
|
||||
}
|
||||
],
|
||||
"profile": {
|
||||
"firstName": "张",
|
||||
"lastName": "三",
|
||||
"fullName": "张三",
|
||||
"department": "开发部",
|
||||
"title": "软件工程师",
|
||||
"phone": "+86-138-0013-8000",
|
||||
"location": "北京",
|
||||
"avatar": null
|
||||
},
|
||||
"preferences": {
|
||||
"language": "zh-CN",
|
||||
"timezone": "Asia/Shanghai",
|
||||
"notifications": {
|
||||
"email": true,
|
||||
"slack": false,
|
||||
"browser": true
|
||||
}
|
||||
},
|
||||
"enabled": true,
|
||||
"lastLoginAt": null,
|
||||
"createdAt": "2024-01-01T12:00:00Z",
|
||||
"updatedAt": "2024-01-01T12:00:00Z"
|
||||
},
|
||||
"message": "用户创建成功"
|
||||
}
|
||||
```
|
||||
|
||||
## 创建组
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
POST /api/groups
|
||||
```
|
||||
|
||||
### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "dev-team",
|
||||
"displayName": "开发团队",
|
||||
"description": "前端和后端开发人员",
|
||||
"parentGroup": null,
|
||||
"permissions": {
|
||||
"servers": {
|
||||
"create": false,
|
||||
"read": true,
|
||||
"update": true,
|
||||
"delete": false,
|
||||
"execute": true
|
||||
},
|
||||
"tools": {
|
||||
"filesystem": {
|
||||
"read": true,
|
||||
"write": true,
|
||||
"paths": ["/app/data", "/tmp"]
|
||||
},
|
||||
"web-search": {
|
||||
"enabled": true,
|
||||
"maxQueries": 100
|
||||
}
|
||||
},
|
||||
"monitoring": {
|
||||
"viewLogs": true,
|
||||
"viewMetrics": true,
|
||||
"exportData": false
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"autoAssign": false,
|
||||
"maxMembers": 50,
|
||||
"requireApproval": true,
|
||||
"sessionTimeout": "8h"
|
||||
},
|
||||
"quotas": {
|
||||
"requests": {
|
||||
"daily": 1000,
|
||||
"monthly": 30000
|
||||
},
|
||||
"storage": {
|
||||
"maxSize": "10GB"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 响应 (201 Created)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "group-abc123",
|
||||
"name": "dev-team",
|
||||
"displayName": "开发团队",
|
||||
"description": "前端和后端开发人员",
|
||||
"parentGroup": null,
|
||||
"permissions": {
|
||||
"servers": {
|
||||
"create": false,
|
||||
"read": true,
|
||||
"update": true,
|
||||
"delete": false,
|
||||
"execute": true
|
||||
},
|
||||
"tools": {
|
||||
"filesystem": {
|
||||
"read": true,
|
||||
"write": true,
|
||||
"paths": ["/app/data", "/tmp"]
|
||||
},
|
||||
"web-search": {
|
||||
"enabled": true,
|
||||
"maxQueries": 100
|
||||
}
|
||||
},
|
||||
"monitoring": {
|
||||
"viewLogs": true,
|
||||
"viewMetrics": true,
|
||||
"exportData": false
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"autoAssign": false,
|
||||
"maxMembers": 50,
|
||||
"requireApproval": true,
|
||||
"sessionTimeout": "8h"
|
||||
},
|
||||
"quotas": {
|
||||
"requests": {
|
||||
"daily": 1000,
|
||||
"monthly": 30000
|
||||
},
|
||||
"storage": {
|
||||
"maxSize": "10GB"
|
||||
}
|
||||
},
|
||||
"memberCount": 0,
|
||||
"serverCount": 0,
|
||||
"createdAt": "2024-01-01T12:00:00Z",
|
||||
"updatedAt": "2024-01-01T12:00:00Z",
|
||||
"createdBy": "admin"
|
||||
},
|
||||
"message": "组创建成功"
|
||||
}
|
||||
```
|
||||
|
||||
## 批量创建
|
||||
|
||||
### 批量创建服务器
|
||||
|
||||
```http
|
||||
POST /api/servers/bulk
|
||||
```
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": [
|
||||
{
|
||||
"name": "dev-server-1",
|
||||
"command": "python",
|
||||
"args": ["-m", "mcp_server"],
|
||||
"env": { "ENV": "development" }
|
||||
},
|
||||
{
|
||||
"name": "dev-server-2",
|
||||
"command": "node",
|
||||
"args": ["server.js"],
|
||||
"env": { "ENV": "development" }
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"skipExisting": true,
|
||||
"validateAll": true,
|
||||
"startAfterCreate": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应 (201 Created)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"created": [
|
||||
{
|
||||
"id": "server-1",
|
||||
"name": "dev-server-1",
|
||||
"status": "created"
|
||||
},
|
||||
{
|
||||
"id": "server-2",
|
||||
"name": "dev-server-2",
|
||||
"status": "created"
|
||||
}
|
||||
],
|
||||
"skipped": [],
|
||||
"failed": [],
|
||||
"summary": {
|
||||
"total": 2,
|
||||
"created": 2,
|
||||
"skipped": 0,
|
||||
"failed": 0
|
||||
}
|
||||
},
|
||||
"message": "批量创建完成,成功创建 2 个服务器"
|
||||
}
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
### 预验证创建请求
|
||||
|
||||
在实际创建资源之前验证请求:
|
||||
|
||||
```http
|
||||
POST /api/servers/validate
|
||||
```
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "test-server",
|
||||
"command": "invalid-command",
|
||||
"args": []
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"data": {
|
||||
"valid": false,
|
||||
"errors": [
|
||||
{
|
||||
"field": "command",
|
||||
"message": "命令 'invalid-command' 不存在或无法执行"
|
||||
}
|
||||
],
|
||||
"warnings": [
|
||||
{
|
||||
"field": "args",
|
||||
"message": "参数数组为空,服务器可能无法正常启动"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
有关更多 API 端点信息,请参阅 [获取资源](/zh/api-reference/endpoint/get)、[删除资源](/zh/api-reference/endpoint/delete) 和 [WebHooks](/zh/api-reference/endpoint/webhook) 文档。
|
||||
@@ -1,303 +0,0 @@
|
||||
---
|
||||
title: 删除资源 API
|
||||
description: 删除各种资源的 API 端点,包括服务器、组和配置等
|
||||
---
|
||||
|
||||
# 删除资源 API
|
||||
|
||||
本文档描述了用于删除各种资源的 API 端点。
|
||||
|
||||
## 删除 MCP 服务器
|
||||
|
||||
删除指定的 MCP 服务器配置。
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
DELETE /api/servers/{id}
|
||||
```
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数名 | 类型 | 位置 | 必需 | 描述 |
|
||||
| ------ | ------ | ---- | ---- | ------------------ |
|
||||
| id | string | path | 是 | 服务器的唯一标识符 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
'https://api.mcphub.io/api/servers/mcp-server-123' \
|
||||
-H 'Authorization: Bearer YOUR_API_TOKEN' \
|
||||
-H 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
### 响应
|
||||
|
||||
#### 成功响应 (204 No Content)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "服务器已成功删除",
|
||||
"data": {
|
||||
"id": "mcp-server-123",
|
||||
"deletedAt": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
**404 Not Found**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "SERVER_NOT_FOUND",
|
||||
"message": "指定的服务器不存在",
|
||||
"details": {
|
||||
"serverId": "mcp-server-123"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**409 Conflict**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "SERVER_IN_USE",
|
||||
"message": "服务器正在使用中,无法删除",
|
||||
"details": {
|
||||
"activeConnections": 5,
|
||||
"associatedGroups": ["group-1", "group-2"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 删除服务器组
|
||||
|
||||
删除指定的服务器组。
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
DELETE /api/groups/{id}
|
||||
```
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数名 | 类型 | 位置 | 必需 | 描述 |
|
||||
| ------ | ------- | ----- | ---- | ------------------------------ |
|
||||
| id | string | path | 是 | 组的唯一标识符 |
|
||||
| force | boolean | query | 否 | 是否强制删除(包含服务器的组) |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
'https://api.mcphub.io/api/groups/production-group?force=true' \
|
||||
-H 'Authorization: Bearer YOUR_API_TOKEN' \
|
||||
-H 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
### 响应
|
||||
|
||||
#### 成功响应 (204 No Content)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "服务器组已成功删除",
|
||||
"data": {
|
||||
"id": "production-group",
|
||||
"deletedServers": ["server-1", "server-2"],
|
||||
"deletedAt": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 删除配置项
|
||||
|
||||
删除指定的配置项。
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
DELETE /api/config/{key}
|
||||
```
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数名 | 类型 | 位置 | 必需 | 描述 |
|
||||
| ------ | ------ | ---- | ---- | -------- |
|
||||
| key | string | path | 是 | 配置键名 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
'https://api.mcphub.io/api/config/custom-setting' \
|
||||
-H 'Authorization: Bearer YOUR_API_TOKEN'
|
||||
```
|
||||
|
||||
### 响应
|
||||
|
||||
#### 成功响应 (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "配置项已删除",
|
||||
"data": {
|
||||
"key": "custom-setting",
|
||||
"previousValue": "old-value",
|
||||
"deletedAt": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 批量删除
|
||||
|
||||
### 批量删除服务器
|
||||
|
||||
删除多个 MCP 服务器。
|
||||
|
||||
#### 端点
|
||||
|
||||
```http
|
||||
DELETE /api/servers/batch
|
||||
```
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"serverIds": ["server-1", "server-2", "server-3"],
|
||||
"force": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "批量删除完成",
|
||||
"data": {
|
||||
"deleted": ["server-1", "server-3"],
|
||||
"failed": [
|
||||
{
|
||||
"id": "server-2",
|
||||
"reason": "服务器正在使用中"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total": 3,
|
||||
"deleted": 2,
|
||||
"failed": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 软删除 vs 硬删除
|
||||
|
||||
### 软删除
|
||||
|
||||
默认情况下,MCPHub 使用软删除机制:
|
||||
|
||||
- 资源被标记为已删除但保留在数据库中
|
||||
- 可以通过恢复 API 恢复删除的资源
|
||||
- 删除的资源在列表 API 中默认不显示
|
||||
|
||||
### 硬删除
|
||||
|
||||
使用 `permanent=true` 参数执行硬删除:
|
||||
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
'https://api.mcphub.io/api/servers/mcp-server-123?permanent=true' \
|
||||
-H 'Authorization: Bearer YOUR_API_TOKEN'
|
||||
```
|
||||
|
||||
<Warning>硬删除操作不可逆,请谨慎使用。</Warning>
|
||||
|
||||
## 权限要求
|
||||
|
||||
| 操作 | 所需权限 |
|
||||
| ---------- | ------------------------ |
|
||||
| 删除服务器 | `servers:delete` |
|
||||
| 删除组 | `groups:delete` |
|
||||
| 删除配置 | `config:delete` |
|
||||
| 硬删除 | `admin:permanent_delete` |
|
||||
|
||||
## 错误代码
|
||||
|
||||
| 错误代码 | HTTP 状态码 | 描述 |
|
||||
| -------------------------- | ----------- | ---------------- |
|
||||
| `RESOURCE_NOT_FOUND` | 404 | 资源不存在 |
|
||||
| `RESOURCE_IN_USE` | 409 | 资源正在使用中 |
|
||||
| `INSUFFICIENT_PERMISSIONS` | 403 | 权限不足 |
|
||||
| `VALIDATION_ERROR` | 400 | 请求参数验证失败 |
|
||||
| `INTERNAL_ERROR` | 500 | 服务器内部错误 |
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 删除前检查
|
||||
|
||||
在删除资源前,建议先检查资源的使用情况:
|
||||
|
||||
```bash
|
||||
# 检查服务器使用情况
|
||||
curl -X GET \
|
||||
'https://api.mcphub.io/api/servers/mcp-server-123/usage' \
|
||||
-H 'Authorization: Bearer YOUR_API_TOKEN'
|
||||
```
|
||||
|
||||
### 2. 备份重要数据
|
||||
|
||||
对于重要资源,建议在删除前进行备份:
|
||||
|
||||
```bash
|
||||
# 导出服务器配置
|
||||
curl -X GET \
|
||||
'https://api.mcphub.io/api/servers/mcp-server-123/export' \
|
||||
-H 'Authorization: Bearer YOUR_API_TOKEN' \
|
||||
> server-backup.json
|
||||
```
|
||||
|
||||
### 3. 使用事务删除
|
||||
|
||||
对于复杂的删除操作,使用事务确保数据一致性:
|
||||
|
||||
```json
|
||||
{
|
||||
"transaction": true,
|
||||
"operations": [
|
||||
{
|
||||
"type": "delete",
|
||||
"resource": "server",
|
||||
"id": "server-1"
|
||||
},
|
||||
{
|
||||
"type": "delete",
|
||||
"resource": "group",
|
||||
"id": "group-1"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 恢复删除的资源
|
||||
|
||||
软删除的资源可以通过恢复 API 恢复:
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
'https://api.mcphub.io/api/servers/mcp-server-123/restore' \
|
||||
-H 'Authorization: Bearer YOUR_API_TOKEN'
|
||||
```
|
||||
@@ -1,607 +0,0 @@
|
||||
---
|
||||
title: '获取资源'
|
||||
description: '查询和检索 MCP 服务器、用户和组信息'
|
||||
---
|
||||
|
||||
## 获取服务器列表
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
GET /api/servers
|
||||
```
|
||||
|
||||
### 查询参数
|
||||
|
||||
| 参数 | 类型 | 描述 | 示例 |
|
||||
| ---------------- | ------- | ------------------------------- | ---------------------------- |
|
||||
| `page` | integer | 页码(从 1 开始) | `?page=2` |
|
||||
| `limit` | integer | 每页记录数(默认 20,最大 100) | `?limit=50` |
|
||||
| `sort` | string | 排序字段 | `?sort=name` |
|
||||
| `order` | string | 排序顺序(asc/desc) | `?order=desc` |
|
||||
| `status` | string | 过滤服务器状态 | `?status=running` |
|
||||
| `search` | string | 搜索服务器名称或描述 | `?search=python` |
|
||||
| `group` | string | 过滤所属组 | `?group=dev-team` |
|
||||
| `tags` | string | 过滤标签(逗号分隔) | `?tags=python,production` |
|
||||
| `enabled` | boolean | 过滤启用状态 | `?enabled=true` |
|
||||
| `created_after` | string | 创建时间起始 | `?created_after=2024-01-01` |
|
||||
| `created_before` | string | 创建时间结束 | `?created_before=2024-01-31` |
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": "server-abc123",
|
||||
"name": "文件系统服务器",
|
||||
"status": "running",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"],
|
||||
"env": {
|
||||
"NODE_ENV": "production"
|
||||
},
|
||||
"cwd": "/app",
|
||||
"pid": 12345,
|
||||
"uptime": 3600000,
|
||||
"enabled": true,
|
||||
"description": "提供文件系统访问的 MCP 服务器",
|
||||
"tags": ["filesystem", "production"],
|
||||
"health": {
|
||||
"status": "healthy",
|
||||
"lastCheck": "2024-01-01T12:00:00Z",
|
||||
"responseTime": "45ms"
|
||||
},
|
||||
"resources": {
|
||||
"memory": {
|
||||
"used": "128MB",
|
||||
"limit": "512MB",
|
||||
"percentage": 25
|
||||
},
|
||||
"cpu": {
|
||||
"used": "15%",
|
||||
"limit": "50%"
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"totalRequests": 1523,
|
||||
"errorCount": 2,
|
||||
"avgResponseTime": "234ms"
|
||||
},
|
||||
"lastRestart": "2024-01-01T08:00:00Z",
|
||||
"createdAt": "2024-01-01T00:00:00Z",
|
||||
"updatedAt": "2024-01-01T12:00:00Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"total": 45,
|
||||
"pages": 3,
|
||||
"hasNext": true,
|
||||
"hasPrev": false
|
||||
},
|
||||
"filters": {
|
||||
"status": "running",
|
||||
"totalFiltered": 12
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 示例
|
||||
|
||||
```bash
|
||||
# 获取运行中的服务器,按名称排序
|
||||
curl -X GET "http://localhost:3000/api/servers?status=running&sort=name&order=asc" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 搜索包含 "python" 的服务器
|
||||
curl -X GET "http://localhost:3000/api/servers?search=python&limit=10" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 获取开发团队的服务器
|
||||
curl -X GET "http://localhost:3000/api/servers?group=dev-team" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## 获取服务器详情
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
GET /api/servers/{serverId}
|
||||
```
|
||||
|
||||
### 路径参数
|
||||
|
||||
- `serverId` (string): 服务器唯一标识符
|
||||
|
||||
### 查询参数
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------------- | ------ | ----------------------------------------------- |
|
||||
| `include` | string | 包含额外信息(逗号分隔):`logs,metrics,events` |
|
||||
| `metrics_range` | string | 指标时间范围:`1h`, `24h`, `7d` |
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "server-abc123",
|
||||
"name": "文件系统服务器",
|
||||
"status": "running",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"],
|
||||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"DEBUG": "mcp:*"
|
||||
},
|
||||
"cwd": "/app",
|
||||
"pid": 12345,
|
||||
"uptime": 3600000,
|
||||
"enabled": true,
|
||||
"description": "提供文件系统访问的 MCP 服务器",
|
||||
"tags": ["filesystem", "production"],
|
||||
"healthCheck": {
|
||||
"enabled": true,
|
||||
"interval": 30000,
|
||||
"timeout": 5000,
|
||||
"retries": 3,
|
||||
"endpoint": "/health",
|
||||
"status": "healthy",
|
||||
"lastCheck": "2024-01-01T12:00:00Z",
|
||||
"responseTime": "45ms",
|
||||
"consecutiveFailures": 0
|
||||
},
|
||||
"resources": {
|
||||
"memory": {
|
||||
"used": "128MB",
|
||||
"limit": "512MB",
|
||||
"warning": "400MB",
|
||||
"percentage": 25
|
||||
},
|
||||
"cpu": {
|
||||
"used": "15%",
|
||||
"limit": "50%",
|
||||
"cores": 4
|
||||
},
|
||||
"network": {
|
||||
"bytesIn": "1.2GB",
|
||||
"bytesOut": "890MB"
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"totalRequests": 1523,
|
||||
"successfulRequests": 1521,
|
||||
"errorCount": 2,
|
||||
"avgResponseTime": "234ms",
|
||||
"p95ResponseTime": "450ms",
|
||||
"requestsPerMinute": 25,
|
||||
"lastError": {
|
||||
"timestamp": "2024-01-01T11:30:00Z",
|
||||
"message": "Temporary connection timeout",
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"capabilities": [
|
||||
{
|
||||
"type": "tool",
|
||||
"name": "read_file",
|
||||
"description": "读取文件内容",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool",
|
||||
"name": "write_file",
|
||||
"description": "写入文件内容",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string" },
|
||||
"content": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": "dev-team",
|
||||
"name": "开发团队",
|
||||
"permissions": ["read", "write", "execute"]
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"id": "event-123",
|
||||
"type": "started",
|
||||
"timestamp": "2024-01-01T08:00:00Z",
|
||||
"message": "服务器启动成功",
|
||||
"metadata": {
|
||||
"pid": 12345,
|
||||
"startupTime": "2.3s"
|
||||
}
|
||||
}
|
||||
],
|
||||
"lastRestart": "2024-01-01T08:00:00Z",
|
||||
"createdAt": "2024-01-01T00:00:00Z",
|
||||
"updatedAt": "2024-01-01T12:00:00Z",
|
||||
"createdBy": "admin"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 示例
|
||||
|
||||
```bash
|
||||
# 获取服务器基本信息
|
||||
curl -X GET "http://localhost:3000/api/servers/server-abc123" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 获取服务器详情包含日志和指标
|
||||
curl -X GET "http://localhost:3000/api/servers/server-abc123?include=logs,metrics&metrics_range=24h" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## 获取服务器状态
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
GET /api/servers/{serverId}/status
|
||||
```
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"serverId": "server-abc123",
|
||||
"status": "running",
|
||||
"health": "healthy",
|
||||
"pid": 12345,
|
||||
"uptime": 3600000,
|
||||
"startedAt": "2024-01-01T08:00:00Z",
|
||||
"lastHealthCheck": "2024-01-01T12:00:00Z",
|
||||
"resources": {
|
||||
"memory": {
|
||||
"rss": 134217728,
|
||||
"heapTotal": 67108864,
|
||||
"heapUsed": 45088768,
|
||||
"external": 8388608
|
||||
},
|
||||
"cpu": {
|
||||
"user": 1000000,
|
||||
"system": 500000,
|
||||
"percentage": 15.5
|
||||
}
|
||||
},
|
||||
"connections": {
|
||||
"active": 5,
|
||||
"total": 127
|
||||
},
|
||||
"performance": {
|
||||
"requestsPerSecond": 12.5,
|
||||
"avgResponseTime": "234ms",
|
||||
"errorRate": "0.1%"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 获取服务器日志
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
GET /api/servers/{serverId}/logs
|
||||
```
|
||||
|
||||
### 查询参数
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| -------- | ------- | ---------------------------------------------- |
|
||||
| `level` | string | 日志级别过滤:`error`, `warn`, `info`, `debug` |
|
||||
| `limit` | integer | 返回日志条数(默认 100,最大 1000) |
|
||||
| `since` | string | 开始时间(ISO 8601 格式) |
|
||||
| `until` | string | 结束时间(ISO 8601 格式) |
|
||||
| `follow` | boolean | 实时跟踪日志流 |
|
||||
| `search` | string | 搜索日志内容 |
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"logs": [
|
||||
{
|
||||
"id": "log-123",
|
||||
"timestamp": "2024-01-01T12:00:00Z",
|
||||
"level": "info",
|
||||
"message": "处理请求: read_file",
|
||||
"source": "mcp-server",
|
||||
"metadata": {
|
||||
"requestId": "req-456",
|
||||
"userId": "user-789",
|
||||
"duration": "45ms"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "log-124",
|
||||
"timestamp": "2024-01-01T12:00:05Z",
|
||||
"level": "error",
|
||||
"message": "文件不存在: /nonexistent/file.txt",
|
||||
"source": "filesystem",
|
||||
"metadata": {
|
||||
"requestId": "req-457",
|
||||
"path": "/nonexistent/file.txt",
|
||||
"error": "ENOENT"
|
||||
}
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"limit": 100,
|
||||
"total": 1523,
|
||||
"hasMore": true,
|
||||
"nextCursor": "cursor-abc123"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 实时日志流
|
||||
|
||||
```bash
|
||||
# 实时跟踪日志
|
||||
curl -X GET "http://localhost:3000/api/servers/server-abc123/logs?follow=true" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Accept: text/event-stream"
|
||||
```
|
||||
|
||||
## 获取服务器指标
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
GET /api/servers/{serverId}/metrics
|
||||
```
|
||||
|
||||
### 查询参数
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| ------------- | ------ | ------------------------------------------- |
|
||||
| `timeRange` | string | 时间范围:`1h`, `24h`, `7d`, `30d` |
|
||||
| `granularity` | string | 数据粒度:`1m`, `5m`, `1h`, `1d` |
|
||||
| `metrics` | string | 指定指标(逗号分隔):`cpu,memory,requests` |
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"timeRange": "1h",
|
||||
"granularity": "5m",
|
||||
"metrics": {
|
||||
"cpu": {
|
||||
"data": [
|
||||
{ "timestamp": "2024-01-01T11:00:00Z", "value": 12.5 },
|
||||
{ "timestamp": "2024-01-01T11:05:00Z", "value": 15.2 }
|
||||
],
|
||||
"summary": {
|
||||
"avg": 13.8,
|
||||
"min": 8.1,
|
||||
"max": 18.5,
|
||||
"current": 15.2
|
||||
}
|
||||
},
|
||||
"memory": {
|
||||
"data": [
|
||||
{ "timestamp": "2024-01-01T11:00:00Z", "value": 125 },
|
||||
{ "timestamp": "2024-01-01T11:05:00Z", "value": 128 }
|
||||
],
|
||||
"summary": {
|
||||
"avg": 126.5,
|
||||
"min": 120,
|
||||
"max": 135,
|
||||
"current": 128
|
||||
}
|
||||
},
|
||||
"requests": {
|
||||
"data": [
|
||||
{ "timestamp": "2024-01-01T11:00:00Z", "value": 45 },
|
||||
{ "timestamp": "2024-01-01T11:05:00Z", "value": 52 }
|
||||
],
|
||||
"summary": {
|
||||
"total": 2847,
|
||||
"avg": 48.5,
|
||||
"peak": 67
|
||||
}
|
||||
},
|
||||
"responseTime": {
|
||||
"data": [
|
||||
{ "timestamp": "2024-01-01T11:00:00Z", "avg": 230, "p95": 450 },
|
||||
{ "timestamp": "2024-01-01T11:05:00Z", "avg": 245, "p95": 480 }
|
||||
],
|
||||
"summary": {
|
||||
"avgResponseTime": "237ms",
|
||||
"p95ResponseTime": "465ms"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 获取用户列表
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
GET /api/users
|
||||
```
|
||||
|
||||
### 查询参数
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| ------------------ | ------- | ---------------- |
|
||||
| `role` | string | 过滤用户角色 |
|
||||
| `group` | string | 过滤所属组 |
|
||||
| `enabled` | boolean | 过滤启用状态 |
|
||||
| `search` | string | 搜索用户名或邮箱 |
|
||||
| `last_login_after` | string | 最后登录时间起始 |
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": "user-abc123",
|
||||
"username": "zhangsan",
|
||||
"email": "zhangsan@example.com",
|
||||
"role": "user",
|
||||
"enabled": true,
|
||||
"profile": {
|
||||
"firstName": "张",
|
||||
"lastName": "三",
|
||||
"fullName": "张三",
|
||||
"department": "开发部",
|
||||
"title": "软件工程师"
|
||||
},
|
||||
"groups": [
|
||||
{
|
||||
"id": "dev-team",
|
||||
"name": "开发团队",
|
||||
"role": "member"
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"totalSessions": 45,
|
||||
"totalRequests": 1234,
|
||||
"lastRequestAt": "2024-01-01T11:30:00Z"
|
||||
},
|
||||
"lastLoginAt": "2024-01-01T08:00:00Z",
|
||||
"createdAt": "2023-12-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"total": 89,
|
||||
"pages": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 获取组列表
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
GET /api/groups
|
||||
```
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": "group-abc123",
|
||||
"name": "dev-team",
|
||||
"displayName": "开发团队",
|
||||
"description": "前端和后端开发人员",
|
||||
"memberCount": 12,
|
||||
"serverCount": 8,
|
||||
"parentGroup": null,
|
||||
"children": [],
|
||||
"permissions": {
|
||||
"servers": ["read", "write", "execute"],
|
||||
"tools": ["read", "execute"]
|
||||
},
|
||||
"quotas": {
|
||||
"requests": {
|
||||
"used": 750,
|
||||
"limit": 1000
|
||||
}
|
||||
},
|
||||
"createdAt": "2023-12-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 搜索
|
||||
|
||||
### 全局搜索
|
||||
|
||||
```http
|
||||
GET /api/search
|
||||
```
|
||||
|
||||
### 查询参数
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| ------- | ------- | ---------------------------------------------- |
|
||||
| `q` | string | 搜索关键词 |
|
||||
| `type` | string | 资源类型:`servers`, `users`, `groups`, `logs` |
|
||||
| `limit` | integer | 每种类型的最大结果数 |
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"query": "python",
|
||||
"results": {
|
||||
"servers": [
|
||||
{
|
||||
"id": "server-1",
|
||||
"name": "Python MCP Server",
|
||||
"type": "server",
|
||||
"relevance": 0.95
|
||||
}
|
||||
],
|
||||
"users": [],
|
||||
"groups": [
|
||||
{
|
||||
"id": "python-devs",
|
||||
"name": "Python 开发者",
|
||||
"type": "group",
|
||||
"relevance": 0.8
|
||||
}
|
||||
],
|
||||
"logs": [
|
||||
{
|
||||
"id": "log-123",
|
||||
"message": "Starting Python server...",
|
||||
"type": "log",
|
||||
"relevance": 0.7
|
||||
}
|
||||
]
|
||||
},
|
||||
"total": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
有关更多信息,请参阅 [创建资源](/zh/api-reference/endpoint/create)、[删除资源](/zh/api-reference/endpoint/delete) 和 [WebHooks](/zh/api-reference/endpoint/webhook) 文档。
|
||||
@@ -1,615 +0,0 @@
|
||||
---
|
||||
title: WebHooks API
|
||||
description: 配置和管理 WebHook 事件通知的完整指南
|
||||
---
|
||||
|
||||
# WebHooks API
|
||||
|
||||
WebHooks 允许 MCPHub 在特定事件发生时向您的应用程序发送实时通知。
|
||||
|
||||
## 概述
|
||||
|
||||
MCPHub WebHooks 系统支持以下功能:
|
||||
|
||||
- 实时事件通知
|
||||
- 自定义过滤器
|
||||
- 重试机制
|
||||
- 签名验证
|
||||
- 批量事件处理
|
||||
|
||||
## 支持的事件类型
|
||||
|
||||
| 事件类型 | 描述 |
|
||||
| ----------------------- | -------------- |
|
||||
| `server.created` | MCP 服务器创建 |
|
||||
| `server.updated` | MCP 服务器更新 |
|
||||
| `server.deleted` | MCP 服务器删除 |
|
||||
| `server.status_changed` | 服务器状态变更 |
|
||||
| `group.created` | 服务器组创建 |
|
||||
| `group.updated` | 服务器组更新 |
|
||||
| `group.deleted` | 服务器组删除 |
|
||||
| `user.login` | 用户登录 |
|
||||
| `user.logout` | 用户登出 |
|
||||
| `config.changed` | 配置变更 |
|
||||
| `system.error` | 系统错误 |
|
||||
|
||||
## 创建 WebHook
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
POST /api/webhooks
|
||||
```
|
||||
|
||||
### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://your-app.com/webhook",
|
||||
"events": ["server.created", "server.status_changed"],
|
||||
"secret": "your-webhook-secret",
|
||||
"active": true,
|
||||
"config": {
|
||||
"contentType": "application/json",
|
||||
"insecureSsl": false,
|
||||
"retryCount": 3,
|
||||
"timeout": 30
|
||||
},
|
||||
"filters": {
|
||||
"serverGroups": ["production", "staging"],
|
||||
"serverTypes": ["ai-assistant", "data-processor"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "webhook-123",
|
||||
"url": "https://your-app.com/webhook",
|
||||
"events": ["server.created", "server.status_changed"],
|
||||
"active": true,
|
||||
"secret": "your-webhook-secret",
|
||||
"config": {
|
||||
"contentType": "application/json",
|
||||
"insecureSsl": false,
|
||||
"retryCount": 3,
|
||||
"timeout": 30
|
||||
},
|
||||
"filters": {
|
||||
"serverGroups": ["production", "staging"],
|
||||
"serverTypes": ["ai-assistant", "data-processor"]
|
||||
},
|
||||
"createdAt": "2024-01-15T10:30:00Z",
|
||||
"updatedAt": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 获取 WebHook 列表
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
GET /api/webhooks
|
||||
```
|
||||
|
||||
### 查询参数
|
||||
|
||||
| 参数名 | 类型 | 描述 |
|
||||
| ------ | ------- | -------------------- |
|
||||
| page | integer | 页码(默认:1) |
|
||||
| limit | integer | 每页数量(默认:20) |
|
||||
| active | boolean | 过滤活跃状态 |
|
||||
| event | string | 过滤事件类型 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```bash
|
||||
curl -X GET \
|
||||
'https://api.mcphub.io/api/webhooks?active=true&limit=10' \
|
||||
-H 'Authorization: Bearer YOUR_API_TOKEN'
|
||||
```
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"webhooks": [
|
||||
{
|
||||
"id": "webhook-123",
|
||||
"url": "https://your-app.com/webhook",
|
||||
"events": ["server.created", "server.status_changed"],
|
||||
"active": true,
|
||||
"lastDelivery": "2024-01-15T09:30:00Z",
|
||||
"deliveryCount": 145,
|
||||
"failureCount": 2,
|
||||
"createdAt": "2024-01-10T10:30:00Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"limit": 10,
|
||||
"total": 25,
|
||||
"pages": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 获取单个 WebHook
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
GET /api/webhooks/{id}
|
||||
```
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "webhook-123",
|
||||
"url": "https://your-app.com/webhook",
|
||||
"events": ["server.created", "server.status_changed"],
|
||||
"active": true,
|
||||
"secret": "your-webhook-secret",
|
||||
"config": {
|
||||
"contentType": "application/json",
|
||||
"insecureSsl": false,
|
||||
"retryCount": 3,
|
||||
"timeout": 30
|
||||
},
|
||||
"filters": {
|
||||
"serverGroups": ["production", "staging"],
|
||||
"serverTypes": ["ai-assistant", "data-processor"]
|
||||
},
|
||||
"stats": {
|
||||
"totalDeliveries": 145,
|
||||
"successfulDeliveries": 143,
|
||||
"failedDeliveries": 2,
|
||||
"lastDelivery": "2024-01-15T09:30:00Z",
|
||||
"lastSuccess": "2024-01-15T09:30:00Z",
|
||||
"lastFailure": "2024-01-14T15:20:00Z"
|
||||
},
|
||||
"createdAt": "2024-01-10T10:30:00Z",
|
||||
"updatedAt": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 更新 WebHook
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
PUT /api/webhooks/{id}
|
||||
```
|
||||
|
||||
### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://your-app.com/new-webhook",
|
||||
"events": ["server.created", "server.updated", "server.deleted"],
|
||||
"active": true,
|
||||
"config": {
|
||||
"retryCount": 5,
|
||||
"timeout": 45
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 删除 WebHook
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
DELETE /api/webhooks/{id}
|
||||
```
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "WebHook 已成功删除"
|
||||
}
|
||||
```
|
||||
|
||||
## WebHook 事件格式
|
||||
|
||||
### 基本结构
|
||||
|
||||
所有 WebHook 事件都遵循以下基本结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "event-123",
|
||||
"type": "server.created",
|
||||
"timestamp": "2024-01-15T10:30:00Z",
|
||||
"version": "1.0",
|
||||
"data": {
|
||||
// 事件特定数据
|
||||
},
|
||||
"metadata": {
|
||||
"source": "mcphub",
|
||||
"environment": "production",
|
||||
"triggeredBy": "user-456"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 服务器事件示例
|
||||
|
||||
#### server.created
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "event-123",
|
||||
"type": "server.created",
|
||||
"timestamp": "2024-01-15T10:30:00Z",
|
||||
"version": "1.0",
|
||||
"data": {
|
||||
"server": {
|
||||
"id": "mcp-server-123",
|
||||
"name": "AI Assistant Server",
|
||||
"type": "ai-assistant",
|
||||
"endpoint": "https://ai-assistant.example.com",
|
||||
"group": "production",
|
||||
"status": "active",
|
||||
"capabilities": ["chat", "completion"],
|
||||
"createdAt": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"source": "mcphub",
|
||||
"environment": "production",
|
||||
"triggeredBy": "user-456"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### server.status_changed
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "event-124",
|
||||
"type": "server.status_changed",
|
||||
"timestamp": "2024-01-15T11:30:00Z",
|
||||
"version": "1.0",
|
||||
"data": {
|
||||
"server": {
|
||||
"id": "mcp-server-123",
|
||||
"name": "AI Assistant Server",
|
||||
"previousStatus": "active",
|
||||
"currentStatus": "inactive",
|
||||
"reason": "Health check failed",
|
||||
"lastHealthCheck": "2024-01-15T11:25:00Z"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"source": "mcphub",
|
||||
"environment": "production",
|
||||
"triggeredBy": "system"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 签名验证
|
||||
|
||||
MCPHub 使用 HMAC-SHA256 签名来验证 WebHook 的真实性。
|
||||
|
||||
### 签名生成
|
||||
|
||||
签名在 `X-MCPHub-Signature-256` 头中发送:
|
||||
|
||||
```
|
||||
X-MCPHub-Signature-256: sha256=5757107ea39eca8e35d1e8...
|
||||
```
|
||||
|
||||
### 验证示例
|
||||
|
||||
#### Node.js
|
||||
|
||||
```javascript
|
||||
const crypto = require('crypto');
|
||||
|
||||
function verifySignature(payload, signature, secret) {
|
||||
const expectedSignature = crypto
|
||||
.createHmac('sha256', secret)
|
||||
.update(payload, 'utf8')
|
||||
.digest('hex');
|
||||
|
||||
const actualSignature = signature.replace('sha256=', '');
|
||||
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(expectedSignature, 'hex'),
|
||||
Buffer.from(actualSignature, 'hex'),
|
||||
);
|
||||
}
|
||||
|
||||
// Express.js 中间件示例
|
||||
app.use('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
|
||||
const signature = req.headers['x-mcphub-signature-256'];
|
||||
const payload = req.body;
|
||||
|
||||
if (!verifySignature(payload, signature, process.env.WEBHOOK_SECRET)) {
|
||||
return res.status(401).send('Unauthorized');
|
||||
}
|
||||
|
||||
// 处理 WebHook 事件
|
||||
const event = JSON.parse(payload);
|
||||
console.log('收到事件:', event.type);
|
||||
|
||||
res.status(200).send('OK');
|
||||
});
|
||||
```
|
||||
|
||||
#### Python
|
||||
|
||||
```python
|
||||
import hmac
|
||||
import hashlib
|
||||
|
||||
def verify_signature(payload, signature, secret):
|
||||
expected_signature = hmac.new(
|
||||
secret.encode('utf-8'),
|
||||
payload,
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
actual_signature = signature.replace('sha256=', '')
|
||||
|
||||
return hmac.compare_digest(expected_signature, actual_signature)
|
||||
|
||||
# Flask 示例
|
||||
from flask import Flask, request, jsonify
|
||||
import json
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/webhook', methods=['POST'])
|
||||
def webhook():
|
||||
signature = request.headers.get('X-MCPHub-Signature-256')
|
||||
payload = request.get_data()
|
||||
|
||||
if not verify_signature(payload, signature, 'your-webhook-secret'):
|
||||
return jsonify({'error': 'Unauthorized'}), 401
|
||||
|
||||
event = json.loads(payload)
|
||||
print(f'收到事件: {event["type"]}')
|
||||
|
||||
return jsonify({'status': 'success'}), 200
|
||||
```
|
||||
|
||||
## 重试机制
|
||||
|
||||
MCPHub 对失败的 WebHook 交付实施指数退避重试:
|
||||
|
||||
- **重试次数**: 可配置(默认 3 次)
|
||||
- **重试间隔**: 2^n 秒(n 为重试次数)
|
||||
- **最大间隔**: 300 秒(5 分钟)
|
||||
- **超时设置**: 可配置(默认 30 秒)
|
||||
|
||||
### 重试时间表
|
||||
|
||||
| 尝试次数 | 延迟时间 |
|
||||
| -------- | -------- |
|
||||
| 1 | 立即 |
|
||||
| 2 | 2 秒 |
|
||||
| 3 | 4 秒 |
|
||||
| 4 | 8 秒 |
|
||||
| 5 | 16 秒 |
|
||||
|
||||
## 获取交付历史
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
GET /api/webhooks/{id}/deliveries
|
||||
```
|
||||
|
||||
### 查询参数
|
||||
|
||||
| 参数名 | 类型 | 描述 |
|
||||
| ---------- | ------- | ------------------------------------ |
|
||||
| page | integer | 页码 |
|
||||
| limit | integer | 每页数量 |
|
||||
| status | string | 过滤状态(success, failed, pending) |
|
||||
| event_type | string | 过滤事件类型 |
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"deliveries": [
|
||||
{
|
||||
"id": "delivery-123",
|
||||
"eventId": "event-123",
|
||||
"eventType": "server.created",
|
||||
"url": "https://your-app.com/webhook",
|
||||
"status": "success",
|
||||
"responseCode": 200,
|
||||
"responseTime": 145,
|
||||
"attempts": 1,
|
||||
"deliveredAt": "2024-01-15T10:30:15Z",
|
||||
"nextRetry": null
|
||||
},
|
||||
{
|
||||
"id": "delivery-124",
|
||||
"eventId": "event-124",
|
||||
"eventType": "server.status_changed",
|
||||
"url": "https://your-app.com/webhook",
|
||||
"status": "failed",
|
||||
"responseCode": 500,
|
||||
"responseTime": 30000,
|
||||
"attempts": 3,
|
||||
"error": "Connection timeout",
|
||||
"deliveredAt": null,
|
||||
"nextRetry": "2024-01-15T11:45:00Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"total": 145,
|
||||
"pages": 8
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 测试 WebHook
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
POST /api/webhooks/{id}/test
|
||||
```
|
||||
|
||||
### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"eventType": "server.created",
|
||||
"customData": {
|
||||
"test": true,
|
||||
"message": "这是一个测试事件"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"deliveryId": "delivery-test-123",
|
||||
"status": "delivered",
|
||||
"responseCode": 200,
|
||||
"responseTime": 124,
|
||||
"sentAt": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 幂等性处理
|
||||
|
||||
确保您的 WebHook 端点能够处理重复事件:
|
||||
|
||||
```javascript
|
||||
const processedEvents = new Set();
|
||||
|
||||
app.post('/webhook', (req, res) => {
|
||||
const event = req.body;
|
||||
|
||||
// 检查事件是否已处理
|
||||
if (processedEvents.has(event.id)) {
|
||||
return res.status(200).send('Already processed');
|
||||
}
|
||||
|
||||
// 处理事件
|
||||
processEvent(event);
|
||||
|
||||
// 记录已处理的事件
|
||||
processedEvents.add(event.id);
|
||||
|
||||
res.status(200).send('OK');
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 异步处理
|
||||
|
||||
对于复杂的处理逻辑,使用异步处理避免阻塞:
|
||||
|
||||
```javascript
|
||||
app.post('/webhook', async (req, res) => {
|
||||
const event = req.body;
|
||||
|
||||
// 立即响应
|
||||
res.status(200).send('OK');
|
||||
|
||||
// 异步处理事件
|
||||
setImmediate(() => {
|
||||
processEventAsync(event);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 错误处理
|
||||
|
||||
实施适当的错误处理和日志记录:
|
||||
|
||||
```javascript
|
||||
app.post('/webhook', (req, res) => {
|
||||
try {
|
||||
const event = req.body;
|
||||
processEvent(event);
|
||||
res.status(200).send('OK');
|
||||
} catch (error) {
|
||||
console.error('WebHook 处理错误:', error);
|
||||
res.status(500).send('Internal Server Error');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 监控和告警
|
||||
|
||||
监控 WebHook 的交付状态:
|
||||
|
||||
```bash
|
||||
# 检查失败的交付
|
||||
curl -X GET \
|
||||
'https://api.mcphub.io/api/webhooks/webhook-123/deliveries?status=failed' \
|
||||
-H 'Authorization: Bearer YOUR_API_TOKEN'
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **签名验证失败**
|
||||
|
||||
- 检查密钥是否正确
|
||||
- 确保使用原始请求体进行验证
|
||||
- 验证 HMAC 计算实现
|
||||
|
||||
2. **超时错误**
|
||||
|
||||
- 增加 WebHook 超时设置
|
||||
- 优化端点响应时间
|
||||
- 使用异步处理
|
||||
|
||||
3. **重复事件**
|
||||
- 实施幂等性检查
|
||||
- 使用事件 ID 去重
|
||||
- 记录处理状态
|
||||
|
||||
### 调试工具
|
||||
|
||||
使用 MCPHub 提供的调试工具:
|
||||
|
||||
```bash
|
||||
# 查看最近的交付日志
|
||||
curl -X GET \
|
||||
'https://api.mcphub.io/api/webhooks/webhook-123/deliveries?limit=5' \
|
||||
-H 'Authorization: Bearer YOUR_API_TOKEN'
|
||||
|
||||
# 重新发送失败的事件
|
||||
curl -X POST \
|
||||
'https://api.mcphub.io/api/webhooks/delivery-124/redeliver' \
|
||||
-H 'Authorization: Bearer YOUR_API_TOKEN'
|
||||
```
|
||||
212
docs/zh/api-reference/groups.mdx
Normal file
212
docs/zh/api-reference/groups.mdx
Normal file
@@ -0,0 +1,212 @@
|
||||
---
|
||||
title: "群组"
|
||||
description: "管理服务器群组以组织和路由请求。"
|
||||
---
|
||||
|
||||
import { Card, Cards } from 'mintlify';
|
||||
|
||||
<Card title="GET /api/groups" href="#get-all-groups">获取所有群组的列表。</Card>
|
||||
<Card title="POST /api/groups" href="#create-a-new-group">创建一个新群组。</Card>
|
||||
<Card title="GET /api/groups/:id" href="#get-a-group">获取特定群组的详细信息。</Card>
|
||||
<Card title="PUT /api/groups/:id" href="#update-a-group">更新现有群组。</Card>
|
||||
<Card title="DELETE /api/groups/:id" href="#delete-a-group">删除一个群组。</Card>
|
||||
<Card title="POST /api/groups/:id/servers" href="#add-server-to-group">将服务器添加到群组。</Card>
|
||||
<Card title="DELETE /api/groups/:id/servers/:serverName" href="#remove-server-from-group">从群组中删除服务器。</Card>
|
||||
<Card title="PUT /api/groups/:id/servers/batch" href="#batch-update-group-servers">批量更新群组中的服务器。</Card>
|
||||
<Card title="GET /api/groups/:id/server-configs" href="#get-group-server-configs">获取群组中详细的服务器配置。</Card>
|
||||
<Card title="PUT /api/groups/:id/server-configs/:serverName/tools" href="#update-group-server-tools">更新群组中服务器的工具选择。</Card>
|
||||
|
||||
---
|
||||
|
||||
### 获取所有群组
|
||||
|
||||
检索所有服务器群组的列表。
|
||||
|
||||
- **端点**: `/api/groups`
|
||||
- **方法**: `GET`
|
||||
- **成功响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": "group-1",
|
||||
"name": "我的群组",
|
||||
"description": "服务器的集合。",
|
||||
"servers": ["server1", "server2"],
|
||||
"owner": "admin"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 创建一个新群组
|
||||
|
||||
创建一个新的服务器群组。
|
||||
|
||||
- **端点**: `/api/groups`
|
||||
- **方法**: `POST`
|
||||
- **正文**:
|
||||
- `name` (string, 必填): 群组的名称。
|
||||
- `description` (string, 可选): 群组的描述。
|
||||
- `servers` (array of strings, 可选): 要包含在群组中的服务器名称列表。
|
||||
- **请求示例**:
|
||||
```json
|
||||
{
|
||||
"name": "我的新群组",
|
||||
"description": "新群组的描述",
|
||||
"servers": ["server1", "server2"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 获取一个群组
|
||||
|
||||
通过 ID 或名称检索特定群组的详细信息。
|
||||
|
||||
- **端点**: `/api/groups/:id`
|
||||
- **方法**: `GET`
|
||||
- **参数**:
|
||||
- `:id` (string, 必填): 群组的 ID 或名称。
|
||||
|
||||
---
|
||||
|
||||
### 更新一个群组
|
||||
|
||||
更新现有群组的名称、描述或服务器列表。
|
||||
|
||||
- **端点**: `/api/groups/:id`
|
||||
- **方法**: `PUT`
|
||||
- **参数**:
|
||||
- `:id` (string, 必填): 要更新的群组的 ID 或名称。
|
||||
- **正文**:
|
||||
- `name` (string, 可选): 群组的新名称。
|
||||
- `description` (string, 可选): 群组的新描述。
|
||||
- `servers` (array, 可选): 群组的新服务器列表。格式请参阅 [批量更新群组服务器](#batch-update-group-servers)。
|
||||
- **请求示例**:
|
||||
```json
|
||||
{
|
||||
"name": "更新后的群组名称",
|
||||
"description": "更新后的描述"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 删除一个群组
|
||||
|
||||
通过 ID 或名称删除一个群组。
|
||||
|
||||
- **端点**: `/api/groups/:id`
|
||||
- **方法**: `DELETE`
|
||||
- **参数**:
|
||||
- `:id` (string, 必填): 要删除的群组的 ID 或名称。
|
||||
|
||||
---
|
||||
|
||||
### 将服务器添加到群组
|
||||
|
||||
将单个服务器添加到群组。
|
||||
|
||||
- **端点**: `/api/groups/:id/servers`
|
||||
- **方法**: `POST`
|
||||
- **参数**:
|
||||
- `:id` (string, 必填): 群组的 ID 或名称。
|
||||
- **正文**:
|
||||
- `serverName` (string, 必填): 要添加的服务器的名称。
|
||||
- **请求示例**:
|
||||
```json
|
||||
{
|
||||
"serverName": "my-server"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 从群组中删除服务器
|
||||
|
||||
从群组中删除单个服务器。
|
||||
|
||||
- **端点**: `/api/groups/:id/servers/:serverName`
|
||||
- **方法**: `DELETE`
|
||||
- **参数**:
|
||||
- `:id` (string, 必填): 群组的 ID 或名称。
|
||||
- `:serverName` (string, 必填): 要删除的服务器的名称。
|
||||
|
||||
---
|
||||
|
||||
### 批量更新群组服务器
|
||||
|
||||
用新的列表替换群组中的所有服务器。该列表可以是简单的字符串或详细的配置对象。
|
||||
|
||||
- **端点**: `/api/groups/:id/servers/batch`
|
||||
- **方法**: `PUT`
|
||||
- **参数**:
|
||||
- `:id` (string, 必填): 群组的 ID 或名称。
|
||||
- **正文**:
|
||||
- `servers` (array, 必填): 服务器名称(字符串)或服务器配置对象的数组。
|
||||
- **请求示例 (简单)**:
|
||||
```json
|
||||
{
|
||||
"servers": ["server1", "server2"]
|
||||
}
|
||||
```
|
||||
- **请求示例 (详细)**:
|
||||
```json
|
||||
{
|
||||
"servers": [
|
||||
{ "name": "server1", "tools": "all" },
|
||||
{ "name": "server2", "tools": ["toolA", "toolB"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 获取群组服务器配置
|
||||
|
||||
检索群组内所有服务器的详细配置,包括启用了哪些工具。
|
||||
|
||||
- **端点**: `/api/groups/:id/server-configs`
|
||||
- **方法**: `GET`
|
||||
- **参数**:
|
||||
- `:id` (string, 必填): 群组的 ID 或名称。
|
||||
- **成功响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"name": "server1",
|
||||
"tools": "all"
|
||||
},
|
||||
{
|
||||
"name": "server2",
|
||||
"tools": ["toolA", "toolB"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 更新群组服务器工具
|
||||
|
||||
更新群组内特定服务器的工具选择。
|
||||
|
||||
- **端点**: `/api/groups/:id/server-configs/:serverName/tools`
|
||||
- **方法**: `PUT`
|
||||
- **参数**:
|
||||
- `:id` (string, 必填): 群组的 ID 或名称。
|
||||
- `:serverName` (string, 必填): 要更新的服务器的名称。
|
||||
- **正文**:
|
||||
- `tools` (string or array of strings, 必填): 字符串 `"all"` 表示启用所有工具,或一个工具名称数组以指定启用哪些工具。
|
||||
- **请求示例**:
|
||||
```json
|
||||
{
|
||||
"tools": ["toolA", "toolC"]
|
||||
}
|
||||
```
|
||||
@@ -1,717 +1,13 @@
|
||||
---
|
||||
title: 'API 参考'
|
||||
description: 'MCPHub REST API 完整参考文档'
|
||||
title: "介绍"
|
||||
description: "欢迎来到 MCPHub API 文档。"
|
||||
---
|
||||
|
||||
## 概述
|
||||
MCPHub API 提供了一整套端点来管理您的 MCP 服务器、群组、用户等。该 API 分为两个主要类别:
|
||||
|
||||
MCPHub 提供全面的 REST API,用于管理 MCP 服务器、用户、组和监控。所有 API 端点都需要身份验证,并支持 JSON 格式的请求和响应。
|
||||
- **MCP 端点**: 这些是与您的 MCP 服务器交互的主要端点。它们提供了一个统一的界面,用于向您的服务器发送请求并实时接收响应。
|
||||
- **管理 API**: 这些端点用于管理 MCPHub 实例本身。这包括管理服务器、群组、用户和系统设置。
|
||||
|
||||
## 基础信息
|
||||
所有 API 端点都在 `/api` 路径下可用。例如,获取所有服务器的端点是 `/api/servers`。
|
||||
|
||||
### 基础 URL
|
||||
|
||||
```
|
||||
https://your-mcphub-instance.com/api
|
||||
```
|
||||
|
||||
### 身份验证
|
||||
|
||||
所有 API 请求都需要身份验证。支持以下方法:
|
||||
|
||||
#### JWT 令牌认证
|
||||
|
||||
```bash
|
||||
curl -X GET https://api.mcphub.com/servers \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
|
||||
#### API 密钥认证
|
||||
|
||||
```bash
|
||||
curl -X GET https://api.mcphub.com/servers \
|
||||
-H "X-API-Key: YOUR_API_KEY"
|
||||
```
|
||||
|
||||
### 请求格式
|
||||
|
||||
- **Content-Type**: `application/json`
|
||||
- **Accept**: `application/json`
|
||||
- **User-Agent**: 建议包含您的应用程序名称和版本
|
||||
|
||||
### 响应格式
|
||||
|
||||
所有响应都采用 JSON 格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
// 响应数据
|
||||
},
|
||||
"message": "操作成功",
|
||||
"timestamp": "2024-01-01T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
错误响应格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "请求数据无效",
|
||||
"details": {
|
||||
"field": "name",
|
||||
"reason": "名称不能为空"
|
||||
}
|
||||
},
|
||||
"timestamp": "2024-01-01T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 状态码
|
||||
|
||||
| 状态码 | 说明 |
|
||||
| ------ | -------------------- |
|
||||
| 200 | 请求成功 |
|
||||
| 201 | 资源创建成功 |
|
||||
| 204 | 请求成功,无返回内容 |
|
||||
| 400 | 请求参数错误 |
|
||||
| 401 | 未授权访问 |
|
||||
| 403 | 权限不足 |
|
||||
| 404 | 资源不存在 |
|
||||
| 409 | 资源冲突 |
|
||||
| 422 | 请求数据验证失败 |
|
||||
| 429 | 请求频率超限 |
|
||||
| 500 | 服务器内部错误 |
|
||||
|
||||
## 分页
|
||||
|
||||
支持分页的端点使用以下参数:
|
||||
|
||||
- `page`: 页码(从 1 开始)
|
||||
- `limit`: 每页记录数(默认 20,最大 100)
|
||||
- `sort`: 排序字段
|
||||
- `order`: 排序顺序(`asc` 或 `desc`)
|
||||
|
||||
```bash
|
||||
curl -X GET "https://api.mcphub.com/servers?page=2&limit=50&sort=name&order=asc" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
分页响应格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"items": [...],
|
||||
"pagination": {
|
||||
"page": 2,
|
||||
"limit": 50,
|
||||
"total": 234,
|
||||
"pages": 5,
|
||||
"hasNext": true,
|
||||
"hasPrev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 过滤和搜索
|
||||
|
||||
支持过滤的端点可以使用以下参数:
|
||||
|
||||
- `search`: 全文搜索
|
||||
- `filter[field]`: 字段过滤
|
||||
- `status`: 状态过滤
|
||||
- `created_after`: 创建时间筛选
|
||||
- `created_before`: 创建时间筛选
|
||||
|
||||
```bash
|
||||
curl -X GET "https://api.mcphub.com/servers?search=python&filter[status]=running&created_after=2024-01-01" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## API 端点
|
||||
|
||||
### 服务器管理
|
||||
|
||||
#### 获取服务器列表
|
||||
|
||||
```http
|
||||
GET /api/servers
|
||||
```
|
||||
|
||||
参数:
|
||||
|
||||
- `status` (可选): 过滤服务器状态 (`running`, `stopped`, `error`)
|
||||
- `group` (可选): 过滤所属组
|
||||
- `search` (可选): 搜索服务器名称或描述
|
||||
|
||||
示例响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": "server-1",
|
||||
"name": "文件系统服务器",
|
||||
"status": "running",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"],
|
||||
"env": {
|
||||
"NODE_ENV": "production"
|
||||
},
|
||||
"cwd": "/app",
|
||||
"pid": 12345,
|
||||
"uptime": 3600000,
|
||||
"lastRestart": "2024-01-01T12:00:00Z",
|
||||
"createdAt": "2024-01-01T10:00:00Z",
|
||||
"updatedAt": "2024-01-01T12:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 创建服务器
|
||||
|
||||
```http
|
||||
POST /api/servers
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "新服务器",
|
||||
"command": "python",
|
||||
"args": ["-m", "mcp_server"],
|
||||
"env": {
|
||||
"API_KEY": "your-api-key",
|
||||
"LOG_LEVEL": "INFO"
|
||||
},
|
||||
"cwd": "/app/python-server",
|
||||
"enabled": true,
|
||||
"description": "Python MCP 服务器",
|
||||
"tags": ["python", "production"]
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取服务器详情
|
||||
|
||||
```http
|
||||
GET /api/servers/{serverId}
|
||||
```
|
||||
|
||||
#### 更新服务器
|
||||
|
||||
```http
|
||||
PUT /api/servers/{serverId}
|
||||
```
|
||||
|
||||
#### 删除服务器
|
||||
|
||||
```http
|
||||
DELETE /api/servers/{serverId}
|
||||
```
|
||||
|
||||
#### 启动服务器
|
||||
|
||||
```http
|
||||
POST /api/servers/{serverId}/start
|
||||
```
|
||||
|
||||
#### 停止服务器
|
||||
|
||||
```http
|
||||
POST /api/servers/{serverId}/stop
|
||||
```
|
||||
|
||||
请求体(可选):
|
||||
|
||||
```json
|
||||
{
|
||||
"graceful": true,
|
||||
"timeout": 30000
|
||||
}
|
||||
```
|
||||
|
||||
#### 重启服务器
|
||||
|
||||
```http
|
||||
POST /api/servers/{serverId}/restart
|
||||
```
|
||||
|
||||
#### 获取服务器日志
|
||||
|
||||
```http
|
||||
GET /api/servers/{serverId}/logs
|
||||
```
|
||||
|
||||
参数:
|
||||
|
||||
- `level` (可选): 日志级别过滤
|
||||
- `limit` (可选): 返回日志条数
|
||||
- `since` (可选): 开始时间
|
||||
- `follow` (可选): 实时跟踪日志
|
||||
|
||||
### 用户管理
|
||||
|
||||
#### 获取用户列表
|
||||
|
||||
```http
|
||||
GET /api/users
|
||||
```
|
||||
|
||||
#### 创建用户
|
||||
|
||||
```http
|
||||
POST /api/users
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "newuser",
|
||||
"email": "user@example.com",
|
||||
"password": "securepassword",
|
||||
"role": "user",
|
||||
"groups": ["dev-team"],
|
||||
"profile": {
|
||||
"firstName": "张",
|
||||
"lastName": "三",
|
||||
"department": "开发部"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取用户详情
|
||||
|
||||
```http
|
||||
GET /api/users/{userId}
|
||||
```
|
||||
|
||||
#### 更新用户
|
||||
|
||||
```http
|
||||
PUT /api/users/{userId}
|
||||
```
|
||||
|
||||
#### 删除用户
|
||||
|
||||
```http
|
||||
DELETE /api/users/{userId}
|
||||
```
|
||||
|
||||
### 组管理
|
||||
|
||||
#### 获取组列表
|
||||
|
||||
```http
|
||||
GET /api/groups
|
||||
```
|
||||
|
||||
#### 创建组
|
||||
|
||||
```http
|
||||
POST /api/groups
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "dev-team",
|
||||
"displayName": "开发团队",
|
||||
"description": "前端和后端开发人员",
|
||||
"parentGroup": null,
|
||||
"permissions": {
|
||||
"servers": ["read", "write", "execute"],
|
||||
"tools": ["read", "execute"]
|
||||
},
|
||||
"settings": {
|
||||
"autoAssign": false,
|
||||
"maxMembers": 50,
|
||||
"requireApproval": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 添加用户到组
|
||||
|
||||
```http
|
||||
POST /api/groups/{groupId}/members
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"userId": "user123",
|
||||
"role": "member"
|
||||
}
|
||||
```
|
||||
|
||||
#### 从组中移除用户
|
||||
|
||||
```http
|
||||
DELETE /api/groups/{groupId}/members/{userId}
|
||||
```
|
||||
|
||||
#### 分配服务器到组
|
||||
|
||||
```http
|
||||
POST /api/groups/{groupId}/servers
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"serverId": "server-1",
|
||||
"permissions": ["read", "write", "execute"]
|
||||
}
|
||||
```
|
||||
|
||||
### 身份验证
|
||||
|
||||
#### 登录
|
||||
|
||||
```http
|
||||
POST /api/auth/login
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "password",
|
||||
"mfaCode": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"refreshToken": "refresh_token_here",
|
||||
"expiresIn": 86400,
|
||||
"user": {
|
||||
"id": "user123",
|
||||
"username": "admin",
|
||||
"role": "admin",
|
||||
"permissions": ["*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 刷新令牌
|
||||
|
||||
```http
|
||||
POST /api/auth/refresh
|
||||
```
|
||||
|
||||
#### 注销
|
||||
|
||||
```http
|
||||
POST /api/auth/logout
|
||||
```
|
||||
|
||||
#### 验证令牌
|
||||
|
||||
```http
|
||||
GET /api/auth/verify
|
||||
```
|
||||
|
||||
### 监控
|
||||
|
||||
#### 获取系统状态
|
||||
|
||||
```http
|
||||
GET /api/monitoring/status
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"system": {
|
||||
"uptime": 86400,
|
||||
"version": "2.1.0",
|
||||
"nodeVersion": "18.17.0"
|
||||
},
|
||||
"servers": {
|
||||
"total": 12,
|
||||
"running": 10,
|
||||
"stopped": 1,
|
||||
"error": 1
|
||||
},
|
||||
"performance": {
|
||||
"requestsPerMinute": 85,
|
||||
"avgResponseTime": "245ms",
|
||||
"errorRate": "0.3%"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取性能指标
|
||||
|
||||
```http
|
||||
GET /api/monitoring/metrics
|
||||
```
|
||||
|
||||
参数:
|
||||
|
||||
- `timeRange`: 时间范围 (`1h`, `24h`, `7d`, `30d`)
|
||||
- `granularity`: 数据粒度 (`1m`, `5m`, `1h`, `1d`)
|
||||
- `metrics`: 指定指标名称(逗号分隔)
|
||||
|
||||
#### 获取日志
|
||||
|
||||
```http
|
||||
GET /api/monitoring/logs
|
||||
```
|
||||
|
||||
参数:
|
||||
|
||||
- `level`: 日志级别
|
||||
- `source`: 日志源
|
||||
- `limit`: 返回条数
|
||||
- `since`: 开始时间
|
||||
- `until`: 结束时间
|
||||
|
||||
### 配置管理
|
||||
|
||||
#### 获取系统配置
|
||||
|
||||
```http
|
||||
GET /api/config
|
||||
```
|
||||
|
||||
#### 更新系统配置
|
||||
|
||||
```http
|
||||
PUT /api/config
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"smtp": {
|
||||
"host": "smtp.example.com",
|
||||
"port": 587,
|
||||
"secure": false,
|
||||
"auth": {
|
||||
"user": "noreply@example.com",
|
||||
"pass": "password"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"email": true,
|
||||
"slack": true,
|
||||
"webhook": "https://hooks.example.com/notifications"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## WebSocket API
|
||||
|
||||
MCPHub 支持 WebSocket 连接以获取实时更新。
|
||||
|
||||
### 连接
|
||||
|
||||
```javascript
|
||||
const ws = new WebSocket('wss://api.mcphub.com/ws');
|
||||
ws.onopen = function () {
|
||||
// 发送认证消息
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'auth',
|
||||
token: 'YOUR_JWT_TOKEN',
|
||||
}),
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 订阅事件
|
||||
|
||||
```javascript
|
||||
// 订阅服务器状态更新
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'subscribe',
|
||||
channel: 'server-status',
|
||||
filters: {
|
||||
serverId: 'server-1',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// 订阅系统监控
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'subscribe',
|
||||
channel: 'monitoring',
|
||||
metrics: ['cpu', 'memory', 'requests'],
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
### 事件类型
|
||||
|
||||
- `server-status`: 服务器状态变化
|
||||
- `server-logs`: 实时日志流
|
||||
- `monitoring`: 系统监控指标
|
||||
- `alerts`: 系统警报
|
||||
- `user-activity`: 用户活动事件
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 错误代码
|
||||
|
||||
| 错误代码 | 描述 |
|
||||
| ----------------------- | -------------- |
|
||||
| `INVALID_REQUEST` | 请求格式无效 |
|
||||
| `AUTHENTICATION_FAILED` | 身份验证失败 |
|
||||
| `AUTHORIZATION_FAILED` | 权限不足 |
|
||||
| `RESOURCE_NOT_FOUND` | 资源不存在 |
|
||||
| `RESOURCE_CONFLICT` | 资源冲突 |
|
||||
| `VALIDATION_ERROR` | 数据验证失败 |
|
||||
| `RATE_LIMIT_EXCEEDED` | 请求频率超限 |
|
||||
| `SERVER_ERROR` | 服务器内部错误 |
|
||||
|
||||
### 错误处理示例
|
||||
|
||||
```javascript
|
||||
async function handleApiRequest() {
|
||||
try {
|
||||
const response = await fetch('/api/servers', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
switch (data.error.code) {
|
||||
case 'AUTHENTICATION_FAILED':
|
||||
// 重新登录
|
||||
redirectToLogin();
|
||||
break;
|
||||
case 'RATE_LIMIT_EXCEEDED':
|
||||
// 延迟重试
|
||||
setTimeout(() => handleApiRequest(), 5000);
|
||||
break;
|
||||
default:
|
||||
// 显示错误消息
|
||||
showError(data.error.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理成功响应
|
||||
handleSuccessResponse(data.data);
|
||||
} catch (error) {
|
||||
// 处理网络错误
|
||||
console.error('网络请求失败:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 速率限制
|
||||
|
||||
API 实施速率限制以防止滥用:
|
||||
|
||||
- **默认限制**: 每分钟 100 请求
|
||||
- **认证用户**: 每分钟 1000 请求
|
||||
- **管理员**: 每分钟 5000 请求
|
||||
|
||||
响应头包含速率限制信息:
|
||||
|
||||
```
|
||||
X-RateLimit-Limit: 1000
|
||||
X-RateLimit-Remaining: 999
|
||||
X-RateLimit-Reset: 1609459200
|
||||
```
|
||||
|
||||
## SDK 和客户端库
|
||||
|
||||
### JavaScript/Node.js
|
||||
|
||||
```bash
|
||||
npm install @mcphub/sdk
|
||||
```
|
||||
|
||||
```javascript
|
||||
import { MCPHubClient } from '@mcphub/sdk';
|
||||
|
||||
const client = new MCPHubClient({
|
||||
baseURL: 'https://api.mcphub.com',
|
||||
token: 'YOUR_JWT_TOKEN',
|
||||
});
|
||||
|
||||
// 获取服务器列表
|
||||
const servers = await client.servers.list();
|
||||
|
||||
// 创建服务器
|
||||
const newServer = await client.servers.create({
|
||||
name: '新服务器',
|
||||
command: 'python',
|
||||
args: ['-m', 'mcp_server'],
|
||||
});
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```bash
|
||||
pip install mcphub-sdk
|
||||
```
|
||||
|
||||
```python
|
||||
from mcphub_sdk import MCPHubClient
|
||||
|
||||
client = MCPHubClient(
|
||||
base_url='https://api.mcphub.com',
|
||||
token='YOUR_JWT_TOKEN'
|
||||
)
|
||||
|
||||
# 获取服务器列表
|
||||
servers = client.servers.list()
|
||||
|
||||
# 创建服务器
|
||||
new_server = client.servers.create(
|
||||
name='新服务器',
|
||||
command='python',
|
||||
args=['-m', 'mcp_server']
|
||||
)
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用 HTTPS**: 始终通过 HTTPS 访问 API
|
||||
2. **安全存储令牌**: 不要在客户端代码中硬编码令牌
|
||||
3. **处理错误**: 实施适当的错误处理和重试逻辑
|
||||
4. **遵守速率限制**: 监控速率限制并实施退避策略
|
||||
5. **使用分页**: 对于大数据集使用分页参数
|
||||
6. **缓存响应**: 适当缓存 API 响应以减少请求
|
||||
7. **版本控制**: 使用 API 版本号以确保兼容性
|
||||
|
||||
有关更多信息,请参阅我们的 [SDK 文档](https://docs.mcphub.com/sdk) 和 [示例代码](https://github.com/mcphub/examples)。
|
||||
大多数管理 API 端点都需要身份验证。有关更多详细信息,请参阅[身份验证](/api-reference/auth)部分。
|
||||
81
docs/zh/api-reference/logs.mdx
Normal file
81
docs/zh/api-reference/logs.mdx
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: "日志"
|
||||
description: "访问和管理服务器日志。"
|
||||
---
|
||||
|
||||
import { Card, Cards } from 'mintlify';
|
||||
|
||||
<Card
|
||||
title="GET /api/logs"
|
||||
href="#get-all-logs"
|
||||
>
|
||||
获取所有日志。
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="DELETE /api/logs"
|
||||
href="#clear-logs"
|
||||
>
|
||||
清除所有日志。
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="GET /api/logs/stream"
|
||||
href="#stream-logs"
|
||||
>
|
||||
实时流式传输日志。
|
||||
</Card>
|
||||
|
||||
---
|
||||
|
||||
### 获取所有日志
|
||||
|
||||
检索所有存储的日志。
|
||||
|
||||
- **端点**: `/api/logs`
|
||||
- **方法**: `GET`
|
||||
- **成功响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"timestamp": "2023-10-27T10:00:00.000Z",
|
||||
"level": "info",
|
||||
"message": "服务器成功启动。",
|
||||
"service": "system"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 清除日志
|
||||
|
||||
删除所有存储的日志。
|
||||
|
||||
- **端点**: `/api/logs`
|
||||
- **方法**: `DELETE`
|
||||
- **成功响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "日志清除成功"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 流式传输日志
|
||||
|
||||
使用服务器发送事件 (SSE) 实时流式传输日志。连接将保持打开状态,新的日志条目将在发生时发送。
|
||||
|
||||
- **端点**: `/api/logs/stream`
|
||||
- **方法**: `GET`
|
||||
- **响应格式**: 该流发送带有包含 JSON 对象的 `data` 字段的事件。第一个事件的 `type` 为 `initial`,包含所有历史日志。后续事件的 `type` 为 `log`,包含单个新日志条目。
|
||||
|
||||
- **事件示例**:
|
||||
```
|
||||
data: {"type":"log","log":{"timestamp":"2023-10-27T10:00:05.000Z","level":"debug","message":"正在处理 /api/some-endpoint 的请求","service":"mcp-server"}}
|
||||
```
|
||||
33
docs/zh/api-reference/mcp-http.mdx
Normal file
33
docs/zh/api-reference/mcp-http.mdx
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: "MCP HTTP 端点"
|
||||
description: "使用统一的 HTTP 端点连接到您的 MCP 服务器。"
|
||||
---
|
||||
|
||||
MCPHub 为您的所有 MCP 服务器提供统一的可流式 HTTP 接口。这使您可以向任何配置的 MCP 服务器发送请求并实时接收响应。
|
||||
|
||||
### 统一端点
|
||||
|
||||
此端点提供对所有已启用的 MCP 服务器的访问。
|
||||
|
||||
- **端点**: `http://localhost:3000/mcp`
|
||||
- **方法**: `POST`
|
||||
|
||||
### 特定群组的端点
|
||||
|
||||
要定向访问特定的服务器群组,请使用基于群组的 HTTP 端点。
|
||||
|
||||
- **端点**: `http://localhost:3000/mcp/{group}`
|
||||
- **方法**: `POST`
|
||||
- **参数**:
|
||||
- `{group}`: 群组的 ID 或名称。
|
||||
|
||||
### 特定服务器的端点
|
||||
|
||||
要直接访问单个服务器,请使用特定于服务器的 HTTP 端点。
|
||||
|
||||
- **端点**: `http://localhost:3000/mcp/{server}`
|
||||
- **方法**: `POST`
|
||||
- **参数**:
|
||||
- `{server}`: 服务器的名称。
|
||||
|
||||
> **注意**: 如果服务器名称和群组名称相同,则群组将优先。
|
||||
25
docs/zh/api-reference/mcp-sse.mdx
Normal file
25
docs/zh/api-reference/mcp-sse.mdx
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: "MCP SSE 端点 (已弃用)"
|
||||
description: "使用 SSE 端点连接到您的 MCP 服务器。"
|
||||
---
|
||||
|
||||
SSE 端点已弃用,并将在未来版本中删除。请改用 [MCP HTTP 端点](/api-reference/mcp-http)。
|
||||
|
||||
### 统一端点
|
||||
|
||||
- **端点**: `http://localhost:3000/sse`
|
||||
- **方法**: `GET`
|
||||
|
||||
### 特定群组的端点
|
||||
|
||||
- **端点**: `http://localhost:3000/sse/{group}`
|
||||
- **方法**: `GET`
|
||||
- **参数**:
|
||||
- `{group}`: 群组的 ID 或名称。
|
||||
|
||||
### 特定服务器的端点
|
||||
|
||||
- **端点**: `http://localhost:3000/sse/{server}`
|
||||
- **方法**: `GET`
|
||||
- **参数**:
|
||||
- `{server}`: 服务器的名称。
|
||||
209
docs/zh/api-reference/servers.mdx
Normal file
209
docs/zh/api-reference/servers.mdx
Normal file
@@ -0,0 +1,209 @@
|
||||
---
|
||||
title: "服务器"
|
||||
description: "管理您的 MCP 服务器。"
|
||||
---
|
||||
|
||||
import { Card, Cards } from 'mintlify';
|
||||
|
||||
<Card
|
||||
title="GET /api/servers"
|
||||
href="#get-all-servers"
|
||||
>
|
||||
获取所有 MCP 服务器的列表。
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="POST /api/servers"
|
||||
href="#create-a-new-server"
|
||||
>
|
||||
创建一个新的 MCP 服务器。
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="PUT /api/servers/:name"
|
||||
href="#update-a-server"
|
||||
>
|
||||
更新现有的 MCP 服务器。
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="DELETE /api/servers/:name"
|
||||
href="#delete-a-server"
|
||||
>
|
||||
删除一个 MCP 服务器。
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="POST /api/servers/:name/toggle"
|
||||
href="#toggle-a-server"
|
||||
>
|
||||
切换服务器的启用状态。
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="POST /api/servers/:serverName/tools/:toolName/toggle"
|
||||
href="#toggle-a-tool"
|
||||
>
|
||||
切换工具的启用状态。
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="PUT /api/servers/:serverName/tools/:toolName/description"
|
||||
href="#update-tool-description"
|
||||
>
|
||||
更新工具的描述。
|
||||
</Card>
|
||||
|
||||
---
|
||||
|
||||
### 获取所有服务器
|
||||
|
||||
检索所有已配置的 MCP 服务器的列表,包括其状态和可用工具。
|
||||
|
||||
- **端点**: `/api/servers`
|
||||
- **方法**: `GET`
|
||||
- **响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"name": "example-server",
|
||||
"status": "connected",
|
||||
"tools": [
|
||||
{
|
||||
"name": "tool1",
|
||||
"description": "工具1的描述"
|
||||
}
|
||||
],
|
||||
"config": {
|
||||
"type": "stdio",
|
||||
"command": "node",
|
||||
"args": ["server.js"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 创建一个新服务器
|
||||
|
||||
将一个新的 MCP 服务器添加到配置中。
|
||||
|
||||
- **端点**: `/api/servers`
|
||||
- **方法**: `POST`
|
||||
- **正文**:
|
||||
```json
|
||||
{
|
||||
"name": "my-new-server",
|
||||
"config": {
|
||||
"type": "stdio",
|
||||
"command": "python",
|
||||
"args": ["-u", "my_script.py"],
|
||||
"owner": "admin"
|
||||
}
|
||||
}
|
||||
```
|
||||
- `name` (string, 必填): 服务器的唯一名称。
|
||||
- `config` (object, 必填): 服务器配置对象。
|
||||
- `type` (string): `stdio`、`sse`、`streamable-http` 或 `openapi`。
|
||||
- `command` (string): `stdio` 类型要执行的命令。
|
||||
- `args` (array of strings): 命令的参数。
|
||||
- `url` (string): `sse`、`streamable-http` 或 `openapi` 类型的 URL。
|
||||
- `openapi` (object): OpenAPI 配置。
|
||||
- `url` (string): OpenAPI 模式的 URL。
|
||||
- `schema` (object): OpenAPI 模式对象本身。
|
||||
- `headers` (object): `sse`、`streamable-http` 和 `openapi` 类型请求要发送的标头。
|
||||
- `keepAliveInterval` (number): `sse` 类型的保持活动间隔(毫秒)。默认为 60000。
|
||||
- `owner` (string): 服务器的所有者。默认为当前用户或“admin”。
|
||||
|
||||
---
|
||||
|
||||
### 更新一个服务器
|
||||
|
||||
更新现有 MCP 服务器的配置。
|
||||
|
||||
- **端点**: `/api/servers/:name`
|
||||
- **方法**: `PUT`
|
||||
- **参数**:
|
||||
- `:name` (string, 必填): 要更新的服务器的名称。
|
||||
- **正文**:
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"type": "stdio",
|
||||
"command": "node",
|
||||
"args": ["new_server.js"]
|
||||
}
|
||||
}
|
||||
```
|
||||
- `config` (object, 必填): 更新后的服务器配置对象。详情请参阅“创建一个新服务器”。
|
||||
|
||||
---
|
||||
|
||||
### 删除一个服务器
|
||||
|
||||
从配置中删除一个 MCP 服务器。
|
||||
|
||||
- **端点**: `/api/servers/:name`
|
||||
- **方法**: `DELETE`
|
||||
- **参数**:
|
||||
- `:name` (string, 必填): 要删除的服务器的名称。
|
||||
|
||||
---
|
||||
|
||||
### 切换一个服务器
|
||||
|
||||
启用或禁用一个 MCP 服务器。
|
||||
|
||||
- **端点**: `/api/servers/:name/toggle`
|
||||
- **方法**: `POST`
|
||||
- **参数**:
|
||||
- `:name` (string, 必填): 要切换的服务器的名称。
|
||||
- **正文**:
|
||||
```json
|
||||
{
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
- `enabled` (boolean, 必填): `true` 启用服务器,`false` 禁用服务器。
|
||||
|
||||
---
|
||||
|
||||
### 切换一个工具
|
||||
|
||||
启用或禁用服务器上的特定工具。
|
||||
|
||||
- **端点**: `/api/servers/:serverName/tools/:toolName/toggle`
|
||||
- **方法**: `POST`
|
||||
- **参数**:
|
||||
- `:serverName` (string, 必填): 服务器的名称。
|
||||
- `:toolName` (string, 必填): 工具的名称。
|
||||
- **正文**:
|
||||
```json
|
||||
{
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
- `enabled` (boolean, 必填): `true` 启用工具,`false` 禁用工具。
|
||||
|
||||
---
|
||||
|
||||
### 更新工具描述
|
||||
|
||||
更新特定工具的描述。
|
||||
|
||||
- **端点**: `/api/servers/:serverName/tools/:toolName/description`
|
||||
- **方法**: `PUT`
|
||||
- **参数**:
|
||||
- `:serverName` (string, 必填): 服务器的名称。
|
||||
- `:toolName` (string, 必填): 工具的名称。
|
||||
- **正文**:
|
||||
```json
|
||||
{
|
||||
"description": "新的工具描述"
|
||||
}
|
||||
```
|
||||
- `description` (string, 必填): 工具的新描述。
|
||||
29
docs/zh/api-reference/smart-routing.mdx
Normal file
29
docs/zh/api-reference/smart-routing.mdx
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: "智能路由"
|
||||
description: "使用向量语义搜索进行智能工具发现。"
|
||||
---
|
||||
|
||||
智能路由是 MCPHub 的智能工具发现系统,它使用向量语义搜索来自动为任何给定任务找到最相关的工具。
|
||||
|
||||
### HTTP 端点
|
||||
|
||||
- **端点**: `http://localhost:3000/mcp/$smart`
|
||||
- **方法**: `POST`
|
||||
|
||||
### SSE 端点 (已弃用)
|
||||
|
||||
- **端点**: `http://localhost:3000/sse/$smart`
|
||||
- **方法**: `GET`
|
||||
|
||||
### 工作原理
|
||||
|
||||
1. **工具索引**: 所有 MCP 工具都会自动转换为向量嵌入并存储在带有 pgvector 的 PostgreSQL 中。
|
||||
2. **语义搜索**: 用户查询被转换为向量,并使用余弦相似度与工具嵌入进行匹配。
|
||||
3. **智能过滤**: 动态阈值可确保相关结果而无噪音。
|
||||
4. **精确执行**: 找到的工具可以通过适当的参数验证直接执行。
|
||||
|
||||
### 设置要求
|
||||
|
||||
- 带有 pgvector 扩展的 PostgreSQL
|
||||
- OpenAI API 密钥(或兼容的嵌入服务)
|
||||
- 在 MCPHub 设置中启用智能路由
|
||||
@@ -1,271 +1,44 @@
|
||||
---
|
||||
title: '环境变量配置'
|
||||
title: '环境变量'
|
||||
description: '使用环境变量配置 MCPHub'
|
||||
---
|
||||
|
||||
# 环境变量配置
|
||||
# 环境变量
|
||||
|
||||
MCPHub 使用环境变量进行配置。本指南涵盖所有可用变量及其用法。
|
||||
MCPHub 使用环境变量进行配置。本指南涵盖了所有可用的变量及其用法。
|
||||
|
||||
## 核心应用设置
|
||||
|
||||
### 服务器配置
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| ----------- | ------------- | ----------------------------------------------- |
|
||||
| `PORT` | `3000` | HTTP 服务器端口号 |
|
||||
| `HOST` | `0.0.0.0` | 服务器绑定的主机地址 |
|
||||
| `NODE_ENV` | `development` | 应用环境(`development`、`production`、`test`) |
|
||||
| `LOG_LEVEL` | `info` | 日志级别(`error`、`warn`、`info`、`debug`) |
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| --- | --- | --- |
|
||||
| `PORT` | `3000` | HTTP 服务器的端口号 |
|
||||
| `INIT_TIMEOUT` | `300000` | 应用程序的初始超时时间 |
|
||||
| `BASE_PATH` | `''` | 应用程序的基本路径 |
|
||||
| `READONLY` | `false` | 设置为 `true` 以启用只读模式 |
|
||||
| `MCPHUB_SETTING_PATH` | | MCPHub 设置文件的路径 |
|
||||
| `NODE_ENV` | `development` | 应用程序环境 (`development`, `production`, `test`) |
|
||||
|
||||
```env
|
||||
PORT=3000
|
||||
HOST=0.0.0.0
|
||||
INIT_TIMEOUT=300000
|
||||
BASE_PATH=/api
|
||||
READONLY=true
|
||||
MCPHUB_SETTING_PATH=/path/to/settings
|
||||
NODE_ENV=production
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
### 数据库配置
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| -------------- | ----------- | --------------------- |
|
||||
| `DATABASE_URL` | - | PostgreSQL 连接字符串 |
|
||||
| `DB_HOST` | `localhost` | 数据库主机 |
|
||||
| `DB_PORT` | `5432` | 数据库端口 |
|
||||
| `DB_NAME` | `mcphub` | 数据库名称 |
|
||||
| `DB_USER` | `mcphub` | 数据库用户名 |
|
||||
| `DB_PASSWORD` | - | 数据库密码 |
|
||||
| `DB_SSL` | `false` | 启用数据库 SSL 连接 |
|
||||
| `DB_POOL_MIN` | `2` | 最小数据库连接池大小 |
|
||||
| `DB_POOL_MAX` | `10` | 最大数据库连接池大小 |
|
||||
|
||||
```env
|
||||
# 选项 1:完整连接字符串
|
||||
DATABASE_URL=postgresql://username:password@localhost:5432/mcphub
|
||||
|
||||
# 选项 2:单独组件
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=mcphub
|
||||
DB_USER=mcphub
|
||||
DB_PASSWORD=your-password
|
||||
DB_SSL=false
|
||||
```
|
||||
|
||||
## 认证与安全
|
||||
|
||||
### JWT 配置
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| ------------------------ | ------- | ------------------------ |
|
||||
| `JWT_SECRET` | - | JWT 令牌签名密钥(必需) |
|
||||
| `JWT_EXPIRES_IN` | `24h` | JWT 令牌过期时间 |
|
||||
| `JWT_REFRESH_EXPIRES_IN` | `7d` | 刷新令牌过期时间 |
|
||||
| `JWT_ALGORITHM` | `HS256` | JWT 签名算法 |
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| --- | --- | --- |
|
||||
| `JWT_SECRET` | - | 用于 JWT 令牌签名的密钥 (必需) |
|
||||
|
||||
```env
|
||||
JWT_SECRET=your-super-secret-key-change-this-in-production
|
||||
JWT_EXPIRES_IN=24h
|
||||
JWT_REFRESH_EXPIRES_IN=7d
|
||||
```
|
||||
|
||||
### 会话与安全
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| ------------------- | ------ | -------------------- |
|
||||
| `SESSION_SECRET` | - | 会话加密密钥 |
|
||||
| `BCRYPT_ROUNDS` | `12` | bcrypt 哈希轮数 |
|
||||
| `RATE_LIMIT_WINDOW` | `15` | 速率限制窗口(分钟) |
|
||||
| `RATE_LIMIT_MAX` | `100` | 每个窗口最大请求数 |
|
||||
| `CORS_ORIGIN` | `*` | 允许的 CORS 来源 |
|
||||
|
||||
```env
|
||||
SESSION_SECRET=your-session-secret
|
||||
BCRYPT_ROUNDS=12
|
||||
RATE_LIMIT_WINDOW=15
|
||||
RATE_LIMIT_MAX=100
|
||||
CORS_ORIGIN=https://your-domain.com,https://admin.your-domain.com
|
||||
```
|
||||
|
||||
## 外部服务
|
||||
|
||||
### OpenAI 配置
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| ------------------------ | ------------------------ | ------------------------------- |
|
||||
| `OPENAI_API_KEY` | - | OpenAI API 密钥(用于智能路由) |
|
||||
| `OPENAI_MODEL` | `gpt-3.5-turbo` | OpenAI 嵌入模型 |
|
||||
| `OPENAI_EMBEDDING_MODEL` | `text-embedding-ada-002` | 向量嵌入模型 |
|
||||
| `OPENAI_MAX_TOKENS` | `1000` | 每个请求最大令牌数 |
|
||||
| `OPENAI_TEMPERATURE` | `0.1` | AI 响应温度 |
|
||||
|
||||
```env
|
||||
OPENAI_API_KEY=sk-your-openai-api-key
|
||||
OPENAI_MODEL=gpt-3.5-turbo
|
||||
OPENAI_EMBEDDING_MODEL=text-embedding-ada-002
|
||||
OPENAI_MAX_TOKENS=1000
|
||||
OPENAI_TEMPERATURE=0.1
|
||||
```
|
||||
|
||||
### Redis 配置(可选)
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| ---------------- | ----------- | ---------------- |
|
||||
| `REDIS_URL` | - | Redis 连接字符串 |
|
||||
| `REDIS_HOST` | `localhost` | Redis 主机 |
|
||||
| `REDIS_PORT` | `6379` | Redis 端口 |
|
||||
| `REDIS_PASSWORD` | - | Redis 密码 |
|
||||
| `REDIS_DB` | `0` | Redis 数据库编号 |
|
||||
| `REDIS_PREFIX` | `mcphub:` | Redis 键前缀 |
|
||||
|
||||
```env
|
||||
# 选项 1:完整连接字符串
|
||||
REDIS_URL=redis://username:password@localhost:6379/0
|
||||
|
||||
# 选项 2:单独组件
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=your-redis-password
|
||||
REDIS_DB=0
|
||||
REDIS_PREFIX=mcphub:
|
||||
```
|
||||
|
||||
## MCP 服务器配置
|
||||
|
||||
### 默认设置
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| ------------------- | ------------------- | ---------------------------- |
|
||||
| `MCP_SETTINGS_FILE` | `mcp_settings.json` | MCP 设置文件路径 |
|
||||
| `MCP_SERVERS_FILE` | `servers.json` | 服务器配置文件路径 |
|
||||
| `MCP_TIMEOUT` | `30000` | MCP 操作默认超时(毫秒) |
|
||||
| `MCP_MAX_RETRIES` | `3` | 失败操作最大重试次数 |
|
||||
| `MCP_RESTART_DELAY` | `5000` | 重启失败服务器的延迟(毫秒) |
|
||||
|
||||
```env
|
||||
MCP_SETTINGS_FILE=./config/mcp_settings.json
|
||||
MCP_SERVERS_FILE=./config/servers.json
|
||||
MCP_TIMEOUT=30000
|
||||
MCP_MAX_RETRIES=3
|
||||
MCP_RESTART_DELAY=5000
|
||||
```
|
||||
|
||||
### 智能路由
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| --------------------------- | ------ | ---------------------- |
|
||||
| `SMART_ROUTING_ENABLED` | `true` | 启用 AI 驱动的智能路由 |
|
||||
| `SMART_ROUTING_THRESHOLD` | `0.7` | 路由相似度阈值 |
|
||||
| `SMART_ROUTING_MAX_RESULTS` | `5` | 返回的最大工具数 |
|
||||
| `VECTOR_CACHE_TTL` | `3600` | 向量缓存 TTL(秒) |
|
||||
|
||||
```env
|
||||
SMART_ROUTING_ENABLED=true
|
||||
SMART_ROUTING_THRESHOLD=0.7
|
||||
SMART_ROUTING_MAX_RESULTS=5
|
||||
VECTOR_CACHE_TTL=3600
|
||||
```
|
||||
|
||||
## 文件存储与上传
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| -------------------- | ---------------- | -------------------------------- |
|
||||
| `UPLOAD_DIR` | `./uploads` | 文件上传目录 |
|
||||
| `MAX_FILE_SIZE` | `10485760` | 最大文件大小(字节,10MB) |
|
||||
| `ALLOWED_FILE_TYPES` | `image/*,text/*` | 允许的 MIME 类型 |
|
||||
| `STORAGE_TYPE` | `local` | 存储类型(`local`、`s3`、`gcs`) |
|
||||
|
||||
```env
|
||||
UPLOAD_DIR=./data/uploads
|
||||
MAX_FILE_SIZE=10485760
|
||||
ALLOWED_FILE_TYPES=image/*,text/*,application/json
|
||||
STORAGE_TYPE=local
|
||||
```
|
||||
|
||||
### S3 存储(可选)
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| ---------------------- | ----------- | -------------- |
|
||||
| `S3_BUCKET` | - | S3 存储桶名称 |
|
||||
| `S3_REGION` | `us-east-1` | S3 区域 |
|
||||
| `S3_ACCESS_KEY_ID` | - | S3 访问密钥 |
|
||||
| `S3_SECRET_ACCESS_KEY` | - | S3 密钥 |
|
||||
| `S3_ENDPOINT` | - | 自定义 S3 端点 |
|
||||
|
||||
```env
|
||||
S3_BUCKET=mcphub-uploads
|
||||
S3_REGION=us-east-1
|
||||
S3_ACCESS_KEY_ID=your-access-key
|
||||
S3_SECRET_ACCESS_KEY=your-secret-key
|
||||
```
|
||||
|
||||
## 监控与日志
|
||||
|
||||
### 应用监控
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| ------------------------ | ------- | -------------------- |
|
||||
| `METRICS_ENABLED` | `true` | 启用指标收集 |
|
||||
| `METRICS_PORT` | `9090` | 指标端点端口 |
|
||||
| `HEALTH_CHECK_INTERVAL` | `30000` | 健康检查间隔(毫秒) |
|
||||
| `PERFORMANCE_MONITORING` | `false` | 启用性能监控 |
|
||||
|
||||
```env
|
||||
METRICS_ENABLED=true
|
||||
METRICS_PORT=9090
|
||||
HEALTH_CHECK_INTERVAL=30000
|
||||
PERFORMANCE_MONITORING=true
|
||||
```
|
||||
|
||||
### 日志配置
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| ------------------ | ------------ | -------------------------------- |
|
||||
| `LOG_FORMAT` | `json` | 日志格式(`json`、`text`) |
|
||||
| `LOG_FILE` | - | 日志文件路径(如果启用文件日志) |
|
||||
| `LOG_MAX_SIZE` | `10m` | 最大日志文件大小 |
|
||||
| `LOG_MAX_FILES` | `5` | 最大日志文件数 |
|
||||
| `LOG_DATE_PATTERN` | `YYYY-MM-DD` | 日志轮换日期模式 |
|
||||
|
||||
```env
|
||||
LOG_FORMAT=json
|
||||
LOG_FILE=./logs/mcphub.log
|
||||
LOG_MAX_SIZE=10m
|
||||
LOG_MAX_FILES=5
|
||||
LOG_DATE_PATTERN=YYYY-MM-DD
|
||||
```
|
||||
|
||||
## 开发与调试
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| ------------------------ | ------- | ------------------------------- |
|
||||
| `DEBUG` | - | 调试命名空间(例如 `mcphub:*`) |
|
||||
| `DEV_TOOLS_ENABLED` | `false` | 启用开发工具 |
|
||||
| `HOT_RELOAD` | `true` | 在开发中启用热重载 |
|
||||
| `MOCK_EXTERNAL_SERVICES` | `false` | 模拟外部 API 调用 |
|
||||
|
||||
```env
|
||||
DEBUG=mcphub:*
|
||||
DEV_TOOLS_ENABLED=true
|
||||
HOT_RELOAD=true
|
||||
MOCK_EXTERNAL_SERVICES=false
|
||||
```
|
||||
|
||||
## 生产优化
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| ------------------ | ------- | ---------------------- |
|
||||
| `CLUSTER_MODE` | `false` | 启用集群模式 |
|
||||
| `WORKER_PROCESSES` | `0` | 工作进程数(0 = 自动) |
|
||||
| `MEMORY_LIMIT` | - | 每个进程内存限制 |
|
||||
| `CPU_LIMIT` | - | 每个进程 CPU 限制 |
|
||||
| `GC_OPTIMIZE` | `false` | 启用垃圾回收优化 |
|
||||
|
||||
```env
|
||||
CLUSTER_MODE=true
|
||||
WORKER_PROCESSES=4
|
||||
MEMORY_LIMIT=512M
|
||||
GC_OPTIMIZE=true
|
||||
```
|
||||
|
||||
## 配置示例
|
||||
@@ -276,22 +49,9 @@ GC_OPTIMIZE=true
|
||||
# .env.development
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# 数据库
|
||||
DATABASE_URL=postgresql://mcphub:password@localhost:5432/mcphub_dev
|
||||
|
||||
# 认证
|
||||
JWT_SECRET=dev-secret-key
|
||||
JWT_EXPIRES_IN=24h
|
||||
|
||||
# OpenAI(开发时可选)
|
||||
# OPENAI_API_KEY=your-dev-key
|
||||
|
||||
# 调试
|
||||
DEBUG=mcphub:*
|
||||
DEV_TOOLS_ENABLED=true
|
||||
HOT_RELOAD=true
|
||||
```
|
||||
|
||||
### 生产环境
|
||||
@@ -300,30 +60,9 @@ HOT_RELOAD=true
|
||||
# .env.production
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
LOG_LEVEL=info
|
||||
LOG_FORMAT=json
|
||||
|
||||
# 数据库
|
||||
DATABASE_URL=postgresql://mcphub:secure-password@db.example.com:5432/mcphub
|
||||
DB_SSL=true
|
||||
DB_POOL_MAX=20
|
||||
|
||||
# 安全
|
||||
JWT_SECRET=your-super-secure-production-secret
|
||||
SESSION_SECRET=your-session-secret
|
||||
BCRYPT_ROUNDS=14
|
||||
|
||||
# 外部服务
|
||||
OPENAI_API_KEY=your-production-openai-key
|
||||
REDIS_URL=redis://redis.example.com:6379
|
||||
|
||||
# 监控
|
||||
METRICS_ENABLED=true
|
||||
PERFORMANCE_MONITORING=true
|
||||
|
||||
# 优化
|
||||
CLUSTER_MODE=true
|
||||
GC_OPTIMIZE=true
|
||||
```
|
||||
|
||||
### Docker 环境
|
||||
@@ -331,21 +70,10 @@ GC_OPTIMIZE=true
|
||||
```env
|
||||
# .env.docker
|
||||
NODE_ENV=production
|
||||
HOST=0.0.0.0
|
||||
PORT=3000
|
||||
|
||||
# 使用 Docker 网络的服务名
|
||||
DATABASE_URL=postgresql://mcphub:password@postgres:5432/mcphub
|
||||
REDIS_URL=redis://redis:6379
|
||||
|
||||
# 安全
|
||||
JWT_SECRET_FILE=/run/secrets/jwt_secret
|
||||
DB_PASSWORD_FILE=/run/secrets/db_password
|
||||
|
||||
# 容器中的文件路径
|
||||
MCP_SETTINGS_FILE=/app/mcp_settings.json
|
||||
UPLOAD_DIR=/app/data/uploads
|
||||
LOG_FILE=/app/logs/mcphub.log
|
||||
```
|
||||
|
||||
## 环境变量加载
|
||||
@@ -353,8 +81,8 @@ LOG_FILE=/app/logs/mcphub.log
|
||||
MCPHub 按以下顺序加载环境变量:
|
||||
|
||||
1. 系统环境变量
|
||||
2. `.env.local`(被 git 忽略)
|
||||
3. `.env.{NODE_ENV}`(例如 `.env.production`)
|
||||
2. `.env.local` (被 git 忽略)
|
||||
3. `.env.{NODE_ENV}` (例如, `.env.production`)
|
||||
4. `.env`
|
||||
|
||||
### 使用 dotenv-expand
|
||||
@@ -364,26 +92,13 @@ MCPHub 支持变量扩展:
|
||||
```env
|
||||
BASE_URL=https://api.example.com
|
||||
API_ENDPOINT=${BASE_URL}/v1
|
||||
DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}
|
||||
```
|
||||
|
||||
## 安全最佳实践
|
||||
|
||||
1. **永远不要提交密钥**到版本控制
|
||||
2. **为生产使用强唯一密钥**
|
||||
1. **永远不要将密钥提交**到版本控制
|
||||
2. **为生产环境使用强大、独特的密钥**
|
||||
3. **定期轮换密钥**
|
||||
4. **使用特定于环境的文件**
|
||||
5. **在启动时验证所有环境变量**
|
||||
6. **为容器部署使用 Docker 密钥**
|
||||
|
||||
## 验证
|
||||
|
||||
MCPHub 在启动时验证环境变量。无效配置将阻止应用程序启动并提供有用的错误消息。
|
||||
|
||||
生产环境必需变量:
|
||||
|
||||
- `JWT_SECRET`
|
||||
- `DATABASE_URL` 或单独的数据库组件
|
||||
- `OPENAI_API_KEY`(如果启用智能路由)
|
||||
|
||||
这个全面的环境配置确保 MCPHub 可以为任何部署场景正确配置。
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -49,448 +49,369 @@ curl -X POST http://localhost:3000/api/servers \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-d '{
|
||||
"name": "my-server",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/files"],
|
||||
"env": {
|
||||
"NODE_ENV": "production"
|
||||
},
|
||||
"cwd": "/app"
|
||||
"name": "fetch-server",
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"],
|
||||
"env": {}
|
||||
}'
|
||||
```
|
||||
|
||||
## 服务器配置
|
||||
## 流行的 MCP 服务器示例
|
||||
|
||||
### 通用配置选项
|
||||
<AccordionGroup>
|
||||
<Accordion title="Web 抓取服务器">
|
||||
提供网页抓取和 HTTP 请求功能:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "filesystem-server",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"],
|
||||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"DEBUG": "mcp:*",
|
||||
"MAX_FILES": "1000"
|
||||
},
|
||||
"cwd": "/app/workspace",
|
||||
"timeout": 30000,
|
||||
"retries": 3,
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
```json
|
||||
{
|
||||
"fetch": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Python 服务器示例
|
||||
**可用工具:**
|
||||
- `fetch`: 发起 HTTP 请求
|
||||
- `fetch_html`: 抓取网页
|
||||
- `fetch_json`: 从 API 获取 JSON 数据
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "python-server",
|
||||
"command": "python",
|
||||
"args": ["-m", "mcp_server", "--config", "config.json"],
|
||||
"env": {
|
||||
"PYTHONPATH": "/app/python",
|
||||
"API_KEY": "${API_KEY}",
|
||||
"LOG_LEVEL": "INFO"
|
||||
},
|
||||
"cwd": "/app/python-server"
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
### Node.js 服务器示例
|
||||
<Accordion title="Playwright 浏览器自动化">
|
||||
用于网页交互的浏览器自动化:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "node-server",
|
||||
"command": "node",
|
||||
"args": ["server.js", "--port", "3001"],
|
||||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"PORT": "3001",
|
||||
"DATABASE_URL": "${DATABASE_URL}"
|
||||
},
|
||||
"cwd": "/app/node-server"
|
||||
}
|
||||
```
|
||||
```json
|
||||
{
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**可用工具:**
|
||||
- `playwright_navigate`: 导航到网页
|
||||
- `playwright_screenshot`: 截取屏幕截图
|
||||
- `playwright_click`: 点击元素
|
||||
- `playwright_fill`: 填写表单
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="文件系统操作">
|
||||
文件和目录管理:
|
||||
|
||||
```json
|
||||
{
|
||||
"filesystem": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**可用工具:**
|
||||
- `read_file`: 读取文件内容
|
||||
- `write_file`: 写入文件
|
||||
- `create_directory`: 创建目录
|
||||
- `list_directory`: 列出目录内容
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="SQLite 数据库">
|
||||
数据库操作:
|
||||
|
||||
```json
|
||||
{
|
||||
"sqlite": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-sqlite", "/path/to/database.db"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**可用工具:**
|
||||
- `execute_query`: 执行 SQL 查询
|
||||
- `describe_tables`: 获取表结构
|
||||
- `create_table`: 创建新表
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Slack 集成">
|
||||
Slack 工作空间集成:
|
||||
|
||||
```json
|
||||
{
|
||||
"slack": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-slack"],
|
||||
"env": {
|
||||
"SLACK_BOT_TOKEN": "xoxb-your-bot-token",
|
||||
"SLACK_TEAM_ID": "T1234567890"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**可用工具:**
|
||||
- `send_slack_message`: 发送消息到频道
|
||||
- `list_slack_channels`: 列出可用频道
|
||||
- `get_slack_thread`: 获取线程消息
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="GitHub 集成">
|
||||
GitHub 仓库操作:
|
||||
|
||||
```json
|
||||
{
|
||||
"github": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-github"],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_your_token"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**可用工具:**
|
||||
- `create_or_update_file`: 创建/更新仓库文件
|
||||
- `search_repositories`: 搜索 GitHub 仓库
|
||||
- `create_issue`: 创建问题
|
||||
- `create_pull_request`: 创建拉取请求
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Google Drive">
|
||||
Google Drive 文件操作:
|
||||
|
||||
```json
|
||||
{
|
||||
"gdrive": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-gdrive"],
|
||||
"env": {
|
||||
"GDRIVE_CLIENT_ID": "your-client-id",
|
||||
"GDRIVE_CLIENT_SECRET": "your-client-secret",
|
||||
"GDRIVE_REDIRECT_URI": "your-redirect-uri"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**可用工具:**
|
||||
- `gdrive_search`: 搜索文件和文件夹
|
||||
- `gdrive_read`: 读取文件内容
|
||||
- `gdrive_create`: 创建新文件
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="高德地图(中国)">
|
||||
中国地图和位置服务:
|
||||
|
||||
```json
|
||||
{
|
||||
"amap": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@amap/amap-maps-mcp-server"],
|
||||
"env": {
|
||||
"AMAP_MAPS_API_KEY": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**可用工具:**
|
||||
- `search_location`: 搜索位置
|
||||
- `get_directions`: 获取路线指引
|
||||
- `reverse_geocode`: 将坐标转换为地址
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## 服务器生命周期管理
|
||||
|
||||
### 启动服务器
|
||||
|
||||
```bash
|
||||
# 启动特定服务器
|
||||
curl -X POST http://localhost:3000/api/servers/my-server/start \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
服务器会在以下情况下自动启动:
|
||||
|
||||
# 启动所有服务器
|
||||
curl -X POST http://localhost:3000/api/servers/start-all \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
- MCPHub 启动时
|
||||
- 通过仪表板或 API 添加服务器时
|
||||
- 服务器配置更新时
|
||||
- 手动重启已停止的服务器时
|
||||
|
||||
### 停止服务器
|
||||
|
||||
```bash
|
||||
# 停止特定服务器
|
||||
curl -X POST http://localhost:3000/api/servers/my-server/stop \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
您可以通过以下方式停止服务器:
|
||||
|
||||
# 优雅停止(等待当前请求完成)
|
||||
curl -X POST http://localhost:3000/api/servers/my-server/stop \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{"graceful": true, "timeout": 30000}'
|
||||
```
|
||||
- **通过仪表板**: 切换服务器状态开关
|
||||
- **通过 API**: 发送 POST 请求到 `/api/servers/{name}/toggle`
|
||||
- **自动停止**: 服务器崩溃或遇到错误时会自动停止
|
||||
|
||||
### 重启服务器
|
||||
|
||||
```bash
|
||||
# 重启服务器
|
||||
curl -X POST http://localhost:3000/api/servers/my-server/restart \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
服务器会在以下情况下自动重启:
|
||||
|
||||
## 热配置重载
|
||||
|
||||
### 更新服务器配置
|
||||
|
||||
无需重启即可更新配置:
|
||||
|
||||
```bash
|
||||
curl -X PUT http://localhost:3000/api/servers/my-server/config \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"env": {
|
||||
"DEBUG": "mcp:verbose",
|
||||
"NEW_SETTING": "value"
|
||||
},
|
||||
"args": ["--verbose", "--new-flag"]
|
||||
}'
|
||||
```
|
||||
|
||||
### 批量配置更新
|
||||
|
||||
```bash
|
||||
curl -X PUT http://localhost:3000/api/servers/bulk-update \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"servers": ["server1", "server2"],
|
||||
"config": {
|
||||
"env": {
|
||||
"LOG_LEVEL": "DEBUG"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
- 配置更改时
|
||||
- 环境变量更新后
|
||||
- 通过仪表板或 API 手动触发时
|
||||
|
||||
## 服务器状态监控
|
||||
|
||||
### 检查服务器状态
|
||||
### 状态指示器
|
||||
|
||||
```bash
|
||||
# 获取所有服务器状态
|
||||
curl -X GET http://localhost:3000/api/servers/status \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
每个服务器都显示状态指示器:
|
||||
|
||||
# 获取特定服务器状态
|
||||
curl -X GET http://localhost:3000/api/servers/my-server/status \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
- 🟢 **运行中**: 服务器处于活动状态并响应
|
||||
- 🟡 **启动中**: 服务器正在初始化
|
||||
- 🔴 **已停止**: 服务器未运行
|
||||
- ⚠️ **错误**: 服务器遇到错误
|
||||
|
||||
响应示例:
|
||||
### 实时日志
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-server",
|
||||
"status": "running",
|
||||
"pid": 12345,
|
||||
"uptime": 3600000,
|
||||
"memory": {
|
||||
"rss": 123456789,
|
||||
"heapTotal": 98765432,
|
||||
"heapUsed": 87654321
|
||||
},
|
||||
"cpu": {
|
||||
"user": 1000000,
|
||||
"system": 500000
|
||||
},
|
||||
"lastRestart": "2024-01-01T12:00:00.000Z"
|
||||
}
|
||||
```
|
||||
实时查看服务器日志:
|
||||
|
||||
1. **仪表板日志**: 点击服务器查看其日志
|
||||
2. **API 日志**: 通过 `/api/logs` 端点访问日志
|
||||
3. **流式日志**: 通过 WebSocket 订阅日志流
|
||||
|
||||
### 健康检查
|
||||
|
||||
配置自动健康检查:
|
||||
MCPHub 自动执行健康检查:
|
||||
|
||||
- **初始化检查**: 验证服务器成功启动
|
||||
- **工具发现**: 确认检测到可用工具
|
||||
- **响应检查**: 测试服务器响应性
|
||||
- **资源监控**: 跟踪 CPU 和内存使用情况
|
||||
|
||||
## 配置管理
|
||||
|
||||
### 环境变量
|
||||
|
||||
服务器可以使用环境变量进行配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-server",
|
||||
"command": "node",
|
||||
"args": ["server.js"],
|
||||
"healthCheck": {
|
||||
"enabled": true,
|
||||
"interval": 30000,
|
||||
"timeout": 5000,
|
||||
"retries": 3,
|
||||
"endpoint": "/health",
|
||||
"expectedStatus": 200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 负载均衡
|
||||
|
||||
### 配置多实例
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "load-balanced-server",
|
||||
"instances": 3,
|
||||
"command": "node",
|
||||
"args": ["server.js"],
|
||||
"loadBalancer": {
|
||||
"strategy": "round-robin",
|
||||
"healthCheck": true,
|
||||
"stickySession": false
|
||||
},
|
||||
"env": {
|
||||
"PORT": "${PORT}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 负载均衡策略
|
||||
|
||||
- **round-robin**: 轮询分发请求
|
||||
- **least-connections**: 分发到连接数最少的实例
|
||||
- **weighted**: 基于权重分发
|
||||
- **ip-hash**: 基于客户端 IP 的一致性哈希
|
||||
|
||||
## 资源限制
|
||||
|
||||
### 设置资源限制
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "resource-limited-server",
|
||||
"command": "python",
|
||||
"args": ["server.py"],
|
||||
"resources": {
|
||||
"memory": {
|
||||
"limit": "512MB",
|
||||
"warning": "400MB"
|
||||
},
|
||||
"cpu": {
|
||||
"limit": "50%",
|
||||
"priority": "normal"
|
||||
},
|
||||
"processes": {
|
||||
"max": 10
|
||||
"server-name": {
|
||||
"command": "python",
|
||||
"args": ["server.py"],
|
||||
"env": {
|
||||
"API_KEY": "${YOUR_API_KEY}",
|
||||
"DEBUG": "true",
|
||||
"MAX_CONNECTIONS": "10"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 监控资源使用
|
||||
**环境变量展开:**
|
||||
|
||||
```bash
|
||||
# 获取资源使用统计
|
||||
curl -X GET http://localhost:3000/api/servers/my-server/resources \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
- `${VAR_NAME}`: 展开为环境变量值
|
||||
- `${VAR_NAME:-default}`: 如果变量未设置则使用默认值
|
||||
- `${VAR_NAME:+value}`: 如果变量已设置则使用指定值
|
||||
|
||||
## 日志管理
|
||||
### 命令变体
|
||||
|
||||
### 配置日志记录
|
||||
指定服务器命令的不同方式:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-server",
|
||||
"command": "node",
|
||||
"args": ["server.js"],
|
||||
"logging": {
|
||||
"level": "info",
|
||||
"file": "/var/log/mcphub/my-server.log",
|
||||
"maxSize": "100MB",
|
||||
"maxFiles": 5,
|
||||
"rotate": true,
|
||||
"format": "json"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 查看日志
|
||||
|
||||
```bash
|
||||
# 获取实时日志
|
||||
curl -X GET http://localhost:3000/api/servers/my-server/logs \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 获取带过滤器的日志
|
||||
curl -X GET "http://localhost:3000/api/servers/my-server/logs?level=error&limit=100" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## 环境变量管理
|
||||
|
||||
### 动态环境变量
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "dynamic-server",
|
||||
"command": "python",
|
||||
"args": ["server.py"],
|
||||
"env": {
|
||||
"API_KEY": "${secrets:api_key}",
|
||||
"DATABASE_URL": "${vault:db_url}",
|
||||
"CURRENT_TIME": "${time:iso}",
|
||||
"SERVER_ID": "${server:id}",
|
||||
"HOSTNAME": "${system:hostname}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 环境变量模板
|
||||
|
||||
支持的模板变量:
|
||||
|
||||
- `${secrets:key}`: 从密钥存储获取
|
||||
- `${vault:path}`: 从 Vault 获取
|
||||
- `${env:VAR}`: 从系统环境变量获取
|
||||
- `${time:format}`: 当前时间戳
|
||||
- `${server:property}`: 服务器属性
|
||||
- `${system:property}`: 系统属性
|
||||
|
||||
## 服务发现
|
||||
|
||||
### 自动服务发现
|
||||
|
||||
```json
|
||||
{
|
||||
"serviceDiscovery": {
|
||||
"enabled": true,
|
||||
"provider": "consul",
|
||||
"config": {
|
||||
"host": "localhost",
|
||||
"port": 8500,
|
||||
"serviceName": "mcp-server",
|
||||
"tags": ["mcp", "ai", "api"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注册服务
|
||||
|
||||
```bash
|
||||
# 手动注册服务
|
||||
curl -X POST http://localhost:3000/api/servers/my-server/register \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"service": {
|
||||
"name": "my-mcp-service",
|
||||
"tags": ["mcp", "production"],
|
||||
"port": 3001,
|
||||
"check": {
|
||||
"http": "http://localhost:3001/health",
|
||||
"interval": "30s"
|
||||
<Tabs>
|
||||
<Tab title="npm/npx">
|
||||
```json
|
||||
{
|
||||
"npm-server": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "package-name", "--option", "value"]
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Python/uvx">
|
||||
```json
|
||||
{
|
||||
"python-server": {
|
||||
"command": "uvx",
|
||||
"args": ["package-name", "--config", "config.json"]
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## 高级功能
|
||||
|
||||
### 热重载
|
||||
|
||||
MCPHub 支持服务器配置的热重载:
|
||||
|
||||
1. **仪表板更新**: 立即应用通过 Web 界面进行的更改
|
||||
2. **API 更新**: 通过 REST API 调用进行实时更新
|
||||
3. **零停机时间**: 优雅的服务器重启,不影响其他服务器
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
<AccordionGroup>
|
||||
<Accordion title="服务器无法启动">
|
||||
**检查以下项目:**
|
||||
- 命令在 PATH 中可用
|
||||
- 已设置所有必需的环境变量
|
||||
- 工作目录存在且可访问
|
||||
- 网络端口未被阻塞
|
||||
- 依赖项已安装
|
||||
|
||||
1. **服务器启动失败**
|
||||
**调试步骤:**
|
||||
1. 在仪表板中检查服务器日志
|
||||
2. 在终端中手动测试命令
|
||||
3. 验证环境变量展开
|
||||
4. 检查文件权限
|
||||
|
||||
```bash
|
||||
# 检查服务器日志
|
||||
curl -X GET http://localhost:3000/api/servers/my-server/logs?level=error \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
2. **配置无效**
|
||||
<Accordion title="服务器持续崩溃">
|
||||
**常见原因:**
|
||||
- 无效的配置参数
|
||||
- 缺少 API 密钥或凭据
|
||||
- 超出资源限制
|
||||
- 依赖项冲突
|
||||
|
||||
```bash
|
||||
# 验证配置
|
||||
curl -X POST http://localhost:3000/api/servers/validate \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d @server-config.json
|
||||
```
|
||||
**解决方案:**
|
||||
1. 查看服务器日志中的错误消息
|
||||
2. 使用最小配置进行测试
|
||||
3. 验证所有凭据和 API 密钥
|
||||
4. 检查系统资源可用性
|
||||
|
||||
3. **性能问题**
|
||||
```bash
|
||||
# 获取性能指标
|
||||
curl -X GET http://localhost:3000/api/servers/my-server/metrics \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
### 调试模式
|
||||
<Accordion title="工具未显示">
|
||||
**可能的问题:**
|
||||
- 服务器未完全初始化
|
||||
- 工具发现超时
|
||||
- 通信协议不匹配
|
||||
- 服务器报告错误
|
||||
|
||||
启用详细调试:
|
||||
**调试步骤:**
|
||||
1. 等待服务器初始化完成
|
||||
2. 检查服务器日志中的工具注册消息
|
||||
3. 测试与服务器的直接通信
|
||||
4. 验证 MCP 协议兼容性
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "debug-server",
|
||||
"command": "node",
|
||||
"args": ["--inspect=0.0.0.0:9229", "server.js"],
|
||||
"env": {
|
||||
"DEBUG": "*",
|
||||
"LOG_LEVEL": "debug",
|
||||
"NODE_ENV": "development"
|
||||
},
|
||||
"debugging": {
|
||||
"enabled": true,
|
||||
"port": 9229,
|
||||
"breakOnStart": false
|
||||
}
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## 高级配置
|
||||
## 下一步
|
||||
|
||||
### 自定义钩子
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "hooked-server",
|
||||
"command": "node",
|
||||
"args": ["server.js"],
|
||||
"hooks": {
|
||||
"beforeStart": ["./scripts/setup.sh"],
|
||||
"afterStart": ["./scripts/notify.sh"],
|
||||
"beforeStop": ["./scripts/cleanup.sh"],
|
||||
"onError": ["./scripts/alert.sh"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 配置模板
|
||||
|
||||
```json
|
||||
{
|
||||
"templates": {
|
||||
"python-server": {
|
||||
"command": "python",
|
||||
"args": ["-m", "mcp_server"],
|
||||
"env": {
|
||||
"PYTHONPATH": "/app/python",
|
||||
"LOG_LEVEL": "INFO"
|
||||
}
|
||||
}
|
||||
},
|
||||
"servers": {
|
||||
"my-python-server": {
|
||||
"extends": "python-server",
|
||||
"args": ["-m", "mcp_server", "--config", "custom.json"],
|
||||
"env": {
|
||||
"API_KEY": "custom-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
有关更多配置选项,请参阅 [MCP 设置配置](/zh/configuration/mcp-settings) 和 [环境变量](/zh/configuration/environment-variables) 文档。
|
||||
<CardGroup cols={2}>
|
||||
<Card title="分组管理" icon="users" href="/zh/features/group-management">
|
||||
将服务器组织成逻辑分组
|
||||
</Card>
|
||||
<Card title="智能路由" icon="route" href="/zh/features/smart-routing">
|
||||
设置 AI 驱动的工具发现
|
||||
</Card>
|
||||
<Card title="API 参考" icon="code" href="/zh/api-reference/servers">
|
||||
服务器管理 API 文档
|
||||
</Card>
|
||||
<Card title="配置指南" icon="cog" href="/zh/configuration/mcp-settings">
|
||||
详细配置选项
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -1,691 +1,367 @@
|
||||
---
|
||||
title: '智能路由'
|
||||
description: '自动负载均衡和请求路由到最佳的 MCP 服务器实例'
|
||||
description: '使用向量语义搜索的 AI 工具发现系统'
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
MCPHub 的智能路由系统自动将传入请求路由到最适合的 MCP 服务器实例。系统考虑服务器负载、响应时间、功能可用性和业务规则来做出路由决策。
|
||||
智能路由是 MCPHub 的智能工具发现系统,它使用向量语义搜索来自动找到与任何给定任务最相关的工具。AI 客户端无需手动指定使用哪些工具,只需描述他们想要完成的任务,智能路由就会识别并提供对最合适工具的访问。
|
||||
|
||||
## 路由策略
|
||||
## 智能路由的工作原理
|
||||
|
||||
### 轮询路由
|
||||
### 1. 工具索引
|
||||
|
||||
最简单的路由策略,按顺序分发请求:
|
||||
当服务器启动时,智能路由会自动:
|
||||
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"strategy": "round-robin",
|
||||
"targets": [
|
||||
{
|
||||
"serverId": "server-1",
|
||||
"weight": 1,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"serverId": "server-2",
|
||||
"weight": 1,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"serverId": "server-3",
|
||||
"weight": 1,
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
- 从 MCP 服务器发现所有可用工具
|
||||
- 提取工具元数据(名称、描述、参数)
|
||||
- 将工具信息转换为向量嵌入
|
||||
- 使用 pgvector 将嵌入存储在 PostgreSQL 中
|
||||
|
||||
### 加权轮询
|
||||
### 2. 语义搜索
|
||||
|
||||
基于服务器容量分配不同权重:
|
||||
当进行查询时:
|
||||
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"strategy": "weighted-round-robin",
|
||||
"targets": [
|
||||
{
|
||||
"serverId": "high-performance-server",
|
||||
"weight": 3,
|
||||
"specs": {
|
||||
"cpu": "8 cores",
|
||||
"memory": "32GB"
|
||||
}
|
||||
},
|
||||
{
|
||||
"serverId": "standard-server-1",
|
||||
"weight": 2,
|
||||
"specs": {
|
||||
"cpu": "4 cores",
|
||||
"memory": "16GB"
|
||||
}
|
||||
},
|
||||
{
|
||||
"serverId": "standard-server-2",
|
||||
"weight": 1,
|
||||
"specs": {
|
||||
"cpu": "2 cores",
|
||||
"memory": "8GB"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
- 用户查询被转换为向量嵌入
|
||||
- 相似性搜索使用余弦相似度找到匹配的工具
|
||||
- 动态阈值过滤掉不相关的结果
|
||||
- 结果按相关性得分排序
|
||||
|
||||
### 最少连接数
|
||||
### 3. 智能过滤
|
||||
|
||||
将请求路由到当前连接数最少的服务器:
|
||||
智能路由应用多个过滤器:
|
||||
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"strategy": "least-connections",
|
||||
"balancingMode": "dynamic",
|
||||
"healthCheck": {
|
||||
"enabled": true,
|
||||
"interval": 10000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- **相关性阈值**:只返回高于相似性阈值的工具
|
||||
- **上下文感知**:考虑对话上下文
|
||||
- **工具可用性**:确保工具当前可访问
|
||||
- **权限过滤**:尊重用户访问权限
|
||||
|
||||
### 基于响应时间
|
||||
### 4. 工具执行
|
||||
|
||||
路由到响应时间最短的服务器:
|
||||
找到的工具可以直接执行:
|
||||
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"strategy": "fastest-response",
|
||||
"metrics": {
|
||||
"measurementWindow": "5m",
|
||||
"sampleSize": 100,
|
||||
"excludeSlowRequests": true,
|
||||
"slowRequestThreshold": "5s"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- 参数验证确保正确的工具使用
|
||||
- 错误处理提供有用的反馈
|
||||
- 响应格式保持一致性
|
||||
- 日志记录跟踪工具使用情况进行分析
|
||||
|
||||
## 基于功能的路由
|
||||
## 前置条件
|
||||
|
||||
### 工具特定路由
|
||||
智能路由需要比基础 MCPHub 使用更多的设置:
|
||||
|
||||
根据请求的工具类型路由到专门的服务器:
|
||||
### 必需组件
|
||||
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"strategy": "capability-based",
|
||||
"rules": [
|
||||
{
|
||||
"condition": {
|
||||
"tool": "filesystem"
|
||||
},
|
||||
"targets": ["filesystem-server-1", "filesystem-server-2"],
|
||||
"strategy": "least-connections"
|
||||
},
|
||||
{
|
||||
"condition": {
|
||||
"tool": "web-search"
|
||||
},
|
||||
"targets": ["search-server-1", "search-server-2"],
|
||||
"strategy": "round-robin"
|
||||
},
|
||||
{
|
||||
"condition": {
|
||||
"tool": "database"
|
||||
},
|
||||
"targets": ["db-server"],
|
||||
"strategy": "single"
|
||||
}
|
||||
],
|
||||
"fallback": {
|
||||
"targets": ["general-server-1", "general-server-2"],
|
||||
"strategy": "round-robin"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
1. **带有 pgvector 的 PostgreSQL**:用于嵌入存储的向量数据库
|
||||
2. **嵌入服务**:OpenAI API 或兼容服务
|
||||
3. **环境配置**:正确的配置变量
|
||||
|
||||
### 内容感知路由
|
||||
## 使用智能路由
|
||||
|
||||
基于请求内容进行智能路由:
|
||||
### 智能路由端点
|
||||
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"strategy": "content-aware",
|
||||
"rules": [
|
||||
{
|
||||
"condition": {
|
||||
"content.language": "python"
|
||||
},
|
||||
"targets": ["python-specialized-server"],
|
||||
"reason": "Python代码分析专用服务器"
|
||||
},
|
||||
{
|
||||
"condition": {
|
||||
"content.size": "> 1MB"
|
||||
},
|
||||
"targets": ["high-memory-server"],
|
||||
"reason": "大文件处理专用服务器"
|
||||
},
|
||||
{
|
||||
"condition": {
|
||||
"content.type": "image"
|
||||
},
|
||||
"targets": ["image-processing-server"],
|
||||
"reason": "图像处理专用服务器"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
通过特殊的 `$smart` 端点访问智能路由:
|
||||
|
||||
## 地理位置路由
|
||||
<Tabs>
|
||||
<Tab title="HTTP MCP">
|
||||
```
|
||||
http://localhost:3000/mcp/$smart
|
||||
```
|
||||
</Tab>
|
||||
|
||||
### 基于客户端位置
|
||||
<Tab title="SSE (Legacy)">
|
||||
```
|
||||
http://localhost:3000/sse/$smart
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
根据客户端地理位置路由到最近的服务器:
|
||||
{/* ## 性能优化
|
||||
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"strategy": "geo-location",
|
||||
"regions": [
|
||||
{
|
||||
"name": "北美",
|
||||
"countries": ["US", "CA", "MX"],
|
||||
"servers": ["us-east-1", "us-west-1", "ca-central-1"],
|
||||
"strategy": "least-latency"
|
||||
},
|
||||
{
|
||||
"name": "欧洲",
|
||||
"countries": ["DE", "FR", "UK", "NL"],
|
||||
"servers": ["eu-west-1", "eu-central-1"],
|
||||
"strategy": "round-robin"
|
||||
},
|
||||
{
|
||||
"name": "亚太",
|
||||
"countries": ["CN", "JP", "KR", "SG"],
|
||||
"servers": ["ap-southeast-1", "ap-northeast-1"],
|
||||
"strategy": "fastest-response"
|
||||
}
|
||||
],
|
||||
"fallback": {
|
||||
"servers": ["global-server-1"],
|
||||
"strategy": "single"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
### 嵌入缓存
|
||||
|
||||
### 延迟优化
|
||||
智能路由缓存嵌入以提高性能:
|
||||
|
||||
```bash
|
||||
# 配置延迟监控
|
||||
curl -X PUT http://localhost:3000/api/routing/latency-config \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
# 配置缓存设置
|
||||
EMBEDDING_CACHE_TTL=3600 # 缓存 1 小时
|
||||
EMBEDDING_CACHE_SIZE=10000 # 最多缓存 10k 个嵌入
|
||||
EMBEDDING_CACHE_CLEANUP=300 # 每 5 分钟清理一次
|
||||
```
|
||||
|
||||
### 批处理
|
||||
|
||||
工具批量索引以提高效率:
|
||||
|
||||
```bash
|
||||
# 嵌入生成的批大小
|
||||
EMBEDDING_BATCH_SIZE=100
|
||||
|
||||
# 并发嵌入请求
|
||||
EMBEDDING_CONCURRENCY=5
|
||||
|
||||
# 索引更新频率
|
||||
INDEX_UPDATE_INTERVAL=3600 # 每小时重新索引
|
||||
```
|
||||
|
||||
### 数据库优化
|
||||
|
||||
为向量操作优化 PostgreSQL:
|
||||
|
||||
```sql
|
||||
-- 创建索引以获得更好的性能
|
||||
CREATE INDEX ON tool_embeddings USING hnsw (embedding vector_cosine_ops);
|
||||
|
||||
-- 调整 PostgreSQL 设置
|
||||
ALTER SYSTEM SET shared_preload_libraries = 'vector';
|
||||
ALTER SYSTEM SET max_connections = 200;
|
||||
ALTER SYSTEM SET shared_buffers = '256MB';
|
||||
ALTER SYSTEM SET effective_cache_size = '1GB';
|
||||
```
|
||||
|
||||
## 监控和分析
|
||||
|
||||
### 智能路由指标
|
||||
|
||||
监控智能路由性能:
|
||||
|
||||
```bash
|
||||
# 获取智能路由统计信息
|
||||
curl http://localhost:3000/api/smart-routing/stats \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
|
||||
响应包括:
|
||||
|
||||
- 查询计数和频率
|
||||
- 平均响应时间
|
||||
- 嵌入缓存命中率
|
||||
- 最受欢迎的工具
|
||||
- 查询模式
|
||||
|
||||
### 工具使用分析
|
||||
|
||||
跟踪哪些工具被发现和使用:
|
||||
|
||||
```bash
|
||||
# 获取工具使用分析
|
||||
curl http://localhost:3000/api/smart-routing/analytics \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
|
||||
指标包括:
|
||||
|
||||
- 工具发现率
|
||||
- 执行成功率
|
||||
- 用户满意度评分
|
||||
- 查询到执行的转换率
|
||||
|
||||
### 性能监控
|
||||
|
||||
监控系统性能:
|
||||
|
||||
```bash
|
||||
# 数据库性能
|
||||
curl http://localhost:3000/api/smart-routing/db-stats \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
|
||||
# 嵌入服务状态
|
||||
curl http://localhost:3000/api/smart-routing/embedding-stats \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
|
||||
## 高级功能
|
||||
|
||||
### 自定义嵌入
|
||||
|
||||
使用自定义嵌入模型:
|
||||
|
||||
```bash
|
||||
# Hugging Face 模型
|
||||
EMBEDDING_SERVICE=huggingface
|
||||
HUGGINGFACE_MODEL=sentence-transformers/all-MiniLM-L6-v2
|
||||
HUGGINGFACE_API_KEY=your_api_key
|
||||
|
||||
# 本地嵌入服务
|
||||
EMBEDDING_SERVICE=local
|
||||
EMBEDDING_SERVICE_URL=http://localhost:8080/embeddings
|
||||
```
|
||||
|
||||
### 查询增强
|
||||
|
||||
增强查询以获得更好的结果:
|
||||
|
||||
```json
|
||||
{
|
||||
"queryEnhancement": {
|
||||
"enabled": true,
|
||||
"measurementInterval": 30000,
|
||||
"regions": [
|
||||
{"id": "us-east", "endpoint": "ping.us-east.example.com"},
|
||||
{"id": "eu-west", "endpoint": "ping.eu-west.example.com"},
|
||||
{"id": "ap-southeast", "endpoint": "ping.ap-southeast.example.com"}
|
||||
],
|
||||
"routing": {
|
||||
"preferLowLatency": true,
|
||||
"maxLatencyThreshold": "200ms",
|
||||
"fallbackOnTimeout": true
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## 负载感知路由
|
||||
|
||||
### 实时负载监控
|
||||
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"strategy": "load-aware",
|
||||
"loadMetrics": {
|
||||
"cpu": {
|
||||
"threshold": 80,
|
||||
"weight": 0.4
|
||||
},
|
||||
"memory": {
|
||||
"threshold": 85,
|
||||
"weight": 0.3
|
||||
},
|
||||
"connections": {
|
||||
"threshold": 1000,
|
||||
"weight": 0.2
|
||||
},
|
||||
"responseTime": {
|
||||
"threshold": "2s",
|
||||
"weight": 0.1
|
||||
}
|
||||
},
|
||||
"adaptation": {
|
||||
"enabled": true,
|
||||
"adjustmentInterval": 60000,
|
||||
"emergencyThreshold": 95
|
||||
}
|
||||
"expandAcronyms": true,
|
||||
"addSynonyms": true,
|
||||
"contextualExpansion": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 预测性负载均衡
|
||||
### 结果过滤
|
||||
|
||||
基于条件过滤结果:
|
||||
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"strategy": "predictive",
|
||||
"prediction": {
|
||||
"algorithm": "linear-regression",
|
||||
"trainingWindow": "7d",
|
||||
"predictionHorizon": "1h",
|
||||
"factors": ["historical_load", "time_of_day", "day_of_week", "seasonal_patterns"]
|
||||
},
|
||||
"adaptation": {
|
||||
"preemptiveScaling": true,
|
||||
"scaleUpThreshold": 70,
|
||||
"scaleDownThreshold": 30
|
||||
}
|
||||
"resultFiltering": {
|
||||
"minRelevanceScore": 0.7,
|
||||
"maxResults": 10,
|
||||
"preferredServers": ["fetch", "playwright"],
|
||||
"excludeServers": ["deprecated-server"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 故障转移和恢复
|
||||
### 反馈学习
|
||||
|
||||
### 自动故障转移
|
||||
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"strategy": "high-availability",
|
||||
"failover": {
|
||||
"enabled": true,
|
||||
"detection": {
|
||||
"healthCheckFailures": 3,
|
||||
"timeoutThreshold": "10s",
|
||||
"checkInterval": 5000
|
||||
},
|
||||
"recovery": {
|
||||
"automaticRecovery": true,
|
||||
"recoveryChecks": 5,
|
||||
"recoveryInterval": 30000
|
||||
}
|
||||
},
|
||||
"clusters": [
|
||||
{
|
||||
"name": "primary",
|
||||
"servers": ["server-1", "server-2"],
|
||||
"priority": 1
|
||||
},
|
||||
{
|
||||
"name": "secondary",
|
||||
"servers": ["backup-server-1", "backup-server-2"],
|
||||
"priority": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 断路器模式
|
||||
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"circuitBreaker": {
|
||||
"enabled": true,
|
||||
"failureThreshold": 10,
|
||||
"timeWindow": 60000,
|
||||
"halfOpenRetries": 3,
|
||||
"fallback": {
|
||||
"type": "cached-response",
|
||||
"ttl": 300000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 会话亲和性
|
||||
|
||||
### 粘性会话
|
||||
|
||||
保持用户会话与特定服务器的关联:
|
||||
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"strategy": "session-affinity",
|
||||
"affinity": {
|
||||
"type": "cookie",
|
||||
"cookieName": "mcphub-server-id",
|
||||
"ttl": 3600000,
|
||||
"fallbackOnUnavailable": true
|
||||
},
|
||||
"sessionStore": {
|
||||
"type": "redis",
|
||||
"config": {
|
||||
"host": "localhost",
|
||||
"port": 6379,
|
||||
"db": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 基于用户 ID 的路由
|
||||
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"strategy": "user-based",
|
||||
"userRouting": {
|
||||
"algorithm": "consistent-hashing",
|
||||
"hashFunction": "sha256",
|
||||
"virtualNodes": 100,
|
||||
"replicationFactor": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 动态路由配置
|
||||
|
||||
### 运行时配置更新
|
||||
基于用户反馈改进结果:
|
||||
|
||||
```bash
|
||||
# 更新路由配置
|
||||
curl -X PUT http://localhost:3000/api/routing/config \
|
||||
# 对搜索结果提供反馈
|
||||
curl -X POST http://localhost:3000/api/smart-routing/feedback \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-d '{
|
||||
"strategy": "weighted-round-robin",
|
||||
"weights": {
|
||||
"server-1": 3,
|
||||
"server-2": 2,
|
||||
"server-3": 1
|
||||
},
|
||||
"applyImmediately": true
|
||||
"queryId": "search-123",
|
||||
"toolName": "fetch_html",
|
||||
"rating": 5,
|
||||
"successful": true,
|
||||
"comments": "完美适合这个任务的工具"
|
||||
}'
|
||||
```
|
||||
|
||||
### A/B 测试路由
|
||||
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"strategy": "ab-testing",
|
||||
"experiments": [
|
||||
{
|
||||
"name": "new-algorithm-test",
|
||||
"enabled": true,
|
||||
"trafficSplit": {
|
||||
"control": 70,
|
||||
"variant": 30
|
||||
},
|
||||
"rules": {
|
||||
"control": {
|
||||
"strategy": "round-robin",
|
||||
"servers": ["stable-server-1", "stable-server-2"]
|
||||
},
|
||||
"variant": {
|
||||
"strategy": "ai-optimized",
|
||||
"servers": ["experimental-server-1"]
|
||||
}
|
||||
},
|
||||
"metrics": ["response_time", "error_rate", "user_satisfaction"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 路由分析和监控
|
||||
|
||||
### 实时路由指标
|
||||
|
||||
```bash
|
||||
# 获取路由统计
|
||||
curl -X GET http://localhost:3000/api/routing/metrics \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2024-01-01T12:00:00Z",
|
||||
"totalRequests": 15420,
|
||||
"routingDistribution": {
|
||||
"server-1": { "requests": 6168, "percentage": 40 },
|
||||
"server-2": { "requests": 4626, "percentage": 30 },
|
||||
"server-3": { "requests": 3084, "percentage": 20 },
|
||||
"backup-server": { "requests": 1542, "percentage": 10 }
|
||||
},
|
||||
"performance": {
|
||||
"avgResponseTime": "245ms",
|
||||
"p95ResponseTime": "580ms",
|
||||
"errorRate": "0.3%"
|
||||
},
|
||||
"failovers": {
|
||||
"total": 2,
|
||||
"byServer": {
|
||||
"server-2": 1,
|
||||
"server-3": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 路由决策日志
|
||||
|
||||
```bash
|
||||
# 启用路由决策日志
|
||||
curl -X PUT http://localhost:3000/api/routing/logging \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"enabled": true,
|
||||
"level": "info",
|
||||
"includeDecisionFactors": true,
|
||||
"sampleRate": 0.1
|
||||
}'
|
||||
```
|
||||
|
||||
## 自定义路由规则
|
||||
|
||||
### 基于业务逻辑的路由
|
||||
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"strategy": "custom-rules",
|
||||
"rules": [
|
||||
{
|
||||
"name": "premium-users",
|
||||
"priority": 1,
|
||||
"condition": "user.tier === 'premium'",
|
||||
"action": {
|
||||
"targetServers": ["premium-server-1", "premium-server-2"],
|
||||
"strategy": "least-connections",
|
||||
"qos": {
|
||||
"maxResponseTime": "1s",
|
||||
"priority": "high"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "high-volume-requests",
|
||||
"priority": 2,
|
||||
"condition": "request.size > 10MB",
|
||||
"action": {
|
||||
"targetServers": ["high-capacity-server"],
|
||||
"strategy": "single",
|
||||
"timeout": "60s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "batch-processing",
|
||||
"priority": 3,
|
||||
"condition": "request.type === 'batch'",
|
||||
"action": {
|
||||
"targetServers": ["batch-server-1", "batch-server-2"],
|
||||
"strategy": "queue-based",
|
||||
"queueConfig": {
|
||||
"maxSize": 1000,
|
||||
"timeout": "5m"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### JavaScript 路由函数
|
||||
|
||||
```javascript
|
||||
// 自定义路由函数
|
||||
function customRouting(request, servers, metrics) {
|
||||
const { user, content, timestamp } = request;
|
||||
|
||||
// 工作时间优先使用高性能服务器
|
||||
const isBusinessHours =
|
||||
new Date(timestamp).getHours() >= 9 && new Date(timestamp).getHours() <= 17;
|
||||
|
||||
if (isBusinessHours && user.priority === 'high') {
|
||||
return servers.filter((s) => s.tags.includes('high-performance'));
|
||||
}
|
||||
|
||||
// 基于内容类型的特殊路由
|
||||
if (content.type === 'code-analysis') {
|
||||
return servers.filter((s) => s.capabilities.includes('code-analysis'));
|
||||
}
|
||||
|
||||
// 默认负载均衡
|
||||
return servers.sort((a, b) => a.currentLoad - b.currentLoad);
|
||||
}
|
||||
```
|
||||
|
||||
## 路由优化
|
||||
|
||||
### 机器学习优化
|
||||
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"strategy": "ml-optimized",
|
||||
"mlConfig": {
|
||||
"algorithm": "reinforcement-learning",
|
||||
"rewardFunction": "response_time_weighted",
|
||||
"trainingData": {
|
||||
"features": [
|
||||
"server_load",
|
||||
"response_time_history",
|
||||
"request_complexity",
|
||||
"user_pattern",
|
||||
"time_of_day"
|
||||
],
|
||||
"targetMetric": "overall_satisfaction"
|
||||
},
|
||||
"updateFrequency": "hourly",
|
||||
"explorationRate": 0.1
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 缓存感知路由
|
||||
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"strategy": "cache-aware",
|
||||
"caching": {
|
||||
"enabled": true,
|
||||
"levels": [
|
||||
{
|
||||
"type": "local",
|
||||
"ttl": 300,
|
||||
"maxSize": "100MB"
|
||||
},
|
||||
{
|
||||
"type": "distributed",
|
||||
"provider": "redis",
|
||||
"ttl": 3600,
|
||||
"maxSize": "1GB"
|
||||
}
|
||||
],
|
||||
"routing": {
|
||||
"preferCachedServers": true,
|
||||
"cacheHitBonus": 0.3,
|
||||
"cacheMissThreshold": 0.8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
``` */}
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 路由调试
|
||||
<AccordionGroup>
|
||||
<Accordion title="数据库连接问题">
|
||||
**症状:**
|
||||
- 智能路由不可用
|
||||
- 数据库连接错误
|
||||
- 嵌入存储失败
|
||||
|
||||
```bash
|
||||
# 调试特定请求的路由决策
|
||||
curl -X POST http://localhost:3000/api/routing/debug \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"request": {
|
||||
"userId": "user123",
|
||||
"tool": "filesystem",
|
||||
"content": {"type": "read", "path": "/data/file.txt"}
|
||||
},
|
||||
"traceRoute": true
|
||||
}'
|
||||
```
|
||||
**解决方案:**
|
||||
1. 验证 PostgreSQL 是否正在运行
|
||||
2. 检查 DATABASE_URL 格式
|
||||
3. 确保安装了 pgvector 扩展
|
||||
4. 手动测试连接:
|
||||
```bash
|
||||
psql $DATABASE_URL -c "SELECT 1;"
|
||||
```
|
||||
|
||||
### 路由性能分析
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
# 获取路由性能报告
|
||||
curl -X GET http://localhost:3000/api/routing/performance \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-G -d "timeRange=1h" -d "detailed=true"
|
||||
```
|
||||
<Accordion title="嵌入服务问题">
|
||||
**症状:**
|
||||
- 工具索引失败
|
||||
- 查询处理错误
|
||||
- API 速率限制错误
|
||||
|
||||
### 常见问题
|
||||
**解决方案:**
|
||||
1. 验证 API 密钥有效性
|
||||
2. 检查网络连接
|
||||
3. 监控速率限制
|
||||
4. 测试嵌入服务:
|
||||
```bash
|
||||
curl -X POST https://api.openai.com/v1/embeddings \
|
||||
-H "Authorization: Bearer $OPENAI_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"input": "test", "model": "text-embedding-3-small"}'
|
||||
```
|
||||
|
||||
1. **不均匀的负载分布**
|
||||
</Accordion>
|
||||
|
||||
- 检查服务器权重配置
|
||||
- 验证健康检查设置
|
||||
- 分析请求模式
|
||||
<Accordion title="搜索结果不佳">
|
||||
**症状:**
|
||||
- 返回不相关的工具
|
||||
- 相关性得分低
|
||||
- 缺少预期的工具
|
||||
|
||||
2. **频繁的故障转移**
|
||||
**解决方案:**
|
||||
1. 调整相似性阈值
|
||||
2. 使用更好的描述重新索引工具
|
||||
3. 使用更具体的查询
|
||||
4. 检查工具元数据质量
|
||||
```bash
|
||||
# 重新索引所有工具
|
||||
curl -X POST http://localhost:3000/api/smart-routing/reindex \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
|
||||
- 调整健康检查阈值
|
||||
- 检查网络连接稳定性
|
||||
- 优化服务器资源
|
||||
</Accordion>
|
||||
|
||||
3. **路由延迟过高**
|
||||
- 简化路由规则
|
||||
- 优化路由算法
|
||||
- 使用缓存加速决策
|
||||
<Accordion title="性能问题">
|
||||
**症状:**
|
||||
- 查询响应缓慢
|
||||
- 数据库负载高
|
||||
- 内存使用激增
|
||||
|
||||
有关更多信息,请参阅 [监控](/zh/features/monitoring) 和 [服务器管理](/zh/features/server-management) 文档。
|
||||
**解决方案:**
|
||||
1. 优化数据库配置
|
||||
2. 增加缓存大小
|
||||
3. 减少批处理大小
|
||||
4. 监控系统资源
|
||||
```bash
|
||||
# 检查系统性能
|
||||
curl http://localhost:3000/api/smart-routing/performance \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 查询编写
|
||||
|
||||
<Tip>
|
||||
**要具体描述**:在查询中使用具体、描述性的语言以获得更好的工具匹配。
|
||||
</Tip>
|
||||
|
||||
<Tip>
|
||||
**包含上下文**:提供有关您的任务或领域的相关上下文以获得更准确的结果。
|
||||
</Tip>
|
||||
|
||||
<Tip>**使用自然语言**:像向人类描述任务一样编写查询。</Tip>
|
||||
|
||||
### 工具描述
|
||||
|
||||
<Warning>
|
||||
**质量元数据**:确保 MCP 服务器提供高质量的工具描述和元数据。
|
||||
</Warning>
|
||||
|
||||
<Warning>**定期更新**:随着功能的发展保持工具描述的最新状态。</Warning>
|
||||
|
||||
<Warning>
|
||||
**一致的命名**:在工具和服务器中使用一致的命名约定。
|
||||
</Warning>
|
||||
|
||||
### 系统维护
|
||||
|
||||
<Info>**定期重新索引**:定期重新索引工具以确保嵌入质量。</Info>
|
||||
|
||||
<Info>**监控性能**:跟踪查询模式并根据使用情况进行优化。</Info>
|
||||
|
||||
<Info>
|
||||
**更新模型**:随着新嵌入模型的出现,考虑更新到更新的模型。
|
||||
</Info>
|
||||
|
||||
## 下一步
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="身份验证" icon="shield" href="/zh/features/authentication">
|
||||
用户管理和访问控制
|
||||
</Card>
|
||||
<Card title="监控" icon="chart-line" href="/zh/features/monitoring">
|
||||
系统监控和分析
|
||||
</Card>
|
||||
<Card title="API 参考" icon="code" href="/zh/api-reference/smart-routing">
|
||||
完整的智能路由 API 文档
|
||||
</Card>
|
||||
<Card title="配置" icon="cog" href="/zh/configuration/environment-variables">
|
||||
高级配置选项
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
---
|
||||
title: '欢迎使用 MCPHub'
|
||||
description: 'MCPHub 是一个强大的 Model Context Protocol (MCP) 服务器管理平台,提供智能路由、负载均衡和实时监控功能'
|
||||
title: '欢迎使用'
|
||||
description: 'MCPHub 是一个强大的 Model Context Protocol (MCP) 服务器管理平台,提供分组管理、智能路由和实时监控等功能'
|
||||
---
|
||||
|
||||
<img className="block dark:hidden" src="/images/hero-light.png" alt="MCPHub Hero Light" />
|
||||
<img className="hidden dark:block" src="/images/hero-dark.png" alt="MCPHub Hero Dark" />
|
||||
{/* <img className="block dark:hidden" src="/images/hero-light.png" alt="MCPHub Hero Light" />
|
||||
<img className="hidden dark:block" src="/images/hero-dark.png" alt="MCPHub Hero Dark" /> */}
|
||||
|
||||
## 什么是 MCPHub?
|
||||
|
||||
MCPHub 是一个现代化的 Model Context Protocol (MCP) 服务器管理平台,旨在简化 AI 模型服务的部署、管理和监控。通过智能路由和负载均衡技术,MCPHub 帮助您构建高可用、可扩展的 AI 服务架构。
|
||||
MCPHub 是一个现代化的 Model Context Protocol (MCP) 服务器管理平台,旨在简化 AI 模型服务的部署、管理和监控。通过分组管理和智能路由技术,MCPHub 帮助您构建高可用、可扩展的 AI 服务架构。
|
||||
|
||||
### 核心功能
|
||||
|
||||
- **🚀 智能路由** - 基于负载、延迟和健康状态的智能请求分发
|
||||
- **⚖️ 负载均衡** - 多种负载均衡策略,确保最优性能
|
||||
- **🏗️ 分组管理** - 灵活的服务器分组和配置管理
|
||||
- **🚀 智能路由** - 基于语义检索的智能路由分发
|
||||
- **📊 实时监控** - 全面的性能指标和健康检查
|
||||
- **🔐 安全认证** - 企业级身份认证和访问控制
|
||||
- **🏗️ 服务器组管理** - 灵活的服务器分组和配置管理
|
||||
- **🔄 故障转移** - 自动故障检测和流量切换
|
||||
- **🔐 安全认证** - 身份认证和访问控制
|
||||
|
||||
## 快速开始
|
||||
|
||||
|
||||
570
docs/zh/installation.mdx
Normal file
570
docs/zh/installation.mdx
Normal file
@@ -0,0 +1,570 @@
|
||||
---
|
||||
title: '安装指南'
|
||||
description: '各种平台的详细安装说明'
|
||||
---
|
||||
|
||||
## 先决条件
|
||||
|
||||
在安装 MCPHub 之前,请确保您具备以下先决条件:
|
||||
|
||||
- **Node.js** 18+ (用于本地开发)
|
||||
- **Docker** (推荐用于生产环境)
|
||||
- **pnpm** (用于本地开发)
|
||||
|
||||
智能路由的可选要求:
|
||||
|
||||
- **PostgreSQL** 带 pgvector 扩展
|
||||
- **OpenAI API Key** 或兼容的嵌入服务
|
||||
|
||||
## 安装方法
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Docker (推荐)">
|
||||
### Docker 安装
|
||||
|
||||
Docker 是在生产环境中部署 MCPHub 的推荐方式。
|
||||
|
||||
#### 1. 基础安装
|
||||
|
||||
```bash
|
||||
# 拉取最新镜像
|
||||
docker pull samanhappy/mcphub:latest
|
||||
|
||||
# 使用默认设置运行
|
||||
docker run -d \
|
||||
--name mcphub \
|
||||
-p 3000:3000 \
|
||||
samanhappy/mcphub:latest
|
||||
```
|
||||
|
||||
#### 2. 使用自定义配置
|
||||
|
||||
```bash
|
||||
# 创建您的配置文件
|
||||
cat > mcp_settings.json << 'EOF'
|
||||
{
|
||||
"mcpServers": {
|
||||
"fetch": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"]
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless"]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# 使用挂载的配置运行
|
||||
docker run -d \
|
||||
--name mcphub \
|
||||
-p 3000:3000 \
|
||||
-v $(pwd)/mcp_settings.json:/app/mcp_settings.json \
|
||||
samanhappy/mcphub:latest
|
||||
```
|
||||
|
||||
#### 3. 使用环境变量
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name mcphub \
|
||||
-p 3000:3000 \
|
||||
-e PORT=3000 \
|
||||
-e BASE_PATH="" \
|
||||
samanhappy/mcphub:latest
|
||||
```
|
||||
|
||||
#### 4. Docker Compose
|
||||
|
||||
创建 `docker-compose.yml` 文件:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
mcphub:
|
||||
image: samanhappy/mcphub:latest
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./mcp_settings.json:/app/mcp_settings.json
|
||||
environment:
|
||||
- PORT=3000
|
||||
- BASE_PATH=""
|
||||
- REQUEST_TIMEOUT=60000
|
||||
restart: unless-stopped
|
||||
|
||||
# 可选:用于智能路由的 PostgreSQL
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
environment:
|
||||
POSTGRES_DB: mcphub
|
||||
POSTGRES_USER: mcphub
|
||||
POSTGRES_PASSWORD: mcphub_password
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
```
|
||||
|
||||
运行命令:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="npm 包">
|
||||
### npm 包安装
|
||||
|
||||
将 MCPHub 安装为全局 npm 包:
|
||||
|
||||
#### 1. 全局安装
|
||||
|
||||
```bash
|
||||
# 全局安装
|
||||
npm install -g @samanhappy/mcphub
|
||||
|
||||
# 或使用 yarn
|
||||
yarn global add @samanhappy/mcphub
|
||||
|
||||
# 或使用 pnpm
|
||||
pnpm add -g @samanhappy/mcphub
|
||||
```
|
||||
|
||||
#### 2. 运行 MCPHub
|
||||
|
||||
```bash
|
||||
# 使用默认设置运行
|
||||
mcphub
|
||||
|
||||
# 使用自定义端口运行
|
||||
PORT=8080 mcphub
|
||||
```
|
||||
|
||||
{/* #### 3. 本地安装
|
||||
|
||||
您也可以在项目中本地安装 MCPHub:
|
||||
|
||||
```bash
|
||||
# 创建新目录
|
||||
mkdir my-mcphub
|
||||
cd my-mcphub
|
||||
|
||||
# 初始化 package.json
|
||||
npm init -y
|
||||
|
||||
# 本地安装 MCPHub
|
||||
npm install @samanhappy/mcphub
|
||||
|
||||
# 创建启动脚本
|
||||
echo '#!/bin/bash\nnpx mcphub' > start.sh
|
||||
chmod +x start.sh
|
||||
|
||||
# 运行 MCPHub
|
||||
./start.sh
|
||||
``` */}
|
||||
</Tab>
|
||||
|
||||
<Tab title="本地开发">
|
||||
### 本地开发环境设置
|
||||
|
||||
用于开发、自定义或贡献:
|
||||
|
||||
#### 1. 克隆仓库
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
git clone https://github.com/samanhappy/mcphub.git
|
||||
cd mcphub
|
||||
```
|
||||
|
||||
#### 2. 安装依赖
|
||||
|
||||
```bash
|
||||
# 使用 pnpm 安装依赖(推荐)
|
||||
pnpm install
|
||||
|
||||
# 或使用 npm
|
||||
npm install
|
||||
|
||||
# 或使用 yarn
|
||||
yarn install
|
||||
```
|
||||
|
||||
#### 3. 开发模式
|
||||
|
||||
```bash
|
||||
# 在开发模式下同时启动后端和前端
|
||||
pnpm dev
|
||||
|
||||
# 这将启动:
|
||||
# - 后端在 http://localhost:3001
|
||||
# - 前端在 http://localhost:5173
|
||||
# - 前端代理 API 调用到后端
|
||||
```
|
||||
|
||||
#### 4. 生产构建
|
||||
|
||||
```bash
|
||||
# 构建后端和前端
|
||||
pnpm build
|
||||
|
||||
# 启动生产服务器
|
||||
pnpm start
|
||||
```
|
||||
|
||||
#### 5. 开发脚本
|
||||
|
||||
```bash
|
||||
# 仅后端(用于 API 开发)
|
||||
pnpm backend:dev
|
||||
|
||||
# 仅前端(当后端单独运行时)
|
||||
pnpm frontend:dev
|
||||
|
||||
# 运行测试
|
||||
pnpm test
|
||||
|
||||
# 代码检查
|
||||
pnpm lint
|
||||
|
||||
# 代码格式化
|
||||
pnpm format
|
||||
```
|
||||
|
||||
<Note>
|
||||
在 Windows 上,您可能需要分别运行后端和前端:
|
||||
```bash
|
||||
# 终端 1:后端
|
||||
pnpm backend:dev
|
||||
|
||||
# 终端 2:前端
|
||||
pnpm frontend:dev
|
||||
```
|
||||
</Note>
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="Kubernetes">
|
||||
### Kubernetes 部署
|
||||
|
||||
使用这些清单在 Kubernetes 上部署 MCPHub:
|
||||
|
||||
#### 1. 设置的 ConfigMap
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: mcphub-config
|
||||
data:
|
||||
mcp_settings.json: |
|
||||
{
|
||||
"mcpServers": {
|
||||
"fetch": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"]
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 部署
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: mcphub
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: mcphub
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mcphub
|
||||
spec:
|
||||
containers:
|
||||
- name: mcphub
|
||||
image: samanhappy/mcphub:latest
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
env:
|
||||
- name: PORT
|
||||
value: "3000"
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /app/mcp_settings.json
|
||||
subPath: mcp_settings.json
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: mcphub-config
|
||||
```
|
||||
|
||||
#### 3. 服务
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mcphub-service
|
||||
spec:
|
||||
selector:
|
||||
app: mcphub
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 3000
|
||||
type: ClusterIP
|
||||
```
|
||||
|
||||
#### 4. Ingress (可选)
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: mcphub-ingress
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/proxy-buffering: "off"
|
||||
spec:
|
||||
rules:
|
||||
- host: mcphub.yourdomain.com
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: mcphub-service
|
||||
port:
|
||||
number: 80
|
||||
```
|
||||
|
||||
部署命令:
|
||||
```bash
|
||||
kubectl apply -f mcphub-configmap.yaml
|
||||
kubectl apply -f mcphub-deployment.yaml
|
||||
kubectl apply -f mcphub-service.yaml
|
||||
kubectl apply -f mcphub-ingress.yaml
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## 智能路由设置 (可选)
|
||||
|
||||
智能路由使用向量语义搜索提供 AI 驱动的工具发现。
|
||||
|
||||
### 先决条件
|
||||
|
||||
1. **PostgreSQL 带 pgvector 扩展**
|
||||
2. **OpenAI API Key** (或兼容的嵌入服务)
|
||||
|
||||
### 数据库设置
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Docker PostgreSQL">
|
||||
```bash
|
||||
# 运行带 pgvector 的 PostgreSQL
|
||||
docker run -d \
|
||||
--name mcphub-postgres \
|
||||
-e POSTGRES_DB=mcphub \
|
||||
-e POSTGRES_USER=mcphub \
|
||||
-e POSTGRES_PASSWORD=your_password \
|
||||
-p 5432:5432 \
|
||||
pgvector/pgvector:pg16
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="现有 PostgreSQL">
|
||||
如果您有现有的 PostgreSQL 实例:
|
||||
|
||||
```sql
|
||||
-- 连接到您的 PostgreSQL 实例
|
||||
-- 创建数据库
|
||||
CREATE DATABASE mcphub;
|
||||
|
||||
-- 连接到 mcphub 数据库
|
||||
\c mcphub;
|
||||
|
||||
-- 启用 pgvector 扩展
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="云 PostgreSQL">
|
||||
对于云提供商(AWS RDS、Google Cloud SQL 等):
|
||||
|
||||
1. 在您的云提供商控制台中启用 pgvector 扩展
|
||||
2. 创建名为 `mcphub` 的数据库
|
||||
3. 记下连接详细信息
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
{/* ### 环境配置
|
||||
|
||||
设置以下环境变量:
|
||||
|
||||
```bash
|
||||
# 数据库连接
|
||||
DATABASE_URL=postgresql://mcphub:your_password@localhost:5432/mcphub
|
||||
|
||||
# 用于嵌入的 OpenAI API
|
||||
OPENAI_API_KEY=your_openai_api_key
|
||||
|
||||
# 可选:自定义嵌入模型
|
||||
EMBEDDING_MODEL=text-embedding-3-small
|
||||
|
||||
# 可选:启用智能路由
|
||||
ENABLE_SMART_ROUTING=true
|
||||
``` */}
|
||||
|
||||
## 验证
|
||||
|
||||
安装后,验证 MCPHub 是否正常工作:
|
||||
|
||||
{/* ### 1. 健康检查
|
||||
|
||||
```bash
|
||||
curl http://localhost:3000/api/health
|
||||
```
|
||||
|
||||
预期响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "x.x.x",
|
||||
"uptime": 123
|
||||
}
|
||||
``` */}
|
||||
|
||||
### 控制台访问
|
||||
|
||||
打开浏览器并导航到:
|
||||
|
||||
```
|
||||
http://localhost:3000
|
||||
```
|
||||
|
||||
{/* ### 3. API 测试
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/mcp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/list",
|
||||
"params": {}
|
||||
}'
|
||||
``` */}
|
||||
|
||||
## 故障排除
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Docker 问题">
|
||||
**端口已被使用:**
|
||||
```bash
|
||||
# 检查是什么在使用端口 3000
|
||||
lsof -i :3000
|
||||
|
||||
# 使用不同的端口
|
||||
docker run -p 8080:3000 samanhappy/mcphub
|
||||
```
|
||||
|
||||
**容器无法启动:**
|
||||
```bash
|
||||
# 检查容器日志
|
||||
docker logs mcphub
|
||||
|
||||
# 交互式运行以进行调试
|
||||
docker run -it --rm samanhappy/mcphub /bin/bash
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="npm 安装问题">
|
||||
**权限错误:**
|
||||
```bash
|
||||
# 使用 npx 而不是全局安装
|
||||
npx @samanhappy/mcphub
|
||||
|
||||
# 或修复 npm 权限
|
||||
npm config set prefix ~/.npm-global
|
||||
export PATH=~/.npm-global/bin:$PATH
|
||||
```
|
||||
|
||||
**Node 版本问题:**
|
||||
```bash
|
||||
# 检查 Node 版本
|
||||
node --version
|
||||
|
||||
# 使用 nvm 安装 Node 18+
|
||||
nvm install 18
|
||||
nvm use 18
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="网络问题">
|
||||
**无法访问控制台:**
|
||||
- 检查 MCPHub 是否在运行:`ps aux | grep mcphub`
|
||||
- 验证端口绑定:`netstat -tlnp | grep 3000`
|
||||
- 检查防火墙设置
|
||||
- 尝试通过 `127.0.0.1:3000` 而不是 `localhost:3000` 访问
|
||||
|
||||
**AI 客户端无法连接:**
|
||||
- 确保端点 URL 正确
|
||||
- 检查 MCPHub 是否在代理后面
|
||||
- 验证 Kubernetes/Docker 环境中的网络策略
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="智能路由问题">
|
||||
**数据库连接失败:**
|
||||
```bash
|
||||
# 测试数据库连接
|
||||
psql $DATABASE_URL -c "SELECT 1;"
|
||||
|
||||
# 检查是否安装了 pgvector
|
||||
psql $DATABASE_URL -c "CREATE EXTENSION IF NOT EXISTS vector;"
|
||||
```
|
||||
|
||||
**嵌入服务错误:**
|
||||
- 验证 OpenAI API 密钥是否有效
|
||||
- 检查互联网连接
|
||||
- 监控速率限制
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## 下一步
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="配置" icon="cog" href="/zh/configuration/mcp-settings">
|
||||
配置您的 MCP 服务器和设置
|
||||
</Card>
|
||||
<Card title="快速开始" icon="rocket" href="/zh/quickstart">
|
||||
5分钟内启动并运行
|
||||
</Card>
|
||||
<Card title="服务器管理" icon="server" href="/zh/features/server-management">
|
||||
了解如何管理您的 MCP 服务器
|
||||
</Card>
|
||||
<Card title="API 参考" icon="code" href="/zh/api-reference/introduction">
|
||||
探索完整的 API 文档
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -1,304 +1,212 @@
|
||||
---
|
||||
title: '快速开始'
|
||||
description: '5 分钟内部署 MCPHub 并连接您的第一个 MCP 服务器'
|
||||
title: '快速开始指南'
|
||||
description: '5 分钟内运行 MCPHub'
|
||||
---
|
||||
|
||||
## 欢迎使用 MCPHub!
|
||||
## 安装
|
||||
|
||||
本指南将帮助您在 5 分钟内完成 MCPHub 的部署和配置,并连接您的第一个 MCP 服务器。
|
||||
|
||||
## 前提条件
|
||||
|
||||
在开始之前,请确保您的系统满足以下要求:
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion icon="desktop" title="系统要求">
|
||||
- **操作系统**: Linux、macOS 或 Windows
|
||||
- **内存**: 最少 2GB RAM(推荐 4GB+)
|
||||
- **存储**: 至少 1GB 可用空间
|
||||
- **网络**: 稳定的互联网连接
|
||||
</Accordion>
|
||||
|
||||
<Accordion icon="code" title="软件依赖">
|
||||
- **Node.js**: 18.0+ 版本
|
||||
- **Docker**: 最新版本(可选,用于容器化部署)
|
||||
- **Git**: 用于代码管理
|
||||
|
||||
检查版本:
|
||||
```bash
|
||||
node --version # 应该 >= 18.0.0
|
||||
npm --version # 应该 >= 8.0.0
|
||||
docker --version # 可选
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## 安装 MCPHub
|
||||
|
||||
### 方式一:使用 npm(推荐)
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion icon="download" title="安装 MCPHub CLI">
|
||||
首先安装 MCPHub 命令行工具:
|
||||
<Tabs>
|
||||
<Tab title="Docker(推荐)">
|
||||
使用 Docker 是最快的开始方式:
|
||||
|
||||
```bash
|
||||
npm install -g @mcphub/cli
|
||||
# 使用默认配置运行
|
||||
docker run -p 3000:3000 samanhappy/mcphub
|
||||
```
|
||||
|
||||
验证安装:
|
||||
或者挂载自定义配置:
|
||||
|
||||
```bash
|
||||
mcphub --version
|
||||
# 使用自定义 MCP 设置运行
|
||||
docker run -p 3000:3000 \
|
||||
-v $(pwd)/mcp_settings.json:/app/mcp_settings.json \
|
||||
samanhappy/mcphub
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion icon="folder-plus" title="创建新项目">
|
||||
创建一个新的 MCPHub 项目:
|
||||
</Tab>
|
||||
<Tab title="本地开发">
|
||||
用于开发或自定义:
|
||||
|
||||
```bash
|
||||
# 创建项目
|
||||
mcphub init my-mcphub-project
|
||||
cd my-mcphub-project
|
||||
# 克隆仓库
|
||||
git clone https://github.com/samanhappy/mcphub.git
|
||||
cd mcphub
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
pnpm install
|
||||
|
||||
# 启动开发服务器
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
这会同时启动后端(端口 3001)和前端(端口 5173)的开发模式。
|
||||
|
||||
<Accordion icon="gear" title="配置环境">
|
||||
复制并编辑环境变量文件:
|
||||
</Tab>
|
||||
<Tab title="npm 包">
|
||||
将 MCPHub 安装为全局包:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# 全局安装
|
||||
npm install -g @samanhappy/mcphub
|
||||
|
||||
# 运行 MCPHub
|
||||
mcphub
|
||||
```
|
||||
|
||||
编辑 `.env` 文件,设置基本配置:
|
||||
```bash
|
||||
# 服务器配置
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
# 数据库配置(使用内置 SQLite)
|
||||
DATABASE_URL=sqlite:./data/mcphub.db
|
||||
## 初始设置
|
||||
|
||||
# JWT 密钥(请更改为安全的随机字符串)
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-me
|
||||
### 1. 访问控制面板
|
||||
|
||||
# 管理员账户
|
||||
ADMIN_EMAIL=admin@example.com
|
||||
ADMIN_PASSWORD=admin123
|
||||
```
|
||||
打开浏览器并导航到:
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
### 方式二:使用 Docker
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion icon="docker" title="Docker 快速部署">
|
||||
使用 Docker Compose 一键部署:
|
||||
|
||||
```bash
|
||||
# 下载配置文件
|
||||
curl -O https://raw.githubusercontent.com/mcphub/mcphub/main/docker-compose.yml
|
||||
|
||||
# 启动服务
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
或者直接运行 Docker 容器:
|
||||
```bash
|
||||
docker run -d \
|
||||
--name mcphub \
|
||||
-p 3000:3000 \
|
||||
-e NODE_ENV=production \
|
||||
-e JWT_SECRET=your-secret-key \
|
||||
mcphub/server:latest
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## 启动 MCPHub
|
||||
|
||||
### 开发模式启动
|
||||
|
||||
```bash
|
||||
# 初始化数据库
|
||||
npm run db:setup
|
||||
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
```
|
||||
http://localhost:3000
|
||||
```
|
||||
|
||||
### 生产模式启动
|
||||
### 2. 登录
|
||||
|
||||
```bash
|
||||
# 构建应用
|
||||
npm run build
|
||||
使用默认凭据:
|
||||
|
||||
# 启动生产服务器
|
||||
npm start
|
||||
```
|
||||
|
||||
<Note>开发模式下,MCPHub 会在 `http://localhost:3000` 启动,并具有热重载功能。</Note>
|
||||
|
||||
## 首次访问和配置
|
||||
|
||||
### 1. 访问管理界面
|
||||
|
||||
打开浏览器,访问 `http://localhost:3000`,您将看到 MCPHub 的欢迎页面。
|
||||
|
||||
### 2. 登录管理员账户
|
||||
|
||||
使用您在 `.env` 文件中设置的管理员凭据登录:
|
||||
|
||||
- **邮箱**: `admin@example.com`
|
||||
- **用户名**: `admin`
|
||||
- **密码**: `admin123`
|
||||
|
||||
<Warning>首次登录后,请立即更改默认密码以确保安全!</Warning>
|
||||
<Warning>为了安全起见,请在首次登录后立即更改这些默认凭据。</Warning>
|
||||
|
||||
### 3. 完成初始配置
|
||||
### 3. 配置您的第一个 MCP 服务器
|
||||
|
||||
登录后,系统会引导您完成初始配置:
|
||||
1. 在控制面板中点击 **"添加服务器"**
|
||||
2. 输入服务器详细信息:
|
||||
- **名称**: 唯一标识符(例如 `fetch`)
|
||||
- **命令**: 可执行命令(`uvx`)
|
||||
- **参数**: 命令参数(`["mcp-server-fetch"]`)
|
||||
- **环境**: 任何所需的环境变量
|
||||
|
||||
1. **更改管理员密码**
|
||||
2. **设置组织信息**
|
||||
3. **配置基本设置**
|
||||
fetch 服务器的示例配置:
|
||||
|
||||
## 添加您的第一个 MCP 服务器
|
||||
|
||||
### 1. 准备 MCP 服务器
|
||||
|
||||
如果您还没有 MCP 服务器,可以使用我们的示例服务器进行测试:
|
||||
|
||||
```bash
|
||||
# 克隆示例服务器
|
||||
git clone https://github.com/mcphub/example-mcp-server.git
|
||||
cd example-mcp-server
|
||||
|
||||
# 安装依赖并启动
|
||||
npm install
|
||||
npm start
|
||||
```json
|
||||
{
|
||||
"name": "fetch",
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"],
|
||||
"env": {}
|
||||
}
|
||||
```
|
||||
|
||||
示例服务器将在 `http://localhost:3001` 启动。
|
||||
## 基本使用
|
||||
|
||||
### 2. 在 MCPHub 中添加服务器
|
||||
### 连接 AI 客户端
|
||||
|
||||
在 MCPHub 管理界面中:
|
||||
一旦配置了服务器,使用 MCPHub 端点连接您的 AI 客户端:
|
||||
|
||||
1. 点击 **"添加服务器"** 按钮
|
||||
2. 填写服务器信息:
|
||||
```
|
||||
名称: Example MCP Server
|
||||
端点: http://localhost:3001
|
||||
描述: 示例 MCP 服务器用于测试
|
||||
```
|
||||
3. 选择功能类型(如:chat、completion、analysis)
|
||||
4. 点击 **"测试连接"** 验证服务器可达性
|
||||
5. 点击 **"保存"** 完成添加
|
||||
<Tabs>
|
||||
<Tab title="所有服务器">
|
||||
访问所有已配置的 MCP 服务器:``` http://localhost:3000/mcp ```
|
||||
</Tab>
|
||||
<Tab title="特定组">
|
||||
访问特定组中的服务器:``` http://localhost:3000/mcp/{groupName} ```
|
||||
</Tab>
|
||||
<Tab title="单个服务器">
|
||||
访问单个服务器:``` http://localhost:3000/mcp/{serverName} ```
|
||||
</Tab>
|
||||
<Tab title="智能路由">
|
||||
使用 AI 驱动的工具发现:``` http://localhost:3000/mcp/$smart ```
|
||||
<Info>智能路由需要使用 pgvector 的 PostgreSQL 和 OpenAI API 密钥。</Info>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### 3. 验证服务器状态
|
||||
### 示例:添加热门 MCP 服务器
|
||||
|
||||
添加成功后,您应该能在服务器列表中看到新添加的服务器,状态显示为 **"活跃"**(绿色)。
|
||||
以下是一些您可以添加的热门 MCP 服务器:
|
||||
|
||||
## 测试路由功能
|
||||
|
||||
### 发送测试请求
|
||||
|
||||
使用 cURL 或其他 HTTP 客户端测试路由功能:
|
||||
|
||||
```bash
|
||||
# 发送聊天请求
|
||||
curl -X POST http://localhost:3000/api/chat \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Hello, this is a test message!"
|
||||
<AccordionGroup>
|
||||
<Accordion title="Web Fetch 服务器">
|
||||
```json
|
||||
{
|
||||
"name": "fetch",
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"]
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Playwright 浏览器自动化">
|
||||
```json
|
||||
{
|
||||
"name": "playwright",
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless"]
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="高德地图(需要 API 密钥)">
|
||||
```json
|
||||
{
|
||||
"name": "amap",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@amap/amap-maps-mcp-server"],
|
||||
"env": {
|
||||
"AMAP_MAPS_API_KEY": "your-api-key-here"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
### 查看请求日志
|
||||
|
||||
在 MCPHub 管理界面的 **"监控"** 页面中,您可以实时查看:
|
||||
|
||||
- 请求数量和响应时间
|
||||
- 服务器健康状态
|
||||
- 错误日志和统计
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Slack 集成">
|
||||
```json
|
||||
{
|
||||
"name": "slack",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-slack"],
|
||||
"env": {
|
||||
"SLACK_BOT_TOKEN": "your-bot-token",
|
||||
"SLACK_TEAM_ID": "your-team-id"
|
||||
}
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## 后续步骤
|
||||
|
||||
恭喜!您已经成功部署了 MCPHub 并添加了第一个 MCP 服务器。接下来您可以:
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="配置负载均衡" icon="balance-scale" href="/zh/features/smart-routing">
|
||||
学习如何配置智能路由和负载均衡策略
|
||||
<Card title="服务器管理" icon="server" href="/zh/features/server-management">
|
||||
学习高级服务器配置和管理
|
||||
</Card>
|
||||
<Card title="添加更多服务器" icon="plus" href="/zh/features/server-management">
|
||||
了解服务器管理的高级功能
|
||||
<Card title="组管理" icon="users" href="/zh/features/group-management">
|
||||
将服务器组织成逻辑组
|
||||
</Card>
|
||||
<Card title="设置监控告警" icon="bell" href="/zh/features/monitoring">
|
||||
配置性能监控和告警通知
|
||||
<Card title="智能路由" icon="route" href="/zh/features/smart-routing">
|
||||
设置 AI 驱动的工具发现
|
||||
</Card>
|
||||
<Card title="API 集成" icon="code" href="/zh/api-reference/introduction">
|
||||
将 MCPHub 集成到您的应用程序中
|
||||
<Card title="API 参考" icon="code" href="/zh/api-reference/introduction">
|
||||
探索完整的 API 文档
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## 常见问题
|
||||
## 故障排除
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion icon="question" title="无法连接到 MCP 服务器">
|
||||
**可能原因**:
|
||||
- 服务器地址错误或服务器未启动
|
||||
- 防火墙阻止连接
|
||||
- 网络配置问题
|
||||
|
||||
**解决方案**:
|
||||
1. 验证服务器是否正在运行:`curl http://localhost:3001/health`
|
||||
2. 检查防火墙设置
|
||||
3. 确认网络连接正常
|
||||
|
||||
<Accordion title="服务器无法启动">
|
||||
- 检查 MCP 服务器命令是否在您的 PATH 中可访问
|
||||
- 验证环境变量是否正确设置
|
||||
- 检查 MCPHub 日志以获取详细错误信息
|
||||
</Accordion>
|
||||
|
||||
<Accordion icon="question" title="服务器状态显示为离线">
|
||||
**可能原因**:
|
||||
- 健康检查失败
|
||||
- 服务器响应超时
|
||||
- 服务器崩溃或重启
|
||||
|
||||
**解决方案**:
|
||||
1. 检查服务器日志
|
||||
2. 调整健康检查间隔
|
||||
3. 重启服务器进程
|
||||
|
||||
<Accordion title="无法从 AI 客户端连接">
|
||||
- 确保 MCPHub 在正确的端口上运行
|
||||
- 检查防火墙设置
|
||||
- 验证端点 URL 格式
|
||||
</Accordion>
|
||||
|
||||
<Accordion icon="question" title="忘记管理员密码">
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 重置管理员密码
|
||||
npm run reset-admin-password
|
||||
```
|
||||
或者删除数据库文件重新初始化:
|
||||
```bash
|
||||
rm data/mcphub.db
|
||||
npm run db:setup
|
||||
```
|
||||
<Accordion title="身份验证问题">
|
||||
- 验证凭据是否正确
|
||||
- 检查 JWT 令牌是否有效
|
||||
- 尝试清除浏览器缓存和 cookie
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## 获取帮助
|
||||
|
||||
如果您在设置过程中遇到问题:
|
||||
|
||||
- 📖 查看 [完整文档](/zh/development/getting-started)
|
||||
- 🐛 在 [GitHub](https://github.com/mcphub/mcphub/issues) 上报告问题
|
||||
- 💬 加入 [Discord 社区](https://discord.gg/mcphub) 获取实时帮助
|
||||
- 📧 发送邮件至 support@mcphub.io
|
||||
需要更多帮助?加入我们的 [Discord 社区](https://discord.gg/qMKNsn5Q) 获取支持!
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MCP Hub Dashboard</title>
|
||||
<title>MCPHub Dashboard</title>
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom';
|
||||
import { BrowserRouter as Router, Route, Routes, Navigate, useParams } from 'react-router-dom';
|
||||
import { AuthProvider } from './contexts/AuthContext';
|
||||
import { ToastProvider } from './contexts/ToastContext';
|
||||
import { ThemeProvider } from './contexts/ThemeContext';
|
||||
@@ -15,6 +15,12 @@ import MarketPage from './pages/MarketPage';
|
||||
import LogsPage from './pages/LogsPage';
|
||||
import { getBasePath } from './utils/runtime';
|
||||
|
||||
// Helper component to redirect cloud server routes to market
|
||||
const CloudRedirect: React.FC = () => {
|
||||
const { serverName } = useParams<{ serverName: string }>();
|
||||
return <Navigate to={`/market/${serverName}?tab=cloud`} replace />;
|
||||
};
|
||||
|
||||
function App() {
|
||||
const basename = getBasePath();
|
||||
return (
|
||||
@@ -35,6 +41,12 @@ function App() {
|
||||
<Route path="/users" element={<UsersPage />} />
|
||||
<Route path="/market" element={<MarketPage />} />
|
||||
<Route path="/market/:serverName" element={<MarketPage />} />
|
||||
{/* Legacy cloud routes redirect to market with cloud tab */}
|
||||
<Route path="/cloud" element={<Navigate to="/market?tab=cloud" replace />} />
|
||||
<Route
|
||||
path="/cloud/:serverName"
|
||||
element={<CloudRedirect />}
|
||||
/>
|
||||
<Route path="/logs" element={<LogsPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
</Route>
|
||||
|
||||
@@ -50,9 +50,8 @@ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => {
|
||||
}
|
||||
|
||||
const result = await createGroup(formData.name, formData.description, formData.servers)
|
||||
|
||||
if (!result) {
|
||||
setError(t('groups.createError'))
|
||||
if (!result || !result.success) {
|
||||
setError(result?.message || t('groups.createError'))
|
||||
setIsSubmitting(false)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ServerForm from './ServerForm'
|
||||
import { getApiUrl } from '../utils/runtime'
|
||||
import { apiPost } from '../utils/fetchInterceptor'
|
||||
import { detectVariables } from '../utils/variableDetection'
|
||||
|
||||
interface AddServerFormProps {
|
||||
@@ -34,26 +34,12 @@ const AddServerForm = ({ onAdd }: AddServerFormProps) => {
|
||||
const submitServer = async (payload: any) => {
|
||||
try {
|
||||
setError(null)
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/servers'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || ''
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
const result = await apiPost('/servers', payload)
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
if (!result.success) {
|
||||
// Use specific error message from the response if available
|
||||
if (result && result.message) {
|
||||
setError(result.message)
|
||||
} else if (response.status === 400) {
|
||||
setError(t('server.invalidData'))
|
||||
} else if (response.status === 409) {
|
||||
setError(t('server.alreadyExists', { serverName: payload.name }))
|
||||
} else {
|
||||
setError(t('server.addError'))
|
||||
}
|
||||
|
||||
144
frontend/src/components/CloudServerCard.tsx
Normal file
144
frontend/src/components/CloudServerCard.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CloudServer } from '@/types';
|
||||
|
||||
interface CloudServerCardProps {
|
||||
server: CloudServer;
|
||||
onClick: (server: CloudServer) => void;
|
||||
}
|
||||
|
||||
const CloudServerCard: React.FC<CloudServerCardProps> = ({ server, onClick }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleClick = () => {
|
||||
onClick(server);
|
||||
};
|
||||
|
||||
// Extract a brief description from content if description is too long
|
||||
const getDisplayDescription = () => {
|
||||
if (server.description && server.description.length <= 150) {
|
||||
return server.description;
|
||||
}
|
||||
|
||||
// Try to extract a summary from content
|
||||
if (server.content) {
|
||||
const lines = server.content.split('\n').filter(line => line.trim());
|
||||
for (const line of lines) {
|
||||
if (line.length > 50 && line.length <= 150) {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return server.description ?
|
||||
server.description.slice(0, 150) + '...' :
|
||||
t('cloud.noDescription');
|
||||
};
|
||||
|
||||
// Format date for display
|
||||
const formatDate = (dateString: string) => {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
return `${year}/${month}/${day}`;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// Get initials for avatar
|
||||
const getAuthorInitials = (name: string) => {
|
||||
return name
|
||||
.split(' ')
|
||||
.map(word => word.charAt(0))
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-white border border-gray-200 rounded-xl p-4 hover:shadow-lg hover:border-blue-400 hover:-translate-y-1 transition-all duration-300 cursor-pointer group relative overflow-hidden h-full flex flex-col"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{/* Background gradient overlay on hover */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-50/0 to-purple-50/0 group-hover:from-blue-50/30 group-hover:to-purple-50/30 transition-all duration-300 pointer-events-none" />
|
||||
|
||||
{/* Server Header */}
|
||||
<div className="relative z-10 flex-1 flex flex-col">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-bold text-gray-900 group-hover:text-blue-600 transition-colors duration-200 mb-2 line-clamp-2">
|
||||
{server.title || server.name}
|
||||
</h3>
|
||||
|
||||
{/* Author Section */}
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<div className="w-7 h-7 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white text-xs font-semibold">
|
||||
{getAuthorInitials(server.author_name)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-700">{server.author_name}</p>
|
||||
{server.updated_at && (
|
||||
<p className="text-xs text-gray-500">
|
||||
{t('cloud.updated')} {formatDate(server.updated_at)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Server Type Badge */}
|
||||
<div className="flex flex-col items-end space-y-2">
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
MCP Server
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="mb-3 flex-1">
|
||||
<p className="text-gray-600 text-sm leading-relaxed line-clamp-2">
|
||||
{getDisplayDescription()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tools Info */}
|
||||
{server.tools && server.tools.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<span className="text-sm text-gray-600 font-medium">
|
||||
{server.tools.length} {server.tools.length === 1 ? t('cloud.tool') : t('cloud.tools')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer - 固定在底部 */}
|
||||
<div className="flex items-center justify-between pt-3 border-t border-gray-100 mt-auto">
|
||||
<div className="flex items-center space-x-2 text-xs text-gray-500">
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span>{formatDate(server.created_at)}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center text-blue-600 text-sm font-medium group-hover:text-blue-700 transition-colors">
|
||||
<span>{t('cloud.viewDetails')}</span>
|
||||
<svg className="w-4 h-4 ml-1 transform group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CloudServerCard;
|
||||
573
frontend/src/components/CloudServerDetail.tsx
Normal file
573
frontend/src/components/CloudServerDetail.tsx
Normal file
@@ -0,0 +1,573 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CloudServer, CloudServerTool, ServerConfig } from '@/types';
|
||||
import { apiGet } from '@/utils/fetchInterceptor';
|
||||
import { useSettingsData } from '@/hooks/useSettingsData';
|
||||
import MCPRouterApiKeyError from './MCPRouterApiKeyError';
|
||||
import ServerForm from './ServerForm';
|
||||
|
||||
interface CloudServerDetailProps {
|
||||
serverName: string;
|
||||
onBack: () => void;
|
||||
onCallTool?: (serverName: string, toolName: string, args: Record<string, any>) => Promise<any>;
|
||||
fetchServerTools?: (serverName: string) => Promise<CloudServerTool[]>;
|
||||
onInstall?: (server: CloudServer, config: ServerConfig) => void;
|
||||
installing?: boolean;
|
||||
isInstalled?: boolean;
|
||||
}
|
||||
|
||||
const CloudServerDetail: React.FC<CloudServerDetailProps> = ({
|
||||
serverName,
|
||||
onBack,
|
||||
onCallTool,
|
||||
fetchServerTools,
|
||||
onInstall,
|
||||
installing = false,
|
||||
isInstalled = false
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { mcpRouterConfig } = useSettingsData();
|
||||
const [server, setServer] = useState<CloudServer | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [tools, setTools] = useState<CloudServerTool[]>([]);
|
||||
const [loadingTools, setLoadingTools] = useState(false);
|
||||
const [toolsApiKeyError, setToolsApiKeyError] = useState(false);
|
||||
const [toolCallLoading, setToolCallLoading] = useState<string | null>(null);
|
||||
const [toolCallResults, setToolCallResults] = useState<Record<string, any>>({});
|
||||
const [toolArgs, setToolArgs] = useState<Record<string, Record<string, any>>>({});
|
||||
const [expandedSchemas, setExpandedSchemas] = useState<Record<string, boolean>>({});
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [installError, setInstallError] = useState<string | null>(null);
|
||||
|
||||
// Helper function to check if error is MCPRouter API key not configured
|
||||
const isMCPRouterApiKeyError = (errorMessage: string) => {
|
||||
console.error('Checking for MCPRouter API key error:', errorMessage);
|
||||
return errorMessage === 'MCPROUTER_API_KEY_NOT_CONFIGURED' ||
|
||||
errorMessage.toLowerCase().includes('mcprouter api key not configured');
|
||||
};
|
||||
|
||||
// Helper function to determine button state for install
|
||||
const getInstallButtonProps = () => {
|
||||
if (isInstalled) {
|
||||
return {
|
||||
className: "bg-green-600 cursor-default px-4 py-2 rounded text-sm font-medium text-white",
|
||||
disabled: true,
|
||||
text: t('market.installed')
|
||||
};
|
||||
} else if (installing) {
|
||||
return {
|
||||
className: "bg-gray-400 cursor-not-allowed px-4 py-2 rounded text-sm font-medium text-white",
|
||||
disabled: true,
|
||||
text: t('market.installing')
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
className: "bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded text-sm font-medium text-white transition-colors",
|
||||
disabled: false,
|
||||
text: t('market.install')
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Handle install button click
|
||||
const handleInstall = () => {
|
||||
if (!isInstalled && onInstall) {
|
||||
setModalVisible(true);
|
||||
setInstallError(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle modal close
|
||||
const handleModalClose = () => {
|
||||
setModalVisible(false);
|
||||
setInstallError(null);
|
||||
};
|
||||
|
||||
// Handle install form submission
|
||||
const handleInstallSubmit = async (payload: any) => {
|
||||
try {
|
||||
if (!server || !onInstall) return;
|
||||
|
||||
setInstallError(null);
|
||||
onInstall(server, payload.config);
|
||||
setModalVisible(false);
|
||||
} catch (err) {
|
||||
console.error('Error installing server:', err);
|
||||
setInstallError(t('errors.serverInstall'));
|
||||
}
|
||||
};
|
||||
|
||||
// Load server details
|
||||
useEffect(() => {
|
||||
const loadServerDetails = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await apiGet(`/cloud/servers/${serverName}`);
|
||||
|
||||
if (response && response.success && response.data) {
|
||||
setServer(response.data);
|
||||
setTools(response.data.tools || []);
|
||||
} else {
|
||||
setError(t('cloud.serverNotFound'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load server details:', err);
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadServerDetails();
|
||||
}, [serverName, t]);
|
||||
|
||||
// Load tools if not already loaded
|
||||
useEffect(() => {
|
||||
const loadTools = async () => {
|
||||
if (server && (!server.tools || server.tools.length === 0) && fetchServerTools) {
|
||||
setLoadingTools(true);
|
||||
setToolsApiKeyError(false);
|
||||
try {
|
||||
const fetchedTools = await fetchServerTools(server.name);
|
||||
setTools(fetchedTools);
|
||||
} catch (error) {
|
||||
console.error('Failed to load tools:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
if (isMCPRouterApiKeyError(errorMessage)) {
|
||||
setToolsApiKeyError(true);
|
||||
}
|
||||
} finally {
|
||||
setLoadingTools(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadTools();
|
||||
}, [server?.name, server?.tools, fetchServerTools]);
|
||||
|
||||
// Format creation date
|
||||
const formatDate = (dateStr: string) => {
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
} catch {
|
||||
return dateStr;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle tool argument changes
|
||||
const handleArgChange = (toolName: string, argName: string, value: any) => {
|
||||
setToolArgs(prev => ({
|
||||
...prev,
|
||||
[toolName]: {
|
||||
...prev[toolName],
|
||||
[argName]: value
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
// Handle tool call
|
||||
const handleCallTool = async (toolName: string) => {
|
||||
if (!onCallTool || !server) return;
|
||||
|
||||
setToolCallLoading(toolName);
|
||||
try {
|
||||
const args = toolArgs[toolName] || {};
|
||||
const result = await onCallTool(server.server_key, toolName, args);
|
||||
setToolCallResults(prev => ({
|
||||
...prev,
|
||||
[toolName]: result
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Tool call failed:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
setToolCallResults(prev => ({
|
||||
...prev,
|
||||
[toolName]: { error: errorMessage }
|
||||
}));
|
||||
} finally {
|
||||
setToolCallLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle schema visibility
|
||||
const toggleSchema = (toolName: string) => {
|
||||
setExpandedSchemas(prev => ({
|
||||
...prev,
|
||||
[toolName]: !prev[toolName]
|
||||
}));
|
||||
};
|
||||
|
||||
// Render tool input field based on schema
|
||||
const renderToolInput = (tool: CloudServerTool, propName: string, propSchema: any) => {
|
||||
const currentValue = toolArgs[tool.name]?.[propName] || '';
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
let value: any = e.target.value;
|
||||
|
||||
// Convert value based on schema type
|
||||
if (propSchema.type === 'number' || propSchema.type === 'integer') {
|
||||
value = value === '' ? undefined : Number(value);
|
||||
} else if (propSchema.type === 'boolean') {
|
||||
value = e.target.value === 'true';
|
||||
}
|
||||
|
||||
handleArgChange(tool.name, propName, value);
|
||||
};
|
||||
|
||||
if (propSchema.type === 'boolean') {
|
||||
return (
|
||||
<select
|
||||
value={currentValue === true ? 'true' : currentValue === false ? 'false' : ''}
|
||||
onChange={handleChange}
|
||||
className="w-full border rounded-md px-3 py-2 border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 form-input"
|
||||
>
|
||||
<option value=""></option>
|
||||
<option value="true">True</option>
|
||||
<option value="false">False</option>
|
||||
</select>
|
||||
);
|
||||
} else if (propSchema.type === 'number' || propSchema.type === 'integer') {
|
||||
return (
|
||||
<input
|
||||
type="number"
|
||||
step={propSchema.type === 'integer' ? '1' : 'any'}
|
||||
value={currentValue || ''}
|
||||
onChange={handleChange}
|
||||
className="w-full border rounded-md px-3 py-2 border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 form-input"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={currentValue || ''}
|
||||
onChange={handleChange}
|
||||
className="w-full border rounded-md px-3 py-2 border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 form-input"
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<div className="mb-6">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="inline-flex items-center text-gray-600 hover:text-gray-900 transition-colors group"
|
||||
>
|
||||
<svg className="h-5 w-5 mr-2 transform group-hover:-translate-x-1 transition-transform" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
{t('cloud.backToList')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="bg-white rounded-xl shadow-sm p-12">
|
||||
<div className="flex flex-col items-center">
|
||||
<svg className="animate-spin h-12 w-12 text-blue-500 mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<p className="text-gray-600 text-lg">{t('app.loading')}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : error && !isMCPRouterApiKeyError(error) ? (
|
||||
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<div className="flex items-center">
|
||||
<svg className="h-5 w-5 text-red-400 mr-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<p className="text-red-700">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : !server ? (
|
||||
<div className="bg-white rounded-xl shadow-sm p-12">
|
||||
<div className="text-center">
|
||||
<svg className="h-12 w-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M9.172 16.172a4 4 0 015.656 0M9 12h6m-6-4h6m2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
||||
</svg>
|
||||
<p className="text-gray-600 text-lg">{t('cloud.serverNotFound')}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{/* Server Header Card */}
|
||||
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-gray-100 to-gray-200 px-6 py-4">
|
||||
<div className="flex justify-between items-end">
|
||||
<div className="flex-1">
|
||||
<h1 className="text-2xl font-bold text-gray-800 mb-2">
|
||||
{server.title || server.name}
|
||||
</h1>
|
||||
<div className="flex flex-wrap items-center gap-4 text-gray-600">
|
||||
<span className="text-sm bg-white/60 text-gray-700 px-3 py-1 rounded-full">
|
||||
{server.name}
|
||||
</span>
|
||||
<div className="flex items-center">
|
||||
<svg className="h-4 w-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" />
|
||||
</svg>
|
||||
{t('cloud.by')} {server.author_name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right flex flex-col items-end gap-3">
|
||||
<div className="text-xs text-gray-500">
|
||||
{t('cloud.updated')}: {formatDate(server.updated_at)}
|
||||
</div>
|
||||
{onInstall && !isMCPRouterApiKeyError(error || '') && !toolsApiKeyError && (
|
||||
<button
|
||||
onClick={handleInstall}
|
||||
disabled={getInstallButtonProps().disabled}
|
||||
className={getInstallButtonProps().className}
|
||||
>
|
||||
{getInstallButtonProps().text}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description Card */}
|
||||
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center">
|
||||
<svg className="h-5 w-5 text-gray-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{t('cloud.description')}
|
||||
</h2>
|
||||
<p className="text-gray-700 leading-relaxed">{server.description}</p>
|
||||
</div>
|
||||
|
||||
{/* Content Card */}
|
||||
{server.content && (
|
||||
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center">
|
||||
<svg className="h-5 w-5 text-gray-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
||||
</svg>
|
||||
{t('cloud.details')}
|
||||
</h2>
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4 overflow-auto">
|
||||
<pre className="text-sm text-gray-800 whitespace-pre-wrap">{server.content}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tools Card */}
|
||||
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center">
|
||||
<svg className="h-5 w-5 text-gray-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
{t('cloud.tools')}
|
||||
{tools.length > 0 && (
|
||||
<span className="ml-2 bg-blue-100 text-blue-800 text-sm font-medium px-2.5 py-0.5 rounded-full">
|
||||
{tools.length}
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
|
||||
{/* Check for API key error */}
|
||||
{toolsApiKeyError && (
|
||||
<MCPRouterApiKeyError />
|
||||
)}
|
||||
|
||||
{loadingTools ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<svg className="animate-spin h-8 w-8 text-blue-500 mr-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span className="text-gray-600">{t('cloud.loadingTools')}</span>
|
||||
</div>
|
||||
) : tools.length === 0 && !toolsApiKeyError ? (
|
||||
<div className="text-center py-12">
|
||||
<svg className="h-12 w-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||
</svg>
|
||||
<p className="text-gray-600">{t('cloud.noTools')}</p>
|
||||
</div>
|
||||
) : tools.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{tools.map((tool, index) => (
|
||||
<div key={index} className="border border-gray-200 rounded-lg p-6 hover:border-gray-300 transition-colors">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2 flex items-center">
|
||||
<span className="bg-blue-100 text-blue-800 text-xs font-medium px-2 py-1 rounded mr-3">
|
||||
TOOL
|
||||
</span>
|
||||
{tool.name}
|
||||
</h3>
|
||||
<p className="text-gray-600 leading-relaxed whitespace-pre-wrap">{tool.description}</p>
|
||||
</div>
|
||||
{onCallTool && (
|
||||
<button
|
||||
onClick={() => handleCallTool(tool.name)}
|
||||
disabled={toolCallLoading === tool.name}
|
||||
className="ml-4 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center min-w-[100px] justify-center"
|
||||
>
|
||||
{toolCallLoading === tool.name ? (
|
||||
<>
|
||||
<svg className="animate-spin h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
{t('cloud.calling')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.828 14.828a4 4 0 01-5.656 0M9 10h6m2 8l4-4H7l4 4z" />
|
||||
</svg>
|
||||
{t('cloud.callTool')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tool inputs */}
|
||||
{tool.inputSchema && tool.inputSchema.properties && Object.keys(tool.inputSchema.properties).length > 0 && (
|
||||
<div className="border-t border-gray-100 pt-4">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<h4 className="text-sm font-medium text-gray-700">{t('cloud.parameters')}</h4>
|
||||
<button
|
||||
onClick={() => toggleSchema(tool.name)}
|
||||
className="text-sm text-blue-600 hover:text-blue-800 focus:outline-none flex items-center gap-1 transition-colors"
|
||||
>
|
||||
{t('cloud.viewSchema')}
|
||||
<svg
|
||||
className={`h-3 w-3 transition-transform duration-200 ${expandedSchemas[tool.name] ? 'rotate-90' : 'rotate-0'}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Schema content */}
|
||||
{expandedSchemas[tool.name] && (
|
||||
<div className="mb-4">
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3 overflow-auto">
|
||||
<pre className="text-sm text-gray-800">
|
||||
{JSON.stringify(tool.inputSchema, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
{Object.entries(tool.inputSchema.properties).map(([propName, propSchema]: [string, any]) => (
|
||||
<div key={propName} className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
{propName}
|
||||
{tool.inputSchema.required?.includes(propName) && (
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
)}
|
||||
</label>
|
||||
{propSchema.description && (
|
||||
<p className="text-xs text-gray-500">{propSchema.description}</p>
|
||||
)}
|
||||
{renderToolInput(tool, propName, propSchema)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tool call result */}
|
||||
{toolCallResults[tool.name] && (
|
||||
<div className="border-t border-gray-100 pt-4 mt-4">
|
||||
{toolCallResults[tool.name].error ? (
|
||||
<>
|
||||
{isMCPRouterApiKeyError(toolCallResults[tool.name].error) ? (
|
||||
<MCPRouterApiKeyError />
|
||||
) : (
|
||||
<>
|
||||
<h4 className="text-sm font-medium text-red-600 mb-3 flex items-center">
|
||||
<svg className="h-4 w-4 text-red-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
{t('cloud.error')}
|
||||
</h4>
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<pre className="text-sm text-red-800 whitespace-pre-wrap overflow-auto">
|
||||
{toolCallResults[tool.name].error}
|
||||
</pre>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
||||
<svg className="h-4 w-4 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
{t('cloud.result')}
|
||||
</h4>
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<pre className="text-sm text-gray-800 whitespace-pre-wrap overflow-auto">
|
||||
{JSON.stringify(toolCallResults[tool.name], null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Install Modal */}
|
||||
{modalVisible && server && (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||
<ServerForm
|
||||
onSubmit={handleInstallSubmit}
|
||||
onCancel={handleModalClose}
|
||||
modalTitle={t('cloud.installServer', { name: server.title || server.name })}
|
||||
formError={installError}
|
||||
initialData={{
|
||||
name: server.name,
|
||||
status: 'disconnected',
|
||||
config: {
|
||||
type: 'streamable-http',
|
||||
url: server.server_url,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${mcpRouterConfig.apiKey || '<MCPROUTER_API_KEY>'}`,
|
||||
'HTTP-Referer': mcpRouterConfig.referer || '<YOUR_APP_URL>',
|
||||
'X-Title': mcpRouterConfig.title || '<YOUR_APP_NAME>'
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CloudServerDetail;
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { apiPost, apiGet, apiPut, fetchWithInterceptors } from '@/utils/fetchInterceptor';
|
||||
import { getApiUrl } from '@/utils/runtime';
|
||||
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
||||
|
||||
@@ -81,12 +82,8 @@ const DxtUploadForm: React.FC<DxtUploadFormProps> = ({ onSuccess, onCancel }) =>
|
||||
const formData = new FormData();
|
||||
formData.append('dxtFile', selectedFile);
|
||||
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/dxt/upload'), {
|
||||
const response = await fetchWithInterceptors(getApiUrl('/dxt/upload'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
@@ -119,19 +116,11 @@ const DxtUploadForm: React.FC<DxtUploadFormProps> = ({ onSuccess, onCancel }) =>
|
||||
// Convert DXT manifest to MCPHub stdio server configuration
|
||||
const serverConfig = convertDxtToMcpConfig(manifestData, extractDir, serverName);
|
||||
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
|
||||
// First, check if server exists
|
||||
if (!forceOverride) {
|
||||
const checkResponse = await fetch(getApiUrl('/servers'), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
const checkResult = await apiGet('/servers');
|
||||
|
||||
if (checkResponse.ok) {
|
||||
const checkResult = await checkResponse.json();
|
||||
if (checkResult.success) {
|
||||
const existingServer = checkResult.data?.find((server: any) => server.name === serverName);
|
||||
|
||||
if (existingServer) {
|
||||
@@ -145,25 +134,17 @@ const DxtUploadForm: React.FC<DxtUploadFormProps> = ({ onSuccess, onCancel }) =>
|
||||
}
|
||||
|
||||
// Install or override the server
|
||||
const method = forceOverride ? 'PUT' : 'POST';
|
||||
const url = forceOverride ? getApiUrl(`/servers/${encodeURIComponent(serverName)}`) : getApiUrl('/servers');
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
let result;
|
||||
if (forceOverride) {
|
||||
result = await apiPut(`/servers/${encodeURIComponent(serverName)}`, {
|
||||
name: serverName,
|
||||
config: serverConfig,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.message || `HTTP error! Status: ${response.status}`);
|
||||
});
|
||||
} else {
|
||||
result = await apiPost('/servers', {
|
||||
name: serverName,
|
||||
config: serverConfig,
|
||||
});
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
|
||||
@@ -56,8 +56,8 @@ const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => {
|
||||
servers: formData.servers
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
setError(t('groups.updateError'))
|
||||
if (!result || !result.success) {
|
||||
setError(result?.message || t('groups.updateError'))
|
||||
setIsSubmitting(false)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Server } from '@/types'
|
||||
import { getApiUrl } from '../utils/runtime'
|
||||
import { apiPut } from '../utils/fetchInterceptor'
|
||||
import ServerForm from './ServerForm'
|
||||
|
||||
interface EditServerFormProps {
|
||||
@@ -17,26 +17,12 @@ const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => {
|
||||
const handleSubmit = async (payload: any) => {
|
||||
try {
|
||||
setError(null)
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl(`/servers/${server.name}`), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || ''
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
const result = await apiPut(`/servers/${server.name}`, payload)
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
if (!result.success) {
|
||||
// Use specific error message from the response if available
|
||||
if (result && result.message) {
|
||||
setError(result.message)
|
||||
} else if (response.status === 404) {
|
||||
setError(t('server.notFound', { serverName: server.name }))
|
||||
} else if (response.status === 400) {
|
||||
setError(t('server.invalidData'))
|
||||
} else {
|
||||
setError(t('server.updateError', { serverName: server.name }))
|
||||
}
|
||||
|
||||
92
frontend/src/components/MCPRouterApiKeyError.tsx
Normal file
92
frontend/src/components/MCPRouterApiKeyError.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const MCPRouterApiKeyError: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleConfigureSettings = () => {
|
||||
navigate('/settings');
|
||||
};
|
||||
|
||||
const handleGetApiKey = () => {
|
||||
window.open('https://mcprouter.co', '_blank', 'noopener,noreferrer');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-6 mb-6">
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0">
|
||||
<svg
|
||||
className="h-5 w-5 text-amber-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3 flex-1">
|
||||
<h3 className="text-sm font-medium text-amber-800">
|
||||
{t('cloud.apiKeyNotConfigured')}
|
||||
</h3>
|
||||
<div className="mt-2 text-sm text-amber-700">
|
||||
<p>{t('cloud.apiKeyNotConfiguredDescription')}</p>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={handleGetApiKey}
|
||||
className="inline-flex items-center px-3 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-1.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||
/>
|
||||
</svg>
|
||||
{t('cloud.getApiKey')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfigureSettings}
|
||||
className="inline-flex items-center px-3 py-2 text-sm font-medium text-amber-800 bg-amber-100 border border-amber-300 rounded-md hover:bg-amber-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-amber-500 transition-colors duration-200"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-1.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
{t('cloud.configureInSettings')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MCPRouterApiKeyError;
|
||||
@@ -10,6 +10,16 @@ interface MarketServerCardProps {
|
||||
const MarketServerCard: React.FC<MarketServerCardProps> = ({ server, onClick }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Get initials for avatar
|
||||
const getAuthorInitials = (name: string) => {
|
||||
return name
|
||||
.split(' ')
|
||||
.map(word => word.charAt(0))
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
};
|
||||
|
||||
// Intelligently calculate how many tags to display to ensure they fit in a single line
|
||||
const getTagsToDisplay = () => {
|
||||
if (!server.tags || server.tags.length === 0) {
|
||||
@@ -80,70 +90,89 @@ const MarketServerCard: React.FC<MarketServerCardProps> = ({ server, onClick })
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-white rounded-lg shadow-md p-5 hover:shadow-lg transition-all duration-200 cursor-pointer flex flex-col h-full page-card"
|
||||
className="bg-white border border-gray-200 rounded-xl p-4 hover:shadow-lg hover:border-blue-400 hover:-translate-y-1 transition-all duration-300 cursor-pointer group relative overflow-hidden h-full flex flex-col"
|
||||
onClick={() => onClick(server)}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900 line-clamp-1 mr-2">{server.display_name}</h3>
|
||||
{server.is_official && (
|
||||
<span className="text-xs font-medium px-2.5 py-0.5 rounded flex-shrink-0 label-primary">
|
||||
{t('market.official')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm mb-4 line-clamp-2 min-h-[40px]">{server.description}</p>
|
||||
{/* Background gradient overlay on hover */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-50/0 to-purple-50/0 group-hover:from-blue-50/30 group-hover:to-purple-50/30 transition-all duration-300 pointer-events-none" />
|
||||
|
||||
{/* Categories */}
|
||||
<div className="flex flex-wrap gap-1 mb-2 min-h-[28px]">
|
||||
{server.categories?.length > 0 ? (
|
||||
server.categories.map((category, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="bg-gray-100 text-gray-800 text-xs px-2 py-1.5 rounded whitespace-nowrap"
|
||||
>
|
||||
{category}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-gray-400 py-1">-</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Server Header */}
|
||||
<div className="relative z-10 flex-1 flex flex-col">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-bold text-gray-900 group-hover:text-blue-600 transition-colors duration-200 mb-1 line-clamp-1 mr-2">
|
||||
{server.display_name}
|
||||
</h3>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="relative mb-3 min-h-[28px] overflow-x-auto">
|
||||
{server.tags?.length > 0 ? (
|
||||
<div className="flex gap-1 items-center whitespace-nowrap">
|
||||
{tagsToShow.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="bg-green-50 text-green-700 text-xs px-2 py-1 rounded flex-shrink-0 label-secondary"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
{hasMore && (
|
||||
<span className="bg-gray-100 text-gray-600 text-xs px-1.5 py-1 rounded flex-shrink-0">
|
||||
+{moreCount} {t('market.moreTags')}
|
||||
{/* Author Section */}
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
<div className="w-6 h-6 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white text-xs font-semibold">
|
||||
{getAuthorInitials(server.author?.name || t('market.unknown'))}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-medium text-gray-700">{server.author?.name || t('market.unknown')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Server Type Badge */}
|
||||
<div className="flex flex-col items-end space-y-2">
|
||||
{server.is_official && (
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
{t('market.official')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400 py-1">-</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center mt-auto pt-2 text-xs text-gray-500">
|
||||
<div className="overflow-hidden">
|
||||
<span className="whitespace-nowrap">{t('market.by')} </span>
|
||||
<span className="font-medium whitespace-nowrap overflow-hidden text-ellipsis max-w-[120px] inline-block align-bottom">
|
||||
{server.author?.name || t('market.unknown')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center flex-shrink-0">
|
||||
<svg className="h-4 w-4 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span>{server.tools?.length || 0} {t('market.tools')}</span>
|
||||
|
||||
{/* Description */}
|
||||
<div className="mb-2 flex-1">
|
||||
<p className="text-gray-600 text-sm leading-relaxed line-clamp-2 min-h-[36px]">
|
||||
{server.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="mb-2">
|
||||
<div className="flex flex-wrap gap-1 min-h-[24px]">
|
||||
{server.categories?.length > 0 ? (
|
||||
server.categories.map((category, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="bg-gray-100 text-gray-800 text-xs px-2 py-1 rounded whitespace-nowrap"
|
||||
>
|
||||
{category}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-gray-400 py-1">-</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="mb-2">
|
||||
<div className="relative min-h-[24px] overflow-x-auto">
|
||||
{server.tags?.length > 0 ? (
|
||||
<div className="flex gap-1 items-center whitespace-nowrap">
|
||||
{tagsToShow.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="bg-green-50 text-green-700 text-xs px-2 py-1 rounded flex-shrink-0"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
{hasMore && (
|
||||
<span className="bg-gray-100 text-gray-600 text-xs px-1.5 py-1 rounded flex-shrink-0">
|
||||
+{moreCount} {t('market.moreTags')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400 py-1">-</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -297,7 +297,7 @@ const DynamicForm: React.FC<DynamicFormProps> = ({ schema, onSubmit, onCancel, l
|
||||
<input
|
||||
type="number"
|
||||
step={schema.type === 'integer' ? '1' : 'any'}
|
||||
value={value || ''}
|
||||
value={value ?? ''}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value === '' ? '' : schema.type === 'integer' ? parseInt(e.target.value) : parseFloat(e.target.value);
|
||||
onChange(val);
|
||||
@@ -542,7 +542,7 @@ const DynamicForm: React.FC<DynamicFormProps> = ({ schema, onSubmit, onCancel, l
|
||||
<input
|
||||
type="number"
|
||||
step={propSchema.type === 'integer' ? '1' : 'any'}
|
||||
value={value || ''}
|
||||
value={value !== undefined && value !== null ? value : ''}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value === '' ? '' : propSchema.type === 'integer' ? parseInt(e.target.value) : parseFloat(e.target.value);
|
||||
handleInputChange(fullPath, val);
|
||||
|
||||
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;
|
||||
@@ -14,6 +14,15 @@ interface ToolCardProps {
|
||||
onDescriptionUpdate?: (toolName: string, description: string) => void
|
||||
}
|
||||
|
||||
// Helper to check for "empty" values
|
||||
function isEmptyValue(value: any): boolean {
|
||||
if (value == null) return true; // null or undefined
|
||||
if (typeof value === 'string') return value.trim() === '';
|
||||
if (Array.isArray(value)) return value.length === 0;
|
||||
if (typeof value === 'object') return Object.keys(value).length === 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
const ToolCard = ({ tool, server, onToggle, onDescriptionUpdate }: ToolCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
@@ -100,6 +109,8 @@ const ToolCard = ({ tool, server, onToggle, onDescriptionUpdate }: ToolCardProps
|
||||
const handleRunTool = async (arguments_: Record<string, any>) => {
|
||||
setIsRunning(true)
|
||||
try {
|
||||
// filter empty values
|
||||
arguments_ = Object.fromEntries(Object.entries(arguments_).filter(([_, v]) => !isEmptyValue(v)))
|
||||
const result = await callTool({
|
||||
toolName: tool.name,
|
||||
arguments: arguments_,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { AuthState } from '../types';
|
||||
import * as authService from '../services/authService';
|
||||
import { shouldSkipAuth } from '../services/configService';
|
||||
import { getPublicConfig } from '../services/configService';
|
||||
|
||||
// Initial auth state
|
||||
const initialState: AuthState = {
|
||||
@@ -32,7 +32,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
useEffect(() => {
|
||||
const loadUser = async () => {
|
||||
// First check if authentication should be skipped
|
||||
const skipAuth = await shouldSkipAuth();
|
||||
const { skipAuth, permissions } = await getPublicConfig();
|
||||
|
||||
if (skipAuth) {
|
||||
// If authentication is disabled, set user as authenticated with a dummy user
|
||||
@@ -42,6 +42,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
user: {
|
||||
username: 'guest',
|
||||
isAdmin: true,
|
||||
permissions,
|
||||
},
|
||||
error: null,
|
||||
});
|
||||
|
||||
350
frontend/src/hooks/useCloudData.ts
Normal file
350
frontend/src/hooks/useCloudData.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CloudServer, ApiResponse, CloudServerTool } from '@/types';
|
||||
import { apiGet, apiPost } from '../utils/fetchInterceptor';
|
||||
|
||||
export const useCloudData = () => {
|
||||
const { t } = useTranslation();
|
||||
const [servers, setServers] = useState<CloudServer[]>([]);
|
||||
const [allServers, setAllServers] = useState<CloudServer[]>([]);
|
||||
const [categories, setCategories] = useState<string[]>([]);
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('');
|
||||
const [selectedTag, setSelectedTag] = useState<string>('');
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [currentServer, setCurrentServer] = useState<CloudServer | null>(null);
|
||||
|
||||
// Pagination states
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [serversPerPage, setServersPerPage] = useState(9);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
|
||||
// Fetch all cloud market servers
|
||||
const fetchCloudServers = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data: ApiResponse<CloudServer[]> = await apiGet('/cloud/servers');
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setAllServers(data.data);
|
||||
// Apply pagination to the fetched data
|
||||
applyPagination(data.data, currentPage);
|
||||
} else {
|
||||
console.error('Invalid cloud market servers data format:', data);
|
||||
setError(t('cloud.fetchError'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching cloud market servers:', err);
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
// Keep the original error message for API key errors
|
||||
if (
|
||||
errorMessage === 'MCPROUTER_API_KEY_NOT_CONFIGURED' ||
|
||||
errorMessage.toLowerCase().includes('mcprouter api key not configured')
|
||||
) {
|
||||
setError(errorMessage);
|
||||
} else {
|
||||
setError(errorMessage);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
// Apply pagination to data
|
||||
const applyPagination = useCallback(
|
||||
(data: CloudServer[], page: number, itemsPerPage = serversPerPage) => {
|
||||
const totalItems = data.length;
|
||||
const calculatedTotalPages = Math.ceil(totalItems / itemsPerPage);
|
||||
setTotalPages(calculatedTotalPages);
|
||||
|
||||
// Ensure current page is valid
|
||||
const validPage = Math.max(1, Math.min(page, calculatedTotalPages));
|
||||
if (validPage !== page) {
|
||||
setCurrentPage(validPage);
|
||||
}
|
||||
|
||||
const startIndex = (validPage - 1) * itemsPerPage;
|
||||
const paginatedServers = data.slice(startIndex, startIndex + itemsPerPage);
|
||||
setServers(paginatedServers);
|
||||
},
|
||||
[serversPerPage],
|
||||
);
|
||||
|
||||
// Change page
|
||||
const changePage = useCallback(
|
||||
(page: number) => {
|
||||
setCurrentPage(page);
|
||||
applyPagination(allServers, page, serversPerPage);
|
||||
},
|
||||
[allServers, applyPagination, serversPerPage],
|
||||
);
|
||||
|
||||
// Fetch all categories
|
||||
const fetchCategories = useCallback(async () => {
|
||||
try {
|
||||
const data: ApiResponse<string[]> = await apiGet('/cloud/categories');
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setCategories(data.data);
|
||||
} else {
|
||||
console.error('Invalid cloud market categories data format:', data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching cloud market categories:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Fetch all tags
|
||||
const fetchTags = useCallback(async () => {
|
||||
try {
|
||||
const data: ApiResponse<string[]> = await apiGet('/cloud/tags');
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setTags(data.data);
|
||||
} else {
|
||||
console.error('Invalid cloud market tags data format:', data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching cloud market tags:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Fetch server by name
|
||||
const fetchServerByName = useCallback(
|
||||
async (name: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data: ApiResponse<CloudServer> = await apiGet(`/cloud/servers/${name}`);
|
||||
|
||||
if (data && data.success && data.data) {
|
||||
setCurrentServer(data.data);
|
||||
return data.data;
|
||||
} else {
|
||||
console.error('Invalid cloud server data format:', data);
|
||||
setError(t('cloud.serverNotFound'));
|
||||
return null;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error fetching cloud server ${name}:`, err);
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
// Keep the original error message for API key errors
|
||||
if (
|
||||
errorMessage === 'MCPROUTER_API_KEY_NOT_CONFIGURED' ||
|
||||
errorMessage.toLowerCase().includes('mcprouter api key not configured')
|
||||
) {
|
||||
setError(errorMessage);
|
||||
} else {
|
||||
setError(errorMessage);
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
// Search servers by query
|
||||
const searchServers = useCallback(
|
||||
async (query: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setSearchQuery(query);
|
||||
|
||||
if (!query.trim()) {
|
||||
// Fetch fresh data from server instead of just applying pagination
|
||||
fetchCloudServers();
|
||||
return;
|
||||
}
|
||||
|
||||
const data: ApiResponse<CloudServer[]> = await apiGet(
|
||||
`/cloud/servers/search?query=${encodeURIComponent(query)}`,
|
||||
);
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setAllServers(data.data);
|
||||
setCurrentPage(1);
|
||||
applyPagination(data.data, 1);
|
||||
} else {
|
||||
console.error('Invalid cloud search results format:', data);
|
||||
setError(t('cloud.searchError'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error searching cloud servers:', err);
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[t, allServers, applyPagination, fetchCloudServers],
|
||||
);
|
||||
|
||||
// Filter servers by category
|
||||
const filterByCategory = useCallback(
|
||||
async (category: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setSelectedCategory(category);
|
||||
setSelectedTag(''); // Reset tag filter when filtering by category
|
||||
|
||||
if (!category) {
|
||||
fetchCloudServers();
|
||||
return;
|
||||
}
|
||||
|
||||
const data: ApiResponse<CloudServer[]> = await apiGet(
|
||||
`/cloud/categories/${encodeURIComponent(category)}`,
|
||||
);
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setAllServers(data.data);
|
||||
setCurrentPage(1);
|
||||
applyPagination(data.data, 1);
|
||||
} else {
|
||||
console.error('Invalid cloud category filter results format:', data);
|
||||
setError(t('cloud.filterError'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error filtering cloud servers by category:', err);
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[t, fetchCloudServers, applyPagination],
|
||||
);
|
||||
|
||||
// Filter servers by tag
|
||||
const filterByTag = useCallback(
|
||||
async (tag: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setSelectedTag(tag);
|
||||
setSelectedCategory(''); // Reset category filter when filtering by tag
|
||||
|
||||
if (!tag) {
|
||||
fetchCloudServers();
|
||||
return;
|
||||
}
|
||||
|
||||
const data: ApiResponse<CloudServer[]> = await apiGet(
|
||||
`/cloud/tags/${encodeURIComponent(tag)}`,
|
||||
);
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setAllServers(data.data);
|
||||
setCurrentPage(1);
|
||||
applyPagination(data.data, 1);
|
||||
} else {
|
||||
console.error('Invalid cloud tag filter results format:', data);
|
||||
setError(t('cloud.tagFilterError'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error filtering cloud servers by tag:', err);
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[t, fetchCloudServers, applyPagination],
|
||||
);
|
||||
|
||||
// Fetch tools for a specific server
|
||||
const fetchServerTools = useCallback(async (serverName: string) => {
|
||||
try {
|
||||
const data: ApiResponse<CloudServerTool[]> = await apiGet(
|
||||
`/cloud/servers/${serverName}/tools`,
|
||||
);
|
||||
|
||||
if (!data.success) {
|
||||
console.error('Failed to fetch cloud server tools:', data);
|
||||
throw new Error(data.message || 'Failed to fetch cloud server tools');
|
||||
}
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
return data.data;
|
||||
} else {
|
||||
console.error('Invalid cloud server tools data format:', data);
|
||||
return [];
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error fetching tools for cloud server ${serverName}:`, err);
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
// Re-throw API key errors so they can be handled by the component
|
||||
if (
|
||||
errorMessage === 'MCPROUTER_API_KEY_NOT_CONFIGURED' ||
|
||||
errorMessage.toLowerCase().includes('mcprouter api key not configured')
|
||||
) {
|
||||
throw err;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Call a tool on a cloud server
|
||||
const callServerTool = useCallback(
|
||||
async (serverName: string, toolName: string, args: Record<string, any>) => {
|
||||
try {
|
||||
const data = await apiPost(`/cloud/servers/${serverName}/tools/${toolName}/call`, {
|
||||
arguments: args,
|
||||
});
|
||||
|
||||
if (data && data.success) {
|
||||
return data.data;
|
||||
} else {
|
||||
throw new Error(data.message || 'Failed to call tool');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error calling tool ${toolName} on cloud server ${serverName}:`, err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Change servers per page
|
||||
const changeServersPerPage = useCallback(
|
||||
(perPage: number) => {
|
||||
setServersPerPage(perPage);
|
||||
setCurrentPage(1);
|
||||
applyPagination(allServers, 1, perPage);
|
||||
},
|
||||
[allServers, applyPagination],
|
||||
);
|
||||
|
||||
// Load initial data
|
||||
useEffect(() => {
|
||||
fetchCloudServers();
|
||||
fetchCategories();
|
||||
fetchTags();
|
||||
}, [fetchCloudServers, fetchCategories, fetchTags]);
|
||||
|
||||
return {
|
||||
servers,
|
||||
allServers,
|
||||
categories,
|
||||
tags,
|
||||
selectedCategory,
|
||||
selectedTag,
|
||||
searchQuery,
|
||||
loading,
|
||||
error,
|
||||
setError,
|
||||
currentServer,
|
||||
fetchCloudServers: fetchCloudServers,
|
||||
fetchServerByName,
|
||||
searchServers,
|
||||
filterByCategory,
|
||||
filterByTag,
|
||||
fetchServerTools,
|
||||
callServerTool,
|
||||
// Pagination properties and methods
|
||||
currentPage,
|
||||
totalPages,
|
||||
serversPerPage,
|
||||
changePage,
|
||||
changeServersPerPage,
|
||||
};
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Group, ApiResponse, IGroupServerConfig } from '@/types';
|
||||
import { getApiUrl } from '../utils/runtime';
|
||||
import { apiGet, apiPost, apiPut, apiDelete } from '../utils/fetchInterceptor';
|
||||
|
||||
export const useGroupData = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -13,18 +13,7 @@ export const useGroupData = () => {
|
||||
const fetchGroups = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/groups'), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: ApiResponse<Group[]> = await response.json();
|
||||
const data: ApiResponse<Group[]> = await apiGet('/groups');
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setGroups(data.data);
|
||||
@@ -55,25 +44,16 @@ export const useGroupData = () => {
|
||||
servers: string[] | IGroupServerConfig[] = [],
|
||||
) => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/groups'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: JSON.stringify({ name, description, servers }),
|
||||
});
|
||||
const result: ApiResponse<Group> = await apiPost('/groups', { name, description, servers });
|
||||
console.log('Group created successfully:', result);
|
||||
|
||||
const result: ApiResponse<Group> = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setError(result.message || t('groups.createError'));
|
||||
return null;
|
||||
if (!result || !result.success) {
|
||||
setError(result?.message || t('groups.createError'));
|
||||
return result;
|
||||
}
|
||||
|
||||
triggerRefresh();
|
||||
return result.data || null;
|
||||
return result || null;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to create group');
|
||||
return null;
|
||||
@@ -86,25 +66,14 @@ export const useGroupData = () => {
|
||||
data: { name?: string; description?: string; servers?: string[] | IGroupServerConfig[] },
|
||||
) => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl(`/groups/${id}`), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
const result: ApiResponse<Group> = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setError(result.message || t('groups.updateError'));
|
||||
return null;
|
||||
const result: ApiResponse<Group> = await apiPut(`/groups/${id}`, data);
|
||||
if (!result || !result.success) {
|
||||
setError(result?.message || t('groups.updateError'));
|
||||
return result;
|
||||
}
|
||||
|
||||
triggerRefresh();
|
||||
return result.data || null;
|
||||
return result || null;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to update group');
|
||||
return null;
|
||||
@@ -114,20 +83,12 @@ export const useGroupData = () => {
|
||||
// Update servers in a group (for batch updates)
|
||||
const updateGroupServers = async (groupId: string, servers: string[] | IGroupServerConfig[]) => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl(`/groups/${groupId}/servers/batch`), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: JSON.stringify({ servers }),
|
||||
const result: ApiResponse<Group> = await apiPut(`/groups/${groupId}/servers/batch`, {
|
||||
servers,
|
||||
});
|
||||
|
||||
const result: ApiResponse<Group> = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setError(result.message || t('groups.updateError'));
|
||||
if (!result || !result.success) {
|
||||
setError(result?.message || t('groups.updateError'));
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -142,46 +103,29 @@ export const useGroupData = () => {
|
||||
// Delete a group
|
||||
const deleteGroup = async (id: string) => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl(`/groups/${id}`), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setError(result.message || t('groups.deleteError'));
|
||||
return false;
|
||||
const result = await apiDelete(`/groups/${id}`);
|
||||
if (!result || !result.success) {
|
||||
setError(result?.message || t('groups.deleteError'));
|
||||
return result;
|
||||
}
|
||||
|
||||
triggerRefresh();
|
||||
return true;
|
||||
return result;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to delete group');
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Add server to a group
|
||||
const addServerToGroup = async (groupId: string, serverName: string) => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl(`/groups/${groupId}/servers`), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: JSON.stringify({ serverName }),
|
||||
const result: ApiResponse<Group> = await apiPost(`/groups/${groupId}/servers`, {
|
||||
serverName,
|
||||
});
|
||||
|
||||
const result: ApiResponse<Group> = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setError(result.message || t('groups.serverAddError'));
|
||||
if (!result || !result.success) {
|
||||
setError(result?.message || t('groups.serverAddError'));
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -196,18 +140,12 @@ export const useGroupData = () => {
|
||||
// Remove server from group
|
||||
const removeServerFromGroup = async (groupId: string, serverName: string) => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl(`/groups/${groupId}/servers/${serverName}`), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
const result: ApiResponse<Group> = await apiDelete(
|
||||
`/groups/${groupId}/servers/${serverName}`,
|
||||
);
|
||||
|
||||
const result: ApiResponse<Group> = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setError(result.message || t('groups.serverRemoveError'));
|
||||
if (!result || !result.success) {
|
||||
setError(result?.message || t('groups.serverRemoveError'));
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MarketServer, ApiResponse, ServerConfig } from '@/types';
|
||||
import { getApiUrl } from '../utils/runtime';
|
||||
import { apiGet, apiPost } from '../utils/fetchInterceptor';
|
||||
|
||||
export const useMarketData = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -26,18 +26,7 @@ export const useMarketData = () => {
|
||||
const fetchMarketServers = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/market/servers'), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: ApiResponse<MarketServer[]> = await response.json();
|
||||
const data: ApiResponse<MarketServer[]> = await apiGet('/market/servers');
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setAllServers(data.data);
|
||||
@@ -87,18 +76,7 @@ export const useMarketData = () => {
|
||||
// Fetch all categories
|
||||
const fetchCategories = useCallback(async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/market/categories'), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: ApiResponse<string[]> = await response.json();
|
||||
const data: ApiResponse<string[]> = await apiGet('/market/categories');
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setCategories(data.data);
|
||||
@@ -113,18 +91,7 @@ export const useMarketData = () => {
|
||||
// Fetch all tags
|
||||
const fetchTags = useCallback(async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/market/tags'), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: ApiResponse<string[]> = await response.json();
|
||||
const data: ApiResponse<string[]> = await apiGet('/market/tags');
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setTags(data.data);
|
||||
@@ -141,18 +108,7 @@ export const useMarketData = () => {
|
||||
async (name: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl(`/market/servers/${name}`), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: ApiResponse<MarketServer> = await response.json();
|
||||
const data: ApiResponse<MarketServer> = await apiGet(`/market/servers/${name}`);
|
||||
|
||||
if (data && data.success && data.data) {
|
||||
setCurrentServer(data.data);
|
||||
@@ -186,22 +142,10 @@ export const useMarketData = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(
|
||||
getApiUrl(`/market/servers/search?query=${encodeURIComponent(query)}`),
|
||||
{
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
},
|
||||
const data: ApiResponse<MarketServer[]> = await apiGet(
|
||||
`/market/servers/search?query=${encodeURIComponent(query)}`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: ApiResponse<MarketServer[]> = await response.json();
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setAllServers(data.data);
|
||||
setCurrentPage(1);
|
||||
@@ -233,22 +177,10 @@ export const useMarketData = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(
|
||||
getApiUrl(`/market/categories/${encodeURIComponent(category)}`),
|
||||
{
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
},
|
||||
const data: ApiResponse<MarketServer[]> = await apiGet(
|
||||
`/market/categories/${encodeURIComponent(category)}`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: ApiResponse<MarketServer[]> = await response.json();
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setAllServers(data.data);
|
||||
setCurrentPage(1);
|
||||
@@ -280,18 +212,9 @@ export const useMarketData = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl(`/market/tags/${encodeURIComponent(tag)}`), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: ApiResponse<MarketServer[]> = await response.json();
|
||||
const data: ApiResponse<MarketServer[]> = await apiGet(
|
||||
`/market/tags/${encodeURIComponent(tag)}`,
|
||||
);
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setAllServers(data.data);
|
||||
@@ -314,18 +237,7 @@ export const useMarketData = () => {
|
||||
// Fetch installed servers
|
||||
const fetchInstalledServers = useCallback(async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/servers'), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const data = await apiGet<{ success: boolean; data: any[] }>('/servers');
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
// Extract server names
|
||||
@@ -365,27 +277,24 @@ export const useMarketData = () => {
|
||||
// Prepare server configuration, merging with customConfig
|
||||
const serverConfig = {
|
||||
name: server.name,
|
||||
config: customConfig.type === 'stdio' ? {
|
||||
command: customConfig.command || installation.command || '',
|
||||
args: customConfig.args || installation.args || [],
|
||||
env: { ...installation.env, ...customConfig.env },
|
||||
} : customConfig
|
||||
config:
|
||||
customConfig.type === 'stdio'
|
||||
? {
|
||||
command: customConfig.command || installation.command || '',
|
||||
args: customConfig.args || installation.args || [],
|
||||
env: { ...installation.env, ...customConfig.env },
|
||||
}
|
||||
: customConfig,
|
||||
};
|
||||
|
||||
// Call the createServer API
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/servers'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: JSON.stringify(serverConfig),
|
||||
});
|
||||
const result = await apiPost<{ success: boolean; message?: string }>(
|
||||
'/servers',
|
||||
serverConfig,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || `Status: ${response.status}`);
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || 'Failed to install server');
|
||||
}
|
||||
|
||||
// Update installed servers list after successful installation
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Server, ApiResponse } from '@/types';
|
||||
import { getApiUrl } from '../utils/runtime';
|
||||
import { apiGet, apiPost, apiDelete } from '../utils/fetchInterceptor';
|
||||
|
||||
// Configuration options
|
||||
const CONFIG = {
|
||||
@@ -44,13 +44,7 @@ export const useServerData = () => {
|
||||
|
||||
const fetchServers = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/servers'), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
const data = await apiGet('/servers');
|
||||
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setServers(data.data);
|
||||
@@ -97,13 +91,7 @@ export const useServerData = () => {
|
||||
// Initialization phase request function
|
||||
const fetchInitialData = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/servers'), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
const data = await apiGet('/servers');
|
||||
|
||||
// Handle API response wrapper object, extract data field
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
@@ -203,14 +191,8 @@ export const useServerData = () => {
|
||||
const handleServerEdit = async (server: Server) => {
|
||||
try {
|
||||
// Fetch settings to get the full server config before editing
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/settings'), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
const settingsData: ApiResponse<{ mcpServers: Record<string, any> }> = await response.json();
|
||||
const settingsData: ApiResponse<{ mcpServers: Record<string, any> }> =
|
||||
await apiGet('/settings');
|
||||
|
||||
if (
|
||||
settingsData &&
|
||||
@@ -240,17 +222,10 @@ export const useServerData = () => {
|
||||
|
||||
const handleServerRemove = async (serverName: string) => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl(`/servers/${serverName}`), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
const result = await response.json();
|
||||
const result = await apiDelete(`/servers/${serverName}`);
|
||||
|
||||
if (!response.ok) {
|
||||
setError(result.message || t('server.deleteError', { serverName }));
|
||||
if (!result || !result.success) {
|
||||
setError(result?.message || t('server.deleteError', { serverName }));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -264,21 +239,11 @@ export const useServerData = () => {
|
||||
|
||||
const handleServerToggle = async (server: Server, enabled: boolean) => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl(`/servers/${server.name}/toggle`), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: JSON.stringify({ enabled }),
|
||||
});
|
||||
const result = await apiPost(`/servers/${server.name}/toggle`, { enabled });
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
if (!result || !result.success) {
|
||||
console.error('Failed to toggle server:', result);
|
||||
setError(t('server.toggleError', { serverName: server.name }));
|
||||
setError(result?.message || t('server.toggleError', { serverName: server.name }));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ApiResponse } from '@/types';
|
||||
import { useToast } from '@/contexts/ToastContext';
|
||||
import { getApiUrl } from '../utils/runtime';
|
||||
import { apiGet, apiPut } from '../utils/fetchInterceptor';
|
||||
|
||||
// Define types for the settings data
|
||||
interface RoutingConfig {
|
||||
@@ -27,11 +27,19 @@ interface SmartRoutingConfig {
|
||||
openaiApiEmbeddingModel: string;
|
||||
}
|
||||
|
||||
interface MCPRouterConfig {
|
||||
apiKey: string;
|
||||
referer: string;
|
||||
title: string;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
interface SystemSettings {
|
||||
systemConfig?: {
|
||||
routing?: RoutingConfig;
|
||||
install?: InstallConfig;
|
||||
smartRouting?: SmartRoutingConfig;
|
||||
mcpRouter?: MCPRouterConfig;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -69,6 +77,13 @@ export const useSettingsData = () => {
|
||||
openaiApiEmbeddingModel: '',
|
||||
});
|
||||
|
||||
const [mcpRouterConfig, setMCPRouterConfig] = useState<MCPRouterConfig>({
|
||||
apiKey: '',
|
||||
referer: 'https://mcphub.app',
|
||||
title: 'MCPHub',
|
||||
baseUrl: 'https://api.mcprouter.to/v1',
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
@@ -84,18 +99,7 @@ export const useSettingsData = () => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/settings'), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: ApiResponse<SystemSettings> = await response.json();
|
||||
const data: ApiResponse<SystemSettings> = await apiGet('/settings');
|
||||
|
||||
if (data.success && data.data?.systemConfig?.routing) {
|
||||
setRoutingConfig({
|
||||
@@ -123,6 +127,14 @@ export const useSettingsData = () => {
|
||||
data.data.systemConfig.smartRouting.openaiApiEmbeddingModel || '',
|
||||
});
|
||||
}
|
||||
if (data.success && data.data?.systemConfig?.mcpRouter) {
|
||||
setMCPRouterConfig({
|
||||
apiKey: data.data.systemConfig.mcpRouter.apiKey || '',
|
||||
referer: data.data.systemConfig.mcpRouter.referer || 'https://mcphub.app',
|
||||
title: data.data.systemConfig.mcpRouter.title || 'MCPHub',
|
||||
baseUrl: data.data.systemConfig.mcpRouter.baseUrl || 'https://api.mcprouter.to/v1',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch settings:', error);
|
||||
setError(error instanceof Error ? error.message : 'Failed to fetch settings');
|
||||
@@ -134,34 +146,17 @@ export const useSettingsData = () => {
|
||||
}, [t]); // 移除 showToast 依赖
|
||||
|
||||
// Update routing configuration
|
||||
const updateRoutingConfig = async <T extends keyof RoutingConfig>(
|
||||
key: T,
|
||||
value: RoutingConfig[T],
|
||||
) => {
|
||||
const updateRoutingConfig = async (key: keyof RoutingConfig, value: any) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/system-config'), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
const data = await apiPut('/system-config', {
|
||||
routing: {
|
||||
[key]: value,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
routing: {
|
||||
[key]: value,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setRoutingConfig({
|
||||
...routingConfig,
|
||||
@@ -170,7 +165,7 @@ export const useSettingsData = () => {
|
||||
showToast(t('settings.systemConfigUpdated'));
|
||||
return true;
|
||||
} else {
|
||||
showToast(t('errors.failedToUpdateRouteConfig'));
|
||||
showToast(data.message || t('errors.failedToUpdateRouteConfig'));
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -189,26 +184,12 @@ export const useSettingsData = () => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/system-config'), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
const data = await apiPut('/system-config', {
|
||||
install: {
|
||||
[key]: value,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
install: {
|
||||
[key]: value,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setInstallConfig({
|
||||
...installConfig,
|
||||
@@ -217,7 +198,7 @@ export const useSettingsData = () => {
|
||||
showToast(t('settings.systemConfigUpdated'));
|
||||
return true;
|
||||
} else {
|
||||
showToast(t('errors.failedToUpdateSystemConfig'));
|
||||
showToast(data.message || t('errors.failedToUpdateSystemConfig'));
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -239,27 +220,12 @@ export const useSettingsData = () => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/system-config'), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
const data = await apiPut('/system-config', {
|
||||
smartRouting: {
|
||||
[key]: value,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
smartRouting: {
|
||||
[key]: value,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || `HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setSmartRoutingConfig({
|
||||
...smartRoutingConfig,
|
||||
@@ -289,25 +255,10 @@ export const useSettingsData = () => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/system-config'), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
smartRouting: updates,
|
||||
}),
|
||||
const data = await apiPut('/system-config', {
|
||||
smartRouting: updates,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || `HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setSmartRoutingConfig({
|
||||
...smartRoutingConfig,
|
||||
@@ -337,24 +288,10 @@ export const useSettingsData = () => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/system-config'), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
routing: updates,
|
||||
}),
|
||||
const data = await apiPut('/system-config', {
|
||||
routing: updates,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setRoutingConfig({
|
||||
...routingConfig,
|
||||
@@ -363,7 +300,7 @@ export const useSettingsData = () => {
|
||||
showToast(t('settings.systemConfigUpdated'));
|
||||
return true;
|
||||
} else {
|
||||
showToast(t('errors.failedToUpdateRouteConfig'));
|
||||
showToast(data.message || t('errors.failedToUpdateRouteConfig'));
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -376,6 +313,77 @@ export const useSettingsData = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Update MCPRouter configuration
|
||||
const updateMCPRouterConfig = async <T extends keyof MCPRouterConfig>(
|
||||
key: T,
|
||||
value: MCPRouterConfig[T],
|
||||
) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const data = await apiPut('/system-config', {
|
||||
mcpRouter: {
|
||||
[key]: value,
|
||||
},
|
||||
});
|
||||
|
||||
if (data.success) {
|
||||
setMCPRouterConfig({
|
||||
...mcpRouterConfig,
|
||||
[key]: value,
|
||||
});
|
||||
showToast(t('settings.systemConfigUpdated'));
|
||||
return true;
|
||||
} else {
|
||||
showToast(data.message || t('errors.failedToUpdateSystemConfig'));
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update MCPRouter config:', error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to update MCPRouter config';
|
||||
setError(errorMessage);
|
||||
showToast(errorMessage);
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Update multiple MCPRouter configuration fields at once
|
||||
const updateMCPRouterConfigBatch = async (updates: Partial<MCPRouterConfig>) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const data = await apiPut('/system-config', {
|
||||
mcpRouter: updates,
|
||||
});
|
||||
|
||||
if (data.success) {
|
||||
setMCPRouterConfig({
|
||||
...mcpRouterConfig,
|
||||
...updates,
|
||||
});
|
||||
showToast(t('settings.systemConfigUpdated'));
|
||||
return true;
|
||||
} else {
|
||||
showToast(data.message || t('errors.failedToUpdateSystemConfig'));
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update MCPRouter config:', error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to update MCPRouter config';
|
||||
setError(errorMessage);
|
||||
showToast(errorMessage);
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch settings when the component mounts or refreshKey changes
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
@@ -395,6 +403,7 @@ export const useSettingsData = () => {
|
||||
setTempRoutingConfig,
|
||||
installConfig,
|
||||
smartRoutingConfig,
|
||||
mcpRouterConfig,
|
||||
loading,
|
||||
error,
|
||||
setError,
|
||||
@@ -405,5 +414,7 @@ export const useSettingsData = () => {
|
||||
updateSmartRoutingConfig,
|
||||
updateSmartRoutingConfigBatch,
|
||||
updateRoutingConfigBatch,
|
||||
updateMCPRouterConfig,
|
||||
updateMCPRouterConfigBatch,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,9 +2,9 @@ import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
// Import translations
|
||||
import enTranslation from './locales/en.json';
|
||||
import zhTranslation from './locales/zh.json';
|
||||
// Import shared translations from root locales directory
|
||||
import enTranslation from '../../locales/en.json';
|
||||
import zhTranslation from '../../locales/zh.json';
|
||||
|
||||
i18n
|
||||
// Detect user language
|
||||
@@ -15,18 +15,18 @@ i18n
|
||||
.init({
|
||||
resources: {
|
||||
en: {
|
||||
translation: enTranslation
|
||||
translation: enTranslation,
|
||||
},
|
||||
zh: {
|
||||
translation: zhTranslation
|
||||
}
|
||||
translation: zhTranslation,
|
||||
},
|
||||
},
|
||||
fallbackLng: 'en',
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
|
||||
|
||||
// Common namespace used for all translations
|
||||
defaultNS: 'translation',
|
||||
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false, // React already safe from XSS
|
||||
},
|
||||
@@ -36,7 +36,7 @@ i18n
|
||||
order: ['localStorage', 'cookie', 'htmlTag', 'navigator'],
|
||||
// Cache the language in localStorage
|
||||
caches: ['localStorage', 'cookie'],
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
export default i18n;
|
||||
|
||||
@@ -442,6 +442,30 @@ tbody tr:hover {
|
||||
color: rgba(239, 154, 154, 0.9) !important;
|
||||
}
|
||||
|
||||
/* External link styles */
|
||||
.external-link {
|
||||
color: #2563eb !important; /* Blue-600 for light mode */
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: all 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.external-link:hover {
|
||||
color: #1d4ed8 !important; /* Blue-700 for light mode */
|
||||
border-bottom-color: #1d4ed8;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.dark .external-link {
|
||||
color: #60a5fa !important; /* Blue-400 for dark mode */
|
||||
}
|
||||
|
||||
.dark .external-link:hover {
|
||||
color: #93c5fd !important; /* Blue-300 for dark mode */
|
||||
border-bottom-color: #93c5fd;
|
||||
}
|
||||
|
||||
.border-red {
|
||||
border-color: #937d7d; /* Tailwind red-800 for light mode */
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import App from './App';
|
||||
import './index.css';
|
||||
// Import the i18n configuration
|
||||
import './i18n';
|
||||
// Setup fetch interceptors
|
||||
import './utils/setupInterceptors';
|
||||
import { loadRuntimeConfig } from './utils/runtime';
|
||||
|
||||
// Load runtime configuration before starting the app
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -32,9 +32,9 @@ const GroupsPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleDeleteGroup = async (groupId: string) => {
|
||||
const success = await deleteGroup(groupId);
|
||||
if (!success) {
|
||||
setGroupError(t('groups.deleteError'));
|
||||
const result = await deleteGroup(groupId);
|
||||
if (!result || !result.success) {
|
||||
setGroupError(result?.message || t('groups.deleteError'));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -41,67 +41,102 @@ const LoginPage: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8 login-container">
|
||||
<div className="absolute top-4 right-4 w-full max-w-xs flex justify-end">
|
||||
<div className="relative min-h-screen w-full overflow-hidden bg-gray-50 dark:bg-gray-950">
|
||||
{/* Top-right controls */}
|
||||
<div className="absolute top-4 right-4 z-20 flex items-center gap-2">
|
||||
<ThemeSwitch />
|
||||
<LanguageSwitch />
|
||||
</div>
|
||||
<div className="max-w-md w-full space-y-8 login-card p-8">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-white">
|
||||
{t('auth.loginTitle')}
|
||||
</h2>
|
||||
|
||||
{/* Tech background layer */}
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 -z-10 opacity-60 dark:opacity-70"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'radial-gradient(60rem 60rem at 20% -10%, rgba(99,102,241,0.25), transparent), radial-gradient(50rem 50rem at 120% 10%, rgba(168,85,247,0.15), transparent)',
|
||||
}}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0 -z-10">
|
||||
<svg className="h-full w-full opacity-[0.08] dark:opacity-[0.12]" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="grid" width="32" height="32" patternUnits="userSpaceOnUse">
|
||||
<path d="M 32 0 L 0 0 0 32" fill="none" stroke="currentColor" strokeWidth="0.5" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#grid)" className="text-gray-400 dark:text-gray-300" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="relative mx-auto flex min-h-screen w-full max-w-md items-center justify-center px-6 py-16">
|
||||
<div className="w-full space-y-6">
|
||||
{/* Centered slogan */}
|
||||
<div className="flex justify-center w-full">
|
||||
<h1 className="text-3xl font-extrabold leading-tight tracking-tight text-gray-900 dark:text-white sm:text-4xl whitespace-nowrap">
|
||||
<span className="bg-gradient-to-r from-indigo-400 via-cyan-400 to-emerald-400 bg-clip-text text-transparent">
|
||||
{t('auth.slogan')}
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Centered login card */}
|
||||
<div className="login-card relative w-full rounded-2xl border border-white/10 bg-white/60 p-8 shadow-xl backdrop-blur-md transition dark:border-white/10 dark:bg-gray-900/60">
|
||||
<div className="absolute -top-24 right-12 h-40 w-40 -translate-y-6 rounded-full bg-indigo-500/30 blur-3xl" />
|
||||
<div className="absolute -bottom-24 -left-12 h-40 w-40 translate-y-6 rounded-full bg-cyan-500/20 blur-3xl" />
|
||||
<form className="mt-4 space-y-4" onSubmit={handleSubmit}>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="username" className="sr-only">
|
||||
{t('auth.username')}
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
required
|
||||
className="login-input appearance-none relative block w-full rounded-md border border-gray-300/60 bg-white/70 px-3 py-3 text-gray-900 shadow-sm outline-none ring-0 transition-all placeholder:text-gray-500 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 dark:border-gray-700/60 dark:bg-gray-800/70 dark:text-white dark:placeholder:text-gray-400"
|
||||
placeholder={t('auth.username')}
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password" className="sr-only">
|
||||
{t('auth.password')}
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
className="login-input appearance-none relative block w-full rounded-md border border-gray-300/60 bg-white/70 px-3 py-3 text-gray-900 shadow-sm outline-none ring-0 transition-all placeholder:text-gray-500 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 dark:border-gray-700/60 dark:bg-gray-800/70 dark:text-white dark:placeholder:text-gray-400"
|
||||
placeholder={t('auth.password')}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="error-box rounded border border-red-500/20 bg-red-500/10 p-2 text-center text-sm text-red-600 dark:text-red-400">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="login-button btn-primary group relative flex w-full items-center justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-all hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-70"
|
||||
>
|
||||
{loading ? t('auth.loggingIn') : t('auth.login')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="username" className="sr-only">
|
||||
{t('auth.username')}
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
required
|
||||
className="appearance-none relative block w-full px-3 py-3 border border-gray-300 dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white dark:bg-gray-800 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm transition-all duration-200 form-input shadow-sm"
|
||||
placeholder={t('auth.username')}
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password" className="sr-only">
|
||||
{t('auth.password')}
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
className="appearance-none relative block w-full px-3 py-3 border border-gray-300 dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white dark:bg-gray-800 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm login-input transition-all duration-200 form-input shadow-sm"
|
||||
placeholder={t('auth.password')}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-red-500 dark:text-red-400 text-sm text-center error-box p-2 rounded">{error}</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 login-button transition-all duration-200 btn-primary"
|
||||
>
|
||||
{loading ? t('auth.loggingIn') : t('auth.login')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { MarketServer, ServerConfig } from '@/types';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { MarketServer, CloudServer, ServerConfig } from '@/types';
|
||||
import { useMarketData } from '@/hooks/useMarketData';
|
||||
import { useCloudData } from '@/hooks/useCloudData';
|
||||
import { useToast } from '@/contexts/ToastContext';
|
||||
import { apiPost } from '@/utils/fetchInterceptor';
|
||||
import MarketServerCard from '@/components/MarketServerCard';
|
||||
import MarketServerDetail from '@/components/MarketServerDetail';
|
||||
import CloudServerCard from '@/components/CloudServerCard';
|
||||
import CloudServerDetail from '@/components/CloudServerDetail';
|
||||
import MCPRouterApiKeyError from '@/components/MCPRouterApiKeyError';
|
||||
import Pagination from '@/components/ui/Pagination';
|
||||
|
||||
const MarketPage: React.FC = () => {
|
||||
@@ -14,82 +19,140 @@ const MarketPage: React.FC = () => {
|
||||
const { serverName } = useParams<{ serverName?: string }>();
|
||||
const { showToast } = useToast();
|
||||
|
||||
// Get tab from URL search params, default to cloud market
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const currentTab = searchParams.get('tab') || 'cloud';
|
||||
|
||||
// Local market data
|
||||
const {
|
||||
servers,
|
||||
allServers,
|
||||
categories,
|
||||
loading,
|
||||
error,
|
||||
setError,
|
||||
searchServers,
|
||||
filterByCategory,
|
||||
filterByTag,
|
||||
selectedCategory,
|
||||
selectedTag,
|
||||
installServer,
|
||||
fetchServerByName,
|
||||
servers: localServers,
|
||||
allServers: allLocalServers,
|
||||
categories: localCategories,
|
||||
loading: localLoading,
|
||||
error: localError,
|
||||
setError: setLocalError,
|
||||
searchServers: searchLocalServers,
|
||||
filterByCategory: filterLocalByCategory,
|
||||
filterByTag: filterLocalByTag,
|
||||
selectedCategory: selectedLocalCategory,
|
||||
selectedTag: selectedLocalTag,
|
||||
installServer: installLocalServer,
|
||||
fetchServerByName: fetchLocalServerByName,
|
||||
isServerInstalled,
|
||||
// Pagination
|
||||
currentPage,
|
||||
totalPages,
|
||||
changePage,
|
||||
serversPerPage,
|
||||
changeServersPerPage
|
||||
currentPage: localCurrentPage,
|
||||
totalPages: localTotalPages,
|
||||
changePage: changeLocalPage,
|
||||
serversPerPage: localServersPerPage,
|
||||
changeServersPerPage: changeLocalServersPerPage
|
||||
} = useMarketData();
|
||||
|
||||
// Cloud market data
|
||||
const {
|
||||
servers: cloudServers,
|
||||
allServers: allCloudServers,
|
||||
loading: cloudLoading,
|
||||
error: cloudError,
|
||||
setError: setCloudError,
|
||||
fetchServerTools,
|
||||
callServerTool,
|
||||
// Pagination
|
||||
currentPage: cloudCurrentPage,
|
||||
totalPages: cloudTotalPages,
|
||||
changePage: changeCloudPage,
|
||||
serversPerPage: cloudServersPerPage,
|
||||
changeServersPerPage: changeCloudServersPerPage
|
||||
} = useCloudData();
|
||||
|
||||
const [selectedServer, setSelectedServer] = useState<MarketServer | null>(null);
|
||||
const [selectedCloudServer, setSelectedCloudServer] = useState<CloudServer | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [installing, setInstalling] = useState(false);
|
||||
const [installedCloudServers, setInstalledCloudServers] = useState<Set<string>>(new Set());
|
||||
|
||||
// Load server details if a server name is in the URL
|
||||
useEffect(() => {
|
||||
const loadServerDetails = async () => {
|
||||
if (serverName) {
|
||||
const server = await fetchServerByName(serverName);
|
||||
if (server) {
|
||||
setSelectedServer(server);
|
||||
// Determine if it's a cloud or local server based on the current tab
|
||||
if (currentTab === 'cloud') {
|
||||
// Try to find the server in cloud servers
|
||||
const server = cloudServers.find(s => s.name === serverName);
|
||||
if (server) {
|
||||
setSelectedCloudServer(server);
|
||||
} else {
|
||||
// If server not found, navigate back to market page
|
||||
navigate('/market?tab=cloud');
|
||||
}
|
||||
} else {
|
||||
// If server not found, navigate back to market page
|
||||
navigate('/market');
|
||||
// Local market
|
||||
const server = await fetchLocalServerByName(serverName);
|
||||
if (server) {
|
||||
setSelectedServer(server);
|
||||
} else {
|
||||
// If server not found, navigate back to market page
|
||||
navigate('/market?tab=local');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setSelectedServer(null);
|
||||
setSelectedCloudServer(null);
|
||||
}
|
||||
};
|
||||
|
||||
loadServerDetails();
|
||||
}, [serverName, fetchServerByName, navigate]);
|
||||
}, [serverName, currentTab, cloudServers, fetchLocalServerByName, navigate]);
|
||||
|
||||
// Tab switching handler
|
||||
const switchTab = (tab: 'local' | 'cloud') => {
|
||||
const newSearchParams = new URLSearchParams(searchParams);
|
||||
newSearchParams.set('tab', tab);
|
||||
setSearchParams(newSearchParams);
|
||||
// Clear any selected server when switching tabs
|
||||
if (serverName) {
|
||||
navigate('/market?' + newSearchParams.toString());
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
searchServers(searchQuery);
|
||||
if (currentTab === 'local') {
|
||||
searchLocalServers(searchQuery);
|
||||
}
|
||||
// Cloud search is not implemented in the original cloud page
|
||||
};
|
||||
|
||||
const handleCategoryClick = (category: string) => {
|
||||
filterByCategory(category);
|
||||
if (currentTab === 'local') {
|
||||
filterLocalByCategory(category);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearFilters = () => {
|
||||
setSearchQuery('');
|
||||
filterByCategory('');
|
||||
filterByTag('');
|
||||
if (currentTab === 'local') {
|
||||
filterLocalByCategory('');
|
||||
filterLocalByTag('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleServerClick = (server: MarketServer) => {
|
||||
navigate(`/market/${server.name}`);
|
||||
const handleServerClick = (server: MarketServer | CloudServer) => {
|
||||
if (currentTab === 'cloud') {
|
||||
navigate(`/market/${server.name}?tab=cloud`);
|
||||
} else {
|
||||
navigate(`/market/${server.name}?tab=local`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackToList = () => {
|
||||
navigate('/market');
|
||||
navigate(`/market?tab=${currentTab}`);
|
||||
};
|
||||
|
||||
const handleInstall = async (server: MarketServer, config: ServerConfig) => {
|
||||
const handleLocalInstall = async (server: MarketServer, config: ServerConfig) => {
|
||||
try {
|
||||
setInstalling(true);
|
||||
// Pass the server object and the config to the installServer function
|
||||
const success = await installServer(server, config);
|
||||
const success = await installLocalServer(server, config);
|
||||
if (success) {
|
||||
// Show success message using toast instead of alert
|
||||
showToast(t('market.installSuccess', { serverName: server.display_name }), 'success');
|
||||
}
|
||||
} finally {
|
||||
@@ -97,15 +160,75 @@ const MarketPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Handle cloud server installation
|
||||
const handleCloudInstall = async (server: CloudServer, config: ServerConfig) => {
|
||||
try {
|
||||
setInstalling(true);
|
||||
|
||||
const payload = {
|
||||
name: server.name,
|
||||
config: config
|
||||
};
|
||||
|
||||
const result = await apiPost('/servers', payload);
|
||||
|
||||
if (!result.success) {
|
||||
const errorMessage = result?.message || t('server.addError');
|
||||
showToast(errorMessage, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update installed servers set
|
||||
setInstalledCloudServers(prev => new Set(prev).add(server.name));
|
||||
showToast(t('cloud.installSuccess', { name: server.title || server.name }), 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error installing cloud server:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
showToast(t('cloud.installError', { error: errorMessage }), 'error');
|
||||
} finally {
|
||||
setInstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCallTool = async (serverName: string, toolName: string, args: Record<string, any>) => {
|
||||
try {
|
||||
const result = await callServerTool(serverName, toolName, args);
|
||||
showToast(t('cloud.toolCallSuccess', { toolName }), 'success');
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
// Don't show toast for API key errors, let the component handle it
|
||||
if (!isMCPRouterApiKeyError(errorMessage)) {
|
||||
showToast(t('cloud.toolCallError', { toolName, error: errorMessage }), 'error');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to check if error is MCPRouter API key not configured
|
||||
const isMCPRouterApiKeyError = (errorMessage: string) => {
|
||||
return errorMessage === 'MCPROUTER_API_KEY_NOT_CONFIGURED' ||
|
||||
errorMessage.toLowerCase().includes('mcprouter api key not configured');
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
changePage(page);
|
||||
if (currentTab === 'local') {
|
||||
changeLocalPage(page);
|
||||
} else {
|
||||
changeCloudPage(page);
|
||||
}
|
||||
// Scroll to top of page when changing pages
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const handleChangeItemsPerPage = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const newValue = parseInt(e.target.value, 10);
|
||||
changeServersPerPage(newValue);
|
||||
if (currentTab === 'local') {
|
||||
changeLocalServersPerPage(newValue);
|
||||
} else {
|
||||
changeCloudServersPerPage(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
// Render detailed view if a server is selected
|
||||
@@ -114,164 +237,201 @@ const MarketPage: React.FC = () => {
|
||||
<MarketServerDetail
|
||||
server={selectedServer}
|
||||
onBack={handleBackToList}
|
||||
onInstall={handleInstall}
|
||||
onInstall={handleLocalInstall}
|
||||
installing={installing}
|
||||
isInstalled={isServerInstalled(selectedServer.name)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Render cloud server detail if selected
|
||||
if (selectedCloudServer) {
|
||||
return (
|
||||
<CloudServerDetail
|
||||
serverName={selectedCloudServer.name}
|
||||
onBack={handleBackToList}
|
||||
onCallTool={handleCallTool}
|
||||
fetchServerTools={fetchServerTools}
|
||||
onInstall={handleCloudInstall}
|
||||
installing={installing}
|
||||
isInstalled={installedCloudServers.has(selectedCloudServer.name)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Get current data based on active tab
|
||||
const isLocalTab = currentTab === 'local';
|
||||
const servers = isLocalTab ? localServers : cloudServers;
|
||||
const allServers = isLocalTab ? allLocalServers : allCloudServers;
|
||||
const categories = isLocalTab ? localCategories : [];
|
||||
const loading = isLocalTab ? localLoading : cloudLoading;
|
||||
const error = isLocalTab ? localError : cloudError;
|
||||
const setError = isLocalTab ? setLocalError : setCloudError;
|
||||
const selectedCategory = isLocalTab ? selectedLocalCategory : '';
|
||||
const selectedTag = isLocalTab ? selectedLocalTag : '';
|
||||
const currentPage = isLocalTab ? localCurrentPage : cloudCurrentPage;
|
||||
const totalPages = isLocalTab ? localTotalPages : cloudTotalPages;
|
||||
const serversPerPage = isLocalTab ? localServersPerPage : cloudServersPerPage;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 flex items-center">
|
||||
{t('market.title')}
|
||||
<span className="text-sm text-gray-500 font-normal ml-2">{t('pages.market.title').split(' - ')[1]}</span>
|
||||
</h1>
|
||||
{/* Tab Navigation */}
|
||||
<div className="mb-6">
|
||||
<div className="border-b border-gray-200">
|
||||
<nav className="-mb-px flex space-x-3">
|
||||
<button
|
||||
onClick={() => switchTab('cloud')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-lg hover:cursor-pointer transition-colors duration-200 ${!isLocalTab
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
{t('cloud.title')}
|
||||
<span className="text-xs text-gray-400 font-normal ml-1">(
|
||||
<a
|
||||
href="https://mcprouter.co"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="external-link"
|
||||
>
|
||||
MCPRouter
|
||||
</a>
|
||||
)
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => switchTab('local')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-lg hover:cursor-pointer transition-colors duration-200 ${isLocalTab
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
{t('market.title')}
|
||||
<span className="text-xs text-gray-400 font-normal ml-1">(
|
||||
<a
|
||||
href="https://mcpm.sh"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="external-link"
|
||||
>
|
||||
MCPM
|
||||
</a>
|
||||
)
|
||||
</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6 error-box rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<p>{error}</p>
|
||||
<>
|
||||
{!isLocalTab && isMCPRouterApiKeyError(error) ? (
|
||||
<MCPRouterApiKeyError />
|
||||
) : (
|
||||
<div className="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6 error-box rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<p>{error}</p>
|
||||
<button
|
||||
onClick={() => setError(null)}
|
||||
className="text-red-700 hover:text-red-900 transition-colors duration-200"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M4.293 4.293a1 1 011.414 0L10 8.586l4.293-4.293a1 1 01.414 1.414L11.414 10l4.293 4.293a1 1 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 01-1.414-1.414L8.586 10 4.293 5.707a1 1 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Search bar for local market only */}
|
||||
{isLocalTab && (
|
||||
<div className="bg-white shadow rounded-lg p-6 mb-6 page-card">
|
||||
<form onSubmit={handleSearch} className="flex space-x-4 mb-0">
|
||||
<div className="flex-grow">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder={t('market.searchPlaceholder')}
|
||||
className="shadow appearance-none border border-gray-200 rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setError(null)}
|
||||
className="text-red-700 hover:text-red-900 transition-colors duration-200"
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 flex items-center btn-primary transition-all duration-200"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M4.293 4.293a1 1 011.414 0L10 8.586l4.293-4.293a1 1 01.414 1.414L11.414 10l4.293 4.293a1 1 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 01-1.414-1.414L8.586 10 4.293 5.707a1 1 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
{t('market.search')}
|
||||
</button>
|
||||
</div>
|
||||
{(searchQuery || selectedCategory || selectedTag) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearFilters}
|
||||
className="border border-gray-300 text-gray-700 font-medium py-2 px-4 rounded hover:bg-gray-50 btn-secondary transition-all duration-200"
|
||||
>
|
||||
{t('market.clearFilters')}
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search bar at the top */}
|
||||
<div className="bg-white shadow rounded-lg p-6 mb-6 page-card">
|
||||
<form onSubmit={handleSearch} className="flex space-x-4 mb-0">
|
||||
<div className="flex-grow">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder={t('market.searchPlaceholder')}
|
||||
className="shadow appearance-none border border-gray-200 rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 flex items-center btn-primary transition-all duration-200"
|
||||
>
|
||||
{t('market.search')}
|
||||
</button>
|
||||
{(searchQuery || selectedCategory || selectedTag) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearFilters}
|
||||
className="border border-gray-300 text-gray-700 font-medium py-2 px-4 rounded hover:bg-gray-50 btn-secondary transition-all duration-200"
|
||||
>
|
||||
{t('market.clearFilters')}
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-6">
|
||||
{/* Left sidebar for filters (without search) */}
|
||||
<div className="md:w-48 flex-shrink-0">
|
||||
<div className="bg-white shadow rounded-lg p-4 mb-6 sticky top-4 page-card">
|
||||
{/* Categories */}
|
||||
{categories.length > 0 ? (
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
|
||||
{selectedCategory && (
|
||||
<span className="text-xs text-blue-600 cursor-pointer hover:underline transition-colors duration-200" onClick={() => filterByCategory('')}>
|
||||
{t('market.clearCategoryFilter')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
key={category}
|
||||
onClick={() => handleCategoryClick(category)}
|
||||
className={`px-3 py-2 rounded text-sm text-left transition-all duration-200 ${selectedCategory === category
|
||||
? 'bg-blue-100 text-blue-800 font-medium btn-primary'
|
||||
: 'bg-gray-100 text-gray-800 hover:bg-gray-200 btn-secondary'
|
||||
}`}
|
||||
>
|
||||
{category}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : loading ? (
|
||||
<div className="mb-6">
|
||||
<div className="mb-3">
|
||||
<h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 items-center py-4 loading-container">
|
||||
<svg className="animate-spin h-6 w-6 text-blue-500 mb-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<p className="text-sm text-gray-600">{t('app.loading')}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-6">
|
||||
<div className="mb-3">
|
||||
<h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 py-2">{t('market.noCategories')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tags */}
|
||||
{/* {tags.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<div className="flex items-center">
|
||||
<h3 className="font-medium text-gray-900">{t('market.tags')}</h3>
|
||||
<button
|
||||
onClick={toggleTagsVisibility}
|
||||
className="ml-2 p-1 text-gray-600 hover:text-blue-600 hover:bg-gray-100 rounded-full"
|
||||
aria-label={showTags ? t('market.hideTags') : t('market.showTags')}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className={`h-5 w-5 transition-transform ${showTags ? 'rotate-180' : ''}`} viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M5.293 7.293a1 1 011.414 0L10 10.586l3.293-3.293a1 1 011.414 1.414l-4 4a1 1 01-1.414 0l-4-4a1 1 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
{/* Left sidebar for filters (local market only) */}
|
||||
{isLocalTab && (
|
||||
<div className="md:w-48 flex-shrink-0">
|
||||
<div className="bg-white shadow rounded-lg p-4 mb-6 sticky top-4 page-card">
|
||||
{/* Categories */}
|
||||
{categories.length > 0 ? (
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
|
||||
{selectedCategory && (
|
||||
<span className="text-xs text-blue-600 cursor-pointer hover:underline transition-colors duration-200" onClick={() => filterLocalByCategory('')}>
|
||||
{t('market.clearCategoryFilter')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{selectedTag && (
|
||||
<span className="text-xs text-blue-600 cursor-pointer hover:underline" onClick={() => filterByTag('')}>
|
||||
{t('market.clearTagFilter')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{showTags && (
|
||||
<div className="flex flex-wrap gap-2 max-h-48 overflow-y-auto pr-2">
|
||||
{tags.map((tag) => (
|
||||
<div className="flex flex-col gap-2">
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
key={tag}
|
||||
onClick={() => handleTagClick(tag)}
|
||||
className={`px-2 py-1 rounded text-xs ${selectedTag === tag
|
||||
? 'bg-green-100 text-green-800 font-medium'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
key={category}
|
||||
onClick={() => handleCategoryClick(category)}
|
||||
className={`px-3 py-2 rounded text-sm text-left transition-all duration-200 ${selectedCategory === category
|
||||
? 'bg-blue-100 text-blue-800 font-medium btn-primary'
|
||||
: 'bg-gray-100 text-gray-800 hover:bg-gray-200 btn-secondary'
|
||||
}`}
|
||||
>
|
||||
#{tag}
|
||||
{category}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
) : loading ? (
|
||||
<div className="mb-6">
|
||||
<div className="mb-3">
|
||||
<h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 items-center py-4 loading-container">
|
||||
<svg className="animate-spin h-6 w-6 text-blue-500 mb-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<p className="text-sm text-gray-600">{t('app.loading')}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-6">
|
||||
<div className="mb-3">
|
||||
<h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 py-2">{t('market.noCategories')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main content area */}
|
||||
<div className="flex-grow">
|
||||
@@ -287,27 +447,43 @@ const MarketPage: React.FC = () => {
|
||||
</div>
|
||||
) : servers.length === 0 ? (
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<p className="text-gray-600">{t('market.noServers')}</p>
|
||||
<p className="text-gray-600">{isLocalTab ? t('market.noServers') : t('cloud.noServers')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{servers.map((server, index) => (
|
||||
<MarketServerCard
|
||||
key={index}
|
||||
server={server}
|
||||
onClick={handleServerClick}
|
||||
/>
|
||||
isLocalTab ? (
|
||||
<MarketServerCard
|
||||
key={index}
|
||||
server={server as MarketServer}
|
||||
onClick={handleServerClick}
|
||||
/>
|
||||
) : (
|
||||
<CloudServerCard
|
||||
key={index}
|
||||
server={server as CloudServer}
|
||||
onClick={handleServerClick}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="text-sm text-gray-500">
|
||||
{t('market.showing', {
|
||||
from: (currentPage - 1) * serversPerPage + 1,
|
||||
to: Math.min(currentPage * serversPerPage, allServers.length),
|
||||
total: allServers.length
|
||||
})}
|
||||
{isLocalTab ? (
|
||||
t('market.showing', {
|
||||
from: (currentPage - 1) * serversPerPage + 1,
|
||||
to: Math.min(currentPage * serversPerPage, allServers.length),
|
||||
total: allServers.length
|
||||
})
|
||||
) : (
|
||||
t('cloud.showing', {
|
||||
from: (currentPage - 1) * serversPerPage + 1,
|
||||
to: Math.min(currentPage * serversPerPage, allServers.length),
|
||||
total: allServers.length
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
@@ -316,7 +492,7 @@ const MarketPage: React.FC = () => {
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<label htmlFor="perPage" className="text-sm text-gray-600">
|
||||
{t('market.perPage')}:
|
||||
{isLocalTab ? t('market.perPage') : t('cloud.perPage')}:
|
||||
</label>
|
||||
<select
|
||||
id="perPage"
|
||||
@@ -333,7 +509,6 @@ const MarketPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -103,7 +103,6 @@ const ServersPage: React.FC = () => {
|
||||
<div className="mb-6 bg-red-50 border-l-4 border-red-500 p-4 rounded shadow-sm error-box">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-red-600 text-lg font-medium">{t('app.error')}</h3>
|
||||
<p className="text-gray-600 mt-1">{error}</p>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -36,18 +36,32 @@ const SettingsPage: React.FC = () => {
|
||||
openaiApiEmbeddingModel: '',
|
||||
});
|
||||
|
||||
const [tempMCPRouterConfig, setTempMCPRouterConfig] = useState<{
|
||||
apiKey: string;
|
||||
referer: string;
|
||||
title: string;
|
||||
baseUrl: string;
|
||||
}>({
|
||||
apiKey: '',
|
||||
referer: 'https://mcphub.app',
|
||||
title: 'MCPHub',
|
||||
baseUrl: 'https://api.mcprouter.to/v1',
|
||||
});
|
||||
|
||||
const {
|
||||
routingConfig,
|
||||
tempRoutingConfig,
|
||||
setTempRoutingConfig,
|
||||
installConfig: savedInstallConfig,
|
||||
smartRoutingConfig,
|
||||
mcpRouterConfig,
|
||||
loading,
|
||||
updateRoutingConfig,
|
||||
updateRoutingConfigBatch,
|
||||
updateInstallConfig,
|
||||
updateSmartRoutingConfig,
|
||||
updateSmartRoutingConfigBatch
|
||||
updateSmartRoutingConfigBatch,
|
||||
updateMCPRouterConfig
|
||||
} = useSettingsData();
|
||||
|
||||
// Update local installConfig when savedInstallConfig changes
|
||||
@@ -69,14 +83,27 @@ const SettingsPage: React.FC = () => {
|
||||
}
|
||||
}, [smartRoutingConfig]);
|
||||
|
||||
// Update local tempMCPRouterConfig when mcpRouterConfig changes
|
||||
useEffect(() => {
|
||||
if (mcpRouterConfig) {
|
||||
setTempMCPRouterConfig({
|
||||
apiKey: mcpRouterConfig.apiKey || '',
|
||||
referer: mcpRouterConfig.referer || 'https://mcphub.app',
|
||||
title: mcpRouterConfig.title || 'MCPHub',
|
||||
baseUrl: mcpRouterConfig.baseUrl || 'https://api.mcprouter.to/v1',
|
||||
});
|
||||
}
|
||||
}, [mcpRouterConfig]);
|
||||
|
||||
const [sectionsVisible, setSectionsVisible] = useState({
|
||||
routingConfig: false,
|
||||
installConfig: false,
|
||||
smartRoutingConfig: false,
|
||||
mcpRouterConfig: false,
|
||||
password: false
|
||||
});
|
||||
|
||||
const toggleSection = (section: 'routingConfig' | 'installConfig' | 'smartRoutingConfig' | 'password') => {
|
||||
const toggleSection = (section: 'routingConfig' | 'installConfig' | 'smartRoutingConfig' | 'mcpRouterConfig' | 'password') => {
|
||||
setSectionsVisible(prev => ({
|
||||
...prev,
|
||||
[section]: !prev[section]
|
||||
@@ -143,6 +170,17 @@ const SettingsPage: React.FC = () => {
|
||||
await updateSmartRoutingConfig(key, tempSmartRoutingConfig[key]);
|
||||
};
|
||||
|
||||
const handleMCPRouterConfigChange = (key: 'apiKey' | 'referer' | 'title' | 'baseUrl', value: string) => {
|
||||
setTempMCPRouterConfig({
|
||||
...tempMCPRouterConfig,
|
||||
[key]: value
|
||||
});
|
||||
};
|
||||
|
||||
const saveMCPRouterConfig = async (key: 'apiKey' | 'referer' | 'title' | 'baseUrl') => {
|
||||
await updateMCPRouterConfig(key, tempMCPRouterConfig[key]);
|
||||
};
|
||||
|
||||
const handleSmartRoutingEnabledChange = async (value: boolean) => {
|
||||
// If enabling Smart Routing, validate required fields and save any unsaved changes
|
||||
if (value) {
|
||||
@@ -197,7 +235,7 @@ const SettingsPage: React.FC = () => {
|
||||
|
||||
{/* Smart Routing Configuration Settings */}
|
||||
<PermissionChecker permissions={PERMISSIONS.SETTINGS_SMART_ROUTING}>
|
||||
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 page-card">
|
||||
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 page-card dashboard-card">
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer transition-colors duration-200 hover:text-blue-600"
|
||||
onClick={() => toggleSection('smartRoutingConfig')}
|
||||
@@ -322,8 +360,123 @@ const SettingsPage: React.FC = () => {
|
||||
</div>
|
||||
</PermissionChecker>
|
||||
|
||||
{/* MCPRouter Configuration Settings */}
|
||||
<PermissionChecker permissions={PERMISSIONS.SETTINGS_INSTALL_CONFIG}>
|
||||
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 page-card dashboard-card">
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer transition-colors duration-200 hover:text-blue-600"
|
||||
onClick={() => toggleSection('mcpRouterConfig')}
|
||||
>
|
||||
<h2 className="font-semibold text-gray-800">{t('settings.mcpRouterConfig')}</h2>
|
||||
<span className="text-gray-500 transition-transform duration-200">
|
||||
{sectionsVisible.mcpRouterConfig ? '▼' : '►'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{sectionsVisible.mcpRouterConfig && (
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="p-3 bg-gray-50 rounded-md">
|
||||
<div className="mb-2">
|
||||
<h3 className="font-medium text-gray-700">{t('settings.mcpRouterApiKey')}</h3>
|
||||
<p className="text-sm text-gray-500">{t('settings.mcpRouterApiKeyDescription')}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="password"
|
||||
value={tempMCPRouterConfig.apiKey}
|
||||
onChange={(e) => handleMCPRouterConfigChange('apiKey', e.target.value)}
|
||||
placeholder={t('settings.mcpRouterApiKeyPlaceholder')}
|
||||
className="flex-1 mt-1 block w-full py-2 px-3 border rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm border-gray-300 form-input"
|
||||
disabled={loading}
|
||||
/>
|
||||
<button
|
||||
onClick={() => saveMCPRouterConfig('apiKey')}
|
||||
disabled={loading}
|
||||
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50 btn-primary"
|
||||
>
|
||||
{t('common.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-gray-50 rounded-md">
|
||||
<div className="mb-2">
|
||||
<h3 className="font-medium text-gray-700">{t('settings.mcpRouterReferer')}</h3>
|
||||
<p className="text-sm text-gray-500">{t('settings.mcpRouterRefererDescription')}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="text"
|
||||
value={tempMCPRouterConfig.referer}
|
||||
onChange={(e) => handleMCPRouterConfigChange('referer', e.target.value)}
|
||||
placeholder={t('settings.mcpRouterRefererPlaceholder')}
|
||||
className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm form-input"
|
||||
disabled={loading}
|
||||
/>
|
||||
<button
|
||||
onClick={() => saveMCPRouterConfig('referer')}
|
||||
disabled={loading}
|
||||
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50 btn-primary"
|
||||
>
|
||||
{t('common.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-gray-50 rounded-md">
|
||||
<div className="mb-2">
|
||||
<h3 className="font-medium text-gray-700">{t('settings.mcpRouterTitle')}</h3>
|
||||
<p className="text-sm text-gray-500">{t('settings.mcpRouterTitleDescription')}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="text"
|
||||
value={tempMCPRouterConfig.title}
|
||||
onChange={(e) => handleMCPRouterConfigChange('title', e.target.value)}
|
||||
placeholder={t('settings.mcpRouterTitlePlaceholder')}
|
||||
className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm form-input"
|
||||
disabled={loading}
|
||||
/>
|
||||
<button
|
||||
onClick={() => saveMCPRouterConfig('title')}
|
||||
disabled={loading}
|
||||
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50 btn-primary"
|
||||
>
|
||||
{t('common.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-gray-50 rounded-md">
|
||||
<div className="mb-2">
|
||||
<h3 className="font-medium text-gray-700">{t('settings.mcpRouterBaseUrl')}</h3>
|
||||
<p className="text-sm text-gray-500">{t('settings.mcpRouterBaseUrlDescription')}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="text"
|
||||
value={tempMCPRouterConfig.baseUrl}
|
||||
onChange={(e) => handleMCPRouterConfigChange('baseUrl', e.target.value)}
|
||||
placeholder={t('settings.mcpRouterBaseUrlPlaceholder')}
|
||||
className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm form-input"
|
||||
disabled={loading}
|
||||
/>
|
||||
<button
|
||||
onClick={() => saveMCPRouterConfig('baseUrl')}
|
||||
disabled={loading}
|
||||
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50 btn-primary"
|
||||
>
|
||||
{t('common.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PermissionChecker>
|
||||
|
||||
{/* Route Configuration Settings */}
|
||||
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6">
|
||||
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 dashboard-card">
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => toggleSection('routingConfig')}
|
||||
@@ -418,7 +571,7 @@ const SettingsPage: React.FC = () => {
|
||||
|
||||
{/* Installation Configuration Settings */}
|
||||
<PermissionChecker permissions={PERMISSIONS.SETTINGS_INSTALL_CONFIG}>
|
||||
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6">
|
||||
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 dashboard-card">
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => toggleSection('installConfig')}
|
||||
@@ -508,7 +661,7 @@ const SettingsPage: React.FC = () => {
|
||||
</PermissionChecker>
|
||||
|
||||
{/* Change Password */}
|
||||
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6">
|
||||
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 dashboard-card">
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => toggleSection('password')}
|
||||
|
||||
@@ -4,45 +4,27 @@ import {
|
||||
RegisterCredentials,
|
||||
ChangePasswordCredentials,
|
||||
} from '../types';
|
||||
import { getApiUrl } from '../utils/runtime';
|
||||
import { apiPost, apiGet } from '../utils/fetchInterceptor';
|
||||
import { getToken, setToken, removeToken } from '../utils/interceptors';
|
||||
|
||||
// Token key in localStorage
|
||||
const TOKEN_KEY = 'mcphub_token';
|
||||
|
||||
// Get token from localStorage
|
||||
export const getToken = (): string | null => {
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
};
|
||||
|
||||
// Set token in localStorage
|
||||
export const setToken = (token: string): void => {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
};
|
||||
|
||||
// Remove token from localStorage
|
||||
export const removeToken = (): void => {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
};
|
||||
// Export token management functions
|
||||
export { getToken, setToken, removeToken };
|
||||
|
||||
// Login user
|
||||
export const login = async (credentials: LoginCredentials): Promise<AuthResponse> => {
|
||||
try {
|
||||
console.log(getApiUrl('/auth/login'));
|
||||
const response = await fetch(getApiUrl('/auth/login'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
const response = await apiPost<AuthResponse>('/auth/login', credentials);
|
||||
|
||||
const data: AuthResponse = await response.json();
|
||||
|
||||
if (data.success && data.token) {
|
||||
setToken(data.token);
|
||||
// The auth API returns data directly, not wrapped in a data field
|
||||
if (response.success && response.token) {
|
||||
setToken(response.token);
|
||||
return response;
|
||||
}
|
||||
|
||||
return data;
|
||||
return {
|
||||
success: false,
|
||||
message: response.message || 'Login failed',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
return {
|
||||
@@ -55,21 +37,17 @@ export const login = async (credentials: LoginCredentials): Promise<AuthResponse
|
||||
// Register user
|
||||
export const register = async (credentials: RegisterCredentials): Promise<AuthResponse> => {
|
||||
try {
|
||||
const response = await fetch(getApiUrl('/auth/register'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
const response = await apiPost<AuthResponse>('/auth/register', credentials);
|
||||
|
||||
const data: AuthResponse = await response.json();
|
||||
|
||||
if (data.success && data.token) {
|
||||
setToken(data.token);
|
||||
if (response.success && response.token) {
|
||||
setToken(response.token);
|
||||
return response;
|
||||
}
|
||||
|
||||
return data;
|
||||
return {
|
||||
success: false,
|
||||
message: response.message || 'Registration failed',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Register error:', error);
|
||||
return {
|
||||
@@ -91,14 +69,8 @@ export const getCurrentUser = async (): Promise<AuthResponse> => {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(getApiUrl('/auth/user'), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'x-auth-token': token,
|
||||
},
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
const response = await apiGet<AuthResponse>('/auth/user');
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Get current user error:', error);
|
||||
return {
|
||||
@@ -122,16 +94,8 @@ export const changePassword = async (
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(getApiUrl('/auth/change-password'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token,
|
||||
},
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
const response = await apiPost<AuthResponse>('/auth/change-password', credentials);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Change password error:', error);
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getApiUrl, getBasePath } from '../utils/runtime';
|
||||
import { apiGet, fetchWithInterceptors } from '../utils/fetchInterceptor';
|
||||
import { getBasePath } from '../utils/runtime';
|
||||
|
||||
export interface SystemConfig {
|
||||
routing?: {
|
||||
@@ -25,6 +26,7 @@ export interface PublicConfigResponse {
|
||||
success: boolean;
|
||||
data?: {
|
||||
skipAuth?: boolean;
|
||||
permissions?: any;
|
||||
};
|
||||
message?: string;
|
||||
}
|
||||
@@ -40,10 +42,10 @@ export interface SystemConfigResponse {
|
||||
/**
|
||||
* Get public configuration (skipAuth setting) without authentication
|
||||
*/
|
||||
export const getPublicConfig = async (): Promise<{ skipAuth: boolean }> => {
|
||||
export const getPublicConfig = async (): Promise<{ skipAuth: boolean; permissions?: any }> => {
|
||||
try {
|
||||
const basePath = getBasePath();
|
||||
const response = await fetch(`${basePath}/public-config`, {
|
||||
const response = await fetchWithInterceptors(`${basePath}/public-config`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -52,7 +54,7 @@ export const getPublicConfig = async (): Promise<{ skipAuth: boolean }> => {
|
||||
|
||||
if (response.ok) {
|
||||
const data: PublicConfigResponse = await response.json();
|
||||
return { skipAuth: data.data?.skipAuth === true };
|
||||
return { skipAuth: data.data?.skipAuth === true, permissions: data.data?.permissions || {} };
|
||||
}
|
||||
|
||||
return { skipAuth: false };
|
||||
@@ -69,16 +71,10 @@ export const getPublicConfig = async (): Promise<{ skipAuth: boolean }> => {
|
||||
*/
|
||||
export const getSystemConfigPublic = async (): Promise<SystemConfig | null> => {
|
||||
try {
|
||||
const response = await fetch(getApiUrl('/settings'), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const response = await apiGet<SystemConfigResponse>('/settings');
|
||||
|
||||
if (response.ok) {
|
||||
const data: SystemConfigResponse = await response.json();
|
||||
return data.data?.systemConfig || null;
|
||||
if (response.success) {
|
||||
return response.data?.systemConfig || null;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getToken } from './authService'; // Import getToken function
|
||||
import { apiGet, apiDelete } from '../utils/fetchInterceptor';
|
||||
import { getApiUrl } from '../utils/runtime';
|
||||
import { getToken } from '../utils/interceptors';
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: number;
|
||||
@@ -13,21 +14,13 @@ export interface LogEntry {
|
||||
// Fetch all logs
|
||||
export const fetchLogs = async (): Promise<LogEntry[]> => {
|
||||
try {
|
||||
// Get authentication token
|
||||
const token = getToken();
|
||||
const response = await fetch(getApiUrl('/logs'), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
const response = await apiGet<{ success: boolean; data: LogEntry[]; error?: string }>('/logs');
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to fetch logs');
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || 'Failed to fetch logs');
|
||||
}
|
||||
|
||||
return result.data;
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching logs:', error);
|
||||
throw error;
|
||||
@@ -37,19 +30,10 @@ export const fetchLogs = async (): Promise<LogEntry[]> => {
|
||||
// Clear all logs
|
||||
export const clearLogs = async (): Promise<void> => {
|
||||
try {
|
||||
// Get authentication token
|
||||
const token = getToken();
|
||||
const response = await fetch(getApiUrl('/logs'), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
const response = await apiDelete<{ success: boolean; error?: string }>('/logs');
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to clear logs');
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || 'Failed to clear logs');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error clearing logs:', error);
|
||||
|
||||
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',
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getApiUrl } from '../utils/runtime';
|
||||
import { getToken } from './authService';
|
||||
import { apiPost, apiPut } from '../utils/fetchInterceptor';
|
||||
|
||||
export interface ToolCallRequest {
|
||||
toolName: string;
|
||||
@@ -25,38 +24,32 @@ export const callTool = async (
|
||||
server?: string,
|
||||
): Promise<ToolCallResult> => {
|
||||
try {
|
||||
const token = getToken();
|
||||
// Construct the URL with optional server parameter
|
||||
const url = server ? `/tools/call/${server}` : '/tools/call';
|
||||
|
||||
const response = await fetch(getApiUrl(url), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '', // Include token for authentication
|
||||
Authorization: `Bearer ${token}`, // Add bearer auth for MCP routing
|
||||
},
|
||||
body: JSON.stringify({
|
||||
const response = await apiPost<any>(
|
||||
url,
|
||||
{
|
||||
toolName: request.toolName,
|
||||
arguments: request.arguments,
|
||||
}),
|
||||
});
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('mcphub_token')}`, // Add bearer auth for MCP routing
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.success) {
|
||||
if (!response.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.message || 'Tool call failed',
|
||||
error: response.message || 'Tool call failed',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
content: data.data.content || [],
|
||||
content: response.data?.content || [],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error calling tool:', error);
|
||||
@@ -76,25 +69,19 @@ export const toggleTool = async (
|
||||
enabled: boolean,
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
const token = getToken();
|
||||
const response = await fetch(getApiUrl(`/servers/${serverName}/tools/${toolName}/toggle`), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
Authorization: `Bearer ${token}`,
|
||||
const response = await apiPost<any>(
|
||||
`/servers/${serverName}/tools/${toolName}/toggle`,
|
||||
{ enabled },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('mcphub_token')}`,
|
||||
},
|
||||
},
|
||||
body: JSON.stringify({ enabled }),
|
||||
});
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
success: data.success,
|
||||
error: data.success ? undefined : data.message,
|
||||
success: response.success,
|
||||
error: response.success ? undefined : response.message,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error toggling tool:', error);
|
||||
@@ -114,28 +101,19 @@ export const updateToolDescription = async (
|
||||
description: string,
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
const token = getToken();
|
||||
const response = await fetch(
|
||||
getApiUrl(`/servers/${serverName}/tools/${toolName}/description`),
|
||||
const response = await apiPut<any>(
|
||||
`/servers/${serverName}/tools/${toolName}/description`,
|
||||
{ description },
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
Authorization: `Bearer ${token || ''}`,
|
||||
Authorization: `Bearer ${localStorage.getItem('mcphub_token')}`,
|
||||
},
|
||||
body: JSON.stringify({ description }),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
success: data.success,
|
||||
error: data.success ? undefined : data.message,
|
||||
success: response.success,
|
||||
error: response.success ? undefined : response.message,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error updating tool description:', error);
|
||||
|
||||
@@ -55,6 +55,27 @@ export interface MarketServer {
|
||||
is_official?: boolean;
|
||||
}
|
||||
|
||||
// Cloud Server types (for MCPRouter API)
|
||||
export interface CloudServer {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
name: string;
|
||||
author_name: string;
|
||||
title: string;
|
||||
description: string;
|
||||
content: string;
|
||||
server_key: string;
|
||||
config_name: string;
|
||||
server_url: string;
|
||||
tools?: CloudServerTool[];
|
||||
}
|
||||
|
||||
export interface CloudServerTool {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: Record<string, any>;
|
||||
}
|
||||
|
||||
// Tool input schema types
|
||||
export interface ToolInputSchema {
|
||||
type: string;
|
||||
@@ -70,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';
|
||||
@@ -80,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
|
||||
@@ -132,6 +168,7 @@ export interface Server {
|
||||
status: ServerStatus;
|
||||
error?: string;
|
||||
tools?: Tool[];
|
||||
prompts?: Prompt[];
|
||||
config?: ServerConfig;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
174
frontend/src/utils/fetchInterceptor.ts
Normal file
174
frontend/src/utils/fetchInterceptor.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { getApiUrl } from './runtime';
|
||||
|
||||
// Define the interceptor interface
|
||||
export interface FetchInterceptor {
|
||||
request?: (url: string, config: RequestInit) => Promise<{ url: string; config: RequestInit }>;
|
||||
response?: (response: Response) => Promise<Response>;
|
||||
error?: (error: Error) => Promise<Error>;
|
||||
}
|
||||
|
||||
// Define the enhanced fetch response interface
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Global interceptors store
|
||||
const interceptors: FetchInterceptor[] = [];
|
||||
|
||||
// Add an interceptor
|
||||
export const addInterceptor = (interceptor: FetchInterceptor): void => {
|
||||
interceptors.push(interceptor);
|
||||
};
|
||||
|
||||
// Remove an interceptor
|
||||
export const removeInterceptor = (interceptor: FetchInterceptor): void => {
|
||||
const index = interceptors.indexOf(interceptor);
|
||||
if (index > -1) {
|
||||
interceptors.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
// Clear all interceptors
|
||||
export const clearInterceptors = (): void => {
|
||||
interceptors.length = 0;
|
||||
};
|
||||
|
||||
// Enhanced fetch function with interceptors
|
||||
export const fetchWithInterceptors = async (
|
||||
input: string | URL | Request,
|
||||
init: RequestInit = {},
|
||||
): Promise<Response> => {
|
||||
let url = input.toString();
|
||||
let config = { ...init };
|
||||
|
||||
try {
|
||||
// Apply request interceptors
|
||||
for (const interceptor of interceptors) {
|
||||
if (interceptor.request) {
|
||||
const result = await interceptor.request(url, config);
|
||||
url = result.url;
|
||||
config = result.config;
|
||||
}
|
||||
}
|
||||
|
||||
// Make the actual fetch request
|
||||
let response = await fetch(url, config);
|
||||
|
||||
// Apply response interceptors
|
||||
for (const interceptor of interceptors) {
|
||||
if (interceptor.response) {
|
||||
response = await interceptor.response(response);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
let processedError = error as Error;
|
||||
|
||||
// Apply error interceptors
|
||||
for (const interceptor of interceptors) {
|
||||
if (interceptor.error) {
|
||||
processedError = await interceptor.error(processedError);
|
||||
}
|
||||
}
|
||||
|
||||
throw processedError;
|
||||
}
|
||||
};
|
||||
|
||||
// Convenience function for API calls with automatic URL construction
|
||||
export const apiRequest = async <T = any>(endpoint: string, init: RequestInit = {}): Promise<T> => {
|
||||
try {
|
||||
const url = getApiUrl(endpoint);
|
||||
const response = await fetchWithInterceptors(url, init);
|
||||
|
||||
// Try to parse JSON response
|
||||
let data: T;
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch (parseError) {
|
||||
// If JSON parsing fails, create a generic response
|
||||
const genericResponse = {
|
||||
success: response.ok,
|
||||
message: response.ok
|
||||
? 'Request successful'
|
||||
: `HTTP ${response.status}: ${response.statusText}`,
|
||||
};
|
||||
data = genericResponse as T;
|
||||
}
|
||||
|
||||
// If response is not ok, but no explicit error in parsed data
|
||||
if (!response.ok && typeof data === 'object' && data !== null) {
|
||||
const responseObj = data as any;
|
||||
if (responseObj.success !== false) {
|
||||
responseObj.success = false;
|
||||
responseObj.message =
|
||||
responseObj.message || `HTTP ${response.status}: ${response.statusText}`;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('API request error:', error);
|
||||
const errorResponse = {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'An unknown error occurred',
|
||||
};
|
||||
return errorResponse as T;
|
||||
}
|
||||
};
|
||||
|
||||
// Convenience methods for common HTTP methods
|
||||
export const apiGet = <T = any>(endpoint: string, init: Omit<RequestInit, 'method'> = {}) =>
|
||||
apiRequest<T>(endpoint, { ...init, method: 'GET' });
|
||||
|
||||
export const apiPost = <T = any>(
|
||||
endpoint: string,
|
||||
data?: any,
|
||||
init: Omit<RequestInit, 'method' | 'body'> = {},
|
||||
) =>
|
||||
apiRequest<T>(endpoint, {
|
||||
...init,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...init.headers,
|
||||
},
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
});
|
||||
|
||||
export const apiPut = <T = any>(
|
||||
endpoint: string,
|
||||
data?: any,
|
||||
init: Omit<RequestInit, 'method' | 'body'> = {},
|
||||
) =>
|
||||
apiRequest<T>(endpoint, {
|
||||
...init,
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...init.headers,
|
||||
},
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
});
|
||||
|
||||
export const apiDelete = <T = any>(endpoint: string, init: Omit<RequestInit, 'method'> = {}) =>
|
||||
apiRequest<T>(endpoint, { ...init, method: 'DELETE' });
|
||||
|
||||
export const apiPatch = <T = any>(
|
||||
endpoint: string,
|
||||
data?: any,
|
||||
init: Omit<RequestInit, 'method' | 'body'> = {},
|
||||
) =>
|
||||
apiRequest<T>(endpoint, {
|
||||
...init,
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...init.headers,
|
||||
},
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
});
|
||||
99
frontend/src/utils/interceptors.ts
Normal file
99
frontend/src/utils/interceptors.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { addInterceptor, removeInterceptor, type FetchInterceptor } from './fetchInterceptor';
|
||||
|
||||
// Token key in localStorage
|
||||
const TOKEN_KEY = 'mcphub_token';
|
||||
|
||||
// Get token from localStorage
|
||||
export const getToken = (): string | null => {
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
};
|
||||
|
||||
// Set token in localStorage
|
||||
export const setToken = (token: string): void => {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
};
|
||||
|
||||
// Remove token from localStorage
|
||||
export const removeToken = (): void => {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
};
|
||||
|
||||
// Auth interceptor for automatically adding authorization headers
|
||||
export const authInterceptor: FetchInterceptor = {
|
||||
request: async (url: string, config: RequestInit) => {
|
||||
const headers = new Headers(config.headers);
|
||||
const language = localStorage.getItem('i18nextLng') || 'en';
|
||||
headers.set('Accept-Language', language);
|
||||
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
headers.set('x-auth-token', token);
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
config: {
|
||||
...config,
|
||||
headers,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
response: async (response: Response) => {
|
||||
// Handle unauthorized responses
|
||||
if (response.status === 401) {
|
||||
// Token might be expired or invalid, remove it
|
||||
removeToken();
|
||||
|
||||
// You could also trigger a redirect to login page here
|
||||
// window.location.href = '/login';
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
error: async (error: Error) => {
|
||||
console.error('Auth interceptor error:', error);
|
||||
return error;
|
||||
},
|
||||
};
|
||||
|
||||
// Install the auth interceptor
|
||||
export const installAuthInterceptor = (): void => {
|
||||
addInterceptor(authInterceptor);
|
||||
};
|
||||
|
||||
// Uninstall the auth interceptor
|
||||
export const uninstallAuthInterceptor = (): void => {
|
||||
removeInterceptor(authInterceptor);
|
||||
};
|
||||
|
||||
// Logging interceptor for development
|
||||
export const loggingInterceptor: FetchInterceptor = {
|
||||
request: async (url: string, config: RequestInit) => {
|
||||
console.log(`🚀 [${config.method || 'GET'}] ${url}`, config);
|
||||
return { url, config };
|
||||
},
|
||||
|
||||
response: async (response: Response) => {
|
||||
console.log(`✅ [${response.status}] ${response.url}`);
|
||||
return response;
|
||||
},
|
||||
|
||||
error: async (error: Error) => {
|
||||
console.error(`❌ Fetch error:`, error);
|
||||
return error;
|
||||
},
|
||||
};
|
||||
|
||||
// Install the logging interceptor (only in development)
|
||||
export const installLoggingInterceptor = (): void => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
addInterceptor(loggingInterceptor);
|
||||
}
|
||||
};
|
||||
|
||||
// Uninstall the logging interceptor
|
||||
export const uninstallLoggingInterceptor = (): void => {
|
||||
removeInterceptor(loggingInterceptor);
|
||||
};
|
||||
19
frontend/src/utils/setupInterceptors.ts
Normal file
19
frontend/src/utils/setupInterceptors.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { installAuthInterceptor, installLoggingInterceptor } from './interceptors';
|
||||
|
||||
/**
|
||||
* Setup all default interceptors for the application
|
||||
* This should be called once when the app initializes
|
||||
*/
|
||||
export const setupInterceptors = (): void => {
|
||||
// Install auth interceptor for automatic token handling
|
||||
installAuthInterceptor();
|
||||
|
||||
// Install logging interceptor in development mode
|
||||
installLoggingInterceptor();
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize interceptors automatically when this module is imported
|
||||
* This ensures interceptors are set up as early as possible
|
||||
*/
|
||||
setupInterceptors();
|
||||
@@ -1,12 +1,9 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
darkMode: 'class', // Use class strategy for dark mode
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
plugins: [require('@tailwindcss/line-clamp')],
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "MCP Hub Dashboard",
|
||||
"title": "MCPHub Dashboard",
|
||||
"error": "Error",
|
||||
"closeButton": "Close",
|
||||
"noServers": "No MCP servers available",
|
||||
@@ -10,11 +10,11 @@
|
||||
"changePassword": "Change Password",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"welcomeUser": "Welcome, {{username}}",
|
||||
"name": "MCP Hub"
|
||||
"name": "MCPHub"
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
"versionInfo": "MCP Hub Version: {{version}}",
|
||||
"versionInfo": "MCPHub Version: {{version}}",
|
||||
"newVersion": "New version available!",
|
||||
"currentVersion": "Current version",
|
||||
"newVersionAvailable": "New version {{version}} is available",
|
||||
@@ -30,7 +30,7 @@
|
||||
"label": "Sponsor",
|
||||
"title": "Support the Project",
|
||||
"rewardAlt": "Reward QR Code",
|
||||
"supportMessage": "Support the development of MCP Hub by buying me a coffee!",
|
||||
"supportMessage": "Support the development of MCPHub by buying me a coffee!",
|
||||
"supportButton": "Support on Ko-fi"
|
||||
},
|
||||
"wechat": {
|
||||
@@ -52,7 +52,9 @@
|
||||
},
|
||||
"auth": {
|
||||
"login": "Login",
|
||||
"loginTitle": "Login to MCP Hub",
|
||||
"loginTitle": "Login to MCPHub",
|
||||
"slogan": "The Unified Hub for MCP Servers",
|
||||
"subtitle": "Centralized management platform for Model Context Protocol servers. Organize, monitor, and scale multiple MCP servers with flexible routing strategies.",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"loggingIn": "Logging in...",
|
||||
@@ -78,6 +80,7 @@
|
||||
"deleteWarning": "Deleting server '{{name}}' will remove it and all its data. This action cannot be undone.",
|
||||
"status": "Status",
|
||||
"tools": "Tools",
|
||||
"prompts": "Prompts",
|
||||
"name": "Server Name",
|
||||
"url": "Server URL",
|
||||
"apiKey": "API Key",
|
||||
@@ -197,6 +200,7 @@
|
||||
"settings": "Settings",
|
||||
"changePassword": "Change Password",
|
||||
"market": "Market",
|
||||
"cloud": "Cloud Market",
|
||||
"logs": "Logs"
|
||||
},
|
||||
"pages": {
|
||||
@@ -228,7 +232,7 @@
|
||||
"smartRouting": "Smart Routing"
|
||||
},
|
||||
"market": {
|
||||
"title": "Server Market - (Data from mcpm.sh)"
|
||||
"title": "Market Hub - Local and Cloud Markets"
|
||||
},
|
||||
"logs": {
|
||||
"title": "System Logs"
|
||||
@@ -281,7 +285,7 @@
|
||||
"configureTools": "Configure Tools"
|
||||
},
|
||||
"market": {
|
||||
"title": "Server Market",
|
||||
"title": "Local Installation",
|
||||
"official": "Official",
|
||||
"by": "By",
|
||||
"unknown": "Unknown",
|
||||
@@ -324,6 +328,58 @@
|
||||
"confirmVariablesMessage": "Please ensure these variables are properly defined in your runtime environment. Continue installing server?",
|
||||
"confirmAndInstall": "Confirm and Install"
|
||||
},
|
||||
"cloud": {
|
||||
"title": "Cloud Support",
|
||||
"subtitle": "Powered by MCPRouter",
|
||||
"by": "By",
|
||||
"server": "Server",
|
||||
"config": "Config",
|
||||
"created": "Created",
|
||||
"updated": "Updated",
|
||||
"available": "Available",
|
||||
"description": "Description",
|
||||
"details": "Details",
|
||||
"tools": "Tools",
|
||||
"tool": "tool",
|
||||
"toolsAvailable": "{{count}} tool available||{{count}} tools available",
|
||||
"loadingTools": "Loading tools...",
|
||||
"noTools": "No tools available for this server",
|
||||
"noDescription": "No description available",
|
||||
"viewDetails": "View Details",
|
||||
"parameters": "Parameters",
|
||||
"result": "Result",
|
||||
"error": "Error",
|
||||
"callTool": "Call",
|
||||
"calling": "Calling...",
|
||||
"toolCallSuccess": "Tool {{toolName}} executed successfully",
|
||||
"toolCallError": "Failed to call tool {{toolName}}: {{error}}",
|
||||
"viewSchema": "View Schema",
|
||||
"backToList": "Back to Cloud Market",
|
||||
"search": "Search",
|
||||
"searchPlaceholder": "Search cloud servers by name, title, or author",
|
||||
"clearFilters": "Clear Filters",
|
||||
"clearCategoryFilter": "Clear",
|
||||
"clearTagFilter": "Clear",
|
||||
"categories": "Categories",
|
||||
"tags": "Tags",
|
||||
"noCategories": "No categories found",
|
||||
"noTags": "No tags found",
|
||||
"noServers": "No cloud servers found",
|
||||
"fetchError": "Error fetching cloud servers",
|
||||
"serverNotFound": "Cloud server not found",
|
||||
"searchError": "Error searching cloud servers",
|
||||
"filterError": "Error filtering cloud servers by category",
|
||||
"tagFilterError": "Error filtering cloud servers by tag",
|
||||
"showing": "Showing {{from}}-{{to}} of {{total}} cloud servers",
|
||||
"perPage": "Per page",
|
||||
"apiKeyNotConfigured": "MCPRouter API key not configured",
|
||||
"apiKeyNotConfiguredDescription": "To use cloud servers, you need to configure your MCPRouter API key.",
|
||||
"getApiKey": "Get API Key",
|
||||
"configureInSettings": "Configure in Settings",
|
||||
"installServer": "Install {{name}}",
|
||||
"installSuccess": "Server {{name}} installed successfully",
|
||||
"installError": "Failed to install server: {{error}}"
|
||||
},
|
||||
"tool": {
|
||||
"run": "Run",
|
||||
"running": "Running...",
|
||||
@@ -359,6 +415,23 @@
|
||||
"addItem": "Add {{key}} item",
|
||||
"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": {
|
||||
"enableGlobalRoute": "Enable Global Route",
|
||||
"enableGlobalRouteDescription": "Allow connections to /sse endpoint without specifying a group ID",
|
||||
@@ -394,7 +467,20 @@
|
||||
"openaiApiEmbeddingModelPlaceholder": "text-embedding-3-small",
|
||||
"smartRoutingConfigUpdated": "Smart routing configuration updated successfully",
|
||||
"smartRoutingRequiredFields": "Database URL and OpenAI API Key are required to enable smart routing",
|
||||
"smartRoutingValidationError": "Please fill in the required fields before enabling Smart Routing: {{fields}}"
|
||||
"smartRoutingValidationError": "Please fill in the required fields before enabling Smart Routing: {{fields}}",
|
||||
"mcpRouterConfig": "Cloud Market",
|
||||
"mcpRouterApiKey": "MCPRouter API Key",
|
||||
"mcpRouterApiKeyDescription": "API key for accessing MCPRouter cloud market services",
|
||||
"mcpRouterApiKeyPlaceholder": "Enter MCPRouter API key",
|
||||
"mcpRouterReferer": "Referer",
|
||||
"mcpRouterRefererDescription": "Referer header for MCPRouter API requests",
|
||||
"mcpRouterRefererPlaceholder": "https://mcphub.app",
|
||||
"mcpRouterTitle": "Title",
|
||||
"mcpRouterTitleDescription": "Title header for MCPRouter API requests",
|
||||
"mcpRouterTitlePlaceholder": "MCPHub",
|
||||
"mcpRouterBaseUrl": "Base URL",
|
||||
"mcpRouterBaseUrlDescription": "Base URL for MCPRouter API",
|
||||
"mcpRouterBaseUrlPlaceholder": "https://api.mcprouter.to/v1"
|
||||
},
|
||||
"dxt": {
|
||||
"upload": "Upload",
|
||||
@@ -456,5 +542,64 @@
|
||||
"deleteConfirmation": "Are you sure you want to delete user '{{username}}'? This action cannot be undone.",
|
||||
"confirmDelete": "Delete User",
|
||||
"deleteWarning": "Are you sure you want to delete user '{{username}}'? This action cannot be undone."
|
||||
},
|
||||
"api": {
|
||||
"errors": {
|
||||
"readonly": "Readonly for demo environment",
|
||||
"invalid_credentials": "Invalid username or password",
|
||||
"serverNameRequired": "Server name is required",
|
||||
"serverConfigRequired": "Server configuration is required",
|
||||
"serverConfigInvalid": "Server configuration must include either a URL, OpenAPI specification URL or schema, or command with arguments",
|
||||
"serverTypeInvalid": "Server type must be one of: stdio, sse, streamable-http, openapi",
|
||||
"urlRequiredForType": "URL is required for {{type}} server type",
|
||||
"openapiSpecRequired": "OpenAPI specification URL or schema is required for openapi server type",
|
||||
"headersInvalidFormat": "Headers must be an object",
|
||||
"headersNotSupportedForStdio": "Headers are not supported for stdio server type",
|
||||
"serverNotFound": "Server not found",
|
||||
"failedToRemoveServer": "Server not found or failed to remove",
|
||||
"internalServerError": "Internal server error",
|
||||
"failedToGetServers": "Failed to get servers information",
|
||||
"failedToGetServerSettings": "Failed to get server settings",
|
||||
"failedToGetServerConfig": "Failed to get server configuration",
|
||||
"failedToSaveSettings": "Failed to save settings",
|
||||
"toolNameRequired": "Server name and tool name are required",
|
||||
"descriptionMustBeString": "Description must be a string",
|
||||
"groupIdRequired": "Group ID is required",
|
||||
"groupNameRequired": "Group name is required",
|
||||
"groupNotFound": "Group not found",
|
||||
"groupIdAndServerNameRequired": "Group ID and server name are required",
|
||||
"groupOrServerNotFound": "Group or server not found",
|
||||
"toolsMustBeAllOrArray": "Tools must be \"all\" or an array of strings",
|
||||
"serverNameAndToolNameRequired": "Server name and tool name are required",
|
||||
"usernameRequired": "Username is required",
|
||||
"userNotFound": "User not found",
|
||||
"failedToGetUsers": "Failed to get users information",
|
||||
"failedToGetUserInfo": "Failed to get user information",
|
||||
"failedToGetUserStats": "Failed to get user statistics",
|
||||
"marketServerNameRequired": "Server name is required",
|
||||
"marketServerNotFound": "Market server not found",
|
||||
"failedToGetMarketServers": "Failed to get market servers information",
|
||||
"failedToGetMarketServer": "Failed to get market server information",
|
||||
"failedToGetMarketCategories": "Failed to get market categories",
|
||||
"failedToGetMarketTags": "Failed to get market tags",
|
||||
"failedToSearchMarketServers": "Failed to search market servers",
|
||||
"failedToFilterMarketServers": "Failed to filter market servers",
|
||||
"failedToProcessDxtFile": "Failed to process DXT file"
|
||||
},
|
||||
"success": {
|
||||
"serverCreated": "Server created successfully",
|
||||
"serverUpdated": "Server updated successfully",
|
||||
"serverRemoved": "Server removed successfully",
|
||||
"serverToggled": "Server status toggled successfully",
|
||||
"toolToggled": "Tool {{name}} {{action}} successfully",
|
||||
"toolDescriptionUpdated": "Tool {{name}} description updated successfully",
|
||||
"systemConfigUpdated": "System configuration updated successfully",
|
||||
"groupCreated": "Group created successfully",
|
||||
"groupUpdated": "Group updated successfully",
|
||||
"groupDeleted": "Group deleted successfully",
|
||||
"serverAddedToGroup": "Server added to group successfully",
|
||||
"serverRemovedFromGroup": "Server removed from group successfully",
|
||||
"serverToolsUpdated": "Server tools updated successfully"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "MCP Hub 控制面板",
|
||||
"title": "MCPHub 控制面板",
|
||||
"error": "错误",
|
||||
"closeButton": "关闭",
|
||||
"noServers": "没有可用的 MCP 服务器",
|
||||
@@ -10,11 +10,11 @@
|
||||
"changePassword": "修改密码",
|
||||
"toggleSidebar": "切换侧边栏",
|
||||
"welcomeUser": "欢迎, {{username}}",
|
||||
"name": "MCP Hub"
|
||||
"name": "MCPHub"
|
||||
},
|
||||
"about": {
|
||||
"title": "关于",
|
||||
"versionInfo": "MCP Hub 版本: {{version}}",
|
||||
"versionInfo": "MCPHub 版本: {{version}}",
|
||||
"newVersion": "有新版本可用!",
|
||||
"currentVersion": "当前版本",
|
||||
"newVersionAvailable": "新版本 {{version}} 已发布",
|
||||
@@ -30,7 +30,7 @@
|
||||
"label": "赞助",
|
||||
"title": "支持项目",
|
||||
"rewardAlt": "赞赏码",
|
||||
"supportMessage": "通过捐赠支持 MCP Hub 的开发!",
|
||||
"supportMessage": "通过捐赠支持 MCPHub 的开发!",
|
||||
"supportButton": "在 Ko-fi 上支持"
|
||||
},
|
||||
"wechat": {
|
||||
@@ -52,7 +52,9 @@
|
||||
},
|
||||
"auth": {
|
||||
"login": "登录",
|
||||
"loginTitle": "登录 MCP Hub",
|
||||
"loginTitle": "登录 MCPHub",
|
||||
"slogan": "统一的 MCP 服务器管理平台",
|
||||
"subtitle": "模型上下文协议服务器的集中管理平台,通过灵活的路由策略组织、监控和扩展多个 MCP 服务器。",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"loggingIn": "登录中...",
|
||||
@@ -78,6 +80,7 @@
|
||||
"deleteWarning": "删除服务器 '{{name}}' 将会移除该服务器及其所有数据。此操作无法撤销。",
|
||||
"status": "状态",
|
||||
"tools": "工具",
|
||||
"prompts": "提示词",
|
||||
"name": "服务器名称",
|
||||
"url": "服务器 URL",
|
||||
"apiKey": "API 密钥",
|
||||
@@ -198,6 +201,7 @@
|
||||
"groups": "分组",
|
||||
"users": "用户",
|
||||
"market": "市场",
|
||||
"cloud": "云端市场",
|
||||
"logs": "日志"
|
||||
},
|
||||
"pages": {
|
||||
@@ -229,7 +233,7 @@
|
||||
"title": "用户管理"
|
||||
},
|
||||
"market": {
|
||||
"title": "服务器市场 - (数据来源于 mcpm.sh)"
|
||||
"title": "市场中心 - 本地市场和云端市场"
|
||||
},
|
||||
"logs": {
|
||||
"title": "系统日志"
|
||||
@@ -282,7 +286,7 @@
|
||||
"configureTools": "配置工具"
|
||||
},
|
||||
"market": {
|
||||
"title": "服务器市场",
|
||||
"title": "本地安装",
|
||||
"official": "官方",
|
||||
"by": "作者",
|
||||
"unknown": "未知",
|
||||
@@ -314,7 +318,7 @@
|
||||
"required": "必填",
|
||||
"example": "示例",
|
||||
"viewSchema": "查看结构",
|
||||
"fetchError": "获取服务器市场数据失败",
|
||||
"fetchError": "获取本地市场服务器数据失败",
|
||||
"serverNotFound": "未找到服务器",
|
||||
"searchError": "搜索服务器失败",
|
||||
"filterError": "按分类筛选服务器失败",
|
||||
@@ -325,6 +329,58 @@
|
||||
"confirmVariablesMessage": "请确保这些变量在运行环境中已正确定义。是否继续安装服务器?",
|
||||
"confirmAndInstall": "确认并安装"
|
||||
},
|
||||
"cloud": {
|
||||
"title": "云端支持",
|
||||
"subtitle": "由 MCPRouter 提供支持",
|
||||
"by": "作者",
|
||||
"server": "服务器",
|
||||
"config": "配置",
|
||||
"created": "创建时间",
|
||||
"updated": "更新时间",
|
||||
"available": "可用",
|
||||
"description": "描述",
|
||||
"details": "详细信息",
|
||||
"tools": "工具",
|
||||
"tool": "个工具",
|
||||
"toolsAvailable": "{{count}} 个工具可用",
|
||||
"loadingTools": "加载工具中...",
|
||||
"noTools": "该服务器没有可用工具",
|
||||
"noDescription": "无描述信息",
|
||||
"viewDetails": "查看详情",
|
||||
"parameters": "参数",
|
||||
"result": "结果",
|
||||
"error": "错误",
|
||||
"callTool": "调用",
|
||||
"calling": "调用中...",
|
||||
"toolCallSuccess": "工具 {{toolName}} 执行成功",
|
||||
"toolCallError": "调用工具 {{toolName}} 失败:{{error}}",
|
||||
"viewSchema": "查看结构",
|
||||
"backToList": "返回云端市场",
|
||||
"search": "搜索",
|
||||
"searchPlaceholder": "搜索云端服务器名称、标题或作者",
|
||||
"clearFilters": "清除筛选",
|
||||
"clearCategoryFilter": "清除",
|
||||
"clearTagFilter": "清除",
|
||||
"categories": "分类",
|
||||
"tags": "标签",
|
||||
"noCategories": "未找到分类",
|
||||
"noTags": "未找到标签",
|
||||
"noServers": "未找到云端服务器",
|
||||
"fetchError": "获取云端服务器失败",
|
||||
"serverNotFound": "未找到云端服务器",
|
||||
"searchError": "搜索云端服务器失败",
|
||||
"filterError": "按分类筛选云端服务器失败",
|
||||
"tagFilterError": "按标签筛选云端服务器失败",
|
||||
"showing": "显示 {{from}}-{{to}}/{{total}} 个云端服务器",
|
||||
"perPage": "每页显示",
|
||||
"apiKeyNotConfigured": "MCPRouter API 密钥未配置",
|
||||
"apiKeyNotConfiguredDescription": "要使用云端服务器,您需要配置 MCPRouter API 密钥。",
|
||||
"getApiKey": "获取 API 密钥",
|
||||
"configureInSettings": "在设置中配置",
|
||||
"installServer": "安装 {{name}}",
|
||||
"installSuccess": "服务器 {{name}} 安装成功",
|
||||
"installError": "安装服务器失败:{{error}}"
|
||||
},
|
||||
"tool": {
|
||||
"run": "运行",
|
||||
"running": "运行中...",
|
||||
@@ -360,6 +416,23 @@
|
||||
"addItem": "添加 {{key}} 项目",
|
||||
"enterKey": "输入 {{key}}"
|
||||
},
|
||||
"prompt": {
|
||||
"run": "获取",
|
||||
"running": "获取中...",
|
||||
"result": "提示词结果",
|
||||
"error": "提示词错误",
|
||||
"execution": "提示词执行",
|
||||
"successful": "成功",
|
||||
"failed": "失败",
|
||||
"errorDetails": "错误详情:",
|
||||
"noContent": "提示词执行成功但未返回内容。",
|
||||
"unknownError": "发生未知错误",
|
||||
"jsonResponse": "JSON 响应:",
|
||||
"description": "描述",
|
||||
"messages": "消息",
|
||||
"noDescription": "无描述信息",
|
||||
"runPromptWithName": "获取提示词: {{name}}"
|
||||
},
|
||||
"settings": {
|
||||
"enableGlobalRoute": "启用全局路由",
|
||||
"enableGlobalRouteDescription": "允许不指定组 ID 就连接到 /sse 端点",
|
||||
@@ -396,7 +469,20 @@
|
||||
"openaiApiEmbeddingModelPlaceholder": "text-embedding-3-small",
|
||||
"smartRoutingConfigUpdated": "智能路由配置更新成功",
|
||||
"smartRoutingRequiredFields": "启用智能路由需要填写数据库连接地址和 OpenAI API 密钥",
|
||||
"smartRoutingValidationError": "启用智能路由前请先填写必要字段:{{fields}}"
|
||||
"smartRoutingValidationError": "启用智能路由前请先填写必要字段:{{fields}}",
|
||||
"mcpRouterConfig": "云端市场",
|
||||
"mcpRouterApiKey": "MCPRouter API 密钥",
|
||||
"mcpRouterApiKeyDescription": "用于访问 MCPRouter 云端市场服务的 API 密钥",
|
||||
"mcpRouterApiKeyPlaceholder": "请输入 MCPRouter API 密钥",
|
||||
"mcpRouterReferer": "引用地址",
|
||||
"mcpRouterRefererDescription": "MCPRouter API 请求的引用地址头",
|
||||
"mcpRouterRefererPlaceholder": "https://mcphub.app",
|
||||
"mcpRouterTitle": "标题",
|
||||
"mcpRouterTitleDescription": "MCPRouter API 请求的标题头",
|
||||
"mcpRouterTitlePlaceholder": "MCPHub",
|
||||
"mcpRouterBaseUrl": "基础地址",
|
||||
"mcpRouterBaseUrlDescription": "MCPRouter API 的基础地址",
|
||||
"mcpRouterBaseUrlPlaceholder": "https://api.mcprouter.to/v1"
|
||||
},
|
||||
"dxt": {
|
||||
"upload": "上传",
|
||||
@@ -429,7 +515,7 @@
|
||||
"edit": "编辑用户",
|
||||
"delete": "删除用户",
|
||||
"create": "创建",
|
||||
"update": "用户",
|
||||
"update": "更新",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"newPassword": "新密码",
|
||||
@@ -458,5 +544,64 @@
|
||||
"deleteConfirmation": "您确定要删除用户 '{{username}}' 吗?此操作无法撤消。",
|
||||
"confirmDelete": "删除用户",
|
||||
"deleteWarning": "您确定要删除用户 '{{username}}' 吗?此操作无法撤消。"
|
||||
},
|
||||
"api": {
|
||||
"errors": {
|
||||
"readonly": "演示环境无法修改数据",
|
||||
"invalid_credentials": "用户名或密码错误",
|
||||
"serverNameRequired": "服务器名称是必需的",
|
||||
"serverConfigRequired": "服务器配置是必需的",
|
||||
"serverConfigInvalid": "服务器配置必须包含 URL、OpenAPI 规范 URL 或模式,或者带参数的命令",
|
||||
"serverTypeInvalid": "服务器类型必须是以下之一:stdio、sse、streamable-http、openapi",
|
||||
"urlRequiredForType": "{{type}} 服务器类型需要 URL",
|
||||
"openapiSpecRequired": "openapi 服务器类型需要 OpenAPI 规范 URL 或模式",
|
||||
"headersInvalidFormat": "请求头必须是对象格式",
|
||||
"headersNotSupportedForStdio": "stdio 服务器类型不支持请求头",
|
||||
"serverNotFound": "找不到服务器",
|
||||
"failedToRemoveServer": "找不到服务器或删除失败",
|
||||
"internalServerError": "服务器内部错误",
|
||||
"failedToGetServers": "获取服务器信息失败",
|
||||
"failedToGetServerSettings": "获取服务器设置失败",
|
||||
"failedToGetServerConfig": "获取服务器配置失败",
|
||||
"failedToSaveSettings": "保存设置失败",
|
||||
"toolNameRequired": "服务器名称和工具名称是必需的",
|
||||
"descriptionMustBeString": "描述必须是字符串",
|
||||
"groupIdRequired": "分组 ID 是必需的",
|
||||
"groupNameRequired": "分组名称是必需的",
|
||||
"groupNotFound": "找不到分组",
|
||||
"groupIdAndServerNameRequired": "分组 ID 和服务器名称是必需的",
|
||||
"groupOrServerNotFound": "找不到分组或服务器",
|
||||
"toolsMustBeAllOrArray": "工具必须是 \"all\" 或字符串数组",
|
||||
"serverNameAndToolNameRequired": "服务器名称和工具名称是必需的",
|
||||
"usernameRequired": "用户名是必需的",
|
||||
"userNotFound": "找不到用户",
|
||||
"failedToGetUsers": "获取用户信息失败",
|
||||
"failedToGetUserInfo": "获取用户信息失败",
|
||||
"failedToGetUserStats": "获取用户统计信息失败",
|
||||
"marketServerNameRequired": "服务器名称是必需的",
|
||||
"marketServerNotFound": "找不到市场服务器",
|
||||
"failedToGetMarketServers": "获取市场服务器信息失败",
|
||||
"failedToGetMarketServer": "获取市场服务器信息失败",
|
||||
"failedToGetMarketCategories": "获取市场类别失败",
|
||||
"failedToGetMarketTags": "获取市场标签失败",
|
||||
"failedToSearchMarketServers": "搜索市场服务器失败",
|
||||
"failedToFilterMarketServers": "过滤市场服务器失败",
|
||||
"failedToProcessDxtFile": "处理 DXT 文件失败"
|
||||
},
|
||||
"success": {
|
||||
"serverCreated": "服务器创建成功",
|
||||
"serverUpdated": "服务器更新成功",
|
||||
"serverRemoved": "服务器删除成功",
|
||||
"serverToggled": "服务器状态切换成功",
|
||||
"toolToggled": "工具 {{name}} {{action}} 成功",
|
||||
"toolDescriptionUpdated": "工具 {{name}} 描述更新成功",
|
||||
"systemConfigUpdated": "系统配置更新成功",
|
||||
"groupCreated": "分组创建成功",
|
||||
"groupUpdated": "分组更新成功",
|
||||
"groupDeleted": "分组删除成功",
|
||||
"serverAddedToGroup": "服务器添加到分组成功",
|
||||
"serverRemovedFromGroup": "服务器从分组移除成功",
|
||||
"serverToolsUpdated": "服务器工具更新成功"
|
||||
}
|
||||
}
|
||||
}
|
||||
13253
package-lock.json
generated
Normal file
13253
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -46,7 +46,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "^11.0.1",
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
"@modelcontextprotocol/sdk": "^1.17.2",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/multer": "^1.4.13",
|
||||
"@types/pg": "^8.15.2",
|
||||
@@ -57,6 +57,7 @@
|
||||
"dotenv-expand": "^12.0.2",
|
||||
"express": "^4.21.2",
|
||||
"express-validator": "^7.2.1",
|
||||
"i18next-fs-backend": "^2.6.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"multer": "^2.0.1",
|
||||
"openai": "^4.103.0",
|
||||
@@ -74,6 +75,7 @@
|
||||
"@shadcn/ui": "^0.0.4",
|
||||
"@swc/core": "^1.13.0",
|
||||
"@swc/jest": "^0.2.39",
|
||||
"@tailwindcss/line-clamp": "^0.4.4",
|
||||
"@tailwindcss/postcss": "^4.1.3",
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
|
||||
31
pnpm-lock.yaml
generated
31
pnpm-lock.yaml
generated
@@ -12,8 +12,8 @@ importers:
|
||||
specifier: ^11.0.1
|
||||
version: 11.0.1(openapi-types@12.1.3)
|
||||
'@modelcontextprotocol/sdk':
|
||||
specifier: ^1.12.1
|
||||
version: 1.13.2
|
||||
specifier: ^1.17.2
|
||||
version: 1.17.2
|
||||
'@types/adm-zip':
|
||||
specifier: ^0.5.7
|
||||
version: 0.5.7
|
||||
@@ -44,6 +44,9 @@ importers:
|
||||
express-validator:
|
||||
specifier: ^7.2.1
|
||||
version: 7.2.1
|
||||
i18next-fs-backend:
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.0
|
||||
jsonwebtoken:
|
||||
specifier: ^9.0.2
|
||||
version: 9.0.2
|
||||
@@ -90,6 +93,9 @@ importers:
|
||||
'@swc/jest':
|
||||
specifier: ^0.2.39
|
||||
version: 0.2.39(@swc/core@1.13.0)
|
||||
'@tailwindcss/line-clamp':
|
||||
specifier: ^0.4.4
|
||||
version: 0.4.4(tailwindcss@4.1.11)
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4.1.3
|
||||
version: 4.1.11
|
||||
@@ -1015,8 +1021,8 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.9':
|
||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||
|
||||
'@modelcontextprotocol/sdk@1.13.2':
|
||||
resolution: {integrity: sha512-Vx7qOcmoKkR3qhaQ9qf3GxiVKCEu+zfJddHv6x3dY/9P6+uIwJnmuAur5aB+4FDXf41rRrDnOEGkviX5oYZ67w==}
|
||||
'@modelcontextprotocol/sdk@1.17.2':
|
||||
resolution: {integrity: sha512-EFLRNXR/ixpXQWu6/3Cu30ndDFIFNaqUXcTqsGebujeMan9FzhAaFFswLRiFj61rgygDRr8WO1N+UijjgRxX9g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@next/env@15.3.4':
|
||||
@@ -1442,6 +1448,11 @@ packages:
|
||||
'@swc/types@0.1.23':
|
||||
resolution: {integrity: sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==}
|
||||
|
||||
'@tailwindcss/line-clamp@0.4.4':
|
||||
resolution: {integrity: sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g==}
|
||||
peerDependencies:
|
||||
tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1'
|
||||
|
||||
'@tailwindcss/node@4.1.11':
|
||||
resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==}
|
||||
|
||||
@@ -2669,6 +2680,9 @@ packages:
|
||||
i18next-browser-languagedetector@8.2.0:
|
||||
resolution: {integrity: sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==}
|
||||
|
||||
i18next-fs-backend@2.6.0:
|
||||
resolution: {integrity: sha512-3ZlhNoF9yxnM8pa8bWp5120/Ob6t4lVl1l/tbLmkml/ei3ud8IWySCHt2lrY5xWRlSU5D9IV2sm5bEbGuTqwTw==}
|
||||
|
||||
i18next@24.2.3:
|
||||
resolution: {integrity: sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==}
|
||||
peerDependencies:
|
||||
@@ -5144,13 +5158,14 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.3
|
||||
|
||||
'@modelcontextprotocol/sdk@1.13.2':
|
||||
'@modelcontextprotocol/sdk@1.17.2':
|
||||
dependencies:
|
||||
ajv: 6.12.6
|
||||
content-type: 1.0.5
|
||||
cors: 2.8.5
|
||||
cross-spawn: 7.0.6
|
||||
eventsource: 3.0.6
|
||||
eventsource-parser: 3.0.1
|
||||
express: 5.1.0
|
||||
express-rate-limit: 7.5.0(express@5.1.0)
|
||||
pkce-challenge: 5.0.0
|
||||
@@ -5480,6 +5495,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@swc/counter': 0.1.3
|
||||
|
||||
'@tailwindcss/line-clamp@0.4.4(tailwindcss@4.1.11)':
|
||||
dependencies:
|
||||
tailwindcss: 4.1.11
|
||||
|
||||
'@tailwindcss/node@4.1.11':
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
@@ -6902,6 +6921,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.0
|
||||
|
||||
i18next-fs-backend@2.6.0: {}
|
||||
|
||||
i18next@24.2.3(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import dotenv from 'dotenv';
|
||||
import fs from 'fs';
|
||||
import { McpSettings } from '../types/index.js';
|
||||
import { McpSettings, IUser } from '../types/index.js';
|
||||
import { getConfigFilePath } from '../utils/path.js';
|
||||
import { getPackageVersion } from '../utils/version.js';
|
||||
import { getDataService } from '../services/services.js';
|
||||
@@ -11,8 +11,8 @@ dotenv.config();
|
||||
const defaultConfig = {
|
||||
port: process.env.PORT || 3000,
|
||||
initTimeout: process.env.INIT_TIMEOUT || 300000,
|
||||
timeout: process.env.REQUEST_TIMEOUT || 60000,
|
||||
basePath: process.env.BASE_PATH || '',
|
||||
readonly: 'true' === process.env.READONLY || false,
|
||||
mcpHubName: 'mcphub',
|
||||
mcpHubVersion: getPackageVersion(),
|
||||
};
|
||||
@@ -53,14 +53,14 @@ export const loadOriginalSettings = (): McpSettings => {
|
||||
}
|
||||
};
|
||||
|
||||
export const loadSettings = (): McpSettings => {
|
||||
return dataService.filterSettings!(loadOriginalSettings());
|
||||
export const loadSettings = (user?: IUser): McpSettings => {
|
||||
return dataService.filterSettings!(loadOriginalSettings(), user);
|
||||
};
|
||||
|
||||
export const saveSettings = (settings: McpSettings): boolean => {
|
||||
export const saveSettings = (settings: McpSettings, user?: IUser): boolean => {
|
||||
const settingsPath = getSettingsPath();
|
||||
try {
|
||||
const mergedSettings = dataService.mergeSettings!(loadOriginalSettings(), settings);
|
||||
const mergedSettings = dataService.mergeSettings!(loadOriginalSettings(), settings, user);
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2), 'utf8');
|
||||
|
||||
// Update cache after successful save
|
||||
|
||||
13
src/config/jwt.ts
Normal file
13
src/config/jwt.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
let jwtSecret = process.env.JWT_SECRET;
|
||||
if (!jwtSecret) {
|
||||
jwtSecret = crypto.randomBytes(32).toString('hex');
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.warn(
|
||||
'Warning: JWT_SECRET is not set. Using a temporary secret. Please set a strong, persistent secret in your environment variables for production.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const JWT_SECRET = jwtSecret;
|
||||
@@ -9,19 +9,25 @@ import {
|
||||
} from '../models/User.js';
|
||||
import { getDataService } from '../services/services.js';
|
||||
import { DataService } from '../services/dataService.js';
|
||||
import { JWT_SECRET } from '../config/jwt.js';
|
||||
|
||||
const dataService: DataService = getDataService();
|
||||
|
||||
// Default secret key - in production, use an environment variable
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-this';
|
||||
const TOKEN_EXPIRY = '24h';
|
||||
|
||||
// Login user
|
||||
export const login = async (req: Request, res: Response): Promise<void> => {
|
||||
// Get translation function from request
|
||||
const t = (req as any).t;
|
||||
|
||||
// Validate request
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({ success: false, errors: errors.array() });
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: t('api.errors.validation_failed'),
|
||||
errors: errors.array(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -32,7 +38,10 @@ export const login = async (req: Request, res: Response): Promise<void> => {
|
||||
const user = findUserByUsername(username);
|
||||
|
||||
if (!user) {
|
||||
res.status(401).json({ success: false, message: 'Invalid credentials' });
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
message: t('api.errors.invalid_credentials'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -40,7 +49,10 @@ export const login = async (req: Request, res: Response): Promise<void> => {
|
||||
const isPasswordValid = await verifyPassword(password, user.password);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
res.status(401).json({ success: false, message: 'Invalid credentials' });
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
message: t('api.errors.invalid_credentials'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -56,6 +68,7 @@ export const login = async (req: Request, res: Response): Promise<void> => {
|
||||
if (err) throw err;
|
||||
res.json({
|
||||
success: true,
|
||||
message: t('api.success.login_successful'),
|
||||
token,
|
||||
user: {
|
||||
username: user.username,
|
||||
@@ -66,16 +79,26 @@ export const login = async (req: Request, res: Response): Promise<void> => {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
res.status(500).json({ success: false, message: 'Server error' });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: t('api.errors.server_error'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Register new user
|
||||
export const register = async (req: Request, res: Response): Promise<void> => {
|
||||
// Get translation function from request
|
||||
const t = (req as any).t;
|
||||
|
||||
// Validate request
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({ success: false, errors: errors.array() });
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: t('api.errors.validation_failed'),
|
||||
errors: errors.array(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
273
src/controllers/cloudController.ts
Normal file
273
src/controllers/cloudController.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { ApiResponse, CloudServer, CloudTool } from '../types/index.js';
|
||||
import {
|
||||
getCloudServers,
|
||||
getCloudServerByName,
|
||||
getCloudServerTools,
|
||||
callCloudServerTool,
|
||||
getCloudCategories,
|
||||
getCloudTags,
|
||||
searchCloudServers,
|
||||
filterCloudServersByCategory,
|
||||
filterCloudServersByTag,
|
||||
} from '../services/cloudService.js';
|
||||
|
||||
// Get all cloud market servers
|
||||
export const getAllCloudServers = async (_: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const cloudServers = await getCloudServers();
|
||||
const response: ApiResponse<CloudServer[]> = {
|
||||
success: true,
|
||||
data: cloudServers,
|
||||
};
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
console.error('Error getting cloud market servers:', error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to get cloud market servers';
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get a specific cloud market server by name
|
||||
export const getCloudServer = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { name } = req.params;
|
||||
if (!name) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Server name is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const server = await getCloudServerByName(name);
|
||||
if (!server) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: 'Cloud server not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch tools for this server
|
||||
try {
|
||||
const tools = await getCloudServerTools(server.server_key);
|
||||
server.tools = tools;
|
||||
} catch (toolError) {
|
||||
console.warn(`Failed to fetch tools for server ${server.name}:`, toolError);
|
||||
// Continue without tools
|
||||
}
|
||||
|
||||
const response: ApiResponse<CloudServer> = {
|
||||
success: true,
|
||||
data: server,
|
||||
};
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
console.error('Error getting cloud market server:', error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to get cloud market server';
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get all cloud market categories
|
||||
export const getAllCloudCategories = async (_: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const categories = await getCloudCategories();
|
||||
const response: ApiResponse<string[]> = {
|
||||
success: true,
|
||||
data: categories,
|
||||
};
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
console.error('Error getting cloud market categories:', error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to get cloud market categories';
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get all cloud market tags
|
||||
export const getAllCloudTags = async (_: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const tags = await getCloudTags();
|
||||
const response: ApiResponse<string[]> = {
|
||||
success: true,
|
||||
data: tags,
|
||||
};
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
console.error('Error getting cloud market tags:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to get cloud market tags';
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Search cloud market servers
|
||||
export const searchCloudServersByQuery = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const query = req.query.query as string;
|
||||
if (!query || query.trim() === '') {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Search query is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const servers = await searchCloudServers(query);
|
||||
const response: ApiResponse<CloudServer[]> = {
|
||||
success: true,
|
||||
data: servers,
|
||||
};
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
console.error('Error searching cloud market servers:', error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to search cloud market servers';
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get cloud market servers by category
|
||||
export const getCloudServersByCategory = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { category } = req.params;
|
||||
if (!category) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Category is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const servers = await filterCloudServersByCategory(category);
|
||||
const response: ApiResponse<CloudServer[]> = {
|
||||
success: true,
|
||||
data: servers,
|
||||
};
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
console.error('Error getting cloud market servers by category:', error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to get cloud market servers by category';
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get cloud market servers by tag
|
||||
export const getCloudServersByTag = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { tag } = req.params;
|
||||
if (!tag) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Tag is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const servers = await filterCloudServersByTag(tag);
|
||||
const response: ApiResponse<CloudServer[]> = {
|
||||
success: true,
|
||||
data: servers,
|
||||
};
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
console.error('Error getting cloud market servers by tag:', error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to get cloud market servers by tag';
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get tools for a specific cloud server
|
||||
export const getCloudServerToolsList = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { serverName } = req.params;
|
||||
if (!serverName) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Server name is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const tools = await getCloudServerTools(serverName);
|
||||
const response: ApiResponse<CloudTool[]> = {
|
||||
success: true,
|
||||
data: tools,
|
||||
};
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
console.error('Error getting cloud server tools:', error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to get cloud server tools';
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Call a tool on a cloud server
|
||||
export const callCloudTool = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { serverName, toolName } = req.params;
|
||||
const { arguments: args } = req.body;
|
||||
|
||||
if (!serverName) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Server name is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!toolName) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Tool name is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await callCloudServerTool(serverName, toolName, args || {});
|
||||
const response: ApiResponse = {
|
||||
success: true,
|
||||
data: result,
|
||||
};
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
console.error('Error calling cloud server tool:', error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to call cloud server tool';
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user