Skip to content

Commit 8406e57

Browse files
committed
feat(ui): migrate static ui from native API to MCP API endpoints (#2826)
1 parent a2a4a7b commit 8406e57

File tree

2 files changed

+132
-93
lines changed

2 files changed

+132
-93
lines changed

internal/server/static/js/loadTools.js

Lines changed: 87 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
import { renderToolInterface } from "./toolDisplay.js";
1616
import { escapeHtml } from "./sanitize.js";
1717

18-
let toolDetailsAbortController = null;
18+
let currentToolsList = [];
1919

2020
/**
21-
* Fetches a toolset from the /api/toolset endpoint and initiates creating the tool list.
21+
* Fetches a toolset from the /mcp endpoint and initiates creating the tool list.
2222
* @param {!HTMLElement} secondNavContent The HTML element where the tool list will be rendered.
2323
* @param {!HTMLElement} toolDisplayArea The HTML element where the details of a selected tool will be displayed.
2424
* @param {string} toolsetName The name of the toolset to load (empty string loads all tools).
@@ -27,10 +27,22 @@ let toolDetailsAbortController = null;
2727
export async function loadTools(secondNavContent, toolDisplayArea, toolsetName) {
2828
secondNavContent.innerHTML = '<p>Fetching tools...</p>';
2929
try {
30-
const response = await fetch(`/api/toolset/${toolsetName}`);
30+
const url = toolsetName ? `/mcp/${toolsetName}` : `/mcp`;
31+
const response = await fetch(url, {
32+
method: 'POST',
33+
headers: { 'Content-Type': 'application/json' },
34+
body: JSON.stringify({
35+
jsonrpc: "2.0",
36+
id: "1",
37+
method: "tools/list",
38+
'Mcp-Protocol-Version': '2025-11-25'
39+
})
40+
});
41+
3142
if (!response.ok) {
3243
throw new Error(`HTTP error! status: ${response.status}`);
3344
}
45+
3446
const apiResponse = await response.json();
3547
renderToolList(apiResponse, secondNavContent, toolDisplayArea);
3648
} catch (error) {
@@ -41,33 +53,32 @@ export async function loadTools(secondNavContent, toolDisplayArea, toolsetName)
4153

4254
/**
4355
* Renders the list of tools as buttons within the provided HTML element.
44-
* @param {?{tools: ?Object<string,*>} } apiResponse The API response object containing the tools.
56+
* @param {Object} apiResponse The API response object containing the tools.
4557
* @param {!HTMLElement} secondNavContent The HTML element to render the tool list into.
4658
* @param {!HTMLElement} toolDisplayArea The HTML element for displaying tool details (passed to event handlers).
4759
*/
4860
function renderToolList(apiResponse, secondNavContent, toolDisplayArea) {
4961
secondNavContent.innerHTML = '';
5062

51-
if (!apiResponse || typeof apiResponse.tools !== 'object' || apiResponse.tools === null) {
52-
console.error('Error: Expected an object with a "tools" property, but received:', apiResponse);
63+
if (!apiResponse || !apiResponse.result || !Array.isArray(apiResponse.result.tools)) {
64+
console.error('Error: Expected a valid MCP response with "result.tools" array, but received:', apiResponse);
5365
secondNavContent.textContent = 'Error: Invalid response format from toolset API.';
5466
return;
5567
}
5668

57-
const toolsObject = apiResponse.tools;
58-
const toolNames = Object.keys(toolsObject);
69+
currentToolsList = apiResponse.result.tools;
5970

60-
if (toolNames.length === 0) {
71+
if (currentToolsList.length === 0) {
6172
secondNavContent.textContent = 'No tools found.';
6273
return;
6374
}
6475

6576
const ul = document.createElement('ul');
66-
toolNames.forEach(toolName => {
77+
currentToolsList.forEach(toolObj => {
6778
const li = document.createElement('li');
6879
const button = document.createElement('button');
69-
button.textContent = toolName;
70-
button.dataset.toolname = toolName;
80+
button.textContent = toolObj.name;
81+
button.dataset.toolname = toolObj.name;
7182
button.classList.add('tool-button');
7283
button.addEventListener('click', (event) => handleToolClick(event, secondNavContent, toolDisplayArea));
7384
li.appendChild(button);
@@ -90,86 +101,81 @@ function handleToolClick(event, secondNavContent, toolDisplayArea) {
90101
currentActive.classList.remove('active');
91102
}
92103
event.target.classList.add('active');
93-
fetchToolDetails(toolName, toolDisplayArea);
104+
renderToolDetails(toolName, toolDisplayArea);
94105
}
95106
}
96107

97108
/**
98-
* Fetches details for a specific tool /api/tool endpoint.
99-
* It aborts any previous in-flight request for tool details to stop race condition.
100-
* @param {string} toolName The name of the tool to fetch details for.
109+
* Renders details for a specific tool from the cached MCP tools list.
110+
* @param {string} toolName The name of the tool to render details for.
101111
* @param {!HTMLElement} toolDisplayArea The HTML element to display the tool interface in.
102-
* @returns {!Promise<void>} A promise that resolves when the tool details are fetched and rendered, or rejects on error.
103112
*/
104-
async function fetchToolDetails(toolName, toolDisplayArea) {
105-
if (toolDetailsAbortController) {
106-
toolDetailsAbortController.abort();
107-
console.debug("Aborted previous tool fetch.");
108-
}
113+
function renderToolDetails(toolName, toolDisplayArea) {
114+
const toolObject = currentToolsList.find(t => t.name === toolName);
109115

110-
toolDetailsAbortController = new AbortController();
111-
const signal = toolDetailsAbortController.signal;
116+
if (!toolObject) {
117+
toolDisplayArea.innerHTML = `<p class="error">Tool "${escapeHtml(toolName)}" data not found.</p>`;
118+
return;
119+
}
112120

113-
toolDisplayArea.innerHTML = '<p>Loading tool details...</p>';
121+
console.debug("Rendering tool object: ", toolObject);
114122

115-
try {
116-
const response = await fetch(`/api/tool/${encodeURIComponent(toolName)}`, { signal });
117-
if (!response.ok) {
118-
throw new Error(`HTTP error! status: ${response.status}`);
123+
let toolAuthRequired = [];
124+
let toolAuthParams = {};
125+
if (toolObject._meta) {
126+
if (toolObject._meta["toolbox/authInvoke"]) {
127+
toolAuthRequired = toolObject._meta["toolbox/authInvoke"];
119128
}
120-
const apiResponse = await response.json();
121-
122-
if (!apiResponse.tools || !apiResponse.tools[toolName]) {
123-
throw new Error(`Tool "${toolName}" data not found in API response.`);
129+
if (toolObject._meta["toolbox/authParam"]) {
130+
toolAuthParams = toolObject._meta["toolbox/authParam"];
124131
}
125-
const toolObject = apiResponse.tools[toolName];
126-
console.debug("Received tool object: ", toolObject)
127-
128-
const toolInterfaceData = {
129-
id: toolName,
130-
name: toolName,
131-
description: toolObject.description || "No description provided.",
132-
authRequired: toolObject.authRequired || [],
133-
parameters: (toolObject.parameters || []).map(param => {
134-
let inputType = 'text';
135-
const apiType = param.type ? param.type.toLowerCase() : 'string';
136-
let valueType = 'string';
137-
let label = param.description || param.name;
138-
139-
if (apiType === 'integer' || apiType === 'float') {
140-
inputType = 'number';
141-
valueType = 'number';
142-
} else if (apiType === 'boolean') {
143-
inputType = 'checkbox';
144-
valueType = 'boolean';
145-
} else if (apiType === 'array') {
146-
inputType = 'textarea';
147-
const itemType = param.items && param.items.type ? param.items.type.toLowerCase() : 'string';
148-
valueType = `array<${itemType}>`;
149-
label += ' (Array)';
150-
}
151-
152-
return {
153-
name: param.name,
154-
type: inputType,
155-
valueType: valueType,
156-
label: label,
157-
authServices: param.authServices, // TODO: to be updated when the native endpoint is no longer supported.
158-
required: param.required || false,
159-
// defaultValue: param.default, can't do this yet bc tool manifest doesn't have default
160-
};
161-
})
162-
};
163-
164-
console.debug("Transformed toolInterfaceData:", toolInterfaceData);
132+
}
165133

166-
renderToolInterface(toolInterfaceData, toolDisplayArea);
167-
} catch (error) {
168-
if (error.name === 'AbortError') {
169-
console.debug("Previous fetch was aborted, expected behavior.");
170-
} else {
171-
console.error(`Failed to load details for tool "${toolName}":`, error);
172-
toolDisplayArea.innerHTML = `<p class="error">Failed to load details for ${escapeHtml(toolName)}. ${escapeHtml(error.message)}</p>`;
173-
}
134+
// Default processing if inputSchema properties are not present
135+
let toolParameters = [];
136+
if (toolObject.inputSchema && toolObject.inputSchema.properties) {
137+
const props = toolObject.inputSchema.properties;
138+
const requiredFields = toolObject.inputSchema.required || [];
139+
140+
toolParameters = Object.keys(props).map(paramName => {
141+
const param = props[paramName];
142+
let inputType = 'text';
143+
const apiType = param.type ? param.type.toLowerCase() : 'string';
144+
let valueType = 'string';
145+
let label = param.description || paramName;
146+
147+
if (apiType === 'integer' || apiType === 'number') {
148+
inputType = 'number';
149+
valueType = 'number';
150+
} else if (apiType === 'boolean') {
151+
inputType = 'checkbox';
152+
valueType = 'boolean';
153+
} else if (apiType === 'array') {
154+
inputType = 'textarea';
155+
const itemType = param.items && param.items.type ? param.items.type.toLowerCase() : 'string';
156+
valueType = `array<${itemType}>`;
157+
label += ' (Array)';
158+
}
159+
160+
return {
161+
name: paramName,
162+
type: inputType,
163+
valueType: valueType,
164+
label: label,
165+
required: requiredFields.includes(paramName),
166+
authServices: toolAuthParams[paramName] || []
167+
};
168+
});
174169
}
170+
171+
const toolInterfaceData = {
172+
id: toolName,
173+
name: toolName,
174+
description: toolObject.description || "No description provided.",
175+
authRequired: toolAuthRequired,
176+
parameters: toolParameters
177+
};
178+
179+
console.debug("Transformed toolInterfaceData:", toolInterfaceData);
180+
renderToolInterface(toolInterfaceData, toolDisplayArea);
175181
}

internal/server/static/js/runTool.js

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -80,15 +80,33 @@ export async function handleRunTool(toolId, form, responseArea, parameters, pret
8080

8181
console.debug('Running tool:', toolId, 'with typed params:', typedParams);
8282
try {
83-
const response = await fetch(`/api/tool/${toolId}/invoke`, {
83+
const body = {
84+
jsonrpc: "2.0",
85+
id: "2",
86+
method: "tools/call",
87+
params: {
88+
name: toolId,
89+
arguments: typedParams
90+
}
91+
};
92+
93+
const mcpHeaders = {
94+
...headers,
95+
'Content-Type': 'application/json',
96+
'Mcp-Protocol-Version': '2025-11-25'
97+
};
98+
99+
const response = await fetch(`/mcp`, {
84100
method: 'POST',
85-
headers: headers,
86-
body: JSON.stringify(typedParams)
101+
headers: mcpHeaders,
102+
body: JSON.stringify(body)
87103
});
104+
88105
if (!response.ok) {
89106
const errorBody = await response.text();
90107
throw new Error(`HTTP error ${response.status}: ${errorBody}`);
91108
}
109+
92110
const results = await response.json();
93111
updateLastResults(results);
94112
displayResults(results, responseArea, prettifyCheckbox.checked);
@@ -144,19 +162,34 @@ export function displayResults(results, responseArea, prettify) {
144162
if (results === null || results === undefined) {
145163
return;
146164
}
165+
166+
if (results.error) {
167+
responseArea.value = `MCP Error ${results.error.code}: ${results.error.message}\n${JSON.stringify(results.error.data, null, 2) || ''}`;
168+
return;
169+
}
170+
147171
try {
148-
const resultJson = JSON.parse(results.result);
149-
if (prettify) {
150-
responseArea.value = JSON.stringify(resultJson, null, 2);
172+
let textContent = "";
173+
if (results.result && Array.isArray(results.result.content)) {
174+
textContent = results.result.content
175+
.filter(c => c.type === 'text' && typeof c.text === 'string')
176+
.map(c => c.text)
177+
.join('\n');
178+
} else if (results.result && typeof results.result.content === 'string') {
179+
textContent = results.result.content;
151180
} else {
152-
responseArea.value = JSON.stringify(resultJson);
181+
textContent = JSON.stringify(results.result, null, 2);
182+
}
183+
184+
try {
185+
const resultJson = JSON.parse(textContent);
186+
responseArea.value = prettify ? JSON.stringify(resultJson, null, 2) : JSON.stringify(resultJson);
187+
} catch (e) {
188+
// Not pure JSON string, output as-is
189+
responseArea.value = textContent;
153190
}
154191
} catch (error) {
155192
console.error("Error parsing or stringifying results:", error);
156-
if (typeof results.result === 'string') {
157-
responseArea.value = results.result;
158-
} else {
159-
responseArea.value = "Error displaying results. Invalid format.";
160-
}
193+
responseArea.value = typeof results === 'object' ? JSON.stringify(results, null, 2) : String(results);
161194
}
162195
}

0 commit comments

Comments
 (0)