Skip to content

[UI-REWRITE] Add tool preview tab - #53

Merged
gcgoncalves merged 4 commits into
mainfrom
feat/6316-tool-preview-drawer
Aug 21, 2026
Merged

[UI-REWRITE] Add tool preview tab#53
gcgoncalves merged 4 commits into
mainfrom
feat/6316-tool-preview-drawer

Conversation

@gandhipratik203

@gandhipratik203 gandhipratik203 commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add Try it and Definition tabs to the tool details panel, with Try it as the default
  • add the mock-backed tool preview API wrapper, preview hook, schema-driven args form, passthrough headers editor, and basic preview result panel
  • add i18n keys and mock-backed unit/e2e coverage for preview and denied passthrough headers

PR split plan

This PR is the first slice of issue #5630: it adds the mocked Try it workflow. Result rendering and live invocation are left for follow-up PRs.

Issue #5630: Tool Try it / Preview
|
|-- PR 1: Mocked Try it workflow  <- this PR
|   |-- Try it tab
|   |-- Arguments form
|   |-- Headers editor
|   |-- Preview / Re-run
|   `-- Mock-backed tests
|
|-- PR 2: Result rendering
|   |-- MCP content rendering
|   |-- Warnings
|   `-- Size guard
|
`-- PR 3: Live invoke + snippets
    |-- Real invoke route
    |-- Safety gate
    `-- Code snippets

Scope

Closes IBM/mcp-context-forge#6316
Refs IBM/mcp-context-forge#5630

This is PR 1 of the split. Full content-block rendering remains in IBM/mcp-context-forge#6317, and live invocation/spec-aware snippets remain in IBM/mcp-context-forge#6318.

Temporary flag removal is tracked in IBM/mcp-context-forge#6322.

Tests

  • npm run test
  • npm run lint
  • npx tsc --noEmit -p tsconfig.app.json
  • npm run format:check
  • npm run e2e -- e2e/tools.spec.ts

Explanatory diagrams

PR file/folder tree
PR #53 file/folder tree
=======================

src/api/
├── tools.ts
│   └── Adds toolsApi.preview() for POST /api/tools/preview/{toolName}.
│
└── tools.test.ts
    └── Tests preview URL, body, headers, and name validation.


src/hooks/
├── useToolPreview.ts
│   └── Manages preview loading/result/error/reset/abort state.
│
└── useToolPreview.test.tsx
    └── Tests preview success, failure, reset, and abort behavior.


src/components/tools/
├── ToolDetailsPanel.tsx
│   └── Adds Try it and Definition tabs to the drawer.
│
├── ToolDetailsPanel.test.tsx
│   └── Tests drawer tab behavior.
│
├── ToolTryItTab.tsx
│   └── Composes the Try it UI.
│
├── ToolArgumentsForm.tsx
│   └── Builds argument inputs from tool.inputSchema.
│
├── ToolArgumentsForm.test.tsx
│   └── Tests schema form generation and validation.
│
├── ToolHeadersEditor.tsx
│   └── Adds passthrough header rows and denylist validation.
│
├── ToolHeadersEditor.test.tsx
│   └── Tests allowed, denied, invalid, add, and remove behavior.
│
├── ToolPreviewButton.tsx
│   └── Renders Preview / Previewing / Re-run states.
│
├── ToolPreviewResult.tsx
│   └── Renders status, target, warnings, args, and raw response.
│
├── ToolPreviewResult.test.tsx
│   └── Tests preview result rendering.
│
├── toolAnnotations.ts
│   └── Normalizes readOnlyHint and destructiveHint.
│
└── toolAnnotations.test.ts
    └── Tests annotation normalization.


src/i18n/locales/
├── en-US/tools.json
├── es-ES/tools.json
└── pt-BR/tools.json
    └── Adds localized strings for the new Try it UI.


src/pages/
└── Tools.test.tsx
    └── Updates page tests for the new Definition tab location.


e2e/
└── tools.spec.ts
    └── Verifies the Try it flow and existing Tools flows in a browser.
Before vs after
Before
======

Tools
  |
  v
Tools for github-server
  |
  v
Definition-style table shown directly
  |
  +-- View schema
  +-- Edit
  +-- Activate / Deactivate
  +-- Delete


After
=====

Tools
  |
  v
Tools for github-server
  |
  +-- Try it  [default]
  |     |
  |     v
  |   Tool preview
  |     |
  |     +-- Arguments
  |     +-- Headers
  |     +-- Preview / Re-run
  |     +-- Preview 200
  |     +-- Resolved arguments
  |     +-- Raw preview response
  |
  +-- Definition
        |
        v
      Existing tools table
        |
        +-- View schema
        +-- Edit
        +-- Activate / Deactivate
        +-- Delete

