diff --git a/openapi.json b/openapi.json index 7af09ad..99e3566 100644 --- a/openapi.json +++ b/openapi.json @@ -36803,6 +36803,89 @@ } ] } + }, + "/v1/mcp-servers/test-handshake": { + "post": { + "tags": [ + "MCP Servers" + ], + "summary": "Check Mcp Server Handshake", + "description": "Test whether an MCP server URL speaks MCP via a protocol handshake.\n\nDelegates to ``test_gateway_handshake`` in\n``mcpgateway.services.gateway_service``, which tries the stateless\n``server/discover`` method first and falls back to a stateful SDK\n``initialize`` round-trip, classifying failures for actionable UI copy.\n\nArgs:\n request (GatewayHandshakeRequest): The request object containing the server URL and optional headers.\n team_id (Optional[str]): Optional team ID for team-specific gateways.\n user: Authenticated user context.\n db (Session): Database session dependency.\n\nReturns:\n GatewayHandshakeResponse: The handshake outcome, including negotiation path,\n server identity, capabilities, component counts, and failure classification.\n\nExamples:\n >>> callable(check_mcp_server_handshake)\n True\n >>> check_mcp_server_handshake.__name__\n 'check_mcp_server_handshake'", + "operationId": "check_mcp_server_handshake_v1_mcp_servers_test_handshake_post", + "security": [ + { + "ConfigurableHTTPBearer": [] + } + ], + "parameters": [ + { + "name": "team_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Filter by team ID", + "title": "Team Id" + }, + "description": "Filter by team ID" + }, + { + "name": "jwt_token", + "in": "cookie", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Jwt Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GatewayHandshakeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GatewayHandshakeResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } } }, "components": { @@ -52526,6 +52609,203 @@ "type" ], "title": "ValidationError" + }, + "GatewayHandshakeRequest": { + "properties": { + "baseUrl": { + "type": "string", + "minLength": 1, + "format": "uri", + "title": "Baseurl", + "description": "Base URL of the MCP server to test" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Path", + "description": "Optional path appended to the base URL" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Headers", + "description": "Optional headers (e.g. Authorization) sent with the handshake" + } + }, + "type": "object", + "required": [ + "baseUrl" + ], + "title": "GatewayHandshakeRequest", + "description": "Request to run an MCP handshake test against a server URL.", + "nullable": true + }, + "GatewayHandshakeResponse": { + "properties": { + "success": { + "type": "boolean", + "title": "Success" + }, + "latencyMs": { + "type": "integer", + "title": "Latencyms" + }, + "negotiationPath": { + "anyOf": [ + { + "type": "string", + "enum": [ + "server_discover", + "initialize" + ] + }, + { + "type": "null" + } + ], + "title": "Negotiationpath", + "description": "Which handshake path produced the result" + }, + "protocolVersion": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Protocolversion" + }, + "serverName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Servername" + }, + "serverVersion": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Serverversion" + }, + "capabilities": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Capabilities" + }, + "componentCounts": { + "anyOf": [ + { + "additionalProperties": { + "type": "integer" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Componentcounts", + "description": "Counts for tools/resources/prompts; a key is absent when the capability is not advertised" + }, + "countsPartial": { + "type": "boolean", + "title": "Countspartial", + "description": "True when any list result had a nextCursor (counts are first-page lower bounds)", + "default": false + }, + "credentialSource": { + "type": "string", + "enum": [ + "stored", + "form", + "none" + ], + "title": "Credentialsource", + "default": "none" + }, + "failureClass": { + "anyOf": [ + { + "type": "string", + "enum": [ + "transport", + "protocol", + "auth", + "invalid_response" + ] + }, + { + "type": "null" + } + ], + "title": "Failureclass" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error" + }, + "rawPreview": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Rawpreview", + "description": "Size-capped JSON preview of the final handshake payload" + } + }, + "type": "object", + "required": [ + "success", + "latencyMs" + ], + "title": "GatewayHandshakeResponse", + "description": "Result of an MCP handshake test.", + "nullable": true } }, "securitySchemes": { diff --git a/src/api/servers.ts b/src/api/servers.ts index 539b9f8..b131491 100644 --- a/src/api/servers.ts +++ b/src/api/servers.ts @@ -7,7 +7,12 @@ import { api } from "./client"; import type { ServersResponse, MCPServer } from "../types/server"; -import type { GatewayTestRequest, GatewayTestResponse } from "@/generated/types"; +import type { + GatewayHandshakeRequest, + GatewayHandshakeResponse, + GatewayTestRequest, + GatewayTestResponse, +} from "@/generated/types"; const serverByIdRequestCache = new Map>(); @@ -130,6 +135,19 @@ export const serversApi = { return api.post("/v1/mcp-servers/test", request, { signal }); }, + /** + * Test whether an MCP server URL speaks MCP via a protocol handshake. + * + * Tries the stateless server/discover method (MCP 2026-07-28+) first and + * falls back to a stateful initialize round-trip for earlier specs. + */ + testHandshake: ( + request: GatewayHandshakeRequest, + signal?: AbortSignal, + ): Promise => { + return api.post("/v1/mcp-servers/test-handshake", request, { signal }); + }, + /** * Toggle the enabled state of an MCP server (activate/deactivate) */ diff --git a/src/components/servers/TestConnectionPanel.test.tsx b/src/components/servers/TestConnectionPanel.test.tsx index 582ae3c..9905e5f 100644 --- a/src/components/servers/TestConnectionPanel.test.tsx +++ b/src/components/servers/TestConnectionPanel.test.tsx @@ -1,11 +1,13 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render, screen, waitFor } from "@testing-library/react"; +import { screen, waitFor } from "@testing-library/react"; +import { renderWithProviders as render } from "@/test/test-utils"; import userEvent from "@testing-library/user-event"; import { http, HttpResponse } from "msw"; import { server } from "@/test/mocks/server"; import { TestConnectionPanel } from "./TestConnectionPanel"; const TEST_ENDPOINT = "*/v1/mcp-servers/test"; +const HANDSHAKE_ENDPOINT = "*/v1/mcp-servers/test-handshake"; describe("TestConnectionPanel", () => { const defaultProps = { @@ -392,4 +394,163 @@ describe("TestConnectionPanel", () => { expect(screen.queryByText(/path shouldn't include a scheme or host/i)).not.toBeInTheDocument(); }); + + describe("MCP handshake mode", () => { + it("hides Method, Content type, and Body while keeping URL, Path, and Headers", async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole("tab", { name: /mcp handshake/i })); + + expect(screen.getByLabelText(/^url/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/^path/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/headers/i)).toBeInTheDocument(); + expect(screen.queryByRole("radiogroup", { name: /method/i })).not.toBeInTheDocument(); + expect(screen.queryByLabelText(/content type/i)).not.toBeInTheDocument(); + expect(screen.queryByLabelText(/body/i)).not.toBeInTheDocument(); + }); + + it("shows the stored-credentials hint under Headers", async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole("tab", { name: /mcp handshake/i })); + + expect(screen.getByText(/stored credentials for registered servers/i)).toBeInTheDocument(); + }); + + it("renders server identity rows and component count badges on success", async () => { + const user = userEvent.setup(); + let requestBody: Record | undefined; + server.use( + http.post(HANDSHAKE_ENDPOINT, async ({ request }) => { + requestBody = (await request.json()) as Record; + return HttpResponse.json({ + success: true, + latencyMs: 12, + negotiationPath: "server_discover", + protocolVersion: "2026-07-28", + serverName: "git-server", + serverVersion: "1.2.3", + capabilities: { tools: {}, resources: {} }, + componentCounts: { tools: 3, resources: 1 }, + countsPartial: false, + credentialSource: "none", + }); + }), + ); + render(); + + await user.click(screen.getByRole("tab", { name: /mcp handshake/i })); + await user.click(screen.getByRole("button", { name: /^test connection$/i })); + + await waitFor(() => { + expect(screen.getByText(/handshake succeeded/i)).toBeInTheDocument(); + }); + expect(screen.getByText("git-server")).toBeInTheDocument(); + expect(screen.getByText("1.2.3")).toBeInTheDocument(); + expect(screen.getByText("2026-07-28")).toBeInTheDocument(); + expect(screen.getByText("server/discover")).toBeInTheDocument(); + expect(screen.getByText("3 tools")).toBeInTheDocument(); + expect(screen.getByText("1 resource")).toBeInTheDocument(); + expect(requestBody).toEqual(expect.objectContaining({ baseUrl: "https://example.com" })); + }); + + it("keeps the plural label when counts are partial", async () => { + const user = userEvent.setup(); + server.use( + http.post(HANDSHAKE_ENDPOINT, () => + HttpResponse.json({ + success: true, + latencyMs: 12, + negotiationPath: "server_discover", + protocolVersion: "2026-07-28", + serverName: "git-server", + serverVersion: "1.2.3", + capabilities: { tools: {} }, + componentCounts: { tools: 1 }, + countsPartial: true, + credentialSource: "none", + }), + ), + ); + render(); + + await user.click(screen.getByRole("tab", { name: /mcp handshake/i })); + await user.click(screen.getByRole("button", { name: /^test connection$/i })); + + await waitFor(() => { + expect(screen.getByText("1+ tools")).toBeInTheDocument(); + }); + }); + + it("clears field errors when switching modes", async () => { + const user = userEvent.setup(); + render(); + + await user.clear(screen.getByLabelText(/^url/i)); + await user.click(screen.getByRole("button", { name: /^test connection$/i })); + await waitFor(() => expect(screen.getByText(/url is required/i)).toBeInTheDocument()); + + await user.click(screen.getByRole("tab", { name: /mcp handshake/i })); + + expect(screen.queryByText(/url is required/i)).not.toBeInTheDocument(); + }); + + it.each([ + ["transport", "Transport"], + ["protocol", "Protocol negotiation"], + ["auth", "Authentication"], + ["invalid_response", "Invalid response"], + ])("renders the %s failure classification and error copy", async (failureClass, label) => { + const user = userEvent.setup(); + server.use( + http.post(HANDSHAKE_ENDPOINT, () => + HttpResponse.json({ + success: false, + latencyMs: 5, + credentialSource: "none", + failureClass, + error: "Actionable copy for the failure.", + }), + ), + ); + render(); + + await user.click(screen.getByRole("tab", { name: /mcp handshake/i })); + await user.click(screen.getByRole("button", { name: /^test connection$/i })); + + await waitFor(() => { + expect(screen.getByRole("alert")).toBeInTheDocument(); + }); + expect(screen.getByText(/handshake failed/i)).toBeInTheDocument(); + expect(screen.getByText(label)).toBeInTheDocument(); + expect(screen.getByText("Actionable copy for the failure.")).toBeInTheDocument(); + }); + + it("cancels the in-flight handshake when the panel unmounts", async () => { + const user = userEvent.setup(); + let aborted = false; + server.use( + http.post(HANDSHAKE_ENDPOINT, async ({ request }) => { + // Resolve only once the client aborts, so the test can observe cancellation. + await new Promise((resolve) => { + request.signal.addEventListener("abort", () => { + aborted = true; + resolve(); + }); + }); + return HttpResponse.json({ success: true, latencyMs: 1 }); + }), + ); + const { unmount } = render(); + + await user.click(screen.getByRole("tab", { name: /mcp handshake/i })); + await user.click(screen.getByRole("button", { name: /^test connection$/i })); + + unmount(); + + await waitFor(() => expect(aborted).toBe(true)); + }); + }); }); diff --git a/src/components/servers/TestConnectionPanel.tsx b/src/components/servers/TestConnectionPanel.tsx index 41f1514..5abcc09 100644 --- a/src/components/servers/TestConnectionPanel.tsx +++ b/src/components/servers/TestConnectionPanel.tsx @@ -7,11 +7,19 @@ import { Input } from "../ui/input"; import { Label } from "../ui/label"; import { RadioGroup } from "../ui/radio-group"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; +import { Tabs, TabsList, TabsTrigger } from "../ui/tabs"; +import { Badge } from "../ui/badge"; import { Textarea } from "../ui/textarea"; import { JsonHighlighter } from "../ui/json-highlighter"; import { copyToClipboard } from "@/lib/clipboard"; import { serversApi } from "@/api/servers"; -import type { GatewayTestRequest, GatewayTestResponse } from "@/generated/types"; +import type { + GatewayHandshakeRequest, + GatewayHandshakeResponse, + GatewayTestRequest, + GatewayTestResponse, +} from "@/generated/types"; +import { useIntl } from "react-intl"; import { parseApiError } from "@/lib/errorUtils"; import { cn } from "@/lib/utils"; @@ -20,6 +28,44 @@ interface TestConnectionPanelProps { } type TestStatus = "idle" | "testing" | "success" | "error"; +type TestMode = "http" | "handshake"; + +const SEGMENTED_TRIGGER_CLASS = + "rounded-md px-3 py-1 font-medium data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm"; + +const FAILURE_CLASS_MESSAGE_IDS: Record = { + transport: "mcpServer.testConnection.failureClass.transport", + protocol: "mcpServer.testConnection.failureClass.protocol", + auth: "mcpServer.testConnection.failureClass.auth", + invalid_response: "mcpServer.testConnection.failureClass.invalidResponse", +}; + +const CREDENTIAL_SOURCE_MESSAGE_IDS: Record = { + stored: "mcpServer.testConnection.credentialSource.stored", + form: "mcpServer.testConnection.credentialSource.form", + none: "mcpServer.testConnection.credentialSource.none", +}; + +const COUNT_MESSAGE_IDS: Record = { + tools: "mcpServer.testConnection.counts.tools", + resources: "mcpServer.testConnection.counts.resources", + prompts: "mcpServer.testConnection.counts.prompts", +}; + +const PARTIAL_COUNT_MESSAGE_IDS: Record = { + tools: "mcpServer.testConnection.countsPartial.tools", + resources: "mcpServer.testConnection.countsPartial.resources", + prompts: "mcpServer.testConnection.countsPartial.prompts", +}; + +function DetailRow({ label, children }: { label: string; children: React.ReactNode }) { + return ( +
+
{label}
+
{children}
+
+ ); +} const HTTP_METHODS = ["Get", "Post", "Put", "Delete", "Patch"] as const; @@ -123,7 +169,9 @@ function FieldLabel({ } export function TestConnectionPanel({ serverUrl }: TestConnectionPanelProps) { + const intl = useIntl(); const [status, setStatus] = useState("idle"); + const [mode, setMode] = useState("http"); const [method, setMethod] = useState("Get"); const [url, setUrl] = useState(serverUrl); const [path, setPath] = useState(""); @@ -131,6 +179,7 @@ export function TestConnectionPanel({ serverUrl }: TestConnectionPanelProps) { const [contentType, setContentType] = useState("application/json"); const [body, setBody] = useState(""); const [response, setResponse] = useState(null); + const [handshakeResponse, setHandshakeResponse] = useState(null); const [error, setError] = useState(""); const [errors, setErrors] = useState({}); // Aborted on unmount or via Cancel to avoid state updates on a stale request. @@ -146,6 +195,7 @@ export function TestConnectionPanel({ serverUrl }: TestConnectionPanelProps) { const handleTest = useCallback(async () => { setResponse(null); + setHandshakeResponse(null); setError(""); // Validate every field up front and surface problems inline; don't send a @@ -154,7 +204,7 @@ export function TestConnectionPanel({ serverUrl }: TestConnectionPanelProps) { url: validateUrl(url), path: validatePath(path), headers: validateHeaders(headers), - body: validateBody(body, method, contentType), + body: mode === "http" ? validateBody(body, method, contentType) : undefined, }; setErrors(nextErrors); if (nextErrors.url || nextErrors.path || nextErrors.headers || nextErrors.body) { @@ -189,6 +239,33 @@ export function TestConnectionPanel({ serverUrl }: TestConnectionPanelProps) { abortRef.current = controller; setStatus("testing"); + + if (mode === "handshake") { + const handshakePayload: GatewayHandshakeRequest = { + baseUrl: url.trim(), + ...(path.trim() ? { path: path.trim() } : {}), + ...(parsedHeaders ? { headers: parsedHeaders } : {}), + }; + try { + const result = await serversApi.testHandshake(handshakePayload, controller.signal); + if (controller.signal.aborted) { + return; + } + setHandshakeResponse(result); + setStatus(result?.success ? "success" : "error"); + } catch (e) { + if (controller.signal.aborted) { + return; + } + setHandshakeResponse(null); + setStatus("error"); + setError( + parseApiError(e, intl.formatMessage({ id: "mcpServer.testConnection.handshakeError" })), + ); + } + return; + } + try { const result = await serversApi.testConnectivity(payload, controller.signal); if (controller.signal.aborted) { @@ -206,7 +283,7 @@ export function TestConnectionPanel({ serverUrl }: TestConnectionPanelProps) { setStatus("error"); setError(parseApiError(e, "Connection test failed. Please try again.")); } - }, [url, headers, body, method, path, contentType]); + }, [url, headers, body, method, path, contentType, mode, intl]); const handleCancel = useCallback(() => { abortRef.current?.abort(); @@ -220,15 +297,58 @@ export function TestConnectionPanel({ serverUrl }: TestConnectionPanelProps) { : JSON.stringify(response.body, null, 2); }, [response]); - const headline = response - ? `Status: ${response.statusCode} ${status === "success" ? "OK" : "error"}` - : error || "Connection failed"; + const handshakeRawPreview = useMemo(() => { + if (!handshakeResponse?.rawPreview) return ""; + try { + return JSON.stringify(JSON.parse(handshakeResponse.rawPreview), null, 2); + } catch { + return handshakeResponse.rawPreview; + } + }, [handshakeResponse]); + + const handshakeCountChips = useMemo(() => { + const counts = handshakeResponse?.componentCounts; + if (!counts) return []; + return ["tools", "resources", "prompts"].filter((key) => counts[key] != null); + }, [handshakeResponse]); + + const headline = + mode === "handshake" + ? handshakeResponse + ? handshakeResponse.success + ? intl.formatMessage({ id: "mcpServer.testConnection.handshakeSucceeded" }) + : intl.formatMessage({ id: "mcpServer.testConnection.handshakeFailed" }) + : error || intl.formatMessage({ id: "mcpServer.testConnection.handshakeFailed" }) + : response + ? `Status: ${response.statusCode} ${status === "success" ? "OK" : "error"}` + : error || "Connection failed"; const isTesting = status === "testing"; const hasResult = status === "success" || status === "error"; return (
+ { + setMode(value as TestMode); + setStatus("idle"); + setResponse(null); + setHandshakeResponse(null); + setError(""); + setErrors({}); + }} + > + + + {intl.formatMessage({ id: "mcpServer.testConnection.mode.http" })} + + + {intl.formatMessage({ id: "mcpServer.testConnection.mode.handshake" })} + + + +
{/* Left column — request form */}
@@ -259,32 +379,34 @@ export function TestConnectionPanel({ serverUrl }: TestConnectionPanelProps) {
{/* Method */} -
- Method - - {HTTP_METHODS.map((m) => ( - - {m} - - ))} - -
+ {mode === "http" && ( +
+ Method + + {HTTP_METHODS.map((m) => ( + + {m} + + ))} + +
+ )} {/* Path */}
@@ -313,23 +435,25 @@ export function TestConnectionPanel({ serverUrl }: TestConnectionPanelProps) {
{/* Content type */} -
- Content type - -
+ {mode === "http" && ( +
+ Content type + +
+ )} {/* Headers */}
@@ -355,10 +479,15 @@ export function TestConnectionPanel({ serverUrl }: TestConnectionPanelProps) { {errors.headers}

)} + {mode === "handshake" && ( +

+ {intl.formatMessage({ id: "mcpServer.testConnection.storedCredentialsHint" })} +

+ )}
{/* Body — not applicable to GET requests */} - {method !== "Get" && ( + {mode === "http" && method !== "Get" && (
Body @@ -450,7 +579,6 @@ export function TestConnectionPanel({ serverUrl }: TestConnectionPanelProps) { )} -
{status === "success" ? ( @@ -462,21 +590,133 @@ export function TestConnectionPanel({ serverUrl }: TestConnectionPanelProps) {
- {response && ( -

- Latency: {response.latencyMs} ms -

- )} - - {responseBodyText && ( -
-

Response body:

-
-                      
-                        
-                      
-                    
-
+ {mode === "http" ? ( + <> + {response && ( +

+ Latency: {response.latencyMs} ms +

+ )} + + {responseBodyText && ( +
+

Response body:

+
+                          
+                            
+                          
+                        
+
+ )} + + ) : ( + <> + {handshakeResponse && ( +

+ Latency: {handshakeResponse.latencyMs} ms +

+ )} + + {handshakeResponse?.success && ( +
+ {handshakeResponse.serverName && ( + + {handshakeResponse.serverName} + + )} + {handshakeResponse.serverVersion && ( + + {handshakeResponse.serverVersion} + + )} + {handshakeResponse.protocolVersion && ( + + {handshakeResponse.protocolVersion} + + )} + {handshakeResponse.negotiationPath && ( + + {handshakeResponse.negotiationPath === "server_discover" + ? "server/discover" + : "initialize"} + + )} + + {intl.formatMessage({ + id: CREDENTIAL_SOURCE_MESSAGE_IDS[ + handshakeResponse.credentialSource ?? "none" + ], + })} + +
+ )} + + {handshakeResponse?.success && handshakeCountChips.length > 0 && ( +
+ {handshakeCountChips.map((key) => { + const count = handshakeResponse.componentCounts?.[key] ?? 0; + const messageIds = handshakeResponse.countsPartial + ? PARTIAL_COUNT_MESSAGE_IDS + : COUNT_MESSAGE_IDS; + return ( + + {intl.formatMessage({ id: messageIds[key] }, { count })} + + ); + })} +
+ )} + + {status === "error" && ( +
+ {handshakeResponse?.failureClass && ( + + {intl.formatMessage({ + id: FAILURE_CLASS_MESSAGE_IDS[handshakeResponse.failureClass], + })} + + )} + {(handshakeResponse?.error || error) && ( +

+ {handshakeResponse?.error ?? error} +

+ )} +
+ )} + + {handshakeRawPreview && ( +
+ + {intl.formatMessage({ id: "mcpServer.testConnection.rawResponse" })} + +
+                          
+                            
+                          
+                        
+
+ )} + )}
)} diff --git a/src/i18n/locales/en-US/mcpServer.json b/src/i18n/locales/en-US/mcpServer.json index 89ba9ac..a088053 100644 --- a/src/i18n/locales/en-US/mcpServer.json +++ b/src/i18n/locales/en-US/mcpServer.json @@ -31,5 +31,30 @@ "mcpServer.catalog.noneConnected": "No MCP server catalog options are connected.", "mcpServer.catalog.disabled": "Server catalog is disabled for this gateway.", "mcpServer.catalog.error": "Unable to load server catalog. Try again.", - "mcpServer.catalog.retry": "Retry" + "mcpServer.catalog.retry": "Retry", + "mcpServer.testConnection.mode.http": "HTTP request", + "mcpServer.testConnection.mode.handshake": "MCP handshake", + "mcpServer.testConnection.handshakeSucceeded": "Handshake succeeded", + "mcpServer.testConnection.handshakeFailed": "Handshake failed", + "mcpServer.testConnection.handshakeError": "Handshake test failed. Please try again.", + "mcpServer.testConnection.failureClass.transport": "Transport", + "mcpServer.testConnection.failureClass.protocol": "Protocol negotiation", + "mcpServer.testConnection.failureClass.auth": "Authentication", + "mcpServer.testConnection.failureClass.invalidResponse": "Invalid response", + "mcpServer.testConnection.serverName": "Server name", + "mcpServer.testConnection.serverVersion": "Server version", + "mcpServer.testConnection.protocolVersion": "Protocol version", + "mcpServer.testConnection.negotiationPath": "Negotiation path", + "mcpServer.testConnection.credentialSource": "Credential source", + "mcpServer.testConnection.credentialSource.stored": "Stored server credentials", + "mcpServer.testConnection.credentialSource.form": "Form headers", + "mcpServer.testConnection.credentialSource.none": "None", + "mcpServer.testConnection.storedCredentialsHint": "Stored credentials for registered servers are used automatically; headers you enter here override them.", + "mcpServer.testConnection.rawResponse": "Raw response (truncated)", + "mcpServer.testConnection.counts.tools": "{count, plural, one {# tool} other {# tools}}", + "mcpServer.testConnection.counts.resources": "{count, plural, one {# resource} other {# resources}}", + "mcpServer.testConnection.counts.prompts": "{count, plural, one {# prompt} other {# prompts}}", + "mcpServer.testConnection.countsPartial.tools": "{count}+ tools", + "mcpServer.testConnection.countsPartial.resources": "{count}+ resources", + "mcpServer.testConnection.countsPartial.prompts": "{count}+ prompts" } diff --git a/src/i18n/locales/es-ES/mcpServer.json b/src/i18n/locales/es-ES/mcpServer.json index ae04dba..3110206 100644 --- a/src/i18n/locales/es-ES/mcpServer.json +++ b/src/i18n/locales/es-ES/mcpServer.json @@ -31,5 +31,30 @@ "mcpServer.catalog.noneConnected": "Ninguna opción del catálogo de servidores MCP está conectada.", "mcpServer.catalog.disabled": "El catálogo de servidores está deshabilitado para esta puerta de enlace.", "mcpServer.catalog.error": "No se pudo cargar el catálogo de servidores. Inténtalo de nuevo.", - "mcpServer.catalog.retry": "Reintentar" + "mcpServer.catalog.retry": "Reintentar", + "mcpServer.testConnection.mode.http": "Solicitud HTTP", + "mcpServer.testConnection.mode.handshake": "Handshake MCP", + "mcpServer.testConnection.handshakeSucceeded": "Handshake completado", + "mcpServer.testConnection.handshakeFailed": "Handshake fallido", + "mcpServer.testConnection.handshakeError": "La prueba de handshake falló. Inténtalo de nuevo.", + "mcpServer.testConnection.failureClass.transport": "Transporte", + "mcpServer.testConnection.failureClass.protocol": "Negociación de protocolo", + "mcpServer.testConnection.failureClass.auth": "Autenticación", + "mcpServer.testConnection.failureClass.invalidResponse": "Respuesta no válida", + "mcpServer.testConnection.serverName": "Nombre del servidor", + "mcpServer.testConnection.serverVersion": "Versión del servidor", + "mcpServer.testConnection.protocolVersion": "Versión del protocolo", + "mcpServer.testConnection.negotiationPath": "Ruta de negociación", + "mcpServer.testConnection.credentialSource": "Origen de credenciales", + "mcpServer.testConnection.credentialSource.stored": "Credenciales almacenadas del servidor", + "mcpServer.testConnection.credentialSource.form": "Encabezados del formulario", + "mcpServer.testConnection.credentialSource.none": "Ninguna", + "mcpServer.testConnection.storedCredentialsHint": "Las credenciales almacenadas de los servidores registrados se usan automáticamente; los encabezados que introduzcas aquí las anulan.", + "mcpServer.testConnection.rawResponse": "Respuesta sin procesar (truncada)", + "mcpServer.testConnection.counts.tools": "{count, plural, one {# herramienta} other {# herramientas}}", + "mcpServer.testConnection.counts.resources": "{count, plural, one {# recurso} other {# recursos}}", + "mcpServer.testConnection.counts.prompts": "{count, plural, one {# prompt} other {# prompts}}", + "mcpServer.testConnection.countsPartial.tools": "{count}+ herramientas", + "mcpServer.testConnection.countsPartial.resources": "{count}+ recursos", + "mcpServer.testConnection.countsPartial.prompts": "{count}+ prompts" } diff --git a/src/i18n/locales/pt-BR/mcpServer.json b/src/i18n/locales/pt-BR/mcpServer.json index 6d5a4bc..59aed79 100644 --- a/src/i18n/locales/pt-BR/mcpServer.json +++ b/src/i18n/locales/pt-BR/mcpServer.json @@ -31,5 +31,30 @@ "mcpServer.catalog.noneConnected": "Nenhuma opção do catálogo de servidores MCP está conectada.", "mcpServer.catalog.disabled": "O catálogo de servidores está desabilitado para este gateway.", "mcpServer.catalog.error": "Não foi possível carregar o catálogo de servidores. Tente novamente.", - "mcpServer.catalog.retry": "Tentar novamente" + "mcpServer.catalog.retry": "Tentar novamente", + "mcpServer.testConnection.mode.http": "Requisição HTTP", + "mcpServer.testConnection.mode.handshake": "Handshake MCP", + "mcpServer.testConnection.handshakeSucceeded": "Handshake concluído", + "mcpServer.testConnection.handshakeFailed": "Falha no handshake", + "mcpServer.testConnection.handshakeError": "O teste de handshake falhou. Tente novamente.", + "mcpServer.testConnection.failureClass.transport": "Transporte", + "mcpServer.testConnection.failureClass.protocol": "Negociação de protocolo", + "mcpServer.testConnection.failureClass.auth": "Autenticação", + "mcpServer.testConnection.failureClass.invalidResponse": "Resposta inválida", + "mcpServer.testConnection.serverName": "Nome do servidor", + "mcpServer.testConnection.serverVersion": "Versão do servidor", + "mcpServer.testConnection.protocolVersion": "Versão do protocolo", + "mcpServer.testConnection.negotiationPath": "Caminho de negociação", + "mcpServer.testConnection.credentialSource": "Origem das credenciais", + "mcpServer.testConnection.credentialSource.stored": "Credenciais armazenadas do servidor", + "mcpServer.testConnection.credentialSource.form": "Cabeçalhos do formulário", + "mcpServer.testConnection.credentialSource.none": "Nenhuma", + "mcpServer.testConnection.storedCredentialsHint": "As credenciais armazenadas de servidores registrados são usadas automaticamente; os cabeçalhos inseridos aqui as substituem.", + "mcpServer.testConnection.rawResponse": "Resposta bruta (truncada)", + "mcpServer.testConnection.counts.tools": "{count, plural, one {# ferramenta} other {# ferramentas}}", + "mcpServer.testConnection.counts.resources": "{count, plural, one {# recurso} other {# recursos}}", + "mcpServer.testConnection.counts.prompts": "{count, plural, one {# prompt} other {# prompts}}", + "mcpServer.testConnection.countsPartial.tools": "{count}+ ferramentas", + "mcpServer.testConnection.countsPartial.resources": "{count}+ recursos", + "mcpServer.testConnection.countsPartial.prompts": "{count}+ prompts" }