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.
Files changed (165) hide show
  1. package/bin/cli.mjs +204 -2
  2. package/lib/doctor.mjs +38 -1
  3. package/lib/hydrate.mjs +15 -0
  4. package/lib/scaffold.mjs +5 -0
  5. package/lib/setup-wizard.mjs +35 -2
  6. package/package.json +1 -1
  7. package/scaffold/.context/project.yaml.example +19 -0
  8. package/scaffold/mcp-notification-server/package.json +18 -0
  9. package/scaffold/mcp-notification-server/src/index.ts +275 -0
  10. package/scaffold/mcp-notification-server/src/turso-client.ts +142 -0
  11. package/scaffold/mcp-notification-server/tsconfig.json +14 -0
  12. package/scaffold/mcp-pm/package.json +19 -0
  13. package/scaffold/mcp-pm/src/api-client.ts +69 -0
  14. package/scaffold/mcp-pm/src/index.ts +660 -0
  15. package/scaffold/mcp-pm/tsconfig.json +14 -0
  16. package/scaffold/pm-api/package.json +21 -0
  17. package/scaffold/pm-api/sql/001-memo-v2.sql +49 -0
  18. package/scaffold/pm-api/sql/002-notifications.sql +18 -0
  19. package/scaffold/pm-api/sql/003-content.sql +66 -0
  20. package/scaffold/pm-api/sql/004-agent-events.sql +21 -0
  21. package/scaffold/pm-api/sql/005-epic-sprint-decoupling.sql +6 -0
  22. package/scaffold/pm-api/sql/schema-core.sql +331 -0
  23. package/scaffold/pm-api/sql/schema-docs.sql +25 -0
  24. package/scaffold/pm-api/sql/schema-meetings.sql +17 -0
  25. package/scaffold/pm-api/sql/schema-rewards.sql +16 -0
  26. package/scaffold/pm-api/src/auth.ts +28 -0
  27. package/scaffold/pm-api/src/blockchain/adapter.ts +20 -0
  28. package/scaffold/pm-api/src/blockchain/tron.ts +62 -0
  29. package/scaffold/pm-api/src/db/adapter.ts +36 -0
  30. package/scaffold/pm-api/src/db/turso.ts +147 -0
  31. package/scaffold/pm-api/src/index.ts +114 -0
  32. package/scaffold/pm-api/src/mcp-tools/dashboard.ts +40 -0
  33. package/scaffold/pm-api/src/mcp-tools/epic.ts +67 -0
  34. package/scaffold/pm-api/src/mcp-tools/event.ts +89 -0
  35. package/scaffold/pm-api/src/mcp-tools/index.ts +11 -0
  36. package/scaffold/pm-api/src/mcp-tools/initiative.ts +51 -0
  37. package/scaffold/pm-api/src/mcp-tools/memo.ts +164 -0
  38. package/scaffold/pm-api/src/mcp-tools/notification.ts +37 -0
  39. package/scaffold/pm-api/src/mcp-tools/retro.ts +183 -0
  40. package/scaffold/pm-api/src/mcp-tools/sprint.ts +204 -0
  41. package/scaffold/pm-api/src/mcp-tools/standup.ts +136 -0
  42. package/scaffold/pm-api/src/mcp-tools/story.ts +230 -0
  43. package/scaffold/pm-api/src/mcp-tools/task.ts +187 -0
  44. package/scaffold/pm-api/src/mcp-tools/utils.ts +83 -0
  45. package/scaffold/pm-api/src/mcp.ts +871 -0
  46. package/scaffold/pm-api/src/nudge.ts +283 -0
  47. package/scaffold/pm-api/src/routes/auth.ts +32 -0
  48. package/scaffold/pm-api/src/routes/v2-activity.ts +27 -0
  49. package/scaffold/pm-api/src/routes/v2-admin.ts +165 -0
  50. package/scaffold/pm-api/src/routes/v2-dashboard.ts +189 -0
  51. package/scaffold/pm-api/src/routes/v2-docs.ts +34 -0
  52. package/scaffold/pm-api/src/routes/v2-initiatives.ts +118 -0
  53. package/scaffold/pm-api/src/routes/v2-kickoff.ts +265 -0
  54. package/scaffold/pm-api/src/routes/v2-meetings.ts +324 -0
  55. package/scaffold/pm-api/src/routes/v2-memos.ts +257 -0
  56. package/scaffold/pm-api/src/routes/v2-nav.ts +260 -0
  57. package/scaffold/pm-api/src/routes/v2-notifications.ts +79 -0
  58. package/scaffold/pm-api/src/routes/v2-page-content.ts +35 -0
  59. package/scaffold/pm-api/src/routes/v2-pm.ts +380 -0
  60. package/scaffold/pm-api/src/routes/v2-policy.ts +58 -0
  61. package/scaffold/pm-api/src/routes/v2-retro.ts +221 -0
  62. package/scaffold/pm-api/src/routes/v2-rewards.ts +132 -0
  63. package/scaffold/pm-api/src/routes/v2-scenarios.ts +48 -0
  64. package/scaffold/pm-api/src/routes/v2-search.ts +32 -0
  65. package/scaffold/pm-api/src/routes/v2-standup.ts +127 -0
  66. package/scaffold/pm-api/src/routes/v2-user.ts +38 -0
  67. package/scaffold/pm-api/src/types.ts +11 -0
  68. package/scaffold/pm-api/src/utils/activity.ts +22 -0
  69. package/scaffold/pm-api/src/utils/admin.ts +9 -0
  70. package/scaffold/pm-api/src/utils/agent-notify.ts +62 -0
  71. package/scaffold/pm-api/src/utils/assignee.ts +69 -0
  72. package/scaffold/pm-api/src/utils/db.ts +45 -0
  73. package/scaffold/pm-api/src/utils/initiative.ts +23 -0
  74. package/scaffold/pm-api/src/utils/retro-link.ts +32 -0
  75. package/scaffold/pm-api/src/utils/sprint-lifecycle.ts +96 -0
  76. package/scaffold/pm-api/tsconfig.json +15 -0
  77. package/scaffold/pm-api/wrangler.toml.hbs +11 -0
  78. package/scaffold/spec-site/package-lock.json +892 -0
  79. package/scaffold/spec-site/package.json +15 -1
  80. package/scaffold/spec-site/src/api/types.ts +6 -0
  81. package/scaffold/spec-site/src/components/AppHeader.vue +429 -55
  82. package/scaffold/spec-site/src/components/AuthGate.vue +117 -0
  83. package/scaffold/spec-site/src/components/BurndownChart.vue +78 -0
  84. package/scaffold/spec-site/src/components/DocComments.vue +137 -0
  85. package/scaffold/spec-site/src/components/DocEditor.vue +118 -0
  86. package/scaffold/spec-site/src/components/DocExportBar.vue +110 -0
  87. package/scaffold/spec-site/src/components/DocsSidebar.vue +309 -0
  88. package/scaffold/spec-site/src/components/EmptyState.vue +30 -0
  89. package/scaffold/spec-site/src/components/ErrorBanner.vue +38 -0
  90. package/scaffold/spec-site/src/components/Icon.vue +58 -0
  91. package/scaffold/spec-site/src/components/MemberSelect.vue +48 -0
  92. package/scaffold/spec-site/src/components/MemoChecklist.vue +88 -0
  93. package/scaffold/spec-site/src/components/MemoGraph.vue +75 -0
  94. package/scaffold/spec-site/src/components/MemoItem.vue +353 -0
  95. package/scaffold/spec-site/src/components/MemoRelations.vue +101 -0
  96. package/scaffold/spec-site/src/components/MemoTimeline.vue +53 -0
  97. package/scaffold/spec-site/src/components/MentionInput.vue +174 -0
  98. package/scaffold/spec-site/src/components/NotificationDropdown.vue +116 -0
  99. package/scaffold/spec-site/src/components/PriorityBadge.vue +23 -0
  100. package/scaffold/spec-site/src/components/SearchModal.vue +102 -0
  101. package/scaffold/spec-site/src/components/SlashCommand.ts +123 -0
  102. package/scaffold/spec-site/src/components/StateDisplay.vue +54 -0
  103. package/scaffold/spec-site/src/components/TreeNode.vue +82 -0
  104. package/scaffold/spec-site/src/components/UserAvatar.vue +24 -0
  105. package/scaffold/spec-site/src/components/VelocityChart.vue +77 -0
  106. package/scaffold/spec-site/src/composables/navTypes.ts +3 -0
  107. package/scaffold/spec-site/src/composables/pmTypes.ts +15 -2
  108. package/scaffold/spec-site/src/composables/useBottomSheet.ts +103 -0
  109. package/scaffold/spec-site/src/composables/useDashboard.ts +221 -0
  110. package/scaffold/spec-site/src/composables/useMediaQuery.ts +28 -0
  111. package/scaffold/spec-site/src/composables/useMemo.ts +39 -0
  112. package/scaffold/spec-site/src/composables/useNotification.ts +200 -0
  113. package/scaffold/spec-site/src/composables/usePmStore.ts +48 -1
  114. package/scaffold/spec-site/src/composables/useRetro.ts +6 -0
  115. package/scaffold/spec-site/src/composables/useStandup.ts +201 -0
  116. package/scaffold/spec-site/src/composables/useTheme.ts +37 -0
  117. package/scaffold/spec-site/src/composables/useTurso.ts +17 -0
  118. package/scaffold/spec-site/src/composables/useUser.ts +19 -1
  119. package/scaffold/spec-site/src/composables/useViewport.ts +26 -0
  120. package/scaffold/spec-site/src/features.ts +108 -0
  121. package/scaffold/spec-site/src/mockup/ComponentPalette.vue +61 -0
  122. package/scaffold/spec-site/src/mockup/MockupCanvas.vue +459 -0
  123. package/scaffold/spec-site/src/mockup/PropertyPanel.vue +217 -0
  124. package/scaffold/spec-site/src/mockup/componentCatalog.ts +68 -0
  125. package/scaffold/spec-site/src/mockup/useScenarios.ts +67 -0
  126. package/scaffold/spec-site/src/pages/AdminPage.vue +299 -0
  127. package/scaffold/spec-site/src/pages/DashboardPage.vue +650 -0
  128. package/scaffold/spec-site/src/pages/DocsEditor.vue +119 -0
  129. package/scaffold/spec-site/src/pages/DocsHub.vue +157 -0
  130. package/scaffold/spec-site/src/pages/DocsPage.vue +444 -0
  131. package/scaffold/spec-site/src/pages/InboxPage.vue +156 -0
  132. package/scaffold/spec-site/src/pages/MeetingsPage.vue +294 -0
  133. package/scaffold/spec-site/src/pages/MemosPage.vue +857 -0
  134. package/scaffold/spec-site/src/pages/MockupEditorPage.vue +611 -0
  135. package/scaffold/spec-site/src/pages/MockupListPage.vue +121 -0
  136. package/scaffold/spec-site/src/pages/MockupViewerPage.vue +199 -0
  137. package/scaffold/spec-site/src/pages/MyPage.vue +343 -0
  138. package/scaffold/spec-site/src/pages/NotificationSettingsPage.vue +59 -0
  139. package/scaffold/spec-site/src/pages/RewardsPage.vue +266 -0
  140. package/scaffold/spec-site/src/pages/SprintAdmin.vue +521 -0
  141. package/scaffold/spec-site/src/pages/SprintTimeline.vue +159 -0
  142. package/scaffold/spec-site/src/pages/board/BoardAdmin.vue +422 -0
  143. package/scaffold/spec-site/src/pages/board/BoardEpicSection.vue +54 -0
  144. package/scaffold/spec-site/src/pages/board/BoardPage.vue +884 -0
  145. package/scaffold/spec-site/src/pages/board/BoardStoryCard.vue +67 -0
  146. package/scaffold/spec-site/src/pages/board/BoardTaskItem.vue +52 -0
  147. package/scaffold/spec-site/src/pages/board/KanbanBoard.vue +93 -0
  148. package/scaffold/spec-site/src/pages/board/MyTasksPage.vue +202 -0
  149. package/scaffold/spec-site/src/pages/board/SprintClose.vue +167 -0
  150. package/scaffold/spec-site/src/pages/board/SprintColumn.vue +49 -0
  151. package/scaffold/spec-site/src/pages/board/SprintKickoff.vue +389 -0
  152. package/scaffold/spec-site/src/pages/board/StatusBadge.vue +52 -0
  153. package/scaffold/spec-site/src/pages/board/StoryDetailPanel.vue +495 -0
  154. package/scaffold/spec-site/src/pages/board/TaskCard.vue +42 -0
  155. package/scaffold/spec-site/src/pages/retro/RetroCard.vue +36 -2
  156. package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +82 -66
  157. package/scaffold/spec-site/src/pages/retro/RetroPage.vue +47 -18
  158. package/scaffold/spec-site/src/pages/standup/StandupEntryCard.vue +551 -0
  159. package/scaffold/spec-site/src/pages/standup/StandupForm.vue +68 -0
  160. package/scaffold/spec-site/src/pages/standup/StandupList.vue +71 -0
  161. package/scaffold/spec-site/src/pages/standup/StandupPage.vue +225 -0
  162. package/scaffold/spec-site/src/router.ts +141 -0
  163. package/scaffold/spec-site/src/styles/buttons.css +124 -0
  164. package/scaffold/spec-site/src/utils/parseMentions.ts +56 -0
  165. 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
+ }