T1/Phase 1: scaffold Vite SPA + Hono API + Bun worker

- apps/web: Vite + React 19 + TanStack Router/Query + shadcn/ui
   - apps/api: Hono + Bun on :3001 with /api/health, /api/auth/*, /mcp stubs
   - apps/worker: Bun worker stub, DB connection, graceful SIGTERM
   - apps/web-legacy/: old Next.js code moved aside (preserved for T2-T8 reference)
   - Dockerfiles: api (Bun), worker (Bun), spa (multi-stage Caddy)
   - Caddyfile: serves dist + reverse-proxies /api/* + /mcp to api
   - docker-compose.yml: 4-service target (api, spa, db, worker)
   - packages/db/src/client.ts: shared Drizzle client for api + worker
   - db/client.ts: root-level alias for convenience

   Parent: t_e1cbd87d -> t_24c9c3fd (T0)
This commit is contained in:
Hermes
2026-08-01 01:15:31 +00:00
parent 9203aee758
commit fca56ab77e
312 changed files with 3489 additions and 196 deletions
+106
View File
@@ -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) }));
}
});
}
+202
View File
@@ -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) }));
}
});
}
+92
View File
@@ -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) }));
}
});
}
+184
View File
@@ -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) }));
}
});
}
+117
View File
@@ -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) }));
}
});
}
+141
View File
@@ -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) }));
}
});
}
+138
View File
@@ -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) }));
}
});
}
+113
View File
@@ -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) }));
}
});
}
+86
View File
@@ -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) }));
}
});
}
+205
View File
@@ -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) }));
}
});
}
+108
View File
@@ -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) }));
}
});
}