Skip to main content
Glama
tasks.ts6.56 kB
/** * Quick Tasks MCP App UI * * Demonstrates bidirectional communication between embedded UI and AI chat host. */ import { App } from "@modelcontextprotocol/ext-apps"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import "./styles.css"; import { PostMessageTransport } from "@modelcontextprotocol/ext-apps"; interface Task { id: string; text: string; completed: boolean; createdAt: string; } interface TasksResponse { tasks: Task[]; task?: Task; deleted?: Task; error?: string; } // DOM elements const taskListEl = document.getElementById("task-list")!; const newTaskInput = document.getElementById("new-task-input") as HTMLInputElement; const addTaskBtn = document.getElementById("add-task-btn")!; const refreshBtn = document.getElementById("refresh-btn")!; const taskCountEl = document.getElementById("task-count")!; const completedCountEl = document.getElementById("completed-count")!; // State let tasks: Task[] = []; // Create MCP App instance const app = new App({ name: "Quick Tasks", version: "1.0.0" }); // Parse tool result to extract tasks data function parseResult(result: CallToolResult): TasksResponse | null { const textContent = result.content?.find((c) => c.type === "text"); if (!textContent || textContent.type !== "text") return null; try { return JSON.parse(textContent.text); } catch { return null; } } // Render the task list function renderTasks() { if (tasks.length === 0) { taskListEl.innerHTML = '<p class="empty">No tasks yet. Add one above!</p>'; } else { taskListEl.innerHTML = tasks .map( (task) => ` <div class="task ${task.completed ? "completed" : ""}" data-id="${task.id}"> <button class="toggle-btn" title="Toggle completion"> ${task.completed ? "&#10003;" : "&#9675;"} </button> <span class="task-text">${escapeHtml(task.text)}</span> <button class="delete-btn" title="Delete task">&times;</button> </div> ` ) .join(""); // Add event listeners for toggle and delete taskListEl.querySelectorAll(".toggle-btn").forEach((btn) => { btn.addEventListener("click", (e) => { const taskEl = (e.target as HTMLElement).closest(".task") as HTMLElement; toggleTask(taskEl.dataset.id!); }); }); taskListEl.querySelectorAll(".delete-btn").forEach((btn) => { btn.addEventListener("click", (e) => { const taskEl = (e.target as HTMLElement).closest(".task") as HTMLElement; deleteTask(taskEl.dataset.id!); }); }); } // Update stats const completed = tasks.filter((t) => t.completed).length; taskCountEl.textContent = `${tasks.length} task${tasks.length !== 1 ? "s" : ""}`; completedCountEl.textContent = `${completed} completed`; } function escapeHtml(text: string): string { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } // Fetch tasks from server async function fetchTasks() { taskListEl.innerHTML = '<p class="loading">Loading...</p>'; try { const result = await app.callServerTool({ name: "get-tasks", arguments: {} }); const data = parseResult(result); if (data?.tasks) { tasks = data.tasks; renderTasks(); } } catch (err) { taskListEl.innerHTML = '<p class="error">Failed to load tasks</p>'; console.error("[APP] Error fetching tasks:", err); } } // Add a new task async function addTask() { const text = newTaskInput.value.trim(); if (!text) return; newTaskInput.disabled = true; addTaskBtn.textContent = "Adding..."; try { const result = await app.callServerTool({ name: "add-task", arguments: { text } }); const data = parseResult(result); if (data?.tasks) { tasks = data.tasks; renderTasks(); newTaskInput.value = ""; // Notify the chat that a task was added await app.sendMessage({ role: "user", content: [{ type: "text", text: `Added task: "${text}"` }], }); } } catch (err) { console.error("[APP] Error adding task:", err); } finally { newTaskInput.disabled = false; addTaskBtn.textContent = "Add"; newTaskInput.focus(); } } // Toggle task completion async function toggleTask(id: string) { const task = tasks.find((t) => t.id === id); if (!task) return; // Optimistic update task.completed = !task.completed; renderTasks(); try { const result = await app.callServerTool({ name: "toggle-task", arguments: { id } }); const data = parseResult(result); if (data?.tasks) { tasks = data.tasks; renderTasks(); // Notify the chat const status = data.task?.completed ? "completed" : "uncompleted"; await app.sendMessage({ role: "user", content: [{ type: "text", text: `Task ${status}: "${data.task?.text}"` }], }); } } catch (err) { // Revert on error task.completed = !task.completed; renderTasks(); console.error("[APP] Error toggling task:", err); } } // Delete a task async function deleteTask(id: string) { const task = tasks.find((t) => t.id === id); if (!task) return; // Optimistic update tasks = tasks.filter((t) => t.id !== id); renderTasks(); try { const result = await app.callServerTool({ name: "delete-task", arguments: { id } }); const data = parseResult(result); if (data?.tasks) { tasks = data.tasks; renderTasks(); // Notify the chat await app.sendMessage({ role: "user", content: [{ type: "text", text: `Deleted task: "${data.deleted?.text}"` }], }); } } catch (err) { // Revert on error by refetching await fetchTasks(); console.error("[APP] Error deleting task:", err); } } // Event handlers app.ontoolinput = (params) => { console.log("[APP] Tool input received:", params); }; app.ontoolresult = (result) => { console.log("[APP] Tool result received:", result); const data = parseResult(result); if (data?.tasks) { tasks = data.tasks; renderTasks(); } }; app.onerror = (err) => { console.error("[APP] Error:", err); }; app.onteardown = async () => { console.log("[APP] Teardown"); return {}; }; // UI event listeners addTaskBtn.addEventListener("click", addTask); newTaskInput.addEventListener("keypress", (e) => { if (e.key === "Enter") addTask(); }); refreshBtn.addEventListener("click", fetchTasks); // Connect to host and load initial data app.connect(new PostMessageTransport(window.parent)); fetchTasks();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jamesdowzard/mcp-apps-poc'

If you have feedback or need assistance with the MCP directory API, please join our Discord server