refactor: migrate to monorepo structure with Docker, PocketBase, and e2e tests
- Reorganize into apps/, packages/, docs/, e2e/, pocketbase/ directories - Add Dockerfiles for web, worker, and PocketBase services - Add docker-compose.yml for local orchestration - Add turbo.json for monorepo task management - Add Playwright e2e test infrastructure - Add PocketBase backend with migrations - Remove Vite/Next.js/ESLint/PostCSS config files - Update package.json with workspace dependencies - Add .env.example and .dockerignore
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import { z } from 'zod';
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { createAdminClient } from '@/lib/pocketbase';
|
||||
|
||||
const pb = createAdminClient();
|
||||
|
||||
function textContent(text: string) {
|
||||
return { content: [{ type: 'text' as const, text }] };
|
||||
}
|
||||
|
||||
export function registerAgentTools(server: McpServer) {
|
||||
server.tool('create_agent', 'Create a new agent', {
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
domain: z.string(),
|
||||
status: z.enum(['active', 'disabled']).optional(),
|
||||
permission_tier: z.enum(['full_access', 'read_only', 'content_creator', 'task_manager', 'custom']).optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const agent = await pb.collection('agents').create({
|
||||
name: args.name,
|
||||
description: args.description || '',
|
||||
domain: args.domain,
|
||||
status: args.status || 'active',
|
||||
permission_tier: args.permission_tier || 'read_only',
|
||||
tags: args.tags || [],
|
||||
api_key: crypto.randomUUID(),
|
||||
});
|
||||
return textContent(JSON.stringify({ success: true, agent }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('get_agent', 'Get an agent by ID', {
|
||||
agent_id: z.string(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const agent = await pb.collection('agents').getOne(args.agent_id);
|
||||
return textContent(JSON.stringify({ success: true, agent }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('list_agents', 'List agents with optional filters', {
|
||||
status: z.enum(['active', 'disabled']).optional(),
|
||||
domain: z.string().optional(),
|
||||
limit: z.number().optional(),
|
||||
offset: z.number().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const filters: string[] = [];
|
||||
if (args.status) filters.push(`status = "${args.status}"`);
|
||||
if (args.domain) filters.push(`domain = "${args.domain}"`);
|
||||
|
||||
const page = Math.floor((args.offset || 0) / (args.limit || 20)) + 1;
|
||||
const result = await pb.collection('agents').getList(page, args.limit || 20, {
|
||||
filter: filters.join(' && ') || '',
|
||||
sort: '-created',
|
||||
});
|
||||
return textContent(JSON.stringify({
|
||||
success: true,
|
||||
agents: result.items,
|
||||
total: result.totalItems,
|
||||
page: result.page,
|
||||
limit: args.limit || 20,
|
||||
}));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('update_agent', 'Update an existing agent', {
|
||||
agent_id: z.string(),
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
status: z.enum(['active', 'disabled']).optional(),
|
||||
permission_tier: z.enum(['full_access', 'read_only', 'content_creator', 'task_manager', 'custom']).optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const { agent_id, ...updateData } = args;
|
||||
const cleaned: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(updateData)) {
|
||||
if (value !== undefined) cleaned[key] = value;
|
||||
}
|
||||
const agent = await pb.collection('agents').update(agent_id, cleaned);
|
||||
return textContent(JSON.stringify({ success: true, agent }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('delete_agent', 'Delete an agent', {
|
||||
agent_id: z.string(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
await pb.collection('agents').delete(args.agent_id);
|
||||
return textContent(JSON.stringify({ success: true, deleted: args.agent_id }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import { z } from 'zod';
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { createAdminClient } from '@/lib/pocketbase';
|
||||
|
||||
const pb = createAdminClient();
|
||||
|
||||
function textContent(text: string) {
|
||||
return { content: [{ type: 'text' as const, text }] };
|
||||
}
|
||||
|
||||
export function registerAnalyticsTools(server: McpServer) {
|
||||
server.tool('get_analytics', 'Get analytics data for a given period', {
|
||||
period_days: z.number().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const days = args.period_days || 30;
|
||||
const startDate = new Date();
|
||||
startDate.setDate(startDate.getDate() - days);
|
||||
const startStr = startDate.toISOString();
|
||||
|
||||
// Task completion rate
|
||||
const tasks = await pb.collection('tasks').getFullList({
|
||||
filter: `created >= "${startStr}"`,
|
||||
});
|
||||
const completedTasks = tasks.filter((t: Record<string, unknown>) => t.status === 'done');
|
||||
const taskCompletionRate = tasks.length > 0
|
||||
? Math.round((completedTasks.length / tasks.length) * 100)
|
||||
: 0;
|
||||
|
||||
// Habit consistency
|
||||
const habits = await pb.collection('habits').getFullList();
|
||||
const habitLogs = await pb.collection('habit_logs').getFullList({
|
||||
filter: `logged_at >= "${startStr}"`,
|
||||
});
|
||||
const habitConsistency = habits.length > 0
|
||||
? Math.round((habitLogs.length / (habits.length * days)) * 100)
|
||||
: 0;
|
||||
|
||||
// Time tracked
|
||||
const timeEntries = await pb.collection('task_time_entries').getFullList({
|
||||
filter: `started_at >= "${startStr}"`,
|
||||
});
|
||||
const totalTimeMinutes = timeEntries.reduce(
|
||||
(sum: number, e: Record<string, unknown>) => sum + ((e.duration_minutes as number) || 0),
|
||||
0,
|
||||
);
|
||||
|
||||
// Active streaks
|
||||
const activeStreaks = habits.filter(
|
||||
(h: Record<string, unknown>) => ((h.current_streak as number) || 0) > 0,
|
||||
);
|
||||
const bestStreak = Math.max(
|
||||
...habits.map((h: Record<string, unknown>) => (h.best_streak as number) || 0),
|
||||
0,
|
||||
);
|
||||
|
||||
return textContent(JSON.stringify({
|
||||
success: true,
|
||||
analytics: {
|
||||
taskCompletionRate,
|
||||
habitConsistency,
|
||||
totalTimeMinutes,
|
||||
activeStreaks: activeStreaks.length,
|
||||
bestStreak,
|
||||
period: days,
|
||||
},
|
||||
}));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('get_time_summary', 'Get aggregated time tracking summary', {
|
||||
start_date: z.string().optional(),
|
||||
end_date: z.string().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const startDate = args.start_date || new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
||||
const endDate = args.end_date || new Date().toISOString();
|
||||
|
||||
const entries = await pb.collection('task_time_entries').getFullList({
|
||||
filter: `started_at >= "${startDate}" && started_at <= "${endDate}"`,
|
||||
});
|
||||
|
||||
const byDomain: Record<string, number> = {};
|
||||
const byProject: Record<string, number> = {};
|
||||
const byTag: Record<string, number> = {};
|
||||
let totalMinutes = 0;
|
||||
|
||||
for (const entry of entries) {
|
||||
const duration = (entry as Record<string, unknown>).duration_minutes as number || 0;
|
||||
totalMinutes += duration;
|
||||
|
||||
const taskId = (entry as Record<string, unknown>).task_id as string;
|
||||
if (taskId) {
|
||||
try {
|
||||
const task = await pb.collection('tasks').getOne(taskId);
|
||||
const taskRecord = task as unknown as Record<string, unknown>;
|
||||
const domain = taskRecord.domain as string;
|
||||
if (domain) {
|
||||
byDomain[domain] = (byDomain[domain] || 0) + duration;
|
||||
}
|
||||
|
||||
const projectId = taskRecord.project_id as string | undefined;
|
||||
if (projectId) {
|
||||
byProject[projectId] = (byProject[projectId] || 0) + duration;
|
||||
}
|
||||
|
||||
const tags = (taskRecord.tags as string[]) || [];
|
||||
for (const tag of tags) {
|
||||
byTag[tag] = (byTag[tag] || 0) + duration;
|
||||
}
|
||||
} catch {
|
||||
// Skip if task not found
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return textContent(JSON.stringify({
|
||||
success: true,
|
||||
time_summary: {
|
||||
totalMinutes,
|
||||
byDomain,
|
||||
byProject,
|
||||
byTag,
|
||||
startDate,
|
||||
endDate,
|
||||
},
|
||||
}));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('search', 'Search across tasks, habits, projects, notes, and reports', {
|
||||
query: z.string(),
|
||||
types: z.array(z.string()).optional(),
|
||||
limit: z.number().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const types = args.types || ['tasks', 'habits', 'projects', 'notes', 'reports'];
|
||||
const limit = args.limit || 10;
|
||||
const safeQuery = args.query.replace(/"/g, '\\"');
|
||||
const results: Array<{ type: string; items: unknown[] }> = [];
|
||||
|
||||
for (const type of types) {
|
||||
try {
|
||||
let filter = '';
|
||||
switch (type) {
|
||||
case 'tasks':
|
||||
filter = `title ~ "${safeQuery}" || description ~ "${safeQuery}"`;
|
||||
break;
|
||||
case 'habits':
|
||||
filter = `name ~ "${safeQuery}" || description ~ "${safeQuery}"`;
|
||||
break;
|
||||
case 'projects':
|
||||
filter = `name ~ "${safeQuery}" || description ~ "${safeQuery}"`;
|
||||
break;
|
||||
case 'notes':
|
||||
filter = `title ~ "${safeQuery}" || content ~ "${safeQuery}"`;
|
||||
break;
|
||||
case 'reports':
|
||||
filter = `title ~ "${safeQuery}" || summary ~ "${safeQuery}"`;
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
const items = await pb.collection(type).getList(1, limit, { filter });
|
||||
results.push({ type, items: items.items });
|
||||
} catch {
|
||||
// Skip collections that fail
|
||||
}
|
||||
}
|
||||
|
||||
return textContent(JSON.stringify({ success: true, results }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('get_agent_activity', 'Get recent agent activity', {
|
||||
limit: z.number().optional(),
|
||||
offset: z.number().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const page = Math.floor((args.offset || 0) / (args.limit || 20)) + 1;
|
||||
const result = await pb.collection('agent_activity').getList(page, args.limit || 20, {
|
||||
sort: '-created',
|
||||
});
|
||||
return textContent(JSON.stringify({
|
||||
success: true,
|
||||
activity: result.items,
|
||||
total: result.totalItems,
|
||||
page: result.page,
|
||||
limit: args.limit || 20,
|
||||
}));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { z } from 'zod';
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { createAdminClient } from '@/lib/pocketbase';
|
||||
|
||||
const pb = createAdminClient();
|
||||
|
||||
function textContent(text: string) {
|
||||
return { content: [{ type: 'text' as const, text }] };
|
||||
}
|
||||
|
||||
export function registerDomainTools(server: McpServer) {
|
||||
server.tool('create_domain', 'Create a new domain', {
|
||||
name: z.string(),
|
||||
color: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
sort_order: z.number().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const domain = await pb.collection('domains').create({
|
||||
name: args.name,
|
||||
color: args.color || null,
|
||||
icon: args.icon || null,
|
||||
sort_order: args.sort_order || 0,
|
||||
});
|
||||
return textContent(JSON.stringify({ success: true, domain }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('get_domain', 'Get a domain by ID', {
|
||||
domain_id: z.string(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const domain = await pb.collection('domains').getOne(args.domain_id);
|
||||
return textContent(JSON.stringify({ success: true, domain }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('list_domains', 'List all domains', {
|
||||
limit: z.number().optional(),
|
||||
offset: z.number().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const page = Math.floor((args.offset || 0) / (args.limit || 50)) + 1;
|
||||
const result = await pb.collection('domains').getList(page, args.limit || 50, {
|
||||
sort: 'sort_order',
|
||||
});
|
||||
return textContent(JSON.stringify({
|
||||
success: true,
|
||||
domains: result.items,
|
||||
total: result.totalItems,
|
||||
page: result.page,
|
||||
}));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('update_domain', 'Update an existing domain', {
|
||||
domain_id: z.string(),
|
||||
name: z.string().optional(),
|
||||
color: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
sort_order: z.number().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const { domain_id, ...updateData } = args;
|
||||
const cleaned: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(updateData)) {
|
||||
if (value !== undefined) cleaned[key] = value;
|
||||
}
|
||||
const domain = await pb.collection('domains').update(domain_id, cleaned);
|
||||
return textContent(JSON.stringify({ success: true, domain }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('delete_domain', 'Delete a domain', {
|
||||
domain_id: z.string(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
await pb.collection('domains').delete(args.domain_id);
|
||||
return textContent(JSON.stringify({ success: true, deleted: args.domain_id }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import { z } from 'zod';
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { createAdminClient } from '@/lib/pocketbase';
|
||||
|
||||
const pb = createAdminClient();
|
||||
|
||||
function textContent(text: string) {
|
||||
return { content: [{ type: 'text' as const, text }] };
|
||||
}
|
||||
|
||||
export function registerHabitTools(server: McpServer) {
|
||||
server.tool('create_habit', 'Create a new habit', {
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
domain: z.string(),
|
||||
frequency: z.enum(['daily', 'weekly', 'custom']).optional(),
|
||||
difficulty: z.enum(['easy', 'medium', 'hard']).optional(),
|
||||
goal_per_period: z.number().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const habit = await pb.collection('habits').create({
|
||||
name: args.name,
|
||||
description: args.description || '',
|
||||
domain: args.domain,
|
||||
frequency: args.frequency || 'daily',
|
||||
difficulty: args.difficulty || 'medium',
|
||||
completion_mode: 'quick',
|
||||
goal_per_period: args.goal_per_period || 1,
|
||||
tags: args.tags || [],
|
||||
active: true,
|
||||
current_streak: 0,
|
||||
best_streak: 0,
|
||||
total_completions: 0,
|
||||
score: 0,
|
||||
});
|
||||
return textContent(JSON.stringify({ success: true, habit }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('get_habit', 'Get a habit by ID', {
|
||||
habit_id: z.string(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const habit = await pb.collection('habits').getOne(args.habit_id);
|
||||
return textContent(JSON.stringify({ success: true, habit }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('list_habits', 'List habits with optional filters', {
|
||||
domain: z.string().optional(),
|
||||
frequency: z.enum(['daily', 'weekly', 'custom']).optional(),
|
||||
active: z.boolean().optional(),
|
||||
limit: z.number().optional(),
|
||||
offset: z.number().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const filters: string[] = [];
|
||||
if (args.domain) filters.push(`domain = "${args.domain}"`);
|
||||
if (args.frequency) filters.push(`frequency = "${args.frequency}"`);
|
||||
if (args.active !== undefined) filters.push(`active = ${args.active}`);
|
||||
|
||||
const page = Math.floor((args.offset || 0) / (args.limit || 20)) + 1;
|
||||
const result = await pb.collection('habits').getList(page, args.limit || 20, {
|
||||
filter: filters.join(' && ') || '',
|
||||
sort: '-created',
|
||||
});
|
||||
return textContent(JSON.stringify({
|
||||
success: true,
|
||||
habits: result.items,
|
||||
total: result.totalItems,
|
||||
page: result.page,
|
||||
limit: args.limit || 20,
|
||||
}));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('update_habit', 'Update an existing habit', {
|
||||
habit_id: z.string(),
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
domain: z.string().optional(),
|
||||
frequency: z.enum(['daily', 'weekly', 'custom']).optional(),
|
||||
difficulty: z.enum(['easy', 'medium', 'hard']).optional(),
|
||||
active: z.boolean().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const { habit_id, ...updateData } = args;
|
||||
const cleaned: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(updateData)) {
|
||||
if (value !== undefined) cleaned[key] = value;
|
||||
}
|
||||
const habit = await pb.collection('habits').update(habit_id, cleaned);
|
||||
return textContent(JSON.stringify({ success: true, habit }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('delete_habit', 'Delete a habit', {
|
||||
habit_id: z.string(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
await pb.collection('habits').delete(args.habit_id);
|
||||
return textContent(JSON.stringify({ success: true, deleted: args.habit_id }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('log_habit_completion', 'Log a habit completion', {
|
||||
habit_id: z.string(),
|
||||
completed: z.boolean().optional(),
|
||||
notes: z.string().optional(),
|
||||
value: z.number().optional(),
|
||||
mood: z.number().optional(),
|
||||
logged_at: z.string().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const log = await pb.collection('habit_logs').create({
|
||||
habit_id: args.habit_id,
|
||||
completed: args.completed !== undefined ? args.completed : true,
|
||||
notes: args.notes || '',
|
||||
value: args.value,
|
||||
mood: args.mood,
|
||||
logged_at: args.logged_at || new Date().toISOString(),
|
||||
skipped: false,
|
||||
});
|
||||
|
||||
// Update habit streak and count
|
||||
const habit = await pb.collection('habits').getOne(args.habit_id);
|
||||
const now = new Date();
|
||||
const lastUpdated = habit.updated ? new Date(habit.updated) : null;
|
||||
let newStreak = (habit as Record<string, unknown>).current_streak as number || 0;
|
||||
|
||||
if (lastUpdated) {
|
||||
const daysDiff = Math.floor((now.getTime() - lastUpdated.getTime()) / (1000 * 60 * 60 * 24));
|
||||
if (daysDiff === 1) newStreak += 1;
|
||||
else if (daysDiff > 1) newStreak = 1;
|
||||
} else {
|
||||
newStreak = 1;
|
||||
}
|
||||
|
||||
const bestStreak = Math.max(newStreak, (habit as Record<string, unknown>).best_streak as number || 0);
|
||||
|
||||
await pb.collection('habits').update(args.habit_id, {
|
||||
current_streak: newStreak,
|
||||
best_streak: bestStreak,
|
||||
total_completions: ((habit as Record<string, unknown>).total_completions as number || 0) + 1,
|
||||
});
|
||||
|
||||
return textContent(JSON.stringify({ success: true, log, habit_id: args.habit_id, current_streak: newStreak }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('get_habit_streaks', 'Get streak information for all active habits', {}, async () => {
|
||||
try {
|
||||
const habits = await pb.collection('habits').getFullList({
|
||||
filter: 'active = true',
|
||||
sort: '-current_streak',
|
||||
});
|
||||
const streaks = habits.map((h: Record<string, unknown>) => ({
|
||||
habit_id: h.id,
|
||||
name: h.name,
|
||||
current_streak: h.current_streak,
|
||||
best_streak: h.best_streak,
|
||||
total_completions: h.total_completions,
|
||||
score: h.score,
|
||||
}));
|
||||
return textContent(JSON.stringify({ success: true, streaks }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { z } from 'zod';
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { createAdminClient } from '@/lib/pocketbase';
|
||||
|
||||
const pb = createAdminClient();
|
||||
|
||||
function textContent(text: string) {
|
||||
return { content: [{ type: 'text' as const, text }] };
|
||||
}
|
||||
|
||||
export function registerMilestoneTools(server: McpServer) {
|
||||
server.tool('create_milestone', 'Create a new milestone', {
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
project_id: z.string(),
|
||||
domain: z.string(),
|
||||
status: z.enum(['planned', 'in_progress', 'complete']).optional(),
|
||||
target_date: z.string().optional(),
|
||||
sort_order: z.number().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const milestone = await pb.collection('milestones').create({
|
||||
name: args.name,
|
||||
description: args.description || '',
|
||||
project_id: args.project_id,
|
||||
domain: args.domain,
|
||||
status: args.status || 'planned',
|
||||
target_date: args.target_date || null,
|
||||
sort_order: args.sort_order || 0,
|
||||
tags: args.tags || [],
|
||||
tasks: [],
|
||||
dependencies: [],
|
||||
});
|
||||
return textContent(JSON.stringify({ success: true, milestone }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('get_milestone', 'Get a milestone by ID', {
|
||||
milestone_id: z.string(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const milestone = await pb.collection('milestones').getOne(args.milestone_id);
|
||||
return textContent(JSON.stringify({ success: true, milestone }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('list_milestones', 'List milestones with optional filters', {
|
||||
project_id: z.string().optional(),
|
||||
status: z.enum(['planned', 'in_progress', 'complete']).optional(),
|
||||
domain: z.string().optional(),
|
||||
limit: z.number().optional(),
|
||||
offset: z.number().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const filters: string[] = [];
|
||||
if (args.project_id) filters.push(`project_id = "${args.project_id}"`);
|
||||
if (args.status) filters.push(`status = "${args.status}"`);
|
||||
if (args.domain) filters.push(`domain = "${args.domain}"`);
|
||||
|
||||
const page = Math.floor((args.offset || 0) / (args.limit || 20)) + 1;
|
||||
const result = await pb.collection('milestones').getList(page, args.limit || 20, {
|
||||
filter: filters.join(' && ') || '',
|
||||
sort: 'sort_order',
|
||||
});
|
||||
return textContent(JSON.stringify({
|
||||
success: true,
|
||||
milestones: result.items,
|
||||
total: result.totalItems,
|
||||
page: result.page,
|
||||
limit: args.limit || 20,
|
||||
}));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('update_milestone', 'Update an existing milestone', {
|
||||
milestone_id: z.string(),
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
status: z.enum(['planned', 'in_progress', 'complete']).optional(),
|
||||
target_date: z.string().optional(),
|
||||
sort_order: z.number().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const { milestone_id, ...updateData } = args;
|
||||
const cleaned: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(updateData)) {
|
||||
if (value !== undefined) cleaned[key] = value;
|
||||
}
|
||||
if (cleaned.status === 'complete') {
|
||||
cleaned.completed_at = new Date().toISOString();
|
||||
}
|
||||
const milestone = await pb.collection('milestones').update(milestone_id, cleaned);
|
||||
return textContent(JSON.stringify({ success: true, milestone }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('delete_milestone', 'Delete a milestone', {
|
||||
milestone_id: z.string(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
await pb.collection('milestones').delete(args.milestone_id);
|
||||
return textContent(JSON.stringify({ success: true, deleted: args.milestone_id }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { z } from 'zod';
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { createAdminClient } from '@/lib/pocketbase';
|
||||
|
||||
const pb = createAdminClient();
|
||||
|
||||
function textContent(text: string) {
|
||||
return { content: [{ type: 'text' as const, text }] };
|
||||
}
|
||||
|
||||
export function registerNoteTools(server: McpServer) {
|
||||
server.tool('create_note', 'Create a new note', {
|
||||
title: z.string(),
|
||||
content: z.string().optional(),
|
||||
domain: z.string(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
project_id: z.string().optional(),
|
||||
is_pinned: z.boolean().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const content = args.content || '';
|
||||
const wordCount = content.split(/\s+/).filter(Boolean).length;
|
||||
const note = await pb.collection('notes').create({
|
||||
title: args.title,
|
||||
content,
|
||||
domain: args.domain,
|
||||
tags: args.tags || [],
|
||||
project_id: args.project_id || '',
|
||||
is_pinned: args.is_pinned || false,
|
||||
is_archived: false,
|
||||
word_count: wordCount,
|
||||
});
|
||||
return textContent(JSON.stringify({ success: true, note }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('get_note', 'Get a note by ID', {
|
||||
note_id: z.string(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const note = await pb.collection('notes').getOne(args.note_id);
|
||||
return textContent(JSON.stringify({ success: true, note }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('list_notes', 'List notes with optional filters', {
|
||||
domain: z.string().optional(),
|
||||
project_id: z.string().optional(),
|
||||
is_archived: z.boolean().optional(),
|
||||
is_pinned: z.boolean().optional(),
|
||||
limit: z.number().optional(),
|
||||
offset: z.number().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const filters: string[] = [];
|
||||
if (args.domain) filters.push(`domain = "${args.domain}"`);
|
||||
if (args.project_id) filters.push(`project_id = "${args.project_id}"`);
|
||||
if (args.is_archived !== undefined) filters.push(`is_archived = ${args.is_archived}`);
|
||||
if (args.is_pinned !== undefined) filters.push(`is_pinned = ${args.is_pinned}`);
|
||||
|
||||
const page = Math.floor((args.offset || 0) / (args.limit || 20)) + 1;
|
||||
const result = await pb.collection('notes').getList(page, args.limit || 20, {
|
||||
filter: filters.join(' && ') || '',
|
||||
sort: '-created',
|
||||
});
|
||||
return textContent(JSON.stringify({
|
||||
success: true,
|
||||
notes: result.items,
|
||||
total: result.totalItems,
|
||||
page: result.page,
|
||||
limit: args.limit || 20,
|
||||
}));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('update_note', 'Update an existing note', {
|
||||
note_id: z.string(),
|
||||
title: z.string().optional(),
|
||||
content: z.string().optional(),
|
||||
domain: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
project_id: z.string().optional(),
|
||||
is_pinned: z.boolean().optional(),
|
||||
is_archived: z.boolean().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const { note_id, ...updateData } = args;
|
||||
const cleaned: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(updateData)) {
|
||||
if (value !== undefined) cleaned[key] = value;
|
||||
}
|
||||
if (typeof cleaned.content === 'string') {
|
||||
cleaned.word_count = cleaned.content.split(/\s+/).filter(Boolean).length;
|
||||
}
|
||||
const note = await pb.collection('notes').update(note_id, cleaned);
|
||||
return textContent(JSON.stringify({ success: true, note }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('delete_note', 'Delete a note', {
|
||||
note_id: z.string(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
await pb.collection('notes').delete(args.note_id);
|
||||
return textContent(JSON.stringify({ success: true, deleted: args.note_id }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('get_note_graph', 'Get the note graph showing connections between notes', {}, async () => {
|
||||
try {
|
||||
const notes = await pb.collection('notes').getFullList();
|
||||
const links = await pb.collection('note_links').getFullList();
|
||||
|
||||
const nodes = notes.map((n: Record<string, unknown>) => ({
|
||||
id: n.id,
|
||||
title: n.title,
|
||||
domain: n.domain,
|
||||
}));
|
||||
|
||||
const edges = links.map((l: Record<string, unknown>) => ({
|
||||
source: l.source_note_id,
|
||||
target: l.target_note_id,
|
||||
label: l.label || '',
|
||||
}));
|
||||
|
||||
return textContent(JSON.stringify({ success: true, graph: { nodes, edges } }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { z } from 'zod';
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { createAdminClient } from '@/lib/pocketbase';
|
||||
|
||||
const pb = createAdminClient();
|
||||
|
||||
function textContent(text: string) {
|
||||
return { content: [{ type: 'text' as const, text }] };
|
||||
}
|
||||
|
||||
export function registerProjectTools(server: McpServer) {
|
||||
server.tool('create_project', 'Create a new project', {
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
status: z.enum(['active', 'paused', 'archived']).optional(),
|
||||
domain: z.string(),
|
||||
color: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
owner: z.string().optional(),
|
||||
start_date: z.string().optional(),
|
||||
target_date: z.string().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const project = await pb.collection('projects').create({
|
||||
name: args.name,
|
||||
description: args.description || '',
|
||||
status: args.status || 'active',
|
||||
domain: args.domain,
|
||||
color: args.color || null,
|
||||
icon: args.icon || null,
|
||||
tags: args.tags || [],
|
||||
owner: args.owner || '',
|
||||
start_date: args.start_date || null,
|
||||
target_date: args.target_date || null,
|
||||
});
|
||||
return textContent(JSON.stringify({ success: true, project }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('get_project', 'Get a project by ID', {
|
||||
project_id: z.string(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const project = await pb.collection('projects').getOne(args.project_id);
|
||||
return textContent(JSON.stringify({ success: true, project }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('list_projects', 'List projects with optional filters', {
|
||||
status: z.enum(['active', 'paused', 'archived']).optional(),
|
||||
domain: z.string().optional(),
|
||||
limit: z.number().optional(),
|
||||
offset: z.number().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const filters: string[] = [];
|
||||
if (args.status) filters.push(`status = "${args.status}"`);
|
||||
if (args.domain) filters.push(`domain = "${args.domain}"`);
|
||||
|
||||
const page = Math.floor((args.offset || 0) / (args.limit || 20)) + 1;
|
||||
const result = await pb.collection('projects').getList(page, args.limit || 20, {
|
||||
filter: filters.join(' && ') || '',
|
||||
sort: '-created',
|
||||
});
|
||||
return textContent(JSON.stringify({
|
||||
success: true,
|
||||
projects: result.items,
|
||||
total: result.totalItems,
|
||||
page: result.page,
|
||||
limit: args.limit || 20,
|
||||
}));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('update_project', 'Update an existing project', {
|
||||
project_id: z.string(),
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
status: z.enum(['active', 'paused', 'archived']).optional(),
|
||||
domain: z.string().optional(),
|
||||
color: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
owner: z.string().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const { project_id, ...updateData } = args;
|
||||
const cleaned: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(updateData)) {
|
||||
if (value !== undefined) cleaned[key] = value;
|
||||
}
|
||||
const project = await pb.collection('projects').update(project_id, cleaned);
|
||||
return textContent(JSON.stringify({ success: true, project }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('delete_project', 'Delete a project', {
|
||||
project_id: z.string(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
await pb.collection('projects').delete(args.project_id);
|
||||
return textContent(JSON.stringify({ success: true, deleted: args.project_id }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('get_project_progress', 'Get project progress based on task completion', {
|
||||
project_id: z.string(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const tasks = await pb.collection('tasks').getFullList({
|
||||
filter: `project_id = "${args.project_id}"`,
|
||||
});
|
||||
const total = tasks.length;
|
||||
const done = tasks.filter((t: Record<string, unknown>) => t.status === 'done').length;
|
||||
const progress = total > 0 ? Math.round((done / total) * 100) : 0;
|
||||
return textContent(JSON.stringify({
|
||||
success: true,
|
||||
project_id: args.project_id,
|
||||
total_tasks: total,
|
||||
completed_tasks: done,
|
||||
progress,
|
||||
}));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { z } from 'zod';
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { createAdminClient } from '@/lib/pocketbase';
|
||||
|
||||
const pb = createAdminClient();
|
||||
|
||||
function textContent(text: string) {
|
||||
return { content: [{ type: 'text' as const, text }] };
|
||||
}
|
||||
|
||||
export function registerReportTools(server: McpServer) {
|
||||
server.tool('create_report', 'Create a new report', {
|
||||
title: z.string(),
|
||||
type: z.enum(['weekly', 'monthly', 'project', 'habit', 'custom']),
|
||||
domain: z.string(),
|
||||
date_range_start: z.string(),
|
||||
date_range_end: z.string(),
|
||||
summary: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
is_draft: z.boolean().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const report = await pb.collection('reports').create({
|
||||
title: args.title,
|
||||
type: args.type,
|
||||
domain: args.domain,
|
||||
date_range: {
|
||||
start: args.date_range_start,
|
||||
end: args.date_range_end,
|
||||
},
|
||||
sections: [],
|
||||
summary: args.summary || '',
|
||||
tags: args.tags || [],
|
||||
is_draft: args.is_draft !== undefined ? args.is_draft : true,
|
||||
});
|
||||
return textContent(JSON.stringify({ success: true, report }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('get_report', 'Get a report by ID', {
|
||||
report_id: z.string(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const report = await pb.collection('reports').getOne(args.report_id);
|
||||
return textContent(JSON.stringify({ success: true, report }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('list_reports', 'List reports with optional filters', {
|
||||
type: z.enum(['weekly', 'monthly', 'project', 'habit', 'custom']).optional(),
|
||||
domain: z.string().optional(),
|
||||
is_draft: z.boolean().optional(),
|
||||
limit: z.number().optional(),
|
||||
offset: z.number().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const filters: string[] = [];
|
||||
if (args.type) filters.push(`type = "${args.type}"`);
|
||||
if (args.domain) filters.push(`domain = "${args.domain}"`);
|
||||
if (args.is_draft !== undefined) filters.push(`is_draft = ${args.is_draft}`);
|
||||
|
||||
const page = Math.floor((args.offset || 0) / (args.limit || 20)) + 1;
|
||||
const result = await pb.collection('reports').getList(page, args.limit || 20, {
|
||||
filter: filters.join(' && ') || '',
|
||||
sort: '-created',
|
||||
});
|
||||
return textContent(JSON.stringify({
|
||||
success: true,
|
||||
reports: result.items,
|
||||
total: result.totalItems,
|
||||
page: result.page,
|
||||
limit: args.limit || 20,
|
||||
}));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('update_report', 'Update an existing report', {
|
||||
report_id: z.string(),
|
||||
title: z.string().optional(),
|
||||
summary: z.string().optional(),
|
||||
is_draft: z.boolean().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const { report_id, ...updateData } = args;
|
||||
const cleaned: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(updateData)) {
|
||||
if (value !== undefined) cleaned[key] = value;
|
||||
}
|
||||
const report = await pb.collection('reports').update(report_id, cleaned);
|
||||
return textContent(JSON.stringify({ success: true, report }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('delete_report', 'Delete a report', {
|
||||
report_id: z.string(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
await pb.collection('reports').delete(args.report_id);
|
||||
return textContent(JSON.stringify({ success: true, deleted: args.report_id }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { z } from 'zod';
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { createAdminClient } from '@/lib/pocketbase';
|
||||
|
||||
const pb = createAdminClient();
|
||||
|
||||
function textContent(text: string) {
|
||||
return { content: [{ type: 'text' as const, text }] };
|
||||
}
|
||||
|
||||
export function registerTagTools(server: McpServer) {
|
||||
server.tool('create_tag', 'Create a new tag', {
|
||||
name: z.string(),
|
||||
color: z.string().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const tag = await pb.collection('tags').create({
|
||||
name: args.name,
|
||||
color: args.color || null,
|
||||
});
|
||||
return textContent(JSON.stringify({ success: true, tag }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('get_tag', 'Get a tag by ID', {
|
||||
tag_id: z.string(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const tag = await pb.collection('tags').getOne(args.tag_id);
|
||||
return textContent(JSON.stringify({ success: true, tag }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('list_tags', 'List all tags', {
|
||||
limit: z.number().optional(),
|
||||
offset: z.number().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const page = Math.floor((args.offset || 0) / (args.limit || 50)) + 1;
|
||||
const result = await pb.collection('tags').getList(page, args.limit || 50, {
|
||||
sort: 'name',
|
||||
});
|
||||
return textContent(JSON.stringify({
|
||||
success: true,
|
||||
tags: result.items,
|
||||
total: result.totalItems,
|
||||
page: result.page,
|
||||
}));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('update_tag', 'Update an existing tag', {
|
||||
tag_id: z.string(),
|
||||
name: z.string().optional(),
|
||||
color: z.string().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const { tag_id, ...updateData } = args;
|
||||
const cleaned: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(updateData)) {
|
||||
if (value !== undefined) cleaned[key] = value;
|
||||
}
|
||||
const tag = await pb.collection('tags').update(tag_id, cleaned);
|
||||
return textContent(JSON.stringify({ success: true, tag }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('delete_tag', 'Delete a tag', {
|
||||
tag_id: z.string(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
await pb.collection('tags').delete(args.tag_id);
|
||||
return textContent(JSON.stringify({ success: true, deleted: args.tag_id }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import { z } from 'zod';
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { createPocketBaseClient, createAdminClient } from '@/lib/pocketbase';
|
||||
|
||||
const pb = createAdminClient();
|
||||
|
||||
function textContent(text: string) {
|
||||
return { content: [{ type: 'text' as const, text }] };
|
||||
}
|
||||
|
||||
export function registerTaskTools(server: McpServer) {
|
||||
server.tool('create_task', 'Create a new task', {
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
status: z.enum(['todo', 'in_progress', 'done', 'cancelled']).optional(),
|
||||
priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
|
||||
due_date: z.string().optional(),
|
||||
project_id: z.string().optional(),
|
||||
milestone_id: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
domain: z.string(),
|
||||
assignee: z.string().optional(),
|
||||
estimate: z.number().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const task = await pb.collection('tasks').create({
|
||||
title: args.title,
|
||||
description: args.description || '',
|
||||
status: args.status || 'todo',
|
||||
priority: args.priority || 'medium',
|
||||
due_date: args.due_date || null,
|
||||
project_id: args.project_id || '',
|
||||
milestone_id: args.milestone_id || '',
|
||||
tags: args.tags || [],
|
||||
domain: args.domain,
|
||||
assignee: args.assignee || '',
|
||||
estimate: args.estimate || null,
|
||||
attachments: [],
|
||||
dependencies: [],
|
||||
subtasks: [],
|
||||
});
|
||||
return textContent(JSON.stringify({ success: true, task }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('get_task', 'Get a task by ID', {
|
||||
task_id: z.string(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const task = await pb.collection('tasks').getOne(args.task_id);
|
||||
return textContent(JSON.stringify({ success: true, task }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('list_tasks', 'List tasks with optional filters', {
|
||||
status: z.enum(['todo', 'in_progress', 'done', 'cancelled']).optional(),
|
||||
priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
|
||||
project_id: z.string().optional(),
|
||||
milestone_id: z.string().optional(),
|
||||
domain: z.string().optional(),
|
||||
limit: z.number().optional(),
|
||||
offset: z.number().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const filters: string[] = [];
|
||||
if (args.status) filters.push(`status = "${args.status}"`);
|
||||
if (args.priority) filters.push(`priority = "${args.priority}"`);
|
||||
if (args.project_id) filters.push(`project_id = "${args.project_id}"`);
|
||||
if (args.milestone_id) filters.push(`milestone_id = "${args.milestone_id}"`);
|
||||
if (args.domain) filters.push(`domain = "${args.domain}"`);
|
||||
|
||||
const page = Math.floor((args.offset || 0) / (args.limit || 20)) + 1;
|
||||
const result = await pb.collection('tasks').getList(page, args.limit || 20, {
|
||||
filter: filters.join(' && ') || '',
|
||||
sort: '-created',
|
||||
});
|
||||
return textContent(JSON.stringify({
|
||||
success: true,
|
||||
tasks: result.items,
|
||||
total: result.totalItems,
|
||||
page: result.page,
|
||||
limit: args.limit || 20,
|
||||
}));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('update_task', 'Update an existing task', {
|
||||
task_id: z.string(),
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
status: z.enum(['todo', 'in_progress', 'done', 'cancelled']).optional(),
|
||||
priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
|
||||
due_date: z.string().optional(),
|
||||
project_id: z.string().optional(),
|
||||
milestone_id: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
domain: z.string().optional(),
|
||||
assignee: z.string().optional(),
|
||||
estimate: z.number().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const { task_id, ...updateData } = args;
|
||||
const cleaned: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(updateData)) {
|
||||
if (value !== undefined) cleaned[key] = value;
|
||||
}
|
||||
const task = await pb.collection('tasks').update(task_id, cleaned);
|
||||
return textContent(JSON.stringify({ success: true, task }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('delete_task', 'Delete a task', {
|
||||
task_id: z.string(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
await pb.collection('tasks').delete(args.task_id);
|
||||
return textContent(JSON.stringify({ success: true, deleted: args.task_id }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('bulk_create_tasks', 'Create multiple tasks at once', {
|
||||
tasks: z.array(z.object({
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
status: z.enum(['todo', 'in_progress', 'done', 'cancelled']).optional(),
|
||||
priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
|
||||
due_date: z.string().optional(),
|
||||
project_id: z.string().optional(),
|
||||
domain: z.string(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
})),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const created = [];
|
||||
for (const taskData of args.tasks) {
|
||||
const task = await pb.collection('tasks').create({
|
||||
title: taskData.title,
|
||||
description: taskData.description || '',
|
||||
status: taskData.status || 'todo',
|
||||
priority: taskData.priority || 'medium',
|
||||
due_date: taskData.due_date || null,
|
||||
project_id: taskData.project_id || '',
|
||||
domain: taskData.domain,
|
||||
tags: taskData.tags || [],
|
||||
attachments: [],
|
||||
dependencies: [],
|
||||
subtasks: [],
|
||||
});
|
||||
created.push(task);
|
||||
}
|
||||
return textContent(JSON.stringify({ success: true, created, count: created.length }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('bulk_update_tasks', 'Update multiple tasks at once', {
|
||||
updates: z.array(z.object({
|
||||
task_id: z.string(),
|
||||
title: z.string().optional(),
|
||||
status: z.enum(['todo', 'in_progress', 'done', 'cancelled']).optional(),
|
||||
priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
|
||||
})),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const updated = [];
|
||||
for (const { task_id, ...data } of args.updates) {
|
||||
const cleaned: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (value !== undefined) cleaned[key] = value;
|
||||
}
|
||||
const task = await pb.collection('tasks').update(task_id, cleaned);
|
||||
updated.push(task);
|
||||
}
|
||||
return textContent(JSON.stringify({ success: true, updated, count: updated.length }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('bulk_delete_tasks', 'Delete multiple tasks at once', {
|
||||
task_ids: z.array(z.string()),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const deleted = [];
|
||||
for (const id of args.task_ids) {
|
||||
await pb.collection('tasks').delete(id);
|
||||
deleted.push(id);
|
||||
}
|
||||
return textContent(JSON.stringify({ success: true, deleted, count: deleted.length }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { z } from 'zod';
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { createAdminClient } from '@/lib/pocketbase';
|
||||
|
||||
const pb = createAdminClient();
|
||||
|
||||
function textContent(text: string) {
|
||||
return { content: [{ type: 'text' as const, text }] };
|
||||
}
|
||||
|
||||
export function registerWebhookTools(server: McpServer) {
|
||||
server.tool('create_webhook', 'Create a new webhook', {
|
||||
name: z.string(),
|
||||
url: z.string(),
|
||||
events: z.array(z.string()),
|
||||
domain: z.string(),
|
||||
secret: z.string().optional(),
|
||||
active: z.boolean().optional(),
|
||||
retry_count: z.number().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const webhook = await pb.collection('webhooks').create({
|
||||
name: args.name,
|
||||
url: args.url,
|
||||
events: args.events,
|
||||
domain: args.domain,
|
||||
secret: args.secret || '',
|
||||
active: args.active !== undefined ? args.active : true,
|
||||
retry_count: args.retry_count || 3,
|
||||
});
|
||||
return textContent(JSON.stringify({ success: true, webhook }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('get_webhook', 'Get a webhook by ID', {
|
||||
webhook_id: z.string(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const webhook = await pb.collection('webhooks').getOne(args.webhook_id);
|
||||
return textContent(JSON.stringify({ success: true, webhook }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('list_webhooks', 'List webhooks with optional filters', {
|
||||
domain: z.string().optional(),
|
||||
active: z.boolean().optional(),
|
||||
limit: z.number().optional(),
|
||||
offset: z.number().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const filters: string[] = [];
|
||||
if (args.domain) filters.push(`domain = "${args.domain}"`);
|
||||
if (args.active !== undefined) filters.push(`active = ${args.active}`);
|
||||
|
||||
const page = Math.floor((args.offset || 0) / (args.limit || 20)) + 1;
|
||||
const result = await pb.collection('webhooks').getList(page, args.limit || 20, {
|
||||
filter: filters.join(' && ') || '',
|
||||
sort: '-created',
|
||||
});
|
||||
return textContent(JSON.stringify({
|
||||
success: true,
|
||||
webhooks: result.items,
|
||||
total: result.totalItems,
|
||||
page: result.page,
|
||||
limit: args.limit || 20,
|
||||
}));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('update_webhook', 'Update an existing webhook', {
|
||||
webhook_id: z.string(),
|
||||
name: z.string().optional(),
|
||||
url: z.string().optional(),
|
||||
events: z.array(z.string()).optional(),
|
||||
domain: z.string().optional(),
|
||||
active: z.boolean().optional(),
|
||||
retry_count: z.number().optional(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
const { webhook_id, ...updateData } = args;
|
||||
const cleaned: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(updateData)) {
|
||||
if (value !== undefined) cleaned[key] = value;
|
||||
}
|
||||
const webhook = await pb.collection('webhooks').update(webhook_id, cleaned);
|
||||
return textContent(JSON.stringify({ success: true, webhook }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.tool('delete_webhook', 'Delete a webhook', {
|
||||
webhook_id: z.string(),
|
||||
}, async (args) => {
|
||||
try {
|
||||
await pb.collection('webhooks').delete(args.webhook_id);
|
||||
return textContent(JSON.stringify({ success: true, deleted: args.webhook_id }));
|
||||
} catch (error) {
|
||||
return textContent(JSON.stringify({ success: false, error: String(error) }));
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user