mirror of
https://github.com/supabase/supabase.git
synced 2026-10-12 21:05:06 +03:00
refactor
This commit is contained in:
1 parent
97aeb76b30
commit
56ed950987
10 files changed
+316
-348
No files matched your search
@@ -3,6 +3,7 @@ import { openai } from '@ai-sdk/openai'
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
||||
import { convertToModelMessages, stepCountIs, streamText } from 'ai'
|
||||
import { corsHeaders } from '../_shared/cors.ts'
|
||||
import { systemPrompt } from './prompt.ts'
|
||||
|
||||
type ChatRequestBody = {
|
||||
messages?: unknown[]
|
||||
@@ -85,7 +86,7 @@ Deno.serve(async (req) => {
|
||||
model: openai(modelId),
|
||||
messages: normalizedMessages,
|
||||
stopWhen: stepCountIs(5),
|
||||
system: 'You are a helpful AI assistant.',
|
||||
system: systemPrompt,
|
||||
tools,
|
||||
onFinish: async () => {
|
||||
// Close the MCP client when the stream is complete
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
const currentDate = new Date().toISOString().split('T')[0]
|
||||
|
||||
export const systemPrompt = `Today is ${currentDate}. You are Supabase Tasks, an AI assistant focused on helping users plan, prioritize, and manage their work. Keep conversations grounded in their task lists, suggest concrete next actions, and clarify requirements before acting.`
|
||||
+16
-160
@@ -3,6 +3,7 @@ import { McpServer, StreamableHttpTransport } from 'mcp-lite'
|
||||
import { z } from 'zod'
|
||||
import { corsHeaders } from '../_shared/cors.ts'
|
||||
import { createClient } from 'jsr:@supabase/supabase-js@2'
|
||||
import { toolRegistry } from './tools/registry.ts'
|
||||
|
||||
// We create two Hono instances:
|
||||
// 1. `app` is the root handler for the Supabase Edge Function (must match the function name, e.g. /mcp-server)
|
||||
@@ -52,186 +53,41 @@ mcp.use(async (ctx: any, next: any) => {
|
||||
await next()
|
||||
})
|
||||
|
||||
mcp.tool('getUserTasks', {
|
||||
description: 'Fetch tasks for the authenticated user with optional filtering by due date and title search.',
|
||||
inputSchema: z.object({
|
||||
due_date: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Optional due date filter in YYYY-MM-DD format. Returns tasks with this exact due date.'),
|
||||
title_search: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Optional title search term. Returns tasks where the title contains this text (case-insensitive).'),
|
||||
}),
|
||||
handler: async ({ due_date, title_search }: { due_date?: string; title_search?: string }, ctx: any) => {
|
||||
const authHeader = ctx.state?.authHeader
|
||||
toolRegistry.forEach((tool) => {
|
||||
mcp.tool(tool.name, {
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema,
|
||||
handler: async (input: unknown, ctx: any) => {
|
||||
const authHeader = ctx.state?.authHeader
|
||||
|
||||
if (!authHeader) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Error: Authentication required to fetch tasks.',
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const supabase = createAuthenticatedClient(authHeader)
|
||||
|
||||
let query = supabase
|
||||
.from('tasks')
|
||||
.select('id, created_at, title, assignee_id, due_at')
|
||||
.order('due_at', { ascending: true, nullsFirst: false })
|
||||
|
||||
// Apply due_date filter if provided
|
||||
if (due_date) {
|
||||
query = query.eq('due_at', due_date)
|
||||
}
|
||||
|
||||
// Apply title_search filter if provided
|
||||
if (title_search) {
|
||||
query = query.ilike('title', `%${title_search}%`)
|
||||
}
|
||||
|
||||
const { data, error } = await query
|
||||
|
||||
if (error) {
|
||||
if (!authHeader) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error fetching tasks: ${error.message}`,
|
||||
text: tool.authErrorMessage,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
|
||||
const taskCount = data?.length || 0
|
||||
const tasksText = data && taskCount > 0
|
||||
? data.map((task) => {
|
||||
const dueDate = task.due_at ? `Due: ${task.due_at}` : 'No due date'
|
||||
return `- [${task.id}] ${task.title || 'Untitled'} (${dueDate})`
|
||||
}).join('\n')
|
||||
: 'No tasks found.'
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Found ${taskCount} task(s):\n\n${tasksText}`,
|
||||
},
|
||||
],
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Unexpected error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
mcp.tool('createTask', {
|
||||
description: 'Create a task for the authenticated user. Optionally provide a due date (YYYY-MM-DD).',
|
||||
inputSchema: z.object({
|
||||
title: z
|
||||
.string()
|
||||
.min(1, 'Title must not be empty')
|
||||
.describe('Task title to display in the list.'),
|
||||
due_date: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Optional due date in YYYY-MM-DD format.'),
|
||||
}),
|
||||
handler: async ({ title, due_date }: { title: string; due_date?: string }, ctx: any) => {
|
||||
const authHeader = ctx.state?.authHeader
|
||||
|
||||
if (!authHeader) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Error: Authentication required to create tasks.',
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const supabase = createAuthenticatedClient(authHeader)
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
error: userError,
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (userError || !user) {
|
||||
try {
|
||||
const supabase = createAuthenticatedClient(authHeader)
|
||||
return await tool.run(supabase, input)
|
||||
} catch (err) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error retrieving authenticated user: ${userError?.message ?? 'Not found'}`,
|
||||
text: `Unexpected error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('tasks')
|
||||
.insert({
|
||||
title,
|
||||
due_at: due_date ?? null,
|
||||
assignee_id: user.id,
|
||||
})
|
||||
.select('id, title, due_at, assignee_id')
|
||||
.single()
|
||||
|
||||
if (error || !data) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error creating task: ${error?.message ?? 'Unknown error'}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
|
||||
const dueDateText = data.due_at ? `Due ${data.due_at}` : 'No due date set'
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Created task [${data.id}] "${data.title}" for user ${data.assignee_id}. ${dueDateText}.`,
|
||||
},
|
||||
],
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Unexpected error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const transport = new StreamableHttpTransport()
|
||||
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
import { createClient } from 'jsr:@supabase/supabase-js@2'
|
||||
|
||||
export function createAuthenticatedClient(authHeader: string) {
|
||||
return createClient(
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
Deno.env.get('SUPABASE_ANON_KEY')!,
|
||||
{
|
||||
global: {
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
+60
-72
@@ -1,98 +1,86 @@
|
||||
import { z } from 'zod'
|
||||
import { createAuthenticatedClient } from '../lib/supabase.ts'
|
||||
import type { ToolDefinition } from './types.ts'
|
||||
|
||||
export const createTaskTool = {
|
||||
type CreateTaskInput = {
|
||||
title: string
|
||||
due_date?: string
|
||||
}
|
||||
|
||||
export const createTaskTool: ToolDefinition<CreateTaskInput> = {
|
||||
name: 'createTask',
|
||||
options: {
|
||||
description: 'Create a task for the authenticated user. Optionally provide a due date (YYYY-MM-DD).',
|
||||
inputSchema: z.object({
|
||||
title: z
|
||||
.string()
|
||||
.min(1, 'Title must not be empty')
|
||||
.describe('Task title to display in the list.'),
|
||||
due_date: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Optional due date in YYYY-MM-DD format.'),
|
||||
}),
|
||||
handler: async ({ title, due_date }: { title: string; due_date?: string }, ctx: any) => {
|
||||
const authHeader = ctx.state?.authHeader
|
||||
description: 'Create a task for the authenticated user. Optionally provide a due date (YYYY-MM-DD).',
|
||||
authErrorMessage: 'Error: Authentication required to create tasks.',
|
||||
inputSchema: z.object({
|
||||
title: z
|
||||
.string()
|
||||
.min(1, 'Title must not be empty')
|
||||
.describe('Task title to display in the list.'),
|
||||
due_date: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Optional due date in YYYY-MM-DD format.'),
|
||||
}),
|
||||
run: async (supabase, { title, due_date }) => {
|
||||
try {
|
||||
const {
|
||||
data: { user },
|
||||
error: userError,
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (!authHeader) {
|
||||
if (userError || !user) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Error: Authentication required to create tasks.',
|
||||
text: `Error retrieving authenticated user: ${userError?.message ?? 'Not found'}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const supabase = createAuthenticatedClient(authHeader)
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
error: userError,
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (userError || !user) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error retrieving authenticated user: ${userError?.message ?? 'Not found'}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('tasks')
|
||||
.insert({
|
||||
title,
|
||||
due_at: due_date ?? null,
|
||||
assignee_id: user.id,
|
||||
})
|
||||
.select('id, title, due_at, assignee_id')
|
||||
.single()
|
||||
|
||||
if (error || !data) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error creating task: ${error?.message ?? 'Unknown error'}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
|
||||
const dueDateText = data.due_at ? `Due ${data.due_at}` : 'No due date set'
|
||||
const { data, error } = await supabase
|
||||
.from('tasks')
|
||||
.insert({
|
||||
title,
|
||||
due_at: due_date ?? null,
|
||||
assignee_id: user.id,
|
||||
})
|
||||
.select('id, title, due_at, assignee_id')
|
||||
.single()
|
||||
|
||||
if (error || !data) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Created task [${data.id}] "${data.title}" for user ${data.assignee_id}. ${dueDateText}.`,
|
||||
},
|
||||
],
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Unexpected error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
text: `Error creating task: ${error?.message ?? 'Unknown error'}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
const dueDateText = data.due_at ? `Due ${data.due_at}` : 'No due date set'
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Created task [${data.id}] "${data.title}" for user ${data.assignee_id}. ${dueDateText}.`,
|
||||
},
|
||||
],
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Unexpected error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { z } from 'zod'
|
||||
import type { ToolDefinition } from './types.ts'
|
||||
|
||||
type DeleteTaskInput = {
|
||||
id: number
|
||||
}
|
||||
|
||||
export const deleteTaskTool: ToolDefinition<DeleteTaskInput> = {
|
||||
name: 'deleteTask',
|
||||
description: 'Delete a task using its numeric id.',
|
||||
authErrorMessage: 'Error: Authentication required to delete tasks.',
|
||||
inputSchema: z.object({
|
||||
id: z.number().int().describe('Task id to delete.'),
|
||||
}),
|
||||
run: async (supabase, { id }) => {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('tasks')
|
||||
.delete()
|
||||
.eq('id', id)
|
||||
.select('id, title')
|
||||
.single()
|
||||
|
||||
if (error || !data) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error deleting task: ${error?.message ?? 'Task not found or already deleted.'}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Deleted task [${data.id}] "${data.title ?? 'Untitled'}" successfully.`,
|
||||
},
|
||||
],
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Unexpected error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
+80
-97
@@ -1,101 +1,84 @@
|
||||
import { z } from 'zod'
|
||||
import { createAuthenticatedClient } from '../lib/supabase.ts'
|
||||
import type { ToolDefinition } from './types.ts'
|
||||
|
||||
export const getUserTasksTool = {
|
||||
name: 'getUserTasks',
|
||||
options: {
|
||||
description:
|
||||
'Fetch tasks for the authenticated user with optional filtering by due date and title search.',
|
||||
inputSchema: z.object({
|
||||
due_date: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Optional due date filter in YYYY-MM-DD format. Returns tasks with this exact due date.'),
|
||||
title_search: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Optional title search term. Returns tasks where the title contains this text (case-insensitive).'),
|
||||
}),
|
||||
handler: async (
|
||||
{ due_date, title_search }: { due_date?: string; title_search?: string },
|
||||
ctx: any
|
||||
) => {
|
||||
const authHeader = ctx.state?.authHeader
|
||||
|
||||
if (!authHeader) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Error: Authentication required to fetch tasks.',
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const supabase = createAuthenticatedClient(authHeader)
|
||||
|
||||
let query = supabase
|
||||
.from('tasks')
|
||||
.select('id, created_at, title, assignee_id, due_at')
|
||||
.order('due_at', { ascending: true, nullsFirst: false })
|
||||
|
||||
if (due_date) {
|
||||
query = query.eq('due_at', due_date)
|
||||
}
|
||||
|
||||
if (title_search) {
|
||||
query = query.ilike('title', `%${title_search}%`)
|
||||
}
|
||||
|
||||
const { data, error } = await query
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error fetching tasks: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
|
||||
const taskCount = data?.length || 0
|
||||
const tasksText =
|
||||
data && taskCount > 0
|
||||
? data
|
||||
.map((task) => {
|
||||
const dueDate = task.due_at ? `Due: ${task.due_at}` : 'No due date'
|
||||
return `- [${task.id}] ${task.title || 'Untitled'} (${dueDate})`
|
||||
})
|
||||
.join('\n')
|
||||
: 'No tasks found.'
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Found ${taskCount} task(s):\n\n${tasksText}`,
|
||||
},
|
||||
],
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Unexpected error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
type GetUserTasksInput = {
|
||||
due_date?: string
|
||||
title_search?: string
|
||||
}
|
||||
|
||||
export type ToolDefinition = typeof getUserTasksTool
|
||||
export const getUserTasksTool: ToolDefinition<GetUserTasksInput> = {
|
||||
name: 'getUserTasks',
|
||||
description:
|
||||
'Fetch tasks for the authenticated user with optional filtering by due date and title search.',
|
||||
authErrorMessage: 'Error: Authentication required to fetch tasks.',
|
||||
inputSchema: z.object({
|
||||
due_date: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Optional due date filter in YYYY-MM-DD format. Returns tasks with this exact due date.'),
|
||||
title_search: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Optional title search term. Returns tasks where the title contains this text (case-insensitive).'),
|
||||
}),
|
||||
run: async (supabase, { due_date, title_search }) => {
|
||||
try {
|
||||
let query = supabase
|
||||
.from('tasks')
|
||||
.select('id, created_at, title, assignee_id, due_at')
|
||||
.order('due_at', { ascending: true, nullsFirst: false })
|
||||
|
||||
if (due_date) {
|
||||
query = query.eq('due_at', due_date)
|
||||
}
|
||||
|
||||
if (title_search) {
|
||||
query = query.ilike('title', `%${title_search}%`)
|
||||
}
|
||||
|
||||
const { data, error } = await query
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error fetching tasks: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
|
||||
const taskCount = data?.length || 0
|
||||
const tasksText =
|
||||
data && taskCount > 0
|
||||
? data
|
||||
.map((task) => {
|
||||
const dueDate = task.due_at ? `Due: ${task.due_at}` : 'No due date'
|
||||
return `- [${task.id}] ${task.title || 'Untitled'} (${dueDate})`
|
||||
})
|
||||
.join('\n')
|
||||
: 'No tasks found.'
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Found ${taskCount} task(s):\n\n${tasksText}`,
|
||||
},
|
||||
],
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Unexpected error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
+9
-3
@@ -1,6 +1,12 @@
|
||||
import { createTaskTool } from './create-task.ts'
|
||||
import { deleteTaskTool } from './delete-task.ts'
|
||||
import { getUserTasksTool } from './get-user-tasks.ts'
|
||||
import { updateTaskTool } from './update-task.ts'
|
||||
import type { ToolDefinition } from './types.ts'
|
||||
|
||||
export const toolRegistry = [getUserTasksTool, createTaskTool]
|
||||
|
||||
export type ToolDefinition = (typeof toolRegistry)[number]
|
||||
export const toolRegistry: ToolDefinition[] = [
|
||||
getUserTasksTool,
|
||||
createTaskTool,
|
||||
updateTaskTool,
|
||||
deleteTaskTool,
|
||||
]
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import type { SupabaseClient } from 'jsr:@supabase/supabase-js@2'
|
||||
import type { ZodTypeAny } from 'zod'
|
||||
|
||||
export interface ToolDefinition<Input = unknown> {
|
||||
name: string
|
||||
description: string
|
||||
inputSchema: ZodTypeAny
|
||||
authErrorMessage: string
|
||||
run: (supabase: SupabaseClient, input: Input) => Promise<any>
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import { z } from 'zod'
|
||||
import type { ToolDefinition } from './types.ts'
|
||||
|
||||
type UpdateTaskInput = {
|
||||
id: number
|
||||
title?: string
|
||||
due_date?: string
|
||||
}
|
||||
|
||||
export const updateTaskTool: ToolDefinition<UpdateTaskInput> = {
|
||||
name: 'updateTask',
|
||||
description: 'Update a task title and/or due date by id.',
|
||||
authErrorMessage: 'Error: Authentication required to update tasks.',
|
||||
inputSchema: z
|
||||
.object({
|
||||
id: z.number().int().describe('Task id to update.'),
|
||||
title: z.string().optional().describe('New title for the task.'),
|
||||
due_date: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('New due date in YYYY-MM-DD format. Set to empty string to clear.'),
|
||||
})
|
||||
.refine((data) => data.title !== undefined || data.due_date !== undefined, {
|
||||
message: 'Provide at least a title or due_date to update.',
|
||||
path: ['title'],
|
||||
}),
|
||||
run: async (supabase, { id, title, due_date }) => {
|
||||
try {
|
||||
const updatePayload: Record<string, string | null> = {}
|
||||
|
||||
if (title !== undefined) {
|
||||
updatePayload.title = title
|
||||
}
|
||||
|
||||
if (due_date !== undefined) {
|
||||
updatePayload.due_at = due_date || null
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('tasks')
|
||||
.update(updatePayload)
|
||||
.eq('id', id)
|
||||
.select('id, title, due_at, assignee_id')
|
||||
.single()
|
||||
|
||||
if (error || !data) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error updating task: ${error?.message ?? 'Task not found.'}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
|
||||
const dueDateText = data.due_at ? `Due ${data.due_at}` : 'No due date set'
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Updated task [${data.id}] "${data.title}". ${dueDateText}.`,
|
||||
},
|
||||
],
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Unexpected error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
Reference in new issue
Block a user