Manual verification

Manual test steps

Setup

git checkout feat/6316-tool-preview-drawer
npm ci                 # if node_modules is missing
npm run generate       # if src/generated/ is missing

Save the mock script from the next collapsible at the repo root as tool-preview-manual.mjs.

Two terminals:

# terminal A - dev server
npm run dev                    # :5173, wait for "ready in ..."

# terminal B - opens the mocked browser
node tool-preview-manual.mjs

Terminal B opens a Chrome for Testing window with /auth/session, /api/rbac/my/permissions, /api/tools, /api/gateways, and /api/tools/preview/search_issues mocked. Ctrl-C in terminal B to close. Do everything in that window, in the tab it opens.

Steps

1. Open More options for github-server -> View details.
Expect: the details drawer opens with tabs Try it and Definition. Try it is selected by default.

2. Check the Try it tab.
Expect: Tool preview is visible, the Search repository issues description is shown, and the Read-only badge appears from readOnlyHint.

3. Leave query empty.
Expect: Preview is disabled because query is required by the input schema.

4. Fill query with cloudflare and limit with 5.
Expect: Preview becomes enabled.

5. Click Add header. Enter X-Api-Key as the header name and team-a as the value, then click Preview.
Expect: result shows Preview 200, Resolved arguments, and Raw preview response.

6. Look at terminal B.
Expect: the preview request body is { "arguments": { "query": "cloudflare", "limit": 5 } }, and the logged passthrough headers include x-api-key: team-a.

7. Change X-Api-Key to X-Tenant-Id, then click Re-run.
Expect: the logged passthrough headers include x-tenant-id: team-a.

8. Change X-Tenant-Id to Authorization.
Expect: the header input turns red, the inline warning says This header is not forwardable from the web UI., and Re-run is disabled.

9. Click Definition.
Expect: the existing tools table is visible, including row actions like schema/edit/delete/toggle. This confirms the old details-table surface moved under the Definition tab rather than disappearing.

Teardown

Ctrl-C both terminals. If :5173 is stuck:

lsof -ti:5173 | xargs kill
Mock script (tool-preview-manual.mjs)

Save at the repo root. Requires @playwright/test, already a dev dependency; run npx playwright install chromium if the browser is missing.

// Manual UI testing for contextforge-web-ui#53 - mocked tool preview tab.
//
//   npm run dev                  # terminal A, Vite on :5173
//   node tool-preview-manual.mjs # terminal B
//
// Ctrl-C in terminal B to close the headed browser.
//
// This mocks the backend endpoints needed by /app/tools, including
// /api/tools/preview/search_issues. It verifies frontend wiring only: tabs,
// schema args, passthrough-header filtering, preview request construction, and
// basic result rendering.

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

const BASE = process.env.BASE_URL ?? "http://localhost:5173";
const HEADED = !process.env.HEADLESS;

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 TOOL = {
  id: "tool-search-issues",
  name: "search_issues",
  originalName: "search_issues",
  description: "Search repository issues",
  originalDescription: "Search repository issues",
  title: "Search issues",
  gatewayId: "gw-github-server",
  gatewaySlug: "github-server",
  customName: "search_issues",
  customNameSlug: "search_issues",
  enabled: true,
  reachable: true,
  deprecated: false,
  executionCount: 0,
  tags: [],
  integrationType: "mcp",
  requestType: "http",
  url: "https://github.example/mcp",
  headers: {},
  inputSchema: {
    type: "object",
    required: ["query"],
    properties: {
      query: { type: "string", description: "Search query" },
      limit: { type: "integer", description: "Maximum issues to return" },
    },
  },
  annotations: { readOnlyHint: true },
  jsonpathFilter: null,
  auth: null,
  createdAt: "2026-04-10T10:00:00Z",
  updatedAt: "2026-04-10T10:00:00Z",
};

const GATEWAY_RESPONSE = {
  gateways: [
    {
      id: "gw-github-server",
      name: "github-server",
      url: "https://github.example/mcp",
      description: "Mocked GitHub MCP tools for manual preview verification",
    },
  ],
  nextCursor: null,
};

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

