Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 120 additions & 4 deletions internal/server/static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ body {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
min-width: 0;
overflow-x: hidden;
}

Expand Down Expand Up @@ -97,7 +97,7 @@ body {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
min-width: 0;
overflow-x: hidden;
}

Expand Down Expand Up @@ -170,6 +170,7 @@ body {
gap: 20px;
margin: 0 0 20px 0;
align-items: start;
flex-shrink: 0;
}

.tool-info {
Expand All @@ -178,11 +179,21 @@ body {
gap: 15px;
}

.tool-execution-area {
display: flex;
flex-direction: column;
gap: 12px;
}

.tool-params {
background-color: #ffffff;
padding: 15px;
border-radius: 4px;
border: 1px solid #ddd;

h5 {
margin-bottom: 0;
}
}

.tool-box {
Expand All @@ -198,6 +209,25 @@ body {
}
}

.params-header {
display: flex;
justify-content: flex-end;
margin-bottom: 8px;
padding-right: 6px;
font-weight: bold;
font-size: 0.9em;
color: #555;
}

.params-disclaimer {
font-style: italic;
color: #555;
font-size: 0.8em;
margin-bottom: 10px;
width: 100%;
word-wrap: break-word;
}

.param-item {
margin-bottom: 12px;

Expand All @@ -207,6 +237,18 @@ body {
font-family: inherit;
}

&.disabled-param {
> label {
color: #888;
text-decoration: line-through;
}

.param-input-element {
background-color: #f5f5f5;
opacity: 0.6;
}
}

input[type="text"],
input[type="number"],
select,
Expand All @@ -218,17 +260,69 @@ body {
font-family: inherit;
}

input[type="checkbox"].param-input-element {
width: auto;
padding: 0;
border: initial;
border-radius: initial;
vertical-align: middle;
margin-right: 4px;
accent-color: #4285f4;
flex-grow: 0;
}
}

.input-checkbox-wrapper {
display: flex;
align-items: center;
gap: 10px;
}

.param-input-element-container {
flex-grow: 1;
}

.param-input-element {
box-sizing: border-box;
}

.include-param-container {
display: flex;
align-items: center;
white-space: nowrap;

input[type="checkbox"] {
width: auto;
padding: 0;
border: initial;
border-radius: initial;
vertical-align: middle;
margin-right: 4px;
accent-color: #4285f4;
margin-right: 0;
accent-color: #4285f4;
}
}

.include-param-container input[type="checkbox"] {
width: auto;
padding: 0;
border: initial;
border-radius: initial;
vertical-align: middle;
margin: 0;
accent-color: #4285f4;
}

.checkbox-bool-label {
margin-left: 5px;
font-style: italic;
color: #555;
}

.checkbox-bool-label.disabled {
color: #aaa;
cursor: not-allowed;
}

.param-label-extras {
font-style: italic;
font-weight: lighter;
Expand All @@ -240,6 +334,28 @@ body {
cursor: not-allowed;
}

.run-button-container {
display: flex;
justify-content: flex-end;
}

.run-tool-btn {
background-color: #4285f4ff;
color: white;
border: none;
border-radius: 30px;
padding: 10px 20px;
cursor: pointer;
font: inherit;
font-size: 1em;
font-weight: bolder;
transition: background-color 0.3s ease;

&:hover {
background-color: #357ae8;
}
}

.tool-response {
margin: 20px 0 0 0;

Expand Down
5 changes: 3 additions & 2 deletions internal/server/static/js/mainContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
/**
* Renders the main content area into the HTML.
* @param {string} containerId The ID of the DOM element to inject the content into.
* @param {string | null} idString The id of the item inside the main content area.
* @param {string} idString The id of the item inside the main content area.
*/
function renderMainContent(containerId, idString) {
const mainContentContainer = document.getElementById(containerId);
Expand All @@ -24,11 +24,12 @@ function renderMainContent(containerId, idString) {
return;
}

const idAttribute = idString ? `id="${idString}"` : '';
const contentHTML = `
<div class="main-content-area">
<div class="top-bar">
</div>
<main class="content" id="${idString}">
<main class="content" ${idAttribute}">
<h1>Welcome to MCP Toolbox UI</h1>
<p>This is the main content area. Click a tab on the left to navigate.</p>
</main>
Expand Down
162 changes: 162 additions & 0 deletions internal/server/static/js/runTool.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { isParamIncluded } from "./toolDisplay.js";

/**
* Runs a specific tool using the /api/tools/toolName/invoke endpoint
* @param {string} toolId The unique identifier for the tool.
* @param {!HTMLFormElement} form The form element containing parameter inputs.
* @param {!HTMLTextAreaElement} responseArea The textarea to display results or errors.
* @param {!Array<!Object>} parameters An array of parameter definition objects
* @param {!HTMLInputElement} prettifyCheckbox The checkbox to control JSON formatting.
* @param {function(?Object): void} updateLastResults Callback to store the last results.
*/
export async function handleRunTool(toolId, form, responseArea, parameters, prettifyCheckbox, updateLastResults) {
const formData = new FormData(form);
const typedParams = {};
responseArea.value = 'Running tool...';
updateLastResults(null);

for (const param of parameters) {
const NAME = param.name;
const VALUE_TYPE = param.valueType;
const RAW_VALUE = formData.get(NAME);
const INCLUDE_CHECKED = isParamIncluded(toolId, NAME)

try {
if (!INCLUDE_CHECKED) {
console.debug(`Param ${NAME} was intentionally skipped.`)
// if param was purposely unchecked, don't include it in body
continue;
}

if (VALUE_TYPE === 'boolean') {
typedParams[NAME] = RAW_VALUE !== null;
console.debug(`Parameter ${NAME} (boolean) set to: ${typedParams[NAME]}`);
continue;
}

// process remaining types
if (VALUE_TYPE && VALUE_TYPE.startsWith('array<')) {
typedParams[NAME] = parseArrayParameter(RAW_VALUE, VALUE_TYPE, NAME);
} else {
switch (VALUE_TYPE) {
case 'number':
Comment thread
Yuan325 marked this conversation as resolved.
if (RAW_VALUE === "") {
console.debug(`Param ${NAME} was empty, setting to empty string.`)
typedParams[NAME] = "";
} else {
const num = Number(RAW_VALUE);
if (isNaN(num)) {
throw new Error(`Invalid number input for ${NAME}: ${RAW_VALUE}`);
}
typedParams[NAME] = num;
}
break;
case 'string':
default:
typedParams[NAME] = RAW_VALUE;
break;
}
}
} catch (error) {
console.error('Error processing parameter:', NAME, error);
responseArea.value = `Error for ${NAME}: ${error.message}`;
return;
}
}

console.debug('Running tool:', toolId, 'with typed params:', typedParams);
try {
const response = await fetch(`/api/tool/${toolId}/invoke`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(typedParams)
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`HTTP error ${response.status}: ${errorBody}`);
}
const results = await response.json();
updateLastResults(results);
displayResults(results, responseArea, prettifyCheckbox.checked);
} catch (error) {
console.error('Error running tool:', error);
responseArea.value = `Error: ${error.message}`;
updateLastResults(null);
}
}

/**
* Parses and validates a single array parameter from a raw string value.
* @param {string} rawValue The raw string value from FormData.
* @param {string} valueType The full array type string (e.g., "array<number>").
* @param {string} paramName The name of the parameter for error messaging.
* @return {!Array<*>} The parsed array.
* @throws {Error} If parsing or type validation fails.
*/
function parseArrayParameter(rawValue, valueType, paramName) {
const ELEMENT_TYPE = valueType.substring(6, valueType.length - 1);
let parsedArray;
try {
parsedArray = JSON.parse(rawValue);
} catch (e) {
throw new Error(`Invalid JSON format for ${paramName}. Expected an array. ${e.message}`);
}

if (!Array.isArray(parsedArray)) {
throw new Error(`Input for ${paramName} must be a JSON array (e.g., ["a", "b"]).`);
}

return parsedArray.map((item, index) => {
switch (ELEMENT_TYPE) {
case 'number':
const NUM = Number(item);
if (isNaN(NUM)) {
throw new Error(`Invalid number "${item}" found in array for ${paramName} at index ${index}.`);
}
return NUM;
case 'boolean':
return item === true || String(item).toLowerCase() === 'true';
case 'string':
default:
return item;
}
});
}

/**
* Displays the results from the tool run in the response area.
*/
export function displayResults(results, responseArea, prettify) {
if (results === null || results === undefined) {
return;
}
try {
const resultJson = JSON.parse(results.result);
if (prettify) {
responseArea.value = JSON.stringify(resultJson, null, 2);
} else {
responseArea.value = JSON.stringify(resultJson);
}
} catch (error) {
console.error("Error parsing or stringifying results:", error);
if (typeof results.result === 'string') {
responseArea.value = results.result;
} else {
responseArea.value = "Error displaying results. Invalid format.";
}
}
}
Loading
Loading