Skip to content

[UI-REWRITE]: Add virtual server component tool testing - #90

Open
gandhipratik203 wants to merge 8 commits into
mainfrom
issue-6417-virtual-server-tool-try-it
Open

gandhipratik203 wants to merge 8 commits into
mainfrom
issue-6417-virtual-server-tool-try-it

Conversation

@gandhipratik203

@gandhipratik203 gandhipratik203 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Refs IBM/mcp-context-forge#6417

Design: IBM/mcp-context-forge#6710
Backend rollout dependencies: IBM/mcp-context-forge#6416, IBM/mcp-context-forge#6743

Summary

  • Add a flag-gated Test action to fetched tool rows in the virtual-server Components view.
  • Replace the component list in place with the reusable tool test UI; the existing top-level Try it handshake tab is unchanged.
  • Default virtual-server tool testing to Preview, with a Live invocation switch for eligible tools.
  • Include server_id in scoped preview requests, live JSON-RPC calls, and generated snippets while preserving direct Tools behavior.
  • Keep fallback associatedToolIds display-only so incomplete tool records cannot be invoked.

Feature flag

VITE_ENABLE_VIRTUAL_SERVER_TOOL_TRY_IT=false by default. When disabled, no component Test action or scoped tool invocation UI is exposed.

Integration and rollout

This PR implements the frontend only. The unit, Playwright, and manual mock tests verify the UI, scoped request payloads, permission states, and result rendering. They do not verify execution against a real backend.

Live invocation sends qualified tool.name and params.server_id to /rpc and uses the existing /rpc cancellation path. Scoped preview sends server_id to /v1/tools/preview/{name}. Backend contract work is tracked by IBM/mcp-context-forge#6416 and IBM/mcp-context-forge#6743.

This frontend PR can be reviewed and merged independently of that backend work. Keep VITE_ENABLE_VIRTUAL_SERVER_TOOL_TRY_IT=false until the backend contracts are implemented and scoped preview/live flows pass real-backend integration tests. The backend issues block enabling the feature, not merging this frontend implementation.

Review follow-up

  • Focus moves to the tool-test heading on Test and returns to the originating tool action on Back; pointer and keyboard routes are tested in Chromium.
  • The disabled Live switch now announces its permission/safety reason.
  • Added snake_case tool-response, mode-reset/loading, and gateway-metadata fallback tests; removed the unused tool-label prop and indexed fetched tools by ID.
  • A closing drawer now unmounts tool testing and cancels an active invoke; error results no longer claim a gateway answered.
  • Blank gateway IDs disable Live invocation, while direct and scoped valid tools retain their existing gates. The Live switch uses the shared Label and the components loading state is translated.

Verification

  • npm test -- --reporter=dot (210 files, 3,457 passed, 1 skipped)
  • npm run lint
  • npm run build
  • npm run format:check
  • npm run e2e -- e2e/tools.spec.ts (38 passed)
  • VITE_ENABLE_VIRTUAL_SERVER_TOOL_TRY_IT=true npm run e2e -- e2e/virtual-servers.spec.ts (45 passed)
  • npm run e2e -- e2e/virtual-servers.spec.ts (41 passed, 4 flag-dependent tests skipped)
  • HEADLESS=1 BASE_URL=http://localhost:5176 node virtual-server-try-it-manual.mjs (virtual server actions: ok)

Manual verification

Mock-backed manual test

Setup

git checkout issue-6417-virtual-server-tool-try-it
npm ci
npm run generate

Save the script from the next collapsible as virtual-server-try-it-manual.mjs at the repository root, then run:

# Terminal A
VITE_ENABLE_VIRTUAL_SERVER_TOOL_TRY_IT=true npm run dev

# Terminal B
node virtual-server-try-it-manual.mjs

If Vite selects another port, pass it to Terminal B, for example BASE_URL=http://localhost:5175 node virtual-server-try-it-manual.mjs.

