@qzoft/check-list 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # check-list
2
+
3
+ A general-purpose MCP App that discovers and displays checklists from **all markdown files** in your project, rendered as an interactive checkbox UI inside VS Code Copilot Chat. Changes are saved automatically — no confirmation needed.
4
+
5
+ ## Prerequisites
6
+
7
+ - Node.js 18+
8
+ - VS Code Insiders (for [MCP Apps support](https://code.visualstudio.com/blogs/2026/01/26/mcp-apps-support))
9
+
10
+ ## Setup
11
+
12
+ 1. Clone this repo:
13
+ ```sh
14
+ git clone https://github.com/qzoft/check-list.git
15
+ cd check-list
16
+ ```
17
+
18
+ 2. Install dependencies:
19
+ ```sh
20
+ npm install
21
+ ```
22
+
23
+ 3. Build the project:
24
+ ```sh
25
+ npm run build
26
+ ```
27
+
28
+ 4. Open VS Code — the `.vscode/mcp.json` config auto-registers the server. VS Code will pick it up automatically when you open the workspace.
29
+
30
+ 5. In Copilot Chat, ask **"show my tasks"** → the `list_tasks` tool scans the project and renders the interactive checkbox UI.
31
+
32
+ ## Usage
33
+
34
+ Once the server is running, you can use two tools in Copilot Chat:
35
+
36
+ - **`list_tasks`** — scans all `.md` files in the project directory, finds checkbox items, and displays an interactive UI grouped by file and section. Toggle any checkbox and it saves automatically.
37
+ - **`update_tasks`** — called automatically when you toggle a checkbox. Can also be called directly by Copilot to update tasks in any markdown file.
38
+
39
+ ## How it works
40
+
41
+ ```
42
+ ┌──────────────────────────┐
43
+ │ VS Code Copilot Chat │
44
+ │ │
45
+ │ "show my tasks" │
46
+ │ │ │
47
+ │ ▼ │
48
+ │ list_tasks tool ───────┼──► MCP Server (Node.js)
49
+ │ │ │ │
50
+ │ │ │ scans project for *.md
51
+ │ │ │ parses checkboxes
52
+ │ │ │ │
53
+ │ renders iframe UI ◄────┼─────────┘
54
+ │ (task-checklist.html) │
55
+ │ │ │
56
+ │ [checkbox toggle] │
57
+ │ │ │
58
+ │ auto-save ─────────────┼──► MCP Server writes to file
59
+ └──────────────────────────┘
60
+ ```
61
+
62
+ 1. The MCP server recursively discovers all `.md` files in the project directory.
63
+ 2. Each file is parsed for `## Section` headers and `- [ ]` / `- [x]` checkbox items.
64
+ 3. VS Code renders `ui/task-checklist.html` as an interactive iframe grouped by file.
65
+ 4. Toggling a checkbox **immediately saves** the change back to the originating file — no save button required.
66
+
67
+ ## Configuration
68
+
69
+ | Variable | Description | Example |
70
+ |---------------|-----------------------------------------------------|----------------------------------|
71
+ | `PROJECT_DIR` | Root directory to scan for markdown files | `~/repos/my-project` |
72
+ | `TASK_FILE` | *(backward-compat)* Falls back to parent directory | `~/repos/my-project/task.md` |
73
+
74
+ If neither is set, the server uses the current working directory.
75
+
76
+ The project directory is configured in `.vscode/mcp.json`:
77
+
78
+ ```json
79
+ {
80
+ "servers": {
81
+ "check-list": {
82
+ "command": "node",
83
+ "args": ["dist/server.js"],
84
+ "env": {
85
+ "PROJECT_DIR": "${workspaceFolder}"
86
+ }
87
+ }
88
+ }
89
+ }
90
+ ```
91
+
92
+ ## Task file format
93
+
94
+ The parser recognizes `## Section` headers and checkbox list items in any `.md` file:
95
+
96
+ ```markdown
97
+ ## Today
98
+ - [ ] Write tests
99
+ - [ ] Update README
100
+
101
+ ## This Week
102
+ - [ ] Review PRs
103
+ - [ ] Deploy to staging
104
+ ```
105
+
106
+ Checkbox items that appear before any section header are grouped under a default "Tasks" section. Non-checkbox content is preserved as-is when writing back.
107
+
108
+ The server skips common non-project directories (`node_modules`, `.git`, `dist`, `build`, etc.) during scanning.
109
+
110
+ ## Project structure
111
+
112
+ ```
113
+ check-list/
114
+ ├── package.json
115
+ ├── tsconfig.json
116
+ ├── README.md
117
+ ├── .vscode/
118
+ │ └── mcp.json # VS Code MCP server config
119
+ ├── src/
120
+ │ ├── server.ts # MCP server entry point
121
+ │ ├── parser.ts # Markdown checkbox parser
122
+ │ └── writer.ts # File discovery & read/write
123
+ └── ui/
124
+ └── task-checklist.html # MCP App UI rendered in iframe
125
+ ```
126
+
127
+ ## Learn more
128
+
129
+ - [MCP Apps support in VS Code](https://code.visualstudio.com/blogs/2026/01/26/mcp-apps-support)
130
+ - [Model Context Protocol SDK](https://github.com/modelcontextprotocol/typescript-sdk)
@@ -0,0 +1,27 @@
1
+ export interface Task {
2
+ text: string;
3
+ checked: boolean;
4
+ line: number;
5
+ }
6
+ export interface TaskSection {
7
+ name: string;
8
+ tasks: Task[];
9
+ }
10
+ /** Represents all checkbox tasks found in a single markdown file. */
11
+ export interface FileTaskGroup {
12
+ file: string;
13
+ sections: TaskSection[];
14
+ }
15
+ /**
16
+ * Parses a markdown string, finding section headers (## SectionName) and
17
+ * checkbox lines within each section.
18
+ */
19
+ export declare function parseTasks(markdown: string): TaskSection[];
20
+ /**
21
+ * Applies updates to the original markdown, toggling checkbox state for each
22
+ * specified line. All other content is preserved exactly as-is.
23
+ */
24
+ export declare function serializeTasks(original: string, updates: {
25
+ line: number;
26
+ checked: boolean;
27
+ }[]): string;
package/dist/parser.js ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Parses a markdown string, finding section headers (## SectionName) and
3
+ * checkbox lines within each section.
4
+ */
5
+ export function parseTasks(markdown) {
6
+ const lines = markdown.replace(/\r/g, '').split('\n');
7
+ const sections = [];
8
+ let currentSection = null;
9
+ for (let i = 0; i < lines.length; i++) {
10
+ const line = lines[i];
11
+ // Match section headers: ## SectionName
12
+ const sectionMatch = line.match(/^##\s+(.+)$/);
13
+ if (sectionMatch) {
14
+ currentSection = { name: sectionMatch[1].trim(), tasks: [] };
15
+ sections.push(currentSection);
16
+ continue;
17
+ }
18
+ // Match checkbox lines (with or without a section header)
19
+ const checkedMatch = line.match(/^- \[x\]\s+(.+)$/i);
20
+ const uncheckedMatch = line.match(/^- \[ \]\s+(.+)$/);
21
+ if (checkedMatch || uncheckedMatch) {
22
+ // Ensure there is a section to hold the task
23
+ if (!currentSection) {
24
+ currentSection = { name: 'Tasks', tasks: [] };
25
+ sections.push(currentSection);
26
+ }
27
+ if (checkedMatch) {
28
+ currentSection.tasks.push({
29
+ text: checkedMatch[1].trim(),
30
+ checked: true,
31
+ line: i,
32
+ });
33
+ }
34
+ else if (uncheckedMatch) {
35
+ currentSection.tasks.push({
36
+ text: uncheckedMatch[1].trim(),
37
+ checked: false,
38
+ line: i,
39
+ });
40
+ }
41
+ }
42
+ }
43
+ return sections;
44
+ }
45
+ /**
46
+ * Applies updates to the original markdown, toggling checkbox state for each
47
+ * specified line. All other content is preserved exactly as-is.
48
+ */
49
+ export function serializeTasks(original, updates) {
50
+ const lines = original.split('\n');
51
+ for (const update of updates) {
52
+ const line = lines[update.line];
53
+ if (line === undefined)
54
+ continue;
55
+ if (update.checked) {
56
+ // Mark as checked: replace - [ ] with - [x]
57
+ lines[update.line] = line.replace(/^(- )\[ \]/, '$1[x]');
58
+ }
59
+ else {
60
+ // Mark as unchecked: replace - [x] with - [ ]
61
+ lines[update.line] = line.replace(/^(- )\[[xX]\]/, '$1[ ]');
62
+ }
63
+ }
64
+ return lines.join('\n');
65
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/server.js ADDED
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server';
5
+ import { z } from 'zod';
6
+ import { fileURLToPath } from 'url';
7
+ import path from 'path';
8
+ import fs from 'fs';
9
+ import { discoverMarkdownFiles, readTaskFile, writeTaskFile } from './writer.js';
10
+ import { parseTasks, serializeTasks } from './parser.js';
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+ // Use PROJECT_DIR if set, otherwise fall back to TASK_FILE's parent dir, or CWD
14
+ const projectDir = (() => {
15
+ const dir = process.env['PROJECT_DIR'];
16
+ if (dir) {
17
+ return dir.startsWith('~')
18
+ ? path.join(process.env['HOME'] ?? '', dir.slice(1))
19
+ : path.resolve(dir);
20
+ }
21
+ // Backward-compat: if TASK_FILE is set, use its parent directory
22
+ const taskFile = process.env['TASK_FILE'];
23
+ if (taskFile) {
24
+ const resolved = taskFile.startsWith('~')
25
+ ? path.join(process.env['HOME'] ?? '', taskFile.slice(1))
26
+ : path.resolve(taskFile);
27
+ return path.dirname(resolved);
28
+ }
29
+ return process.cwd();
30
+ })();
31
+ // MCP App UI resource
32
+ const uiHtmlPath = path.resolve(__dirname, '..', 'ui', 'task-checklist.html');
33
+ const uiResourceUri = 'ui://check-list/task-checklist.html';
34
+ const server = new McpServer({
35
+ name: 'check-list',
36
+ version: '2.0.0',
37
+ });
38
+ // Register the HTML resource for the UI
39
+ registerAppResource(server, 'Task Checklist', uiResourceUri, { description: 'Interactive checkbox UI for task management' }, async () => ({
40
+ contents: [{
41
+ uri: uiResourceUri,
42
+ mimeType: RESOURCE_MIME_TYPE,
43
+ text: fs.readFileSync(uiHtmlPath, 'utf-8'),
44
+ }],
45
+ }));
46
+ registerAppTool(server, 'list_tasks', {
47
+ description: 'Discover and display checklists from all markdown files in the project',
48
+ _meta: { ui: { resourceUri: uiResourceUri } },
49
+ }, async () => {
50
+ let mdFiles;
51
+ try {
52
+ mdFiles = await discoverMarkdownFiles(projectDir);
53
+ }
54
+ catch (err) {
55
+ const message = err instanceof Error ? err.message : String(err);
56
+ return {
57
+ isError: true,
58
+ content: [
59
+ {
60
+ type: 'text',
61
+ text: `Error scanning project directory ${projectDir}: ${message}`,
62
+ },
63
+ ],
64
+ };
65
+ }
66
+ const fileGroups = [];
67
+ for (const filePath of mdFiles) {
68
+ let content;
69
+ try {
70
+ content = await readTaskFile(filePath);
71
+ }
72
+ catch {
73
+ continue; // skip files we can't read
74
+ }
75
+ const sections = parseTasks(content);
76
+ const hasTasks = sections.some((s) => s.tasks.length > 0);
77
+ if (!hasTasks)
78
+ continue;
79
+ // Use relative path for cleaner display
80
+ const relPath = path.relative(projectDir, filePath);
81
+ fileGroups.push({ file: relPath, sections });
82
+ }
83
+ return {
84
+ content: [
85
+ {
86
+ type: 'text',
87
+ text: 'The tasks are displayed in the interactive UI above. Do not repeat or summarize the task content in your response — the user can already see and interact with them.',
88
+ },
89
+ ],
90
+ structuredContent: { files: fileGroups },
91
+ };
92
+ });
93
+ registerAppTool(server, 'update_tasks', {
94
+ description: 'Update checkbox states in a project markdown file (auto-saved on toggle)',
95
+ inputSchema: {
96
+ file: z.string().describe('Relative path to the markdown file within the project'),
97
+ updates: z.array(z.object({
98
+ line: z.number().describe('0-indexed line number in the markdown file'),
99
+ checked: z.boolean().describe('New checked state for the checkbox'),
100
+ })).describe('Array of line updates to apply'),
101
+ },
102
+ _meta: { ui: { resourceUri: uiResourceUri, visibility: ['app'] } },
103
+ }, async ({ file, updates }) => {
104
+ const filePath = path.resolve(projectDir, file);
105
+ let content;
106
+ try {
107
+ content = await readTaskFile(filePath);
108
+ }
109
+ catch (err) {
110
+ const message = err instanceof Error ? err.message : String(err);
111
+ return {
112
+ isError: true,
113
+ content: [
114
+ {
115
+ type: 'text',
116
+ text: `Error reading file ${file}: ${message}`,
117
+ },
118
+ ],
119
+ };
120
+ }
121
+ const updated = serializeTasks(content, updates);
122
+ try {
123
+ await writeTaskFile(filePath, updated);
124
+ }
125
+ catch (err) {
126
+ const message = err instanceof Error ? err.message : String(err);
127
+ return {
128
+ isError: true,
129
+ content: [
130
+ {
131
+ type: 'text',
132
+ text: `Error writing file ${file}: ${message}`,
133
+ },
134
+ ],
135
+ };
136
+ }
137
+ const checkedCount = updates.filter((u) => u.checked).length;
138
+ const uncheckedCount = updates.filter((u) => !u.checked).length;
139
+ const parts = [];
140
+ if (checkedCount > 0)
141
+ parts.push(`${checkedCount} task(s) marked as done`);
142
+ if (uncheckedCount > 0)
143
+ parts.push(`${uncheckedCount} task(s) marked as undone`);
144
+ return {
145
+ content: [
146
+ {
147
+ type: 'text',
148
+ text: `✅ Saved ${file}: ${parts.join(', ')}.`,
149
+ },
150
+ ],
151
+ };
152
+ });
153
+ const transport = new StdioServerTransport();
154
+ await server.connect(transport);
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Recursively discovers all `.md` files under the given directory,
3
+ * skipping common non-project directories.
4
+ */
5
+ export declare function discoverMarkdownFiles(dir: string): Promise<string[]>;
6
+ /**
7
+ * Reads the markdown file from disk and returns its content as a string.
8
+ */
9
+ export declare function readTaskFile(filePath: string): Promise<string>;
10
+ /**
11
+ * Writes the updated markdown content back to the local file.
12
+ */
13
+ export declare function writeTaskFile(filePath: string, content: string): Promise<void>;
package/dist/writer.js ADDED
@@ -0,0 +1,59 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+ /** Directories to skip when discovering markdown files. */
4
+ const IGNORED_DIRS = new Set([
5
+ 'node_modules',
6
+ '.git',
7
+ 'dist',
8
+ '.next',
9
+ '.nuxt',
10
+ 'build',
11
+ 'coverage',
12
+ 'vendor',
13
+ '__pycache__',
14
+ ]);
15
+ /**
16
+ * Recursively discovers all `.md` files under the given directory,
17
+ * skipping common non-project directories.
18
+ */
19
+ export async function discoverMarkdownFiles(dir) {
20
+ const results = [];
21
+ async function walk(current) {
22
+ let entries;
23
+ try {
24
+ entries = await fs.readdir(current, { withFileTypes: true });
25
+ }
26
+ catch {
27
+ return; // skip directories we can't read
28
+ }
29
+ for (const entry of entries) {
30
+ if (entry.name.startsWith('.'))
31
+ continue;
32
+ const fullPath = path.join(current, entry.name);
33
+ if (entry.isDirectory()) {
34
+ if (!IGNORED_DIRS.has(entry.name)) {
35
+ await walk(fullPath);
36
+ }
37
+ }
38
+ else if (entry.isFile() && entry.name.endsWith('.md')) {
39
+ results.push(fullPath);
40
+ }
41
+ }
42
+ }
43
+ await walk(dir);
44
+ // Sort alphabetically for consistent ordering in the UI
45
+ results.sort();
46
+ return results;
47
+ }
48
+ /**
49
+ * Reads the markdown file from disk and returns its content as a string.
50
+ */
51
+ export async function readTaskFile(filePath) {
52
+ return fs.readFile(filePath, 'utf-8');
53
+ }
54
+ /**
55
+ * Writes the updated markdown content back to the local file.
56
+ */
57
+ export async function writeTaskFile(filePath, content) {
58
+ await fs.writeFile(filePath, content, 'utf-8');
59
+ }
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@qzoft/check-list",
3
+ "version": "1.0.0",
4
+ "description": "MCP App for interactive task management from markdown files",
5
+ "type": "module",
6
+ "main": "dist/server.js",
7
+ "bin": "dist/server.js",
8
+ "files": ["dist", "ui"],
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "node dist/server.js",
12
+ "prepare": "npm run build"
13
+ },
14
+ "dependencies": {
15
+ "@modelcontextprotocol/ext-apps": "^1.2.0",
16
+ "@modelcontextprotocol/sdk": "^1.27.1"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^22.0.0",
20
+ "typescript": "^5.7.0"
21
+ }
22
+ }
@@ -0,0 +1,369 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Tasks</title>
7
+ <style>
8
+ *, *::before, *::after {
9
+ box-sizing: border-box;
10
+ }
11
+
12
+ :root {
13
+ --bg: #ffffff;
14
+ --fg: #1f2328;
15
+ --border: #d1d5db;
16
+ --section-bg: #f6f8fa;
17
+ --checked-fg: #6b7280;
18
+ --accent: #0969da;
19
+ --accent-hover: #0550ae;
20
+ --file-bg: #f0f4f8;
21
+ --file-border: #c8d1da;
22
+ --radius: 6px;
23
+ --save-ok: #1a7f37;
24
+ --save-err: #cf222e;
25
+ }
26
+
27
+ @media (prefers-color-scheme: dark) {
28
+ :root {
29
+ --bg: #0d1117;
30
+ --fg: #e6edf3;
31
+ --border: #30363d;
32
+ --section-bg: #161b22;
33
+ --checked-fg: #8b949e;
34
+ --accent: #58a6ff;
35
+ --accent-hover: #79c0ff;
36
+ --file-bg: #161b22;
37
+ --file-border: #30363d;
38
+ --save-ok: #3fb950;
39
+ --save-err: #f85149;
40
+ }
41
+ }
42
+
43
+ body {
44
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
45
+ font-size: 14px;
46
+ background: var(--bg);
47
+ color: var(--fg);
48
+ margin: 0;
49
+ padding: 16px;
50
+ line-height: 1.5;
51
+ }
52
+
53
+ #app {
54
+ max-width: 600px;
55
+ }
56
+
57
+ h2 {
58
+ font-size: 16px;
59
+ font-weight: 600;
60
+ margin: 0 0 16px;
61
+ }
62
+
63
+ .file-group {
64
+ margin-bottom: 20px;
65
+ border: 1px solid var(--file-border);
66
+ border-radius: var(--radius);
67
+ overflow: hidden;
68
+ }
69
+
70
+ .file-header {
71
+ font-size: 12px;
72
+ font-weight: 600;
73
+ font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
74
+ padding: 6px 10px;
75
+ background: var(--file-bg);
76
+ border-bottom: 1px solid var(--file-border);
77
+ color: var(--checked-fg);
78
+ display: flex;
79
+ align-items: center;
80
+ gap: 6px;
81
+ }
82
+
83
+ .file-header .icon {
84
+ font-size: 14px;
85
+ }
86
+
87
+ .file-body {
88
+ padding: 8px 10px;
89
+ }
90
+
91
+ .section {
92
+ margin-bottom: 12px;
93
+ }
94
+
95
+ .section:last-child {
96
+ margin-bottom: 0;
97
+ }
98
+
99
+ .section-title {
100
+ font-size: 13px;
101
+ font-weight: 600;
102
+ text-transform: uppercase;
103
+ letter-spacing: 0.05em;
104
+ color: var(--checked-fg);
105
+ margin: 0 0 6px;
106
+ padding-bottom: 4px;
107
+ border-bottom: 1px solid var(--border);
108
+ }
109
+
110
+ .task-list {
111
+ list-style: none;
112
+ margin: 0;
113
+ padding: 0;
114
+ }
115
+
116
+ .task-item {
117
+ display: flex;
118
+ align-items: flex-start;
119
+ gap: 8px;
120
+ padding: 3px 0;
121
+ }
122
+
123
+ .task-item input[type="checkbox"] {
124
+ margin-top: 2px;
125
+ flex-shrink: 0;
126
+ cursor: pointer;
127
+ accent-color: var(--accent);
128
+ width: 14px;
129
+ height: 14px;
130
+ }
131
+
132
+ .task-item input[type="checkbox"]:disabled {
133
+ cursor: wait;
134
+ }
135
+
136
+ .task-label {
137
+ cursor: pointer;
138
+ user-select: none;
139
+ }
140
+
141
+ .task-label.done {
142
+ text-decoration: line-through;
143
+ color: var(--checked-fg);
144
+ }
145
+
146
+ #status {
147
+ margin-top: 8px;
148
+ font-size: 12px;
149
+ color: var(--checked-fg);
150
+ min-height: 16px;
151
+ transition: opacity 0.3s;
152
+ }
153
+
154
+ #status.ok { color: var(--save-ok); }
155
+ #status.err { color: var(--save-err); }
156
+
157
+ #loading {
158
+ color: var(--checked-fg);
159
+ }
160
+ </style>
161
+ </head>
162
+ <body>
163
+ <div id="app">
164
+ <h2>📋 Tasks</h2>
165
+ <div id="sections">
166
+ <p id="loading">Loading tasks…</p>
167
+ </div>
168
+ <div id="status"></div>
169
+ </div>
170
+
171
+ <script>
172
+ const sectionsEl = document.getElementById('sections');
173
+ const statusEl = document.getElementById('status');
174
+
175
+ const STATUS_DISPLAY_MS = 3000;
176
+ let statusTimer = null;
177
+
178
+ // --- MCP Apps postMessage transport ---
179
+ let rpcId = 1;
180
+ const pendingRequests = new Map();
181
+
182
+ function sendRequest(method, params) {
183
+ const id = rpcId++;
184
+ window.parent.postMessage({ jsonrpc: '2.0', id, method, params }, '*');
185
+ return new Promise((resolve, reject) => {
186
+ pendingRequests.set(id, { resolve, reject });
187
+ });
188
+ }
189
+
190
+ function sendNotification(method, params) {
191
+ window.parent.postMessage({ jsonrpc: '2.0', method, params }, '*');
192
+ }
193
+
194
+ // Handle incoming messages from host
195
+ window.addEventListener('message', (event) => {
196
+ const data = event.data;
197
+ if (!data || typeof data !== 'object') return;
198
+
199
+ // Response to a pending request
200
+ if ('id' in data && pendingRequests.has(data.id)) {
201
+ const { resolve, reject } = pendingRequests.get(data.id);
202
+ pendingRequests.delete(data.id);
203
+ if (data.error) {
204
+ reject(new Error(data.error.message || JSON.stringify(data.error)));
205
+ } else {
206
+ resolve(data.result);
207
+ }
208
+ return;
209
+ }
210
+
211
+ // Notifications from host
212
+ if (data.method === 'ui/notifications/tool-result') {
213
+ handleToolResult(data.params);
214
+ }
215
+ });
216
+
217
+ function handleToolResult(result) {
218
+ try {
219
+ // Prefer structuredContent, fall back to parsing text content
220
+ let fileGroups = result.structuredContent?.files;
221
+ if (!fileGroups) {
222
+ const textContent = result.content?.find(c => c.type === 'text');
223
+ if (textContent) {
224
+ fileGroups = JSON.parse(textContent.text);
225
+ }
226
+ }
227
+ if (fileGroups) {
228
+ renderFileGroups(fileGroups);
229
+ }
230
+ } catch (err) {
231
+ sectionsEl.innerHTML = '<p>Error loading tasks: ' + (err.message || String(err)) + '</p>';
232
+ }
233
+ }
234
+
235
+ // Notify host when content size changes so the iframe resizes
236
+ let lastWidth = 0;
237
+ let lastHeight = 0;
238
+ const resizeObserver = new ResizeObserver(() => {
239
+ const width = document.documentElement.scrollWidth;
240
+ const height = document.documentElement.scrollHeight;
241
+ if (width !== lastWidth || height !== lastHeight) {
242
+ lastWidth = width;
243
+ lastHeight = height;
244
+ sendNotification('ui/notifications/size-changed', { width, height });
245
+ }
246
+ });
247
+ resizeObserver.observe(document.documentElement);
248
+
249
+ // Initialize: handshake with host
250
+ (async () => {
251
+ try {
252
+ await sendRequest('ui/initialize', {
253
+ protocolVersion: '2026-01-26',
254
+ capabilities: {},
255
+ clientInfo: { name: 'check-list', version: '1.0.0' },
256
+ });
257
+ sendNotification('ui/notifications/initialized', {});
258
+ } catch (err) {
259
+ console.error('MCP Apps init failed:', err);
260
+ }
261
+ })();
262
+
263
+ function showStatus(msg, type) {
264
+ statusEl.textContent = msg;
265
+ statusEl.className = type || '';
266
+ if (statusTimer) clearTimeout(statusTimer);
267
+ statusTimer = setTimeout(() => { statusEl.textContent = ''; statusEl.className = ''; }, STATUS_DISPLAY_MS);
268
+ }
269
+
270
+ /**
271
+ * Auto-save a single checkbox toggle by calling the update_tasks tool via the host.
272
+ */
273
+ async function autoSave(file, line, checked, checkboxEl, labelEl) {
274
+ checkboxEl.disabled = true;
275
+
276
+ try {
277
+ await sendRequest('tools/call', {
278
+ name: 'update_tasks',
279
+ arguments: { file, updates: [{ line, checked }] },
280
+ });
281
+ showStatus('\u2705 Saved', 'ok');
282
+ } catch (err) {
283
+ // Revert on failure
284
+ checkboxEl.checked = !checked;
285
+ labelEl.className = 'task-label' + (!checked ? ' done' : '');
286
+ showStatus('\u274C Error: ' + (err.message || String(err)), 'err');
287
+ } finally {
288
+ checkboxEl.disabled = false;
289
+ }
290
+ }
291
+
292
+ function renderFileGroups(fileGroups) {
293
+ sectionsEl.innerHTML = '';
294
+
295
+ const withTasks = fileGroups.filter(fg =>
296
+ fg.sections.some(s => s.tasks.length > 0)
297
+ );
298
+
299
+ if (withTasks.length === 0) {
300
+ sectionsEl.innerHTML = '<p>No checklists found in any markdown file.</p>';
301
+ return;
302
+ }
303
+
304
+ for (const fg of withTasks) {
305
+ const groupEl = document.createElement('div');
306
+ groupEl.className = 'file-group';
307
+
308
+ const headerEl = document.createElement('div');
309
+ headerEl.className = 'file-header';
310
+ const iconSpan = document.createElement('span');
311
+ iconSpan.className = 'icon';
312
+ iconSpan.textContent = '\uD83D\uDCC4';
313
+ headerEl.appendChild(iconSpan);
314
+ headerEl.appendChild(document.createTextNode(' ' + fg.file));
315
+ groupEl.appendChild(headerEl);
316
+
317
+ const bodyEl = document.createElement('div');
318
+ bodyEl.className = 'file-body';
319
+
320
+ const visibleSections = fg.sections.filter(s => s.tasks.length > 0);
321
+
322
+ for (const section of visibleSections) {
323
+ const sectionEl = document.createElement('div');
324
+ sectionEl.className = 'section';
325
+
326
+ const titleEl = document.createElement('div');
327
+ titleEl.className = 'section-title';
328
+ titleEl.textContent = section.name;
329
+ sectionEl.appendChild(titleEl);
330
+
331
+ const listEl = document.createElement('ul');
332
+ listEl.className = 'task-list';
333
+
334
+ for (const task of section.tasks) {
335
+ const itemEl = document.createElement('li');
336
+ itemEl.className = 'task-item';
337
+
338
+ const checkboxEl = document.createElement('input');
339
+ checkboxEl.type = 'checkbox';
340
+ checkboxEl.checked = task.checked;
341
+ checkboxEl.id = 'task-' + fg.file + '-' + task.line;
342
+
343
+ const labelEl = document.createElement('label');
344
+ labelEl.htmlFor = checkboxEl.id;
345
+ labelEl.className = 'task-label' + (task.checked ? ' done' : '');
346
+ labelEl.textContent = task.text;
347
+
348
+ checkboxEl.addEventListener('change', () => {
349
+ const newChecked = checkboxEl.checked;
350
+ labelEl.className = 'task-label' + (newChecked ? ' done' : '');
351
+ autoSave(fg.file, task.line, newChecked, checkboxEl, labelEl);
352
+ });
353
+
354
+ itemEl.appendChild(checkboxEl);
355
+ itemEl.appendChild(labelEl);
356
+ listEl.appendChild(itemEl);
357
+ }
358
+
359
+ sectionEl.appendChild(listEl);
360
+ bodyEl.appendChild(sectionEl);
361
+ }
362
+
363
+ groupEl.appendChild(bodyEl);
364
+ sectionsEl.appendChild(groupEl);
365
+ }
366
+ }
367
+ </script>
368
+ </body>
369
+ </html>