feat(ui): i18n for mcp_tools batch 3

This commit is contained in:
吹雪 2026-06-10 19:05:15 +09:00
parent c233d094cf
commit 8708a1e614
7 changed files with 394 additions and 113 deletions

View file

@ -2,6 +2,7 @@ import React from "react";
import { Form, Input, InputNumber, Select, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { Button, TextInput } from "@tremor/react";
import { useTranslation } from "react-i18next";
import { OAUTH_FLOW } from "./types";
interface OAuthFlowStatus {
@ -38,15 +39,16 @@ const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
initialFlowType,
docsUrl,
}) => {
const placeholderSuffix = isEditing ? " (leave blank to keep existing)" : "";
const { t } = useTranslation();
const placeholderSuffix = isEditing ? ` (${t("mcpTools.oAuthFormFields.leaveBlankToKeep")})` : "";
return (
<>
<Form.Item
label={
<FieldLabel
label="OAuth Flow Type"
tooltip="Choose how the proxy authenticates with this MCP server. M2M is for server-to-server communication using client credentials. Interactive (PKCE) is for user-facing flows that require browser-based authorization."
label={t("mcpTools.oAuthFormFields.flowTypeLabel")}
tooltip={t("mcpTools.oAuthFormFields.flowTypeTooltip")}
/>
}
name="oauth_flow_type"
@ -55,14 +57,14 @@ const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
<Select className="rounded-lg" size="large">
<Select.Option value={OAUTH_FLOW.M2M}>
<div>
<span className="font-medium">Machine-to-Machine (M2M)</span>
<span className="text-gray-400 text-xs ml-2">server-to-server, no user interaction</span>
<span className="font-medium">{t("mcpTools.oAuthFormFields.m2mLabel")}</span>
<span className="text-gray-400 text-xs ml-2">{t("mcpTools.oAuthFormFields.m2mDesc")}</span>
</div>
</Select.Option>
<Select.Option value={OAUTH_FLOW.INTERACTIVE}>
<div>
<span className="font-medium">Interactive (PKCE)</span>
<span className="text-gray-400 text-xs ml-2">browser-based user authorization</span>
<span className="font-medium">{t("mcpTools.oAuthFormFields.interactiveLabel")}</span>
<span className="text-gray-400 text-xs ml-2">{t("mcpTools.oAuthFormFields.interactiveDesc")}</span>
</div>
</Select.Option>
</Select>
@ -71,46 +73,65 @@ const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
{isM2M ? (
<>
<Form.Item
label={<FieldLabel label="Client ID" tooltip="OAuth2 client ID for the client_credentials grant." />}
label={
<FieldLabel
label={t("mcpTools.oAuthFormFields.clientIdLabel")}
tooltip={t("mcpTools.oAuthFormFields.clientIdM2MTooltip")}
/>
}
name={["credentials", "client_id"]}
rules={[{ required: true, message: "Client ID is required for M2M OAuth" }]}
rules={[{ required: true, message: t("mcpTools.oAuthFormFields.clientIdM2MRequired") }]}
>
<TextInput
type="password"
placeholder={`Enter OAuth client ID${placeholderSuffix}`}
placeholder={`${t("mcpTools.oAuthFormFields.clientIdM2MPlaceholder")}${placeholderSuffix}`}
className={fieldClassName}
/>
</Form.Item>
<Form.Item
label={
<FieldLabel label="Client Secret" tooltip="OAuth2 client secret for the client_credentials grant." />
<FieldLabel
label={t("mcpTools.oAuthFormFields.clientSecretLabel")}
tooltip={t("mcpTools.oAuthFormFields.clientSecretM2MTooltip")}
/>
}
name={["credentials", "client_secret"]}
rules={[{ required: true, message: "Client Secret is required for M2M OAuth" }]}
rules={[{ required: true, message: t("mcpTools.oAuthFormFields.clientSecretM2MRequired") }]}
>
<TextInput
type="password"
placeholder={`Enter OAuth client secret${placeholderSuffix}`}
placeholder={`${t("mcpTools.oAuthFormFields.clientSecretM2MPlaceholder")}${placeholderSuffix}`}
className={fieldClassName}
/>
</Form.Item>
<Form.Item
label={<FieldLabel label="Token URL" tooltip="Token endpoint URL for the client_credentials grant." />}
label={
<FieldLabel
label={t("mcpTools.oAuthFormFields.tokenUrlLabel")}
tooltip={t("mcpTools.oAuthFormFields.tokenUrlM2MTooltip")}
/>
}
name="token_url"
rules={[{ required: true, message: "Token URL is required for M2M OAuth" }]}
rules={[{ required: true, message: t("mcpTools.oAuthFormFields.tokenUrlM2MRequired") }]}
>
<TextInput placeholder="https://auth.example.com/oauth/token" className={fieldClassName} />
</Form.Item>
<Form.Item
label={
<FieldLabel
label="Scopes (optional)"
tooltip="Optional scopes to request with the client_credentials grant."
label={t("mcpTools.oAuthFormFields.scopesOptionalLabel")}
tooltip={t("mcpTools.oAuthFormFields.scopesM2MTooltip")}
/>
}
name={["credentials", "scopes"]}
>
<Select mode="tags" tokenSeparators={[","]} placeholder="Add scopes" className="rounded-lg" size="large" />
<Select
mode="tags"
tokenSeparators={[","]}
placeholder={t("mcpTools.oAuthFormFields.scopesPlaceholder")}
className="rounded-lg"
size="large"
/>
</Form.Item>
</>
) : (
@ -119,8 +140,8 @@ const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
label={
<span className="flex items-center justify-between w-full">
<FieldLabel
label="Client ID (optional)"
tooltip="Provide only if your MCP server cannot handle dynamic client registration."
label={t("mcpTools.oAuthFormFields.clientIdOptionalLabel")}
tooltip={t("mcpTools.oAuthFormFields.clientIdInteractiveTooltip")}
/>
{docsUrl && (
<a
@ -130,46 +151,56 @@ const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
className="text-xs text-blue-500 hover:text-blue-700 ml-2 font-normal"
onClick={(e) => e.stopPropagation()}
>
Create OAuth App →
{t("mcpTools.oAuthFormFields.createOAuthAppLink")}
</a>
)}
</span>
}
name={["credentials", "client_id"]}
>
<TextInput type="password" placeholder={`Enter client ID${placeholderSuffix}`} className={fieldClassName} />
</Form.Item>
<Form.Item
label={
<FieldLabel
label="Client Secret (optional)"
tooltip="Provide only if your MCP server cannot handle dynamic client registration."
/>
}
name={["credentials", "client_secret"]}
>
<TextInput
type="password"
placeholder={`Enter client secret${placeholderSuffix}`}
placeholder={`${t("mcpTools.oAuthFormFields.clientIdInteractivePlaceholder")}${placeholderSuffix}`}
className={fieldClassName}
/>
</Form.Item>
<Form.Item
label={
<FieldLabel
label="Scopes (optional)"
tooltip="Optional scopes requested during token exchange. Separate multiple scopes with enter or commas."
label={t("mcpTools.oAuthFormFields.clientSecretOptionalLabel")}
tooltip={t("mcpTools.oAuthFormFields.clientSecretInteractiveTooltip")}
/>
}
name={["credentials", "scopes"]}
name={["credentials", "client_secret"]}
>
<Select mode="tags" tokenSeparators={[","]} placeholder="Add scopes" className="rounded-lg" size="large" />
<TextInput
type="password"
placeholder={`${t("mcpTools.oAuthFormFields.clientSecretInteractivePlaceholder")}${placeholderSuffix}`}
className={fieldClassName}
/>
</Form.Item>
<Form.Item
label={
<FieldLabel
label="Authorization URL (optional)"
tooltip="Optional override for the authorization endpoint."
label={t("mcpTools.oAuthFormFields.scopesOptionalLabel")}
tooltip={t("mcpTools.oAuthFormFields.scopesInteractiveTooltip")}
/>
}
name={["credentials", "scopes"]}
>
<Select
mode="tags"
tokenSeparators={[","]}
placeholder={t("mcpTools.oAuthFormFields.scopesPlaceholder")}
className="rounded-lg"
size="large"
/>
</Form.Item>
<Form.Item
label={
<FieldLabel
label={t("mcpTools.oAuthFormFields.authorizationUrlOptionalLabel")}
tooltip={t("mcpTools.oAuthFormFields.authorizationUrlTooltip")}
/>
}
name="authorization_url"
@ -177,7 +208,12 @@ const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
<TextInput placeholder="https://example.com/oauth/authorize" className={fieldClassName} />
</Form.Item>
<Form.Item
label={<FieldLabel label="Token URL (optional)" tooltip="Optional override for the token endpoint." />}
label={
<FieldLabel
label={t("mcpTools.oAuthFormFields.tokenUrlOptionalLabel")}
tooltip={t("mcpTools.oAuthFormFields.tokenUrlInteractiveTooltip")}
/>
}
name="token_url"
>
<TextInput placeholder="https://example.com/oauth/token" className={fieldClassName} />
@ -185,8 +221,8 @@ const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
<Form.Item
label={
<FieldLabel
label="Registration URL (optional)"
tooltip="Optional override for the dynamic client registration endpoint."
label={t("mcpTools.oAuthFormFields.registrationUrlOptionalLabel")}
tooltip={t("mcpTools.oAuthFormFields.registrationUrlTooltip")}
/>
}
name="registration_url"
@ -196,8 +232,8 @@ const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
<Form.Item
label={
<FieldLabel
label="Token Validation Rules (optional)"
tooltip='JSON object of key-value rules checked against the OAuth token response before storing. Supports dot-notation for nested fields (e.g. {"organization": "my-org", "team.id": "123"}). Tokens that fail validation are rejected with HTTP 403.'
label={t("mcpTools.oAuthFormFields.tokenValidationRulesLabel")}
tooltip={t("mcpTools.oAuthFormFields.tokenValidationRulesTooltip")}
/>
}
name="token_validation_json"
@ -209,7 +245,7 @@ const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
JSON.parse(value);
return Promise.resolve();
} catch {
return Promise.reject(new Error("Must be valid JSON"));
return Promise.reject(new Error(t("mcpTools.oAuthFormFields.mustBeValidJson")));
}
},
},
@ -224,35 +260,37 @@ const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
<Form.Item
label={
<FieldLabel
label="Token Storage TTL (seconds, optional)"
tooltip="How long to cache each user's OAuth access token in Redis before evicting it (regardless of the token's own expires_in). Leave blank to derive the TTL from the token's expires_in, or fall back to the 12-hour default."
label={t("mcpTools.oAuthFormFields.tokenStorageTtlLabel")}
tooltip={t("mcpTools.oAuthFormFields.tokenStorageTtlTooltip")}
/>
}
name="token_storage_ttl_seconds"
>
<InputNumber min={1} placeholder="e.g. 3600" className="w-full rounded-lg" style={{ width: "100%" }} />
<InputNumber
min={1}
placeholder={t("mcpTools.oAuthFormFields.tokenStorageTtlPlaceholder")}
className="w-full rounded-lg"
style={{ width: "100%" }}
/>
</Form.Item>
{oauthFlow && (
<div className="rounded-lg border border-dashed border-gray-300 p-4 space-y-2">
<p className="text-sm text-gray-600">
Use OAuth to fetch a fresh access token and temporarily save it in the session as the authentication
value.
</p>
<p className="text-sm text-gray-600">{t("mcpTools.oAuthFormFields.oauthFlowDescription")}</p>
<Button
variant="secondary"
onClick={oauthFlow.startOAuthFlow}
disabled={oauthFlow.status === "authorizing" || oauthFlow.status === "exchanging"}
>
{oauthFlow.status === "authorizing"
? "Waiting for authorization..."
? t("mcpTools.oAuthFormFields.waitingForAuthorization")
: oauthFlow.status === "exchanging"
? "Exchanging authorization code..."
: "Authorize & Fetch Token"}
? t("mcpTools.oAuthFormFields.exchangingCode")
: t("mcpTools.oAuthFormFields.authorizeAndFetch")}
</Button>
{oauthFlow.error && <p className="text-sm text-red-500">{oauthFlow.error}</p>}
{oauthFlow.status === "success" && oauthFlow.tokenResponse?.access_token && (
<p className="text-sm text-green-600">
Token fetched. Expires in {oauthFlow.tokenResponse.expires_in ?? "?"} seconds.
{t("mcpTools.oAuthFormFields.tokenFetched", { seconds: oauthFlow.tokenResponse.expires_in ?? "?" })}
</p>
)}
</div>

View file

@ -2,6 +2,7 @@ import React, { useState } from "react";
import { Form, Input, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { FormInstance } from "antd/es/form";
import { useTranslation } from "react-i18next";
import { AUTH_TYPE, OAUTH_FLOW } from "./types";
import OpenAPIQuickPicker, { OpenAPIRegistryEntry, OpenAPIKeyTool } from "./OpenAPIQuickPicker";
@ -31,6 +32,7 @@ const OpenAPIFormSection: React.FC<OpenAPIFormSectionProps> = ({
onLogoUrlChange,
onOAuthDocsUrlChange,
}) => {
const { t } = useTranslation();
const [selectedPreset, setSelectedPreset] = useState<string | null>(null);
const handlePresetSelect = (entry: OpenAPIRegistryEntry) => {
@ -66,14 +68,14 @@ const OpenAPIFormSection: React.FC<OpenAPIFormSectionProps> = ({
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
OpenAPI Spec URL
<Tooltip title="URL to an OpenAPI specification (JSON or YAML). MCP tools will be automatically generated from the API endpoints defined in the spec.">
{t("mcpTools.openAPIFormSection.specUrlLabel")}
<Tooltip title={t("mcpTools.openAPIFormSection.specUrlTooltip")}>
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name="spec_path"
rules={[{ required: true, message: "Please enter an OpenAPI spec URL" }]}
rules={[{ required: true, message: t("mcpTools.openAPIFormSection.specUrlRequired") }]}
>
<Input
placeholder="https://petstore3.swagger.io/api/v3/openapi.json"

View file

@ -1,5 +1,6 @@
import React, { useEffect, useState } from "react";
import { Spin } from "antd";
import { useTranslation } from "react-i18next";
import { fetchOpenAPIRegistry } from "../networking";
export interface OpenAPIKeyTool {
@ -29,6 +30,7 @@ interface OpenAPIQuickPickerProps {
}
const OpenAPIQuickPicker: React.FC<OpenAPIQuickPickerProps> = ({ accessToken, selectedName, onSelect }) => {
const { t } = useTranslation();
const [apis, setApis] = useState<OpenAPIRegistryEntry[]>([]);
const [loading, setLoading] = useState(false);
const [imgErrors, setImgErrors] = useState<Set<string>>(new Set());
@ -49,7 +51,7 @@ const OpenAPIQuickPicker: React.FC<OpenAPIQuickPickerProps> = ({ accessToken, se
if (loading) {
return (
<div className="mb-4">
<span className="text-sm font-medium text-gray-700">Popular APIs</span>
<span className="text-sm font-medium text-gray-700">{t("mcpTools.openAPIQuickPicker.popularApis")}</span>
<div className="flex justify-center py-6">
<Spin size="small" />
</div>
@ -61,7 +63,9 @@ const OpenAPIQuickPicker: React.FC<OpenAPIQuickPickerProps> = ({ accessToken, se
return (
<div className="mb-4">
<span className="text-sm font-medium text-gray-700 block mb-2">Popular APIs</span>
<span className="text-sm font-medium text-gray-700 block mb-2">
{t("mcpTools.openAPIQuickPicker.popularApis")}
</span>
<div className="grid grid-cols-5 gap-2">
{apis.map((api) => {
@ -98,9 +102,7 @@ const OpenAPIQuickPicker: React.FC<OpenAPIQuickPickerProps> = ({ accessToken, se
})}
</div>
<p className="text-xs text-gray-400 mt-2">
Select an API to pre-fill the spec URL and OAuth 2.0 settings, or enter your own spec URL below.
</p>
<p className="text-xs text-gray-400 mt-2">{t("mcpTools.openAPIQuickPicker.selectHint")}</p>
</div>
);
};

View file

@ -1,6 +1,7 @@
import React from "react";
import { Form, Input, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
interface StdioConfigurationProps {
isVisible: boolean;
@ -12,21 +13,23 @@ interface StdioConfigurationProps {
}
const StdioConfiguration: React.FC<StdioConfigurationProps> = ({ isVisible, required = true }) => {
const { t } = useTranslation();
if (!isVisible) return null;
return (
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Stdio Configuration (JSON)
<Tooltip title="Paste your stdio MCP server configuration in JSON format. You can use the full mcpServers structure from config.yaml or just the inner server configuration.">
{t("mcpTools.stdioConfiguration.label")}
<Tooltip title={t("mcpTools.stdioConfiguration.tooltip")}>
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name="stdio_config"
rules={[
...(required ? [{ required: true, message: "Please enter stdio configuration" }] : []),
...(required ? [{ required: true, message: t("mcpTools.stdioConfiguration.required") }] : []),
{
validator: (_, value) => {
if (!value) return Promise.resolve();
@ -34,7 +37,7 @@ const StdioConfiguration: React.FC<StdioConfigurationProps> = ({ isVisible, requ
JSON.parse(value);
return Promise.resolve();
} catch {
return Promise.reject("Please enter valid JSON");
return Promise.reject(t("mcpTools.stdioConfiguration.invalidJson"));
}
},
},

View file

@ -3,6 +3,7 @@ import { Button, TextInput } from "@tremor/react";
import { MCPTool, InputSchema, InputSchemaProperty } from "./types";
import { Form, Select, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
import NotificationsManager from "../molecules/notifications_manager";
const isPlainObject = (value: unknown): value is Record<string, any> =>
@ -113,6 +114,7 @@ export function ToolTestPanel({
error: Error | null;
onClose: () => void;
}) {
const { t } = useTranslation();
const [form] = Form.useForm();
const [viewMode, setViewMode] = React.useState<"formatted" | "json">("formatted");
const [startTime, setStartTime] = React.useState<number | null>(null);
@ -278,18 +280,18 @@ export function ToolTestPanel({
const handleCopyResult = async () => {
const success = await copyToClipboard(JSON.stringify(result, null, 2));
if (success) {
NotificationsManager.success("Result copied to clipboard");
NotificationsManager.success(t("mcpTools.toolTestPanel.resultCopied"));
} else {
NotificationsManager.fromBackend("Failed to copy result");
NotificationsManager.fromBackend(t("mcpTools.toolTestPanel.copyResultFailed"));
}
};
const handleCopyToolName = async () => {
const success = await copyToClipboard(tool.name);
if (success) {
NotificationsManager.success("Tool name copied to clipboard");
NotificationsManager.success(t("mcpTools.toolTestPanel.toolNameCopied"));
} else {
NotificationsManager.fromBackend("Failed to copy tool name");
NotificationsManager.fromBackend(t("mcpTools.toolTestPanel.copyToolNameFailed"));
}
};
@ -308,11 +310,11 @@ export function ToolTestPanel({
)}
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-1">
<h2 className="text-lg font-semibold text-gray-900">Test Tool:</h2>
<h2 className="text-lg font-semibold text-gray-900">{t("mcpTools.toolTestPanel.testToolTitle")}</h2>
<div
className="group inline-flex items-center space-x-1 bg-slate-50 hover:bg-slate-100 px-3 py-1 rounded-md cursor-pointer transition-colors border border-slate-200"
onClick={handleCopyToolName}
title="Click to copy tool name"
title={t("mcpTools.toolTestPanel.clickToCopyToolName")}
>
<span className="font-mono text-slate-700 font-medium text-sm">{tool.name}</span>
<svg
@ -331,7 +333,9 @@ export function ToolTestPanel({
</div>
</div>
<p className="text-xs text-gray-600">{tool.description}</p>
<p className="text-xs text-gray-500">Provider: {tool.mcp_info.server_name}</p>
<p className="text-xs text-gray-500">
{t("mcpTools.toolTestPanel.provider", { name: tool.mcp_info.server_name })}
</p>
</div>
</div>
<Button onClick={onClose} variant="light" size="sm" className="text-gray-500 hover:text-gray-700">
@ -347,8 +351,8 @@ export function ToolTestPanel({
<div className="bg-white border border-gray-200 rounded-lg">
<div className="border-b border-gray-100 px-4 py-2">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-gray-900">Input Parameters</h3>
<Tooltip title="Configure the input parameters for this tool call">
<h3 className="text-sm font-semibold text-gray-900">{t("mcpTools.toolTestPanel.inputParameters")}</h3>
<Tooltip title={t("mcpTools.toolTestPanel.inputParametersTooltip")}>
<InfoCircleOutlined className="text-gray-400 hover:text-gray-600" />
</Tooltip>
</div>
@ -361,15 +365,15 @@ export function ToolTestPanel({
<Form.Item
label={
<span className="text-sm font-medium text-gray-700">
Input <span className="text-red-500">*</span>
{t("mcpTools.toolTestPanel.inputLabel")} <span className="text-red-500">*</span>
</span>
}
name="input"
rules={[{ required: true, message: "Please enter input for this tool" }]}
rules={[{ required: true, message: t("mcpTools.toolTestPanel.inputRequired") }]}
className="mb-3"
>
<TextInput
placeholder="Enter input for this tool"
placeholder={t("mcpTools.toolTestPanel.inputPlaceholder")}
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
@ -377,8 +381,10 @@ export function ToolTestPanel({
) : actualSchema.properties === undefined ? (
<div className="text-center py-6 bg-gray-50 rounded-lg border border-gray-200">
<div className="max-w-sm mx-auto">
<h4 className="text-sm font-medium text-gray-900 mb-1">No Parameters Required</h4>
<p className="text-xs text-gray-500">This tool can be called without any input parameters.</p>
<h4 className="text-sm font-medium text-gray-900 mb-1">
{t("mcpTools.toolTestPanel.noParametersTitle")}
</h4>
<p className="text-xs text-gray-500">{t("mcpTools.toolTestPanel.noParametersDesc")}</p>
</div>
</div>
) : (
@ -404,7 +410,7 @@ export function ToolTestPanel({
rules={[
{
required: actualSchema.required?.includes(key),
message: `Please enter ${key}`,
message: t("mcpTools.toolTestPanel.fieldRequired", { key }),
},
...(prop.type === "object" || prop.type === "array"
? [
@ -436,12 +442,12 @@ export function ToolTestPanel({
return Promise.reject(
new Error(
prop.type === "object"
? "Please enter a JSON object"
: "Please enter a JSON array",
? t("mcpTools.toolTestPanel.invalidJsonObject")
: t("mcpTools.toolTestPanel.invalidJsonArray"),
),
);
} catch (error) {
return Promise.reject(new Error("Invalid JSON"));
return Promise.reject(new Error(t("mcpTools.toolTestPanel.invalidJson")));
}
},
},
@ -455,7 +461,9 @@ export function ToolTestPanel({
className="w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-colors"
defaultValue={(initialValue as string) ?? ""}
>
{!actualSchema.required?.includes(key) && <option value="">Select {key}</option>}
{!actualSchema.required?.includes(key) && (
<option value="">{t("mcpTools.toolTestPanel.selectKey", { key })}</option>
)}
{prop.enum.map((value) => (
<option key={value} value={value}>
{value}
@ -466,7 +474,7 @@ export function ToolTestPanel({
{prop.type === "string" && !prop.enum && (
<TextInput
placeholder={prop.description || `Enter ${key}`}
placeholder={prop.description || t("mcpTools.toolTestPanel.enterKey", { key })}
defaultValue={(initialValue as string) ?? ""}
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
@ -476,7 +484,7 @@ export function ToolTestPanel({
<input
type="number"
step={prop.type === "integer" ? 1 : "any"}
placeholder={prop.description || `Enter ${key}`}
placeholder={prop.description || t("mcpTools.toolTestPanel.enterKey", { key })}
defaultValue={initialValue ?? 0}
className="w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-colors"
/>
@ -484,12 +492,12 @@ export function ToolTestPanel({
{prop.type === "boolean" && (
<Select
placeholder={`Select ${key}`}
placeholder={t("mcpTools.toolTestPanel.selectKey", { key })}
allowClear={!actualSchema.required?.includes(key)}
className="w-full"
>
<Select.Option value={true}>True</Select.Option>
<Select.Option value={false}>False</Select.Option>
<Select.Option value={true}>{t("mcpTools.toolTestPanel.booleanTrue")}</Select.Option>
<Select.Option value={false}>{t("mcpTools.toolTestPanel.booleanFalse")}</Select.Option>
</Select>
)}
@ -500,8 +508,8 @@ export function ToolTestPanel({
placeholder={
prop.description ||
(prop.type === "object"
? `Enter JSON object for ${key}`
: `Enter JSON array for ${key}`)
? t("mcpTools.toolTestPanel.jsonObjectPlaceholder", { key })
: t("mcpTools.toolTestPanel.jsonArrayPlaceholder", { key }))
}
defaultValue={(initialValue as string) ?? (prop.type === "object" ? "{}" : "[]")}
spellCheck={false}
@ -509,7 +517,9 @@ export function ToolTestPanel({
className="w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm font-mono"
/>
<p className="text-xs text-gray-500">
{prop.type === "object" ? "Provide a valid JSON object." : "Provide a valid JSON array."}
{prop.type === "object"
? t("mcpTools.toolTestPanel.provideJsonObject")
: t("mcpTools.toolTestPanel.provideJsonArray")}
</p>
</div>
)}
@ -528,7 +538,11 @@ export function ToolTestPanel({
className="w-full"
loading={isLoading}
>
{isLoading ? "Calling Tool..." : result || error ? "Call Again" : "Call Tool"}
{isLoading
? t("mcpTools.toolTestPanel.callingTool")
: result || error
? t("mcpTools.toolTestPanel.callAgain")
: t("mcpTools.toolTestPanel.callTool")}
</Button>
</div>
</Form>
@ -538,7 +552,7 @@ export function ToolTestPanel({
{/* Right Column - Tool Result */}
<div className="bg-white border border-gray-200 rounded-lg">
<div className="border-b border-gray-100 px-4 py-2">
<h3 className="text-sm font-semibold text-gray-900">Tool Result</h3>
<h3 className="text-sm font-semibold text-gray-900">{t("mcpTools.toolTestPanel.toolResult")}</h3>
</div>
<div className="p-4">
@ -561,10 +575,10 @@ export function ToolTestPanel({
/>
</svg>
</div>
<h4 className="text-sm font-medium text-gray-900 mb-1">Ready to Call Tool</h4>
<p className="text-xs text-gray-500 leading-relaxed">
Configure the input parameters and click &quot;Call Tool&quot; to see the results here.
</p>
<h4 className="text-sm font-medium text-gray-900 mb-1">
{t("mcpTools.toolTestPanel.readyToCallTitle")}
</h4>
<p className="text-xs text-gray-500 leading-relaxed">{t("mcpTools.toolTestPanel.readyToCallDesc")}</p>
</div>
</div>
) : (
@ -582,7 +596,9 @@ export function ToolTestPanel({
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<h4 className="text-xs font-medium text-green-900">Tool executed successfully</h4>
<h4 className="text-xs font-medium text-green-900">
{t("mcpTools.toolTestPanel.executedSuccessfully")}
</h4>
{duration !== null && (
<span className="text-xs text-green-600 ml-1">• {(duration / 1000).toFixed(2)}s</span>
)}
@ -598,7 +614,7 @@ export function ToolTestPanel({
: "text-green-600 hover:text-green-800"
}`}
>
Formatted
{t("mcpTools.toolTestPanel.viewFormatted")}
</button>
<button
onClick={() => setViewMode("json")}
@ -608,14 +624,14 @@ export function ToolTestPanel({
: "text-green-600 hover:text-green-800"
}`}
>
JSON
{t("mcpTools.toolTestPanel.viewJson")}
</button>
</div>
<button
onClick={handleCopyResult}
className="p-1 hover:bg-green-100 rounded text-green-700"
title="Copy response"
title={t("mcpTools.toolTestPanel.copyResponse")}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -644,8 +660,8 @@ export function ToolTestPanel({
<div className="animate-spin rounded-full h-8 w-8 border-2 border-gray-200"></div>
<div className="animate-spin rounded-full h-8 w-8 border-2 border-blue-600 border-t-transparent absolute top-0"></div>
</div>
<p className="text-sm font-medium mt-3">Calling tool...</p>
<p className="text-xs text-gray-400 mt-1">Please wait while we process your request</p>
<p className="text-sm font-medium mt-3">{t("mcpTools.toolTestPanel.callingToolProgress")}</p>
<p className="text-xs text-gray-400 mt-1">{t("mcpTools.toolTestPanel.pleaseWait")}</p>
</div>
)}
@ -664,7 +680,9 @@ export function ToolTestPanel({
</div>
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1">
<h4 className="text-xs font-medium text-red-900">Tool Call Failed</h4>
<h4 className="text-xs font-medium text-red-900">
{t("mcpTools.toolTestPanel.toolCallFailed")}
</h4>
{duration !== null && (
<span className="text-xs text-red-600">• {(duration / 1000).toFixed(2)}s</span>
)}
@ -691,7 +709,7 @@ export function ToolTestPanel({
<div>
<div className="bg-gray-50 px-3 py-1 border-b border-gray-200">
<span className="text-xs font-medium text-gray-700 uppercase tracking-wide">
Text Response
{t("mcpTools.toolTestPanel.textResponse")}
</span>
</div>
<div className="p-3">
@ -780,7 +798,7 @@ export function ToolTestPanel({
<div>
<div className="bg-gray-50 px-3 py-1 border-b border-gray-200">
<span className="text-xs font-medium text-gray-700 uppercase tracking-wide">
Image Response
{t("mcpTools.toolTestPanel.imageResponse")}
</span>
</div>
<div className="p-3">
@ -800,7 +818,7 @@ export function ToolTestPanel({
<div>
<div className="bg-gray-50 px-3 py-1 border-b border-gray-200">
<span className="text-xs font-medium text-gray-700 uppercase tracking-wide">
Embedded Resource
{t("mcpTools.toolTestPanel.embeddedResource")}
</span>
</div>
<div className="p-3">
@ -822,7 +840,7 @@ export function ToolTestPanel({
</div>
<div className="flex-1">
<p className="text-xs font-medium text-blue-900">
Resource Type: {content.resource_type}
{t("mcpTools.toolTestPanel.resourceType", { type: content.resource_type })}
</p>
{content.url && (
<a
@ -831,7 +849,7 @@ export function ToolTestPanel({
rel="noopener noreferrer"
className="inline-flex items-center text-xs text-blue-600 hover:text-blue-800 hover:underline mt-1 transition-colors"
>
View Resource
{t("mcpTools.toolTestPanel.viewResource")}
<svg className="ml-1 h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />

View file

@ -3262,6 +3262,115 @@
"howToolsetsWorkPath": "API Keys → Edit Key → MCP Servers",
"howToolsetsWorkDesc2": ", then point your MCP client at the toolset URL. The client only sees the tools you picked.",
"configLabel": "Claude Code / Cursor config"
},
"oAuthFormFields": {
"leaveBlankToKeep": "leave blank to keep existing",
"flowTypeLabel": "OAuth Flow Type",
"flowTypeTooltip": "Choose how the proxy authenticates with this MCP server. M2M is for server-to-server communication using client credentials. Interactive (PKCE) is for user-facing flows that require browser-based authorization.",
"m2mLabel": "Machine-to-Machine (M2M)",
"m2mDesc": "server-to-server, no user interaction",
"interactiveLabel": "Interactive (PKCE)",
"interactiveDesc": "browser-based user authorization",
"clientIdLabel": "Client ID",
"clientIdM2MTooltip": "OAuth2 client ID for the client_credentials grant.",
"clientIdM2MRequired": "Client ID is required for M2M OAuth",
"clientIdM2MPlaceholder": "Enter OAuth client ID",
"clientSecretLabel": "Client Secret",
"clientSecretM2MTooltip": "OAuth2 client secret for the client_credentials grant.",
"clientSecretM2MRequired": "Client Secret is required for M2M OAuth",
"clientSecretM2MPlaceholder": "Enter OAuth client secret",
"tokenUrlLabel": "Token URL",
"tokenUrlM2MTooltip": "Token endpoint URL for the client_credentials grant.",
"tokenUrlM2MRequired": "Token URL is required for M2M OAuth",
"scopesOptionalLabel": "Scopes (optional)",
"scopesM2MTooltip": "Optional scopes to request with the client_credentials grant.",
"scopesInteractiveTooltip": "Optional scopes requested during token exchange. Separate multiple scopes with enter or commas.",
"scopesPlaceholder": "Add scopes",
"clientIdOptionalLabel": "Client ID (optional)",
"clientIdInteractiveTooltip": "Provide only if your MCP server cannot handle dynamic client registration.",
"clientIdInteractivePlaceholder": "Enter client ID",
"clientSecretOptionalLabel": "Client Secret (optional)",
"clientSecretInteractiveTooltip": "Provide only if your MCP server cannot handle dynamic client registration.",
"clientSecretInteractivePlaceholder": "Enter client secret",
"authorizationUrlOptionalLabel": "Authorization URL (optional)",
"authorizationUrlTooltip": "Optional override for the authorization endpoint.",
"tokenUrlOptionalLabel": "Token URL (optional)",
"tokenUrlInteractiveTooltip": "Optional override for the token endpoint.",
"registrationUrlOptionalLabel": "Registration URL (optional)",
"registrationUrlTooltip": "Optional override for the dynamic client registration endpoint.",
"tokenValidationRulesLabel": "Token Validation Rules (optional)",
"tokenValidationRulesTooltip": "JSON object of key-value rules checked against the OAuth token response before storing. Supports dot-notation for nested fields (e.g. {\"organization\": \"my-org\", \"team.id\": \"123\"}). Tokens that fail validation are rejected with HTTP 403.",
"mustBeValidJson": "Must be valid JSON",
"tokenStorageTtlLabel": "Token Storage TTL (seconds, optional)",
"tokenStorageTtlTooltip": "How long to cache each user's OAuth access token in Redis before evicting it (regardless of the token's own expires_in). Leave blank to derive the TTL from the token's expires_in, or fall back to the 12-hour default.",
"tokenStorageTtlPlaceholder": "e.g. 3600",
"createOAuthAppLink": "Create OAuth App",
"oauthFlowDescription": "Use OAuth to fetch a fresh access token and temporarily save it in the session as the authentication value.",
"waitingForAuthorization": "Waiting for authorization...",
"exchangingCode": "Exchanging authorization code...",
"authorizeAndFetch": "Authorize & Fetch Token",
"tokenFetched": "Token fetched. Expires in {{seconds}} seconds."
},
"openAPIFormSection": {
"specUrlLabel": "OpenAPI Spec URL",
"specUrlTooltip": "URL to an OpenAPI specification (JSON or YAML). MCP tools will be automatically generated from the API endpoints defined in the spec.",
"specUrlRequired": "Please enter an OpenAPI spec URL"
},
"openAPIQuickPicker": {
"popularApis": "Popular APIs",
"selectHint": "Select an API to pre-fill the spec URL and OAuth 2.0 settings, or enter your own spec URL below."
},
"stdioConfiguration": {
"label": "Stdio Configuration (JSON)",
"tooltip": "Paste your stdio MCP server configuration in JSON format. You can use the full mcpServers structure from config.yaml or just the inner server configuration.",
"required": "Please enter stdio configuration",
"invalidJson": "Please enter valid JSON"
},
"toolTestPanel": {
"testToolTitle": "Test Tool:",
"clickToCopyToolName": "Click to copy tool name",
"provider": "Provider: {{name}}",
"inputParameters": "Input Parameters",
"inputParametersTooltip": "Configure the input parameters for this tool call",
"inputLabel": "Input",
"inputRequired": "Please enter input for this tool",
"inputPlaceholder": "Enter input for this tool",
"noParametersTitle": "No Parameters Required",
"noParametersDesc": "This tool can be called without any input parameters.",
"fieldRequired": "Please enter {{key}}",
"invalidJsonObject": "Please enter a JSON object",
"invalidJsonArray": "Please enter a JSON array",
"invalidJson": "Invalid JSON",
"selectKey": "Select {{key}}",
"enterKey": "Enter {{key}}",
"booleanTrue": "True",
"booleanFalse": "False",
"jsonObjectPlaceholder": "Enter JSON object for {{key}}",
"jsonArrayPlaceholder": "Enter JSON array for {{key}}",
"provideJsonObject": "Provide a valid JSON object.",
"provideJsonArray": "Provide a valid JSON array.",
"callingTool": "Calling Tool...",
"callAgain": "Call Again",
"callTool": "Call Tool",
"toolResult": "Tool Result",
"readyToCallTitle": "Ready to Call Tool",
"readyToCallDesc": "Configure the input parameters and click \"Call Tool\" to see the results here.",
"executedSuccessfully": "Tool executed successfully",
"viewFormatted": "Formatted",
"viewJson": "JSON",
"copyResponse": "Copy response",
"callingToolProgress": "Calling tool...",
"pleaseWait": "Please wait while we process your request",
"toolCallFailed": "Tool Call Failed",
"textResponse": "Text Response",
"imageResponse": "Image Response",
"embeddedResource": "Embedded Resource",
"resourceType": "Resource Type: {{type}}",
"viewResource": "View Resource",
"resultCopied": "Result copied to clipboard",
"copyResultFailed": "Failed to copy result",
"toolNameCopied": "Tool name copied to clipboard",
"copyToolNameFailed": "Failed to copy tool name"
}
}
}

View file

@ -3262,6 +3262,115 @@
"howToolsetsWorkPath": "API 密钥 → 编辑密钥 → MCP 服务器",
"howToolsetsWorkDesc2": "将其分配给密钥,然后将您的 MCP 客户端指向工具集 URL。客户端只会看到您选定的工具。",
"configLabel": "Claude Code / Cursor 配置"
},
"oAuthFormFields": {
"leaveBlankToKeep": "留空以保留现有值",
"flowTypeLabel": "OAuth 流程类型",
"flowTypeTooltip": "选择代理与此 MCP 服务器的认证方式。M2M 使用客户端凭据进行服务器间通信;Interactive (PKCE) 用于需要浏览器授权的用户交互流程。",
"m2mLabel": "机器对机器 (M2M)",
"m2mDesc": "服务器间通信,无需用户交互",
"interactiveLabel": "Interactive (PKCE)",
"interactiveDesc": "基于浏览器的用户授权",
"clientIdLabel": "Client ID",
"clientIdM2MTooltip": "client_credentials 授权方式的 OAuth2 Client ID。",
"clientIdM2MRequired": "M2M OAuth 需要提供 Client ID",
"clientIdM2MPlaceholder": "输入 OAuth Client ID",
"clientSecretLabel": "Client Secret",
"clientSecretM2MTooltip": "client_credentials 授权方式的 OAuth2 Client Secret。",
"clientSecretM2MRequired": "M2M OAuth 需要提供 Client Secret",
"clientSecretM2MPlaceholder": "输入 OAuth Client Secret",
"tokenUrlLabel": "Token URL",
"tokenUrlM2MTooltip": "client_credentials 授权方式的 Token 端点 URL。",
"tokenUrlM2MRequired": "M2M OAuth 需要提供 Token URL",
"scopesOptionalLabel": "Scopes(可选)",
"scopesM2MTooltip": "client_credentials 授权时可选择请求的 scopes。",
"scopesInteractiveTooltip": "Token 交换时请求的可选 scopes,多个 scopes 可用回车或逗号分隔。",
"scopesPlaceholder": "添加 scopes",
"clientIdOptionalLabel": "Client ID(可选)",
"clientIdInteractiveTooltip": "仅在您的 MCP 服务器不支持动态客户端注册时提供。",
"clientIdInteractivePlaceholder": "输入 Client ID",
"clientSecretOptionalLabel": "Client Secret(可选)",
"clientSecretInteractiveTooltip": "仅在您的 MCP 服务器不支持动态客户端注册时提供。",
"clientSecretInteractivePlaceholder": "输入 Client Secret",
"authorizationUrlOptionalLabel": "Authorization URL(可选)",
"authorizationUrlTooltip": "授权端点的可选覆盖地址。",
"tokenUrlOptionalLabel": "Token URL(可选)",
"tokenUrlInteractiveTooltip": "Token 端点的可选覆盖地址。",
"registrationUrlOptionalLabel": "Registration URL(可选)",
"registrationUrlTooltip": "动态客户端注册端点的可选覆盖地址。",
"tokenValidationRulesLabel": "Token 验证规则(可选)",
"tokenValidationRulesTooltip": "存储前对 OAuth Token 响应进行校验的键值规则 JSON 对象。支持点号表示法访问嵌套字段(如 {\"organization\": \"my-org\", \"team.id\": \"123\"})。验证失败的 Token 将以 HTTP 403 拒绝。",
"mustBeValidJson": "必须是有效的 JSON",
"tokenStorageTtlLabel": "Token 存储 TTL(秒,可选)",
"tokenStorageTtlTooltip": "在 Redis 中缓存每位用户 OAuth 访问 Token 的时长(与 Token 自身的 expires_in 无关)。留空则从 Token 的 expires_in 推导 TTL,或回退到默认的 12 小时。",
"tokenStorageTtlPlaceholder": "例如:3600",
"createOAuthAppLink": "创建 OAuth App",
"oauthFlowDescription": "使用 OAuth 获取新的访问 Token,并临时保存在会话中作为认证凭据。",
"waitingForAuthorization": "等待授权...",
"exchangingCode": "正在交换授权码...",
"authorizeAndFetch": "授权并获取 Token",
"tokenFetched": "Token 已获取,有效期 {{seconds}} 秒。"
},
"openAPIFormSection": {
"specUrlLabel": "OpenAPI Spec URL",
"specUrlTooltip": "OpenAPI 规范文件的 URL(JSON 或 YAML 格式)。MCP 工具将自动根据规范中定义的 API 端点生成。",
"specUrlRequired": "请输入 OpenAPI Spec URL"
},
"openAPIQuickPicker": {
"popularApis": "热门 API",
"selectHint": "选择 API 可自动填充 Spec URL 和 OAuth 2.0 设置,或在下方输入自定义 Spec URL。"
},
"stdioConfiguration": {
"label": "Stdio 配置(JSON)",
"tooltip": "以 JSON 格式粘贴您的 stdio MCP 服务器配置。可使用 config.yaml 中的完整 mcpServers 结构,或仅填写内部服务器配置。",
"required": "请输入 stdio 配置",
"invalidJson": "请输入有效的 JSON"
},
"toolTestPanel": {
"testToolTitle": "测试工具:",
"clickToCopyToolName": "点击复制工具名称",
"provider": "提供商:{{name}}",
"inputParameters": "输入参数",
"inputParametersTooltip": "配置此工具调用的输入参数",
"inputLabel": "输入",
"inputRequired": "请为此工具输入内容",
"inputPlaceholder": "为此工具输入内容",
"noParametersTitle": "无需参数",
"noParametersDesc": "此工具无需任何输入参数即可调用。",
"fieldRequired": "请输入 {{key}}",
"invalidJsonObject": "请输入 JSON 对象",
"invalidJsonArray": "请输入 JSON 数组",
"invalidJson": "无效的 JSON",
"selectKey": "选择 {{key}}",
"enterKey": "输入 {{key}}",
"booleanTrue": "True",
"booleanFalse": "False",
"jsonObjectPlaceholder": "为 {{key}} 输入 JSON 对象",
"jsonArrayPlaceholder": "为 {{key}} 输入 JSON 数组",
"provideJsonObject": "请提供有效的 JSON 对象。",
"provideJsonArray": "请提供有效的 JSON 数组。",
"callingTool": "调用中...",
"callAgain": "再次调用",
"callTool": "调用工具",
"toolResult": "工具结果",
"readyToCallTitle": "准备就绪",
"readyToCallDesc": "配置输入参数后点击「调用工具」查看结果。",
"executedSuccessfully": "工具执行成功",
"viewFormatted": "格式化",
"viewJson": "JSON",
"copyResponse": "复制响应",
"callingToolProgress": "正在调用工具...",
"pleaseWait": "请稍候,正在处理您的请求",
"toolCallFailed": "工具调用失败",
"textResponse": "文本响应",
"imageResponse": "图片响应",
"embeddedResource": "嵌入资源",
"resourceType": "资源类型:{{type}}",
"viewResource": "查看资源",
"resultCopied": "结果已复制到剪贴板",
"copyResultFailed": "复制结果失败",
"toolNameCopied": "工具名称已复制到剪贴板",
"copyToolNameFailed": "复制工具名称失败"
}
}
}