Steps

  1. Open Actions for testVS -> View details. Confirm the existing handshake Try it tab is selected.
  2. Open Components, then Actions for Search issues -> Test. Confirm Tool test replaces the list and starts in Preview mode.
  3. Enter query=cloudflare, limit=5, and header X-Tenant-Id=team-a. Click Preview and confirm Preview 200.
  4. Confirm Terminal B logs a preview body containing server_id, the arguments, and forwarded header.
  5. Enable Live invocation, click Live invoke, and confirm Live invoke 200, Requested through testVS, Answered by github-mcp, and the scoped result.
  6. Confirm Terminal B logs /api/rpc with method=tools/call, qualified params.name, params.server_id, arguments, and the forwarded header.
  7. Rerun with PERMISSIONS=NO_EXECUTE and PERMISSIONS=NO_SERVERS_USE; Preview remains available while the Live switch is disabled with the relevant permission message.
  8. Rerun with TOOLS=EMPTY; fallback component rows remain visible but do not expose Test.

Ctrl-C both terminals to stop.

Mock script

Requires @playwright/test, which is already a development dependency.

// Manual UI testing for virtual-server scoped tool testing.
//
// Terminal A:
//   VITE_ENABLE_VIRTUAL_SERVER_TOOL_TRY_IT=true npm run dev
//
// Terminal B:
//   node virtual-server-try-it-manual.mjs
//
// Optional modes:
//   PERMISSIONS=NO_EXECUTE node virtual-server-try-it-manual.mjs
//   PERMISSIONS=NO_SERVERS_USE node virtual-server-try-it-manual.mjs
//   TOOLS=EMPTY node virtual-server-try-it-manual.mjs
//
// Ctrl-C in terminal B to close the headed browser.

import { chromium } from "@playwright/test";

const BASE = process.env.BASE_URL ?? "http://localhost:5173";
const HEADED = !process.env.HEADLESS;
const PERMISSIONS =
  process.env.PERMISSIONS === "NO_EXECUTE"
    ? ["servers.read", "servers.use"]
    : process.env.PERMISSIONS === "NO_SERVERS_USE"
      ? ["servers.read", "tools.execute"]
      : ["*"];
const PERMISSIONS_MODE = process.env.PERMISSIONS ?? "full access";

const SERVER_ID = "76c7b637dafc4d7197f14817ddffeda9"; // pragma: allowlist secret

const USER = {
  email: "test@example.com",
  full_name: "Test User",
  is_admin: true,
  is_active: true,
  auth_provider: "local",
  email_verified: true,
  password_change_required: false,
};

const VIRTUAL_SERVER = {
  id: SERVER_ID,
  name: "testVS",
  description: "Virtual server endpoint: developer tooling server exposing repository workflows.",
  icon: "",
  createdAt: "2026-04-28T15:41:31.233166",
  updatedAt: "2026-04-28T15:41:31.233168",
  enabled: true,
  associatedTools: ["Get Repo Issues", "Create New Issue"],
  associatedToolIds: ["GITHUB_GET_REPO_ISSUES", "GITHUB_CREATE_ISSUE"],
  associatedResources: ["github://repo/{owner}/{repo}"],
  associatedPrompts: ["summarize_pull_request"],
  associatedA2aAgents: [],
  metrics: null,
  tags: [{ id: "tag-development", label: "development" }],
  createdBy: "admin@example.com",
  createdFromIp: "127.0.0.1",
  createdVia: "ui",
  createdUserAgent: "Mozilla/5.0",
  modifiedBy: null,
  modifiedFromIp: null,
  modifiedVia: null,
  modifiedUserAgent: null,
  importBatchId: null,
  federationSource: null,
  version: 1,
  teamId: "0a9b06bd22974fe386dcacb18548ed61", // pragma: allowlist secret
  team: "Platform Administrator's Team",
  ownerEmail: "admin@example.com",
  visibility: "public",
  oauthEnabled: false,
  oauthConfig: null,
};

const MCP_SERVER = {
  id: "mcp-gateway-1",
  name: "github-mcp",
  url: "http://localhost:9000",
  transport: "SSE",
  enabled: true,
  reachable: true,
  visibility: "public",
  tool_count: 1,
  resource_count: 1,
  prompt_count: 1,
  created_at: "2026-04-28T15:41:31.233166",
  updated_at: "2026-04-28T15:41:31.233168",
};

