@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.
- package/.env.example +19 -0
- package/AGENT.md +1165 -0
- package/CHANGELOG.md +72 -0
- package/agent/commands/acp.commit.md +511 -0
- package/agent/commands/acp.init.md +376 -0
- package/agent/commands/acp.package-install.md +347 -0
- package/agent/commands/acp.proceed.md +311 -0
- package/agent/commands/acp.report.md +392 -0
- package/agent/commands/acp.status.md +280 -0
- package/agent/commands/acp.sync.md +323 -0
- package/agent/commands/acp.update.md +301 -0
- package/agent/commands/acp.validate.md +385 -0
- package/agent/commands/acp.version-check-for-updates.md +275 -0
- package/agent/commands/acp.version-check.md +190 -0
- package/agent/commands/acp.version-update.md +288 -0
- package/agent/commands/command.template.md +273 -0
- package/agent/commands/git.commit.md +511 -0
- package/agent/commands/git.init.md +513 -0
- package/agent/design/.gitkeep +0 -0
- package/agent/design/acp-task-execution-requirements.md +555 -0
- package/agent/design/api-dto-design.md +394 -0
- package/agent/design/code-extraction-guide.md +827 -0
- package/agent/design/design.template.md +136 -0
- package/agent/design/requirements.template.md +387 -0
- package/agent/design/rest-api-integration.md +489 -0
- package/agent/design/sdk-export-requirements.md +549 -0
- package/agent/milestones/.gitkeep +0 -0
- package/agent/milestones/milestone-1-{title}.template.md +206 -0
- package/agent/milestones/milestone-2-task-infrastructure.md +232 -0
- package/agent/milestones/milestone-4-autonomous-execution.md +235 -0
- package/agent/patterns/.gitkeep +0 -0
- package/agent/patterns/bootstrap.md +1271 -0
- package/agent/patterns/bootstrap.template.md +1237 -0
- package/agent/patterns/pattern.template.md +364 -0
- package/agent/progress.template.yaml +158 -0
- package/agent/progress.yaml +375 -0
- package/agent/scripts/check-for-updates.sh +88 -0
- package/agent/scripts/install.sh +157 -0
- package/agent/scripts/uninstall.sh +75 -0
- package/agent/scripts/update.sh +139 -0
- package/agent/scripts/version.sh +35 -0
- package/agent/tasks/.gitkeep +0 -0
- package/agent/tasks/task-1-{title}.template.md +225 -0
- package/agent/tasks/task-86-task-data-model-schemas.md +143 -0
- package/agent/tasks/task-87-task-database-service.md +220 -0
- package/agent/tasks/task-88-firebase-client-wrapper.md +139 -0
- package/agent/tasks/task-88-task-execution-engine.md +277 -0
- package/agent/tasks/task-89-mcp-server-implementation.md +197 -0
- package/agent/tasks/task-90-build-configuration.md +146 -0
- package/agent/tasks/task-91-deployment-configuration.md +128 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +191 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +191 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/client.ts.html +1030 -0
- package/coverage/lcov-report/src/constant/collections.ts.html +469 -0
- package/coverage/lcov-report/src/constant/index.html +116 -0
- package/coverage/lcov-report/src/dto/index.html +116 -0
- package/coverage/lcov-report/src/dto/transformers.ts.html +568 -0
- package/coverage/lcov-report/src/index.html +146 -0
- package/coverage/lcov-report/src/schemas/index.html +116 -0
- package/coverage/lcov-report/src/schemas/task.ts.html +547 -0
- package/coverage/lcov-report/src/server-factory.ts.html +418 -0
- package/coverage/lcov-report/src/server.ts.html +289 -0
- package/coverage/lcov-report/src/services/index.html +116 -0
- package/coverage/lcov-report/src/services/task-database.service.ts.html +1495 -0
- package/coverage/lcov-report/src/tools/index.html +236 -0
- package/coverage/lcov-report/src/tools/index.ts.html +292 -0
- package/coverage/lcov-report/src/tools/task-add-message.ts.html +277 -0
- package/coverage/lcov-report/src/tools/task-complete-task-item.ts.html +343 -0
- package/coverage/lcov-report/src/tools/task-create-milestone.ts.html +286 -0
- package/coverage/lcov-report/src/tools/task-create-task-item.ts.html +358 -0
- package/coverage/lcov-report/src/tools/task-get-next-step.ts.html +460 -0
- package/coverage/lcov-report/src/tools/task-get-status.ts.html +316 -0
- package/coverage/lcov-report/src/tools/task-report-completion.ts.html +343 -0
- package/coverage/lcov-report/src/tools/task-update-progress.ts.html +232 -0
- package/coverage/lcov.info +974 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/client.ts.html +1030 -0
- package/coverage/src/constant/collections.ts.html +469 -0
- package/coverage/src/constant/index.html +116 -0
- package/coverage/src/dto/index.html +116 -0
- package/coverage/src/dto/transformers.ts.html +568 -0
- package/coverage/src/index.html +146 -0
- package/coverage/src/schemas/index.html +116 -0
- package/coverage/src/schemas/task.ts.html +547 -0
- package/coverage/src/server-factory.ts.html +418 -0
- package/coverage/src/server.ts.html +289 -0
- package/coverage/src/services/index.html +116 -0
- package/coverage/src/services/task-database.service.ts.html +1495 -0
- package/coverage/src/tools/index.html +236 -0
- package/coverage/src/tools/index.ts.html +292 -0
- package/coverage/src/tools/task-add-message.ts.html +277 -0
- package/coverage/src/tools/task-complete-task-item.ts.html +343 -0
- package/coverage/src/tools/task-create-milestone.ts.html +286 -0
- package/coverage/src/tools/task-create-task-item.ts.html +358 -0
- package/coverage/src/tools/task-get-next-step.ts.html +460 -0
- package/coverage/src/tools/task-get-status.ts.html +316 -0
- package/coverage/src/tools/task-report-completion.ts.html +343 -0
- package/coverage/src/tools/task-update-progress.ts.html +232 -0
- package/firestore.rules +95 -0
- package/jest.config.js +31 -0
- package/package.json +67 -0
- package/src/client.spec.ts +199 -0
- package/src/client.ts +315 -0
- package/src/constant/collections.ts +128 -0
- package/src/dto/index.ts +47 -0
- package/src/dto/task-api.dto.ts +219 -0
- package/src/dto/transformers.spec.ts +462 -0
- package/src/dto/transformers.ts +161 -0
- package/src/schemas/task.ts +154 -0
- package/src/server-factory.spec.ts +70 -0
- package/src/server-factory.ts +111 -0
- package/src/server.ts +68 -0
- package/src/services/task-database.service.e2e.ts +116 -0
- package/src/services/task-database.service.spec.ts +479 -0
- package/src/services/task-database.service.ts +470 -0
- package/src/test-schemas.ts +161 -0
- package/src/tools/index.ts +69 -0
- package/src/tools/task-add-message.ts +64 -0
- package/src/tools/task-complete-task-item.ts +86 -0
- package/src/tools/task-create-milestone.ts +67 -0
- package/src/tools/task-create-task-item.ts +91 -0
- package/src/tools/task-get-next-step.spec.ts +136 -0
- package/src/tools/task-get-next-step.ts +125 -0
- package/src/tools/task-get-status.spec.ts +213 -0
- package/src/tools/task-get-status.ts +77 -0
- package/src/tools/task-report-completion.ts +86 -0
- package/src/tools/task-update-progress.ts +49 -0
- package/src/tools/tools.spec.ts +194 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Data Model and Schemas
|
|
3
|
+
*
|
|
4
|
+
* Zod schemas and TypeScript interfaces for the task execution system.
|
|
5
|
+
* These schemas define the structure of Task documents in Firestore.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Milestone Schema
|
|
12
|
+
* Represents a major phase in task execution with multiple sub-tasks
|
|
13
|
+
*/
|
|
14
|
+
export const MilestoneSchema = z.object({
|
|
15
|
+
id: z.string(),
|
|
16
|
+
name: z.string(),
|
|
17
|
+
description: z.string(),
|
|
18
|
+
status: z.enum(['not_started', 'in_progress', 'completed']),
|
|
19
|
+
progress: z.number().min(0).max(100),
|
|
20
|
+
tasks_completed: z.number().min(0),
|
|
21
|
+
tasks_total: z.number().min(0),
|
|
22
|
+
started_at: z.string().optional(),
|
|
23
|
+
completed_at: z.string().optional()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Task Item Schema
|
|
28
|
+
* Represents a granular work item within a milestone
|
|
29
|
+
*/
|
|
30
|
+
export const TaskItemSchema = z.object({
|
|
31
|
+
id: z.string(),
|
|
32
|
+
name: z.string(),
|
|
33
|
+
description: z.string(),
|
|
34
|
+
status: z.enum(['not_started', 'in_progress', 'completed']),
|
|
35
|
+
estimated_hours: z.number().optional(),
|
|
36
|
+
completed_at: z.string().optional(),
|
|
37
|
+
notes: z.string().optional()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Task Progress Schema
|
|
42
|
+
* Tracks overall progress and milestone/task completion
|
|
43
|
+
*/
|
|
44
|
+
export const TaskProgressSchema = z.object({
|
|
45
|
+
current_milestone: z.string(),
|
|
46
|
+
current_task: z.string(),
|
|
47
|
+
overall_percentage: z.number().min(0).max(100),
|
|
48
|
+
milestones: z.array(MilestoneSchema),
|
|
49
|
+
tasks: z.record(z.string(), z.array(TaskItemSchema))
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Task Execution Schema
|
|
54
|
+
* Stores execution state including messages and tool results
|
|
55
|
+
*/
|
|
56
|
+
export const TaskExecutionSchema = z.object({
|
|
57
|
+
api_messages: z.array(z.any()),
|
|
58
|
+
task_messages: z.array(z.any()),
|
|
59
|
+
tool_results: z.array(z.any()),
|
|
60
|
+
error: z.string().optional(),
|
|
61
|
+
abort_reason: z.string().optional()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Task Configuration Schema
|
|
66
|
+
* Configuration for task execution behavior
|
|
67
|
+
* Note: Model is configured globally by the tenant platform, not per-task
|
|
68
|
+
*/
|
|
69
|
+
export const TaskConfigSchema = z.object({
|
|
70
|
+
system_prompt: z.string(),
|
|
71
|
+
auto_approve: z.boolean(),
|
|
72
|
+
max_iterations: z.number().optional()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Task Metadata Schema
|
|
77
|
+
* Optional metadata for task organization and tracking
|
|
78
|
+
*/
|
|
79
|
+
export const TaskMetadataSchema = z.object({
|
|
80
|
+
conversation_id: z.string().optional(),
|
|
81
|
+
parent_task_id: z.string().optional(),
|
|
82
|
+
tags: z.array(z.string()).optional()
|
|
83
|
+
}).optional()
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Task Schema
|
|
87
|
+
* Complete task document structure
|
|
88
|
+
*/
|
|
89
|
+
export const TaskSchema = z.object({
|
|
90
|
+
id: z.string(),
|
|
91
|
+
user_id: z.string(),
|
|
92
|
+
title: z.string(),
|
|
93
|
+
description: z.string(),
|
|
94
|
+
status: z.enum(['not_started', 'in_progress', 'paused', 'completed', 'failed']),
|
|
95
|
+
created_at: z.string(),
|
|
96
|
+
updated_at: z.string(),
|
|
97
|
+
started_at: z.string().optional(),
|
|
98
|
+
completed_at: z.string().optional(),
|
|
99
|
+
|
|
100
|
+
progress: TaskProgressSchema,
|
|
101
|
+
execution: TaskExecutionSchema,
|
|
102
|
+
config: TaskConfigSchema,
|
|
103
|
+
metadata: TaskMetadataSchema
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Task Message Schema
|
|
108
|
+
* Messages in the task conversation thread
|
|
109
|
+
*/
|
|
110
|
+
export const TaskMessageSchema = z.object({
|
|
111
|
+
id: z.string(),
|
|
112
|
+
task_id: z.string(),
|
|
113
|
+
role: z.enum(['user', 'assistant', 'system']),
|
|
114
|
+
content: z.string(),
|
|
115
|
+
timestamp: z.string(),
|
|
116
|
+
metadata: z.any().optional()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// Export TypeScript types inferred from Zod schemas
|
|
120
|
+
export type Milestone = z.infer<typeof MilestoneSchema>
|
|
121
|
+
export type TaskItem = z.infer<typeof TaskItemSchema>
|
|
122
|
+
export type TaskProgress = z.infer<typeof TaskProgressSchema>
|
|
123
|
+
export type TaskExecution = z.infer<typeof TaskExecutionSchema>
|
|
124
|
+
export type TaskConfig = z.infer<typeof TaskConfigSchema>
|
|
125
|
+
export type TaskMetadata = z.infer<typeof TaskMetadataSchema>
|
|
126
|
+
export type Task = z.infer<typeof TaskSchema>
|
|
127
|
+
export type TaskMessage = z.infer<typeof TaskMessageSchema>
|
|
128
|
+
|
|
129
|
+
// Export status enums for convenience
|
|
130
|
+
export const TaskStatus = {
|
|
131
|
+
NOT_STARTED: 'not_started',
|
|
132
|
+
IN_PROGRESS: 'in_progress',
|
|
133
|
+
PAUSED: 'paused',
|
|
134
|
+
COMPLETED: 'completed',
|
|
135
|
+
FAILED: 'failed'
|
|
136
|
+
} as const
|
|
137
|
+
|
|
138
|
+
export const MilestoneStatus = {
|
|
139
|
+
NOT_STARTED: 'not_started',
|
|
140
|
+
IN_PROGRESS: 'in_progress',
|
|
141
|
+
COMPLETED: 'completed'
|
|
142
|
+
} as const
|
|
143
|
+
|
|
144
|
+
export const TaskItemStatus = {
|
|
145
|
+
NOT_STARTED: 'not_started',
|
|
146
|
+
IN_PROGRESS: 'in_progress',
|
|
147
|
+
COMPLETED: 'completed'
|
|
148
|
+
} as const
|
|
149
|
+
|
|
150
|
+
export const MessageRole = {
|
|
151
|
+
USER: 'user',
|
|
152
|
+
ASSISTANT: 'assistant',
|
|
153
|
+
SYSTEM: 'system'
|
|
154
|
+
} as const
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Factory Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createServer } from './server-factory.js'
|
|
6
|
+
import { FirebaseClient } from './client.js'
|
|
7
|
+
|
|
8
|
+
// Mock FirebaseClient
|
|
9
|
+
jest.mock('./client.js', () => ({
|
|
10
|
+
FirebaseClient: jest.fn().mockImplementation(() => ({
|
|
11
|
+
connect: jest.fn().mockResolvedValue(undefined),
|
|
12
|
+
disconnect: jest.fn().mockResolvedValue(undefined)
|
|
13
|
+
}))
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
describe('Server Factory', () => {
|
|
17
|
+
describe('Parameter Validation', () => {
|
|
18
|
+
it('should create server instance with valid userId', async () => {
|
|
19
|
+
const server = await createServer('test-token', 'test-user-123')
|
|
20
|
+
expect(server).toBeDefined()
|
|
21
|
+
expect(server).toHaveProperty('connect')
|
|
22
|
+
expect(server).toHaveProperty('setRequestHandler')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('should throw error if userId is empty', async () => {
|
|
26
|
+
await expect(createServer('test-token', '')).rejects.toThrow('userId is required')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should accept custom options', async () => {
|
|
30
|
+
const server = await createServer('test-token', 'test-user-123', {
|
|
31
|
+
name: 'custom-task-mcp',
|
|
32
|
+
version: '1.0.0'
|
|
33
|
+
})
|
|
34
|
+
expect(server).toBeDefined()
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('Server Isolation', () => {
|
|
39
|
+
it('should create separate instances for different users', async () => {
|
|
40
|
+
const server1 = await createServer('test-token', 'user-1')
|
|
41
|
+
const server2 = await createServer('test-token', 'user-2')
|
|
42
|
+
|
|
43
|
+
expect(server1).not.toBe(server2)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should scope operations to userId', async () => {
|
|
47
|
+
const server = await createServer('test-token', 'test-user-123')
|
|
48
|
+
|
|
49
|
+
// Server should be isolated to this user
|
|
50
|
+
expect(server).toBeDefined()
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('Options', () => {
|
|
55
|
+
it('should use default name if not provided', async () => {
|
|
56
|
+
const server = await createServer('test-token', 'test-user-123')
|
|
57
|
+
expect(server).toBeDefined()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should use custom name if provided', async () => {
|
|
61
|
+
const server = await createServer('test-token', 'test-user-123', { name: 'custom-name' })
|
|
62
|
+
expect(server).toBeDefined()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should use custom version if provided', async () => {
|
|
66
|
+
const server = await createServer('test-token', 'test-user-123', { version: '2.0.0' })
|
|
67
|
+
expect(server).toBeDefined()
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server Factory
|
|
3
|
+
*
|
|
4
|
+
* Creates isolated MCP server instances for multi-tenant usage.
|
|
5
|
+
* Each server instance is scoped to a specific userId.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
9
|
+
import {
|
|
10
|
+
CallToolRequestSchema,
|
|
11
|
+
ListToolsRequestSchema,
|
|
12
|
+
ErrorCode,
|
|
13
|
+
McpError
|
|
14
|
+
} from '@modelcontextprotocol/sdk/types.js'
|
|
15
|
+
import { FirebaseClient } from './client.js'
|
|
16
|
+
import { allTools, getToolHandler } from './tools/index.js'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Server configuration options
|
|
20
|
+
*/
|
|
21
|
+
export interface ServerOptions {
|
|
22
|
+
name?: string
|
|
23
|
+
version?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create an MCP server instance for a specific user
|
|
28
|
+
*
|
|
29
|
+
* This function is compatible with mcp-auth wrapper which calls it with:
|
|
30
|
+
* createServer(accessToken, userId)
|
|
31
|
+
*
|
|
32
|
+
* @param accessToken - Access token (not used by task-mcp, but required by mcp-auth)
|
|
33
|
+
* @param userId - User ID to scope operations to
|
|
34
|
+
* @param options - Optional server configuration
|
|
35
|
+
* @returns Configured MCP Server instance
|
|
36
|
+
*/
|
|
37
|
+
export async function createServer(
|
|
38
|
+
accessToken: string,
|
|
39
|
+
userId: string,
|
|
40
|
+
options: ServerOptions = {}
|
|
41
|
+
): Promise<Server> {
|
|
42
|
+
if (!userId) {
|
|
43
|
+
throw new Error('userId is required')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Initialize Firebase client for this user
|
|
47
|
+
const client = new FirebaseClient({ userId })
|
|
48
|
+
await client.connect()
|
|
49
|
+
|
|
50
|
+
// Create MCP server
|
|
51
|
+
const server = new Server(
|
|
52
|
+
{
|
|
53
|
+
name: options.name || 'task-mcp',
|
|
54
|
+
version: options.version || '0.1.0'
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
capabilities: {
|
|
58
|
+
tools: {}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// Register list_tools handler
|
|
64
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
65
|
+
return {
|
|
66
|
+
tools: allTools
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// Register call_tool handler
|
|
71
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
72
|
+
const { name, arguments: args } = request.params
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
// Get handler for this tool
|
|
76
|
+
const handler = getToolHandler(name)
|
|
77
|
+
|
|
78
|
+
if (!handler) {
|
|
79
|
+
throw new McpError(
|
|
80
|
+
ErrorCode.MethodNotFound,
|
|
81
|
+
`Unknown tool: ${name}`
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Execute tool handler
|
|
86
|
+
const result = await handler(client, args as any || {})
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: 'text',
|
|
92
|
+
text: result
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// Re-throw MCP errors as-is
|
|
98
|
+
if (error instanceof McpError) {
|
|
99
|
+
throw error
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Wrap other errors in MCP error
|
|
103
|
+
throw new McpError(
|
|
104
|
+
ErrorCode.InternalError,
|
|
105
|
+
`Tool execution failed: ${error instanceof Error ? error.message : String(error)}`
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
return server
|
|
111
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MCP Server - Standalone Entry Point
|
|
5
|
+
*
|
|
6
|
+
* Starts an MCP server with stdio transport for a single user.
|
|
7
|
+
* This is the main entry point for running task-mcp as a standalone server.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
11
|
+
import { createServer } from './server-factory.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Initialize and start the MCP server
|
|
15
|
+
*/
|
|
16
|
+
async function main(): Promise<void> {
|
|
17
|
+
// Get userId from environment variable (set by mcp-auth wrapper)
|
|
18
|
+
const userId = process.env.TASK_MCP_USER_ID
|
|
19
|
+
|
|
20
|
+
if (!userId) {
|
|
21
|
+
console.error('Error: TASK_MCP_USER_ID environment variable is required')
|
|
22
|
+
console.error('This server should be run through mcp-auth wrapper')
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// Create server instance
|
|
28
|
+
// Note: accessToken is not used by task-mcp but required by mcp-auth signature
|
|
29
|
+
const server = await createServer('', userId, {
|
|
30
|
+
name: 'task-mcp',
|
|
31
|
+
version: '0.1.0'
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// Create stdio transport
|
|
35
|
+
const transport = new StdioServerTransport()
|
|
36
|
+
|
|
37
|
+
// Connect server to transport
|
|
38
|
+
await server.connect(transport)
|
|
39
|
+
|
|
40
|
+
// Log startup (to stderr to not interfere with stdio protocol)
|
|
41
|
+
console.error(`task-mcp server started for user: ${userId}`)
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('Failed to start server:', error)
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Setup graceful shutdown handlers
|
|
50
|
+
*/
|
|
51
|
+
function setupShutdownHandlers(): void {
|
|
52
|
+
const shutdown = async (signal: string) => {
|
|
53
|
+
console.error(`\nReceived ${signal}, shutting down gracefully...`)
|
|
54
|
+
process.exit(0)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
process.on('SIGINT', () => shutdown('SIGINT'))
|
|
58
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Setup shutdown handlers
|
|
62
|
+
setupShutdownHandlers()
|
|
63
|
+
|
|
64
|
+
// Start server
|
|
65
|
+
main().catch((error) => {
|
|
66
|
+
console.error('Fatal error:', error)
|
|
67
|
+
process.exit(1)
|
|
68
|
+
})
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E Tests for TaskDatabaseService
|
|
3
|
+
*
|
|
4
|
+
* Tests with real Firestore emulator for integration testing.
|
|
5
|
+
*
|
|
6
|
+
* To run these tests:
|
|
7
|
+
* 1. Start Firestore emulator: firebase emulators:start --only firestore
|
|
8
|
+
* 2. Run tests: npm run test:e2e
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals'
|
|
12
|
+
import { initializeApp, cert, deleteApp } from 'firebase-admin/app'
|
|
13
|
+
import { getFirestore } from 'firebase-admin/firestore'
|
|
14
|
+
import { TaskDatabaseService } from '../../src/services/task-database.service.js'
|
|
15
|
+
import type { Task } from '../../src/schemas/task.js'
|
|
16
|
+
|
|
17
|
+
describe('TaskDatabaseService E2E', () => {
|
|
18
|
+
let app: any
|
|
19
|
+
|
|
20
|
+
beforeAll(async () => {
|
|
21
|
+
// Initialize Firebase Admin with emulator
|
|
22
|
+
process.env.FIRESTORE_EMULATOR_HOST = 'localhost:8080'
|
|
23
|
+
|
|
24
|
+
app = initializeApp({
|
|
25
|
+
projectId: 'test-project'
|
|
26
|
+
}, 'task-mcp-e2e-test')
|
|
27
|
+
|
|
28
|
+
const db = getFirestore(app)
|
|
29
|
+
TaskDatabaseService.initialize(db)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
afterAll(async () => {
|
|
33
|
+
if (app) {
|
|
34
|
+
await deleteApp(app)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('Full Task Lifecycle', () => {
|
|
39
|
+
it('should create, read, update, and delete a task', async () => {
|
|
40
|
+
const userId = 'test-user-' + Date.now()
|
|
41
|
+
|
|
42
|
+
// Create
|
|
43
|
+
const task = await TaskDatabaseService.createTask(
|
|
44
|
+
userId,
|
|
45
|
+
'E2E Test Task',
|
|
46
|
+
'Testing full lifecycle'
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
expect(task.id).toBeDefined()
|
|
50
|
+
expect(task.title).toBe('E2E Test Task')
|
|
51
|
+
|
|
52
|
+
// Read
|
|
53
|
+
const retrieved = await TaskDatabaseService.getTask(userId, task.id)
|
|
54
|
+
expect(retrieved).not.toBeNull()
|
|
55
|
+
expect(retrieved?.title).toBe('E2E Test Task')
|
|
56
|
+
|
|
57
|
+
// Update status
|
|
58
|
+
await TaskDatabaseService.updateTaskStatus(userId, task.id, 'in_progress')
|
|
59
|
+
const updated = await TaskDatabaseService.getTask(userId, task.id)
|
|
60
|
+
expect(updated?.status).toBe('in_progress')
|
|
61
|
+
expect(updated?.started_at).toBeDefined()
|
|
62
|
+
|
|
63
|
+
// Delete
|
|
64
|
+
await TaskDatabaseService.deleteTask(userId, task.id)
|
|
65
|
+
const deleted = await TaskDatabaseService.getTask(userId, task.id)
|
|
66
|
+
expect(deleted).toBeNull()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should handle task messages', async () => {
|
|
70
|
+
const userId = 'test-user-' + Date.now()
|
|
71
|
+
|
|
72
|
+
const task = await TaskDatabaseService.createTask(
|
|
73
|
+
userId,
|
|
74
|
+
'Message Test',
|
|
75
|
+
'Testing messages'
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
// Add messages
|
|
79
|
+
const msg1 = await TaskDatabaseService.addMessage(userId, task.id, 'user', 'Hello')
|
|
80
|
+
const msg2 = await TaskDatabaseService.addMessage(userId, task.id, 'assistant', 'Hi there')
|
|
81
|
+
|
|
82
|
+
expect(msg1).toBeDefined()
|
|
83
|
+
expect(msg2).toBeDefined()
|
|
84
|
+
|
|
85
|
+
// Get messages
|
|
86
|
+
const messages = await TaskDatabaseService.getMessages(userId, task.id)
|
|
87
|
+
expect(messages).toHaveLength(2)
|
|
88
|
+
expect(messages[0].role).toBe('user')
|
|
89
|
+
expect(messages[1].role).toBe('assistant')
|
|
90
|
+
|
|
91
|
+
// Cleanup
|
|
92
|
+
await TaskDatabaseService.deleteTask(userId, task.id)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('Query Operations', () => {
|
|
97
|
+
it('should filter tasks by status', async () => {
|
|
98
|
+
const userId = 'test-user-' + Date.now()
|
|
99
|
+
|
|
100
|
+
// Create tasks with different statuses
|
|
101
|
+
const task1 = await TaskDatabaseService.createTask(userId, 'Task 1', 'Active')
|
|
102
|
+
await TaskDatabaseService.updateTaskStatus(userId, task1.id, 'in_progress')
|
|
103
|
+
|
|
104
|
+
const task2 = await TaskDatabaseService.createTask(userId, 'Task 2', 'Pending')
|
|
105
|
+
|
|
106
|
+
// Query active tasks
|
|
107
|
+
const activeTasks = await TaskDatabaseService.getActiveTasks(userId)
|
|
108
|
+
expect(activeTasks.length).toBeGreaterThanOrEqual(1)
|
|
109
|
+
expect(activeTasks.some(t => t.id === task1.id)).toBe(true)
|
|
110
|
+
|
|
111
|
+
// Cleanup
|
|
112
|
+
await TaskDatabaseService.deleteTask(userId, task1.id)
|
|
113
|
+
await TaskDatabaseService.deleteTask(userId, task2.id)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
})
|