Skip to content

Commit e772acf

Browse files
committed
feat: add MCP handshake test mode to Test Connection
UI half of the MCP handshake test, split out of IBM/mcp-context-forge#5934 now that the client lives in this repo. Test Connection gains a mode toggle. HTTP request keeps the existing raw-request behavior; MCP handshake calls the new POST /v1/mcp-servers/test-handshake and reports whether the target actually speaks MCP: - Detail rows for server name/version, protocol version, negotiation path (server/discover or initialize) and credential source - Count badges for first-page tools/resources/prompts, rendered as "3+ tools" when countsPartial marks the listing truncated - A failure-class badge (transport / protocol negotiation / authentication / invalid response) with the backend's actionable copy - A collapsible raw-response preview - Method, content type and body inputs are hidden in handshake mode; the in-flight request is aborted on unmount, cancel, and mode switch New user-facing copy goes through react-intl, with keys added to the en-US, es-ES and pt-BR mcpServer namespaces. The en-US messages are byte-identical to the strings they replace. Component counts use ICU plural forms. openapi.json gains only the new /v1/mcp-servers/test-handshake path plus the GatewayHandshakeRequest/GatewayHandshakeResponse schemas, extracted from the gateway's app.openapi(). The snapshot stays pinned at API v1.0.7 otherwise, so the generated types pick up the handshake endpoint without dragging in unrelated spec drift. Relates to IBM/mcp-context-forge#5649 Signed-off-by: Ahmad Al Tamimi <altamimi.dev@gmail.com>
1 parent 3c03e4c commit e772acf

7 files changed

Lines changed: 1196 additions & 206 deletions

File tree

