Compare commits
3 Commits
a1754feaf3
...
4b50a75a3d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b50a75a3d | ||
|
|
9d001c86fc | ||
|
|
776acc2373 |
@ -311,6 +311,22 @@ async def init_agent(config: AgentConfig):
|
||||
sandbox, sandbox_type, workspace_root = await sandbox_task
|
||||
logger.info(f"init_agent sandbox ready, elapsed: {time.time() - create_start:.3f}s")
|
||||
|
||||
# Inject shell_env into Daytona sandbox via BASH_ENV file
|
||||
if sandbox is not None and sandbox_type == "daytona":
|
||||
_shell_env = {
|
||||
"ASSISTANT_ID": config.bot_id,
|
||||
"USER_IDENTIFIER": config.user_identifier,
|
||||
"TRACE_ID": config.trace_id,
|
||||
"ENABLE_SELF_KNOWLEDGE": str(config.enable_self_knowledge).lower(),
|
||||
**(config.shell_env or {}),
|
||||
}
|
||||
env_lines = "\n".join(f'export {k}="{v}"' for k, v in _shell_env.items() if v is not None)
|
||||
if env_lines:
|
||||
from utils.daytona_sync import REMOTE_BASH_ENV_PATH, REMOTE_WORKSPACE_ROOT
|
||||
bash_env_content = f"cd {REMOTE_WORKSPACE_ROOT}\n{env_lines}"
|
||||
sandbox.execute(f"cat > {REMOTE_BASH_ENV_PATH} << 'ENVEOF'\n{bash_env_content}\nENVEOF")
|
||||
logger.info(f"Injected {len(_shell_env)} env vars into Daytona BASH_ENV")
|
||||
|
||||
# Load sub-agents from skill directories
|
||||
subagents = await load_subagents(
|
||||
bot_id=config.bot_id,
|
||||
|
||||
1063
docs/mcp-app-training.md
Normal file
1063
docs/mcp-app-training.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "ecommerce-storefront",
|
||||
"description": "Renders interactive product browsing, selection, and order confirmation UI for e-commerce scenarios.",
|
||||
"hooks": {
|
||||
"PrePrompt": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python hooks/pre_prompt.py"
|
||||
}
|
||||
]
|
||||
},
|
||||
"mcpServers": {
|
||||
"ecommerce_storefront": {
|
||||
"transport": "stdio",
|
||||
"command": "python",
|
||||
"args": ["./ecommerce_server.py", "{bot_id}"]
|
||||
}
|
||||
}
|
||||
}
|
||||
233
skills/developing/ecommerce-storefront/apps/order-confirm.html
Normal file
233
skills/developing/ecommerce-storefront/apps/order-confirm.html
Normal file
@ -0,0 +1,233 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Order Confirmation</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f7; padding: 20px; }
|
||||
|
||||
.order-card { background: #fff; border-radius: 16px; border: 1px solid #e5e5ea; overflow: hidden; max-width: 480px; margin: 0 auto; }
|
||||
.order-header { padding: 20px 24px 16px; border-bottom: 1px solid #f0f0f0; }
|
||||
.order-title { font-size: 20px; font-weight: 700; color: #1d1d1f; }
|
||||
.order-id { font-size: 13px; color: #86868b; margin-top: 4px; }
|
||||
|
||||
.order-items { padding: 16px 24px; }
|
||||
.order-item { display: flex; align-items: center; padding: 12px 0; border-bottom: 1px solid #f5f5f7; }
|
||||
.order-item:last-child { border-bottom: none; }
|
||||
.item-img { width: 48px; height: 48px; border-radius: 10px; object-fit: cover; background: #f0f0f0; margin-right: 12px; flex-shrink: 0; }
|
||||
.item-img-placeholder { width: 48px; height: 48px; border-radius: 10px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex; align-items: center; justify-content: center; color: #fff; font-size: 18px; font-weight: 600; margin-right: 12px; flex-shrink: 0; }
|
||||
.item-info { flex: 1; min-width: 0; }
|
||||
.item-name { font-size: 15px; font-weight: 500; color: #1d1d1f; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.item-qty { font-size: 13px; color: #86868b; margin-top: 2px; }
|
||||
.item-price { font-size: 15px; font-weight: 600; color: #1d1d1f; flex-shrink: 0; margin-left: 12px; }
|
||||
|
||||
.order-summary { padding: 16px 24px; border-top: 1px solid #e5e5ea; background: #fafafa; }
|
||||
.summary-row { display: flex; justify-content: space-between; padding: 4px 0; font-size: 14px; color: #6e6e73; }
|
||||
.summary-row.discount { color: #34c759; }
|
||||
.summary-row.total { padding-top: 12px; margin-top: 8px; border-top: 1px solid #e5e5ea;
|
||||
font-size: 18px; font-weight: 700; color: #1d1d1f; }
|
||||
|
||||
.payment-section { padding: 20px 24px; }
|
||||
.payment-qr { display: flex; flex-direction: column; align-items: center; padding: 16px 0; }
|
||||
.payment-qr img { width: 160px; height: 160px; border-radius: 8px; border: 1px solid #e5e5ea; }
|
||||
.payment-qr-hint { font-size: 13px; color: #86868b; margin-top: 8px; }
|
||||
|
||||
.btn-row { display: flex; gap: 10px; }
|
||||
.btn { flex: 1; padding: 14px; border: none; border-radius: 12px; font-size: 15px; font-weight: 600; cursor: pointer; transition: all 0.15s; }
|
||||
.btn-primary { background: #0071e3; color: #fff; }
|
||||
.btn-primary:hover { background: #0077ed; }
|
||||
.btn-primary:active { background: #005bb5; }
|
||||
.btn-secondary { background: #f5f5f7; color: #1d1d1f; border: 1px solid #d1d1d6; }
|
||||
.btn-secondary:hover { background: #e8e8ed; }
|
||||
.btn-link { background: #34c759; color: #fff; text-decoration: none; text-align: center; display: block; }
|
||||
.btn-link:hover { background: #2db84d; }
|
||||
|
||||
.success-overlay { display: none; position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(255,255,255,0.95); border-radius: 16px;
|
||||
flex-direction: column; align-items: center; justify-content: center; }
|
||||
.success-overlay.show { display: flex; }
|
||||
.success-icon { font-size: 48px; margin-bottom: 12px; }
|
||||
.success-text { font-size: 18px; font-weight: 600; color: #1d1d1f; }
|
||||
.success-sub { font-size: 14px; color: #86868b; margin-top: 4px; }
|
||||
|
||||
.order-card-wrapper { position: relative; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="order-card-wrapper">
|
||||
<div class="order-card" id="order-card"></div>
|
||||
<div class="success-overlay" id="success-overlay">
|
||||
<div class="success-icon" id="success-icon"></div>
|
||||
<div class="success-text" id="success-text"></div>
|
||||
<div class="success-sub" id="success-sub"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
function esc(t) { var d = document.createElement('div'); d.textContent = t; return d.innerHTML; }
|
||||
|
||||
function render(payload) {
|
||||
var card = document.getElementById('order-card');
|
||||
card.innerHTML = '';
|
||||
var order = payload.order || {};
|
||||
var payment = payload.payment || {};
|
||||
var currency = order.currency || '$';
|
||||
|
||||
// Header
|
||||
var header = document.createElement('div');
|
||||
header.className = 'order-header';
|
||||
header.innerHTML = '<div class="order-title">' + esc(payload.title || 'Order Confirmation') + '</div>'
|
||||
+ (order.order_id ? '<div class="order-id">Order #' + esc(order.order_id) + '</div>' : '');
|
||||
card.appendChild(header);
|
||||
|
||||
// Items
|
||||
var itemsDiv = document.createElement('div');
|
||||
itemsDiv.className = 'order-items';
|
||||
(order.items || []).forEach(function (item) {
|
||||
var row = document.createElement('div');
|
||||
row.className = 'order-item';
|
||||
|
||||
if (item.image) {
|
||||
var img = document.createElement('img');
|
||||
img.className = 'item-img';
|
||||
img.src = item.image;
|
||||
img.onerror = function () {
|
||||
var ph = document.createElement('div');
|
||||
ph.className = 'item-img-placeholder';
|
||||
ph.textContent = (item.name || 'I').charAt(0).toUpperCase();
|
||||
row.replaceChild(ph, img);
|
||||
};
|
||||
row.appendChild(img);
|
||||
} else {
|
||||
var ph = document.createElement('div');
|
||||
ph.className = 'item-img-placeholder';
|
||||
ph.textContent = (item.name || 'I').charAt(0).toUpperCase();
|
||||
row.appendChild(ph);
|
||||
}
|
||||
|
||||
var info = document.createElement('div');
|
||||
info.className = 'item-info';
|
||||
info.innerHTML = '<div class="item-name">' + esc(item.name || '') + '</div>'
|
||||
+ '<div class="item-qty">x' + (item.quantity || 1) + '</div>';
|
||||
row.appendChild(info);
|
||||
|
||||
var price = document.createElement('div');
|
||||
price.className = 'item-price';
|
||||
price.textContent = currency + ((item.price || 0) * (item.quantity || 1)).toFixed(2);
|
||||
row.appendChild(price);
|
||||
|
||||
itemsDiv.appendChild(row);
|
||||
});
|
||||
card.appendChild(itemsDiv);
|
||||
|
||||
// Summary
|
||||
var summary = document.createElement('div');
|
||||
summary.className = 'order-summary';
|
||||
if (order.subtotal !== undefined) {
|
||||
summary.innerHTML += '<div class="summary-row"><span>Subtotal</span><span>' + currency + (order.subtotal || 0).toFixed(2) + '</span></div>';
|
||||
}
|
||||
if (order.tax) {
|
||||
summary.innerHTML += '<div class="summary-row"><span>Tax</span><span>' + currency + order.tax.toFixed(2) + '</span></div>';
|
||||
}
|
||||
if (order.discount) {
|
||||
summary.innerHTML += '<div class="summary-row discount"><span>Discount</span><span>-' + currency + order.discount.toFixed(2) + '</span></div>';
|
||||
}
|
||||
summary.innerHTML += '<div class="summary-row total"><span>Total</span><span>' + currency + (order.total || 0).toFixed(2) + '</span></div>';
|
||||
card.appendChild(summary);
|
||||
|
||||
// Payment
|
||||
var payDiv = document.createElement('div');
|
||||
payDiv.className = 'payment-section';
|
||||
|
||||
if (payment.method === 'qrcode' && payment.qrcode_url) {
|
||||
var qrDiv = document.createElement('div');
|
||||
qrDiv.className = 'payment-qr';
|
||||
qrDiv.innerHTML = '<img src="' + esc(payment.qrcode_url) + '" alt="Payment QR Code">'
|
||||
+ '<div class="payment-qr-hint">Scan to pay</div>';
|
||||
payDiv.appendChild(qrDiv);
|
||||
}
|
||||
|
||||
var btnRow = document.createElement('div');
|
||||
btnRow.className = 'btn-row';
|
||||
|
||||
// Cancel button
|
||||
var cancelBtn = document.createElement('button');
|
||||
cancelBtn.className = 'btn btn-secondary';
|
||||
cancelBtn.textContent = 'Cancel';
|
||||
cancelBtn.addEventListener('click', function () {
|
||||
showOverlay('cancelled');
|
||||
window.parent.postMessage({
|
||||
type: 'mcp-app-response',
|
||||
payload: { action: 'cancel', order_id: order.order_id || '' }
|
||||
}, '*');
|
||||
});
|
||||
btnRow.appendChild(cancelBtn);
|
||||
|
||||
if (payment.method === 'link' && payment.payment_url) {
|
||||
var linkBtn = document.createElement('a');
|
||||
linkBtn.className = 'btn btn-link';
|
||||
linkBtn.href = payment.payment_url;
|
||||
linkBtn.target = '_blank';
|
||||
linkBtn.rel = 'noopener';
|
||||
linkBtn.textContent = payment.button_text || 'Pay Now';
|
||||
linkBtn.addEventListener('click', function () {
|
||||
setTimeout(function () {
|
||||
showOverlay('confirmed');
|
||||
window.parent.postMessage({
|
||||
type: 'mcp-app-response',
|
||||
payload: { action: 'confirm', order_id: order.order_id || '' }
|
||||
}, '*');
|
||||
}, 500);
|
||||
});
|
||||
btnRow.appendChild(linkBtn);
|
||||
} else {
|
||||
var confirmBtn = document.createElement('button');
|
||||
confirmBtn.className = 'btn btn-primary';
|
||||
confirmBtn.textContent = payment.button_text || 'Confirm Payment';
|
||||
confirmBtn.addEventListener('click', function () {
|
||||
showOverlay('confirmed');
|
||||
window.parent.postMessage({
|
||||
type: 'mcp-app-response',
|
||||
payload: { action: 'confirm', order_id: order.order_id || '' }
|
||||
}, '*');
|
||||
});
|
||||
btnRow.appendChild(confirmBtn);
|
||||
}
|
||||
|
||||
payDiv.appendChild(btnRow);
|
||||
card.appendChild(payDiv);
|
||||
}
|
||||
|
||||
function showOverlay(type) {
|
||||
var overlay = document.getElementById('success-overlay');
|
||||
var icon = document.getElementById('success-icon');
|
||||
var text = document.getElementById('success-text');
|
||||
var sub = document.getElementById('success-sub');
|
||||
if (type === 'confirmed') {
|
||||
icon.textContent = '\u2705';
|
||||
text.textContent = 'Payment Confirmed';
|
||||
sub.textContent = 'Your order has been placed successfully!';
|
||||
} else {
|
||||
icon.textContent = '\u274C';
|
||||
text.textContent = 'Order Cancelled';
|
||||
sub.textContent = 'Your order has been cancelled.';
|
||||
}
|
||||
overlay.classList.add('show');
|
||||
}
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
var msg = event.data;
|
||||
if (msg && msg.type === 'mcp-app-data') {
|
||||
document.getElementById('success-overlay').classList.remove('show');
|
||||
render(msg.payload);
|
||||
}
|
||||
});
|
||||
|
||||
window.parent.postMessage({ type: 'mcp-app-ready' }, '*');
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
288
skills/developing/ecommerce-storefront/apps/product-list.html
Normal file
288
skills/developing/ecommerce-storefront/apps/product-list.html
Normal file
@ -0,0 +1,288 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Product List</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f7; padding: 20px; }
|
||||
h1 { font-size: 22px; font-weight: 700; color: #1d1d1f; margin-bottom: 20px; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
|
||||
|
||||
.card { background: #fff; border-radius: 16px; overflow: hidden; border: 1px solid #e5e5ea;
|
||||
transition: box-shadow 0.2s, transform 0.2s; display: flex; flex-direction: column; }
|
||||
.card:hover { box-shadow: 0 8px 24px rgba(0,0,0,0.08); transform: translateY(-2px); }
|
||||
|
||||
.card-img { width: 100%; height: 180px; object-fit: cover; background: #f0f0f0; }
|
||||
.card-img-placeholder { width: 100%; height: 180px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex; align-items: center; justify-content: center; color: #fff; font-size: 40px; }
|
||||
|
||||
.card-body { padding: 16px; flex: 1; display: flex; flex-direction: column; }
|
||||
.card-tags { display: flex; gap: 6px; margin-bottom: 8px; flex-wrap: wrap; }
|
||||
.tag { padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
|
||||
.tag-hot { background: #fee2e2; color: #dc2626; }
|
||||
.tag-new { background: #dbeafe; color: #2563eb; }
|
||||
.tag-sale { background: #fef3c7; color: #d97706; }
|
||||
.tag-default { background: #f1f5f9; color: #475569; }
|
||||
|
||||
.card-name { font-size: 17px; font-weight: 600; color: #1d1d1f; margin-bottom: 4px; }
|
||||
.card-desc { font-size: 13px; color: #86868b; margin-bottom: 12px; line-height: 1.4; }
|
||||
.card-price { font-size: 22px; font-weight: 700; color: #0071e3; margin-bottom: 12px; }
|
||||
|
||||
.spec-group { margin-bottom: 10px; }
|
||||
.spec-label { font-size: 12px; font-weight: 600; color: #6e6e73; margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.spec-options { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.spec-btn { padding: 6px 12px; border: 1.5px solid #d1d1d6; border-radius: 8px; background: #fff;
|
||||
cursor: pointer; font-size: 13px; color: #1d1d1f; transition: all 0.15s; white-space: nowrap; }
|
||||
.spec-btn:hover { border-color: #0071e3; color: #0071e3; }
|
||||
.spec-btn.selected { border-color: #0071e3; background: #0071e3; color: #fff; }
|
||||
.spec-btn .delta { font-size: 11px; color: #86868b; margin-left: 4px; }
|
||||
.spec-btn.selected .delta { color: rgba(255,255,255,0.8); }
|
||||
|
||||
.quantity-row { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
|
||||
.quantity-label { font-size: 12px; font-weight: 600; color: #6e6e73; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.quantity-ctrl { display: flex; align-items: center; gap: 0; }
|
||||
.qty-btn { width: 32px; height: 32px; border: 1.5px solid #d1d1d6; background: #fff; cursor: pointer;
|
||||
font-size: 16px; font-weight: 600; color: #1d1d1f; display: flex; align-items: center; justify-content: center; }
|
||||
.qty-btn:first-child { border-radius: 8px 0 0 8px; }
|
||||
.qty-btn:last-child { border-radius: 0 8px 8px 0; }
|
||||
.qty-btn:hover { background: #f5f5f7; }
|
||||
.qty-val { width: 40px; height: 32px; border-top: 1.5px solid #d1d1d6; border-bottom: 1.5px solid #d1d1d6;
|
||||
display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 600; }
|
||||
|
||||
.spacer { flex: 1; }
|
||||
.add-btn { width: 100%; padding: 12px; border: none; border-radius: 12px; background: #0071e3;
|
||||
color: #fff; font-size: 15px; font-weight: 600; cursor: pointer; transition: background 0.15s; }
|
||||
.add-btn:hover { background: #0077ed; }
|
||||
.add-btn:active { background: #005bb5; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="title"></h1>
|
||||
<div class="grid" id="grid"></div>
|
||||
<script>
|
||||
(function () {
|
||||
function esc(t) { var d = document.createElement('div'); d.textContent = t; return d.innerHTML; }
|
||||
|
||||
function getTagClass(tag) {
|
||||
var t = (tag || '').toLowerCase();
|
||||
if (t === 'hot' || t === 'popular') return 'tag-hot';
|
||||
if (t === 'new') return 'tag-new';
|
||||
if (t === 'sale' || t === 'discount') return 'tag-sale';
|
||||
return 'tag-default';
|
||||
}
|
||||
|
||||
function render(payload) {
|
||||
document.getElementById('title').textContent = payload.title || 'Products';
|
||||
var grid = document.getElementById('grid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
(payload.products || []).forEach(function (p) {
|
||||
var card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
|
||||
// State
|
||||
var state = { specs: {}, quantity: 1 };
|
||||
var currency = p.currency || '$';
|
||||
|
||||
// Initialize default spec selections (first option of each group)
|
||||
(p.specs || []).forEach(function (sg) {
|
||||
if (sg.options && sg.options.length > 0) {
|
||||
state.specs[sg.label] = sg.options[0];
|
||||
}
|
||||
});
|
||||
|
||||
function calcPrice() {
|
||||
var total = p.price || 0;
|
||||
Object.keys(state.specs).forEach(function (k) {
|
||||
total += (state.specs[k].price_delta || 0);
|
||||
});
|
||||
return total;
|
||||
}
|
||||
|
||||
// Image
|
||||
if (p.image) {
|
||||
var img = document.createElement('img');
|
||||
img.className = 'card-img';
|
||||
img.src = p.image;
|
||||
img.alt = p.name || '';
|
||||
img.onerror = function () {
|
||||
var ph = document.createElement('div');
|
||||
ph.className = 'card-img-placeholder';
|
||||
ph.textContent = (p.name || 'P').charAt(0).toUpperCase();
|
||||
card.replaceChild(ph, img);
|
||||
};
|
||||
card.appendChild(img);
|
||||
} else {
|
||||
var ph = document.createElement('div');
|
||||
ph.className = 'card-img-placeholder';
|
||||
ph.textContent = (p.name || 'P').charAt(0).toUpperCase();
|
||||
card.appendChild(ph);
|
||||
}
|
||||
|
||||
var body = document.createElement('div');
|
||||
body.className = 'card-body';
|
||||
|
||||
// Tags
|
||||
if (p.tags && p.tags.length > 0) {
|
||||
var tagsDiv = document.createElement('div');
|
||||
tagsDiv.className = 'card-tags';
|
||||
p.tags.forEach(function (t) {
|
||||
var span = document.createElement('span');
|
||||
span.className = 'tag ' + getTagClass(t);
|
||||
span.textContent = t;
|
||||
tagsDiv.appendChild(span);
|
||||
});
|
||||
body.appendChild(tagsDiv);
|
||||
}
|
||||
|
||||
// Name
|
||||
var name = document.createElement('div');
|
||||
name.className = 'card-name';
|
||||
name.textContent = p.name || '';
|
||||
body.appendChild(name);
|
||||
|
||||
// Description
|
||||
if (p.description) {
|
||||
var desc = document.createElement('div');
|
||||
desc.className = 'card-desc';
|
||||
desc.textContent = p.description;
|
||||
body.appendChild(desc);
|
||||
}
|
||||
|
||||
// Price
|
||||
var priceEl = document.createElement('div');
|
||||
priceEl.className = 'card-price';
|
||||
priceEl.textContent = currency + calcPrice().toFixed(2);
|
||||
body.appendChild(priceEl);
|
||||
|
||||
// Specs
|
||||
(p.specs || []).forEach(function (sg) {
|
||||
var group = document.createElement('div');
|
||||
group.className = 'spec-group';
|
||||
|
||||
var label = document.createElement('div');
|
||||
label.className = 'spec-label';
|
||||
label.textContent = sg.label;
|
||||
group.appendChild(label);
|
||||
|
||||
var optionsDiv = document.createElement('div');
|
||||
optionsDiv.className = 'spec-options';
|
||||
|
||||
(sg.options || []).forEach(function (opt, idx) {
|
||||
var btn = document.createElement('button');
|
||||
btn.className = 'spec-btn' + (idx === 0 ? ' selected' : '');
|
||||
var deltaText = '';
|
||||
if (opt.price_delta && opt.price_delta !== 0) {
|
||||
deltaText = (opt.price_delta > 0 ? '+' : '') + currency + Math.abs(opt.price_delta).toFixed(2);
|
||||
}
|
||||
btn.innerHTML = esc(opt.name) + (deltaText ? ' <span class="delta">' + deltaText + '</span>' : '');
|
||||
|
||||
btn.addEventListener('click', function () {
|
||||
state.specs[sg.label] = opt;
|
||||
optionsDiv.querySelectorAll('.spec-btn').forEach(function (b) { b.classList.remove('selected'); });
|
||||
btn.classList.add('selected');
|
||||
priceEl.textContent = currency + calcPrice().toFixed(2);
|
||||
});
|
||||
|
||||
optionsDiv.appendChild(btn);
|
||||
});
|
||||
|
||||
group.appendChild(optionsDiv);
|
||||
body.appendChild(group);
|
||||
});
|
||||
|
||||
// Quantity
|
||||
var qtyRow = document.createElement('div');
|
||||
qtyRow.className = 'quantity-row';
|
||||
var qtyLabel = document.createElement('div');
|
||||
qtyLabel.className = 'quantity-label';
|
||||
qtyLabel.textContent = 'Quantity';
|
||||
qtyRow.appendChild(qtyLabel);
|
||||
|
||||
var qtyCtrl = document.createElement('div');
|
||||
qtyCtrl.className = 'quantity-ctrl';
|
||||
|
||||
var minusBtn = document.createElement('button');
|
||||
minusBtn.className = 'qty-btn';
|
||||
minusBtn.textContent = '-';
|
||||
|
||||
var qtyVal = document.createElement('div');
|
||||
qtyVal.className = 'qty-val';
|
||||
qtyVal.textContent = '1';
|
||||
|
||||
var plusBtn = document.createElement('button');
|
||||
plusBtn.className = 'qty-btn';
|
||||
plusBtn.textContent = '+';
|
||||
|
||||
minusBtn.addEventListener('click', function () {
|
||||
if (state.quantity > 1) {
|
||||
state.quantity--;
|
||||
qtyVal.textContent = state.quantity;
|
||||
}
|
||||
});
|
||||
plusBtn.addEventListener('click', function () {
|
||||
if (state.quantity < 99) {
|
||||
state.quantity++;
|
||||
qtyVal.textContent = state.quantity;
|
||||
}
|
||||
});
|
||||
|
||||
qtyCtrl.appendChild(minusBtn);
|
||||
qtyCtrl.appendChild(qtyVal);
|
||||
qtyCtrl.appendChild(plusBtn);
|
||||
qtyRow.appendChild(qtyCtrl);
|
||||
body.appendChild(qtyRow);
|
||||
|
||||
// Spacer
|
||||
var spacer = document.createElement('div');
|
||||
spacer.className = 'spacer';
|
||||
body.appendChild(spacer);
|
||||
|
||||
// Add to Cart button
|
||||
var addBtn = document.createElement('button');
|
||||
addBtn.className = 'add-btn';
|
||||
addBtn.textContent = 'Add to Cart';
|
||||
addBtn.addEventListener('click', function () {
|
||||
var selectedSpecs = {};
|
||||
Object.keys(state.specs).forEach(function (k) {
|
||||
selectedSpecs[k] = state.specs[k].name;
|
||||
});
|
||||
window.parent.postMessage({
|
||||
type: 'mcp-app-response',
|
||||
payload: {
|
||||
product_id: p.id,
|
||||
product_name: p.name,
|
||||
selected_specs: selectedSpecs,
|
||||
final_price: calcPrice(),
|
||||
quantity: state.quantity,
|
||||
currency: currency
|
||||
}
|
||||
}, '*');
|
||||
addBtn.textContent = 'Added!';
|
||||
addBtn.style.background = '#34c759';
|
||||
setTimeout(function () {
|
||||
addBtn.textContent = 'Add to Cart';
|
||||
addBtn.style.background = '#0071e3';
|
||||
}, 1500);
|
||||
});
|
||||
body.appendChild(addBtn);
|
||||
|
||||
card.appendChild(body);
|
||||
grid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
var msg = event.data;
|
||||
if (msg && msg.type === 'mcp-app-data') {
|
||||
render(msg.payload);
|
||||
}
|
||||
});
|
||||
|
||||
window.parent.postMessage({ type: 'mcp-app-ready' }, '*');
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
213
skills/developing/ecommerce-storefront/ecommerce_server.py
Normal file
213
skills/developing/ecommerce-storefront/ecommerce_server.py
Normal file
@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
E-Commerce Storefront MCP Server - standard MCP Apps protocol (SEP-1865).
|
||||
|
||||
- tools/call returns structured data only (no HTML)
|
||||
- resources/read returns static HTML App files
|
||||
- Host renders HTML App in iframe, passes tool data via postMessage
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from typing import Any, Dict
|
||||
|
||||
from mcp_common import (
|
||||
create_error_response,
|
||||
create_ping_response,
|
||||
create_tools_list_response,
|
||||
load_tools_from_json,
|
||||
handle_mcp_streaming,
|
||||
)
|
||||
|
||||
RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"
|
||||
APPS_DIR = os.path.join(os.path.dirname(__file__), "apps")
|
||||
|
||||
# Resource URI -> static HTML App file mapping
|
||||
RESOURCE_MAP = {
|
||||
"ui://ecommerce-storefront/product-list": "product-list.html",
|
||||
"ui://ecommerce-storefront/order-confirm": "order-confirm.html",
|
||||
}
|
||||
|
||||
RESOURCE_DEFINITIONS = [
|
||||
{
|
||||
"uri": "ui://ecommerce-storefront/product-list",
|
||||
"name": "product-list",
|
||||
"title": "Product List",
|
||||
"description": "Interactive product cards with spec selection",
|
||||
"mimeType": RESOURCE_MIME_TYPE,
|
||||
},
|
||||
{
|
||||
"uri": "ui://ecommerce-storefront/order-confirm",
|
||||
"name": "order-confirm",
|
||||
"title": "Order Confirmation",
|
||||
"description": "Order summary with payment options",
|
||||
"mimeType": RESOURCE_MIME_TYPE,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _load_app_html(uri: str) -> str:
|
||||
"""Load static HTML App file for the given resource URI."""
|
||||
filename = RESOURCE_MAP.get(uri)
|
||||
if not filename:
|
||||
raise ValueError(f"Unknown resource URI: {uri}")
|
||||
filepath = os.path.join(APPS_DIR, filename)
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def _create_app_response(resource_uri: str, data: Dict[str, Any],
|
||||
width: str = "100%", height: str = "auto") -> Dict[str, Any]:
|
||||
"""Create a tool result for MCP Apps protocol."""
|
||||
app_json = json.dumps({
|
||||
"type": "app",
|
||||
"resourceUri": resource_uri,
|
||||
"data": data,
|
||||
"_meta": {
|
||||
"mcpui.dev/ui-preferred-frame-size": [width, height],
|
||||
},
|
||||
}, ensure_ascii=False)
|
||||
return {"content": [{"type": "text", "text": app_json}]}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool handlers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _handle_render_product_list(arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||
title = arguments.get("title", "Products")
|
||||
products = arguments.get("products", [])
|
||||
if not products:
|
||||
raise ValueError("Missing required parameter: products")
|
||||
return _create_app_response(
|
||||
"ui://ecommerce-storefront/product-list",
|
||||
{"title": title, "products": products},
|
||||
"100%", "auto",
|
||||
)
|
||||
|
||||
|
||||
def _handle_render_order_confirm(arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||
title = arguments.get("title", "Order Confirmation")
|
||||
order = arguments.get("order")
|
||||
payment = arguments.get("payment")
|
||||
if not order:
|
||||
raise ValueError("Missing required parameter: order")
|
||||
if not payment:
|
||||
raise ValueError("Missing required parameter: payment")
|
||||
if not order.get("items"):
|
||||
raise ValueError("Missing required parameter: order.items")
|
||||
return _create_app_response(
|
||||
"ui://ecommerce-storefront/order-confirm",
|
||||
{"title": title, "order": order, "payment": payment},
|
||||
"100%", "auto",
|
||||
)
|
||||
|
||||
|
||||
TOOL_HANDLERS = {
|
||||
"render_product_list": _handle_render_product_list,
|
||||
"render_order_confirm": _handle_render_order_confirm,
|
||||
}
|
||||
|
||||
|
||||
async def handle_request(request: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Handle an MCP request."""
|
||||
try:
|
||||
method = request.get("method")
|
||||
params = request.get("params", {})
|
||||
request_id = request.get("id")
|
||||
|
||||
if method == "initialize":
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {
|
||||
"tools": {},
|
||||
"resources": {},
|
||||
},
|
||||
"serverInfo": {
|
||||
"name": "ecommerce-storefront",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
elif method == "ping":
|
||||
return create_ping_response(request_id)
|
||||
|
||||
elif method == "tools/list":
|
||||
tools = load_tools_from_json("ecommerce_tools.json")
|
||||
if not tools:
|
||||
tools = [
|
||||
{
|
||||
"name": name,
|
||||
"description": f"Render {name.replace('render_', '')}",
|
||||
"inputSchema": {"type": "object", "properties": {}, "required": []},
|
||||
"_meta": {"ui": {"resourceUri": uri}},
|
||||
}
|
||||
for name, uri in [
|
||||
("render_product_list", "ui://ecommerce-storefront/product-list"),
|
||||
("render_order_confirm", "ui://ecommerce-storefront/order-confirm"),
|
||||
]
|
||||
]
|
||||
return create_tools_list_response(request_id, tools)
|
||||
|
||||
elif method == "resources/list":
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {"resources": RESOURCE_DEFINITIONS},
|
||||
}
|
||||
|
||||
elif method == "resources/read":
|
||||
uri = params.get("uri", "")
|
||||
try:
|
||||
html = _load_app_html(uri)
|
||||
except (ValueError, FileNotFoundError) as e:
|
||||
return create_error_response(request_id, -32602, str(e))
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {
|
||||
"contents": [
|
||||
{
|
||||
"uri": uri,
|
||||
"mimeType": RESOURCE_MIME_TYPE,
|
||||
"text": html,
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
elif method == "tools/call":
|
||||
tool_name = params.get("name")
|
||||
arguments = params.get("arguments", {})
|
||||
|
||||
handler = TOOL_HANDLERS.get(tool_name)
|
||||
if not handler:
|
||||
return create_error_response(request_id, -32601, f"Unknown tool: {tool_name}")
|
||||
|
||||
try:
|
||||
result = handler(arguments)
|
||||
except ValueError as e:
|
||||
return create_error_response(request_id, -32602, str(e))
|
||||
|
||||
return {"jsonrpc": "2.0", "id": request_id, "result": result}
|
||||
|
||||
else:
|
||||
return create_error_response(request_id, -32601, f"Unknown method: {method}")
|
||||
|
||||
except Exception as e:
|
||||
return create_error_response(
|
||||
request.get("id"), -32603, f"Internal error: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
await handle_mcp_streaming(handle_request)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
125
skills/developing/ecommerce-storefront/ecommerce_tools.json
Normal file
125
skills/developing/ecommerce-storefront/ecommerce_tools.json
Normal file
@ -0,0 +1,125 @@
|
||||
[
|
||||
{
|
||||
"name": "render_product_list",
|
||||
"description": "Render an interactive product card list. Users can browse products, select specifications (size/flavor/etc.), and add items to cart. Returns the user's selection via mcp-app-response.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Title displayed at the top of the product list"
|
||||
},
|
||||
"products": {
|
||||
"type": "array",
|
||||
"description": "Array of product objects to display as cards",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string", "description": "Unique product identifier" },
|
||||
"name": { "type": "string", "description": "Product name" },
|
||||
"description": { "type": "string", "description": "Short product description" },
|
||||
"image": { "type": "string", "description": "Product image URL" },
|
||||
"price": { "type": "number", "description": "Base price" },
|
||||
"currency": { "type": "string", "description": "Currency symbol, default: '$'", "default": "$" },
|
||||
"specs": {
|
||||
"type": "array",
|
||||
"description": "Available specification groups (e.g. size, flavor)",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": { "type": "string", "description": "Spec group label, e.g. 'Size'" },
|
||||
"options": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string", "description": "Option name, e.g. 'Large'" },
|
||||
"price_delta": { "type": "number", "description": "Price adjustment, default 0", "default": 0 }
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["label", "options"]
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Optional tags like 'Hot', 'New', 'Sale'"
|
||||
}
|
||||
},
|
||||
"required": ["id", "name", "price"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["title", "products"]
|
||||
},
|
||||
"_meta": {
|
||||
"ui": {
|
||||
"resourceUri": "ui://ecommerce-storefront/product-list"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "render_order_confirm",
|
||||
"description": "Render an order confirmation page with order details, total price, and a payment action button or QR code. Returns 'confirmed' or 'cancelled' via mcp-app-response.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Page title, e.g. 'Order Confirmation'"
|
||||
},
|
||||
"order": {
|
||||
"type": "object",
|
||||
"description": "Order details",
|
||||
"properties": {
|
||||
"order_id": { "type": "string", "description": "Order ID" },
|
||||
"items": {
|
||||
"type": "array",
|
||||
"description": "Ordered items",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string", "description": "Product name with specs" },
|
||||
"quantity": { "type": "integer", "description": "Quantity", "default": 1 },
|
||||
"price": { "type": "number", "description": "Unit price" },
|
||||
"image": { "type": "string", "description": "Product image URL" }
|
||||
},
|
||||
"required": ["name", "price"]
|
||||
}
|
||||
},
|
||||
"subtotal": { "type": "number", "description": "Subtotal before tax/fees" },
|
||||
"tax": { "type": "number", "description": "Tax amount", "default": 0 },
|
||||
"discount": { "type": "number", "description": "Discount amount", "default": 0 },
|
||||
"total": { "type": "number", "description": "Final total" },
|
||||
"currency": { "type": "string", "description": "Currency symbol", "default": "$" }
|
||||
},
|
||||
"required": ["order_id", "items", "total"]
|
||||
},
|
||||
"payment": {
|
||||
"type": "object",
|
||||
"description": "Payment configuration",
|
||||
"properties": {
|
||||
"method": {
|
||||
"type": "string",
|
||||
"enum": ["button", "qrcode", "link"],
|
||||
"description": "Payment UI type: button (confirm button), qrcode (QR code image), link (external payment URL)"
|
||||
},
|
||||
"qrcode_url": { "type": "string", "description": "QR code image URL (when method=qrcode)" },
|
||||
"payment_url": { "type": "string", "description": "External payment URL (when method=link)" },
|
||||
"button_text": { "type": "string", "description": "Payment button text, default: 'Confirm Payment'", "default": "Confirm Payment" }
|
||||
},
|
||||
"required": ["method"]
|
||||
}
|
||||
},
|
||||
"required": ["title", "order", "payment"]
|
||||
},
|
||||
"_meta": {
|
||||
"ui": {
|
||||
"resourceUri": "ui://ecommerce-storefront/order-confirm"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
102
skills/developing/ecommerce-storefront/hooks/ecommerce_guide.md
Normal file
102
skills/developing/ecommerce-storefront/hooks/ecommerce_guide.md
Normal file
@ -0,0 +1,102 @@
|
||||
## E-Commerce Storefront Tools Usage Guide
|
||||
|
||||
Two tools are available for e-commerce product browsing and ordering:
|
||||
- `render_product_list` — Display interactive product cards with spec selection
|
||||
- `render_order_confirm` — Display order confirmation with payment options
|
||||
|
||||
---
|
||||
|
||||
### 1. render_product_list
|
||||
|
||||
When to use: User wants to browse, search, or buy products. Show product cards with images, prices, and selectable specs (size, flavor, color, etc.).
|
||||
|
||||
The user can select specs and click "Add to Cart" on any product. The selection is returned as an mcp-app-response containing: `{ product_id, product_name, selected_specs: {...}, final_price, quantity }`.
|
||||
|
||||
```
|
||||
render_product_list(
|
||||
title="Coffee Menu",
|
||||
products=[
|
||||
{
|
||||
"id": "latte-001",
|
||||
"name": "Caffe Latte",
|
||||
"description": "Rich espresso with steamed milk",
|
||||
"image": "https://images.unsplash.com/photo-1572442388796-11668a67e53d?w=300&h=200&fit=crop",
|
||||
"price": 4.50,
|
||||
"currency": "$",
|
||||
"specs": [
|
||||
{
|
||||
"label": "Size",
|
||||
"options": [
|
||||
{"name": "Small", "price_delta": 0},
|
||||
{"name": "Medium", "price_delta": 0.5},
|
||||
{"name": "Large", "price_delta": 1.0}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Milk",
|
||||
"options": [
|
||||
{"name": "Whole Milk", "price_delta": 0},
|
||||
{"name": "Oat Milk", "price_delta": 0.6},
|
||||
{"name": "Almond Milk", "price_delta": 0.6}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tags": ["Hot", "Popular"]
|
||||
}
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
After receiving the user's selection, proceed to generate an order and call `render_order_confirm`.
|
||||
|
||||
---
|
||||
|
||||
### 2. render_order_confirm
|
||||
|
||||
When to use: After the user selects a product and you have generated an order.
|
||||
|
||||
Payment method options:
|
||||
- `"button"` — Simple confirm/cancel buttons (default)
|
||||
- `"qrcode"` — Show a QR code image for mobile payment
|
||||
- `"link"` — Provide an external payment URL
|
||||
|
||||
```
|
||||
render_order_confirm(
|
||||
title="Order Confirmation",
|
||||
order={
|
||||
"order_id": "ORD-20260522-001",
|
||||
"items": [
|
||||
{"name": "Caffe Latte (Large, Oat Milk)", "quantity": 1, "price": 6.10}
|
||||
],
|
||||
"subtotal": 6.10,
|
||||
"tax": 0.49,
|
||||
"total": 6.59,
|
||||
"currency": "$"
|
||||
},
|
||||
payment={
|
||||
"method": "button",
|
||||
"button_text": "Confirm Payment"
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The user response will be: `{ action: "confirm", order_id }` or `{ action: "cancel", order_id }`.
|
||||
|
||||
---
|
||||
|
||||
### Workflow
|
||||
|
||||
The typical e-commerce flow is:
|
||||
1. User asks to buy something → call `render_product_list`
|
||||
2. User selects a product → receive mcp-app-response with selection details
|
||||
3. Generate order from selection → call `render_order_confirm`
|
||||
4. User confirms or cancels → receive mcp-app-response with action
|
||||
5. If confirmed → process payment / show success message
|
||||
6. If cancelled → ask user what they'd like to do next
|
||||
|
||||
### Tips
|
||||
- Always include product images when available for better visual experience
|
||||
- Use tags like "Hot", "New", "Sale" to highlight special products
|
||||
- Keep product descriptions short (under 50 characters)
|
||||
- Generate a unique order_id for each order
|
||||
- Include tax/discount breakdowns when applicable for transparency
|
||||
@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PrePrompt hook - inject ecommerce tool guide into system prompt."""
|
||||
import os
|
||||
|
||||
guide_path = os.path.join(os.path.dirname(__file__), "ecommerce_guide.md")
|
||||
if os.path.exists(guide_path):
|
||||
with open(guide_path, "r", encoding="utf-8") as f:
|
||||
print(f.read())
|
||||
252
skills/developing/ecommerce-storefront/mcp_common.py
Normal file
252
skills/developing/ecommerce-storefront/mcp_common.py
Normal file
@ -0,0 +1,252 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Shared utility functions for the MCP server.
|
||||
Provides common functionality for path handling, file validation, and request processing.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
import re
|
||||
|
||||
def get_allowed_directory():
|
||||
"""Get the directory that is allowed to be accessed."""
|
||||
# Prefer dataset_dir passed through command-line arguments.
|
||||
if len(sys.argv) > 1:
|
||||
dataset_dir = sys.argv[1]
|
||||
return os.path.abspath(dataset_dir)
|
||||
|
||||
# Read the project data directory from the environment variable.
|
||||
project_dir = os.getenv("PROJECT_DATA_DIR", "./projects/data")
|
||||
return os.path.abspath(project_dir)
|
||||
|
||||
|
||||
def resolve_file_path(file_path: str, default_subfolder: str = "default") -> str:
|
||||
"""
|
||||
Resolve a file path, supporting both folder/document.txt and document.txt formats.
|
||||
|
||||
Args:
|
||||
file_path: Input file path.
|
||||
default_subfolder: Default subfolder name to use when only a filename is provided.
|
||||
|
||||
Returns:
|
||||
The resolved full file path.
|
||||
"""
|
||||
# If the path contains a folder separator, use it directly.
|
||||
if '/' in file_path or '\\' in file_path:
|
||||
clean_path = file_path.replace('\\', '/')
|
||||
|
||||
# Remove the projects/ prefix if it exists.
|
||||
if clean_path.startswith('projects/'):
|
||||
clean_path = clean_path[9:] # Remove the 'projects/' prefix.
|
||||
elif clean_path.startswith('./projects/'):
|
||||
clean_path = clean_path[11:] # Remove the './projects/' prefix.
|
||||
else:
|
||||
# If only a filename is provided, add the default subfolder.
|
||||
clean_path = f"{default_subfolder}/{file_path}"
|
||||
|
||||
# Get the allowed directory.
|
||||
project_data_dir = get_allowed_directory()
|
||||
|
||||
# Try to locate the file directly under the project directory.
|
||||
full_path = os.path.join(project_data_dir, clean_path.lstrip('./'))
|
||||
if os.path.exists(full_path):
|
||||
return full_path
|
||||
|
||||
# If the direct path does not exist, try a recursive search.
|
||||
found = find_file_in_project(clean_path, project_data_dir)
|
||||
if found:
|
||||
return found
|
||||
|
||||
# If this is a bare filename and it was not found under the default subfolder,
|
||||
# try looking in the project root.
|
||||
if '/' not in file_path and '\\' not in file_path:
|
||||
root_path = os.path.join(project_data_dir, file_path)
|
||||
if os.path.exists(root_path):
|
||||
return root_path
|
||||
|
||||
raise FileNotFoundError(f"File not found: {file_path} (searched in {project_data_dir})")
|
||||
|
||||
|
||||
def find_file_in_project(filename: str, project_dir: str) -> Optional[str]:
|
||||
"""Recursively search for a file inside the project directory."""
|
||||
# If filename includes a path, only search within the specified path.
|
||||
if '/' in filename:
|
||||
parts = filename.split('/')
|
||||
target_file = parts[-1]
|
||||
search_dir = os.path.join(project_dir, *parts[:-1])
|
||||
|
||||
if os.path.exists(search_dir):
|
||||
target_path = os.path.join(search_dir, target_file)
|
||||
if os.path.exists(target_path):
|
||||
return target_path
|
||||
else:
|
||||
# For a bare filename, recursively search the whole project directory.
|
||||
for root, dirs, files in os.walk(project_dir):
|
||||
if filename in files:
|
||||
return os.path.join(root, filename)
|
||||
return None
|
||||
|
||||
|
||||
def load_tools_from_json(tools_file_name: str) -> List[Dict[str, Any]]:
|
||||
"""Load tool definitions from a JSON file."""
|
||||
try:
|
||||
tools_file = os.path.join(os.path.dirname(__file__), tools_file_name)
|
||||
if os.path.exists(tools_file):
|
||||
with open(tools_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
else:
|
||||
# If the JSON file does not exist, use the default definitions.
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"Warning: Unable to load tool definition JSON file: {str(e)}")
|
||||
return []
|
||||
|
||||
|
||||
def create_error_response(request_id: Any, code: int, message: str) -> Dict[str, Any]:
|
||||
"""Create a standardized error response."""
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def create_success_response(request_id: Any, result: Any) -> Dict[str, Any]:
|
||||
"""Create a standardized success response."""
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": result
|
||||
}
|
||||
|
||||
|
||||
def create_initialize_response(request_id: Any, server_name: str, server_version: str = "1.0.0") -> Dict[str, Any]:
|
||||
"""Create a standardized initialize response."""
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {
|
||||
"tools": {}
|
||||
},
|
||||
"serverInfo": {
|
||||
"name": server_name,
|
||||
"version": server_version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def create_ping_response(request_id: Any) -> Dict[str, Any]:
|
||||
"""Create a standardized ping response."""
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {
|
||||
"pong": True
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def create_tools_list_response(request_id: Any, tools: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""Create a standardized tools/list response."""
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {
|
||||
"tools": tools
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def is_regex_pattern(pattern: str) -> bool:
|
||||
"""Check whether a string should be treated as a regular expression pattern."""
|
||||
# Check the /pattern/ format.
|
||||
if pattern.startswith('/') and pattern.endswith('/') and len(pattern) > 2:
|
||||
return True
|
||||
|
||||
# Check the r"pattern" or r'pattern' format.
|
||||
if pattern.startswith(('r"', "r'")) and pattern.endswith(('"', "'")) and len(pattern) > 3:
|
||||
return True
|
||||
|
||||
# Check whether it contains regex metacharacters.
|
||||
regex_chars = {'*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '^', '$', '\\', '.'}
|
||||
return any(char in pattern for char in regex_chars)
|
||||
|
||||
|
||||
def compile_pattern(pattern: str) -> Union[re.Pattern, str, None]:
|
||||
"""Compile a regex pattern, or return the original string if it is not regex."""
|
||||
if not is_regex_pattern(pattern):
|
||||
return pattern
|
||||
|
||||
try:
|
||||
# Handle the /pattern/ format.
|
||||
if pattern.startswith('/') and pattern.endswith('/'):
|
||||
regex_body = pattern[1:-1]
|
||||
return re.compile(regex_body)
|
||||
|
||||
# Handle the r"pattern" or r'pattern' format.
|
||||
if pattern.startswith(('r"', "r'")) and pattern.endswith(('"', "'")):
|
||||
regex_body = pattern[2:-1]
|
||||
return re.compile(regex_body)
|
||||
|
||||
# Directly compile strings that contain regex metacharacters.
|
||||
return re.compile(pattern)
|
||||
except re.error as e:
|
||||
# If compilation fails, return None to indicate an invalid regex.
|
||||
print(f"Warning: Regular expression '{pattern}' compilation failed: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def handle_mcp_streaming(request_handler):
|
||||
"""Handle the standard main loop for MCP requests."""
|
||||
try:
|
||||
while True:
|
||||
# Read from stdin
|
||||
line = await asyncio.get_event_loop().run_in_executor(None, sys.stdin.readline)
|
||||
if not line:
|
||||
break
|
||||
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
try:
|
||||
request = json.loads(line)
|
||||
response = await request_handler(request)
|
||||
|
||||
# Write to stdout
|
||||
sys.stdout.write(json.dumps(response, ensure_ascii=False) + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
except json.JSONDecodeError:
|
||||
error_response = {
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32700,
|
||||
"message": "Parse error"
|
||||
}
|
||||
}
|
||||
sys.stdout.write(json.dumps(error_response, ensure_ascii=False) + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
except Exception as e:
|
||||
error_response = {
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32603,
|
||||
"message": f"Internal error: {str(e)}"
|
||||
}
|
||||
}
|
||||
sys.stdout.write(json.dumps(error_response, ensure_ascii=False) + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
Loading…
Reference in New Issue
Block a user