Skip to content

Commit c3059c2

Browse files
anubhav756Yuan325
andauthored
feat(server): MCP endpoints API consolidation (#2829)
## Overview This PR serves as the foundational layer that will eventually merge all upcoming PRs for the new test harness and new MCP integration tests for multiple databases. **Before** | **After** --- | --- <img width="1133" height="1032" alt="image" src="https://github.com/user-attachments/assets/4a4fe226-aea8-43d7-9c80-fbedf0ce2e5b" /> | <img width="1323" height="1539" alt="image" src="https://github.com/user-attachments/assets/75526df2-3351-4dd9-a3d7-f3e04d177d0e" /> ## Strategy While the legacy integration tests continue to run over the legacy `/api` endpoints, we are introducing the new native MCP JSON-RPC harness in parallel. This allows us to verify both pathways side-by-side without breaking existing CI coverage. To support this, we utilize the `--enable-api` flag to control whether the server initializes the legacy API handlers or runs in MCP-only mode. This ensures that the new tests can accurately verify the behavior of the server when running in its final intended configuration. ## Changes - Updates to `internal/server/server.go` to support dynamic execution and flag-controlled initialization. - Frontend files updated (`loadTools.js` and `runTool.js`) to streamline how tools are loaded and invoked in the UI, aligning it with the upcoming native MCP transition. ## Checklist - [x] Ensure the tests and linter pass - [x] Manually verified functionality of Toolbox UI --------- Co-authored-by: Yuan Teoh <yuanteoh@google.com>
1 parent 28a4910 commit c3059c2

File tree

4 files changed

+147
-95
lines changed

4 files changed

+147
-95
lines changed

UPGRADING.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ If you still require the legacy `/api` endpoint, you must explicitly activate it
2626
endpoint exclusively, as the `/api` endpoint is now deprecated. If your workflow
2727
relied on a non-standard feature that is missing from the new implementation, please submit a
2828
feature request on our [GitHub Issues page](https://github.com/googleapis/genai-toolbox/issues).
29-
* **UI Dependency:** Until the UI is officially migrated, it still requires the API to function. You must run the toolbox with both flags: `./toolbox --ui --enable-api`.
3029

3130
### 2. Strict Tool Naming Validation (SEP986)
3231
Tool names are now strictly validated against [ModelContextProtocol SEP986 guidelines](https://github.com/alexhancock/modelcontextprotocol/blob/main/docs/specification/draft/server/tools.mdx#tool-names) prior to MCP initialization.
@@ -110,4 +109,4 @@ The following CLI flags are deprecated and will be removed in a future release.
110109

111110
* **Prebuilt Tools:** Toolsets have been resized for better performance.
112111
## 📚 Documentation Moved
113-
Our official documentation has a new home! Please update your bookmarks to [mcp-toolbox.dev](http://mcp-toolbox.dev).
112+
Our official documentation has a new home! Please update your bookmarks to [mcp-toolbox.dev](http://mcp-toolbox.dev).

internal/server/server.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
"github.com/go-chi/chi/v5/middleware"
3333
"github.com/go-chi/cors"
3434
"github.com/go-chi/httplog/v3"
35+
"github.com/go-chi/render"
3536
"github.com/googleapis/genai-toolbox/internal/auth"
3637
"github.com/googleapis/genai-toolbox/internal/auth/generic"
3738
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
@@ -469,6 +470,11 @@ func NewServer(ctx context.Context, cfg ServerConfig) (*Server, error) {
469470
return nil, err
470471
}
471472
r.Mount("/api", apiR)
473+
} else {
474+
r.Handle("/api/*", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
475+
err := errors.New("/api native endpoints are disabled by default. Please use the standard /mcp JSON-RPC endpoint")
476+
_ = render.Render(w, r, newErrResponse(err, http.StatusGone))
477+
}))
472478
}
473479
if cfg.UI {
474480
webR, err := webRouter()

internal/server/static/js/loadTools.js

Lines changed: 95 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,24 @@ 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: {
34+
'Content-Type': 'application/json',
35+
'MCP-Protocol-Version': '2025-11-25'
36+
},
37+
body: JSON.stringify({
38+
jsonrpc: "2.0",
39+
id: "1",
40+
method: "tools/list",
41+
})
42+
});
43+
3144
if (!response.ok) {
3245
throw new Error(`HTTP error! status: ${response.status}`);
3346
}
47+
3448
const apiResponse = await response.json();
3549
renderToolList(apiResponse, secondNavContent, toolDisplayArea);
3650
} catch (error) {
@@ -41,33 +55,38 @@ export async function loadTools(secondNavContent, toolDisplayArea, toolsetName)
4155

4256
/**
4357
* 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.
58+
* @param {Object} apiResponse The API response object containing the tools.
4559
* @param {!HTMLElement} secondNavContent The HTML element to render the tool list into.
4660
* @param {!HTMLElement} toolDisplayArea The HTML element for displaying tool details (passed to event handlers).
4761
*/
4862
function renderToolList(apiResponse, secondNavContent, toolDisplayArea) {
4963
secondNavContent.innerHTML = '';
5064

51-
if (!apiResponse || typeof apiResponse.tools !== 'object' || apiResponse.tools === null) {
52-
console.error('Error: Expected an object with a "tools" property, but received:', apiResponse);
65+
if (apiResponse && apiResponse.error) {
66+
console.error('MCP API Error:', apiResponse.error);
67+
secondNavContent.textContent = `Error: ${apiResponse.error.message || 'Unknown MCP error'}`;
68+
return;
69+
}
70+
71+
if (!apiResponse || !apiResponse.result || !Array.isArray(apiResponse.result.tools)) {
72+
console.error('Error: Expected a valid MCP response with "result.tools" array, but received:', apiResponse);
5373
secondNavContent.textContent = 'Error: Invalid response format from toolset API.';
5474
return;
5575
}
5676

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

60-
if (toolNames.length === 0) {
79+
if (currentToolsList.length === 0) {
6180
secondNavContent.textContent = 'No tools found.';
6281
return;
6382
}
6483

6584
const ul = document.createElement('ul');
66-
toolNames.forEach(toolName => {
85+
currentToolsList.forEach(toolObj => {
6786
const li = document.createElement('li');
6887
const button = document.createElement('button');
69-
button.textContent = toolName;
70-
button.dataset.toolname = toolName;
88+
button.textContent = toolObj.name;
89+
button.dataset.toolname = toolObj.name;
7190
button.classList.add('tool-button');
7291
button.addEventListener('click', (event) => handleToolClick(event, secondNavContent, toolDisplayArea));
7392
li.appendChild(button);
@@ -90,86 +109,81 @@ function handleToolClick(event, secondNavContent, toolDisplayArea) {
90109
currentActive.classList.remove('active');
91110
}
92111
event.target.classList.add('active');
93-
fetchToolDetails(toolName, toolDisplayArea);
112+
renderToolDetails(toolName, toolDisplayArea);
94113
}
95114
}
96115

97116
/**
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.
117+
* Renders details for a specific tool from the cached MCP tools list.
118+
* @param {string} toolName The name of the tool to render details for.
101119
* @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.
103120
*/
104-
async function fetchToolDetails(toolName, toolDisplayArea) {
105-
if (toolDetailsAbortController) {
106-
toolDetailsAbortController.abort();
107-
console.debug("Aborted previous tool fetch.");
108-
}
121+
function renderToolDetails(toolName, toolDisplayArea) {
122+
const toolObject = currentToolsList.find(t => t.name === toolName);
109123

110-
toolDetailsAbortController = new AbortController();
111-
const signal = toolDetailsAbortController.signal;
124+
if (!toolObject) {
125+
toolDisplayArea.innerHTML = `<p class="error">Tool "${escapeHtml(toolName)}" data not found.</p>`;
126+
return;
127+
}
112128

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

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}`);
131+
let toolAuthRequired = [];
132+
let toolAuthParams = {};
133+
if (toolObject._meta) {
134+
if (toolObject._meta["toolbox/authInvoke"]) {
135+
toolAuthRequired = toolObject._meta["toolbox/authInvoke"];
119136
}
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.`);
137+
if (toolObject._meta["toolbox/authParam"]) {
138+
toolAuthParams = toolObject._meta["toolbox/authParam"];
124139
}
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);
140+
}
165141

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-
}
142+
// Default processing if inputSchema properties are not present
143+
let toolParameters = [];
144+
if (toolObject.inputSchema && toolObject.inputSchema.properties) {
145+
const props = toolObject.inputSchema.properties;
146+
const requiredFields = toolObject.inputSchema.required || [];
147+
148+
toolParameters = Object.keys(props).map(paramName => {
149+
const param = props[paramName];
150+
let inputType = 'text';
151+
const apiType = param.type ? param.type.toLowerCase() : 'string';
152+
let valueType = 'string';
153+
let label = param.description || paramName;
154+
155+
if (apiType === 'integer' || apiType === 'number') {
156+
inputType = 'number';
157+
valueType = 'number';
158+
} else if (apiType === 'boolean') {
159+
inputType = 'checkbox';
160+
valueType = 'boolean';
161+
} else if (apiType === 'array') {
162+
inputType = 'textarea';
163+
const itemType = param.items && param.items.type ? param.items.type.toLowerCase() : 'string';
164+
valueType = `array<${itemType}>`;
165+
label += ' (Array)';
166+
}
167+
168+
return {
169+
name: paramName,
170+
type: inputType,
171+
valueType: valueType,
172+
label: label,
173+
required: requiredFields.includes(paramName),
174+
authServices: toolAuthParams[paramName] || []
175+
};
176+
});
174177
}
178+
179+
const toolInterfaceData = {
180+
id: toolName,
181+
name: toolName,
182+
description: toolObject.description || "No description provided.",
183+
authRequired: toolAuthRequired,
184+
parameters: toolParameters
185+
};
186+
187+
console.debug("Transformed toolInterfaceData:", toolInterfaceData);
188+
renderToolInterface(toolInterfaceData, toolDisplayArea);
175189
}

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)