openapi.json

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36803,6 +36803,89 @@
3680336803
}
3680436804
]
3680536805
}
36806+
},
36807+
"/v1/mcp-servers/test-handshake": {
36808+
"post": {
36809+
"tags": [
36810+
"MCP Servers"
36811+
],
36812+
"summary": "Check Mcp Server Handshake",
36813+
"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'",
36814+
"operationId": "check_mcp_server_handshake_v1_mcp_servers_test_handshake_post",
36815+
"security": [
36816+
{
36817+
"ConfigurableHTTPBearer": []
36818+
}
36819+
],
36820+
"parameters": [
36821+
{
36822+
"name": "team_id",
36823+
"in": "query",
36824+
"required": false,
36825+
"schema": {
36826+
"anyOf": [
36827+
{
36828+
"type": "string"
36829+
},
36830+
{
36831+
"type": "null"
36832+
}
36833+
],
36834+
"description": "Filter by team ID",
36835+
"title": "Team Id"
36836+
},
36837+
"description": "Filter by team ID"
36838+
},
36839+
{
36840+
"name": "jwt_token",
36841+
"in": "cookie",
36842+
"required": false,
36843+
"schema": {
36844+
"anyOf": [
36845+
{
36846+
"type": "string"
36847+
},
36848+
{
36849+
"type": "null"
36850+
}
36851+
],
36852+
"title": "Jwt Token"
36853+
}
36854+
}
36855+
],
36856+
"requestBody": {
36857+
"required": true,
36858+
"content": {
36859+
"application/json": {
36860+
"schema": {
36861+
"$ref": "#/components/schemas/GatewayHandshakeRequest"
36862+
}
36863+
}
36864+
}
36865+
},
36866+
"responses": {
36867+
"200": {
36868+
"description": "Successful Response",
36869+
"content": {
36870+
"application/json": {
36871+
"schema": {
36872+
"$ref": "#/components/schemas/GatewayHandshakeResponse"
36873+
}
36874+
}
36875+
}
36876+
},
36877+
"422": {
36878+
"description": "Validation Error",
36879+
"content": {
36880+
"application/json": {
36881+
"schema": {
36882+
"$ref": "#/components/schemas/HTTPValidationError"
36883+
}
36884+
}
36885+
}
36886+
}
36887+
}
36888+
}
3680636889
}
3680736890
},
3680836891
"components": {
@@ -52836,6 +52919,203 @@
5283652919
"type"
5283752920
],
5283852921
"title": "ValidationError"
52922+
},
52923+
"GatewayHandshakeRequest": {
52924+
"properties": {
52925+
"baseUrl": {
52926+
"type": "string",
52927+
"minLength": 1,
52928+
"format": "uri",
52929+
"title": "Baseurl",
52930+
"description": "Base URL of the MCP server to test"
52931+
},
52932+
"path": {
52933+
"anyOf": [
52934+
{
52935+
"type": "string"
52936+
},
52937+
{
52938+
"type": "null"
52939+
}
52940+
],
52941+
"title": "Path",
52942+
"description": "Optional path appended to the base URL"
52943+
},
52944+
"headers": {
52945+
"anyOf": [
52946+
{
52947+
"additionalProperties": {
52948+
"type": "string"
52949+
},
52950+
"type": "object"
52951+
},
52952+
{
52953+
"type": "null"
52954+
}
52955+
],
52956+
"title": "Headers",
52957+
"description": "Optional headers (e.g. Authorization) sent with the handshake"
52958+
}
52959+
},
52960+
"type": "object",
52961+
"required": [
52962+
"baseUrl"
52963+
],
52964+
"title": "GatewayHandshakeRequest",
52965+
"description": "Request to run an MCP handshake test against a server URL.",
52966+
"nullable": true
52967+
},
52968+
"GatewayHandshakeResponse": {
52969+
"properties": {
52970+
"success": {
52971+
"type": "boolean",
52972+
"title": "Success"
52973+
},
52974+
"latencyMs": {
52975+
"type": "integer",
52976+
"title": "Latencyms"
52977+
},
52978+
"negotiationPath": {
52979+
"anyOf": [
52980+
{
52981+
"type": "string",
52982+
"enum": [
52983+
"server_discover",
52984+
"initialize"
52985+
]
52986+
},
52987+
{
52988+
"type": "null"
52989+
}
52990+
],
52991+
"title": "Negotiationpath",
52992+
"description": "Which handshake path produced the result"
52993+
},
52994+
"protocolVersion": {
52995+
"anyOf": [
52996+
{
52997+
"type": "string"
52998+
},
52999+
{
53000+
"type": "null"
53001+
}
53002+
],
53003+
"title": "Protocolversion"
53004+
},
53005+
"serverName": {
53006+
"anyOf": [
53007+
{
53008+
"type": "string"
53009+
},
53010+
{
53011+
"type": "null"
53012+
}
53013+
],
53014+
"title": "Servername"
53015+
},
53016+
"serverVersion": {
53017+
"anyOf": [
53018+
{
53019+
"type": "string"
53020+
},
53021+
{
53022+
"type": "null"
53023+
}
53024+
],
53025+
"title": "Serverversion"
53026+
},
53027+
"capabilities": {
53028+
"anyOf": [
53029+
{
53030+
"additionalProperties": true,
53031+
"type": "object"
53032+
},
53033+
{
53034+
"type": "null"
53035+
}
53036+
],
53037+
"title": "Capabilities"
53038+
},
53039+
"componentCounts": {
53040+
"anyOf": [
53041+
{
53042+
"additionalProperties": {
53043+
"type": "integer"
53044+
},
53045+
"type": "object"
53046+
},
53047+
{
53048+
"type": "null"
53049+
}
53050+
],
53051+
"title": "Componentcounts",
53052+
"description": "Counts for tools/resources/prompts; a key is absent when the capability is not advertised"
53053+
},
53054+
"countsPartial": {
53055+
"type": "boolean",
53056+
"title": "Countspartial",
53057+
"description": "True when any list result had a nextCursor (counts are first-page lower bounds)",
53058+
"default": false
53059+
},
53060+
"credentialSource": {
53061+
"type": "string",
53062+
"enum": [
53063+
"stored",
53064+
"form",
53065+
"none"
53066+
],
53067+
"title": "Credentialsource",
53068+
"default": "none"
53069+
},
53070+
"failureClass": {
53071+
"anyOf": [
53072+
{
53073+
"type": "string",
53074+
"enum": [
53075+
"transport",
53076+
"protocol",
53077+
"auth",
53078+
"invalid_response"
53079+
]
53080+
},
53081+
{
53082+
"type": "null"
53083+
}
53084+
],
53085+
"title": "Failureclass"
53086+
},
53087+
"error": {
53088+
"anyOf": [
53089+
{
53090+
"type": "string"
53091+
},
53092+
{
53093+
"type": "null"
53094+
}
53095+
],
53096+
"title": "Error"
53097+
},
53098+
"rawPreview": {
53099+
"anyOf": [
53100+
{
53101+
"type": "string"
53102+
},
53103+
{
53104+
"type": "null"
53105+
}
53106+
],
53107+
"title": "Rawpreview",
53108+
"description": "Size-capped JSON preview of the final handshake payload"
53109+
}
53110+
},
53111+
"type": "object",
53112+
"required": [
53113+
"success",
53114+
"latencyMs"
53115+
],
53116+
"title": "GatewayHandshakeResponse",
53117+
"description": "Result of an MCP handshake test.",
53118+
"nullable": true
5283953119
}
5284053120
},
5284153121
"securitySchemes": {

src/api/servers.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@
77

88
import { api } from "./client";
99
import type { ServersResponse, MCPServer } from "../types/server";
10-
import type { GatewayTestRequest, GatewayTestResponse } from "@/generated/types";
10+
import type {
11+
GatewayHandshakeRequest,
12+
GatewayHandshakeResponse,
13+
GatewayTestRequest,
14+
GatewayTestResponse,
15+
} from "@/generated/types";
1116

1217
const serverByIdRequestCache = new Map<string, Promise<MCPServer>>();
1318

@@ -130,6 +135,19 @@ export const serversApi = {
130135
return api.post("/v1/mcp-servers/test", request, { signal });
131136
},
132137

138+
/**
139+
* Test whether an MCP server URL speaks MCP via a protocol handshake.
140+
*
141+
* Tries the stateless server/discover method (MCP 2026-07-28+) first and
142+
* falls back to a stateful initialize round-trip for earlier specs.
143+
*/
144+
testHandshake: (
145+
request: GatewayHandshakeRequest,
146+
signal?: AbortSignal,
147+
): Promise<GatewayHandshakeResponse> => {
148+
return api.post("/v1/mcp-servers/test-handshake", request, { signal });
149+
},
150+
133151
/**
134152
* Toggle the enabled state of an MCP server (activate/deactivate)
135153
*/

0 commit comments

Comments
 (0)