function makeTool(overrides = {}) {
  return {
    id: "tool-search",
    name: "github.search_issues",
    originalName: "search_issues",
    description: "Search repository issues through the selected virtual server.",
    originalDescription: "Search repository issues through the selected virtual server.",
    title: "Search issues",
    displayName: "Search issues",
    gatewayId: "mcp-gateway-1",
    gatewaySlug: "github-mcp",
    customName: "",
    customNameSlug: "search_issues",
    enabled: true,
    reachable: true,
    deprecated: false,
    executionCount: 0,
    tags: [],
    integrationType: "MCP",
    requestType: "http",
    url: "https://example.com/mcp",
    headers: {},
    annotations: { readOnlyHint: true },
    jsonpathFilter: null,
    auth: null,
    version: 1,
    visibility: "team",
    createdAt: "2026-04-10T10:00:00Z",
    updatedAt: "2026-04-10T10:00:00Z",
    inputSchema: {
      type: "object",
      required: ["query"],
      properties: {
        query: { type: "string", description: "Search query" },
        limit: { type: "integer", description: "Maximum results" },
      },
    },
    outputSchema: { type: "object" },
    ...overrides,
  };
}

const TOOLS = process.env.TOOLS === "EMPTY" ? [] : [makeTool()];

function json(body, status = 200) {
  return {
    status,
    contentType: "application/json",
    body: JSON.stringify(body),
  };
}

function fallbackApiBody(pathname) {
  if (pathname.startsWith("/api/v1/resources")) return { resources: [] };
  if (pathname.startsWith("/api/v1/prompts")) return { prompts: [] };
  if (pathname.startsWith("/api/v1/tools")) return { tools: [] };
  if (pathname.startsWith("/api/v1/mcp-servers")) return { gateways: [], nextCursor: null };
  if (pathname.startsWith("/api/v1/virtual-servers")) return { servers: [] };
  return {};
}

function interestingHeaders(headers) {
  return Object.fromEntries(
    Object.entries(headers).filter(([name]) =>
      ["x-csrf-token", "x-tenant-id", "x-api-key", "authorization"].includes(name.toLowerCase()),
    ),
  );
}

function toolResult(text, extra = {}) {
  return {
    target: { kind: "federated", gateway_name: "github-mcp" },
    content: [{ type: "text", text, mimeType: "text/plain" }],
    structured_output: extra,
  };
}

const browser = await chromium.launch({ headless: !HEADED });
const context = await browser.newContext({ viewport: { width: 1512, height: 950 } });
const page = await context.newPage();

page.on("console", (message) => {
  if (["error", "warning"].includes(message.type())) {
    console.log(`browser ${message.type()}: ${message.text()}`);
  }
});
page.on("pageerror", (error) => {
  console.log(`browser pageerror: ${error.message}`);
});

await page.route("**/*", (route) => {
  const pathname = new URL(route.request().url()).pathname;
  if (pathname.startsWith("/api/")) return route.fulfill(json(fallbackApiBody(pathname)));
  return route.fallback();
});

await page.route("**/auth/session", (route) =>
  route.fulfill(
    json({
      authenticated: true,
      user: USER,
      csrfToken: "mock-csrf-token",
    }),
  ),
);

await page.route("**/api/rbac/my/permissions**", (route) => route.fulfill(json(PERMISSIONS)));
await page.route("**/api/v1/virtual-servers?*", (route) =>
  route.fulfill(json({ servers: [VIRTUAL_SERVER] })),
);
await page.route(`**/api/v1/virtual-servers/${SERVER_ID}`, (route) =>
  route.fulfill(json(VIRTUAL_SERVER)),
);
await page.route(`**/api/v1/virtual-servers/${SERVER_ID}/tools?*`, (route) =>
  route.fulfill(json({ tools: TOOLS })),
);
await page.route(`**/api/v1/virtual-servers/${SERVER_ID}/resources?*`, (route) =>
  route.fulfill(json({ resources: [] })),
);
await page.route(`**/api/v1/virtual-servers/${SERVER_ID}/prompts?*`, (route) =>
  route.fulfill(json({ prompts: [] })),
);
await page.route("**/api/v1/mcp-servers?*", (route) =>
  route.fulfill(json({ gateways: [MCP_SERVER], nextCursor: null })),
);

