mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-05-19 04:00:16 +09:00
feat: MCP market
This commit is contained in:
parent
0c14ce6417
commit
7d51bfd42e
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
require("../polyfill");
|
require("../polyfill");
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import styles from "./home.module.scss";
|
import styles from "./home.module.scss";
|
||||||
|
|
||||||
import BotIcon from "../icons/bot.svg";
|
import BotIcon from "../icons/bot.svg";
|
||||||
@ -18,8 +18,8 @@ import { getISOLang, getLang } from "../locales";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
HashRouter as Router,
|
HashRouter as Router,
|
||||||
Routes,
|
|
||||||
Route,
|
Route,
|
||||||
|
Routes,
|
||||||
useLocation,
|
useLocation,
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import { SideBar } from "./sidebar";
|
import { SideBar } from "./sidebar";
|
||||||
@ -74,6 +74,13 @@ const Sd = dynamic(async () => (await import("./sd")).Sd, {
|
|||||||
loading: () => <Loading noLogo />,
|
loading: () => <Loading noLogo />,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const McpMarketPage = dynamic(
|
||||||
|
async () => (await import("./mcp-market")).McpMarketPage,
|
||||||
|
{
|
||||||
|
loading: () => <Loading noLogo />,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export function useSwitchTheme() {
|
export function useSwitchTheme() {
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
|
|
||||||
@ -193,6 +200,7 @@ function Screen() {
|
|||||||
<Route path={Path.SearchChat} element={<SearchChat />} />
|
<Route path={Path.SearchChat} element={<SearchChat />} />
|
||||||
<Route path={Path.Chat} element={<Chat />} />
|
<Route path={Path.Chat} element={<Chat />} />
|
||||||
<Route path={Path.Settings} element={<Settings />} />
|
<Route path={Path.Settings} element={<Settings />} />
|
||||||
|
<Route path={Path.McpMarket} element={<McpMarketPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</WindowContent>
|
</WindowContent>
|
||||||
</>
|
</>
|
||||||
|
612
app/components/mcp-market.module.scss
Normal file
612
app/components/mcp-market.module.scss
Normal file
@ -0,0 +1,612 @@
|
|||||||
|
@import "../styles/animation.scss";
|
||||||
|
|
||||||
|
.mcp-market-page {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.loading-indicator {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--primary);
|
||||||
|
margin-left: 8px;
|
||||||
|
font-weight: normal;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-market-page-body {
|
||||||
|
padding: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.mcp-market-filter {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
animation: slide-in ease 0.3s;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
flex-grow: 1;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-market-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 20px;
|
||||||
|
border: var(--border-in-light);
|
||||||
|
animation: slide-in ease 0.3s;
|
||||||
|
background-color: var(--white);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom-left-radius: 10px;
|
||||||
|
border-bottom-right-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-market-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.mcp-market-title {
|
||||||
|
.mcp-market-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.server-status {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-left: 8px;
|
||||||
|
background-color: #10b981;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
background-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.waiting {
|
||||||
|
background-color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.9;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-market-info {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--black-50);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-market-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
:global(.icon-button) {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.action-primary {
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
filter: brightness(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--primary);
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.action-warning {
|
||||||
|
background-color: var(--warning);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
filter: brightness(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--warning);
|
||||||
|
border-color: var(--warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.action-danger {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--danger);
|
||||||
|
border-color: var(--danger);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--danger);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
filter: brightness(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.action-error {
|
||||||
|
color: #ef4444 !important;
|
||||||
|
border-color: #ef4444 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.mcp-market-actions {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.array-input {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: var(--white);
|
||||||
|
|
||||||
|
.array-input-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: var(--gray-50);
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 13px;
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--gray-100);
|
||||||
|
border-color: var(--gray-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: var(--white);
|
||||||
|
border-color: var(--primary);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px var(--primary-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--gray-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.icon-button) {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--gray-100);
|
||||||
|
border-color: var(--gray-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.icon-button.add-path-button) {
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: white;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
height: 36px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 4px;
|
||||||
|
filter: brightness(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-list {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.path-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: var(--border-in-light);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: var(--white);
|
||||||
|
color: var(--black);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--gray-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px var(--primary-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--gray-300) !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.browse-button {
|
||||||
|
padding: 8px;
|
||||||
|
border: var(--border-in-light);
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--black-50);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--primary);
|
||||||
|
color: var(--primary);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-button {
|
||||||
|
padding: 8px;
|
||||||
|
border: var(--border-in-light);
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--black-50);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--danger);
|
||||||
|
color: var(--danger);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-button {
|
||||||
|
align-self: flex-start;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: transparent;
|
||||||
|
border: var(--border-in-light);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: var(--black);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--primary);
|
||||||
|
color: var(--primary);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.config-header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.config-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--black);
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-description {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--gray-500);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.array-input {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: var(--white);
|
||||||
|
|
||||||
|
.array-input-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: var(--gray-50);
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 13px;
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--gray-100);
|
||||||
|
border-color: var(--gray-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: var(--white);
|
||||||
|
border-color: var(--primary);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px var(--primary-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--gray-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.icon-button) {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--gray-100);
|
||||||
|
border-color: var(--gray-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.icon-button.add-path-button) {
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: white;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
height: 36px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 4px;
|
||||||
|
filter: brightness(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-item {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: var(--border-in-light);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: var(--white);
|
||||||
|
color: var(--black);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--gray-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px var(--primary-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--gray-300) !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.primitives-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
word-break: break-word;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.primitive-item {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.primitive-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--black);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding-left: 12px;
|
||||||
|
border-left: 3px solid var(--primary);
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primitive-description {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--gray-500);
|
||||||
|
line-height: 1.6;
|
||||||
|
padding-left: 15px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.modal-content {
|
||||||
|
margin-top: 20px;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background-color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.list-header {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.list-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: capitalize;
|
||||||
|
color: var(--black);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-sub-title {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--gray-500);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
564
app/components/mcp-market.tsx
Normal file
564
app/components/mcp-market.tsx
Normal file
@ -0,0 +1,564 @@
|
|||||||
|
import { IconButton } from "./button";
|
||||||
|
import { ErrorBoundary } from "./error";
|
||||||
|
import styles from "./mcp-market.module.scss";
|
||||||
|
import EditIcon from "../icons/edit.svg";
|
||||||
|
import AddIcon from "../icons/add.svg";
|
||||||
|
import CloseIcon from "../icons/close.svg";
|
||||||
|
import DeleteIcon from "../icons/delete.svg";
|
||||||
|
import RestartIcon from "../icons/reload.svg";
|
||||||
|
import EyeIcon from "../icons/eye.svg";
|
||||||
|
import { List, ListItem, Modal, showToast } from "./ui-lib";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import presetServersJson from "../mcp/preset-server.json";
|
||||||
|
const presetServers = presetServersJson as PresetServer[];
|
||||||
|
import {
|
||||||
|
getMcpConfig,
|
||||||
|
updateMcpConfig,
|
||||||
|
getClientPrimitives,
|
||||||
|
restartAllClients,
|
||||||
|
reinitializeMcpClients,
|
||||||
|
getClientErrors,
|
||||||
|
} from "../mcp/actions";
|
||||||
|
import { McpConfig, PresetServer, ServerConfig } from "../mcp/types";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
interface ConfigProperty {
|
||||||
|
type: string;
|
||||||
|
description?: string;
|
||||||
|
required?: boolean;
|
||||||
|
minItems?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function McpMarketPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
|
const [config, setConfig] = useState<McpConfig>({ mcpServers: {} });
|
||||||
|
const [editingServerId, setEditingServerId] = useState<string | undefined>();
|
||||||
|
const [viewingServerId, setViewingServerId] = useState<string | undefined>();
|
||||||
|
const [primitives, setPrimitives] = useState<any[]>([]);
|
||||||
|
const [userConfig, setUserConfig] = useState<Record<string, any>>({});
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [clientErrors, setClientErrors] = useState<
|
||||||
|
Record<string, string | null>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
// 更新服务器状态
|
||||||
|
const updateServerStatus = async () => {
|
||||||
|
await reinitializeMcpClients();
|
||||||
|
const errors = await getClientErrors();
|
||||||
|
setClientErrors(errors);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始加载配置
|
||||||
|
useEffect(() => {
|
||||||
|
const init = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const data = await getMcpConfig();
|
||||||
|
setConfig(data);
|
||||||
|
await updateServerStatus();
|
||||||
|
} catch (error) {
|
||||||
|
showToast("Failed to load configuration");
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
init();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
const saveConfig = async (newConfig: McpConfig) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await updateMcpConfig(newConfig);
|
||||||
|
setConfig(newConfig);
|
||||||
|
await updateServerStatus();
|
||||||
|
showToast("Configuration saved successfully");
|
||||||
|
} catch (error) {
|
||||||
|
showToast("Failed to save configuration");
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查服务器是否已添加
|
||||||
|
const isServerAdded = (id: string) => {
|
||||||
|
return id in config.mcpServers;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载当前编辑服务器的配置
|
||||||
|
useEffect(() => {
|
||||||
|
if (editingServerId) {
|
||||||
|
const currentConfig = config.mcpServers[editingServerId];
|
||||||
|
if (currentConfig) {
|
||||||
|
// 从当前配置中提取用户配置
|
||||||
|
const preset = presetServers.find((s) => s.id === editingServerId);
|
||||||
|
if (preset?.configSchema) {
|
||||||
|
const userConfig: Record<string, any> = {};
|
||||||
|
Object.entries(preset.argsMapping || {}).forEach(([key, mapping]) => {
|
||||||
|
if (mapping.type === "spread") {
|
||||||
|
// 对于 spread 类型,从 args 中提取数组
|
||||||
|
const startPos = mapping.position ?? 0;
|
||||||
|
userConfig[key] = currentConfig.args.slice(startPos);
|
||||||
|
} else if (mapping.type === "single") {
|
||||||
|
// 对于 single 类型,获取单个值
|
||||||
|
userConfig[key] = currentConfig.args[mapping.position ?? 0];
|
||||||
|
} else if (
|
||||||
|
mapping.type === "env" &&
|
||||||
|
mapping.key &&
|
||||||
|
currentConfig.env
|
||||||
|
) {
|
||||||
|
// 对于 env 类型,从环境变量中获取值
|
||||||
|
userConfig[key] = currentConfig.env[mapping.key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setUserConfig(userConfig);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setUserConfig({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [editingServerId, config.mcpServers]);
|
||||||
|
|
||||||
|
// 保存服务器配置
|
||||||
|
const saveServerConfig = async () => {
|
||||||
|
const preset = presetServers.find((s) => s.id === editingServerId);
|
||||||
|
if (!preset || !preset.configSchema || !editingServerId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 构建服务器配置
|
||||||
|
const args = [...preset.baseArgs];
|
||||||
|
const env: Record<string, string> = {};
|
||||||
|
|
||||||
|
Object.entries(preset.argsMapping || {}).forEach(([key, mapping]) => {
|
||||||
|
const value = userConfig[key];
|
||||||
|
if (mapping.type === "spread" && Array.isArray(value)) {
|
||||||
|
const pos = mapping.position ?? 0;
|
||||||
|
args.splice(pos, 0, ...value);
|
||||||
|
} else if (
|
||||||
|
mapping.type === "single" &&
|
||||||
|
mapping.position !== undefined
|
||||||
|
) {
|
||||||
|
args[mapping.position] = value;
|
||||||
|
} else if (
|
||||||
|
mapping.type === "env" &&
|
||||||
|
mapping.key &&
|
||||||
|
typeof value === "string"
|
||||||
|
) {
|
||||||
|
env[mapping.key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const serverConfig: ServerConfig = {
|
||||||
|
command: preset.command,
|
||||||
|
args,
|
||||||
|
...(Object.keys(env).length > 0 ? { env } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新配置
|
||||||
|
const newConfig = {
|
||||||
|
...config,
|
||||||
|
mcpServers: {
|
||||||
|
...config.mcpServers,
|
||||||
|
[editingServerId]: serverConfig,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await saveConfig(newConfig);
|
||||||
|
setEditingServerId(undefined);
|
||||||
|
showToast("Server configuration saved successfully");
|
||||||
|
} catch (error) {
|
||||||
|
showToast(
|
||||||
|
error instanceof Error ? error.message : "Failed to save configuration",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染配置表单
|
||||||
|
const renderConfigForm = () => {
|
||||||
|
const preset = presetServers.find((s) => s.id === editingServerId);
|
||||||
|
if (!preset?.configSchema) return null;
|
||||||
|
|
||||||
|
return Object.entries(preset.configSchema.properties).map(
|
||||||
|
([key, prop]: [string, ConfigProperty]) => {
|
||||||
|
if (prop.type === "array") {
|
||||||
|
const currentValue = userConfig[key as keyof typeof userConfig] || [];
|
||||||
|
return (
|
||||||
|
<ListItem key={key} title={key} subTitle={prop.description}>
|
||||||
|
<div className={styles["path-list"]}>
|
||||||
|
{(currentValue as string[]).map(
|
||||||
|
(value: string, index: number) => (
|
||||||
|
<div key={index} className={styles["path-item"]}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
placeholder={`Path ${index + 1}`}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = [...currentValue] as string[];
|
||||||
|
newValue[index] = e.target.value;
|
||||||
|
setUserConfig({ ...userConfig, [key]: newValue });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={<DeleteIcon />}
|
||||||
|
className={styles["delete-button"]}
|
||||||
|
onClick={() => {
|
||||||
|
const newValue = [...currentValue] as string[];
|
||||||
|
newValue.splice(index, 1);
|
||||||
|
setUserConfig({ ...userConfig, [key]: newValue });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
icon={<AddIcon />}
|
||||||
|
text="Add Path"
|
||||||
|
className={styles["add-button"]}
|
||||||
|
bordered
|
||||||
|
onClick={() => {
|
||||||
|
const newValue = [...currentValue, ""] as string[];
|
||||||
|
setUserConfig({ ...userConfig, [key]: newValue });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
} else if (prop.type === "string") {
|
||||||
|
const currentValue = userConfig[key as keyof typeof userConfig] || "";
|
||||||
|
return (
|
||||||
|
<ListItem key={key} title={key} subTitle={prop.description}>
|
||||||
|
<div className={styles["input-item"]}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={currentValue}
|
||||||
|
placeholder={`Enter ${key}`}
|
||||||
|
onChange={(e) => {
|
||||||
|
setUserConfig({ ...userConfig, [key]: e.target.value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取服务器的 Primitives
|
||||||
|
const loadPrimitives = async (id: string) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const result = await getClientPrimitives(id);
|
||||||
|
if (result) {
|
||||||
|
setPrimitives(result);
|
||||||
|
} else {
|
||||||
|
showToast("Server is not running");
|
||||||
|
setPrimitives([]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showToast("Failed to load primitives");
|
||||||
|
console.error(error);
|
||||||
|
setPrimitives([]);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重启所有客户端
|
||||||
|
const handleRestart = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await restartAllClients();
|
||||||
|
await updateServerStatus();
|
||||||
|
showToast("All clients restarted successfully");
|
||||||
|
} catch (error) {
|
||||||
|
showToast("Failed to restart clients");
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加服务器
|
||||||
|
const addServer = async (preset: PresetServer) => {
|
||||||
|
if (!preset.configurable) {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
showToast("Creating MCP client...");
|
||||||
|
// 如果服务器不需要配置,直接添加
|
||||||
|
const serverConfig: ServerConfig = {
|
||||||
|
command: preset.command,
|
||||||
|
args: [...preset.baseArgs],
|
||||||
|
};
|
||||||
|
const newConfig = {
|
||||||
|
...config,
|
||||||
|
mcpServers: {
|
||||||
|
...config.mcpServers,
|
||||||
|
[preset.id]: serverConfig,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await saveConfig(newConfig);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果需要配置,打开配置对话框
|
||||||
|
setEditingServerId(preset.id);
|
||||||
|
setUserConfig({});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 移除服务器
|
||||||
|
const removeServer = async (id: string) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const { [id]: _, ...rest } = config.mcpServers;
|
||||||
|
const newConfig = {
|
||||||
|
...config,
|
||||||
|
mcpServers: rest,
|
||||||
|
};
|
||||||
|
await saveConfig(newConfig);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<div className={styles["mcp-market-page"]}>
|
||||||
|
<div className="window-header">
|
||||||
|
<div className="window-header-title">
|
||||||
|
<div className="window-header-main-title">
|
||||||
|
MCP Market
|
||||||
|
{isLoading && (
|
||||||
|
<span className={styles["loading-indicator"]}>Loading...</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="window-header-sub-title">
|
||||||
|
{Object.keys(config.mcpServers).length} servers configured
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="window-actions">
|
||||||
|
<div className="window-action-button">
|
||||||
|
<IconButton
|
||||||
|
icon={<RestartIcon />}
|
||||||
|
bordered
|
||||||
|
onClick={handleRestart}
|
||||||
|
text="Restart"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="window-action-button">
|
||||||
|
<IconButton
|
||||||
|
icon={<CloseIcon />}
|
||||||
|
bordered
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles["mcp-market-page-body"]}>
|
||||||
|
<div className={styles["mcp-market-filter"]}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles["search-bar"]}
|
||||||
|
placeholder={"Search MCP Server"}
|
||||||
|
autoFocus
|
||||||
|
onInput={(e) => setSearchText(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles["server-list"]}>
|
||||||
|
{presetServers
|
||||||
|
.filter(
|
||||||
|
(m) =>
|
||||||
|
searchText.length === 0 ||
|
||||||
|
m.name.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||||
|
m.description
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.toLowerCase()),
|
||||||
|
)
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aAdded = isServerAdded(a.id);
|
||||||
|
const bAdded = isServerAdded(b.id);
|
||||||
|
const aError = clientErrors[a.id] !== null;
|
||||||
|
const bError = clientErrors[b.id] !== null;
|
||||||
|
|
||||||
|
if (aAdded !== bAdded) {
|
||||||
|
return aAdded ? -1 : 1;
|
||||||
|
}
|
||||||
|
if (aAdded && bAdded) {
|
||||||
|
if (aError !== bError) {
|
||||||
|
return aError ? -1 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
|
.map((server) => (
|
||||||
|
<div
|
||||||
|
className={clsx(styles["mcp-market-item"], {
|
||||||
|
[styles["disabled"]]: isLoading,
|
||||||
|
})}
|
||||||
|
key={server.id}
|
||||||
|
>
|
||||||
|
<div className={styles["mcp-market-header"]}>
|
||||||
|
<div className={styles["mcp-market-title"]}>
|
||||||
|
<div className={styles["mcp-market-name"]}>
|
||||||
|
{server.name}
|
||||||
|
{isServerAdded(server.id) && (
|
||||||
|
<span
|
||||||
|
className={clsx(styles["server-status"], {
|
||||||
|
[styles["error"]]:
|
||||||
|
clientErrors[server.id] !== null,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{clientErrors[server.id] === null
|
||||||
|
? "Active"
|
||||||
|
: "Error"}
|
||||||
|
{clientErrors[server.id] && (
|
||||||
|
<span className={styles["error-message"]}>
|
||||||
|
: {clientErrors[server.id]}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={clsx(styles["mcp-market-info"], "one-line")}
|
||||||
|
>
|
||||||
|
{server.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles["mcp-market-actions"]}>
|
||||||
|
{isServerAdded(server.id) ? (
|
||||||
|
<>
|
||||||
|
{server.configurable && (
|
||||||
|
<IconButton
|
||||||
|
icon={<EditIcon />}
|
||||||
|
text="Configure"
|
||||||
|
className={clsx({
|
||||||
|
[styles["action-error"]]:
|
||||||
|
clientErrors[server.id] !== null,
|
||||||
|
})}
|
||||||
|
onClick={() => setEditingServerId(server.id)}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isServerAdded(server.id) && (
|
||||||
|
<IconButton
|
||||||
|
icon={<EyeIcon />}
|
||||||
|
text="Detail"
|
||||||
|
onClick={async () => {
|
||||||
|
if (clientErrors[server.id] !== null) {
|
||||||
|
showToast("Server is not running");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setViewingServerId(server.id);
|
||||||
|
await loadPrimitives(server.id);
|
||||||
|
}}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
icon={<DeleteIcon />}
|
||||||
|
text="Remove"
|
||||||
|
className={styles["action-danger"]}
|
||||||
|
onClick={() => removeServer(server.id)}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<IconButton
|
||||||
|
icon={<AddIcon />}
|
||||||
|
text="Add"
|
||||||
|
className={styles["action-primary"]}
|
||||||
|
onClick={() => addServer(server)}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{editingServerId && (
|
||||||
|
<div className="modal-mask">
|
||||||
|
<Modal
|
||||||
|
title={`Configure Server - ${editingServerId}`}
|
||||||
|
onClose={() => !isLoading && setEditingServerId(undefined)}
|
||||||
|
actions={[
|
||||||
|
<IconButton
|
||||||
|
key="cancel"
|
||||||
|
text="Cancel"
|
||||||
|
onClick={() => setEditingServerId(undefined)}
|
||||||
|
bordered
|
||||||
|
disabled={isLoading}
|
||||||
|
/>,
|
||||||
|
<IconButton
|
||||||
|
key="confirm"
|
||||||
|
text="Save"
|
||||||
|
type="primary"
|
||||||
|
onClick={saveServerConfig}
|
||||||
|
bordered
|
||||||
|
disabled={isLoading}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<List>{renderConfigForm()}</List>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{viewingServerId && (
|
||||||
|
<div className="modal-mask">
|
||||||
|
<Modal
|
||||||
|
title={`Server Details - ${viewingServerId}`}
|
||||||
|
onClose={() => setViewingServerId(undefined)}
|
||||||
|
actions={[
|
||||||
|
<IconButton
|
||||||
|
key="close"
|
||||||
|
text="Close"
|
||||||
|
onClick={() => setViewingServerId(undefined)}
|
||||||
|
bordered
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div className={styles["primitives-list"]}>
|
||||||
|
{isLoading ? (
|
||||||
|
<div>Loading...</div>
|
||||||
|
) : primitives.filter((p) => p.type === "tool").length > 0 ? (
|
||||||
|
primitives
|
||||||
|
.filter((p) => p.type === "tool")
|
||||||
|
.map((primitive, index) => (
|
||||||
|
<div key={index} className={styles["primitive-item"]}>
|
||||||
|
<div className={styles["primitive-name"]}>
|
||||||
|
{primitive.value.name}
|
||||||
|
</div>
|
||||||
|
{primitive.value.description && (
|
||||||
|
<div className={styles["primitive-description"]}>
|
||||||
|
{primitive.value.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div>No tools available</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
@ -9,6 +9,7 @@ import ChatGptIcon from "../icons/chatgpt.svg";
|
|||||||
import AddIcon from "../icons/add.svg";
|
import AddIcon from "../icons/add.svg";
|
||||||
import DeleteIcon from "../icons/delete.svg";
|
import DeleteIcon from "../icons/delete.svg";
|
||||||
import MaskIcon from "../icons/mask.svg";
|
import MaskIcon from "../icons/mask.svg";
|
||||||
|
import McpIcon from "../icons/mcp.svg";
|
||||||
import DragIcon from "../icons/drag.svg";
|
import DragIcon from "../icons/drag.svg";
|
||||||
import DiscoveryIcon from "../icons/discovery.svg";
|
import DiscoveryIcon from "../icons/discovery.svg";
|
||||||
|
|
||||||
@ -250,6 +251,15 @@ export function SideBar(props: { className?: string }) {
|
|||||||
}}
|
}}
|
||||||
shadow
|
shadow
|
||||||
/>
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={<McpIcon />}
|
||||||
|
text={shouldNarrow ? undefined : Locale.Mcp.Name}
|
||||||
|
className={styles["sidebar-bar-button"]}
|
||||||
|
onClick={() => {
|
||||||
|
navigate(Path.McpMarket, { state: { fromHome: true } });
|
||||||
|
}}
|
||||||
|
shadow
|
||||||
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<DiscoveryIcon />}
|
icon={<DiscoveryIcon />}
|
||||||
text={shouldNarrow ? undefined : Locale.Discovery.Name}
|
text={shouldNarrow ? undefined : Locale.Discovery.Name}
|
||||||
|
@ -47,6 +47,7 @@ export enum Path {
|
|||||||
SdNew = "/sd-new",
|
SdNew = "/sd-new",
|
||||||
Artifacts = "/artifacts",
|
Artifacts = "/artifacts",
|
||||||
SearchChat = "/search-chat",
|
SearchChat = "/search-chat",
|
||||||
|
McpMarket = "/mcp-market",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ApiPath {
|
export enum ApiPath {
|
||||||
|
15
app/icons/mcp.svg
Normal file
15
app/icons/mcp.svg
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 180 180" fill="none">
|
||||||
|
<g clip-path="url(#clip0_19_13)">
|
||||||
|
<path d="M18 84.8528L85.8822 16.9706C95.2548 7.59798 110.451 7.59798 119.823 16.9706V16.9706C129.196 26.3431 129.196 41.5391 119.823 50.9117L68.5581 102.177"
|
||||||
|
stroke="black" stroke-width="12" stroke-linecap="round"/>
|
||||||
|
<path d="M69.2652 101.47L119.823 50.9117C129.196 41.5391 144.392 41.5391 153.765 50.9117L154.118 51.2652C163.491 60.6378 163.491 75.8338 154.118 85.2063L92.7248 146.6C89.6006 149.724 89.6006 154.789 92.7248 157.913L105.331 170.52"
|
||||||
|
stroke="black" stroke-width="12" stroke-linecap="round"/>
|
||||||
|
<path d="M102.853 33.9411L52.6482 84.1457C43.2756 93.5183 43.2756 108.714 52.6482 118.087V118.087C62.0208 127.459 77.2167 127.459 86.5893 118.087L136.794 67.8822"
|
||||||
|
stroke="black" stroke-width="12" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_19_13">
|
||||||
|
<rect width="180" height="180" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
@ -626,6 +626,9 @@ const cn = {
|
|||||||
Discovery: {
|
Discovery: {
|
||||||
Name: "发现",
|
Name: "发现",
|
||||||
},
|
},
|
||||||
|
Mcp: {
|
||||||
|
Name: "MCP",
|
||||||
|
},
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "你是一个助手",
|
Sysmessage: "你是一个助手",
|
||||||
},
|
},
|
||||||
|
@ -7,15 +7,16 @@ import {
|
|||||||
Primitive,
|
Primitive,
|
||||||
} from "./client";
|
} from "./client";
|
||||||
import { MCPClientLogger } from "./logger";
|
import { MCPClientLogger } from "./logger";
|
||||||
import conf from "./mcp_config.json";
|
import { McpRequestMessage, McpConfig, ServerConfig } from "./types";
|
||||||
import { McpRequestMessage } from "./types";
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
const logger = new MCPClientLogger("MCP Actions");
|
const logger = new MCPClientLogger("MCP Actions");
|
||||||
|
|
||||||
// Use Map to store all clients
|
// Use Map to store all clients
|
||||||
const clientsMap = new Map<
|
const clientsMap = new Map<
|
||||||
string,
|
string,
|
||||||
{ client: Client; primitives: Primitive[] }
|
{ client: Client | null; primitives: Primitive[]; errorMsg: string | null }
|
||||||
>();
|
>();
|
||||||
|
|
||||||
// Whether initialized
|
// Whether initialized
|
||||||
@ -24,27 +25,76 @@ let initialized = false;
|
|||||||
// Store failed clients
|
// Store failed clients
|
||||||
let errorClients: string[] = [];
|
let errorClients: string[] = [];
|
||||||
|
|
||||||
|
const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json");
|
||||||
|
|
||||||
|
// 获取 MCP 配置
|
||||||
|
export async function getMcpConfig(): Promise<McpConfig> {
|
||||||
|
try {
|
||||||
|
const configStr = await fs.readFile(CONFIG_PATH, "utf-8");
|
||||||
|
return JSON.parse(configStr);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to read MCP config:", error);
|
||||||
|
return { mcpServers: {} };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新 MCP 配置
|
||||||
|
export async function updateMcpConfig(config: McpConfig): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to write MCP config:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新初始化所有客户端
|
||||||
|
export async function reinitializeMcpClients() {
|
||||||
|
logger.info("Reinitializing MCP clients...");
|
||||||
|
// 遍历所有客户端,关闭
|
||||||
|
try {
|
||||||
|
for (const [clientId, clientData] of clientsMap.entries()) {
|
||||||
|
clientData.client?.close();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to close clients: ${error}`);
|
||||||
|
}
|
||||||
|
// 清空状态
|
||||||
|
clientsMap.clear();
|
||||||
|
errorClients = [];
|
||||||
|
initialized = false;
|
||||||
|
// 重新初始化
|
||||||
|
return initializeMcpClients();
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize all configured clients
|
// Initialize all configured clients
|
||||||
export async function initializeMcpClients() {
|
export async function initializeMcpClients() {
|
||||||
// If already initialized, return
|
// If already initialized, return
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
return;
|
return { errorClients };
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Starting to initialize MCP clients...");
|
logger.info("Starting to initialize MCP clients...");
|
||||||
|
errorClients = [];
|
||||||
|
|
||||||
|
const config = await getMcpConfig();
|
||||||
// Initialize all clients, key is clientId, value is client config
|
// Initialize all clients, key is clientId, value is client config
|
||||||
for (const [clientId, config] of Object.entries(conf.mcpServers)) {
|
for (const [clientId, serverConfig] of Object.entries(config.mcpServers)) {
|
||||||
try {
|
try {
|
||||||
logger.info(`Initializing MCP client: ${clientId}`);
|
logger.info(`Initializing MCP client: ${clientId}`);
|
||||||
const client = await createClient(config, clientId);
|
const client = await createClient(serverConfig as ServerConfig, clientId);
|
||||||
const primitives = await listPrimitives(client);
|
const primitives = await listPrimitives(client);
|
||||||
clientsMap.set(clientId, { client, primitives });
|
clientsMap.set(clientId, { client, primitives, errorMsg: null });
|
||||||
logger.success(
|
logger.success(
|
||||||
`Client [${clientId}] initialized, ${primitives.length} primitives supported`,
|
`Client [${clientId}] initialized, ${primitives.length} primitives supported`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorClients.push(clientId);
|
errorClients.push(clientId);
|
||||||
|
clientsMap.set(clientId, {
|
||||||
|
client: null,
|
||||||
|
primitives: [],
|
||||||
|
errorMsg: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
logger.error(`Failed to initialize client ${clientId}: ${error}`);
|
logger.error(`Failed to initialize client ${clientId}: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -58,8 +108,9 @@ export async function initializeMcpClients() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const availableClients = await getAvailableClients();
|
const availableClients = await getAvailableClients();
|
||||||
|
|
||||||
logger.info(`Available clients: ${availableClients.join(",")}`);
|
logger.info(`Available clients: ${availableClients.join(",")}`);
|
||||||
|
|
||||||
|
return { errorClients };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute MCP request
|
// Execute MCP request
|
||||||
@ -87,9 +138,9 @@ export async function executeMcpAction(
|
|||||||
|
|
||||||
// Get all available client IDs
|
// Get all available client IDs
|
||||||
export async function getAvailableClients() {
|
export async function getAvailableClients() {
|
||||||
return Array.from(clientsMap.keys()).filter(
|
return Array.from(clientsMap.entries())
|
||||||
(clientId) => !errorClients.includes(clientId),
|
.filter(([_, data]) => data.errorMsg === null)
|
||||||
);
|
.map(([clientId]) => clientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all primitives from all clients
|
// Get all primitives from all clients
|
||||||
@ -104,3 +155,62 @@ export async function getAllPrimitives(): Promise<
|
|||||||
primitives,
|
primitives,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取客户端的 Primitives
|
||||||
|
export async function getClientPrimitives(clientId: string) {
|
||||||
|
try {
|
||||||
|
const clientData = clientsMap.get(clientId);
|
||||||
|
if (!clientData) {
|
||||||
|
console.warn(`Client ${clientId} not found in map`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (clientData.errorMsg) {
|
||||||
|
console.warn(`Client ${clientId} has error: ${clientData.errorMsg}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return clientData.primitives;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to get primitives for client ${clientId}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重启所有客户端
|
||||||
|
export async function restartAllClients() {
|
||||||
|
logger.info("Restarting all MCP clients...");
|
||||||
|
|
||||||
|
// 清空状态
|
||||||
|
clientsMap.clear();
|
||||||
|
errorClients = [];
|
||||||
|
initialized = false;
|
||||||
|
|
||||||
|
// 重新初始化
|
||||||
|
await initializeMcpClients();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: errorClients.length === 0,
|
||||||
|
errorClients,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有客户端状态
|
||||||
|
export async function getAllClientStatus(): Promise<
|
||||||
|
Record<string, string | null>
|
||||||
|
> {
|
||||||
|
const status: Record<string, string | null> = {};
|
||||||
|
for (const [clientId, data] of clientsMap.entries()) {
|
||||||
|
status[clientId] = data.errorMsg;
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查客户端状态
|
||||||
|
export async function getClientErrors(): Promise<
|
||||||
|
Record<string, string | null>
|
||||||
|
> {
|
||||||
|
const errors: Record<string, string | null> = {};
|
||||||
|
for (const [clientId, data] of clientsMap.entries()) {
|
||||||
|
errors[clientId] = data.errorMsg;
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
@ -8,13 +8,29 @@
|
|||||||
"/Users/kadxy/Desktop"
|
"/Users/kadxy/Desktop"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"everything": {
|
|
||||||
"command": "npx",
|
|
||||||
"args": ["-y", "@modelcontextprotocol/server-everything"]
|
|
||||||
},
|
|
||||||
"docker-mcp": {
|
"docker-mcp": {
|
||||||
"command": "uvx",
|
"command": "uvx",
|
||||||
"args": ["docker-mcp"]
|
"args": ["docker-mcp"]
|
||||||
|
},
|
||||||
|
"difyworkflow": {
|
||||||
|
"command": "mcp-difyworkflow-server",
|
||||||
|
"args": ["-base-url", "23"],
|
||||||
|
"env": {
|
||||||
|
"DIFY_WORKFLOW_NAME": "23",
|
||||||
|
"DIFY_API_KEYS": "23"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"postgres": {
|
||||||
|
"command": "docker",
|
||||||
|
"args": ["run", "-i", "--rm", "mcp/postgres", null]
|
||||||
|
},
|
||||||
|
"playwright": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@executeautomation/playwright-mcp-server"]
|
||||||
|
},
|
||||||
|
"gdrive": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@modelcontextprotocol/server-gdrive"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
206
app/mcp/preset-server.json
Normal file
206
app/mcp/preset-server.json
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "filesystem",
|
||||||
|
"name": "Filesystem",
|
||||||
|
"description": "Secure file operations with configurable access controls",
|
||||||
|
"command": "npx",
|
||||||
|
"baseArgs": ["-y", "@modelcontextprotocol/server-filesystem"],
|
||||||
|
"configurable": true,
|
||||||
|
"configSchema": {
|
||||||
|
"properties": {
|
||||||
|
"paths": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Allowed file system paths",
|
||||||
|
"required": true,
|
||||||
|
"minItems": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"argsMapping": {
|
||||||
|
"paths": {
|
||||||
|
"type": "spread",
|
||||||
|
"position": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "github",
|
||||||
|
"name": "GitHub",
|
||||||
|
"description": "Repository management, file operations, and GitHub API integration",
|
||||||
|
"command": "npx",
|
||||||
|
"baseArgs": ["-y", "@modelcontextprotocol/server-github"],
|
||||||
|
"configurable": true,
|
||||||
|
"configSchema": {
|
||||||
|
"properties": {
|
||||||
|
"token": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "GitHub Personal Access Token",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"argsMapping": {
|
||||||
|
"token": {
|
||||||
|
"type": "env",
|
||||||
|
"key": "GITHUB_PERSONAL_ACCESS_TOKEN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gdrive",
|
||||||
|
"name": "Google Drive",
|
||||||
|
"description": "File access and search capabilities for Google Drive",
|
||||||
|
"command": "npx",
|
||||||
|
"baseArgs": ["-y", "@modelcontextprotocol/server-gdrive"],
|
||||||
|
"configurable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "playwright",
|
||||||
|
"name": "Playwright",
|
||||||
|
"description": "Browser automation and webscrapping with Playwright",
|
||||||
|
"command": "npx",
|
||||||
|
"baseArgs": ["-y", "@executeautomation/playwright-mcp-server"],
|
||||||
|
"configurable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mongodb",
|
||||||
|
"name": "MongoDB",
|
||||||
|
"description": "Direct interaction with MongoDB databases",
|
||||||
|
"command": "node",
|
||||||
|
"baseArgs": ["dist/index.js"],
|
||||||
|
"configurable": true,
|
||||||
|
"configSchema": {
|
||||||
|
"properties": {
|
||||||
|
"connectionString": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "MongoDB connection string",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"argsMapping": {
|
||||||
|
"connectionString": {
|
||||||
|
"type": "single",
|
||||||
|
"position": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "difyworkflow",
|
||||||
|
"name": "Dify Workflow",
|
||||||
|
"description": "Tools to query and execute Dify workflows",
|
||||||
|
"command": "mcp-difyworkflow-server",
|
||||||
|
"baseArgs": ["-base-url"],
|
||||||
|
"configurable": true,
|
||||||
|
"configSchema": {
|
||||||
|
"properties": {
|
||||||
|
"baseUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Dify API base URL",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"workflowName": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Dify workflow name",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"apiKeys": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Comma-separated Dify API keys",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"argsMapping": {
|
||||||
|
"baseUrl": {
|
||||||
|
"type": "single",
|
||||||
|
"position": 1
|
||||||
|
},
|
||||||
|
"workflowName": {
|
||||||
|
"type": "env",
|
||||||
|
"key": "DIFY_WORKFLOW_NAME"
|
||||||
|
},
|
||||||
|
"apiKeys": {
|
||||||
|
"type": "env",
|
||||||
|
"key": "DIFY_API_KEYS"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "postgres",
|
||||||
|
"name": "PostgreSQL",
|
||||||
|
"description": "Read-only database access with schema inspection",
|
||||||
|
"command": "docker",
|
||||||
|
"baseArgs": ["run", "-i", "--rm", "mcp/postgres"],
|
||||||
|
"configurable": true,
|
||||||
|
"configSchema": {
|
||||||
|
"properties": {
|
||||||
|
"connectionString": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "PostgreSQL connection string",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"argsMapping": {
|
||||||
|
"connectionString": {
|
||||||
|
"type": "single",
|
||||||
|
"position": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "brave-search",
|
||||||
|
"name": "Brave Search",
|
||||||
|
"description": "Web and local search using Brave's Search API",
|
||||||
|
"command": "npx",
|
||||||
|
"baseArgs": ["-y", "@modelcontextprotocol/server-brave-search"],
|
||||||
|
"configurable": true,
|
||||||
|
"configSchema": {
|
||||||
|
"properties": {
|
||||||
|
"apiKey": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Brave Search API Key",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"argsMapping": {
|
||||||
|
"apiKey": {
|
||||||
|
"type": "env",
|
||||||
|
"key": "BRAVE_API_KEY"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "google-maps",
|
||||||
|
"name": "Google Maps",
|
||||||
|
"description": "Location services, directions, and place details",
|
||||||
|
"command": "npx",
|
||||||
|
"baseArgs": ["-y", "@modelcontextprotocol/server-google-maps"],
|
||||||
|
"configurable": true,
|
||||||
|
"configSchema": {
|
||||||
|
"properties": {
|
||||||
|
"apiKey": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Google Maps API Key",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"argsMapping": {
|
||||||
|
"apiKey": {
|
||||||
|
"type": "env",
|
||||||
|
"key": "GOOGLE_MAPS_API_KEY"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "docker-mcp",
|
||||||
|
"name": "Docker",
|
||||||
|
"description": "Run and manage docker containers, docker compose, and logs",
|
||||||
|
"command": "uvx",
|
||||||
|
"baseArgs": ["docker-mcp"],
|
||||||
|
"configurable": false
|
||||||
|
}
|
||||||
|
]
|
@ -59,3 +59,41 @@ export const McpNotificationsSchema: z.ZodType<McpNotifications> = z.object({
|
|||||||
method: z.string(),
|
method: z.string(),
|
||||||
params: z.record(z.unknown()).optional(),
|
params: z.record(z.unknown()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// MCP 服务器配置相关类型
|
||||||
|
export interface ServerConfig {
|
||||||
|
command: string;
|
||||||
|
args: string[];
|
||||||
|
env?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface McpConfig {
|
||||||
|
mcpServers: Record<string, ServerConfig>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArgsMapping {
|
||||||
|
type: "spread" | "single" | "env";
|
||||||
|
position?: number;
|
||||||
|
key?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PresetServer {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
command: string;
|
||||||
|
baseArgs: string[];
|
||||||
|
configurable: boolean;
|
||||||
|
configSchema?: {
|
||||||
|
properties: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
type: string;
|
||||||
|
description?: string;
|
||||||
|
required?: boolean;
|
||||||
|
minItems?: number;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
argsMapping?: Record<string, ArgsMapping>;
|
||||||
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
export function isMcpJson(content: string) {
|
export function isMcpJson(content: string) {
|
||||||
return content.match(/```json:mcp:(\w+)([\s\S]*?)```/);
|
return content.match(/```json:mcp:([^{\s]+)([\s\S]*?)```/);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractMcpJson(content: string) {
|
export function extractMcpJson(content: string) {
|
||||||
const match = content.match(/```json:mcp:(\w+)([\s\S]*?)```/);
|
const match = content.match(/```json:mcp:([^{\s]+)([\s\S]*?)```/);
|
||||||
if (match) {
|
if (match && match.length === 3) {
|
||||||
return { clientId: match[1], mcp: JSON.parse(match[2]) };
|
return { clientId: match[1], mcp: JSON.parse(match[2]) };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -32,7 +32,6 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
forceSwcTransforms: true,
|
forceSwcTransforms: true,
|
||||||
serverActions: true,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
13
yarn.lock
13
yarn.lock
@ -3076,15 +3076,10 @@ camelcase@^6.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
|
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
|
||||||
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
|
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001503, caniuse-lite@^1.0.30001579:
|
caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001503, caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001646:
|
||||||
version "1.0.30001617"
|
version "1.0.30001692"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001617.tgz#809bc25f3f5027ceb33142a7d6c40759d7a901eb"
|
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz"
|
||||||
integrity sha512-mLyjzNI9I+Pix8zwcrpxEbGlfqOkF9kM3ptzmKNw5tizSyYwMe+nGLTqMK9cO+0E+Bh6TsBxNAaHWEM8xwSsmA==
|
integrity sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001646:
|
|
||||||
version "1.0.30001649"
|
|
||||||
resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001649.tgz#3ec700309ca0da2b0d3d5fb03c411b191761c992"
|
|
||||||
integrity sha512-fJegqZZ0ZX8HOWr6rcafGr72+xcgJKI9oWfDW5DrD7ExUtgZC7a7R7ZYmZqplh7XDocFdGeIFn7roAxhOeYrPQ==
|
|
||||||
|
|
||||||
ccount@^2.0.0:
|
ccount@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
|
Loading…
Reference in New Issue
Block a user