Skip to content

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

Draft
gandhipratik203 wants to merge 1 commit into
mainfrom
issue-6417-virtual-server-tool-try-it
Draft

[UI-REWRITE]: Add virtual server component tool testing#90
gandhipratik203 wants to merge 1 commit 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 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.

Backend status

Live invocation uses qualified tool.name with params.server_id and follows the existing /rpc cancellation path. Backend hardening is tracked by IBM/mcp-context-forge#6416.

Scoped preview sends server_id to /v1/tools/preview/{name}. Backend support is tracked by IBM/mcp-context-forge#6743, so this PR remains draft and the feature remains off by default.

Verification

  • npm test -- --reporter=dot (209 files, 3,432 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 (44 passed)
  • npm run e2e -- e2e/virtual-servers.spec.ts (41 passed, 3 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(() => {});
}

Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>
@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
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.

1 participant