91 lines
2.7 KiB
TypeScript
91 lines
2.7 KiB
TypeScript
import { z } from 'zod'
|
|
import {
|
|
pikkuMCPToolFunc,
|
|
pikkuMCPResourceFunc,
|
|
pikkuMCPPromptFunc,
|
|
wireMCPResource,
|
|
wireMCPPrompt,
|
|
} from '#pikku'
|
|
import { randomUUID } from 'node:crypto'
|
|
|
|
export const listCardsTool = pikkuMCPToolFunc({
|
|
description: 'List all kanban cards on the board.',
|
|
tags: ['kanban', 'cards'],
|
|
func: async ({ kysely }) => {
|
|
const cards = await kysely
|
|
.selectFrom('kanbanCard')
|
|
.selectAll()
|
|
.orderBy('position', 'asc')
|
|
.execute()
|
|
return [{ type: 'text' as const, text: JSON.stringify(cards, null, 2) }]
|
|
},
|
|
})
|
|
|
|
export const CreateCardToolInput = z.object({
|
|
title: z.string().min(1),
|
|
status: z.enum(['todo', 'doing', 'done']).default('todo'),
|
|
})
|
|
|
|
export const createCardTool = pikkuMCPToolFunc({
|
|
input: CreateCardToolInput,
|
|
description: 'Create a new kanban card on the board.',
|
|
tags: ['kanban', 'cards'],
|
|
func: async ({ kysely, queueService }, { title, status }) => {
|
|
const cardId = randomUUID()
|
|
const createdAt = new Date().toISOString()
|
|
const max = await kysely
|
|
.selectFrom('kanbanCard')
|
|
.select(({ fn }) => fn.max<number>('position').as('max'))
|
|
.where('status', '=', status)
|
|
.executeTakeFirst()
|
|
const position = (max?.max ?? 0) + 1
|
|
await kysely
|
|
.insertInto('kanbanCard')
|
|
.values({ cardId, title, status, position, createdAt })
|
|
.execute()
|
|
await queueService!.add('process-card-event', { cardId, action: 'created', status, createdAt })
|
|
return [{ type: 'text' as const, text: `Created card ${cardId}: "${title}" (${status})` }]
|
|
},
|
|
})
|
|
|
|
const kanbanCardsResource = pikkuMCPResourceFunc<void>(async ({ kysely }) => {
|
|
const cards = await kysely
|
|
.selectFrom('kanbanCard')
|
|
.selectAll()
|
|
.orderBy('position', 'asc')
|
|
.execute()
|
|
return [{ uri: 'kanban://cards', text: JSON.stringify(cards, null, 2) }]
|
|
})
|
|
|
|
const kanbanStatusPrompt = pikkuMCPPromptFunc<void>(async ({ kysely }) => {
|
|
const cards = await kysely.selectFrom('kanbanCard').selectAll().execute()
|
|
const byStatus = cards.reduce<Record<string, number>>((acc, c) => {
|
|
acc[c.status] = (acc[c.status] ?? 0) + 1
|
|
return acc
|
|
}, {})
|
|
return [
|
|
{
|
|
role: 'user' as const,
|
|
content: {
|
|
type: 'text' as const,
|
|
text: `Board has ${cards.length} cards: ${JSON.stringify(byStatus)}. Summarise the state.`,
|
|
},
|
|
},
|
|
]
|
|
})
|
|
|
|
wireMCPResource({
|
|
uri: 'kanban://cards',
|
|
title: 'Kanban Board Cards',
|
|
description: 'All cards currently on the kanban board',
|
|
func: kanbanCardsResource,
|
|
tags: ['kanban'],
|
|
})
|
|
|
|
wireMCPPrompt({
|
|
name: 'kanbanStatus',
|
|
description: 'Get a prompt summarising the current kanban board state',
|
|
func: kanbanStatusPrompt,
|
|
tags: ['kanban'],
|
|
})
|