@dalmasonto/taskflow-mcp 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,184 @@
1
+ # TaskFlow MCP Server
2
+
3
+ A local-first task and time tracking system exposed as [MCP](https://modelcontextprotocol.io) tools. Any MCP-compatible AI agent can manage projects, tasks, timers, analytics, and notifications through this server.
4
+
5
+ ## Quickstart
6
+
7
+ ### 1. Install and build
8
+
9
+ ```bash
10
+ cd mcp-server
11
+ npm install
12
+ npm run build
13
+ ```
14
+
15
+ ### 2. Configure your MCP client
16
+
17
+ Add a `.mcp.json` file to your project root (or wherever your MCP client reads config):
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "taskflow": {
23
+ "command": "node",
24
+ "args": ["/absolute/path/to/mcp-server/dist/index.js"]
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ Replace the path with the absolute path to your built `dist/index.js`.
31
+
32
+ ### 3. Auto-allow permissions (Claude Code)
33
+
34
+ By default, Claude Code will prompt you to approve each MCP tool call. To allow all TaskFlow tools without prompts, add this to `.claude/settings.local.json`:
35
+
36
+ ```json
37
+ {
38
+ "permissions": {
39
+ "allow": [
40
+ "mcp__taskflow__*"
41
+ ]
42
+ },
43
+ "enableAllProjectMcpServers": true
44
+ }
45
+ ```
46
+
47
+ The `mcp__taskflow__*` wildcard matches every tool exposed by this server.
48
+
49
+ For other MCP clients (Cursor, Windsurf, etc.), check their docs for permission/auto-approve configuration.
50
+
51
+ ## Agent Integration Guide
52
+
53
+ ### How agents discover TaskFlow
54
+
55
+ TaskFlow uses two layers to guide agent behavior:
56
+
57
+ **Layer 1: Tool descriptions (passive discovery)**
58
+ Every tool has a description that hints at when and why to use it. MCP clients surface these descriptions when the agent connects, so the agent learns the workflow organically. For example, `start_timer` says "Call this before beginning work on any task to track focused time."
59
+
60
+ **Layer 2: `get_agent_instructions` tool (active onboarding)**
61
+ This is the key tool. Its description says **"Call this at the start of every conversation."** When called, it returns:
62
+
63
+ - A role description for the agent
64
+ - A startup checklist (list projects, check in-progress tasks, check notifications)
65
+ - Behavioral rules (when to start/stop timers, how to handle blockers, etc.)
66
+ - Live context (current project count, in-progress tasks, blocked tasks, unread notifications)
67
+ - The full task status workflow with valid transitions
68
+
69
+ Any well-behaved agent will call this tool when it sees the description, without needing explicit user instructions.
70
+
71
+ ### Making it automatic (recommended)
72
+
73
+ For the most reliable experience, add one line to your project's `CLAUDE.md` (or equivalent agent config):
74
+
75
+ ```markdown
76
+ ## MCP Integration
77
+ At the start of each conversation, call the `get_agent_instructions` tool from the taskflow MCP server to understand your task management workflow.
78
+ ```
79
+
80
+ This guarantees the agent calls the instruction tool on every conversation start. Without this, the agent will still likely discover the tool via its description, but the CLAUDE.md line makes it deterministic.
81
+
82
+ ### Example conversation flow
83
+
84
+ Here's what a conversation looks like when the agent is properly connected:
85
+
86
+ ```
87
+ Agent connects → sees get_agent_instructions in tool list → calls it
88
+
89
+ Gets instructions + live context (3 projects, 2 tasks in progress, 1 blocked)
90
+
91
+ Calls list_tasks(status="in_progress") → sees "Build dashboard page" is active
92
+
93
+ User: "Let's work on the dashboard"
94
+
95
+ Agent: calls get_task(id=5) → reads description for implementation details
96
+ Agent: calls start_timer(task_id=5) → time tracking begins
97
+
98
+ Agent implements the feature, referencing task description for acceptance criteria
99
+
100
+ Agent: calls stop_timer(task_id=5, final_status="done")
101
+ Agent: checks if any blocked tasks depended on task 5
102
+ ```
103
+
104
+ ### Strategies for proactive agent behavior
105
+
106
+ The `get_agent_instructions` tool tells agents to:
107
+
108
+ 1. **Check tasks before coding** — before starting work, search for a matching task and start its timer
109
+ 2. **Track time automatically** — start_timer when beginning work, pause_timer on context switches, stop_timer when done
110
+ 3. **Surface blockers** — if stuck, update the task to "blocked" with context in the description
111
+ 4. **Suggest next work** — when the user asks "what should I work on?", surface high-priority unblocked tasks
112
+ 5. **Stay in sync** — create tasks for new work items to keep the tracker up to date
113
+ 6. **Read descriptions** — task descriptions contain implementation details and acceptance criteria
114
+
115
+ ## Available Tools
116
+
117
+ ### Agent
118
+ | Tool | Description |
119
+ |------|-------------|
120
+ | `get_agent_instructions` | Returns onboarding instructions and live context for AI agents. **Call first.** |
121
+
122
+ ### Tasks
123
+ | Tool | Description |
124
+ |------|-------------|
125
+ | `create_task` | Create a task with dependencies, tags, links, and time estimates |
126
+ | `list_tasks` | List tasks with filters (status, project, priority, tag) |
127
+ | `get_task` | Get a task by ID with time tracking info |
128
+ | `update_task` | Update task fields |
129
+ | `update_task_status` | Change status with transition validation |
130
+ | `delete_task` | Delete a task by ID |
131
+ | `bulk_create_tasks` | Create multiple tasks in a single transaction |
132
+ | `search_tasks` | Full-text search by title or description |
133
+
134
+ ### Projects
135
+ | Tool | Description |
136
+ |------|-------------|
137
+ | `create_project` | Create a project (active_project or project_idea) |
138
+ | `list_projects` | List all projects with task counts |
139
+ | `get_project` | Get a project with all its tasks |
140
+ | `update_project` | Update project fields |
141
+ | `delete_project` | Delete a project (tasks are unlinked, not deleted) |
142
+
143
+ ### Timer
144
+ | Tool | Description |
145
+ |------|-------------|
146
+ | `start_timer` | Start a timer session (task transitions to in_progress) |
147
+ | `pause_timer` | Pause the active session (task transitions to paused) |
148
+ | `stop_timer` | Stop timer with final status (done/partial_done/blocked) |
149
+ | `list_sessions` | List sessions with optional date range filter |
150
+
151
+ ### Analytics
152
+ | Tool | Description |
153
+ |------|-------------|
154
+ | `get_analytics` | Summary: focused time, completion rates, status distribution, time per project |
155
+ | `get_timeline` | Focused time grouped by day or week |
156
+
157
+ ### Activity
158
+ | Tool | Description |
159
+ |------|-------------|
160
+ | `get_activity_log` | Recent activity: completions, timer events, status changes |
161
+ | `clear_activity_log` | Delete all activity log entries |
162
+
163
+ ### Notifications
164
+ | Tool | Description |
165
+ |------|-------------|
166
+ | `list_notifications` | List notifications (filter by unread) |
167
+ | `mark_notification_read` | Mark a single notification as read |
168
+ | `mark_all_notifications_read` | Mark all as read |
169
+ | `clear_notifications` | Delete all notifications |
170
+
171
+ ### Settings
172
+ | Tool | Description |
173
+ |------|-------------|
174
+ | `get_setting` | Get a setting by key (returns default if not set) |
175
+ | `update_setting` | Update or create a setting |
176
+
177
+ ## Configuration
178
+
179
+ | Setting | Default | Description |
180
+ |---------|---------|-------------|
181
+ | `TASKFLOW_SSE_PORT` | `3456` | Port for the SSE broadcast server |
182
+ | Database location | `~/.taskflow/taskflow.db` | SQLite database with WAL mode |
183
+
184
+ The SSE server at `http://localhost:3456/events` broadcasts real-time changes to connected UI clients. The `/sync` endpoint returns a full data dump for initial sync.
package/dist/db.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import Database from 'better-sqlite3';
2
+ export declare function resolvePath(p: string): string;
3
+ export declare function getDb(): Database.Database;
4
+ export declare function initDb(path: string): Database.Database;
5
+ export declare function closeDb(): void;
package/dist/db.js ADDED
@@ -0,0 +1,109 @@
1
+ import Database from 'better-sqlite3';
2
+ import { mkdirSync } from 'fs';
3
+ import { dirname, resolve } from 'path';
4
+ import { homedir } from 'os';
5
+ const DEFAULT_DB_PATH = '~/.taskflow/taskflow.db';
6
+ let db = null;
7
+ // Expands ~/path to $HOME/path. Only handles ~/ prefix, not ~user/ paths.
8
+ export function resolvePath(p) {
9
+ if (p.startsWith('~/') || p === '~') {
10
+ return resolve(homedir(), p.slice(2));
11
+ }
12
+ return resolve(p);
13
+ }
14
+ export function getDb() {
15
+ if (db)
16
+ return db;
17
+ return initDb(process.env.TASKFLOW_DB_PATH || DEFAULT_DB_PATH);
18
+ }
19
+ // For testing: initialize with a specific path (use ':memory:' for tests)
20
+ export function initDb(path) {
21
+ if (db) {
22
+ db.close();
23
+ db = null;
24
+ }
25
+ const dbPath = path === ':memory:' ? ':memory:' : resolvePath(path);
26
+ if (path !== ':memory:')
27
+ mkdirSync(dirname(dbPath), { recursive: true });
28
+ db = new Database(dbPath);
29
+ db.pragma('journal_mode = WAL');
30
+ db.pragma('foreign_keys = ON');
31
+ db.pragma('busy_timeout = 5000');
32
+ initSchema(db);
33
+ return db;
34
+ }
35
+ function initSchema(db) {
36
+ db.exec(`
37
+ CREATE TABLE IF NOT EXISTS projects (
38
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ name TEXT NOT NULL,
40
+ color TEXT NOT NULL DEFAULT '#de8eff',
41
+ type TEXT NOT NULL DEFAULT 'active_project',
42
+ description TEXT,
43
+ created_at TEXT NOT NULL,
44
+ updated_at TEXT NOT NULL
45
+ );
46
+
47
+ CREATE TABLE IF NOT EXISTS tasks (
48
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
49
+ title TEXT NOT NULL,
50
+ description TEXT,
51
+ status TEXT NOT NULL DEFAULT 'not_started',
52
+ priority TEXT NOT NULL DEFAULT 'medium',
53
+ project_id INTEGER REFERENCES projects(id) ON DELETE SET NULL,
54
+ dependencies TEXT NOT NULL DEFAULT '[]',
55
+ links TEXT NOT NULL DEFAULT '[]',
56
+ tags TEXT NOT NULL DEFAULT '[]',
57
+ due_date TEXT,
58
+ estimated_time INTEGER,
59
+ created_at TEXT NOT NULL,
60
+ updated_at TEXT NOT NULL
61
+ );
62
+
63
+ CREATE TABLE IF NOT EXISTS sessions (
64
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
65
+ task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
66
+ start TEXT NOT NULL,
67
+ end TEXT
68
+ );
69
+
70
+ CREATE TABLE IF NOT EXISTS activity_logs (
71
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
72
+ action TEXT NOT NULL,
73
+ title TEXT NOT NULL,
74
+ detail TEXT,
75
+ entity_type TEXT,
76
+ entity_id INTEGER,
77
+ created_at TEXT NOT NULL
78
+ );
79
+
80
+ CREATE TABLE IF NOT EXISTS notifications (
81
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
82
+ title TEXT NOT NULL,
83
+ message TEXT NOT NULL,
84
+ type TEXT NOT NULL DEFAULT 'info',
85
+ read INTEGER NOT NULL DEFAULT 0,
86
+ created_at TEXT NOT NULL
87
+ );
88
+
89
+ CREATE TABLE IF NOT EXISTS settings (
90
+ key TEXT PRIMARY KEY,
91
+ value TEXT NOT NULL
92
+ );
93
+
94
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
95
+ CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON tasks(project_id);
96
+ CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority);
97
+ CREATE INDEX IF NOT EXISTS idx_sessions_task_id ON sessions(task_id);
98
+ CREATE INDEX IF NOT EXISTS idx_activity_logs_created_at ON activity_logs(created_at);
99
+ CREATE INDEX IF NOT EXISTS idx_activity_logs_action ON activity_logs(action);
100
+ CREATE INDEX IF NOT EXISTS idx_notifications_read ON notifications(read);
101
+ CREATE INDEX IF NOT EXISTS idx_notifications_created_at ON notifications(created_at);
102
+ `);
103
+ }
104
+ export function closeDb() {
105
+ if (db) {
106
+ db.close();
107
+ db = null;
108
+ }
109
+ }
@@ -0,0 +1,21 @@
1
+ import type { ActivityAction, ErrorCode } from './types.js';
2
+ export declare function logActivity(action: ActivityAction, title: string, options?: {
3
+ detail?: string;
4
+ entityType?: string;
5
+ entityId?: number;
6
+ }): void;
7
+ export declare function errorResponse(error: string, code: ErrorCode): {
8
+ isError: true;
9
+ content: {
10
+ type: "text";
11
+ text: string;
12
+ }[];
13
+ };
14
+ export declare function successResponse(data: unknown): {
15
+ content: {
16
+ type: "text";
17
+ text: string;
18
+ }[];
19
+ };
20
+ export declare function now(): string;
21
+ export declare function broadcastChange(entity: string, action: string, payload: unknown): void;
@@ -0,0 +1,27 @@
1
+ import { getDb } from './db.js';
2
+ import { broadcast } from './sse.js';
3
+ export function logActivity(action, title, options) {
4
+ const db = getDb();
5
+ const ts = new Date().toISOString();
6
+ const result = db.prepare(`INSERT INTO activity_logs (action, title, detail, entity_type, entity_id, created_at)
7
+ VALUES (?, ?, ?, ?, ?, ?)`).run(action, title, options?.detail ?? null, options?.entityType ?? null, options?.entityId ?? null, ts);
8
+ const entry = db.prepare('SELECT * FROM activity_logs WHERE id = ?').get(result.lastInsertRowid);
9
+ broadcast('activity_logged', { entity: 'activity', action: 'activity_logged', payload: entry });
10
+ }
11
+ export function errorResponse(error, code) {
12
+ return {
13
+ isError: true,
14
+ content: [{ type: 'text', text: JSON.stringify({ error, code }) }],
15
+ };
16
+ }
17
+ export function successResponse(data) {
18
+ return {
19
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
20
+ };
21
+ }
22
+ export function now() {
23
+ return new Date().toISOString();
24
+ }
25
+ export function broadcastChange(entity, action, payload) {
26
+ broadcast(action, { entity, action, payload });
27
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ import { startSSEServer } from './sse.js';
3
+ import { getDb } from './db.js';
4
+ import { broadcast } from './sse.js';
5
+ const httpOnly = process.argv.includes('--http-only');
6
+ // Close any orphaned sessions left from a previous crash
7
+ // If the server died while sessions were active, they'll have no `end` timestamp
8
+ function cleanupOrphanedSessions() {
9
+ const db = getDb();
10
+ const now = new Date().toISOString();
11
+ const orphaned = db.prepare('SELECT * FROM sessions WHERE end IS NULL').all();
12
+ if (orphaned.length === 0)
13
+ return;
14
+ db.prepare('UPDATE sessions SET end = ? WHERE end IS NULL').run(now);
15
+ // Set orphaned in_progress tasks back to paused
16
+ const taskIds = [...new Set(orphaned.map(s => s.task_id))];
17
+ for (const taskId of taskIds) {
18
+ const task = db.prepare('SELECT status FROM tasks WHERE id = ?').get(taskId);
19
+ if (task?.status === 'in_progress') {
20
+ db.prepare("UPDATE tasks SET status = 'paused', updated_at = ? WHERE id = ?").run(now, taskId);
21
+ broadcast('task_updated', { entity: 'task', action: 'task_status_changed', payload: db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId) });
22
+ }
23
+ }
24
+ }
25
+ cleanupOrphanedSessions();
26
+ // Always start the HTTP/SSE server
27
+ startSSEServer();
28
+ // Only start MCP stdio transport when not in http-only mode
29
+ if (!httpOnly) {
30
+ const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js');
31
+ const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
32
+ const { registerTaskTools } = await import('./tools/tasks.js');
33
+ const { registerProjectTools } = await import('./tools/projects.js');
34
+ const { registerTimerTools } = await import('./tools/timer.js');
35
+ const { registerAnalyticsTools } = await import('./tools/analytics.js');
36
+ const { registerActivityTools } = await import('./tools/activity.js');
37
+ const { registerNotificationTools } = await import('./tools/notifications.js');
38
+ const { registerSettingsTools } = await import('./tools/settings.js');
39
+ const { registerAgentTools } = await import('./tools/agent.js');
40
+ const server = new McpServer({
41
+ name: 'taskflow',
42
+ version: '1.0.0',
43
+ });
44
+ registerAgentTools(server);
45
+ registerTaskTools(server);
46
+ registerProjectTools(server);
47
+ registerTimerTools(server);
48
+ registerAnalyticsTools(server);
49
+ registerActivityTools(server);
50
+ registerNotificationTools(server);
51
+ registerSettingsTools(server);
52
+ const transport = new StdioServerTransport();
53
+ await server.connect(transport);
54
+ }
package/dist/sse.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ export declare function startSSEServer(): void;
2
+ export declare function markSSEActive(): void;
3
+ /**
4
+ * Broadcast an SSE event. If this process owns the SSE server, send directly.
5
+ * Otherwise, relay via HTTP to the process that does (sidecar on port 3456).
6
+ */
7
+ export declare function broadcast(event: string, data: object): void;