feature/1.7-MCP

This commit is contained in:
ningyv
2025-04-10 10:24:29 +08:00
parent 8cff5b3d40
commit 1eeba2d648
5 changed files with 497 additions and 2 deletions
+3 -1
View File
@@ -48,7 +48,9 @@
"tailwindcss": "^3.3.5",
"uuid": "^9.0.1",
"vite-tsconfig-paths": "^4.3.2",
"react-json-view": "^1.21.3"
"react-json-view": "^1.21.3",
"zod": "^3.23.8",
"@modelcontextprotocol/sdk": "^1.9.0"
},
"devDependencies": {
"@ant-design/cssinjs": "^1.18.2",
@@ -6,6 +6,7 @@ import ReactJson from 'react-json-view'
import { IconButton } from '@common/components/postcat/api/IconButton'
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useFetch } from '@common/hooks/http'
import { useConnection } from './hook/useConnection'
type ConfigList = {
openApi?: {
@@ -31,8 +32,9 @@ const IntegrationAIContainer = ({ type, handleApiKeyChange }: { type: 'global' |
const [activeTab, setActiveTab] = useState('mcp')
const { message } = App.useApp()
const [configContent, setConfigContent] = useState<string>('')
const [apiKey, setApiKey] = useState<string>('暂无数据')
const [apiKey, setApiKey] = useState<string>('')
const [apiKeyList, setApiKeyList] = useState<{ value: string; label: string }[]>([])
const [mcpServerUrl, setMcpServerUrl] = useState<string>('')
const [tabContent, setTabContent] = useState<ConfigList>({
mcp: {
title: $t('MCP 配置'),
@@ -134,6 +136,37 @@ const IntegrationAIContainer = ({ type, handleApiKeyChange }: { type: 'global' |
})
}
const {
connectionStatus,
serverCapabilities,
mcpClient,
requestHistory,
makeRequest: makeConnectionRequest,
sendNotification,
handleCompletion,
completionsSupported,
connect: connectMcpServer,
disconnect: disconnectMcpServer,
} = useConnection({
transportType: 'sse',
sseUrl: mcpServerUrl,
proxyServerUrl: 'mcp/global/sse',
requestTimeout: 1000,
});
console.log('connectionStatus==================', connectionStatus);
// console.log('serverCapabilities==================', serverCapabilities);
// console.log('mcpClient==================', mcpClient);
// console.log('requestHistory==================', requestHistory);
// console.log('makeConnectionRequest==================', makeConnectionRequest);
// console.log('sendNotification==================', sendNotification);
// console.log('handleCompletion==================', handleCompletion);
// console.log('completionsSupported==================', completionsSupported);
// console.log('connectMcpServer==================', connectMcpServer);
// console.log('disconnectMcpServer==================', disconnectMcpServer);
// const useConnectAIagent = () => {
// connectMcpServer()
// }
useEffect(() => {
type === 'global' && getGlobalMcpConfig()
initTabsData()
@@ -146,6 +179,29 @@ const IntegrationAIContainer = ({ type, handleApiKeyChange }: { type: 'global' |
setConfigContent(tabContent.mcp.configContent || '')
}
}, [tabContent, activeTab])
useEffect(() => {
if (configContent && apiKey) {
const parsedConfig = JSON.parse(configContent)
console.log('啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊parsedConfig', parsedConfig, apiKey)
let baseUrl = ''
if (parsedConfig?.mcpServers) {
// 获取 mcpServers 对象中的第一个键
const serverKey = Object.keys(parsedConfig.mcpServers)[0]
baseUrl = parsedConfig.mcpServers[serverKey]?.url
}
baseUrl = baseUrl.replace('{your_api_key}', apiKey)
if (mcpServerUrl === baseUrl) {
return
}
setMcpServerUrl(baseUrl)
console.log('啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊', mcpServerUrl)
if (connectionStatus === 'connected') {
disconnectMcpServer()
}
connectMcpServer()
}
}, [apiKey, configContent, connectMcpServer])
return (
<>
<Card
@@ -0,0 +1,33 @@
// import { InspectorConfig } from "./configurationTypes";
// OAuth-related session storage keys
export const SESSION_KEYS = {
CODE_VERIFIER: "mcp_code_verifier",
SERVER_URL: "mcp_server_url",
TOKENS: "mcp_tokens",
CLIENT_INFORMATION: "mcp_client_information",
} as const;
export type ConnectionStatus =
| "disconnected"
| "connected"
| "error"
| "error-connecting-to-proxy";
export const DEFAULT_MCP_PROXY_LISTEN_PORT = "6277";
/**
* Default configuration for the MCP Inspector, Currently persisted in local_storage in the Browser.
* Future plans: Provide json config file + Browser local_storage to override default values
**/
export const DEFAULT_INSPECTOR_CONFIG: any = {
MCP_SERVER_REQUEST_TIMEOUT: {
description: "Timeout for requests to the MCP server (ms)",
value: 10000,
},
MCP_PROXY_FULL_ADDRESS: {
description:
"Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
value: "",
},
} as const;
@@ -0,0 +1,22 @@
import {
NotificationSchema as BaseNotificationSchema,
ClientNotificationSchema,
ServerNotificationSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
export const StdErrNotificationSchema = BaseNotificationSchema.extend({
method: z.literal("notifications/stderr"),
params: z.object({
content: z.string(),
}),
});
export const NotificationSchema = ClientNotificationSchema.or(
StdErrNotificationSchema,
)
.or(ServerNotificationSchema)
.or(BaseNotificationSchema);
export type StdErrNotification = z.infer<typeof StdErrNotificationSchema>;
export type Notification = z.infer<typeof NotificationSchema>;
@@ -0,0 +1,382 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import {
SSEClientTransport,
SseError,
} from "@modelcontextprotocol/sdk/client/sse.js";
import { App } from 'antd'
import {
ClientNotification,
ClientRequest,
CreateMessageRequestSchema,
ListRootsRequestSchema,
ProgressNotificationSchema,
ResourceUpdatedNotificationSchema,
LoggingMessageNotificationSchema,
Request,
Result,
ServerCapabilities,
PromptReference,
ResourceReference,
McpError,
CompleteResultSchema,
ErrorCode,
CancelledNotificationSchema,
ResourceListChangedNotificationSchema,
ToolListChangedNotificationSchema,
PromptListChangedNotificationSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { useState } from "react";
import { z } from "zod";
import { ConnectionStatus, SESSION_KEYS } from "./constants";
import { Notification, StdErrNotificationSchema } from "./notificationTypes";
// import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
// import { authProvider } from "../auth";
// import packageJson from "../../../package.json";
interface UseConnectionOptions {
transportType: "stdio" | "sse";
command?: string;
args?: string;
sseUrl: string;
env?: Record<string, string>;
proxyServerUrl: string;
bearerToken?: string;
requestTimeout?: number;
onNotification?: (notification: Notification) => void;
onStdErrNotification?: (notification: Notification) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onPendingRequest?: (request: any, resolve: any, reject: any) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getRoots?: () => any[];
}
interface RequestOptions {
signal?: AbortSignal;
timeout?: number;
suppressToast?: boolean;
}
export function useConnection({
transportType,
command,
args,
sseUrl,
env,
proxyServerUrl,
bearerToken,
requestTimeout,
onNotification,
onStdErrNotification,
onPendingRequest,
getRoots,
}: UseConnectionOptions) {
const [connectionStatus, setConnectionStatus] =
useState<ConnectionStatus>("disconnected");
const { message } = App.useApp()
const [serverCapabilities, setServerCapabilities] =
useState<ServerCapabilities | null>(null);
const [mcpClient, setMcpClient] = useState<Client | null>(null);
const [requestHistory, setRequestHistory] = useState<
{ request: string; response?: string }[]
>([]);
const [completionsSupported, setCompletionsSupported] = useState(true);
const pushHistory = (request: object, response?: object) => {
setRequestHistory((prev) => [
...prev,
{
request: JSON.stringify(request),
response: response !== undefined ? JSON.stringify(response) : undefined,
},
]);
};
const makeRequest = async <T extends z.ZodType>(
request: ClientRequest,
schema: T,
options?: RequestOptions,
): Promise<z.output<T>> => {
if (!mcpClient) {
throw new Error("MCP client not connected");
}
try {
const abortController = new AbortController();
const timeoutId = setTimeout(() => {
abortController.abort("Request timed out");
}, options?.timeout ?? requestTimeout);
let response;
try {
response = await mcpClient.request(request, schema, {
signal: options?.signal ?? abortController.signal,
});
pushHistory(request, response);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
pushHistory(request, { error: errorMessage });
throw error;
} finally {
clearTimeout(timeoutId);
}
return response;
} catch (e: unknown) {
if (!options?.suppressToast) {
const errorString = (e as Error).message ?? String(e);
message.error(errorString)
}
throw e;
}
};
const handleCompletion = async (
ref: ResourceReference | PromptReference,
argName: string,
value: string,
signal?: AbortSignal,
): Promise<string[]> => {
if (!mcpClient || !completionsSupported) {
return [];
}
const request: ClientRequest = {
method: "completion/complete",
params: {
argument: {
name: argName,
value,
},
ref,
},
};
try {
const response = await makeRequest(request, CompleteResultSchema, {
signal,
suppressToast: true,
});
return response?.completion.values || [];
} catch (e: unknown) {
// Disable completions silently if the server doesn't support them.
// See https://github.com/modelcontextprotocol/specification/discussions/122
if (e instanceof McpError && e.code === ErrorCode.MethodNotFound) {
setCompletionsSupported(false);
return [];
}
// Unexpected errors - show toast and rethrow
message.error(e instanceof Error ? e.message : String(e))
throw e;
}
};
const sendNotification = async (notification: ClientNotification) => {
if (!mcpClient) {
const error = new Error("MCP client not connected");
message.error(error.message)
throw error;
}
try {
await mcpClient.notification(notification);
// Log successful notifications
pushHistory(notification);
} catch (e: unknown) {
if (e instanceof McpError) {
// Log MCP protocol errors
pushHistory(notification, { error: e.message });
}
message.error(e instanceof Error ? e.message : String(e))
throw e;
}
};
// TODO_先屏蔽,暂时不需要
// const checkProxyHealth = async () => {
// try {
// const proxyHealthUrl = new URL(`${proxyServerUrl}/health`);
// const proxyHealthResponse = await fetch(proxyHealthUrl);
// const proxyHealth = await proxyHealthResponse.json();
// if (proxyHealth?.status !== "ok") {
// throw new Error("MCP Proxy Server is not healthy");
// }
// } catch (e) {
// console.error("Couldn't connect to MCP Proxy Server", e);
// throw e;
// }
// };
// TODO_先屏蔽,暂时不需要
// const handleAuthError = async (error: unknown) => {
// if (error instanceof SseError && error.code === 401) {
// sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl);
// const result = await auth(authProvider, { serverUrl: sseUrl });
// return result === "AUTHORIZED";
// }
// return false;
// };
const connect = async (_e?: unknown, retryCount: number = 0) => {
const client = new Client<Request, Notification, Result>(
{
name: "mcp-inspector",
version: '0.0.1',
},
{
capabilities: {
sampling: {},
roots: {
listChanged: true,
},
},
},
);
// TODO_暂时不需要
// try {
// await checkProxyHealth();
// } catch {
// setConnectionStatus("error-connecting-to-proxy");
// return;
// }
// 使用与http.ts一致的方式处理URL
// 注意:proxyServerUrl应该是完整URL,或者我们需要为其添加基础URL
// 处理两种情况:完整URL或相对路径
let fullUrl;
if (proxyServerUrl.startsWith('http://') || proxyServerUrl.startsWith('https://')) {
// 如果是完整URL,直接使用
fullUrl = `${proxyServerUrl}/sse`;
} else {
// 如果是相对路径,添加基础URL和API前缀
const baseUrl = window.location.origin;
const apiPrefix = '/api/v1/';
fullUrl = `${baseUrl}${apiPrefix}${proxyServerUrl}`;
}
const mcpProxyServerUrl = new URL(fullUrl);
mcpProxyServerUrl.searchParams.append("transportType", transportType);
if (transportType === "stdio") {
mcpProxyServerUrl.searchParams.append("command", command || '');
mcpProxyServerUrl.searchParams.append("args", args || '');
mcpProxyServerUrl.searchParams.append("env", JSON.stringify(env || {}));
} else {
mcpProxyServerUrl.searchParams.append("url", sseUrl);
}
console.log('sseUrl===', sseUrl)
try {
// Inject auth manually instead of using SSEClientTransport, because we're
// proxying through the inspector server first.
const headers: HeadersInit = {};
// TODO_暂时不需要。Use manually provided bearer token if available, otherwise use OAuth tokens
// const token = bearerToken || (await authProvider.tokens())?.access_token;
// if (token) {
// headers["Authorization"] = `Bearer ${token}`;
// }
// 创建SSE客户端传输层
const clientTransport = new SSEClientTransport(mcpProxyServerUrl, {
eventSourceInit: {
fetch: (url, init) => fetch(url, { ...init, headers }),
},
requestInit: {
headers,
},
});
// TODO_暂时不需要
// if (onNotification) {
// [
// CancelledNotificationSchema,
// ProgressNotificationSchema,
// LoggingMessageNotificationSchema,
// ResourceUpdatedNotificationSchema,
// ResourceListChangedNotificationSchema,
// ToolListChangedNotificationSchema,
// PromptListChangedNotificationSchema,
// ].forEach((notificationSchema) => {
// client.setNotificationHandler(notificationSchema, onNotification);
// });
// client.fallbackNotificationHandler = (
// notification: Notification,
// ): Promise<void> => {
// onNotification(notification);
// return Promise.resolve();
// };
// }
// if (onStdErrNotification) {
// client.setNotificationHandler(
// StdErrNotificationSchema,
// onStdErrNotification,
// );
// }
try {
await client.connect(clientTransport);
} catch (error) {
console.error(
`Failed to connect to MCP Server via the MCP Inspector Proxy: ${mcpProxyServerUrl}:`,
error,
);
// TODO_先屏蔽,后续如果需要再处理
// const shouldRetry = await handleAuthError(error);
// if (shouldRetry) {
// return connect(undefined, retryCount + 1);
// }
if (error instanceof SseError && error.code === 401) {
// Don't set error state if we're about to redirect for auth
return;
}
throw error;
}
const capabilities = client.getServerCapabilities();
setServerCapabilities(capabilities ?? null);
setCompletionsSupported(true); // Reset completions support on new connection
// TODO_暂时不需要
// if (onPendingRequest) {
// client.setRequestHandler(CreateMessageRequestSchema, (request) => {
// return new Promise((resolve, reject) => {
// onPendingRequest(request, resolve, reject);
// });
// });
// }
// if (getRoots) {
// client.setRequestHandler(ListRootsRequestSchema, async () => {
// return { roots: getRoots() };
// });
// }
setMcpClient(client);
setConnectionStatus("connected");
} catch (e) {
console.error(e);
setConnectionStatus("error");
}
};
const disconnect = async () => {
await mcpClient?.close();
setMcpClient(null);
setConnectionStatus("disconnected");
setCompletionsSupported(false);
setServerCapabilities(null);
};
return {
connectionStatus,
serverCapabilities,
mcpClient,
requestHistory,
makeRequest,
sendNotification,
handleCompletion,
completionsSupported,
connect,
disconnect,
};
}