@prmichaelsen/task-mcp 0.2.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 (142) hide show
  1. package/.env.example +19 -0
  2. package/AGENT.md +1165 -0
  3. package/CHANGELOG.md +72 -0
  4. package/agent/commands/acp.commit.md +511 -0
  5. package/agent/commands/acp.init.md +376 -0
  6. package/agent/commands/acp.package-install.md +347 -0
  7. package/agent/commands/acp.proceed.md +311 -0
  8. package/agent/commands/acp.report.md +392 -0
  9. package/agent/commands/acp.status.md +280 -0
  10. package/agent/commands/acp.sync.md +323 -0
  11. package/agent/commands/acp.update.md +301 -0
  12. package/agent/commands/acp.validate.md +385 -0
  13. package/agent/commands/acp.version-check-for-updates.md +275 -0
  14. package/agent/commands/acp.version-check.md +190 -0
  15. package/agent/commands/acp.version-update.md +288 -0
  16. package/agent/commands/command.template.md +273 -0
  17. package/agent/commands/git.commit.md +511 -0
  18. package/agent/commands/git.init.md +513 -0
  19. package/agent/design/.gitkeep +0 -0
  20. package/agent/design/acp-task-execution-requirements.md +555 -0
  21. package/agent/design/api-dto-design.md +394 -0
  22. package/agent/design/code-extraction-guide.md +827 -0
  23. package/agent/design/design.template.md +136 -0
  24. package/agent/design/requirements.template.md +387 -0
  25. package/agent/design/rest-api-integration.md +489 -0
  26. package/agent/design/sdk-export-requirements.md +549 -0
  27. package/agent/milestones/.gitkeep +0 -0
  28. package/agent/milestones/milestone-1-{title}.template.md +206 -0
  29. package/agent/milestones/milestone-2-task-infrastructure.md +232 -0
  30. package/agent/milestones/milestone-4-autonomous-execution.md +235 -0
  31. package/agent/patterns/.gitkeep +0 -0
  32. package/agent/patterns/bootstrap.md +1271 -0
  33. package/agent/patterns/bootstrap.template.md +1237 -0
  34. package/agent/patterns/pattern.template.md +364 -0
  35. package/agent/progress.template.yaml +158 -0
  36. package/agent/progress.yaml +375 -0
  37. package/agent/scripts/check-for-updates.sh +88 -0
  38. package/agent/scripts/install.sh +157 -0
  39. package/agent/scripts/uninstall.sh +75 -0
  40. package/agent/scripts/update.sh +139 -0
  41. package/agent/scripts/version.sh +35 -0
  42. package/agent/tasks/.gitkeep +0 -0
  43. package/agent/tasks/task-1-{title}.template.md +225 -0
  44. package/agent/tasks/task-86-task-data-model-schemas.md +143 -0
  45. package/agent/tasks/task-87-task-database-service.md +220 -0
  46. package/agent/tasks/task-88-firebase-client-wrapper.md +139 -0
  47. package/agent/tasks/task-88-task-execution-engine.md +277 -0
  48. package/agent/tasks/task-89-mcp-server-implementation.md +197 -0
  49. package/agent/tasks/task-90-build-configuration.md +146 -0
  50. package/agent/tasks/task-91-deployment-configuration.md +128 -0
  51. package/coverage/base.css +224 -0
  52. package/coverage/block-navigation.js +87 -0
  53. package/coverage/favicon.png +0 -0
  54. package/coverage/index.html +191 -0
  55. package/coverage/lcov-report/base.css +224 -0
  56. package/coverage/lcov-report/block-navigation.js +87 -0
  57. package/coverage/lcov-report/favicon.png +0 -0
  58. package/coverage/lcov-report/index.html +191 -0
  59. package/coverage/lcov-report/prettify.css +1 -0
  60. package/coverage/lcov-report/prettify.js +2 -0
  61. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  62. package/coverage/lcov-report/sorter.js +210 -0
  63. package/coverage/lcov-report/src/client.ts.html +1030 -0
  64. package/coverage/lcov-report/src/constant/collections.ts.html +469 -0
  65. package/coverage/lcov-report/src/constant/index.html +116 -0
  66. package/coverage/lcov-report/src/dto/index.html +116 -0
  67. package/coverage/lcov-report/src/dto/transformers.ts.html +568 -0
  68. package/coverage/lcov-report/src/index.html +146 -0
  69. package/coverage/lcov-report/src/schemas/index.html +116 -0
  70. package/coverage/lcov-report/src/schemas/task.ts.html +547 -0
  71. package/coverage/lcov-report/src/server-factory.ts.html +418 -0
  72. package/coverage/lcov-report/src/server.ts.html +289 -0
  73. package/coverage/lcov-report/src/services/index.html +116 -0
  74. package/coverage/lcov-report/src/services/task-database.service.ts.html +1495 -0
  75. package/coverage/lcov-report/src/tools/index.html +236 -0
  76. package/coverage/lcov-report/src/tools/index.ts.html +292 -0
  77. package/coverage/lcov-report/src/tools/task-add-message.ts.html +277 -0
  78. package/coverage/lcov-report/src/tools/task-complete-task-item.ts.html +343 -0
  79. package/coverage/lcov-report/src/tools/task-create-milestone.ts.html +286 -0
  80. package/coverage/lcov-report/src/tools/task-create-task-item.ts.html +358 -0
  81. package/coverage/lcov-report/src/tools/task-get-next-step.ts.html +460 -0
  82. package/coverage/lcov-report/src/tools/task-get-status.ts.html +316 -0
  83. package/coverage/lcov-report/src/tools/task-report-completion.ts.html +343 -0
  84. package/coverage/lcov-report/src/tools/task-update-progress.ts.html +232 -0
  85. package/coverage/lcov.info +974 -0
  86. package/coverage/prettify.css +1 -0
  87. package/coverage/prettify.js +2 -0
  88. package/coverage/sort-arrow-sprite.png +0 -0
  89. package/coverage/sorter.js +210 -0
  90. package/coverage/src/client.ts.html +1030 -0
  91. package/coverage/src/constant/collections.ts.html +469 -0
  92. package/coverage/src/constant/index.html +116 -0
  93. package/coverage/src/dto/index.html +116 -0
  94. package/coverage/src/dto/transformers.ts.html +568 -0
  95. package/coverage/src/index.html +146 -0
  96. package/coverage/src/schemas/index.html +116 -0
  97. package/coverage/src/schemas/task.ts.html +547 -0
  98. package/coverage/src/server-factory.ts.html +418 -0
  99. package/coverage/src/server.ts.html +289 -0
  100. package/coverage/src/services/index.html +116 -0
  101. package/coverage/src/services/task-database.service.ts.html +1495 -0
  102. package/coverage/src/tools/index.html +236 -0
  103. package/coverage/src/tools/index.ts.html +292 -0
  104. package/coverage/src/tools/task-add-message.ts.html +277 -0
  105. package/coverage/src/tools/task-complete-task-item.ts.html +343 -0
  106. package/coverage/src/tools/task-create-milestone.ts.html +286 -0
  107. package/coverage/src/tools/task-create-task-item.ts.html +358 -0
  108. package/coverage/src/tools/task-get-next-step.ts.html +460 -0
  109. package/coverage/src/tools/task-get-status.ts.html +316 -0
  110. package/coverage/src/tools/task-report-completion.ts.html +343 -0
  111. package/coverage/src/tools/task-update-progress.ts.html +232 -0
  112. package/firestore.rules +95 -0
  113. package/jest.config.js +31 -0
  114. package/package.json +67 -0
  115. package/src/client.spec.ts +199 -0
  116. package/src/client.ts +315 -0
  117. package/src/constant/collections.ts +128 -0
  118. package/src/dto/index.ts +47 -0
  119. package/src/dto/task-api.dto.ts +219 -0
  120. package/src/dto/transformers.spec.ts +462 -0
  121. package/src/dto/transformers.ts +161 -0
  122. package/src/schemas/task.ts +154 -0
  123. package/src/server-factory.spec.ts +70 -0
  124. package/src/server-factory.ts +111 -0
  125. package/src/server.ts +68 -0
  126. package/src/services/task-database.service.e2e.ts +116 -0
  127. package/src/services/task-database.service.spec.ts +479 -0
  128. package/src/services/task-database.service.ts +470 -0
  129. package/src/test-schemas.ts +161 -0
  130. package/src/tools/index.ts +69 -0
  131. package/src/tools/task-add-message.ts +64 -0
  132. package/src/tools/task-complete-task-item.ts +86 -0
  133. package/src/tools/task-create-milestone.ts +67 -0
  134. package/src/tools/task-create-task-item.ts +91 -0
  135. package/src/tools/task-get-next-step.spec.ts +136 -0
  136. package/src/tools/task-get-next-step.ts +125 -0
  137. package/src/tools/task-get-status.spec.ts +213 -0
  138. package/src/tools/task-get-status.ts +77 -0
  139. package/src/tools/task-report-completion.ts +86 -0
  140. package/src/tools/task-update-progress.ts +49 -0
  141. package/src/tools/tools.spec.ts +194 -0
  142. package/tsconfig.json +31 -0