await page.route("**/api/v1/tools/preview/github.search_issues", async (route) => {
  const request = route.request();
  const body = request.postDataJSON();
  const headers = request.headers();

  console.log("\n/api/v1/tools/preview/github.search_issues request body:");
  console.log(JSON.stringify(body, null, 2));
  console.log("preview interesting headers:");
  console.log(JSON.stringify(interestingHeaders(headers), null, 2));

  return route.fulfill(
    json({
      target: { kind: "federated", gateway_name: "github-mcp" },
      resolved_arguments: body?.arguments ?? {},
      annotations: { readOnlyHint: true },
      pre_hooks_run: [],
      warnings: [],
    }),
  );
});

await page.route("**/api/rpc", async (route) => {
  const request = route.request();
  const body = request.postDataJSON();
  const headers = request.headers();

  console.log("\n/api/rpc request body:");
  console.log(JSON.stringify(body, null, 2));
  console.log("/api/rpc interesting headers:");
  console.log(JSON.stringify(interestingHeaders(headers), null, 2));

  return route.fulfill(
    json({
      jsonrpc: "2.0",
      id: body.id,
      result: toolResult(`Scoped result for ${body?.params?.name}`, {
        receivedArguments: body?.params?.arguments ?? {},
        serverId: body?.params?.server_id ?? null,
        tenantHeader: headers["x-tenant-id"] ?? null,
      }),
    }),
  );
});

await page.addInitScript(() => {
  sessionStorage.setItem("mcpgateway_token", "placeholder-token");
});

await page.goto(`${BASE}/app/gateways`, { waitUntil: "networkidle" });

const cardCount = await page.getByRole("button", { name: "Actions for testVS" }).count();
console.log(`virtual server actions: ${cardCount ? "ok" : "MISSING"}`);
console.log(`permissions mode: ${PERMISSIONS_MODE}`);
console.log(`tools mode: ${process.env.TOOLS ?? "attached tool"}`);

if (!HEADED) {
  await browser.close();
} else {
  console.log(`
Browser open. Try:

  1. Open "Actions for testVS" -> "View details".
     Expect the details drawer to open with the handshake "Try it" tab selected.

  2. Click "Components", then open "Actions for Search issues" -> "Test".
     Expect the component list to be replaced by "Tool test" in Preview mode.

  3. Fill query="cloudflare" and limit="5".
     Add header X-Tenant-Id=team-a.
     Click "Preview".
     Expect "Preview 200".

  4. Terminal should show /api/v1/tools/preview/github.search_issues with:
       server_id "${SERVER_ID}"
       arguments query="cloudflare", limit=5
       x-tenant-id "team-a"

  5. Enable "Live invocation".
     Click "Live invoke".
     Expect "Live invoke 200", "Requested through testVS",
     "Answered by github-mcp", and "Scoped result for github.search_issues".

  6. Terminal should show /api/rpc with:
       method "tools/call"
       params.name "github.search_issues"
       params.server_id "${SERVER_ID}"
       params.arguments query="cloudflare", limit=5
       x-tenant-id "team-a"

  7. Optional RBAC denial:
       PERMISSIONS=NO_EXECUTE node virtual-server-try-it-manual.mjs
     Expect Preview to remain available and the Live invocation switch to be disabled with
     "Live invoke requires tools.execute."

  8. Optional servers.use denial:
       PERMISSIONS=NO_SERVERS_USE node virtual-server-try-it-manual.mjs
     Expect Preview to remain available and the Live invocation switch to be disabled with
     "Live invoke requires servers.use."

  9. Optional empty fetched tools:
       TOOLS=EMPTY node virtual-server-try-it-manual.mjs
     Expect fallback component rows to remain visible without a "Test" action.

Ctrl-C to close.
`);
  await new Promise(() => {});
}

@gandhipratik203
gandhipratik203 force-pushed the issue-6417-virtual-server-tool-try-it branch from 724ab19 to c5de120 Compare September 14, 2026 15:14
@gandhipratik203 gandhipratik203 changed the title [UI-REWRITE]: Add virtual server drawer Try-it UI [UI-REWRITE]: Add virtual server component tool testing Sep 14, 2026
@gandhipratik203
gandhipratik203 marked this pull request as ready for review September 15, 2026 11:09
@gandhipratik203 gandhipratik203 self-assigned this Sep 15, 2026

