@krr2020/taskflow-mcp-server 0.1.0-beta.1
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/dist/index.js +232 -0
- package/package.json +27 -0
- package/src/index.ts +261 -0
- package/tsconfig.json +19 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { StateMachine, ConfigLoader, GitManager } from "@krr2020/taskflow-core";
|
|
7
|
+
// Initialize Core Components
|
|
8
|
+
const configLoader = new ConfigLoader();
|
|
9
|
+
const gitManager = new GitManager();
|
|
10
|
+
const stateMachine = new StateMachine(configLoader, gitManager);
|
|
11
|
+
// Initialize MCP Server
|
|
12
|
+
const server = new Server({
|
|
13
|
+
name: "taskflow-mcp-server",
|
|
14
|
+
version: "0.1.0",
|
|
15
|
+
}, {
|
|
16
|
+
capabilities: {
|
|
17
|
+
tools: {},
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
// Tool Definitions
|
|
21
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
22
|
+
return {
|
|
23
|
+
tools: [
|
|
24
|
+
{
|
|
25
|
+
name: "start_task",
|
|
26
|
+
description: "Start a new task, checking out the correct story branch and entering PLANNING mode.",
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
taskId: {
|
|
31
|
+
type: "string",
|
|
32
|
+
description: "The ID of the task to start (e.g., '1.2.3')",
|
|
33
|
+
},
|
|
34
|
+
storyId: {
|
|
35
|
+
type: "string",
|
|
36
|
+
description: "The Story ID this task belongs to (e.g., '15')",
|
|
37
|
+
},
|
|
38
|
+
slug: {
|
|
39
|
+
type: "string",
|
|
40
|
+
description: "Short slug for the story (e.g., 'user-auth')",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
required: ["taskId", "storyId", "slug"],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "approve_plan",
|
|
48
|
+
description: "Approve the implementation plan and switch to EXECUTION mode.",
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: "object",
|
|
51
|
+
properties: {},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "get_status",
|
|
56
|
+
description: "Get the current state machine status and active task.",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: {},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "generate_prd",
|
|
64
|
+
description: "Generate a PRD template based on project context.",
|
|
65
|
+
inputSchema: {
|
|
66
|
+
type: "object",
|
|
67
|
+
properties: {
|
|
68
|
+
requirements: { type: "string" }
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "generate_tasks",
|
|
74
|
+
description: "Generate tasks from a PRD.",
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: {
|
|
78
|
+
prdContent: { type: "string" }
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "run_checks",
|
|
84
|
+
description: "Run project validations and enter VERIFICATION state.",
|
|
85
|
+
inputSchema: {
|
|
86
|
+
type: "object",
|
|
87
|
+
properties: {},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "submit_task",
|
|
92
|
+
description: "Submit the current task and complete the workflow.",
|
|
93
|
+
inputSchema: {
|
|
94
|
+
type: "object",
|
|
95
|
+
properties: {},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
// Tool Execution
|
|
102
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
103
|
+
const { name, arguments: args } = request.params;
|
|
104
|
+
try {
|
|
105
|
+
switch (name) {
|
|
106
|
+
case "start_task": {
|
|
107
|
+
const schema = z.object({
|
|
108
|
+
taskId: z.string(),
|
|
109
|
+
storyId: z.string(),
|
|
110
|
+
slug: z.string(),
|
|
111
|
+
});
|
|
112
|
+
const { taskId, storyId, slug } = schema.parse(args);
|
|
113
|
+
await stateMachine.startTask(taskId, storyId, slug);
|
|
114
|
+
return {
|
|
115
|
+
content: [
|
|
116
|
+
{
|
|
117
|
+
type: "text",
|
|
118
|
+
text: `Task ${taskId} started on branch story/S${storyId}-${slug}. State is now PLANNING.`,
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
case "approve_plan": {
|
|
124
|
+
stateMachine.approvePlan();
|
|
125
|
+
return {
|
|
126
|
+
content: [
|
|
127
|
+
{
|
|
128
|
+
type: "text",
|
|
129
|
+
text: "Plan approved. State is now EXECUTION. You may now write code.",
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
case "get_status": {
|
|
135
|
+
return {
|
|
136
|
+
content: [
|
|
137
|
+
{
|
|
138
|
+
type: "text",
|
|
139
|
+
text: JSON.stringify({
|
|
140
|
+
state: stateMachine.getState(),
|
|
141
|
+
activeTask: stateMachine.getActiveTask(),
|
|
142
|
+
}, null, 2),
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
case "generate_prd": {
|
|
148
|
+
const template = `# Project Requirements Document
|
|
149
|
+
## 1. Objective
|
|
150
|
+
[Describe the goal]
|
|
151
|
+
|
|
152
|
+
## 2. Scope
|
|
153
|
+
- [ ] In Scope
|
|
154
|
+
- [ ] Out of Scope
|
|
155
|
+
|
|
156
|
+
## 3. Technical Requirements
|
|
157
|
+
- Language: [e.g., TypeScript]
|
|
158
|
+
- Framework: [e.g., React]
|
|
159
|
+
|
|
160
|
+
## 4. User Stories
|
|
161
|
+
- Story 1: [Description]
|
|
162
|
+
`;
|
|
163
|
+
return {
|
|
164
|
+
content: [
|
|
165
|
+
{
|
|
166
|
+
type: "text",
|
|
167
|
+
text: template,
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
case "generate_tasks": {
|
|
173
|
+
// In a real implementation, this would parse the PRD.
|
|
174
|
+
// For prototype, we return the schema structure.
|
|
175
|
+
return {
|
|
176
|
+
content: [
|
|
177
|
+
{
|
|
178
|
+
type: "text",
|
|
179
|
+
text: "Please provide the tasks in the following JSON format matching the TaskSchema:\n" +
|
|
180
|
+
JSON.stringify({
|
|
181
|
+
id: "1.0",
|
|
182
|
+
title: "Example Task",
|
|
183
|
+
status: "todo",
|
|
184
|
+
subtasks: [{ id: "1.1", title: "Subtask 1" }]
|
|
185
|
+
}, null, 2)
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
case "run_checks": {
|
|
191
|
+
stateMachine.startVerification();
|
|
192
|
+
// TODO: Actually run the validation commands from config
|
|
193
|
+
return {
|
|
194
|
+
content: [{ type: "text", text: "Verification phase started. Running checks... [MOCK PASSED]" }],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
case "submit_task": {
|
|
198
|
+
stateMachine.completeTask();
|
|
199
|
+
return {
|
|
200
|
+
content: [{ type: "text", text: "Task submitted and completed. State is now IDLE." }],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
default:
|
|
204
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (error instanceof z.ZodError) {
|
|
209
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments: ${error.message}`);
|
|
210
|
+
}
|
|
211
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
212
|
+
return {
|
|
213
|
+
content: [
|
|
214
|
+
{
|
|
215
|
+
type: "text",
|
|
216
|
+
text: `Error executing ${name}: ${errorMessage}`,
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
isError: true,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
// Start Server
|
|
224
|
+
async function main() {
|
|
225
|
+
const transport = new StdioServerTransport();
|
|
226
|
+
await server.connect(transport);
|
|
227
|
+
console.error("Taskflow MCP Server running on stdio");
|
|
228
|
+
}
|
|
229
|
+
main().catch((error) => {
|
|
230
|
+
console.error("Fatal error in main():", error);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@krr2020/taskflow-mcp-server",
|
|
3
|
+
"version": "0.1.0-beta.1",
|
|
4
|
+
"description": "MCP Server for Taskflow 2.0",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"bin": {
|
|
12
|
+
"taskflow-mcp": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@modelcontextprotocol/sdk": "^0.6.0",
|
|
16
|
+
"zod": "^3.22.4",
|
|
17
|
+
"@krr2020/taskflow-core": "0.1.0-beta.1"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"typescript": "^5.3.3",
|
|
21
|
+
"@types/node": "^20.11.0"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"start": "node dist/index.js"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import {
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
ListToolsRequestSchema,
|
|
7
|
+
ErrorCode,
|
|
8
|
+
McpError,
|
|
9
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { StateMachine, ConfigLoader, GitManager } from "@krr2020/taskflow-core";
|
|
12
|
+
|
|
13
|
+
// Initialize Core Components
|
|
14
|
+
const configLoader = new ConfigLoader();
|
|
15
|
+
const gitManager = new GitManager();
|
|
16
|
+
const stateMachine = new StateMachine(configLoader, gitManager);
|
|
17
|
+
|
|
18
|
+
// Initialize MCP Server
|
|
19
|
+
const server = new Server(
|
|
20
|
+
{
|
|
21
|
+
name: "taskflow-mcp-server",
|
|
22
|
+
version: "0.1.0",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
capabilities: {
|
|
26
|
+
tools: {},
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// Tool Definitions
|
|
32
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
33
|
+
return {
|
|
34
|
+
tools: [
|
|
35
|
+
{
|
|
36
|
+
name: "start_task",
|
|
37
|
+
description: "Start a new task, checking out the correct story branch and entering PLANNING mode.",
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
taskId: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "The ID of the task to start (e.g., '1.2.3')",
|
|
44
|
+
},
|
|
45
|
+
storyId: {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "The Story ID this task belongs to (e.g., '15')",
|
|
48
|
+
},
|
|
49
|
+
slug: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "Short slug for the story (e.g., 'user-auth')",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
required: ["taskId", "storyId", "slug"],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "approve_plan",
|
|
59
|
+
description: "Approve the implementation plan and switch to EXECUTION mode.",
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: "object",
|
|
62
|
+
properties: {},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "get_status",
|
|
67
|
+
description: "Get the current state machine status and active task.",
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: "object",
|
|
70
|
+
properties: {},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "generate_prd",
|
|
75
|
+
description: "Generate a PRD template based on project context.",
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: "object",
|
|
78
|
+
properties: {
|
|
79
|
+
requirements: { type: "string" }
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "generate_tasks",
|
|
85
|
+
description: "Generate tasks from a PRD.",
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: "object",
|
|
88
|
+
properties: {
|
|
89
|
+
prdContent: { type: "string" }
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "run_checks",
|
|
95
|
+
description: "Run project validations and enter VERIFICATION state.",
|
|
96
|
+
inputSchema: {
|
|
97
|
+
type: "object",
|
|
98
|
+
properties: {},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "submit_task",
|
|
103
|
+
description: "Submit the current task and complete the workflow.",
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: "object",
|
|
106
|
+
properties: {},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Tool Execution
|
|
114
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
115
|
+
const { name, arguments: args } = request.params;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
switch (name) {
|
|
119
|
+
case "start_task": {
|
|
120
|
+
const schema = z.object({
|
|
121
|
+
taskId: z.string(),
|
|
122
|
+
storyId: z.string(),
|
|
123
|
+
slug: z.string(),
|
|
124
|
+
});
|
|
125
|
+
const { taskId, storyId, slug } = schema.parse(args);
|
|
126
|
+
|
|
127
|
+
await stateMachine.startTask(taskId, storyId, slug);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
content: [
|
|
131
|
+
{
|
|
132
|
+
type: "text",
|
|
133
|
+
text: `Task ${taskId} started on branch story/S${storyId}-${slug}. State is now PLANNING.`,
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
case "approve_plan": {
|
|
140
|
+
stateMachine.approvePlan();
|
|
141
|
+
return {
|
|
142
|
+
content: [
|
|
143
|
+
{
|
|
144
|
+
type: "text",
|
|
145
|
+
text: "Plan approved. State is now EXECUTION. You may now write code.",
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
case "get_status": {
|
|
152
|
+
return {
|
|
153
|
+
content: [
|
|
154
|
+
{
|
|
155
|
+
type: "text",
|
|
156
|
+
text: JSON.stringify({
|
|
157
|
+
state: stateMachine.getState(),
|
|
158
|
+
activeTask: stateMachine.getActiveTask(),
|
|
159
|
+
}, null, 2),
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
case "generate_prd": {
|
|
166
|
+
const template = `# Project Requirements Document
|
|
167
|
+
## 1. Objective
|
|
168
|
+
[Describe the goal]
|
|
169
|
+
|
|
170
|
+
## 2. Scope
|
|
171
|
+
- [ ] In Scope
|
|
172
|
+
- [ ] Out of Scope
|
|
173
|
+
|
|
174
|
+
## 3. Technical Requirements
|
|
175
|
+
- Language: [e.g., TypeScript]
|
|
176
|
+
- Framework: [e.g., React]
|
|
177
|
+
|
|
178
|
+
## 4. User Stories
|
|
179
|
+
- Story 1: [Description]
|
|
180
|
+
`;
|
|
181
|
+
return {
|
|
182
|
+
content: [
|
|
183
|
+
{
|
|
184
|
+
type: "text",
|
|
185
|
+
text: template,
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
case "generate_tasks": {
|
|
192
|
+
// In a real implementation, this would parse the PRD.
|
|
193
|
+
// For prototype, we return the schema structure.
|
|
194
|
+
return {
|
|
195
|
+
content: [
|
|
196
|
+
{
|
|
197
|
+
type: "text",
|
|
198
|
+
text: "Please provide the tasks in the following JSON format matching the TaskSchema:\n" +
|
|
199
|
+
JSON.stringify({
|
|
200
|
+
id: "1.0",
|
|
201
|
+
title: "Example Task",
|
|
202
|
+
status: "todo",
|
|
203
|
+
subtasks: [{ id: "1.1", title: "Subtask 1" }]
|
|
204
|
+
}, null, 2)
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
case "run_checks": {
|
|
211
|
+
stateMachine.startVerification();
|
|
212
|
+
// TODO: Actually run the validation commands from config
|
|
213
|
+
return {
|
|
214
|
+
content: [{ type: "text", text: "Verification phase started. Running checks... [MOCK PASSED]" }],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
case "submit_task": {
|
|
219
|
+
stateMachine.completeTask();
|
|
220
|
+
return {
|
|
221
|
+
content: [{ type: "text", text: "Task submitted and completed. State is now IDLE." }],
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
default:
|
|
226
|
+
throw new McpError(
|
|
227
|
+
ErrorCode.MethodNotFound,
|
|
228
|
+
`Unknown tool: ${name}`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (error instanceof z.ZodError) {
|
|
233
|
+
throw new McpError(
|
|
234
|
+
ErrorCode.InvalidParams,
|
|
235
|
+
`Invalid arguments: ${error.message}`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
239
|
+
return {
|
|
240
|
+
content: [
|
|
241
|
+
{
|
|
242
|
+
type: "text",
|
|
243
|
+
text: `Error executing ${name}: ${errorMessage}`,
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
isError: true,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Start Server
|
|
252
|
+
async function main() {
|
|
253
|
+
const transport = new StdioServerTransport();
|
|
254
|
+
await server.connect(transport);
|
|
255
|
+
console.error("Taskflow MCP Server running on stdio");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
main().catch((error) => {
|
|
259
|
+
console.error("Fatal error in main():", error);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true
|
|
12
|
+
},
|
|
13
|
+
"include": [
|
|
14
|
+
"src/**/*"
|
|
15
|
+
],
|
|
16
|
+
"exclude": [
|
|
17
|
+
"node_modules"
|
|
18
|
+
]
|
|
19
|
+
}
|