<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dynamic Form</title>
<style>
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--text-primary: #e6edf3;
--text-secondary: #8b949e;
--text-muted: #6e7681;
--border-color: #30363d;
--accent-blue: #58a6ff;
--accent-green: #3fb950;
--accent-red: #f85149;
--accent-orange: #d29922;
--input-bg: #0d1117;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
padding: 20px;
line-height: 1.5;
}
.form-container {
max-width: 600px;
margin: 0 auto;
}
.form-header {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-color);
}
.form-title {
font-size: 24px;
font-weight: 600;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 10px;
}
.form-description {
color: var(--text-secondary);
font-size: 14px;
}
.form-section {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
}
.section-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.form-group {
margin-bottom: 20px;
}
.form-group:last-child {
margin-bottom: 0;
}
label {
display: block;
font-size: 14px;
font-weight: 500;
margin-bottom: 6px;
color: var(--text-primary);
}
label .required {
color: var(--accent-red);
margin-left: 2px;
}
.label-hint {
font-weight: 400;
color: var(--text-muted);
font-size: 12px;
margin-left: 8px;
}
input[type="text"],
input[type="email"],
input[type="number"],
input[type="tel"],
input[type="date"],
input[type="url"],
textarea,
select {
width: 100%;
padding: 10px 14px;
font-size: 14px;
background: var(--input-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--text-primary);
transition: border-color 0.2s, box-shadow 0.2s;
}
input:focus,
textarea:focus,
select:focus {
outline: none;
border-color: var(--accent-blue);
box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.15);
}
input::placeholder,
textarea::placeholder {
color: var(--text-muted);
}
input.error,
textarea.error,
select.error {
border-color: var(--accent-red);
}
.error-message {
color: var(--accent-red);
font-size: 12px;
margin-top: 6px;
display: none;
}
.error-message.visible {
display: block;
}
textarea {
min-height: 100px;
resize: vertical;
}
select {
cursor: pointer;
}
.radio-group,
.checkbox-group {
display: flex;
flex-direction: column;
gap: 10px;
}
.radio-option,
.checkbox-option {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
}
input[type="radio"],
input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: var(--accent-blue);
cursor: pointer;
}
.radio-option span,
.checkbox-option span {
font-size: 14px;
color: var(--text-secondary);
}
.slider-container {
display: flex;
align-items: center;
gap: 16px;
}
input[type="range"] {
flex: 1;
height: 6px;
-webkit-appearance: none;
background: var(--bg-tertiary);
border-radius: 3px;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
background: var(--accent-blue);
border-radius: 50%;
cursor: pointer;
}
.slider-value {
min-width: 50px;
text-align: center;
font-weight: 600;
color: var(--accent-blue);
}
.file-upload {
border: 2px dashed var(--border-color);
border-radius: 8px;
padding: 24px;
text-align: center;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
}
.file-upload:hover {
border-color: var(--accent-blue);
background: rgba(88, 166, 255, 0.05);
}
.file-upload-icon {
font-size: 32px;
margin-bottom: 8px;
}
.file-upload-text {
color: var(--text-secondary);
font-size: 14px;
}
.file-upload-hint {
color: var(--text-muted);
font-size: 12px;
margin-top: 4px;
}
.form-actions {
display: flex;
gap: 12px;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid var(--border-color);
}
.btn {
padding: 12px 24px;
font-size: 14px;
font-weight: 500;
border: 1px solid var(--border-color);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.btn-secondary:hover {
background: var(--bg-secondary);
border-color: var(--text-muted);
}
.btn-primary {
background: var(--accent-blue);
border-color: var(--accent-blue);
color: #fff;
flex: 1;
justify-content: center;
}
.btn-primary:hover {
background: #4c9aed;
}
.btn-primary:disabled {
background: var(--bg-tertiary);
border-color: var(--border-color);
color: var(--text-muted);
cursor: not-allowed;
}
.progress-bar {
height: 4px;
background: var(--bg-tertiary);
border-radius: 2px;
margin-bottom: 24px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--accent-blue);
border-radius: 2px;
transition: width 0.3s ease;
}
.progress-text {
font-size: 12px;
color: var(--text-muted);
margin-bottom: 8px;
}
.success-message {
background: rgba(63, 185, 80, 0.1);
border: 1px solid var(--accent-green);
border-radius: 8px;
padding: 16px;
display: none;
align-items: center;
gap: 12px;
}
.success-message.visible {
display: flex;
}
.success-icon {
font-size: 24px;
}
.success-text {
color: var(--accent-green);
font-weight: 500;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 60px;
color: var(--text-secondary);
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 24px;
height: 24px;
border: 2px solid var(--border-color);
border-top-color: var(--accent-blue);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-right: 12px;
}
/* Tags input */
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 8px;
background: var(--input-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
min-height: 44px;
}
.tag {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 4px 8px;
font-size: 13px;
}
.tag-remove {
cursor: pointer;
color: var(--text-muted);
font-size: 16px;
line-height: 1;
}
.tag-remove:hover {
color: var(--accent-red);
}
.tags-input {
flex: 1;
min-width: 100px;
border: none;
background: transparent;
color: var(--text-primary);
font-size: 14px;
padding: 4px;
}
.tags-input:focus {
outline: none;
}
</style>
</head>
<body>
<div class="form-container">
<div id="loading" class="loading">
<div class="spinner"></div>
Loading form...
</div>
<div id="form-content" style="display: none;">
<div class="form-header">
<h1 class="form-title">
<span id="form-icon">📝</span>
<span id="form-title-text">Dynamic Form</span>
</h1>
<p class="form-description" id="form-description">Fill out the form below.</p>
</div>
<div class="progress-text"><span id="progress-count">0</span> of <span id="total-required">0</span> required fields completed</div>
<div class="progress-bar">
<div class="progress-fill" id="progress-fill" style="width: 0%"></div>
</div>
<form id="dynamic-form">
<!-- Form sections will be generated here -->
</form>
<div class="success-message" id="success-message">
<span class="success-icon">✅</span>
<span class="success-text">Form submitted successfully!</span>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" id="clear-btn">
🗑️ Clear
</button>
<button type="submit" class="btn btn-primary" id="submit-btn" form="dynamic-form">
Submit Form
</button>
</div>
</div>
</div>
<script type="module">
import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps";
// State
let formConfig = null;
let formData = {};
// Sample form to show immediately
const sampleFormConfig = {
icon: "📋",
title: "Feedback Form",
description: "Share your thoughts with us",
sections: [
{
title: "About You",
icon: "👤",
fields: [
{ id: "name", type: "text", label: "Your Name", placeholder: "John Doe", required: true },
{ id: "email", type: "email", label: "Email Address", placeholder: "john@example.com", required: true },
{ id: "role", type: "select", label: "Your Role", required: true, options: [
{ value: "developer", label: "Developer" },
{ value: "designer", label: "Designer" },
{ value: "manager", label: "Manager" },
{ value: "other", label: "Other" }
]}
]
},
{
title: "Your Feedback",
icon: "💬",
fields: [
{ id: "rating", type: "range", label: "Overall Rating (1-10)", min: 1, max: 10, default: 7 },
{ id: "features", type: "checkbox", label: "What features do you use?", options: [
{ value: "charts", label: "Data Visualization" },
{ value: "forms", label: "Form Builder" },
{ value: "monitor", label: "System Monitor" },
{ value: "playground", label: "Code Playground" }
]},
{ id: "feedback", type: "textarea", label: "Additional Comments", placeholder: "Tell us what you think...", rows: 4 }
]
}
]
};
// Initialize MCP App
const app = new App({
appInfo: { name: "Dynamic Form", version: "1.0.0" },
appCapabilities: {}
});
app.ontoolinput = (input) => {
console.log("[Form] Tool input received:", input);
};
app.ontoolresult = (result) => {
console.log("[Form] Tool result received:", result);
try {
const content = result.content?.[0];
if (content?.type === 'text') {
formConfig = JSON.parse(content.text);
renderForm();
}
} catch (e) {
console.error("[Form] Failed to parse result:", e);
}
};
// Connect to host
await app.connect(new PostMessageTransport(window.parent));
console.log("[Form] Connected to host");
// Render sample form immediately so users see something
formConfig = sampleFormConfig;
renderForm();
// Render the form based on config
function renderForm() {
if (!formConfig) return;
document.getElementById('loading').style.display = 'none';
document.getElementById('form-content').style.display = 'block';
// Set header
if (formConfig.icon) document.getElementById('form-icon').textContent = formConfig.icon;
if (formConfig.title) document.getElementById('form-title-text').textContent = formConfig.title;
if (formConfig.description) document.getElementById('form-description').textContent = formConfig.description;
// Generate form fields
const form = document.getElementById('dynamic-form');
form.innerHTML = '';
const sections = formConfig.sections || [{ fields: formConfig.fields || [] }];
sections.forEach((section, sectionIndex) => {
const sectionEl = document.createElement('div');
sectionEl.className = 'form-section';
if (section.title) {
sectionEl.innerHTML = `
<div class="section-title">
${section.icon ? `<span>${section.icon}</span>` : ''}
${section.title}
</div>
`;
}
section.fields.forEach(field => {
const fieldEl = createField(field);
sectionEl.appendChild(fieldEl);
});
form.appendChild(sectionEl);
});
updateProgress();
notifySize();
}
function createField(field) {
const group = document.createElement('div');
group.className = 'form-group';
group.dataset.fieldId = field.id;
const required = field.required ? '<span class="required">*</span>' : '';
const hint = field.hint ? `<span class="label-hint">${field.hint}</span>` : '';
let inputHtml = '';
switch (field.type) {
case 'text':
case 'email':
case 'tel':
case 'url':
case 'number':
case 'date':
inputHtml = `<input type="${field.type}" id="${field.id}" name="${field.id}"
placeholder="${field.placeholder || ''}" ${field.required ? 'required' : ''}
${field.min !== undefined ? `min="${field.min}"` : ''}
${field.max !== undefined ? `max="${field.max}"` : ''}>`;
break;
case 'textarea':
inputHtml = `<textarea id="${field.id}" name="${field.id}"
placeholder="${field.placeholder || ''}" ${field.required ? 'required' : ''}
rows="${field.rows || 4}"></textarea>`;
break;
case 'select':
const options = (field.options || []).map(opt =>
`<option value="${opt.value}">${opt.label}</option>`
).join('');
inputHtml = `<select id="${field.id}" name="${field.id}" ${field.required ? 'required' : ''}>
<option value="">${field.placeholder || 'Select an option...'}</option>
${options}
</select>`;
break;
case 'radio':
const radios = (field.options || []).map(opt => `
<label class="radio-option">
<input type="radio" name="${field.id}" value="${opt.value}" ${field.required ? 'required' : ''}>
<span>${opt.label}</span>
</label>
`).join('');
inputHtml = `<div class="radio-group">${radios}</div>`;
break;
case 'checkbox':
if (field.options) {
const checkboxes = field.options.map(opt => `
<label class="checkbox-option">
<input type="checkbox" name="${field.id}" value="${opt.value}">
<span>${opt.label}</span>
</label>
`).join('');
inputHtml = `<div class="checkbox-group">${checkboxes}</div>`;
} else {
inputHtml = `
<label class="checkbox-option">
<input type="checkbox" id="${field.id}" name="${field.id}">
<span>${field.checkboxLabel || 'Yes'}</span>
</label>
`;
}
break;
case 'range':
inputHtml = `
<div class="slider-container">
<input type="range" id="${field.id}" name="${field.id}"
min="${field.min || 0}" max="${field.max || 100}" value="${field.default || 50}">
<span class="slider-value" id="${field.id}-value">${field.default || 50}</span>
</div>
`;
break;
case 'tags':
inputHtml = `
<div class="tags-container" id="${field.id}-container">
<input type="text" class="tags-input" id="${field.id}" placeholder="${field.placeholder || 'Type and press Enter'}">
</div>
`;
break;
default:
inputHtml = `<input type="text" id="${field.id}" name="${field.id}">`;
}
group.innerHTML = `
<label for="${field.id}">${field.label}${required}${hint}</label>
${inputHtml}
<div class="error-message" id="${field.id}-error">${field.errorMessage || 'This field is required'}</div>
`;
return group;
}
// Event handlers
document.getElementById('dynamic-form').addEventListener('input', (e) => {
const target = e.target;
const name = target.name || target.id;
if (target.type === 'range') {
document.getElementById(`${name}-value`).textContent = target.value;
}
updateProgress();
clearError(name);
});
document.getElementById('dynamic-form').addEventListener('change', (e) => {
updateProgress();
});
// Tags input handling
document.addEventListener('keydown', (e) => {
if (e.target.classList.contains('tags-input') && e.key === 'Enter') {
e.preventDefault();
const input = e.target;
const container = input.parentElement;
const value = input.value.trim();
if (value) {
const tag = document.createElement('span');
tag.className = 'tag';
tag.innerHTML = `${value}<span class="tag-remove">×</span>`;
tag.dataset.value = value;
container.insertBefore(tag, input);
input.value = '';
tag.querySelector('.tag-remove').addEventListener('click', () => {
tag.remove();
updateProgress();
});
updateProgress();
}
}
});
document.getElementById('dynamic-form').addEventListener('submit', async (e) => {
e.preventDefault();
if (!validateForm()) return;
const data = collectFormData();
console.log("[Form] Submitting data:", data);
// Send to chat
app.sendMessage('user', [
{ type: 'text', text: formatSubmission(data) }
]);
// Show success
document.getElementById('success-message').classList.add('visible');
document.getElementById('submit-btn').disabled = true;
setTimeout(() => {
document.getElementById('success-message').classList.remove('visible');
document.getElementById('submit-btn').disabled = false;
}, 3000);
});
document.getElementById('clear-btn').addEventListener('click', () => {
document.getElementById('dynamic-form').reset();
// Clear tags
document.querySelectorAll('.tag').forEach(tag => tag.remove());
// Reset sliders
document.querySelectorAll('input[type="range"]').forEach(slider => {
const defaultVal = slider.getAttribute('value') || 50;
slider.value = defaultVal;
document.getElementById(`${slider.id}-value`).textContent = defaultVal;
});
updateProgress();
});
function validateForm() {
let isValid = true;
const form = document.getElementById('dynamic-form');
if (!formConfig) return false;
const allFields = (formConfig.sections || [{ fields: formConfig.fields || [] }])
.flatMap(s => s.fields);
allFields.forEach(field => {
if (field.required) {
const input = form.querySelector(`[name="${field.id}"]`) || form.querySelector(`#${field.id}`);
if (!input) return;
let hasValue = false;
if (field.type === 'checkbox' && field.options) {
hasValue = form.querySelectorAll(`[name="${field.id}"]:checked`).length > 0;
} else if (field.type === 'radio') {
hasValue = form.querySelector(`[name="${field.id}"]:checked`) !== null;
} else if (field.type === 'tags') {
hasValue = form.querySelectorAll(`#${field.id}-container .tag`).length > 0;
} else {
hasValue = input.value.trim() !== '';
}
if (!hasValue) {
isValid = false;
showError(field.id);
}
}
});
return isValid;
}
function showError(fieldId) {
const input = document.querySelector(`[name="${fieldId}"]`) || document.querySelector(`#${fieldId}`);
const error = document.getElementById(`${fieldId}-error`);
if (input) input.classList.add('error');
if (error) error.classList.add('visible');
}
function clearError(fieldId) {
const input = document.querySelector(`[name="${fieldId}"]`) || document.querySelector(`#${fieldId}`);
const error = document.getElementById(`${fieldId}-error`);
if (input) input.classList.remove('error');
if (error) error.classList.remove('visible');
}
function collectFormData() {
const form = document.getElementById('dynamic-form');
const data = {};
if (!formConfig) return data;
const allFields = (formConfig.sections || [{ fields: formConfig.fields || [] }])
.flatMap(s => s.fields);
allFields.forEach(field => {
if (field.type === 'checkbox' && field.options) {
const checked = form.querySelectorAll(`[name="${field.id}"]:checked`);
data[field.id] = Array.from(checked).map(c => c.value);
} else if (field.type === 'radio') {
const checked = form.querySelector(`[name="${field.id}"]:checked`);
data[field.id] = checked ? checked.value : null;
} else if (field.type === 'tags') {
const tags = form.querySelectorAll(`#${field.id}-container .tag`);
data[field.id] = Array.from(tags).map(t => t.dataset.value);
} else if (field.type === 'checkbox') {
const input = form.querySelector(`#${field.id}`);
data[field.id] = input ? input.checked : false;
} else {
const input = form.querySelector(`#${field.id}`);
data[field.id] = input ? input.value : '';
}
});
return data;
}
function formatSubmission(data) {
let text = `📋 **Form Submission: ${formConfig?.title || 'Form'}**\n\n`;
const allFields = (formConfig.sections || [{ fields: formConfig.fields || [] }])
.flatMap(s => s.fields);
allFields.forEach(field => {
const value = data[field.id];
let displayValue = value;
if (Array.isArray(value)) {
displayValue = value.length > 0 ? value.join(', ') : '(none)';
} else if (typeof value === 'boolean') {
displayValue = value ? 'Yes' : 'No';
} else if (!value && value !== 0) {
displayValue = '(not provided)';
}
text += `**${field.label}:** ${displayValue}\n`;
});
return text;
}
function updateProgress() {
if (!formConfig) return;
const allFields = (formConfig.sections || [{ fields: formConfig.fields || [] }])
.flatMap(s => s.fields);
const requiredFields = allFields.filter(f => f.required);
const form = document.getElementById('dynamic-form');
let completed = 0;
requiredFields.forEach(field => {
const input = form.querySelector(`[name="${field.id}"]`) || form.querySelector(`#${field.id}`);
if (!input) return;
let hasValue = false;
if (field.type === 'checkbox' && field.options) {
hasValue = form.querySelectorAll(`[name="${field.id}"]:checked`).length > 0;
} else if (field.type === 'radio') {
hasValue = form.querySelector(`[name="${field.id}"]:checked`) !== null;
} else if (field.type === 'tags') {
hasValue = form.querySelectorAll(`#${field.id}-container .tag`).length > 0;
} else {
hasValue = input.value.trim() !== '';
}
if (hasValue) completed++;
});
const total = requiredFields.length;
const percent = total > 0 ? (completed / total) * 100 : 100;
document.getElementById('progress-count').textContent = completed;
document.getElementById('total-required').textContent = total;
document.getElementById('progress-fill').style.width = `${percent}%`;
}
function notifySize() {
const height = document.body.scrollHeight;
app.sendSizeChanged(undefined, height);
}
// Observe size changes
const observer = new ResizeObserver(notifySize);
observer.observe(document.body);
</script>
</body>
</html>