@marekdano marekdano left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

🟠 Accessibility

1. High — src/components/gateways/VirtualServerDetailsPanel.tsx:965
No focus management on tool-test view swap. Entering/leaving the new tool-test view moves no focus and announces nothing to assistive tech. Clicking Test swaps the components list for "Tool test" with no heading, aria-live region, or focus move. Clicking Back to components drops focus to <body> instead of returning it to the row's "Actions for X" trigger. src/components/server-catalog/CatalogResults.tsx already solves this exact pattern with an actionsTriggerRef.current?.focus() call on close — this PR didn't reuse it.

2. Medium — src/components/tools/ToolTryItTab.tsx:192
Disabled switch reason not linked via aria-describedby. The disabled-reason text for the "Live invocation" switch is a plain sibling <p>, not associated with the control. A screen-reader user tabbing to the disabled switch (e.g. missing tools.execute) hears only "Live invocation, switch, dimmed" — the adjacent explanation ("Live invoke requires tools.execute.") is never announced.

🟡 Test Coverage

3. Medium — src/components/gateways/VirtualServerDetailsPanel.tsx:161
normalizePanelTool's snake_case fallback branches untested. The snake_case fallback branches (gateway_slug, display_name, original_name, input_schema, output_schema) are never exercised — every test fixture uses camelCase makeTool(). If the scoped-tools endpoint ever actually sends snake_case (the whole reason this fallback exists), a regression ships silently — blank display names, empty input schemas.

4. Low-Medium — src/components/tools/ToolTryItTab.tsx:118
No test for live-mode toggle-off or loading state. No test toggles Live invocation on and back off, or exercises the permissionsLoading ("checkingAccess") state disabling the new Switch. A regression in handleLiveModeChange's reset of snippetLanguage/preview/invoke state on the live→preview transition, or in the Switch's disabled wiring while permissions are still loading, wouldn't be caught.

5. Low — src/components/tools/ToolLiveInvokeResult.tsx:129
Only 1 of 8 gateway-name fallback branches covered. Only target.gateway_name is covered by ToolLiveInvokeResult.test.tsx. The other 7 (root-level
gateway_name/gatewayName/resolved_gateway_name/resolvedGatewayName, target.gatewaySlug/gateway_slug) are untested.

🔵 Code Quality / Cleanup

6. Medium — src/components/gateways/VirtualServerDetailsPanel.tsx:155
Casing-normalization pattern duplicated across 4 files. normalizePanelTool hand-reads camelCase/snake_case field pairs to paper over backend casing inconsistency — the same ad-hoc pattern is already duplicated in CreateServer.tsx, ServerCatalog.tsx, and src/api/search.ts. Belongs once in a shared response-normalization layer, not re-implemented per component.

7. Low — src/components/gateways/VirtualServerDetailsPanel.tsx:182
getNonEmptyString duplicated verbatim in two new files. Duplicated in this file and in src/components/tools/ToolLiveInvokeResult.tsx:145, both new in this PR. Risk of the two copies silently drifting on a future change to "non-empty" semantics.

8. Low — src/components/gateways/VirtualServerDetailsPanel.tsx:984
getToolLabel prop passed but dead without onSelectTool. getToolLabel={getFriendlyToolLabel} is passed into ToolTryItTab without onSelectTool, so it's dead: toolLabel() is only invoked inside the onSelectTool && branch, which can never render from this call site.

9. Low — src/components/tools/ToolLiveInvokeResult.tsx:129
8-way ?? chain instead of iterating candidate keys. getBackingGatewayName chains 8 explicit fallbacks over field-name variants instead of iterating a list of candidate keys — hard to audit, easy to miss a branch during review.

10. Low — src/components/gateways/VirtualServerDetailsPanel.tsx:753
O(n·m) find() inside render loop over memoized array. testableTool is computed with fetchedTools.find() inside the visibleComponents.map() render loop, where fetchedTools (already memoized via useMemo, line 318) could be indexed once into a Map for O(1) lookups. Low real-world impact, but a free fix.

@gandhipratik203

Copy link
Copy Markdown
Contributor Author