function fallbackApiBody(pathname) {
  if (pathname.startsWith("/api/resources")) return [];
  if (pathname.startsWith("/api/prompts")) return [];
  if (pathname.startsWith("/api/servers")) return [];
  return {};
}

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}`);
});

// Register broad API fallbacks first. Playwright evaluates the newest matching
// route first, so endpoint-specific mocks below must be registered after this.
// Match by URL path instead of a glob like **/api/**; Vite source modules such
// as /src/api/client.ts are JavaScript assets, not backend API calls.
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(["*"])));
await page.route("**/api/tools?*", (route) => route.fulfill(json([TOOL])));
await page.route("**/api/gateways?*", (route) => route.fulfill(json(GATEWAY_RESPONSE)));

await page.route("**/api/tools/preview/search_issues", async (route) => {
  const request = route.request();
  const body = request.postDataJSON();
  const headers = request.headers();
  const args = body?.arguments ?? {};
  const interestingHeaders = Object.fromEntries(
    Object.entries(headers).filter(([name]) =>
      ["authorization", "x-api-key", "x-tenant-id"].includes(name.toLowerCase()),
    ),
  );

  console.log("\npreview request body:");
  console.log(JSON.stringify(body, null, 2));
  console.log("preview passthrough-ish headers:");
  console.log(JSON.stringify(interestingHeaders, null, 2));

  return route.fulfill(
    json({
      target: { kind: "local" },
      resolved_arguments: args,
      annotations: { readOnlyHint: true },
      pre_hooks_run: [],
      warnings: [],
    }),
  );
});

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

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

const cardCount = await page.getByRole("button", { name: "More options for github-server" }).count();
console.log(`tools card: ${cardCount ? "ok" : "MISSING"}`);

if (!HEADED) {
  await browser.close();
} else {
  console.log(`
Browser open. Try:
  1. Open "More options for github-server" -> "View details"
  2. Expect "Try it" selected by default and "Definition" next to it
  3. Fill query="cloudflare" and limit="5"
  4. Add header X-Tenant-Id=team-a, then click Preview
  5. Expect "Preview 200", "Resolved arguments", and "Raw preview response"
  6. Terminal should log arguments plus x-tenant-id
  7. Change the header name to Authorization
  8. Expect "This header is not forwardable from the web UI." and Preview disabled
  9. Change it to X-Api-Key, click Preview, and confirm authorization is not logged
 10. Switch to Definition and confirm the existing tools table/actions still render

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

Run against feat/6316-tool-preview-drawer at dec90ff, branched from main at 414b714.

# Step Expected Result
1 Open details for github-server Drawer opens; Try it and Definition tabs shown; Try it active Pass
2 Inspect Try it tab Tool preview, description, and Read-only badge shown Pass
3 Required arg empty Preview disabled Pass
4 Fill query=cloudflare, limit=5 Preview enabled Pass
5 Preview with X-Api-Key=team-a Preview 200, Resolved arguments, and Raw preview response shown Pass
6 Check terminal request log Body is { "arguments": { "query": "cloudflare", "limit": 5 } }; header includes x-api-key: team-a Pass
7 Re-run with X-Tenant-Id=team-a Terminal header log includes x-tenant-id: team-a Pass
8 Change header to Authorization Inline denylist warning shown; Re-run disabled Pass
9 Open Definition tab Existing tools table/actions remain available under Definition Pass

Observed terminal output for allowed headers:

preview passthrough-ish headers:
{
  "x-api-key": "team-a"
}
preview passthrough-ish headers:
{
  "x-tenant-id": "team-a"
}

Scope of this verification: all backend responses are mocked. This covers frontend wiring only: drawer tabs, schema argument form, passthrough-header filtering, preview request construction, and basic preview result rendering. It does not verify a live backend preview endpoint. Full content-block rendering is tracked by IBM/mcp-context-forge#6317; live invocation/spec-aware snippets are tracked by IBM/mcp-context-forge#6318.

Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>
@gandhipratik203
gandhipratik203 marked this pull request as ready for review August 20, 2026 11:53
Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>
@gandhipratik203 gandhipratik203 self-assigned this Aug 20, 2026
@gandhipratik203 gandhipratik203 changed the title [UI-REWRITE] Add mocked tool preview tab [UI-REWRITE] Add tool preview tab Aug 20, 2026
@gcgoncalves

Copy link
Copy Markdown
Contributor

Depends on IBM/mcp-context-forge#5629 :(

@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

1. Silent data loss in Try It form

File: src/components/tools/ToolTryItTab.tsx:34
Category: High

The reset effect depends on selectedTool.inputSchema by object reference, so any unrelated tools-list update silently wipes the user's in-progress arguments, headers, and preview result.

Failure scenario: User opens the Try It tab, fills in arguments, gets a preview result, then adds a tag via the always-visible sidebar control (handleAddToolTag in src/pages/Tools.tsx replaces the tool with a freshly-fetched object). ToolTryItTab doesn't remount (same tool id), but its effect fires because inputSchema is a new object reference, resetting args to defaults and discarding headers/preview — without the user touching the Try It form.


2. Content-Type header can be overridden

File: src/components/tools/ToolHeadersEditor.tsx:23
Category: Medium