@@ -0,0 +1,95 @@
1
+ rules_version = '2';
2
+
3
+ service cloud.firestore {
4
+ match /databases/{database}/documents {
5
+
6
+ // Helper function to check if user is authenticated
7
+ function isAuthenticated() {
8
+ return request.auth != null;
9
+ }
10
+
11
+ // Helper function to check if user owns the resource
12
+ function isOwner(userId) {
13
+ return isAuthenticated() && request.auth.uid == userId;
14
+ }
15
+
16
+ // Helper function to validate task structure
17
+ function isValidTask() {
18
+ let task = request.resource.data;
19
+ return task.keys().hasAll(['id', 'user_id', 'title', 'description', 'status',
20
+ 'created_at', 'updated_at', 'progress', 'execution', 'config'])
21
+ && task.status in ['not_started', 'in_progress', 'paused', 'completed', 'failed']
22
+ && task.user_id is string
23
+ && task.title is string
24
+ && task.description is string;
25
+ }
26
+
27
+ // Helper function to validate task message structure
28
+ function isValidTaskMessage() {
29
+ let message = request.resource.data;
30
+ return message.keys().hasAll(['task_id', 'role', 'content', 'timestamp'])
31
+ && message.role in ['user', 'assistant', 'system']
32
+ && message.task_id is string
33
+ && message.content is string
34
+ && message.timestamp is string;
35
+ }
36
+
37
+ // User tasks collection
38
+ // Path: users/{userId}/tasks/{taskId}
39
+ match /users/{userId}/tasks/{taskId} {
40
+ // Allow read if user owns the task
41
+ allow read: if isOwner(userId);
42
+
43
+ // Allow create if user is authenticated and owns the task
44
+ allow create: if isOwner(userId)
45
+ && isValidTask()
46
+ && request.resource.data.user_id == userId;
47
+
48
+ // Allow update if user owns the task and user_id doesn't change
49
+ allow update: if isOwner(userId)
50
+ && isValidTask()
51
+ && request.resource.data.user_id == resource.data.user_id;
52
+
53
+ // Allow delete if user owns the task
54
+ allow delete: if isOwner(userId);
55
+
56
+ // Task messages subcollection
57
+ // Path: users/{userId}/tasks/{taskId}/messages/{messageId}
58
+ match /messages/{messageId} {
59
+ // Allow read if user owns the parent task
60
+ allow read: if isOwner(userId);
61
+
62
+ // Allow create if user owns the parent task and message is valid
63
+ allow create: if isOwner(userId)
64
+ && isValidTaskMessage()
65
+ && request.resource.data.task_id == taskId;
66
+
67
+ // Allow update if user owns the parent task
68
+ allow update: if isOwner(userId)
69
+ && isValidTaskMessage()
70
+ && request.resource.data.task_id == resource.data.task_id;
71
+
72
+ // Allow delete if user owns the parent task
73
+ allow delete: if isOwner(userId);
74
+ }
75
+ }
76
+
77
+ // Task events collection (for WebSocket updates)
78
+ // Path: users/{userId}/task_events/{eventId}
79
+ match /users/{userId}/task_events/{eventId} {
80
+ // Allow read if user owns the events
81
+ allow read: if isOwner(userId);
82
+
83
+ // Allow create if user is authenticated (system can create events)
84
+ allow create: if isAuthenticated();
85
+
86
+ // Deny updates and deletes (events are immutable)
87
+ allow update, delete: if false;
88
+ }
89
+
90
+ // Deny all other access
91
+ match /{document=**} {
92
+ allow read, write: if false;
93
+ }
94
+ }
95
+ }
package/jest.config.js ADDED
@@ -0,0 +1,31 @@
1
+ export default {
2
+ preset: 'ts-jest/presets/default-esm',
3
+ testEnvironment: 'node',
4
+ extensionsToTreatAsEsm: ['.ts'],
5
+ roots: ['<rootDir>/src'],
6
+ testMatch: ['**/*.spec.ts'],
7
+ moduleFileExtensions: ['ts', 'js'],
8
+ collectCoverage: true,
9
+ coverageDirectory: 'coverage',
10
+ coverageReporters: ['text', 'lcov', 'html'],
11
+ collectCoverageFrom: [
12
+ 'src/**/*.ts',
13
+ '!src/**/*.d.ts',
14
+ '!src/**/*.spec.ts',
15
+ '!src/**/*.e2e.ts',
16
+ '!src/test-schemas.ts',
17
+ ],
18
+ moduleNameMapper: {
19
+ '^@/(.*)\\.(js|ts)$': '<rootDir>/src/$1',
20
+ '^@/(.*)$': '<rootDir>/src/$1',
21
+ '^(\\.{1,2}/.*)\\.js$': '$1',
22
+ },
23
+ transform: {
24
+ '^.+\\.ts$': [
25
+ 'ts-jest',
26
+ {
27
+ useESM: true,
28
+ },
29
+ ],
30
+ },
31
+ };
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@prmichaelsen/task-mcp",
3
+ "version": "0.2.0",
4
+ "description": "MCP server for autonomous task execution with ACP-style task management",
5
+ "type": "module",
6
+ "main": "dist/server.js",
7
+ "bin": {
8
+ "task-mcp": "./dist/server.js"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/server.d.ts",
13
+ "import": "./dist/server.js"
14
+ },
15
+ "./factory": {
16
+ "types": "./dist/server-factory.d.ts",
17
+ "import": "./dist/server-factory.js"
18
+ },
19
+ "./client": {
20
+ "types": "./dist/client.d.ts",
21
+ "import": "./dist/client.js"
22
+ },
23
+ "./services": {
24
+ "types": "./dist/services/task-database.service.d.ts",
25
+ "import": "./dist/services/task-database.service.js"
26
+ },
27
+ "./schemas": {
28
+ "types": "./dist/schemas/task.d.ts",
29
+ "import": "./dist/schemas/task.js"
30
+ },
31
+ "./dto": {
32
+ "types": "./dist/dto/index.d.ts",
33
+ "import": "./dist/dto/index.js"
34
+ }
35
+ },
36
+ "scripts": {
37
+ "build": "tsc",
38
+ "test": "jest",
39
+ "test:unit": "jest --testMatch='**/*.spec.ts'",
40
+ "test:e2e": "jest --testMatch='**/*.e2e.ts'",
41
+ "test:schemas": "tsx src/test-schemas.ts",
42
+ "test:all": "npm run test:unit && npm run test:e2e",
43
+ "typecheck": "tsc --noEmit"
44
+ },
45
+ "keywords": [
46
+ "mcp",
47
+ "task-execution",
48
+ "autonomous",
49
+ "acp",
50
+ "firestore"
51
+ ],
52
+ "author": "",
53
+ "license": "MIT",
54
+ "dependencies": {
55
+ "@modelcontextprotocol/sdk": "^1.26.0",
56
+ "firebase-admin": "^13.6.1",
57
+ "zod": "^4.3.6"
58
+ },
59
+ "devDependencies": {
60
+ "@types/jest": "^29.5.14",
61
+ "@types/node": "^25.2.3",
62
+ "jest": "^29.7.0",
63
+ "ts-jest": "^29.4.6",
64
+ "tsx": "^4.21.0",
65
+ "typescript": "^5.9.3"
66
+ }
67
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Unit Tests for FirebaseClient
3
+ */
4
+
5
+ import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'
6
+ import { FirebaseClient } from '@/client.js'
7
+ import * as firebaseApp from 'firebase-admin/app'
8
+ import * as firebaseFirestore from 'firebase-admin/firestore'
9
+ import { TaskDatabaseService } from '@/services/task-database.service.js'
10
+
11
+ // Mock Firebase Admin
12
+ jest.mock('firebase-admin/app')
13
+ jest.mock('firebase-admin/firestore')
14
+ jest.mock('@/services/task-database.service.js')
15
+
16
+ describe('FirebaseClient', () => {
17
+ const mockUserId = 'test-user-123'
18
+ const mockServiceAccountPath = './test-service-account.json'
19
+
20
+ let client: FirebaseClient
21
+ let mockApp: any
22
+ let mockDb: any
23
+
24
+ beforeEach(() => {
25
+ // Setup mocks
26
+ mockApp = { name: `task-mcp-${mockUserId}` }
27
+ mockDb = {}
28
+
29
+ ;(firebaseApp.getApps as jest.Mock<any>).mockReturnValue([])
30
+ ;(firebaseApp.initializeApp as jest.Mock<any>).mockReturnValue(mockApp)
31
+ ;(firebaseApp.cert as jest.Mock<any>).mockReturnValue({})
32
+ ;(firebaseFirestore.getFirestore as jest.Mock<any>).mockReturnValue(mockDb)
33
+ ;(TaskDatabaseService.initialize as jest.Mock<any>).mockReturnValue(undefined)
34
+ })
35
+
36
+ afterEach(() => {
37
+ jest.clearAllMocks()
38
+ })
39
+
40
+ describe('Constructor', () => {
41
+ it('should create client with userId', () => {
42
+ client = new FirebaseClient({
43
+ userId: mockUserId,
44
+ serviceAccountPath: mockServiceAccountPath
45
+ })
46
+
47
+ expect(client).toBeDefined()
48
+ })
49
+
50
+ it('should throw error if userId is missing', () => {
51
+ expect(() => {
52
+ new FirebaseClient({
53
+ userId: '',
54
+ serviceAccountPath: mockServiceAccountPath
55
+ })
56
+ }).toThrow('userId is required')
57
+ })
58
+ })
59
+
60
+ describe('connect', () => {
61
+ it('should initialize Firebase with service account path', async () => {
62
+ client = new FirebaseClient({
63
+ userId: mockUserId,
64
+ serviceAccountPath: mockServiceAccountPath
65
+ })
66
+
67
+ await client.connect()
68
+
69
+ expect(firebaseApp.cert).toHaveBeenCalledWith(mockServiceAccountPath)
70
+ expect(firebaseApp.initializeApp).toHaveBeenCalled()
71
+ expect(firebaseFirestore.getFirestore).toHaveBeenCalledWith(mockApp)
72
+ expect(TaskDatabaseService.initialize).toHaveBeenCalledWith(mockDb)
73
+ })
74
+
75
+ it('should initialize Firebase with service account JSON', async () => {
76
+ const mockServiceAccount = { type: 'service_account', project_id: 'test' }
77
+
78
+ client = new FirebaseClient({
79
+ userId: mockUserId,
80
+ serviceAccountJson: JSON.stringify(mockServiceAccount)
81
+ })
82
+
83
+ await client.connect()
84
+
85
+ expect(firebaseApp.cert).toHaveBeenCalledWith(mockServiceAccount)
86
+ expect(firebaseApp.initializeApp).toHaveBeenCalled()
87
+ })
88
+
89
+ it('should reuse existing app if already initialized', async () => {
90
+ const existingApp = { name: `task-mcp-${mockUserId}` }
91
+ ;(firebaseApp.getApps as jest.Mock<any>).mockReturnValue([existingApp])
92
+
93
+ client = new FirebaseClient({
94
+ userId: mockUserId,
95
+ serviceAccountPath: mockServiceAccountPath
96
+ })
97
+
98
+ await client.connect()
99
+
100
+ expect(firebaseApp.initializeApp).not.toHaveBeenCalled()
101
+ expect(firebaseFirestore.getFirestore).toHaveBeenCalledWith(existingApp)
102
+ })
103
+
104
+ it('should not reconnect if already connected', async () => {
105
+ client = new FirebaseClient({
106
+ userId: mockUserId,
107
+ serviceAccountPath: mockServiceAccountPath
108
+ })
109
+
110
+ await client.connect()
111
+ await client.connect() // Second call
112
+
113
+ expect(firebaseApp.initializeApp).toHaveBeenCalledTimes(1)
114
+ })
115
+ })
116
+
117
+ describe('disconnect', () => {
118
+ it('should delete app and cleanup', async () => {
119
+ ;(firebaseApp.deleteApp as jest.Mock<any>).mockResolvedValue(undefined)
120
+
121
+ client = new FirebaseClient({
122
+ userId: mockUserId,
123
+ serviceAccountPath: mockServiceAccountPath
124
+ })
125
+
126
+ await client.connect()
127
+ await client.disconnect()
128
+
129
+ expect(firebaseApp.deleteApp).toHaveBeenCalledWith(mockApp)
130
+ expect(client.isConnected()).toBe(false)
131
+ })
132
+ })
133
+
134
+ describe('Task Operations', () => {
135
+ beforeEach(async () => {
136
+ client = new FirebaseClient({
137
+ userId: mockUserId,
138
+ serviceAccountPath: mockServiceAccountPath
139
+ })
140
+ await client.connect()
141
+ })
142
+
143
+ it('should get task', async () => {
144
+ const mockTask = { id: 'task-123', title: 'Test Task' } as any
145
+ ;(TaskDatabaseService.getTask as jest.Mock<any>).mockResolvedValue(mockTask)
146
+
147
+ const task = await client.getTask('task-123')
148
+
149
+ expect(TaskDatabaseService.getTask).toHaveBeenCalledWith(mockUserId, 'task-123')
150
+ expect(task).toEqual(mockTask)
151
+ })
152
+
153
+ it('should create task', async () => {
154
+ const mockTask = { id: 'task-456', title: 'New Task' } as any
155
+ ;(TaskDatabaseService.createTask as jest.Mock<any>).mockResolvedValue(mockTask)
156
+
157
+ const task = await client.createTask('New Task', 'Description')
158
+
159
+ expect(TaskDatabaseService.createTask).toHaveBeenCalledWith(
160
+ mockUserId,
161
+ 'New Task',
162
+ 'Description',
163
+ undefined,
164
+ undefined
165
+ )
166
+ expect(task).toEqual(mockTask)
167
+ })
168
+
169
+ it('should update task status', async () => {
170
+ ;(TaskDatabaseService.updateTaskStatus as jest.Mock<any>).mockResolvedValue(undefined)
171
+
172
+ await client.updateTaskStatus('task-123', 'in_progress')
173
+
174
+ expect(TaskDatabaseService.updateTaskStatus).toHaveBeenCalledWith(
175
+ mockUserId,
176
+ 'task-123',
177
+ 'in_progress'
178
+ )
179
+ })
180
+ })
181
+
182
+ describe('Auto-connect', () => {
183
+ it('should auto-connect when calling methods if not connected', async () => {
184
+ client = new FirebaseClient({
185
+ userId: mockUserId,
186
+ serviceAccountPath: mockServiceAccountPath
187
+ })
188
+
189
+ ;(TaskDatabaseService.getTask as jest.Mock<any>).mockResolvedValue(null)
190
+
191
+ // Don't call connect() manually
192
+ await client.getTask('task-123')
193
+
194
+ // Should have auto-connected
195
+ expect(firebaseApp.initializeApp).toHaveBeenCalled()
196
+ expect(client.isConnected()).toBe(true)
197
+ })
198
+ })
199
+ })
package/src/client.ts ADDED
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Firebase Client Wrapper
3
+ *
4
+ * Provides a clean interface for MCP tools to interact with Firestore.
5
+ * Handles service account authentication and user-scoped operations.
6
+ */
7
+
8
+ import { initializeApp, cert, App, getApps, deleteApp } from 'firebase-admin/app'
9
+ import { getFirestore, Firestore } from 'firebase-admin/firestore'
10
+ import { TaskDatabaseService } from '@/services/task-database.service.js'
11
+ import type { Task, Milestone, TaskItem } from '@/schemas/task.js'
12
+
13
+ export interface FirebaseClientConfig {
14
+ userId: string
15
+ serviceAccountPath?: string
16
+ serviceAccountJson?: string
17
+ projectId?: string
18
+ }
19
+
20
+ export class FirebaseClient {
21
+ private app: App | null = null
22
+ private db: Firestore | null = null
23
+ private userId: string
24
+ private config: FirebaseClientConfig
25
+
26
+ constructor(config: FirebaseClientConfig) {
27
+ if (!config.userId) {
28
+ throw new Error('userId is required')
29
+ }
30
+
31
+ this.userId = config.userId
32
+ this.config = config
33
+ }
34
+
35
+ /**
36
+ * Initialize Firebase connection
37
+ */
38
+ async connect(): Promise<void> {
39
+ if (this.app) {
40
+ return // Already connected
41
+ }
42
+
43
+ try {
44
+ // Check if already initialized
45
+ const existingApps = getApps()
46
+ const appName = `task-mcp-${this.userId}`
47
+ const existingApp = existingApps.find(app => app.name === appName)
48
+
49
+ if (existingApp) {
50
+ this.app = existingApp
51
+ this.db = getFirestore(this.app)
52
+ TaskDatabaseService.initialize(this.db)
53
+ return
54
+ }
55
+
56
+ // Get service account credentials
57
+ let credential
58
+
59
+ if (this.config.serviceAccountJson) {
60
+ // Use JSON string
61
+ const serviceAccount = JSON.parse(this.config.serviceAccountJson)
62
+ credential = cert(serviceAccount)
63
+ } else if (this.config.serviceAccountPath) {
64
+ // Use file path
65
+ credential = cert(this.config.serviceAccountPath)
66
+ } else if (process.env.FIREBASE_SERVICE_ACCOUNT_JSON) {
67
+ // From environment variable (JSON)
68
+ const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_JSON)
69
+ credential = cert(serviceAccount)
70
+ } else if (process.env.FIREBASE_SERVICE_ACCOUNT_PATH) {
71
+ // From environment variable (path)
72
+ credential = cert(process.env.FIREBASE_SERVICE_ACCOUNT_PATH)
73
+ } else {
74
+ throw new Error('Firebase service account credentials required')
75
+ }
76
+
77
+ // Initialize app
78
+ this.app = initializeApp(
79
+ {
80
+ credential,
81
+ projectId: this.config.projectId || process.env.FIREBASE_PROJECT_ID
82
+ },
83
+ appName
84
+ )
85
+
86
+ this.db = getFirestore(this.app)
87
+ TaskDatabaseService.initialize(this.db)
88
+ } catch (error) {
89
+ throw new Error(`Failed to initialize Firebase: ${error instanceof Error ? error.message : String(error)}`)
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Disconnect and cleanup
95
+ */
96
+ async disconnect(): Promise<void> {
97
+ if (this.app) {
98
+ await deleteApp(this.app)
99
+ this.app = null
100
+ this.db = null
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Check if connected
106
+ */
107
+ isConnected(): boolean {
108
+ return this.app !== null && this.db !== null
109
+ }
110
+
111
+ // ==================== Task Operations ====================
112
+
113
+ /**
114
+ * Get a task (user-scoped)
115
+ */
116
+ async getTask(taskId: string): Promise<Task | null> {
117
+ if (!this.isConnected()) {
118
+ await this.connect()
119
+ }
120
+ return TaskDatabaseService.getTask(this.userId, taskId)
121
+ }
122
+
123
+ /**
124
+ * Create a task (user-scoped)
125
+ */
126
+ async createTask(
127
+ title: string,
128
+ description: string,
129
+ config?: Partial<Task['config']>,
130
+ metadata?: Task['metadata']
131
+ ): Promise<Task> {
132
+ if (!this.isConnected()) {
133
+ await this.connect()
134
+ }
135
+ return TaskDatabaseService.createTask(this.userId, title, description, config, metadata)
136
+ }
137
+
138
+ /**
139
+ * Update task status (user-scoped)
140
+ */
141
+ async updateTaskStatus(taskId: string, status: Task['status']): Promise<void> {
142
+ if (!this.isConnected()) {
143
+ await this.connect()
144
+ }
145
+ return TaskDatabaseService.updateTaskStatus(this.userId, taskId, status)
146
+ }
147
+
148
+ /**
149
+ * Delete a task (user-scoped)
150
+ */
151
+ async deleteTask(taskId: string): Promise<void> {
152
+ if (!this.isConnected()) {
153
+ await this.connect()
154
+ }
155
+ return TaskDatabaseService.deleteTask(this.userId, taskId)
156
+ }
157
+
158
+ /**
159
+ * List tasks (user-scoped)
160
+ */
161
+ async listTasks(limit = 50): Promise<Task[]> {
162
+ if (!this.isConnected()) {
163
+ await this.connect()
164
+ }
165
+ return TaskDatabaseService.listTasks(this.userId, limit)
166
+ }
167
+
168
+ // ==================== Progress Operations ====================
169
+
170
+ /**
171
+ * Update overall progress (user-scoped)
172
+ */
173
+ async updateOverallProgress(taskId: string, percentage: number): Promise<void> {
174
+ if (!this.isConnected()) {
175
+ await this.connect()
176
+ }
177
+ return TaskDatabaseService.updateOverallProgress(this.userId, taskId, percentage)
178
+ }
179
+
180
+ /**
181
+ * Create a milestone (user-scoped)
182
+ */
183
+ async createMilestone(taskId: string, milestone: Milestone): Promise<void> {
184
+ if (!this.isConnected()) {
185
+ await this.connect()
186
+ }
187
+ return TaskDatabaseService.createMilestone(this.userId, taskId, milestone)
188
+ }
189
+
190
+ /**
191
+ * Update a milestone (user-scoped)
192
+ */
193
+ async updateMilestone(
194
+ taskId: string,
195
+ milestoneId: string,
196
+ updates: Partial<Milestone>
197
+ ): Promise<void> {
198
+ if (!this.isConnected()) {
199
+ await this.connect()
200
+ }
201
+ return TaskDatabaseService.updateMilestone(this.userId, taskId, milestoneId, updates)
202
+ }
203
+
204
+ /**
205
+ * Complete a milestone (user-scoped)
206
+ */
207
+ async completeMilestone(taskId: string, milestoneId: string): Promise<void> {
208
+ if (!this.isConnected()) {
209
+ await this.connect()
210
+ }
211
+ return TaskDatabaseService.completeMilestone(this.userId, taskId, milestoneId)
212
+ }
213
+
214
+ /**
215
+ * Create a task item (user-scoped)
216
+ */
217
+ async createTaskItem(
218
+ taskId: string,
219
+ milestoneId: string,
220
+ taskItem: TaskItem
221
+ ): Promise<void> {
222
+ if (!this.isConnected()) {
223
+ await this.connect()
224
+ }
225
+ return TaskDatabaseService.createTaskItem(this.userId, taskId, milestoneId, taskItem)
226
+ }
227
+
228
+ /**
229
+ * Update a task item (user-scoped)
230
+ */
231
+ async updateTaskItem(
232
+ taskId: string,
233
+ milestoneId: string,
234
+ taskItemId: string,
235
+ updates: Partial<TaskItem>
236
+ ): Promise<void> {
237
+ if (!this.isConnected()) {
238
+ await this.connect()
239
+ }
240
+ return TaskDatabaseService.updateTaskItem(this.userId, taskId, milestoneId, taskItemId, updates)
241
+ }
242
+
243
+ /**
244
+ * Complete a task item (user-scoped)
245
+ */
246
+ async completeTaskItem(
247
+ taskId: string,
248
+ milestoneId: string,
249
+ taskItemId: string
250
+ ): Promise<void> {
251
+ if (!this.isConnected()) {
252
+ await this.connect()
253
+ }
254
+ return TaskDatabaseService.completeTaskItem(this.userId, taskId, milestoneId, taskItemId)
255
+ }
256
+
257
+ // ==================== Message Operations ====================
258
+
259
+ /**
260
+ * Add a message (user-scoped)
261
+ */
262
+ async addMessage(
263
+ taskId: string,
264
+ role: 'user' | 'assistant' | 'system',
265
+ content: string,
266
+ metadata?: any
267
+ ): Promise<string> {
268
+ if (!this.isConnected()) {
269
+ await this.connect()
270
+ }
271
+ return TaskDatabaseService.addMessage(this.userId, taskId, role, content, metadata)
272
+ }
273
+
274
+ /**
275
+ * Get messages (user-scoped)
276
+ */
277
+ async getMessages(taskId: string, limit = 100): Promise<any[]> {
278
+ if (!this.isConnected()) {
279
+ await this.connect()
280
+ }
281
+ return TaskDatabaseService.getMessages(this.userId, taskId, limit)
282
+ }
283
+
284
+ // ==================== Query Operations ====================
285
+
286
+ /**
287
+ * Get tasks by status (user-scoped)
288
+ */
289
+ async getTasksByStatus(status: Task['status'], limit = 50): Promise<Task[]> {
290
+ if (!this.isConnected()) {
291
+ await this.connect()
292
+ }
293
+ return TaskDatabaseService.getTasksByStatus(this.userId, status, limit)
294
+ }
295
+
296
+ /**
297
+ * Get active tasks (user-scoped)
298
+ */
299
+ async getActiveTasks(limit = 50): Promise<Task[]> {
300
+ if (!this.isConnected()) {
301
+ await this.connect()
302
+ }
303
+ return TaskDatabaseService.getActiveTasks(this.userId, limit)
304
+ }
305
+
306
+ /**
307
+ * Search tasks by title (user-scoped)
308
+ */
309
+ async searchTasksByTitle(searchTerm: string, limit = 50): Promise<Task[]> {
310
+ if (!this.isConnected()) {
311
+ await this.connect()
312
+ }
313
+ return TaskDatabaseService.searchTasksByTitle(this.userId, searchTerm, limit)
314
+ }
315
+ }