Addressed the review in 93f85e3: tool-test focus now moves to the heading and returns to the originating row; the disabled Live switch announces its reason. Added casing, mode-transition, permission-loading, and gateway-metadata tests. Removed the unused label prop and indexed fetched tools by ID.

Kept normalization local to virtual-server tools. The tiny string guard and explicit gateway-name precedence remain unchanged to avoid widening this PR.

Local tests and GitHub checks pass. Please re-review.

@marekdano marekdano left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

🔴 High: Misleading success indicator on failed invoke

File: src/components/tools/ToolLiveInvokeResult.tsx:42

"Answered by {gateway}" renders from a static context value regardless of outcome.

// VirtualServerToolTestView always passes:
resultContext={{ backingGatewayName: tool.gatewaySlug || undefined }}

// ToolLiveInvokeResult.tsx:
const backingGatewayName = context?.backingGatewayName ?? getBackingGatewayName(response);

backingGatewayName prefers the static context value unconditionally, so if a scoped live invoke fails (network error, JSON-RPC error, timeout), hasRun is true and result is null, but the panel still renders "Answered by github-mcp" right next to the error message — implying the tool call succeeded when it didn't.

Suggested fix: only derive backingGatewayName from context when the invoke actually succeeded, falling back to getBackingGatewayName(response) only on success.


🔴 High: Federated-tool gating bypassed by empty string

File: src/components/gateways/normalizeVirtualServerTool.ts:13

gatewayId: tool.gatewayId ?? nonEmptyString(record.gateway_id) ?? null,

Every sibling field (displayName, originalName, gatewaySlug) routes through nonEmptyString() first. gatewayId does not.

If the backend ever sends gatewayId: "" instead of null/undefined, isFederated = Boolean(tool.gatewayId) in resolveToolLiveInvokeAvailability (ToolLiveInvokeGate.tsx) evaluates to false. A destructive federated tool then falls through to the requiresConfirmation branch (live-invokable with just a confirm dialog) instead of the intended unavailableFederated block.

Suggested fix: nonEmptyString(tool.gatewayId) ??nonEmptyString(record.gateway_id) ?? null to match the other fields.


🟡 Medium: New i18n key added but never wired up

File: src/components/gateways/VirtualServerDetailsPanel.tsx:744

The key gateways.details.loadingComponents was added to en-US, es-ES, and pt-BR locale files, but the component still renders the hardcoded literal:

<span>Loading components...</span>

instead of:

<span>{intl.formatMessage({ id: "gateways.details.loadingComponents" })}</span>

es-ES/pt-BR users viewing the Components tab while tools/resources/prompts load see the untranslated English string.