DENIED_HEADERS blocks auth-sensitive headers but not content-type or x-requested-with, letting a user-entered header override the JSON Content-Type that client.ts always sets on preview requests.

Failure scenario: User adds a header named Content-Type with an arbitrary value in the Try It headers editor; it isn't rejected, gets spread into extraHeaders in requestWithMeta, and can duplicate/override the Content-Type header sent with the JSON preview body, breaking backend parsing.


3. ".." tool name misroutes requests

File: src/api/tools.ts:89
Category: Medium

TOOL_NAME_PATTERN permits a tool name of exactly "..", which survives encodeURIComponent and gets collapsed by URL path normalization, misrouting the
preview request to a different endpoint.

Failure scenario: toolsApi.preview('..') builds path /tools/preview/..; new URL() resolution in getRequestUrl normalizes this to /api/tools/, so the POST (with the preview request body) hits the tools list endpoint instead of failing validation as an invalid name.


4. Nested required-field logic ignores optional parent

File: src/components/tools/ToolArgumentsForm.tsx:33
Category: Medium

Nested object fields one level deep are always marked required from their own schema's required list, ignoring whether the parent object itself is optional.

Failure scenario: A schema declares an optional owner object containing a required email sub-property, with owner absent from the top-level required array. The generated form still forces the user to fill owner.email, blocking the Preview button even though omitting owner entirely should be valid.


5. NaN bypasses "number" type validation

File: src/components/tools/ToolArgumentsForm.tsx:118
Category: Low

validateToolArguments checks "number" fields with typeof current !== "number", which does not catch NaN, unlike the "integer" branch which correctly uses Number.isInteger.

Failure scenario: A "number" field ends up with value NaN (e.g. via paste or a non-sanitizing input path); no validation error is shown, and JSON.stringify silently turns NaN into null in the preview request body with no indication to the user that their input was dropped.


6. Duplicated header-editor logic

File: src/components/tools/ToolHeadersEditor.tsx:1
Category: Low

ToolHeadersEditor re-implements the add/remove/update key-value row pattern already present in src/components/mcp-servers/CustomHeadersAuth.tsx instead of sharing a common component.

Impact: Not a runtime bug — future header-editing fixes (e.g. tightening name validation, accessibility tweaks) must be duplicated across both files, risking drift between the two nearly-identical editors.

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

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review.

Addressed items 1-5 in 7ecf103:

  • preserved Try it state when the same tool object refreshes
  • denied Content-Type and X-Requested-With passthrough headers
  • rejected unsafe preview names like . / ..
  • fixed optional nested object required-field handling
  • rejected NaN for number fields

For item 6, I kept the header editor local to this PR because preview passthrough headers have different validation/security behavior than MCP server auth headers.

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

@gcgoncalves gcgoncalves 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.

Solid. Particularly, well done with using Vite config for setting the feature flag. 👏

@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.

Nice work addressing items 1–5, and thanks for the explanation on 6. Two things worth fixing before VITE_ENABLE_TOOL_PREVIEW is flipped on by default:

1. Raw-JSON editor clobbers input while typing

File: src/components/tools/ToolArgumentsForm.tsx:184 (raw JSON fallback for complex schemas)
Category: High

The textarea's onChange calls onChange(parsed) on every valid keystroke, which updates the parent value. The useEffect watching value (line ~146) then calls setRawJson(JSON.stringify(value, null, 2)), re-formatting and overwriting the textarea the instant the JSON becomes valid.

Failure scenario: Typing compact JSON like {"query":"cloudflare"} character by character — the moment it parses successfully, the field gets reformatted to indented multi-line JSON mid-keystroke, resetting the cursor and corrupting further typing. Makes the raw-JSON editor effectively unusable for anything beyond a single paste.

2. Tool name still not trimmed before use

File: src/api/tools.ts:96-101
Category: Low

validateToolName computes trimmed to check for ./.., but still runs TOOL_NAME_PATTERN.test(name) and returns the untrimmed name. A name with incidental leading/trailing whitespace (the pattern allows spaces) is sent untrimmed into the preview URL.

Failure scenario: toolsApi.preview(" search_issues ") builds /tools/preview/%20search_issues%20, which won't match the backend's tool lookup - a 404 on an otherwise-valid tool. Fix: use trimmed in both the pattern test and the return.

Not blocking merge since the feature is behind the flag and already gated on the backend dependency, but flagging so No. 1 in particular gets fixed before the flag goes live.

@gcgoncalves
gcgoncalves merged commit 7c13e4a into main Aug 21, 2026
5 checks passed
@gcgoncalves
gcgoncalves deleted the feat/6316-tool-preview-drawer branch August 21, 2026 09:22
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.

[UI-REWRITE]: Add ToolDetailsPanel Try it tab with mocked preview flow

3 participants