popilot 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.mjs +204 -2
- package/lib/doctor.mjs +38 -1
- package/lib/hydrate.mjs +15 -0
- package/lib/scaffold.mjs +5 -0
- package/lib/setup-wizard.mjs +35 -2
- package/package.json +1 -1
- package/scaffold/.context/project.yaml.example +19 -0
- package/scaffold/mcp-notification-server/package.json +18 -0
- package/scaffold/mcp-notification-server/src/index.ts +275 -0
- package/scaffold/mcp-notification-server/src/turso-client.ts +142 -0
- package/scaffold/mcp-notification-server/tsconfig.json +14 -0
- package/scaffold/mcp-pm/package.json +19 -0
- package/scaffold/mcp-pm/src/api-client.ts +69 -0
- package/scaffold/mcp-pm/src/index.ts +660 -0
- package/scaffold/mcp-pm/tsconfig.json +14 -0
- package/scaffold/pm-api/package.json +21 -0
- package/scaffold/pm-api/sql/001-memo-v2.sql +49 -0
- package/scaffold/pm-api/sql/002-notifications.sql +18 -0
- package/scaffold/pm-api/sql/003-content.sql +66 -0
- package/scaffold/pm-api/sql/004-agent-events.sql +21 -0
- package/scaffold/pm-api/sql/005-epic-sprint-decoupling.sql +6 -0
- package/scaffold/pm-api/sql/schema-core.sql +331 -0
- package/scaffold/pm-api/sql/schema-docs.sql +25 -0
- package/scaffold/pm-api/sql/schema-meetings.sql +17 -0
- package/scaffold/pm-api/sql/schema-rewards.sql +16 -0
- package/scaffold/pm-api/src/auth.ts +28 -0
- package/scaffold/pm-api/src/blockchain/adapter.ts +20 -0
- package/scaffold/pm-api/src/blockchain/tron.ts +62 -0
- package/scaffold/pm-api/src/db/adapter.ts +36 -0
- package/scaffold/pm-api/src/db/turso.ts +147 -0
- package/scaffold/pm-api/src/index.ts +114 -0
- package/scaffold/pm-api/src/mcp-tools/dashboard.ts +40 -0
- package/scaffold/pm-api/src/mcp-tools/epic.ts +67 -0
- package/scaffold/pm-api/src/mcp-tools/event.ts +89 -0
- package/scaffold/pm-api/src/mcp-tools/index.ts +11 -0
- package/scaffold/pm-api/src/mcp-tools/initiative.ts +51 -0
- package/scaffold/pm-api/src/mcp-tools/memo.ts +164 -0
- package/scaffold/pm-api/src/mcp-tools/notification.ts +37 -0
- package/scaffold/pm-api/src/mcp-tools/retro.ts +183 -0
- package/scaffold/pm-api/src/mcp-tools/sprint.ts +204 -0
- package/scaffold/pm-api/src/mcp-tools/standup.ts +136 -0
- package/scaffold/pm-api/src/mcp-tools/story.ts +230 -0
- package/scaffold/pm-api/src/mcp-tools/task.ts +187 -0
- package/scaffold/pm-api/src/mcp-tools/utils.ts +83 -0
- package/scaffold/pm-api/src/mcp.ts +871 -0
- package/scaffold/pm-api/src/nudge.ts +283 -0
- package/scaffold/pm-api/src/routes/auth.ts +32 -0
- package/scaffold/pm-api/src/routes/v2-activity.ts +27 -0
- package/scaffold/pm-api/src/routes/v2-admin.ts +165 -0
- package/scaffold/pm-api/src/routes/v2-dashboard.ts +189 -0
- package/scaffold/pm-api/src/routes/v2-docs.ts +34 -0
- package/scaffold/pm-api/src/routes/v2-initiatives.ts +118 -0
- package/scaffold/pm-api/src/routes/v2-kickoff.ts +265 -0
- package/scaffold/pm-api/src/routes/v2-meetings.ts +324 -0
- package/scaffold/pm-api/src/routes/v2-memos.ts +257 -0
- package/scaffold/pm-api/src/routes/v2-nav.ts +260 -0
- package/scaffold/pm-api/src/routes/v2-notifications.ts +79 -0
- package/scaffold/pm-api/src/routes/v2-page-content.ts +35 -0
- package/scaffold/pm-api/src/routes/v2-pm.ts +380 -0
- package/scaffold/pm-api/src/routes/v2-policy.ts +58 -0
- package/scaffold/pm-api/src/routes/v2-retro.ts +221 -0
- package/scaffold/pm-api/src/routes/v2-rewards.ts +132 -0
- package/scaffold/pm-api/src/routes/v2-scenarios.ts +48 -0
- package/scaffold/pm-api/src/routes/v2-search.ts +32 -0
- package/scaffold/pm-api/src/routes/v2-standup.ts +127 -0
- package/scaffold/pm-api/src/routes/v2-user.ts +38 -0
- package/scaffold/pm-api/src/types.ts +11 -0
- package/scaffold/pm-api/src/utils/activity.ts +22 -0
- package/scaffold/pm-api/src/utils/admin.ts +9 -0
- package/scaffold/pm-api/src/utils/agent-notify.ts +62 -0
- package/scaffold/pm-api/src/utils/assignee.ts +69 -0
- package/scaffold/pm-api/src/utils/db.ts +45 -0
- package/scaffold/pm-api/src/utils/initiative.ts +23 -0
- package/scaffold/pm-api/src/utils/retro-link.ts +32 -0
- package/scaffold/pm-api/src/utils/sprint-lifecycle.ts +96 -0
- package/scaffold/pm-api/tsconfig.json +15 -0
- package/scaffold/pm-api/wrangler.toml.hbs +11 -0
- package/scaffold/spec-site/package-lock.json +892 -0
- package/scaffold/spec-site/package.json +15 -1
- package/scaffold/spec-site/src/api/types.ts +6 -0
- package/scaffold/spec-site/src/components/AppHeader.vue +429 -55
- package/scaffold/spec-site/src/components/AuthGate.vue +117 -0
- package/scaffold/spec-site/src/components/BurndownChart.vue +78 -0
- package/scaffold/spec-site/src/components/DocComments.vue +137 -0
- package/scaffold/spec-site/src/components/DocEditor.vue +118 -0
- package/scaffold/spec-site/src/components/DocExportBar.vue +110 -0
- package/scaffold/spec-site/src/components/DocsSidebar.vue +309 -0
- package/scaffold/spec-site/src/components/EmptyState.vue +30 -0
- package/scaffold/spec-site/src/components/ErrorBanner.vue +38 -0
- package/scaffold/spec-site/src/components/Icon.vue +58 -0
- package/scaffold/spec-site/src/components/MemberSelect.vue +48 -0
- package/scaffold/spec-site/src/components/MemoChecklist.vue +88 -0
- package/scaffold/spec-site/src/components/MemoGraph.vue +75 -0
- package/scaffold/spec-site/src/components/MemoItem.vue +353 -0
- package/scaffold/spec-site/src/components/MemoRelations.vue +101 -0
- package/scaffold/spec-site/src/components/MemoTimeline.vue +53 -0
- package/scaffold/spec-site/src/components/MentionInput.vue +174 -0
- package/scaffold/spec-site/src/components/NotificationDropdown.vue +116 -0
- package/scaffold/spec-site/src/components/PriorityBadge.vue +23 -0
- package/scaffold/spec-site/src/components/SearchModal.vue +102 -0
- package/scaffold/spec-site/src/components/SlashCommand.ts +123 -0
- package/scaffold/spec-site/src/components/StateDisplay.vue +54 -0
- package/scaffold/spec-site/src/components/TreeNode.vue +82 -0
- package/scaffold/spec-site/src/components/UserAvatar.vue +24 -0
- package/scaffold/spec-site/src/components/VelocityChart.vue +77 -0
- package/scaffold/spec-site/src/composables/navTypes.ts +3 -0
- package/scaffold/spec-site/src/composables/pmTypes.ts +15 -2
- package/scaffold/spec-site/src/composables/useBottomSheet.ts +103 -0
- package/scaffold/spec-site/src/composables/useDashboard.ts +221 -0
- package/scaffold/spec-site/src/composables/useMediaQuery.ts +28 -0
- package/scaffold/spec-site/src/composables/useMemo.ts +39 -0
- package/scaffold/spec-site/src/composables/useNotification.ts +200 -0
- package/scaffold/spec-site/src/composables/usePmStore.ts +48 -1
- package/scaffold/spec-site/src/composables/useRetro.ts +6 -0
- package/scaffold/spec-site/src/composables/useStandup.ts +201 -0
- package/scaffold/spec-site/src/composables/useTheme.ts +37 -0
- package/scaffold/spec-site/src/composables/useTurso.ts +17 -0
- package/scaffold/spec-site/src/composables/useUser.ts +19 -1
- package/scaffold/spec-site/src/composables/useViewport.ts +26 -0
- package/scaffold/spec-site/src/features.ts +108 -0
- package/scaffold/spec-site/src/mockup/ComponentPalette.vue +61 -0
- package/scaffold/spec-site/src/mockup/MockupCanvas.vue +459 -0
- package/scaffold/spec-site/src/mockup/PropertyPanel.vue +217 -0
- package/scaffold/spec-site/src/mockup/componentCatalog.ts +68 -0
- package/scaffold/spec-site/src/mockup/useScenarios.ts +67 -0
- package/scaffold/spec-site/src/pages/AdminPage.vue +299 -0
- package/scaffold/spec-site/src/pages/DashboardPage.vue +650 -0
- package/scaffold/spec-site/src/pages/DocsEditor.vue +119 -0
- package/scaffold/spec-site/src/pages/DocsHub.vue +157 -0
- package/scaffold/spec-site/src/pages/DocsPage.vue +444 -0
- package/scaffold/spec-site/src/pages/InboxPage.vue +156 -0
- package/scaffold/spec-site/src/pages/MeetingsPage.vue +294 -0
- package/scaffold/spec-site/src/pages/MemosPage.vue +857 -0
- package/scaffold/spec-site/src/pages/MockupEditorPage.vue +611 -0
- package/scaffold/spec-site/src/pages/MockupListPage.vue +121 -0
- package/scaffold/spec-site/src/pages/MockupViewerPage.vue +199 -0
- package/scaffold/spec-site/src/pages/MyPage.vue +343 -0
- package/scaffold/spec-site/src/pages/NotificationSettingsPage.vue +59 -0
- package/scaffold/spec-site/src/pages/RewardsPage.vue +266 -0
- package/scaffold/spec-site/src/pages/SprintAdmin.vue +521 -0
- package/scaffold/spec-site/src/pages/SprintTimeline.vue +159 -0
- package/scaffold/spec-site/src/pages/board/BoardAdmin.vue +422 -0
- package/scaffold/spec-site/src/pages/board/BoardEpicSection.vue +54 -0
- package/scaffold/spec-site/src/pages/board/BoardPage.vue +884 -0
- package/scaffold/spec-site/src/pages/board/BoardStoryCard.vue +67 -0
- package/scaffold/spec-site/src/pages/board/BoardTaskItem.vue +52 -0
- package/scaffold/spec-site/src/pages/board/KanbanBoard.vue +93 -0
- package/scaffold/spec-site/src/pages/board/MyTasksPage.vue +202 -0
- package/scaffold/spec-site/src/pages/board/SprintClose.vue +167 -0
- package/scaffold/spec-site/src/pages/board/SprintColumn.vue +49 -0
- package/scaffold/spec-site/src/pages/board/SprintKickoff.vue +389 -0
- package/scaffold/spec-site/src/pages/board/StatusBadge.vue +52 -0
- package/scaffold/spec-site/src/pages/board/StoryDetailPanel.vue +495 -0
- package/scaffold/spec-site/src/pages/board/TaskCard.vue +42 -0
- package/scaffold/spec-site/src/pages/retro/RetroCard.vue +36 -2
- package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +82 -66
- package/scaffold/spec-site/src/pages/retro/RetroPage.vue +47 -18
- package/scaffold/spec-site/src/pages/standup/StandupEntryCard.vue +551 -0
- package/scaffold/spec-site/src/pages/standup/StandupForm.vue +68 -0
- package/scaffold/spec-site/src/pages/standup/StandupList.vue +71 -0
- package/scaffold/spec-site/src/pages/standup/StandupPage.vue +225 -0
- package/scaffold/spec-site/src/router.ts +141 -0
- package/scaffold/spec-site/src/styles/buttons.css +124 -0
- package/scaffold/spec-site/src/utils/parseMentions.ts +56 -0
- package/scaffold/spec-site/src/utils/timezone.ts +18 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { query, execute } from './turso-client.js'
|
|
5
|
+
|
|
6
|
+
const server = new McpServer({
|
|
7
|
+
name: 'spec-notifications',
|
|
8
|
+
version: '1.0.0',
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
// Tool 1: Check unread notifications
|
|
12
|
+
server.tool(
|
|
13
|
+
'check_notifications',
|
|
14
|
+
'Check unread notifications for a user (max 20)',
|
|
15
|
+
{ user_name: z.string().describe('User name to check notifications for') },
|
|
16
|
+
async ({ user_name }) => {
|
|
17
|
+
const result = await query<{
|
|
18
|
+
id: number; type: string; title: string; body: string | null
|
|
19
|
+
source_type: string; source_id: number; page_id: string
|
|
20
|
+
actor: string; is_read: number; created_at: string
|
|
21
|
+
}>(
|
|
22
|
+
`SELECT id, type, title, body, source_type, source_id, page_id, actor, is_read, created_at
|
|
23
|
+
FROM notifications
|
|
24
|
+
WHERE user_name = ? AND is_read = 0
|
|
25
|
+
ORDER BY created_at DESC
|
|
26
|
+
LIMIT 20`,
|
|
27
|
+
[user_name],
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if (result.error) {
|
|
31
|
+
return { content: [{ type: 'text' as const, text: `Error: ${result.error}` }] }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (result.rows.length === 0) {
|
|
35
|
+
return { content: [{ type: 'text' as const, text: `No unread notifications for ${user_name}.` }] }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const lines = result.rows.map((n, i) => {
|
|
39
|
+
const icon = n.type === 'memo_assigned' ? '📩' : n.type === 'reply_received' ? '💬' : '📢'
|
|
40
|
+
return `${i + 1}. ${icon} [ID:${n.id}] ${n.title}\n ${n.body ?? ''}\n Page: ${n.page_id} | ${n.created_at}`
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
content: [{
|
|
45
|
+
type: 'text' as const,
|
|
46
|
+
text: `Unread notifications for ${user_name} (${result.rows.length}):\n\n${lines.join('\n\n')}`,
|
|
47
|
+
}],
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
// Tool 2: Mark notification as read
|
|
53
|
+
server.tool(
|
|
54
|
+
'mark_notification_read',
|
|
55
|
+
'Mark a notification as read by ID',
|
|
56
|
+
{ notification_id: z.number().describe('Notification ID to mark as read') },
|
|
57
|
+
async ({ notification_id }) => {
|
|
58
|
+
const result = await execute(
|
|
59
|
+
'UPDATE notifications SET is_read = 1 WHERE id = ?',
|
|
60
|
+
[notification_id],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if (result.error) {
|
|
64
|
+
return { content: [{ type: 'text' as const, text: `Error: ${result.error}` }] }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
content: [{
|
|
69
|
+
type: 'text' as const,
|
|
70
|
+
text: result.rowsAffected > 0
|
|
71
|
+
? `Notification #${notification_id} marked as read.`
|
|
72
|
+
: `Notification #${notification_id} not found.`,
|
|
73
|
+
}],
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
// Tool 3: Check open memos assigned to user
|
|
79
|
+
server.tool(
|
|
80
|
+
'check_open_memos',
|
|
81
|
+
'Check all open memos assigned to a user',
|
|
82
|
+
{ user_name: z.string().describe('User name to check memos for') },
|
|
83
|
+
async ({ user_name }) => {
|
|
84
|
+
const result = await query<{
|
|
85
|
+
id: number; page_id: string; content: string; memo_type: string
|
|
86
|
+
created_by: string; assigned_to: string | null; created_at: string
|
|
87
|
+
}>(
|
|
88
|
+
`SELECT id, page_id, content, memo_type, created_by, assigned_to, created_at
|
|
89
|
+
FROM memos_v2
|
|
90
|
+
WHERE assigned_to = ? AND status = 'open'
|
|
91
|
+
ORDER BY created_at DESC`,
|
|
92
|
+
[user_name],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if (result.error) {
|
|
96
|
+
return { content: [{ type: 'text' as const, text: `Error: ${result.error}` }] }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (result.rows.length === 0) {
|
|
100
|
+
return { content: [{ type: 'text' as const, text: `No open memos assigned to ${user_name}.` }] }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const typeIcons: Record<string, string> = {
|
|
104
|
+
memo: '📝', decision: '⚡', request: '📋', backlog: '💡',
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const lines = result.rows.map((m, i) => {
|
|
108
|
+
const icon = typeIcons[m.memo_type] ?? '📝'
|
|
109
|
+
const preview = m.content.length > 80 ? m.content.slice(0, 80) + '…' : m.content
|
|
110
|
+
return `${i + 1}. ${icon} [ID:${m.id}] ${m.created_by} → ${m.assigned_to}\n ${preview}\n Page: ${m.page_id} | ${m.created_at}`
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
content: [{
|
|
115
|
+
type: 'text' as const,
|
|
116
|
+
text: `Open memos for ${user_name} (${result.rows.length}):\n\n${lines.join('\n\n')}`,
|
|
117
|
+
}],
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
// Tool 4: Create notification
|
|
123
|
+
server.tool(
|
|
124
|
+
'create_notification',
|
|
125
|
+
'Create a notification for a user (memo events: assignment, reply, resolve, reopen)',
|
|
126
|
+
{
|
|
127
|
+
user_name: z.string().describe('Notification recipient'),
|
|
128
|
+
type: z.enum(['memo_assigned', 'memo_mention_all', 'reply_received', 'memo_resolved', 'memo_reopened'])
|
|
129
|
+
.describe('Notification type'),
|
|
130
|
+
title: z.string().describe('Notification title'),
|
|
131
|
+
body: z.string().optional().describe('Preview body (60 chars max)'),
|
|
132
|
+
source_id: z.number().describe('Related memo ID'),
|
|
133
|
+
page_id: z.string().describe('Page ID the memo belongs to'),
|
|
134
|
+
actor: z.string().describe('Name of the person who performed the action'),
|
|
135
|
+
},
|
|
136
|
+
async ({ user_name, type, title, body, source_id, page_id, actor }) => {
|
|
137
|
+
const result = await execute(
|
|
138
|
+
`INSERT INTO notifications (user_name, type, title, body, source_type, source_id, page_id, actor)
|
|
139
|
+
VALUES (?, ?, ?, ?, 'memo', ?, ?, ?)`,
|
|
140
|
+
[user_name, type, title, body ?? null, source_id, page_id, actor],
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if (result.error) {
|
|
144
|
+
return { content: [{ type: 'text' as const, text: `Error: ${result.error}` }] }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
content: [{
|
|
149
|
+
type: 'text' as const,
|
|
150
|
+
text: `Notification created: [${type}] "${title}" for ${user_name}`,
|
|
151
|
+
}],
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
// Tool 5: Reply to memo
|
|
157
|
+
server.tool(
|
|
158
|
+
'reply_memo',
|
|
159
|
+
'Reply to a memo and notify the memo author',
|
|
160
|
+
{
|
|
161
|
+
memo_id: z.number().describe('Memo ID to reply to'),
|
|
162
|
+
content: z.string().describe('Reply content'),
|
|
163
|
+
created_by: z.string().describe('Reply author name'),
|
|
164
|
+
},
|
|
165
|
+
async ({ memo_id, content, created_by }) => {
|
|
166
|
+
const memo = await query<{
|
|
167
|
+
id: number; page_id: string; created_by: string; assigned_to: string | null; status: string
|
|
168
|
+
}>(
|
|
169
|
+
'SELECT id, page_id, created_by, assigned_to, status FROM memos_v2 WHERE id = ?',
|
|
170
|
+
[memo_id],
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if (memo.error) {
|
|
174
|
+
return { content: [{ type: 'text' as const, text: `Error: ${memo.error}` }] }
|
|
175
|
+
}
|
|
176
|
+
if (memo.rows.length === 0) {
|
|
177
|
+
return { content: [{ type: 'text' as const, text: `Memo #${memo_id} not found.` }] }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const m = memo.rows[0]
|
|
181
|
+
|
|
182
|
+
const insertResult = await execute(
|
|
183
|
+
'INSERT INTO memo_replies (memo_id, content, created_by) VALUES (?, ?, ?)',
|
|
184
|
+
[memo_id, content, created_by],
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if (insertResult.error) {
|
|
188
|
+
return { content: [{ type: 'text' as const, text: `Error: ${insertResult.error}` }] }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Notify memo author (skip self-notification)
|
|
192
|
+
if (m.created_by !== created_by) {
|
|
193
|
+
await execute(
|
|
194
|
+
`INSERT INTO notifications (user_name, type, title, body, source_type, source_id, page_id, actor)
|
|
195
|
+
VALUES (?, 'reply_received', ?, ?, 'memo', ?, ?, ?)`,
|
|
196
|
+
[
|
|
197
|
+
m.created_by,
|
|
198
|
+
`${created_by} replied to your memo`,
|
|
199
|
+
content.length > 60 ? content.slice(0, 57) + '…' : content,
|
|
200
|
+
memo_id,
|
|
201
|
+
m.page_id,
|
|
202
|
+
created_by,
|
|
203
|
+
],
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
content: [{
|
|
209
|
+
type: 'text' as const,
|
|
210
|
+
text: `Reply posted on memo #${memo_id} by ${created_by}.\nContent: ${content}`,
|
|
211
|
+
}],
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
// Tool 6: Resolve memo
|
|
217
|
+
server.tool(
|
|
218
|
+
'resolve_memo',
|
|
219
|
+
'Mark a memo as resolved and notify related users',
|
|
220
|
+
{
|
|
221
|
+
memo_id: z.number().describe('Memo ID to resolve'),
|
|
222
|
+
resolved_by: z.string().describe('Name of resolver'),
|
|
223
|
+
},
|
|
224
|
+
async ({ memo_id, resolved_by }) => {
|
|
225
|
+
const memo = await query<{
|
|
226
|
+
id: number; page_id: string; created_by: string; assigned_to: string | null; status: string
|
|
227
|
+
}>(
|
|
228
|
+
'SELECT id, page_id, created_by, assigned_to, status FROM memos_v2 WHERE id = ?',
|
|
229
|
+
[memo_id],
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if (memo.error) {
|
|
233
|
+
return { content: [{ type: 'text' as const, text: `Error: ${memo.error}` }] }
|
|
234
|
+
}
|
|
235
|
+
if (memo.rows.length === 0) {
|
|
236
|
+
return { content: [{ type: 'text' as const, text: `Memo #${memo_id} not found.` }] }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const m = memo.rows[0]
|
|
240
|
+
if (m.status === 'resolved') {
|
|
241
|
+
return { content: [{ type: 'text' as const, text: `Memo #${memo_id} is already resolved.` }] }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const updateResult = await execute(
|
|
245
|
+
"UPDATE memos_v2 SET status = 'resolved' WHERE id = ?",
|
|
246
|
+
[memo_id],
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if (updateResult.error) {
|
|
250
|
+
return { content: [{ type: 'text' as const, text: `Error: ${updateResult.error}` }] }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (m.created_by !== resolved_by) {
|
|
254
|
+
await execute(
|
|
255
|
+
`INSERT INTO notifications (user_name, type, title, body, source_type, source_id, page_id, actor)
|
|
256
|
+
VALUES (?, 'memo_resolved', ?, NULL, 'memo', ?, ?, ?)`,
|
|
257
|
+
[m.created_by, `${resolved_by} resolved the memo`, memo_id, m.page_id, resolved_by],
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
content: [{
|
|
263
|
+
type: 'text' as const,
|
|
264
|
+
text: `Memo #${memo_id} resolved by ${resolved_by}.`,
|
|
265
|
+
}],
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
async function main() {
|
|
271
|
+
const transport = new StdioServerTransport()
|
|
272
|
+
await server.connect(transport)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
main().catch(console.error)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database HTTP API client (Turso Hrana v2 protocol)
|
|
3
|
+
* Reads DB_URL and DB_AUTH_TOKEN from environment.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const DB_URL = process.env.DB_URL ?? ''
|
|
7
|
+
const DB_AUTH_TOKEN = process.env.DB_AUTH_TOKEN ?? ''
|
|
8
|
+
|
|
9
|
+
type ArgValue = string | number | null
|
|
10
|
+
|
|
11
|
+
interface HranaValue {
|
|
12
|
+
type: 'text' | 'integer' | 'float' | 'null'
|
|
13
|
+
value?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function toHranaArg(v: ArgValue): HranaValue {
|
|
17
|
+
if (v === null) return { type: 'null' }
|
|
18
|
+
if (typeof v === 'number') {
|
|
19
|
+
return Number.isInteger(v)
|
|
20
|
+
? { type: 'integer', value: String(v) }
|
|
21
|
+
: { type: 'float', value: String(v) }
|
|
22
|
+
}
|
|
23
|
+
return { type: 'text', value: String(v) }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface PipelineResult {
|
|
27
|
+
results: Array<{
|
|
28
|
+
type: 'ok' | 'error'
|
|
29
|
+
response?: {
|
|
30
|
+
type: string
|
|
31
|
+
result?: {
|
|
32
|
+
cols: Array<{ name: string }>
|
|
33
|
+
rows: Array<Array<HranaValue>>
|
|
34
|
+
affected_row_count: number
|
|
35
|
+
last_insert_rowid: string | null
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
error?: { message: string }
|
|
39
|
+
}>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function query<T = Record<string, unknown>>(
|
|
43
|
+
sql: string,
|
|
44
|
+
args: ArgValue[] = [],
|
|
45
|
+
): Promise<{ rows: T[]; error?: string }> {
|
|
46
|
+
if (!DB_URL || !DB_AUTH_TOKEN) {
|
|
47
|
+
return { rows: [], error: 'Missing DB_URL or DB_AUTH_TOKEN' }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const resp = await fetch(DB_URL + '/v2/pipeline', {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: {
|
|
54
|
+
Authorization: 'Bearer ' + DB_AUTH_TOKEN,
|
|
55
|
+
'Content-Type': 'application/json',
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
requests: [
|
|
59
|
+
{ type: 'execute', stmt: { sql, args: args.map(toHranaArg) } },
|
|
60
|
+
{ type: 'close' },
|
|
61
|
+
],
|
|
62
|
+
}),
|
|
63
|
+
signal: AbortSignal.timeout(10000),
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
if (!resp.ok) {
|
|
67
|
+
const text = await resp.text().catch(() => '')
|
|
68
|
+
return { rows: [], error: `HTTP ${resp.status}: ${text}` }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const data: PipelineResult = await resp.json()
|
|
72
|
+
const first = data.results[0]
|
|
73
|
+
|
|
74
|
+
if (first.type === 'error') {
|
|
75
|
+
return { rows: [], error: first.error?.message ?? 'Unknown error' }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const result = first.response?.result
|
|
79
|
+
if (!result) return { rows: [] }
|
|
80
|
+
|
|
81
|
+
const colNames = result.cols.map(c => c.name)
|
|
82
|
+
const rows = result.rows.map(row => {
|
|
83
|
+
const obj: Record<string, unknown> = {}
|
|
84
|
+
colNames.forEach((col, i) => {
|
|
85
|
+
const cell = row[i]
|
|
86
|
+
if (cell.type === 'null') obj[col] = null
|
|
87
|
+
else if (cell.type === 'integer') obj[col] = Number(cell.value)
|
|
88
|
+
else if (cell.type === 'float') obj[col] = Number(cell.value)
|
|
89
|
+
else obj[col] = cell.value
|
|
90
|
+
})
|
|
91
|
+
return obj as T
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
return { rows }
|
|
95
|
+
} catch (err: unknown) {
|
|
96
|
+
const message = err instanceof Error ? err.message : 'Unknown error'
|
|
97
|
+
return { rows: [], error: message }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function execute(
|
|
102
|
+
sql: string,
|
|
103
|
+
args: ArgValue[] = [],
|
|
104
|
+
): Promise<{ rowsAffected: number; error?: string }> {
|
|
105
|
+
if (!DB_URL || !DB_AUTH_TOKEN) {
|
|
106
|
+
return { rowsAffected: 0, error: 'Missing DB_URL or DB_AUTH_TOKEN' }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const resp = await fetch(DB_URL + '/v2/pipeline', {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: {
|
|
113
|
+
Authorization: 'Bearer ' + DB_AUTH_TOKEN,
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
},
|
|
116
|
+
body: JSON.stringify({
|
|
117
|
+
requests: [
|
|
118
|
+
{ type: 'execute', stmt: { sql, args: args.map(toHranaArg) } },
|
|
119
|
+
{ type: 'close' },
|
|
120
|
+
],
|
|
121
|
+
}),
|
|
122
|
+
signal: AbortSignal.timeout(10000),
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
if (!resp.ok) {
|
|
126
|
+
const text = await resp.text().catch(() => '')
|
|
127
|
+
return { rowsAffected: 0, error: `HTTP ${resp.status}: ${text}` }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const data: PipelineResult = await resp.json()
|
|
131
|
+
const first = data.results[0]
|
|
132
|
+
|
|
133
|
+
if (first.type === 'error') {
|
|
134
|
+
return { rowsAffected: 0, error: first.error?.message ?? 'Unknown error' }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { rowsAffected: first.response?.result?.affected_row_count ?? 0 }
|
|
138
|
+
} catch (err: unknown) {
|
|
139
|
+
const message = err instanceof Error ? err.message : 'Unknown error'
|
|
140
|
+
return { rowsAffected: 0, error: message }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-pm",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsc --watch"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
13
|
+
"zod": "^3.23.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"typescript": "^5.7.0",
|
|
17
|
+
"@types/node": "^22.0.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// PM API HTTP client
|
|
2
|
+
// All MCP tools use this client to communicate with the PM API server.
|
|
3
|
+
// Required env vars: PM_API_URL, PM_TOKEN
|
|
4
|
+
|
|
5
|
+
const PM_API_URL = process.env.PM_API_URL ?? ''
|
|
6
|
+
const PM_TOKEN = process.env.PM_TOKEN ?? ''
|
|
7
|
+
|
|
8
|
+
interface ApiResult<T> {
|
|
9
|
+
data: T | null
|
|
10
|
+
error?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function request<T>(method: string, path: string, body?: unknown): Promise<ApiResult<T>> {
|
|
14
|
+
if (!PM_API_URL || !PM_TOKEN) {
|
|
15
|
+
return { data: null, error: 'Missing PM_API_URL or PM_TOKEN' }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const resp = await fetch(`${PM_API_URL}${path}`, {
|
|
20
|
+
method,
|
|
21
|
+
headers: {
|
|
22
|
+
Authorization: `Bearer ${PM_TOKEN}`,
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
},
|
|
25
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
26
|
+
signal: AbortSignal.timeout(15000),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const data = await resp.json()
|
|
30
|
+
|
|
31
|
+
if (!resp.ok) {
|
|
32
|
+
return { data: null, error: data.error ?? `HTTP ${resp.status}` }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { data: data as T }
|
|
36
|
+
} catch (err: unknown) {
|
|
37
|
+
const message = err instanceof Error ? err.message : 'Unknown error'
|
|
38
|
+
return { data: null, error: message }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function apiGet<T>(path: string, params?: Record<string, string>): Promise<ApiResult<T>> {
|
|
43
|
+
let url = path
|
|
44
|
+
if (params) {
|
|
45
|
+
const search = new URLSearchParams()
|
|
46
|
+
for (const [k, v] of Object.entries(params)) {
|
|
47
|
+
if (v) search.set(k, v)
|
|
48
|
+
}
|
|
49
|
+
const qs = search.toString()
|
|
50
|
+
if (qs) url += '?' + qs
|
|
51
|
+
}
|
|
52
|
+
return request<T>('GET', url)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function apiPost<T>(path: string, body: unknown): Promise<ApiResult<T>> {
|
|
56
|
+
return request<T>('POST', path, body)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function apiPatch<T>(path: string, body: unknown): Promise<ApiResult<T>> {
|
|
60
|
+
return request<T>('PATCH', path, body)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function apiPut<T>(path: string, body: unknown): Promise<ApiResult<T>> {
|
|
64
|
+
return request<T>('PUT', path, body)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function apiDelete<T>(path: string): Promise<ApiResult<T>> {
|
|
68
|
+
return request<T>('DELETE', path)
|
|
69
|
+
}
|