@vishu-bh vishu-bh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address the two inline findings before merging: closing the virtual-server drawer leaves an active live invocation running without cancellation, and the new live-mode label should use the shared UI Label component. The lifecycle issue was reproduced with the real panel/hooks: closing sent no cancellation, while unmounting aborted the request and sent cancellation. Add coverage for closing during an active invocation.

].map((source, index, sources) => {
const isSelected = sourceFilter === source.id;
const tabButton = (
{virtualServerToolTryItEnabled && selectedTestTool ? (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Cancel active invocation when the drawer closes

This tool-test view remains mounted when open becomes false. Closing the drawer or pressing Escape hides the cancellation control, but the invocation continues: the close reset effect returns early on !open, so the invoke hook never runs its unmount cancellation. Reproduced with the real panel/hooks and a pending invocation: closing left the signal un-aborted and sent zero cancellation calls; unmounting aborted it and sent cancellation. Clear the selected test or unmount/reset this view on close, and add a regression test for closing during an active invocation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e001097. Closing the drawer now unmounts the tool-test view, aborting the active request and sending cancellation. Tests cover both the close button and Escape.

Comment thread src/components/tools/ToolTryItTab.tsx Outdated
{scopedMode && (
<div className="flex flex-wrap items-start justify-between gap-4 border-y border-border py-3">
<div className="space-y-1">
<label

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Use the shared Label component

The new live-invocation label uses a raw <label> even though components/ui/label already provides the shared Label primitive. Please use that component here to follow the shared UI component convention and retain consistent styling and accessibility behavior.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e001097. The Live switch now uses the shared Label component, with its accessible association preserved.

@gandhipratik203

Copy link
Copy Markdown
Contributor Author

Addressed the follow-up reviews in e001097: failed calls no longer claim a gateway answered; blank gateway IDs block Live; loading text is localized; and the Live label uses the shared component. Closing the drawer now unmounts tool testing, aborts an active call, and sends cancellation. Added regression tests for button and Escape closure. Local tests and all PR checks pass. @marekdano @vishu-bh please re-review.

@marekdano

Copy link
Copy Markdown
Contributor

@gandhipratik203 - please rebase this branch on main

Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>
Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>
Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>
@gandhipratik203
gandhipratik203 force-pushed the issue-6417-virtual-server-tool-try-it branch from e001097 to 19f4315 Compare September 18, 2026 08:58
@gandhipratik203

Copy link
Copy Markdown
Contributor Author

Rebased the branch onto the latest main.

Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>
Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>
Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>

@a-effort a-effort left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notes: all of these concern how it behaves against the deployed backend rather than against the mocks. None of them block merging while the flag is off but will be relevant when its removed. Design feedback follows separately.

);
}

function getBackingGatewayName(response: ToolPreviewResponse | undefined): string | undefined {

@a-effort a-effort Sep 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every fallback in this function is unreachable against the deployed backend, so it always returns undefined.

The /rpc tools/call result is a CallToolResult (mcpgateway/common/models.py:659, aliased to ToolResult at 682), serialized with model_dump(by_alias=True, exclude_none=True). The keys that reach the client are content, isError, structuredContent and _meta. None of them carry a target or a source name.

"Answered by X" renders only from the context value the panel supplies: resultContext={{ backingGatewayName: tool.gatewaySlug }}. That is the client repeating what it already stored on the tool record, not a report of which source handled the call. If the backend resolves the call to a different tool, the line still prints tool.gatewaySlug.

Suggested change: remove this function and the response-parsing path, and to hold the "Answered by" line until [IBM/mcp-context-forge#6416](IBM/mcp-context-forge#6416) adds a field for it.

Note: _meta is not a safe carrier. It is settable by the upstream source, so it cannot be relied on to report which source answered, and the plugin post-invoke path in tool_service.py rebuilds ToolResult without meta, so it is lost whenever a post-invoke plugin rewrites the result.

Comment thread src/api/tools.ts
`/v1/tools/preview/${encodeURIComponent(validName)}`,
{
arguments: args,
...(options.serverId ? { server_id: options.serverId } : {}),

@a-effort a-effort Sep 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deployed backend drops server_id. ToolPreviewRequest (mcpgateway/schemas.py:2103) declares only arguments, and BaseModelWithConfigDict sets extra="ignore" (mcpgateway/utils/base_models.py:96).

A scoped preview returns 200 with a target resolved without any virtual server scoping. The response looks correct and there is no error to notice, so the failure is quiet.

Worth guarding on the client, or at least noting at this call site, since the only thing preventing it is leaving VITE_ENABLE_VIRTUAL_SERVER_TOOL_TRY_IT off until [IBM/mcp-context-forge#6743](IBM/mcp-context-forge#6743) lands.

if (permissionsLoading) return { state: "checkingAccess" };
if (!canExecute) return { state: "missingPermission", permission: "tools.execute" };
if (!canUseServers) return { state: "missingPermission", permission: "servers.use" };
if (invalidGatewayId || (typeof tool.gatewayId === "string" && !tool.gatewayId.trim())) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth checking how often the live path is reachable for virtual server components before more work goes into it.

Any component fetched from an attached MCP server carries a gatewayId, so isFederated at line 44 is true for those rows. From there the only route to available is readOnlyHint at line 52. A federated tool with destructiveHint returns unavailableFederated at line 49, and a federated tool with no annotations returns unavailableFederated at line 53.

Upstream MCP servers frequently send no annotations, so the Live invocation switch will be disabled on most component rows. Neither #6416 nor #6743 changes that, since the gate turns on annotations and on #5437.

The manual mock in the description sets annotations: { readOnlyHint: true } on its only tool, so the documented verification steps all exercise the case that resolves to available. Adding a tool with no annotations to the mock would show what most rows do.

Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>
Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants