@focus-pocus/mcp-server 0.1.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.
@@ -0,0 +1,46 @@
1
+ import { Config } from './config.js';
2
+ export declare class FocusPocusClient {
3
+ private baseUrl;
4
+ private token;
5
+ constructor(config: Config);
6
+ private request;
7
+ listTasks(params?: {
8
+ completed?: boolean;
9
+ goalId?: string;
10
+ collaboratorId?: string;
11
+ }): Promise<unknown>;
12
+ getTask(id: string): Promise<unknown>;
13
+ createTask(data: {
14
+ name: string;
15
+ description?: string;
16
+ priority?: string;
17
+ difficulty?: string;
18
+ dueDate?: string;
19
+ goalId?: string;
20
+ }): Promise<unknown>;
21
+ updateTask(id: string, updates: Array<{
22
+ field: string;
23
+ value: unknown;
24
+ }>): Promise<unknown>;
25
+ completeTask(id: string): Promise<unknown>;
26
+ searchTasks(query: string): Promise<unknown>;
27
+ getFollowUpItems(): Promise<unknown>;
28
+ findRelatedTasks(query: string, limit?: number): Promise<unknown>;
29
+ startWorking(taskId: string): Promise<unknown>;
30
+ stopWorking(taskId: string): Promise<unknown>;
31
+ getCurrentWork(): Promise<unknown>;
32
+ getWorkSummary(): Promise<unknown>;
33
+ stopAllWork(): Promise<unknown>;
34
+ listGoals(): Promise<unknown>;
35
+ getGoal(id: string): Promise<unknown>;
36
+ createGoal(data: {
37
+ name: string;
38
+ description?: string;
39
+ color?: string;
40
+ }): Promise<unknown>;
41
+ linkTaskToGoal(taskId: string, goalId: string): Promise<unknown>;
42
+ getInsights(): Promise<unknown>;
43
+ listCollaborators(): Promise<unknown>;
44
+ getGoalCollaborators(goalId: string): Promise<unknown>;
45
+ addComment(taskId: string, body: string): Promise<unknown>;
46
+ }
package/dist/client.js ADDED
@@ -0,0 +1,125 @@
1
+ export class FocusPocusClient {
2
+ baseUrl;
3
+ token;
4
+ constructor(config) {
5
+ this.baseUrl = config.apiUrl;
6
+ this.token = config.token;
7
+ }
8
+ async request(method, path, body) {
9
+ const url = `${this.baseUrl}/api/v1${path}`;
10
+ const headers = {
11
+ Authorization: `Bearer ${this.token}`,
12
+ 'Content-Type': 'application/json',
13
+ };
14
+ const res = await fetch(url, {
15
+ method,
16
+ headers,
17
+ body: body ? JSON.stringify(body) : undefined,
18
+ });
19
+ if (!res.ok) {
20
+ const text = await res.text();
21
+ let message;
22
+ try {
23
+ const json = JSON.parse(text);
24
+ message = json.error || json.message || text;
25
+ }
26
+ catch {
27
+ message = text;
28
+ }
29
+ const safeMessage = message.length > 200 ? message.substring(0, 200) + '...' : message;
30
+ throw new Error(`API error ${res.status}: ${safeMessage}`);
31
+ }
32
+ if (res.status === 204 || res.headers.get('content-length') === '0') {
33
+ return undefined;
34
+ }
35
+ return res.json();
36
+ }
37
+ // Tasks
38
+ async listTasks(params) {
39
+ const query = new URLSearchParams();
40
+ if (params?.completed !== undefined) {
41
+ query.set('completed', String(params.completed));
42
+ }
43
+ if (params?.goalId)
44
+ query.set('goalId', params.goalId);
45
+ if (params?.collaboratorId)
46
+ query.set('collaboratorId', params.collaboratorId);
47
+ const qs = query.toString();
48
+ return this.request('GET', `/tasks${qs ? `?${qs}` : ''}`);
49
+ }
50
+ async getTask(id) {
51
+ return this.request('GET', `/tasks/${id}`);
52
+ }
53
+ async createTask(data) {
54
+ return this.request('POST', '/tasks', data);
55
+ }
56
+ async updateTask(id, updates) {
57
+ return this.request('PUT', `/tasks/${id}`, { id, updates });
58
+ }
59
+ async completeTask(id) {
60
+ return this.updateTask(id, [
61
+ { field: 'completed', value: true },
62
+ ]);
63
+ }
64
+ async searchTasks(query) {
65
+ const params = new URLSearchParams({ q: query });
66
+ return this.request('GET', `/tasks/search?${params.toString()}`);
67
+ }
68
+ async getFollowUpItems() {
69
+ return this.request('GET', '/tasks/stale');
70
+ }
71
+ async findRelatedTasks(query, limit) {
72
+ const params = new URLSearchParams({
73
+ q: query,
74
+ excludeCompleted: 'true',
75
+ excludeExpired: 'true',
76
+ });
77
+ if (limit)
78
+ params.set('limit', String(limit));
79
+ return this.request('GET', `/tasks/search?${params.toString()}`);
80
+ }
81
+ // Work tracking
82
+ async startWorking(taskId) {
83
+ return this.request('POST', `/tasks/${taskId}/start-working`);
84
+ }
85
+ async stopWorking(taskId) {
86
+ return this.request('POST', `/tasks/${taskId}/stop-working`);
87
+ }
88
+ async getCurrentWork() {
89
+ return this.request('GET', '/current-work');
90
+ }
91
+ async getWorkSummary() {
92
+ return this.request('GET', '/current-work/summary');
93
+ }
94
+ async stopAllWork() {
95
+ return this.request('POST', '/current-work/stop-all');
96
+ }
97
+ // Goals
98
+ async listGoals() {
99
+ return this.request('GET', '/goals');
100
+ }
101
+ async getGoal(id) {
102
+ return this.request('GET', `/goals/${id}`);
103
+ }
104
+ async createGoal(data) {
105
+ return this.request('POST', '/goals', data);
106
+ }
107
+ async linkTaskToGoal(taskId, goalId) {
108
+ return this.request('POST', `/tasks/${taskId}/link-to-goal`, { goalId });
109
+ }
110
+ // Insights
111
+ async getInsights() {
112
+ return this.request('GET', '/insights');
113
+ }
114
+ // Collaborators
115
+ async listCollaborators() {
116
+ return this.request('GET', '/collaborators');
117
+ }
118
+ async getGoalCollaborators(goalId) {
119
+ return this.request('GET', `/goals/${goalId}/collaborators`);
120
+ }
121
+ // Comments
122
+ async addComment(taskId, body) {
123
+ return this.request('POST', `/tasks/${taskId}/comments`, { body });
124
+ }
125
+ }
@@ -0,0 +1,5 @@
1
+ export interface Config {
2
+ apiUrl: string;
3
+ token: string;
4
+ }
5
+ export declare function loadConfig(): Config;
package/dist/config.js ADDED
@@ -0,0 +1,22 @@
1
+ export function loadConfig() {
2
+ const apiUrl = process.env.FOCUS_POCUS_API_URL;
3
+ const token = process.env.FOCUS_POCUS_TOKEN;
4
+ if (!apiUrl) {
5
+ throw new Error('FOCUS_POCUS_API_URL environment variable is required');
6
+ }
7
+ if (!token) {
8
+ throw new Error('FOCUS_POCUS_TOKEN environment variable is required');
9
+ }
10
+ // Validate URL to prevent SSRF
11
+ let parsed;
12
+ try {
13
+ parsed = new URL(apiUrl);
14
+ }
15
+ catch {
16
+ throw new Error('FOCUS_POCUS_API_URL must be a valid URL');
17
+ }
18
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
19
+ throw new Error('FOCUS_POCUS_API_URL must use http or https protocol');
20
+ }
21
+ return { apiUrl: apiUrl.replace(/\/$/, ''), token };
22
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { loadConfig } from './config.js';
4
+ import { createServer } from './server.js';
5
+ async function main() {
6
+ const config = loadConfig();
7
+ const server = createServer(config);
8
+ const transport = new StdioServerTransport();
9
+ await server.connect(transport);
10
+ }
11
+ main().catch((error) => {
12
+ const message = error instanceof Error ? error.message : String(error);
13
+ const redacted = message.replace(/fp_pat_[a-f0-9]+/gi, 'fp_pat_[REDACTED]');
14
+ console.error('Fatal error:', redacted);
15
+ process.exit(1);
16
+ });
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { Config } from './config.js';
3
+ export declare function createServer(config: Config): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,43 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { FocusPocusClient } from './client.js';
3
+ import { registerTaskTools } from './tools/tasks.js';
4
+ import { registerWorkTrackingTools } from './tools/workTracking.js';
5
+ import { registerGoalTools } from './tools/goals.js';
6
+ import { registerCommentTools } from './tools/comments.js';
7
+ import { registerInsightTools } from './tools/insights.js';
8
+ import { registerCollaboratorTools } from './tools/collaborators.js';
9
+ const INSTRUCTIONS = `Focus Pocus is a task and goal management system. Use these tools to manage tasks, track work, and monitor progress.
10
+
11
+ ## Recommended workflow
12
+
13
+ 1. **Before creating a task**, use search_tasks to check for duplicates.
14
+ 2. **When starting work** on a task, call start_working to track it. Call stop_working when done.
15
+ 3. **Log progress** with add_comment as you hit milestones or make decisions.
16
+ 4. **Link tasks to goals** using the goalId parameter on create_task or update_task. Use list_goals to find goal IDs.
17
+ 5. **When work is finished**, call complete_task to mark it done.
18
+
19
+ ## Tool groups
20
+
21
+ - **Tasks**: create_task, update_task, complete_task, list_tasks, get_task, search_tasks
22
+ - **Work tracking**: start_working, stop_working, stop_all_work, get_current_work, get_work_summary
23
+ - **Goals**: list_goals, get_goal
24
+ - **Insights**: get_insights (productivity stats), get_follow_up_items (stale/abandoned tasks), find_related_tasks (semantic similarity)
25
+ - **Collaboration**: list_collaborators, get_goal_collaborators, list_tasks_for_collaborator
26
+ - **Comments**: add_comment
27
+ `;
28
+ export function createServer(config) {
29
+ const server = new McpServer({
30
+ name: 'focus-pocus',
31
+ version: '0.1.0',
32
+ }, {
33
+ instructions: INSTRUCTIONS,
34
+ });
35
+ const client = new FocusPocusClient(config);
36
+ registerTaskTools(server, client);
37
+ registerWorkTrackingTools(server, client);
38
+ registerGoalTools(server, client);
39
+ registerCommentTools(server, client);
40
+ registerInsightTools(server, client);
41
+ registerCollaboratorTools(server, client);
42
+ return server;
43
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { FocusPocusClient } from '../client.js';
3
+ export declare function registerCollaboratorTools(server: McpServer, client: FocusPocusClient): void;
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod';
2
+ export function registerCollaboratorTools(server, client) {
3
+ server.tool('list_collaborators', 'List all collaborators across your shared goals. Returns a deduplicated list of people you collaborate with (excludes yourself).', {}, async () => {
4
+ const result = await client.listCollaborators();
5
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
6
+ });
7
+ server.tool('get_goal_collaborators', 'List collaborators on a specific goal, including the goal owner.', { goalId: z.string().describe('Goal ID (UUID)') }, async ({ goalId }) => {
8
+ const result = await client.getGoalCollaborators(goalId);
9
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
10
+ });
11
+ server.tool('list_tasks_for_collaborator', 'List tasks created by or assigned to a specific collaborator. Useful for checking a teammate\'s workload or finding overdue items.', {
12
+ collaboratorId: z.string().describe('User ID (UUID) of the collaborator'),
13
+ completed: z.boolean().optional().describe('Filter by completion status. Defaults to false (active tasks).'),
14
+ goalId: z.string().optional().describe('Filter to tasks in a specific goal'),
15
+ }, async ({ collaboratorId, completed, goalId }) => {
16
+ const result = await client.listTasks({ collaboratorId, completed, goalId });
17
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
18
+ });
19
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { FocusPocusClient } from '../client.js';
3
+ export declare function registerCommentTools(server: McpServer, client: FocusPocusClient): void;
@@ -0,0 +1,10 @@
1
+ import { z } from 'zod';
2
+ export function registerCommentTools(server, client) {
3
+ server.tool('add_comment', 'Add a comment to a task. Useful for logging progress notes or context.', {
4
+ taskId: z.string().describe('Task ID (UUID) to comment on'),
5
+ body: z.string().describe('Comment text content'),
6
+ }, async ({ taskId, body }) => {
7
+ const result = await client.addComment(taskId, body);
8
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
9
+ });
10
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { FocusPocusClient } from '../client.js';
3
+ export declare function registerGoalTools(server: McpServer, client: FocusPocusClient): void;
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ export function registerGoalTools(server, client) {
3
+ server.tool('list_goals', 'List all goals. Useful for finding goal IDs to associate tasks with.', {}, async () => {
4
+ const result = await client.listGoals();
5
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
6
+ });
7
+ server.tool('get_goal', 'Get a specific goal by ID with full details including tasks, milestones, and collaborators.', { id: z.string().describe('Goal ID (UUID)') }, async ({ id }) => {
8
+ const result = await client.getGoal(id);
9
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
10
+ });
11
+ server.tool('create_goal', 'Create a new goal. Goals help organize tasks toward bigger outcomes.', {
12
+ name: z.string().describe('Goal name/title'),
13
+ description: z.string().optional().describe('Goal description'),
14
+ color: z.string().optional().describe('Goal color (hex code, e.g. "#4CAF50")'),
15
+ }, async ({ name, description, color }) => {
16
+ const result = await client.createGoal({ name, description, color });
17
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
18
+ });
19
+ server.tool('link_task_to_goal', 'Associate a task with a goal. Moves a task under a goal for organization.', {
20
+ taskId: z.string().describe('Task ID (UUID) to link'),
21
+ goalId: z.string().describe('Goal ID (UUID) to link the task to'),
22
+ }, async ({ taskId, goalId }) => {
23
+ const result = await client.linkTaskToGoal(taskId, goalId);
24
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
25
+ });
26
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { FocusPocusClient } from '../client.js';
3
+ export declare function registerInsightTools(server: McpServer, client: FocusPocusClient): void;
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+ export function registerInsightTools(server, client) {
3
+ server.tool('get_follow_up_items', 'Get tasks that need attention: drafts, transferred tasks, never-started tasks (7+ days old), and abandoned tasks (14+ days since last work).', {}, async () => {
4
+ const result = await client.getFollowUpItems();
5
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
6
+ });
7
+ server.tool('get_insights', 'Get productivity insights: task counts, overdue items, completed today, goal alignment stats, and high priority task count.', {}, async () => {
8
+ const result = await client.getInsights();
9
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
10
+ });
11
+ server.tool('find_related_tasks', 'Find tasks semantically related to a concept or topic. Uses embedding-based similarity search to discover related work.', {
12
+ concept: z.string().describe('Concept or topic to find related tasks for (e.g., "marketing", "bug fixes", "Q1 planning")'),
13
+ limit: z.number().min(1).max(20).optional().describe('Maximum results to return (default: 10)'),
14
+ }, async ({ concept, limit }) => {
15
+ const result = await client.findRelatedTasks(concept, limit);
16
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
17
+ });
18
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { FocusPocusClient } from '../client.js';
3
+ export declare function registerTaskTools(server: McpServer, client: FocusPocusClient): void;
@@ -0,0 +1,53 @@
1
+ import { z } from 'zod';
2
+ export function registerTaskTools(server, client) {
3
+ server.tool('list_tasks', 'List tasks from Focus-Pocus. Returns active (incomplete) tasks by default.', {
4
+ completed: z.boolean().optional().describe('Filter by completion status. Defaults to false (active tasks).'),
5
+ goalId: z.string().optional().describe('Filter by goal ID'),
6
+ }, async ({ completed, goalId }) => {
7
+ const result = await client.listTasks({ completed, goalId });
8
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
9
+ });
10
+ server.tool('get_task', 'Get a specific task by ID with full details.', { id: z.string().describe('Task ID (UUID)') }, async ({ id }) => {
11
+ const result = await client.getTask(id);
12
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
13
+ });
14
+ server.tool('create_task', 'Create a new task in Focus-Pocus. The task will be tagged as created via MCP.', {
15
+ name: z.string().describe('Task name/title'),
16
+ description: z.string().optional().describe('Task description'),
17
+ priority: z.enum(['No priority', 'Low', 'Medium', 'High']).optional().describe('Task priority'),
18
+ difficulty: z.enum(['Not sure', 'Small', 'Medium', 'Large']).optional().describe('Task difficulty/size'),
19
+ dueDate: z.string().optional().describe('Due date in ISO 8601 format'),
20
+ goalId: z.string().optional().describe('Goal ID to associate with'),
21
+ }, async ({ name, description, priority, difficulty, dueDate, goalId }) => {
22
+ const result = await client.createTask({
23
+ name, description, priority, difficulty, dueDate, goalId,
24
+ });
25
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
26
+ });
27
+ server.tool('update_task', 'Update an existing task. Provide only the fields you want to change.', {
28
+ id: z.string().describe('Task ID (UUID)'),
29
+ name: z.string().optional().describe('New task name'),
30
+ description: z.string().optional().describe('New description'),
31
+ priority: z.enum(['No priority', 'Low', 'Medium', 'High']).optional().describe('New priority'),
32
+ difficulty: z.enum(['Not sure', 'Small', 'Medium', 'Large']).optional().describe('New difficulty'),
33
+ dueDate: z.string().nullable().optional().describe('New due date (ISO 8601) or null to clear'),
34
+ goalId: z.string().nullable().optional().describe('New goal ID or null to unlink'),
35
+ }, async ({ id, ...fields }) => {
36
+ const updates = Object.entries(fields)
37
+ .filter(([, v]) => v !== undefined)
38
+ .map(([field, value]) => ({ field, value }));
39
+ if (updates.length === 0) {
40
+ return { content: [{ type: 'text', text: 'No fields to update' }], isError: true };
41
+ }
42
+ const result = await client.updateTask(id, updates);
43
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
44
+ });
45
+ server.tool('complete_task', 'Mark a task as completed.', { id: z.string().describe('Task ID (UUID)') }, async ({ id }) => {
46
+ const result = await client.completeTask(id);
47
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
48
+ });
49
+ server.tool('search_tasks', 'Search tasks by text query using semantic search.', { query: z.string().describe('Search query text') }, async ({ query }) => {
50
+ const result = await client.searchTasks(query);
51
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
52
+ });
53
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { FocusPocusClient } from '../client.js';
3
+ export declare function registerWorkTrackingTools(server: McpServer, client: FocusPocusClient): void;
@@ -0,0 +1,23 @@
1
+ import { z } from 'zod';
2
+ export function registerWorkTrackingTools(server, client) {
3
+ server.tool('start_working', 'Start tracking work on a task. Marks the task as currently being worked on.', { taskId: z.string().describe('Task ID (UUID) to start working on') }, async ({ taskId }) => {
4
+ const result = await client.startWorking(taskId);
5
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
6
+ });
7
+ server.tool('stop_working', 'Stop tracking work on a task.', { taskId: z.string().describe('Task ID (UUID) to stop working on') }, async ({ taskId }) => {
8
+ const result = await client.stopWorking(taskId);
9
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
10
+ });
11
+ server.tool('get_current_work', 'Get all tasks currently being worked on.', {}, async () => {
12
+ const result = await client.getCurrentWork();
13
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
14
+ });
15
+ server.tool('get_work_summary', 'Get a summary of work activity including currently working tasks and recent work.', {}, async () => {
16
+ const result = await client.getWorkSummary();
17
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
18
+ });
19
+ server.tool('stop_all_work', 'Stop tracking work on all tasks at once.', {}, async () => {
20
+ const result = await client.stopAllWork();
21
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
22
+ });
23
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@focus-pocus/mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Focus-Pocus task management",
5
+ "type": "module",
6
+ "bin": {
7
+ "focus-pocus-mcp": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsc --watch",
12
+ "typecheck": "tsc --noEmit",
13
+ "lint": "ESLINT_USE_FLAT_CONFIG=false eslint 'src/**/*.ts' --fix"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "dependencies": {
23
+ "@modelcontextprotocol/sdk": "^1.12.1",
24
+ "zod": "^3.24.0"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^22.0.0",
28
+ "@typescript-eslint/eslint-plugin": "^7.0.0",
29
+ "@typescript-eslint/parser": "^7.0.0",
30
+ "eslint": "^8.57.0",
31
+ "typescript": "^5.7.0"
32
+ }
33
+ }