Skip to content

Commit dd3a965

Browse files
committed
feat: Update dependencies and enhance HTML explorer with cURL and tab UI elements tests
1 parent 1ebb7d0 commit dd3a965

3 files changed

Lines changed: 111 additions & 8 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ dev = [
3636
"pytest-cov>=4.0",
3737
"mypy>=1.0",
3838
"ruff>=0.1",
39-
"apdev[dev]>=0.1.6",
39+
"apdev[dev]>=0.2.0",
4040
]
4141

4242
[project.scripts]

src/apcore_mcp/explorer/html.py

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,24 @@
4646
.result-error { color: #f93e3e; }
4747
.result-success { color: #49cc90; }
4848
.exec-disabled { color: #888; font-size: 0.85rem; font-style: italic; margin-top: 16px; }
49+
.curl-section { margin-top: 14px; }
50+
.curl-block { background: #282c34; color: #abb2bf; padding: 12px; padding-right: 60px;
51+
border-radius: 4px; overflow-x: auto; font-size: 0.82rem;
52+
white-space: pre-wrap; word-break: break-all; position: relative;
53+
font-family: monospace; margin-top: 4px; }
54+
.copy-btn { position: absolute; top: 8px; right: 8px; background: #3a3f4b; color: #999;
55+
border: 1px solid #555; border-radius: 3px; padding: 2px 10px;
56+
font-size: 0.72rem; cursor: pointer; }
57+
.copy-btn:hover { background: #4a4f5b; color: #fff; }
58+
.resp-header { display: flex; align-items: center; gap: 12px; margin-top: 12px; }
59+
.resp-tabs { display: inline-flex; }
60+
.resp-tab { padding: 3px 10px; background: #eee; border: 1px solid #ddd;
61+
cursor: pointer; font-size: 0.75rem; user-select: none; }
62+
.resp-tab:first-child { border-radius: 3px 0 0 3px; }
63+
.resp-tab:last-child { border-radius: 0 3px 3px 0; }
64+
.resp-tab.active { background: #555; color: #fff; border-color: #555; }
65+
.resp-pane { display: none; }
66+
.resp-pane.active { display: block; }
4967
</style>
5068
</head>
5169
<body>
@@ -190,10 +208,19 @@
190208
btn.textContent = 'Executing...';
191209
resultArea.innerHTML = '';
192210
211+
var bodyStr = JSON.stringify(inputs);
212+
var callUrl = window.location.origin + base + '/tools/' + encodeURIComponent(name) + '/call';
213+
var curlBody = bodyStr.replace(/'/g, "'\\\\''" );
214+
var curlCmd = [
215+
"curl -X POST '" + callUrl + "' \\\\",
216+
" -H 'Content-Type: application/json' \\\\",
217+
" -d '" + curlBody + "'"
218+
].join("\\n");
219+
193220
fetch(base + '/tools/' + encodeURIComponent(name) + '/call', {
194221
method: 'POST',
195222
headers: {'Content-Type': 'application/json'},
196-
body: JSON.stringify(inputs)
223+
body: bodyStr
197224
})
198225
.then(function(r) {
199226
if (r.status === 403) {
@@ -212,20 +239,70 @@
212239
btn.disabled = false;
213240
btn.textContent = 'Execute';
214241
var data = result.data;
215-
// MCP CallToolResult format: {content: [...], isError: bool, _meta: {...}}
242+
var html = '';
243+
244+
html += '<div class="curl-section">' +
245+
'<span class="schema-label">cURL</span>' +
246+
'<div class="curl-block"><code class="curl-cmd">' + esc(curlCmd) +
247+
'</code><button class="copy-btn" id="copy-curl-btn">Copy</button></div></div>';
248+
216249
if (data.isError) {
217250
var errText = (data.content || []).map(function(c) { return c.text || ''; }).join('\\n');
218-
resultArea.innerHTML = '<span class="schema-label result-error">Error</span>' +
251+
html += '<span class="schema-label result-error">Response &mdash; Error</span>' +
219252
'<pre>' + esc(errText) + '</pre>';
220253
} else {
221-
// Parse text content blocks for display
222254
var texts = (data.content || []).filter(function(c) { return c.type === 'text'; });
223255
var display = texts.map(function(c) {
224256
try { return JSON.parse(c.text); } catch(e) { return c.text; }
225257
});
226258
var output = display.length === 1 ? display[0] : display;
227-
resultArea.innerHTML = '<span class="schema-label result-success">Result</span>' +
228-
'<pre>' + esc(JSON.stringify(output, null, 2)) + '</pre>';
259+
var friendlyJson = JSON.stringify(output, null, 2);
260+
var rawJson = JSON.stringify(data, null, 2);
261+
262+
html += '<div class="resp-header">' +
263+
'<span class="schema-label result-success" style="margin:0">Response</span>' +
264+
'<span class="resp-tabs">' +
265+
'<span class="resp-tab active" data-tab="friendly">Result</span>' +
266+
'<span class="resp-tab" data-tab="raw">Raw MCP</span>' +
267+
'</span></div>' +
268+
'<pre class="resp-pane active" data-pane="friendly">' + esc(friendlyJson) + '</pre>' +
269+
'<pre class="resp-pane" data-pane="raw">' + esc(rawJson) + '</pre>';
270+
}
271+
272+
resultArea.innerHTML = html;
273+
274+
var copyBtn = document.getElementById('copy-curl-btn');
275+
if (copyBtn) {
276+
copyBtn.onclick = function() {
277+
var cmd = resultArea.querySelector('.curl-cmd');
278+
if (cmd && navigator.clipboard) {
279+
navigator.clipboard.writeText(cmd.textContent).then(function() {
280+
copyBtn.textContent = 'Copied!';
281+
setTimeout(function() { copyBtn.textContent = 'Copy'; }, 1500);
282+
}).catch(function() {
283+
copyBtn.textContent = 'Failed';
284+
setTimeout(function() { copyBtn.textContent = 'Copy'; }, 1500);
285+
});
286+
}
287+
};
288+
}
289+
var tabs = resultArea.querySelectorAll('.resp-tab');
290+
for (var i = 0; i < tabs.length; i++) {
291+
(function(tab) {
292+
tab.onclick = function() {
293+
var target = tab.getAttribute('data-tab');
294+
var allTabs = resultArea.querySelectorAll('.resp-tab');
295+
var allPanes = resultArea.querySelectorAll('.resp-pane');
296+
for (var j = 0; j < allTabs.length; j++) {
297+
allTabs[j].className = allTabs[j].getAttribute('data-tab') === target
298+
? 'resp-tab active' : 'resp-tab';
299+
}
300+
for (var j = 0; j < allPanes.length; j++) {
301+
allPanes[j].className = allPanes[j].getAttribute('data-pane') === target
302+
? 'resp-pane active' : 'resp-pane';
303+
}
304+
};
305+
})(tabs[i]);
229306
}
230307
})
231308
.catch(function(e) {

tests/explorer/test_explorer.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,33 @@ def test_explorer_flag_does_not_error_for_stdio(self) -> None:
351351
# ---------------------------------------------------------------------------
352352

353353

354-
class TestTC008CustomPrefix:
354+
# ---------------------------------------------------------------------------
355+
# TC-008: Explorer HTML contains cURL and tab UI elements
356+
# ---------------------------------------------------------------------------
357+
358+
359+
class TestTC008CurlAndTabs:
360+
def test_explorer_page_contains_curl_css(self, explorer_app: Starlette) -> None:
361+
client = TestClient(explorer_app)
362+
response = client.get("/explorer/")
363+
assert ".curl-block" in response.text
364+
assert ".copy-btn" in response.text
365+
assert ".curl-section" in response.text
366+
367+
def test_explorer_page_contains_tab_css(self, explorer_app: Starlette) -> None:
368+
client = TestClient(explorer_app)
369+
response = client.get("/explorer/")
370+
assert ".resp-tab" in response.text
371+
assert ".resp-pane" in response.text
372+
assert ".resp-header" in response.text
373+
374+
375+
# ---------------------------------------------------------------------------
376+
# TC-009: Custom explorer_prefix mounts at /custom/
377+
# ---------------------------------------------------------------------------
378+
379+
380+
class TestTC009CustomPrefix:
355381
def test_custom_prefix(
356382
self,
357383
sample_tools: list[MockTool],

0 commit comments

Comments
 (0)