@notbnull/mcp-cursor-taskmaster
Version:
A project-scoped MCP server requiring explicit --project-dir configuration for task management, memory retrieval, and agentic workflows
521 lines (456 loc) • 14.5 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cursor Taskmaster - Task Approval</title>
<script src="/socket.io/socket.io.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: #0a0a0a;
color: #e0e0e0;
line-height: 1.6;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: #fff;
margin-bottom: 30px;
font-size: 2rem;
font-weight: 600;
}
.no-tasks {
text-align: center;
padding: 60px 20px;
color: #666;
font-size: 1.1rem;
}
.task-card {
background-color: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
padding: 24px;
margin-bottom: 20px;
transition: all 0.2s ease;
}
.task-card:hover {
border-color: #555;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.task-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 16px;
}
.task-title {
font-size: 1.3rem;
font-weight: 600;
color: #fff;
margin-bottom: 8px;
}
.task-meta {
display: flex;
gap: 16px;
font-size: 0.9rem;
color: #999;
}
.priority {
padding: 2px 8px;
border-radius: 4px;
font-size: 0.85rem;
font-weight: 500;
}
.priority-low {
background-color: #1a3a1a;
color: #4ade80;
}
.priority-medium {
background-color: #3a3a1a;
color: #facc15;
}
.priority-high {
background-color: #3a1a1a;
color: #fb923c;
}
.priority-critical {
background-color: #3a1a1a;
color: #ef4444;
}
.task-description {
color: #ccc;
margin-bottom: 20px;
white-space: pre-wrap;
}
.editable-section {
background-color: #0a0a0a;
border: 1px solid #333;
border-radius: 6px;
padding: 16px;
margin-bottom: 20px;
}
.editable-section h3 {
color: #fff;
font-size: 1rem;
margin-bottom: 12px;
font-weight: 500;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
color: #999;
font-size: 0.9rem;
margin-bottom: 6px;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
background-color: #1a1a1a;
border: 1px solid #333;
color: #e0e0e0;
padding: 8px 12px;
border-radius: 4px;
font-size: 0.95rem;
font-family: inherit;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: #4a9eff;
}
.form-group textarea {
min-height: 100px;
resize: vertical;
}
.feedback-input {
width: 100%;
background-color: #1a1a1a;
border: 1px solid #333;
color: #e0e0e0;
padding: 12px;
border-radius: 6px;
font-size: 0.95rem;
margin-bottom: 20px;
min-height: 80px;
resize: vertical;
}
.action-buttons {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn-approve {
background-color: #10b981;
color: white;
}
.btn-approve:hover {
background-color: #059669;
}
.btn-reject {
background-color: #ef4444;
color: white;
}
.btn-reject:hover {
background-color: #dc2626;
}
.btn-edit {
background-color: #3b82f6;
color: white;
}
.btn-edit:hover {
background-color: #2563eb;
}
.status-badge {
position: fixed;
top: 20px;
right: 20px;
padding: 8px 16px;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
}
.status-connected {
background-color: #10b981;
color: white;
}
.status-disconnected {
background-color: #ef4444;
color: white;
}
.spinner {
width: 12px;
height: 12px;
border: 2px solid #ffffff30;
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.prompt-info {
background-color: #1e293b;
border: 1px solid #334155;
border-radius: 6px;
padding: 16px;
margin-bottom: 20px;
}
.prompt-info h4 {
color: #94a3b8;
font-size: 0.9rem;
margin-bottom: 8px;
font-weight: 500;
}
.prompt-info p {
color: #cbd5e1;
}
</style>
</head>
<body>
<div class="container">
<h1>Cursor Taskmaster - Task Approval</h1>
<div id="status" class="status-badge status-disconnected">
<span class="spinner"></span>
Connecting...
</div>
<div id="tasks-container">
<div class="no-tasks">No tasks pending approval</div>
</div>
</div>
<script>
const socket = io();
const tasksContainer = document.getElementById("tasks-container");
const statusBadge = document.getElementById("status");
const pendingTasks = new Map();
socket.on("connect", () => {
statusBadge.className = "status-badge status-connected";
statusBadge.innerHTML = "● Connected";
});
socket.on("disconnect", () => {
statusBadge.className = "status-badge status-disconnected";
statusBadge.innerHTML = '<span class="spinner"></span> Disconnected';
});
socket.on("pending-approvals", (approvals) => {
approvals.forEach(({ id, request }) => {
if (!pendingTasks.has(id)) {
addTaskCard(id, request);
}
});
updateEmptyState();
});
socket.on("new-approval", ({ id, request }) => {
if (!pendingTasks.has(id)) {
addTaskCard(id, request);
updateEmptyState();
}
});
socket.on("approval-resolved", (taskId) => {
const card = pendingTasks.get(taskId);
if (card) {
card.remove();
pendingTasks.delete(taskId);
updateEmptyState();
}
});
function updateEmptyState() {
if (pendingTasks.size === 0) {
tasksContainer.innerHTML =
'<div class="no-tasks">No tasks pending approval</div>';
}
}
function addTaskCard(taskId, request) {
const { task, promptTitle, promptDescription } = request;
const card = document.createElement("div");
card.className = "task-card";
card.innerHTML = `
${
promptTitle || promptDescription
? `
<div class="prompt-info">
${
promptTitle
? `<h4>Prompt: ${escapeHtml(promptTitle)}</h4>`
: ""
}
${
promptDescription
? `<p>${escapeHtml(promptDescription)}</p>`
: ""
}
</div>
`
: ""
}
<div class="task-header">
<div>
<div class="task-title">${escapeHtml(task.title)}</div>
<div class="task-meta">
<span class="priority priority-${
task.priority
}">${task.priority.toUpperCase()}</span>
${
task.estimatedTime
? `<span>⏱ ${task.estimatedTime} min</span>`
: ""
}
${
task.dueDate
? `<span>📅 ${new Date(
task.dueDate
).toLocaleDateString()}</span>`
: ""
}
</div>
</div>
</div>
<div class="task-description">${escapeHtml(
task.description
)}</div>
<div class="editable-section" style="display: none;">
<h3>Edit Task</h3>
<div class="form-group">
<label>Title</label>
<input type="text" id="title-${taskId}" value="${escapeHtml(
task.title
)}">
</div>
<div class="form-group">
<label>Description</label>
<textarea id="desc-${taskId}">${escapeHtml(
task.description
)}</textarea>
</div>
<div class="form-group">
<label>Priority</label>
<select id="priority-${taskId}">
<option value="low" ${
task.priority === "low" ? "selected" : ""
}>Low</option>
<option value="medium" ${
task.priority === "medium" ? "selected" : ""
}>Medium</option>
<option value="high" ${
task.priority === "high" ? "selected" : ""
}>High</option>
<option value="critical" ${
task.priority === "critical" ? "selected" : ""
}>Critical</option>
</select>
</div>
<div class="form-group">
<label>Estimated Time (minutes)</label>
<input type="number" id="time-${taskId}" value="${
task.estimatedTime || ""
}">
</div>
</div>
<textarea class="feedback-input" id="feedback-${taskId}" placeholder="Add feedback or comments (optional)"></textarea>
<div class="action-buttons">
<button class="btn btn-edit" onclick="toggleEdit('${taskId}')">
✏️ Edit
</button>
<button class="btn btn-reject" onclick="rejectTask('${taskId}')">
❌ Reject
</button>
<button class="btn btn-approve" onclick="approveTask('${taskId}')">
✅ Approve
</button>
</div>
`;
// Remove empty state if exists
const emptyState = tasksContainer.querySelector(".no-tasks");
if (emptyState) {
emptyState.remove();
}
tasksContainer.appendChild(card);
pendingTasks.set(taskId, card);
}
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
function toggleEdit(taskId) {
const card = pendingTasks.get(taskId);
const editSection = card.querySelector(".editable-section");
const editBtn = card.querySelector(".btn-edit");
if (editSection.style.display === "none") {
editSection.style.display = "block";
editBtn.textContent = "💾 Save Changes";
} else {
editSection.style.display = "none";
editBtn.textContent = "✏️ Edit";
}
}
function getModifications(taskId) {
const card = pendingTasks.get(taskId);
const editSection = card.querySelector(".editable-section");
if (editSection.style.display === "none") {
return null;
}
const modifications = {};
const title = document.getElementById(`title-${taskId}`).value;
const description = document.getElementById(`desc-${taskId}`).value;
const priority = document.getElementById(`priority-${taskId}`).value;
const estimatedTime = document.getElementById(`time-${taskId}`).value;
if (title) modifications.title = title;
if (description) modifications.description = description;
if (priority) modifications.priority = priority;
if (estimatedTime)
modifications.estimatedTime = parseInt(estimatedTime);
return Object.keys(modifications).length > 0 ? modifications : null;
}
function approveTask(taskId) {
const feedback = document.getElementById(`feedback-${taskId}`).value;
const modifications = getModifications(taskId);
socket.emit("approval-response", {
taskId,
approved: true,
modifications,
userFeedback: feedback || undefined,
});
}
function rejectTask(taskId) {
const feedback = document.getElementById(`feedback-${taskId}`).value;
const modifications = getModifications(taskId);
socket.emit("approval-response", {
taskId,
approved: false,
modifications,
userFeedback: feedback || undefined,
});
}
</script>
</body>
</html>