# Directory Structure
```
├── .env-template
├── .gitignore
├── HOWTO.md
├── package.json
├── README.md
├── src
│ ├── http-server.ts
│ ├── index.ts
│ └── test-client.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Environment variables
2 | .env
3 | .env.local
4 | .env.development
5 | .env.test
6 | .env.production
7 |
8 | # Dependencies
9 | node_modules/
10 | package-lock.json
11 | yarn.lock
12 | pnpm-lock.yaml
13 | .cursor
14 |
15 | CLAUDE.md
16 |
17 | # Logs
18 | logs/
19 | *.log
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 | api_debug.log
24 | api_error.log
25 | server_debug.txt
26 | server_error.txt
27 |
28 | # Testing
29 | test-reports/
30 | coverage/
31 |
32 | # Production build
33 | dist/
34 | build/
35 |
36 | # Misc
37 | .DS_Store
38 | .idea/
39 | .vscode/
40 | *.swp
41 | *.swo
```
--------------------------------------------------------------------------------
/.env-template:
--------------------------------------------------------------------------------
```
1 | # Task API Server - Environment Variables Template
2 | # Copy this file to .env and fill in your own values
3 |
4 | # API Configuration (Required)
5 | # ---------------------------
6 | # URL for the Task API server
7 | TASK_MANAGER_API_BASE_URL=https://taskmanager.mcpai.io/api
8 |
9 | # API key for authentication
10 | TASK_MANAGER_API_KEY=your_api_key_here
11 |
12 | # Server Configuration (Optional)
13 | # ------------------------------
14 | # Port for the HTTP server (defaults to 3000 if not specified)
15 | TASK_MANAGER_HTTP_PORT=3500
16 |
17 | # Alternative port name - takes precedence over TASK_MANAGER_HTTP_PORT if set
18 | # PORT=3000
19 |
20 | # Logging Configuration (Optional)
21 | # ------------------------------
22 | # Enable (1) or disable (0) debug logging (defaults to 0 if not specified)
23 | TASK_MANAGER_DEBUG=0
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Task API Server - MCP TypeScript Implementation
2 |
3 | A Model Context Protocol (MCP) implementation for Task Management API written in TypeScript. This project serves as both a reference implementation and a functional task management server.
4 |
5 | ## Overview
6 |
7 | This MCP server connects to an external Task API service and provides a standardized interface for task management. It supports two runtime modes:
8 |
9 | 1. **STDIO Mode**: Standard input/output communication for CLI-based applications and AI agents
10 | 2. **HTTP+SSE Mode**: Web-accessible server with Server-Sent Events for browser and HTTP-based clients
11 |
12 | The server offers a complete set of task management operations, extensive validation, and robust error handling.
13 |
14 | ## Features
15 |
16 | - **Task Management Operations**:
17 | - List existing tasks with filtering capabilities
18 | - Create new tasks with customizable properties
19 | - Update task details (description, status, category, priority)
20 | - Delete tasks when completed or no longer needed
21 |
22 | - **Dual Interface Modes**:
23 | - STDIO protocol support for command-line and AI agent integration
24 | - HTTP+SSE protocol with web interface for browser-based access
25 |
26 | - **MCP Protocol Implementation**:
27 | - Complete implementation of the Model Context Protocol
28 | - Resources for task data structures
29 | - Tools for task operations
30 | - Error handling and informative messages
31 |
32 | - **Quality Assurance**:
33 | - Comprehensive test client for validation
34 | - Automatic server shutdown after tests complete
35 | - Detailed validation of API responses
36 |
37 | ## Getting Started
38 |
39 | ### Prerequisites
40 |
41 | - Node.js 16.x or higher
42 | - npm or pnpm package manager
43 |
44 | ### Installation
45 |
46 | 1. Clone the repository:
47 | ```
48 | git clone https://github.com/yourusername/mcp-template-ts.git
49 | cd mcp-template-ts
50 | ```
51 |
52 | 2. Install dependencies:
53 | ```
54 | npm install
55 | ```
56 | or using pnpm:
57 | ```
58 | pnpm install
59 | ```
60 |
61 | 3. Create an `.env` file with your Task API credentials:
62 | ```
63 | TASK_MANAGER_API_BASE_URL=https://your-task-api-url.com/api
64 | TASK_MANAGER_API_KEY=your_api_key_here
65 | TASK_MANAGER_HTTP_PORT=3000
66 | ```
67 |
68 | 4. Build the project:
69 | ```
70 | npm run build
71 | ```
72 |
73 | ### Running the Server
74 |
75 | #### STDIO Mode (for CLI/AI integration)
76 |
77 | ```
78 | npm start
79 | ```
80 | or
81 | ```
82 | node dist/index.js
83 | ```
84 |
85 | #### HTTP Mode (for web access)
86 |
87 | ```
88 | npm run start:http
89 | ```
90 | or
91 | ```
92 | node dist/http-server.js
93 | ```
94 |
95 | By default, the HTTP server runs on port 3000. You can change this by setting the `TASK_MANAGER_HTTP_PORT` environment variable.
96 |
97 | ### Testing
98 |
99 | Run the comprehensive test suite to verify functionality:
100 |
101 | ```
102 | npm test
103 | ```
104 |
105 | This will:
106 | 1. Build the project
107 | 2. Start a server instance
108 | 3. Connect a test client to the server
109 | 4. Run through all task operations
110 | 5. Verify correct responses
111 | 6. Automatically shut down the server
112 |
113 | ## Using the MCP Client
114 |
115 | ### STDIO Client
116 |
117 | To connect to the STDIO server from your application:
118 |
119 | ```typescript
120 | import { Client } from "@modelcontextprotocol/sdk/client/index.js";
121 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
122 | import * as path from 'path';
123 |
124 | // Create transport
125 | const transport = new StdioClientTransport({
126 | command: 'node',
127 | args: [path.resolve('path/to/dist/index.js')]
128 | });
129 |
130 | // Initialize client
131 | const client = new Client(
132 | {
133 | name: "your-client-name",
134 | version: "1.0.0"
135 | },
136 | {
137 | capabilities: {
138 | prompts: {},
139 | resources: {},
140 | tools: {}
141 | }
142 | }
143 | );
144 |
145 | // Connect to server
146 | await client.connect(transport);
147 |
148 | // Example: List all tasks
149 | const listTasksResult = await client.callTool({
150 | name: "listTasks",
151 | arguments: {}
152 | });
153 |
154 | // Example: Create a new task
155 | const createTaskResult = await client.callTool({
156 | name: "createTask",
157 | arguments: {
158 | task: "Complete project documentation",
159 | category: "Documentation",
160 | priority: "high"
161 | }
162 | });
163 |
164 | // Clean up when done
165 | await client.close();
166 | ```
167 |
168 | ### HTTP Client
169 |
170 | To connect to the HTTP server from a browser:
171 |
172 | ```html
173 | <!DOCTYPE html>
174 | <html>
175 | <head>
176 | <title>Task Manager</title>
177 | <script type="module">
178 | import { Client } from 'https://cdn.jsdelivr.net/npm/@modelcontextprotocol/sdk/dist/esm/client/index.js';
179 | import { SSEClientTransport } from 'https://cdn.jsdelivr.net/npm/@modelcontextprotocol/sdk/dist/esm/client/sse.js';
180 |
181 | document.addEventListener('DOMContentLoaded', async () => {
182 | // Create transport
183 | const transport = new SSEClientTransport('http://localhost:3000/mcp');
184 |
185 | // Initialize client
186 | const client = new Client(
187 | {
188 | name: "browser-client",
189 | version: "1.0.0"
190 | },
191 | {
192 | capabilities: {
193 | prompts: {},
194 | resources: {},
195 | tools: {}
196 | }
197 | }
198 | );
199 |
200 | // Connect to server
201 | await client.connect(transport);
202 |
203 | // Now you can use client.callTool() for tasks
204 | });
205 | </script>
206 | </head>
207 | <body>
208 | <h1>Task Manager</h1>
209 | <!-- Your interface elements here -->
210 | </body>
211 | </html>
212 | ```
213 |
214 | ## Available Tools
215 |
216 | ### listTasks
217 |
218 | Lists all available tasks.
219 |
220 | ```typescript
221 | const result = await client.callTool({
222 | name: "listTasks",
223 | arguments: {
224 | // Optional filters
225 | status: "pending", // Filter by status
226 | category: "Work", // Filter by category
227 | priority: "high" // Filter by priority
228 | }
229 | });
230 | ```
231 |
232 | ### createTask
233 |
234 | Creates a new task.
235 |
236 | ```typescript
237 | const result = await client.callTool({
238 | name: "createTask",
239 | arguments: {
240 | task: "Complete the project report", // Required: task description
241 | category: "Work", // Optional: task category
242 | priority: "high" // Optional: low, medium, high
243 | }
244 | });
245 | ```
246 |
247 | ### updateTask
248 |
249 | Updates an existing task.
250 |
251 | ```typescript
252 | const result = await client.callTool({
253 | name: "updateTask",
254 | arguments: {
255 | taskId: 123, // Required: ID of task to update
256 | task: "Updated task description", // Optional: new description
257 | status: "done", // Optional: pending, started, done
258 | category: "Personal", // Optional: new category
259 | priority: "medium" // Optional: low, medium, high
260 | }
261 | });
262 | ```
263 |
264 | ### deleteTask
265 |
266 | Deletes a task.
267 |
268 | ```typescript
269 | const result = await client.callTool({
270 | name: "deleteTask",
271 | arguments: {
272 | taskId: 123 // Required: ID of task to delete
273 | }
274 | });
275 | ```
276 |
277 | ## Environment Variables
278 |
279 | | Variable | Description | Default |
280 | |----------|-------------|---------|
281 | | TASK_MANAGER_API_BASE_URL | URL for the external Task API | None (Required) |
282 | | TASK_MANAGER_API_KEY | API key for authentication | None (Required) |
283 | | TASK_MANAGER_HTTP_PORT | Port for the HTTP server | 3000 |
284 | | PORT | Alternative port name (takes precedence) | None |
285 |
286 | ## Project Structure
287 |
288 | ```
289 | mcp-template-ts/
290 | ├── dist/ # Compiled JavaScript files
291 | ├── src/ # TypeScript source files
292 | │ ├── index.ts # STDIO server entry point
293 | │ ├── http-server.ts # HTTP+SSE server entry point
294 | │ ├── test-client.ts # Test client implementation
295 | ├── .env # Environment variables
296 | ├── package.json # Project dependencies
297 | ├── tsconfig.json # TypeScript configuration
298 | └── README.md # Project documentation
299 | ```
300 |
301 | ## Development
302 |
303 | 1. Start the TypeScript compiler in watch mode:
304 | ```
305 | npm run watch
306 | ```
307 |
308 | 2. Run tests to verify changes:
309 | ```
310 | npm test
311 | ```
312 |
313 | ## License
314 |
315 | This project is licensed under the MIT License - see the LICENSE file for details.
316 |
317 | ## Acknowledgments
318 |
319 | - This project uses the [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/sdk) for MCP protocol implementation
320 | - Built for integration with AI tooling and web applications
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "strict": true,
7 | "esModuleInterop": true,
8 | "skipLibCheck": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "resolveJsonModule": true,
11 | "outDir": "./dist",
12 | "rootDir": "./src"
13 | },
14 | "include": ["src/**/*"],
15 | "exclude": ["node_modules"]
16 | }
17 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "Task API Server",
3 | "version": "1.0.0",
4 | "description": "Task API Server TypeScript",
5 | "license": "MIT",
6 | "author": "MCPAI.io",
7 | "type": "module",
8 | "bin": {
9 | "mcp-template-ts": "dist/index.js"
10 | },
11 | "files": [
12 | "dist"
13 | ],
14 | "scripts": {
15 | "build": "tsc && shx chmod +x dist/*.js",
16 | "prepare": "npm run build",
17 | "watch": "tsc --watch",
18 | "test": "tsc && shx chmod +x dist/*.js && node dist/test-client.js",
19 | "start": "node dist/index.js",
20 | "start:http": "node dist/http-server.js"
21 | },
22 | "dependencies": {
23 | "@modelcontextprotocol/sdk": "^1.6.0",
24 | "axios": "^1.8.1",
25 | "cors": "^2.8.5",
26 | "dotenv": "^16.4.7",
27 | "express": "^5.0.1",
28 | "zod": "^3.24.2"
29 | },
30 | "devDependencies": {
31 | "@types/cors": "^2.8.17",
32 | "@types/express": "^5.0.0",
33 | "@types/node": "^22.13.9",
34 | "shx": "^0.3.4",
35 | "typescript": "^5.7.3"
36 | }
37 | }
38 |
```
--------------------------------------------------------------------------------
/HOWTO.md:
--------------------------------------------------------------------------------
```markdown
1 | # How to Use the MCP Task Manager API
2 |
3 | This guide will walk you through installing, configuring, testing, and running the Model Context Protocol (MCP) Task Manager API.
4 |
5 | ## Table of Contents
6 |
7 | 1. [Overview](#overview)
8 | 2. [Installation](#installation)
9 | 3. [Configuration](#configuration)
10 | 4. [Running the Server](#running-the-server)
11 | 5. [Testing](#testing)
12 | 6. [Using the Client](#using-the-client)
13 | 7. [API Reference](#api-reference)
14 | 8. [Troubleshooting](#troubleshooting)
15 |
16 | ## Overview
17 |
18 | This package implements a Model Context Protocol (MCP) server that wraps an external Task Manager API. It provides resources and tools for managing tasks through the MCP standard, allowing AI assistants to interact with your task management system.
19 |
20 | Key features:
21 | - List, create, update, and delete tasks
22 | - Filter tasks by status and priority
23 | - Natural language task creation
24 | - Task progress reporting
25 |
26 | ## Installation
27 |
28 | ### Prerequisites
29 |
30 | - Node.js 16 or higher
31 | - npm or pnpm
32 |
33 | ### Steps
34 |
35 | 1. Clone the repository:
36 | ```bash
37 | git clone <repository-url>
38 | cd mcp-template-ts
39 | ```
40 |
41 | 2. Install dependencies:
42 | ```bash
43 | npm install
44 | # or with pnpm
45 | pnpm install
46 | ```
47 |
48 | 3. Build the project:
49 | ```bash
50 | npm run build
51 | # or
52 | pnpm run build
53 | ```
54 |
55 | ## Configuration
56 |
57 | Create a `.env` file in the project root with the following variables:
58 |
59 | ```
60 | TASK_MANAGER_API_BASE_URL=https://your-task-api-url.com/api
61 | TASK_MANAGER_API_KEY=your_api_key
62 | ```
63 |
64 | Configuration notes:
65 | - `TASK_MANAGER_API_BASE_URL` - The URL for your Task API server (default: "https://task-master-pro-mikaelwestoo.replit.app/api")
66 | - `TASK_MANAGER_API_KEY` - Your API key for authentication (required)
67 |
68 | ## Running the Server
69 |
70 | Run the MCP server to make it available to clients:
71 |
72 | ```bash
73 | node dist/index.js
74 | ```
75 |
76 | The server will start and listen for MCP commands on stdin/stdout.
77 |
78 | To keep the server running in watch mode during development:
79 |
80 | ```bash
81 | npm run watch
82 | ```
83 |
84 | ## Testing
85 |
86 | Run the automated tests to verify the server is working correctly:
87 |
88 | ```bash
89 | npm test
90 | ```
91 |
92 | This will:
93 | 1. Start an MCP server instance
94 | 2. Connect a test client
95 | 3. Test all available tools (list, create, update, delete tasks)
96 | 4. Check resource availability
97 | 5. Report test results
98 |
99 | The test client uses the MCP SDK to communicate with the server, simulating how an AI assistant would interact with it.
100 |
101 | ## Using the Client
102 |
103 | You can build your own client or use the provided example client:
104 |
105 | 1. Build the client (if you've modified it):
106 | ```bash
107 | npm run build
108 | ```
109 |
110 | 2. Run the client:
111 | ```bash
112 | node dist/client.js
113 | ```
114 |
115 | ### Client Integration
116 |
117 | To integrate with your own application, use the MCP SDK client:
118 |
119 | ```typescript
120 | import { Client } from "@modelcontextprotocol/sdk/client/index.js";
121 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
122 |
123 | // Create a client transport
124 | const transport = new StdioClientTransport({
125 | command: "node",
126 | args: ["./dist/index.js"]
127 | });
128 |
129 | // Initialize client
130 | const client = new Client(
131 | {
132 | name: "your-client-name",
133 | version: "1.0.0"
134 | },
135 | {
136 | capabilities: {
137 | prompts: {},
138 | resources: {},
139 | tools: {}
140 | }
141 | }
142 | );
143 |
144 | // Connect to the server
145 | await client.connect(transport);
146 |
147 | // Use the client
148 | const tasks = await client.callTool({
149 | name: "listTasks",
150 | arguments: {}
151 | });
152 |
153 | console.log(tasks.content[0].text);
154 | ```
155 |
156 | ## API Reference
157 |
158 | ### Tools
159 |
160 | #### listTasks
161 | Lists all tasks, optionally filtered by status or priority.
162 |
163 | Parameters:
164 | - `status` (optional): "not_started", "started", or "done"
165 | - `priority` (optional): "low", "medium", or "high"
166 |
167 | #### createTask
168 | Creates a new task.
169 |
170 | Parameters:
171 | - `task` (required): Task description/title
172 | - `category` (required): Task category
173 | - `priority` (optional): "low", "medium", or "high"
174 | - `status` (optional): "not_started", "started", or "done"
175 |
176 | #### updateTask
177 | Updates an existing task.
178 |
179 | Parameters:
180 | - `taskId` (required): ID of the task to update
181 | - `task` (optional): New task description
182 | - `category` (optional): New task category
183 | - `priority` (optional): New task priority
184 | - `status` (optional): New task status
185 |
186 | #### deleteTask
187 | Deletes a task.
188 |
189 | Parameters:
190 | - `taskId` (required): ID of the task to delete
191 |
192 | ### Prompts
193 |
194 | #### listAllTasks
195 | Lists all tasks grouped by category with priority summaries.
196 |
197 | #### createTaskNaturalLanguage
198 | Creates a task from a natural language description.
199 |
200 | Parameters:
201 | - `description`: Natural language description of the task
202 |
203 | #### createNewTask
204 | Creates a task with specific parameters.
205 |
206 | Parameters:
207 | - `task`: Task description
208 | - `category`: Task category
209 | - `priority` (optional): Task priority
210 |
211 | #### taskProgressReport
212 | Generates a progress report on tasks.
213 |
214 | Parameters:
215 | - `status` (optional): Filter by task status
216 |
217 | ### Resources
218 |
219 | - `tasks://list`: List of all tasks
220 | - `tasks://task/{taskId}`: Details of a specific task
221 |
222 | ## Troubleshooting
223 |
224 | ### Common Issues
225 |
226 | 1. **API Key Authentication Failed**
227 | - Ensure you've set the correct API key in the `.env` file
228 | - Check if the API key has the necessary permissions
229 |
230 | 2. **Cannot Connect to Task API**
231 | - Verify the API base URL is correct in your `.env` file
232 | - Check your network connection
233 | - Look for error details in the api_error.log file
234 |
235 | 3. **TypeScript Build Errors**
236 | - Run `npm install` to ensure all dependencies are installed
237 | - Check that you're using Node.js 16+
238 |
239 | 4. **Test Client Errors**
240 | - Check that the server is running on the expected path
241 | - Verify the MCP SDK version is compatible with your code
242 |
243 | For more detailed debugging, check the logs in:
244 | - `api_debug.log`: Detailed API request logging
245 | - `api_error.log`: API error details
```
--------------------------------------------------------------------------------
/src/test-client.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5 | import * as path from 'path';
6 | import { spawn } from 'child_process';
7 |
8 | async function runTests() {
9 | console.log('Starting MCP Task Manager API Tests');
10 |
11 | // Start server in a child process
12 | const serverProcess = spawn('node', [path.resolve('./dist/index.js')], {
13 | stdio: ['pipe', 'pipe', 'pipe']
14 | });
15 |
16 | // Log server output for debugging
17 | serverProcess.stderr.on('data', (data) => {
18 | console.error(`Server stderr: ${data.toString().trim()}`);
19 | });
20 |
21 | // Declare transport outside try block so it's accessible in finally block
22 | let transport;
23 | let client;
24 |
25 | try {
26 | // Wait for server to initialize
27 | await new Promise((resolve) => setTimeout(resolve, 2000));
28 |
29 | console.log('Server should be running now. Setting up MCP client...');
30 |
31 | // Create MCP client transport using the command and args
32 | // This will spawn a new server process instead of using our existing one
33 | transport = new StdioClientTransport({
34 | command: 'node',
35 | args: [path.resolve('./dist/index.js')]
36 | });
37 |
38 | // Initialize the MCP client
39 | client = new Client(
40 | {
41 | name: "task-api-test-client",
42 | version: "1.0.0"
43 | },
44 | {
45 | capabilities: {
46 | prompts: {},
47 | resources: {},
48 | tools: {}
49 | }
50 | }
51 | );
52 |
53 | // Connect to the server
54 | await client.connect(transport);
55 | console.log('Client connected to server');
56 |
57 | // Get server capabilities
58 | console.log('\nRetrieving server capabilities...');
59 | const serverInfo = client.getServerVersion();
60 | const capabilities = client.getServerCapabilities();
61 |
62 | if (serverInfo) {
63 | console.log('Server info:');
64 | console.log(`Server name: ${serverInfo.name}`);
65 | console.log(`Server version: ${serverInfo.version}`);
66 | console.log('✅ Server info retrieved successfully');
67 | } else {
68 | console.log('❌ Failed to retrieve server info');
69 | }
70 |
71 | if (capabilities) {
72 | console.log('Server capabilities:');
73 | console.log(`Available tools: ${capabilities.tools ? 'Yes' : 'No'}`);
74 | console.log(`Available resources: ${capabilities.resources ? 'Yes' : 'No'}`);
75 | console.log('✅ Server capabilities retrieved successfully');
76 | } else {
77 | console.log('❌ Failed to retrieve server capabilities');
78 | }
79 |
80 | // Test the listTasks tool
81 | console.log('\nTesting listTasks tool...');
82 | try {
83 | const listTasksResult = await client.callTool({
84 | name: "listTasks",
85 | arguments: {}
86 | });
87 |
88 | console.log('List tasks response received:');
89 | if (Array.isArray(listTasksResult.content) && listTasksResult.content.length > 0) {
90 | const firstContent = listTasksResult.content[0];
91 | if (firstContent && 'text' in firstContent) {
92 | console.log(firstContent.text);
93 | console.log('✅ List tasks test passed');
94 | } else {
95 | console.log('❌ List tasks test failed - unexpected content format');
96 | }
97 | } else {
98 | console.log('❌ List tasks test failed - empty content');
99 | }
100 | } catch (error: any) {
101 | console.log(`❌ List tasks test failed with error: ${error.message}`);
102 | }
103 |
104 | // Test creating a task
105 | console.log('\nTesting createTask tool...');
106 | let createdTaskId: number | undefined;
107 |
108 | try {
109 | const createTaskResult = await client.callTool({
110 | name: "createTask",
111 | arguments: {
112 | task: `Test task ${new Date().toISOString()}`,
113 | category: "Test",
114 | priority: "medium"
115 | }
116 | });
117 |
118 | console.log('Create task response received:');
119 | if (Array.isArray(createTaskResult.content) && createTaskResult.content.length > 0) {
120 | const firstContent = createTaskResult.content[0];
121 | if (firstContent && 'text' in firstContent) {
122 | console.log(firstContent.text);
123 |
124 | // Extract task ID if successful
125 | const idMatch = firstContent.text.match(/ID: (\d+)/);
126 | if (idMatch && idMatch[1]) {
127 | createdTaskId = parseInt(idMatch[1], 10);
128 | console.log(`✅ Create task test passed. Created task ID: ${createdTaskId}`);
129 | } else {
130 | console.log('❌ Create task test failed - could not extract task ID');
131 | }
132 | } else {
133 | console.log('❌ Create task test failed - unexpected content format');
134 | }
135 | } else {
136 | console.log('❌ Create task test failed - empty content');
137 | }
138 | } catch (error: any) {
139 | console.log(`❌ Create task test failed with error: ${error.message}`);
140 | }
141 |
142 | // If we successfully created a task, test updating it with various field combinations
143 | if (createdTaskId) {
144 | // Test 1: Update task description
145 | console.log('\nTesting updateTask - description change...');
146 | try {
147 | const newDescription = `Updated description ${new Date().toISOString()}`;
148 | const updateDescResult = await client.callTool({
149 | name: "updateTask",
150 | arguments: {
151 | taskId: createdTaskId,
152 | task: newDescription
153 | }
154 | });
155 |
156 | console.log('Update description response received:');
157 | if (Array.isArray(updateDescResult.content) && updateDescResult.content.length > 0) {
158 | const firstContent = updateDescResult.content[0];
159 | if (firstContent && 'text' in firstContent) {
160 | console.log(firstContent.text);
161 |
162 | if (firstContent.text.includes('updated successfully')) {
163 | console.log('✅ Update description test passed');
164 | } else {
165 | console.log('❌ Update description test failed - response does not indicate success');
166 | }
167 | } else {
168 | console.log('❌ Update description test failed - unexpected content format');
169 | }
170 | } else {
171 | console.log('❌ Update description test failed - empty content');
172 | }
173 | } catch (error: any) {
174 | console.log(`❌ Update description test failed with error: ${error.message}`);
175 | }
176 |
177 | // Test 2: Update task status
178 | console.log('\nTesting updateTask - status change...');
179 | try {
180 | const updateStatusResult = await client.callTool({
181 | name: "updateTask",
182 | arguments: {
183 | taskId: createdTaskId,
184 | status: "started"
185 | }
186 | });
187 |
188 | console.log('Update status response received:');
189 | if (Array.isArray(updateStatusResult.content) && updateStatusResult.content.length > 0) {
190 | const firstContent = updateStatusResult.content[0];
191 | if (firstContent && 'text' in firstContent) {
192 | console.log(firstContent.text);
193 |
194 | if (firstContent.text.includes('updated successfully')) {
195 | console.log('✅ Update status test passed');
196 | } else {
197 | console.log('❌ Update status test failed - response does not indicate success');
198 | }
199 |
200 | // Verify the status was actually updated in the response
201 | if (updateStatusResult.content.length > 1) {
202 | const secondContent = updateStatusResult.content[1];
203 | if (secondContent && 'text' in secondContent) {
204 | const responseJson = JSON.parse(secondContent.text);
205 | if (responseJson.status === "started") {
206 | console.log('✅ Status verification passed - status is "started"');
207 | } else {
208 | console.log(`❌ Status verification failed - expected "started" but got "${responseJson.status}"`);
209 | }
210 | }
211 | }
212 | } else {
213 | console.log('❌ Update status test failed - unexpected content format');
214 | }
215 | } else {
216 | console.log('❌ Update status test failed - empty content');
217 | }
218 | } catch (error: any) {
219 | console.log(`❌ Update status test failed with error: ${error.message}`);
220 | }
221 |
222 | // Test 3: Update task category
223 | console.log('\nTesting updateTask - category change...');
224 | try {
225 | const newCategory = `Category-${Date.now().toString().slice(-5)}`;
226 | const updateCategoryResult = await client.callTool({
227 | name: "updateTask",
228 | arguments: {
229 | taskId: createdTaskId,
230 | category: newCategory
231 | }
232 | });
233 |
234 | console.log('Update category response received:');
235 | if (Array.isArray(updateCategoryResult.content) && updateCategoryResult.content.length > 0) {
236 | const firstContent = updateCategoryResult.content[0];
237 | if (firstContent && 'text' in firstContent) {
238 | console.log(firstContent.text);
239 |
240 | if (firstContent.text.includes('updated successfully')) {
241 | console.log('✅ Update category test passed');
242 | } else {
243 | console.log('❌ Update category test failed - response does not indicate success');
244 | }
245 |
246 | // Verify the category was actually updated in the response
247 | if (updateCategoryResult.content.length > 1) {
248 | const secondContent = updateCategoryResult.content[1];
249 | if (secondContent && 'text' in secondContent) {
250 | const responseJson = JSON.parse(secondContent.text);
251 | if (responseJson.category === newCategory) {
252 | console.log(`✅ Category verification passed - category is "${newCategory}"`);
253 | } else {
254 | console.log(`❌ Category verification failed - expected "${newCategory}" but got "${responseJson.category}"`);
255 | }
256 | }
257 | }
258 | } else {
259 | console.log('❌ Update category test failed - unexpected content format');
260 | }
261 | } else {
262 | console.log('❌ Update category test failed - empty content');
263 | }
264 | } catch (error: any) {
265 | console.log(`❌ Update category test failed with error: ${error.message}`);
266 | }
267 |
268 | // Test 4: Update task priority
269 | console.log('\nTesting updateTask - priority change...');
270 | try {
271 | const updatePriorityResult = await client.callTool({
272 | name: "updateTask",
273 | arguments: {
274 | taskId: createdTaskId,
275 | priority: "high"
276 | }
277 | });
278 |
279 | console.log('Update priority response received:');
280 | if (Array.isArray(updatePriorityResult.content) && updatePriorityResult.content.length > 0) {
281 | const firstContent = updatePriorityResult.content[0];
282 | if (firstContent && 'text' in firstContent) {
283 | console.log(firstContent.text);
284 |
285 | if (firstContent.text.includes('updated successfully')) {
286 | console.log('✅ Update priority test passed');
287 | } else {
288 | console.log('❌ Update priority test failed - response does not indicate success');
289 | }
290 |
291 | // Verify the priority was actually updated in the response
292 | if (updatePriorityResult.content.length > 1) {
293 | const secondContent = updatePriorityResult.content[1];
294 | if (secondContent && 'text' in secondContent) {
295 | const responseJson = JSON.parse(secondContent.text);
296 | if (responseJson.priority === "high") {
297 | console.log('✅ Priority verification passed - priority is "high"');
298 | } else {
299 | console.log(`❌ Priority verification failed - expected "high" but got "${responseJson.priority}"`);
300 | }
301 | }
302 | }
303 | } else {
304 | console.log('❌ Update priority test failed - unexpected content format');
305 | }
306 | } else {
307 | console.log('❌ Update priority test failed - empty content');
308 | }
309 | } catch (error: any) {
310 | console.log(`❌ Update priority test failed with error: ${error.message}`);
311 | }
312 |
313 | // Test 5: Update multiple fields at once
314 | console.log('\nTesting updateTask - multiple fields at once...');
315 | try {
316 | const finalDesc = `Final description ${new Date().toISOString()}`;
317 | const finalCategory = `Final-Category-${Date.now().toString().slice(-5)}`;
318 |
319 | const updateMultipleResult = await client.callTool({
320 | name: "updateTask",
321 | arguments: {
322 | taskId: createdTaskId,
323 | task: finalDesc,
324 | category: finalCategory,
325 | priority: "medium",
326 | status: "done"
327 | }
328 | });
329 |
330 | console.log('Update multiple fields response received:');
331 | if (Array.isArray(updateMultipleResult.content) && updateMultipleResult.content.length > 0) {
332 | const firstContent = updateMultipleResult.content[0];
333 | if (firstContent && 'text' in firstContent) {
334 | console.log(firstContent.text);
335 |
336 | if (firstContent.text.includes('updated successfully')) {
337 | console.log('✅ Update multiple fields test passed');
338 | } else {
339 | console.log('❌ Update multiple fields test failed - response does not indicate success');
340 | }
341 |
342 | // Verify all fields were actually updated in the response
343 | if (updateMultipleResult.content.length > 1) {
344 | const secondContent = updateMultipleResult.content[1];
345 | if (secondContent && 'text' in secondContent) {
346 | const responseJson = JSON.parse(secondContent.text);
347 | let verificationsPassed = true;
348 |
349 | if (responseJson.task !== finalDesc) {
350 | console.log(`❌ Description verification failed - expected "${finalDesc}" but got "${responseJson.task}"`);
351 | verificationsPassed = false;
352 | }
353 |
354 | if (responseJson.category !== finalCategory) {
355 | console.log(`❌ Category verification failed - expected "${finalCategory}" but got "${responseJson.category}"`);
356 | verificationsPassed = false;
357 | }
358 |
359 | if (responseJson.priority !== "medium") {
360 | console.log(`❌ Priority verification failed - expected "medium" but got "${responseJson.priority}"`);
361 | verificationsPassed = false;
362 | }
363 |
364 | if (responseJson.status !== "done") {
365 | console.log(`❌ Status verification failed - expected "done" but got "${responseJson.status}"`);
366 | verificationsPassed = false;
367 | }
368 |
369 | if (verificationsPassed) {
370 | console.log('✅ All field verifications passed');
371 | }
372 | }
373 | }
374 | } else {
375 | console.log('❌ Update multiple fields test failed - unexpected content format');
376 | }
377 | } else {
378 | console.log('❌ Update multiple fields test failed - empty content');
379 | }
380 | } catch (error: any) {
381 | console.log(`❌ Update multiple fields test failed with error: ${error.message}`);
382 | }
383 |
384 | // Finally, test deleting the task
385 | console.log('\nTesting deleteTask tool...');
386 | try {
387 | const deleteTaskResult = await client.callTool({
388 | name: "deleteTask",
389 | arguments: {
390 | taskId: createdTaskId
391 | }
392 | });
393 |
394 | console.log('Delete task response received:');
395 | if (Array.isArray(deleteTaskResult.content) && deleteTaskResult.content.length > 0) {
396 | const firstContent = deleteTaskResult.content[0];
397 | if (firstContent && 'text' in firstContent) {
398 | console.log(firstContent.text);
399 |
400 | if (firstContent.text.includes('deleted successfully') ||
401 | firstContent.text.includes('successfully deleted')) {
402 | console.log('✅ Delete task test passed');
403 | } else {
404 | console.log('❌ Delete task test failed - response does not indicate success');
405 | }
406 | } else {
407 | console.log('❌ Delete task test failed - unexpected content format');
408 | }
409 | } else {
410 | console.log('❌ Delete task test failed - empty content');
411 | }
412 | } catch (error: any) {
413 | console.log(`❌ Delete task test failed with error: ${error.message}`);
414 | }
415 | }
416 |
417 | // Test accessing resources
418 | console.log('\nTesting resources...');
419 | try {
420 | const resourcesList = await client.listResources();
421 |
422 | if (resourcesList && 'resources' in resourcesList) {
423 | const resources = resourcesList.resources;
424 | console.log(`Available resources: ${resources.map(r => r.name).join(', ')}`);
425 |
426 | if (resources.length > 0) {
427 | console.log('✅ List resources test passed');
428 |
429 | // Try to read a resource if any are available
430 | const resourceURI = `tasks://${resources[0].name}`;
431 | try {
432 | const resourceResult = await client.readResource({ uri: resourceURI });
433 |
434 | if (resourceResult && 'contents' in resourceResult) {
435 | console.log(`Resource ${resources[0].name} retrieved successfully`);
436 | console.log('✅ Read resource test passed');
437 | } else {
438 | console.log('❌ Read resource test failed - unexpected result format');
439 | }
440 | } catch (error: any) {
441 | console.log(`❌ Read resource test failed with error: ${error.message}`);
442 | }
443 | } else {
444 | console.log('ℹ️ No resources available to test');
445 | }
446 | } else {
447 | console.log('❌ List resources test failed - unexpected result format');
448 | }
449 | } catch (error: any) {
450 | console.log(`❌ List resources test failed with error: ${error.message}`);
451 | }
452 |
453 | console.log('\nTests completed');
454 |
455 | } catch (error: any) {
456 | console.error('Test execution error:', error);
457 | } finally {
458 | // Clean up - kill the server process
459 | console.log('Terminating test server');
460 | serverProcess.kill();
461 |
462 | // Make sure to close the client which will terminate the second server process
463 | if (client) {
464 | try {
465 | await client.close();
466 | console.log('Test client closed');
467 | } catch (closeError) {
468 | console.error('Error closing client:', closeError);
469 | }
470 | }
471 |
472 | // Force exit after a short delay to ensure all processes are terminated
473 | setTimeout(() => {
474 | console.log('Exiting test process');
475 | process.exit(0);
476 | }, 500);
477 | }
478 | }
479 |
480 | // Run the tests
481 | runTests().catch(console.error);
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import {
4 | McpServer,
5 | ResourceTemplate,
6 | } from "@modelcontextprotocol/sdk/server/mcp.js";
7 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8 | import { z } from "zod";
9 | import * as fs from 'fs';
10 | import axios from 'axios';
11 | import dotenv from 'dotenv';
12 |
13 | // Load environment variables from .env file
14 | dotenv.config();
15 |
16 | // Base URL for the Task API from environment variables
17 | const API_BASE_URL = process.env.TASK_MANAGER_API_BASE_URL || "https://task-master-pro-mikaelwestoo.replit.app/api";
18 | // API Key from environment variables
19 | const API_KEY = process.env.TASK_MANAGER_API_KEY;
20 |
21 | // Helper function for logging to file
22 | function logToFile(filename: string, message: string): void {
23 | const timestamp = new Date().toISOString();
24 | const logEntry = `[${timestamp}] ${message}\n`;
25 | fs.appendFileSync(filename, logEntry);
26 | }
27 |
28 | // Helper function to log errors
29 | function logError(message: string, details: any = null): void {
30 | let errorMessage = `[ERROR] ${message}`;
31 | if (details) {
32 | errorMessage += `\nDetails: ${JSON.stringify(details, null, 2)}`;
33 | }
34 | logToFile("server_error.log", errorMessage);
35 | }
36 |
37 | // Helper function to log debug info
38 | function logDebug(message: string, data: any = null): void {
39 | let debugMessage = `[DEBUG] ${message}`;
40 | if (data) {
41 | debugMessage += `\nData: ${JSON.stringify(data, null, 2)}`;
42 | }
43 | logToFile("server_debug.log", debugMessage);
44 | }
45 |
46 | // Schema definitions
47 | const TaskSchema = z.object({
48 | id: z.number().int().positive().describe("Unique task identifier"),
49 | task: z.string().describe("The task description/title"),
50 | category: z.string().describe("Task category (e.g., 'Development', 'Documentation')"),
51 | priority: z.enum(["low", "medium", "high"]).describe("Task priority level"),
52 | status: z.enum(["not_started", "started", "done"]).describe("Current task status"),
53 | create_time: z.string().describe("Task creation timestamp in ISO format"),
54 | });
55 |
56 | const TaskListSchema = z.object({
57 | tasks: z.array(TaskSchema).describe("List of tasks"),
58 | });
59 |
60 | // Create an MCP server
61 | const server = new McpServer({
62 | name: "Task Management API Server",
63 | version: "1.0.0",
64 | description: "Task Management API that provides CRUD operations for tasks with categories, priorities, and statuses",
65 | });
66 |
67 | // Helper function to make authenticated API requests
68 | async function makeApiRequest(method: string, endpoint: string, data: any = null, params: any = null): Promise<any> {
69 | const url = `${API_BASE_URL}${endpoint}`;
70 |
71 | // Validate that API_KEY is defined
72 | if (!API_KEY) {
73 | throw new Error("TASK_MANAGER_API_KEY environment variable is not defined. Please check your .env file.");
74 | }
75 |
76 | logDebug(`API Request: ${method} ${url}`);
77 |
78 | // Standard headers
79 | const headers = {
80 | "X-API-Key": API_KEY,
81 | "Content-Type": "application/json; charset=utf-8",
82 | "Accept": "application/json, text/plain, */*",
83 | "User-Agent": "TaskMcpServer/1.0",
84 | "Connection": "close",
85 | "Cache-Control": "no-cache"
86 | };
87 |
88 | try {
89 | // Log request details
90 | const logEntry = `Timestamp: ${new Date().toISOString()}\nMethod: ${method}\nURL: ${url}\nParams: ${JSON.stringify(params)}\nData: ${JSON.stringify(data)}\nHeaders: ${JSON.stringify(headers)}\n\n`;
91 | fs.appendFileSync("api_debug.log", logEntry);
92 |
93 | // Configure axios request options
94 | const requestConfig: any = {
95 | method,
96 | url,
97 | headers,
98 | data,
99 | params,
100 | maxRedirects: 0,
101 | timeout: 20000,
102 | decompress: false,
103 | validateStatus: function (status: number) {
104 | return status < 500; // Don't reject if status code is less than 500
105 | }
106 | };
107 |
108 | // Ensure proper data encoding for all requests
109 | if (data) {
110 | requestConfig.data = JSON.stringify(data);
111 | }
112 |
113 | // Add transform request for properly handling all requests
114 | requestConfig.transformRequest = [(data: any, headers: any) => {
115 | // Force proper content type
116 | headers['Content-Type'] = 'application/json; charset=utf-8';
117 | return typeof data === 'string' ? data : JSON.stringify(data);
118 | }];
119 |
120 | // Add specific URL handling for individual task endpoints
121 | if (endpoint.startsWith('/tasks/') && method === 'GET') {
122 | // Fix to retrieve individual task by adding specific query parameters
123 | requestConfig.params = { ...params, id: endpoint.split('/')[2] };
124 | }
125 |
126 | const response = await axios(requestConfig);
127 |
128 | // Check for HTTP error status codes we didn't automatically reject
129 | if (response.status >= 400 && response.status < 500) {
130 | logError(`HTTP error ${response.status} from API`, response.data);
131 |
132 | // Enhanced error logging
133 | const errorLogEntry = `Timestamp: ${new Date().toISOString()}\nError: HTTP ${response.status}\nURL: ${url}\nMethod: ${method}\nResponse: ${JSON.stringify(response.data)}\n\n`;
134 | fs.appendFileSync("api_error.log", errorLogEntry);
135 |
136 | throw new Error(`API Error (${response.status}): ${JSON.stringify(response.data)}`);
137 | }
138 |
139 | // Check if response has expected format
140 | if ((method === "POST" && endpoint === "/tasks/list") || (method === "GET" && endpoint === "/tasks")) {
141 | logDebug(`listTasks response`, response.data.tasks || []);
142 | if (!response.data || !response.data.tasks || response.data.tasks.length === 0) {
143 | logDebug("API returned empty tasks array");
144 | }
145 | }
146 |
147 | return response.data;
148 | } catch (error: any) {
149 | logError(`API Error: ${error.message}`);
150 |
151 | // Enhanced error logging with more details
152 | const errorDetails = error.response
153 | ? `Status: ${error.response.status}, Data: ${JSON.stringify(error.response.data || 'No response data')}`
154 | : (error.request ? 'No response received' : error.message);
155 |
156 | const errorLogEntry = `Timestamp: ${new Date().toISOString()}\nError: ${error.message}\nDetails: ${errorDetails}\nURL: ${url}\nMethod: ${method}\n\n`;
157 | fs.appendFileSync("api_error.log", errorLogEntry);
158 |
159 | if (error.response) {
160 | throw new Error(
161 | `API Error (${error.response.status}): ${JSON.stringify(error.response.data || 'No response data')}`,
162 | );
163 | } else if (error.request) {
164 | throw new Error(`API Request Error: No response received (possible network issue)`);
165 | }
166 | throw error;
167 | }
168 | }
169 |
170 | // Resource: Tasks list
171 | server.resource(
172 | "tasks",
173 | new ResourceTemplate("tasks://list", { list: undefined }),
174 | async (uri: any) => {
175 | try {
176 | const tasks = await makeApiRequest("POST", "/tasks/list");
177 |
178 | // Validate the tasks structure
179 | if (!tasks || !tasks.tasks || !Array.isArray(tasks.tasks)) {
180 | logError(`Invalid tasks data structure`, tasks);
181 | return {
182 | contents: [{
183 | uri: "tasks://error",
184 | text: `Error: Received invalid task data from API`,
185 | metadata: { error: "Invalid data structure", data: tasks }
186 | }]
187 | };
188 | }
189 |
190 | // Format tasks for easy display and use
191 | return {
192 | contents: tasks.tasks.map((task: any) => ({
193 | uri: `tasks://task/${task.id}`,
194 | text: `ID: ${task.id}
195 | Task: ${task.task || 'No description'}
196 | Category: ${task.category || 'Uncategorized'}
197 | Priority: ${task.priority || 'medium'}
198 | Status: ${task.status || 'not_started'}
199 | Created: ${task.create_time || 'unknown'}`,
200 | metadata: {
201 | id: task.id,
202 | task: task.task || 'No description',
203 | category: task.category,
204 | priority: task.priority || 'medium',
205 | status: task.status || 'not_started',
206 | create_time: task.create_time,
207 | },
208 | })),
209 | };
210 | } catch (error: any) {
211 | logError(`Error fetching tasks: ${error.message}`);
212 | return {
213 | contents: [{
214 | uri: "tasks://error",
215 | text: `Error retrieving tasks: ${error.message}`,
216 | metadata: { error: error.message }
217 | }]
218 | };
219 | }
220 | }
221 | );
222 |
223 | // Resource: Individual task
224 | server.resource(
225 | "task",
226 | new ResourceTemplate("tasks://task/{taskId}", { list: undefined }),
227 | async (uri: any, params: any) => {
228 | try {
229 | const taskId = params.taskId;
230 | // Try direct task endpoint first
231 | let task;
232 | try {
233 | const taskResult = await makeApiRequest("GET", `/tasks/${taskId}`);
234 | if (taskResult && (taskResult.id || taskResult.task)) {
235 | task = taskResult;
236 | }
237 | } catch (directError) {
238 | logDebug(`Direct task fetch failed, using task list fallback: ${directError}`);
239 | // Fallback to getting all tasks and filtering
240 | const tasks = await makeApiRequest("POST", "/tasks/list");
241 | task = tasks.tasks.find((t: any) => t.id === Number(taskId) || t.id === taskId);
242 | }
243 |
244 | if (!task) {
245 | return {
246 | contents: [{
247 | uri: uri.href,
248 | text: `Task with ID ${taskId} not found`,
249 | metadata: { error: "Task not found" }
250 | }]
251 | };
252 | }
253 |
254 | // Format task for easy display
255 | return {
256 | contents: [
257 | {
258 | uri: uri.href,
259 | text: `ID: ${task.id}
260 | Task: ${task.task}
261 | Category: ${task.category}
262 | Priority: ${task.priority}
263 | Status: ${task.status}
264 | Created: ${task.create_time}`,
265 | metadata: task,
266 | },
267 | ],
268 | };
269 | } catch (error: any) {
270 | return {
271 | contents: [{
272 | uri: uri.href,
273 | text: `Error retrieving task ${params.taskId}: ${error.message}`,
274 | metadata: { error: error.message }
275 | }]
276 | };
277 | }
278 | }
279 | );
280 |
281 | // Tool: List Tasks
282 | server.tool(
283 | "listTasks",
284 | {
285 | status: z.enum(["not_started", "started", "done"]).optional()
286 | .describe("Filter tasks by status (optional)"),
287 | priority: z.enum(["low", "medium", "high"]).optional()
288 | .describe("Filter tasks by priority level (optional)")
289 | },
290 | async ({ status, priority }: { status?: string, priority?: string }) => {
291 | try {
292 | const params: any = {};
293 | if (status) params.status = status;
294 | if (priority) params.priority = priority;
295 |
296 | const tasksResponse = await makeApiRequest("POST", "/tasks/list", { status, priority });
297 |
298 | // More flexible validation for tasks data structure
299 | let tasks: any[] = [];
300 |
301 | // Handle various response formats that might come from the API
302 | if (tasksResponse) {
303 | if (Array.isArray(tasksResponse.tasks)) {
304 | // Standard format: { tasks: [...] }
305 | tasks = tasksResponse.tasks;
306 | logDebug("Found tasks array in standard format");
307 | } else if (Array.isArray(tasksResponse)) {
308 | // Direct array format: [...]
309 | tasks = tasksResponse;
310 | logDebug("Found tasks in direct array format");
311 | } else if (typeof tasksResponse === 'object' && tasksResponse !== null) {
312 | // Try to extract tasks from any available property
313 | const possibleTasksProperties = Object.entries(tasksResponse)
314 | .filter(([_, value]) => Array.isArray(value))
315 | .map(([key, value]) => ({ key, value }));
316 |
317 | if (possibleTasksProperties.length > 0) {
318 | // Use the first array property as tasks
319 | const tasksProp = possibleTasksProperties[0];
320 | tasks = tasksProp.value as any[];
321 | logDebug(`Found tasks array in property: ${tasksProp.key}`);
322 | } else {
323 | logError(`No tasks array found in response`, tasksResponse);
324 | }
325 | }
326 | }
327 |
328 | // If we still couldn't find tasks, log error and return empty array
329 | if (tasks.length === 0) {
330 | logError(`Invalid or empty tasks data structure`, tasksResponse);
331 | }
332 |
333 | // Format response in a way that's useful for AI to parse
334 | const formattedTasks = tasks.map(task => ({
335 | id: task.id,
336 | task: task.task || "No description",
337 | category: task.category,
338 | priority: task.priority || "medium",
339 | status: task.status || "not_started",
340 | createTime: task.create_time || task.created_at || task.createTime || new Date().toISOString()
341 | }));
342 |
343 | // Log the formatted response for debugging
344 | logDebug(`listTasks formatted response`, formattedTasks);
345 |
346 | return {
347 | content: [
348 | {
349 | type: "text",
350 | text: `Found ${tasks.length} tasks${status ? ` with status '${status}'` : ''}${priority ? ` and priority '${priority}'` : ''}.`
351 | },
352 | {
353 | type: "text",
354 | text: JSON.stringify(formattedTasks, null, 2)
355 | }
356 | ]
357 | };
358 | } catch (error: any) {
359 | return {
360 | content: [
361 | {
362 | type: "text",
363 | text: `Error listing tasks: ${error.message}`
364 | }
365 | ]
366 | };
367 | }
368 | }
369 | );
370 |
371 | // Tool: Create Task
372 | server.tool(
373 | "createTask",
374 | {
375 | task: z.string().min(1, "Task description is required")
376 | .describe("The task description or title"),
377 | category: z.string().min(1, "Category is required")
378 | .describe("Task category (e.g., 'Development', 'Documentation')"),
379 | priority: z.enum(["low", "medium", "high"]).optional()
380 | .describe("Task priority level (defaults to 'medium' if not specified)"),
381 | status: z.enum(["not_started", "started", "done"]).optional()
382 | .describe("Initial task status (defaults to 'not_started' if not specified)")
383 | },
384 | async ({ task, category, priority, status }: {
385 | task: string;
386 | category: string;
387 | priority?: string;
388 | status?: string
389 | }) => {
390 | try {
391 | const requestBody: any = {
392 | task,
393 | category,
394 | };
395 |
396 | if (priority) requestBody.priority = priority;
397 | if (status) requestBody.status = status;
398 |
399 | const newTask = await makeApiRequest("POST", "/tasks", requestBody);
400 |
401 | logDebug(`Created new task with ID ${newTask.id}`);
402 |
403 | return {
404 | content: [
405 | {
406 | type: "text",
407 | text: `Task created successfully with ID: ${newTask.id}`
408 | },
409 | {
410 | type: "text",
411 | text: JSON.stringify({
412 | id: newTask.id,
413 | task: newTask.task || task,
414 | category: newTask.category || category,
415 | priority: newTask.priority || priority || "medium",
416 | status: newTask.status || status || "not_started",
417 | create_time: newTask.create_time || new Date().toISOString()
418 | }, null, 2)
419 | }
420 | ]
421 | };
422 | } catch (error: any) {
423 | logError(`Error in createTask: ${error.message}`);
424 |
425 | return {
426 | content: [
427 | {
428 | type: "text",
429 | text: `Error creating task: ${error.message}`
430 | }
431 | ]
432 | };
433 | }
434 | }
435 | );
436 |
437 | // Tool: Update Task
438 | server.tool(
439 | "updateTask",
440 | {
441 | taskId: z.number().int().positive("Task ID must be a positive integer")
442 | .describe("The unique ID of the task to update"),
443 | task: z.string().optional()
444 | .describe("New task description/title (if you want to change it)"),
445 | category: z.string().optional()
446 | .describe("New task category (if you want to change it)"),
447 | priority: z.enum(["low", "medium", "high"]).optional()
448 | .describe("New task priority (if you want to change it)"),
449 | status: z.enum(["not_started", "started", "done"]).optional()
450 | .describe("New task status (if you want to change it)")
451 | },
452 | async ({ taskId, task, category, priority, status }: {
453 | taskId: number;
454 | task?: string;
455 | category?: string;
456 | priority?: string;
457 | status?: string;
458 | }) => {
459 | try {
460 | const requestBody: any = {};
461 |
462 | if (task) requestBody.task = task;
463 | if (category) requestBody.category = category;
464 | if (priority) requestBody.priority = priority;
465 | if (status) requestBody.status = status;
466 |
467 | if (Object.keys(requestBody).length === 0) {
468 | return {
469 | content: [
470 | {
471 | type: "text",
472 | text: "No updates provided. Task remains unchanged."
473 | }
474 | ]
475 | };
476 | }
477 |
478 | const updatedTask = await makeApiRequest(
479 | "PATCH",
480 | `/tasks/${taskId}`,
481 | requestBody
482 | );
483 |
484 | return {
485 | content: [
486 | {
487 | type: "text",
488 | text: `Task ${taskId} updated successfully.`
489 | },
490 | {
491 | type: "text",
492 | text: JSON.stringify({
493 | id: updatedTask.id,
494 | task: updatedTask.task,
495 | category: updatedTask.category,
496 | priority: updatedTask.priority,
497 | status: updatedTask.status,
498 | created: updatedTask.create_time
499 | }, null, 2)
500 | }
501 | ]
502 | };
503 | } catch (error: any) {
504 | return {
505 | content: [
506 | {
507 | type: "text",
508 | text: `Error updating task: ${error.message}`
509 | }
510 | ]
511 | };
512 | }
513 | }
514 | );
515 |
516 | // Tool: Delete Task
517 | server.tool(
518 | "deleteTask",
519 | {
520 | taskId: z.number().int().positive("Task ID must be a positive integer")
521 | .describe("The unique ID of the task to delete")
522 | },
523 | async ({ taskId }: { taskId: number }) => {
524 | try {
525 | const response = await makeApiRequest("DELETE", `/tasks/${taskId}`);
526 |
527 | logDebug(`Deleted task ID ${taskId}`);
528 |
529 | return {
530 | content: [
531 | {
532 | type: "text",
533 | text: response.message || `Task ${taskId} deleted successfully.`
534 | }
535 | ]
536 | };
537 | } catch (error: any) {
538 | logError(`Error in deleteTask: ${error.message}`);
539 |
540 | return {
541 | content: [
542 | {
543 | type: "text",
544 | text: `Error deleting task: ${error.message}`
545 | }
546 | ]
547 | };
548 | }
549 | }
550 | );
551 |
552 | // Prompt: List all tasks with category analysis
553 | server.prompt(
554 | "listAllTasks",
555 | {},
556 | () => ({
557 | messages: [
558 | {
559 | role: "user",
560 | content: {
561 | type: "text",
562 | text: "Please list all tasks in my task management system. Group them by category and summarize the priorities for each category."
563 | }
564 | }
565 | ]
566 | })
567 | );
568 |
569 | // Prompt: Create task with natural language
570 | server.prompt(
571 | "createTaskNaturalLanguage",
572 | {
573 | description: z.string().min(10, "Task description must be at least 10 characters")
574 | .describe("A natural language description of the task to create")
575 | },
576 | ({ description }: { description: string }) => ({
577 | messages: [
578 | {
579 | role: "user",
580 | content: {
581 | type: "text",
582 | text: `Please analyze this task description and create an appropriate task:
583 |
584 | "${description}"
585 |
586 | Extract the most suitable category, determine an appropriate priority level, and create the task with the right parameters.`
587 | }
588 | }
589 | ]
590 | })
591 | );
592 |
593 | // Prompt: Create new task with specific parameters
594 | server.prompt(
595 | "createNewTask",
596 | {
597 | task: z.string().min(1, "Task description is required")
598 | .describe("The task description or title"),
599 | category: z.string().min(1, "Category is required")
600 | .describe("Task category"),
601 | priority: z.enum(["low", "medium", "high"]).optional()
602 | .describe("Task priority level")
603 | },
604 | ({ task, category, priority }: { task: string; category: string; priority?: string }) => ({
605 | messages: [
606 | {
607 | role: "user",
608 | content: {
609 | type: "text",
610 | text: `Please create a new task in my task management system with the following details:
611 |
612 | Task: ${task}
613 | Category: ${category}
614 | ${priority ? `Priority: ${priority}` : ""}
615 |
616 | Please confirm once the task is created and provide the task ID for reference.`
617 | }
618 | }
619 | ]
620 | })
621 | );
622 |
623 | // Prompt: Task progress report
624 | server.prompt(
625 | "taskProgressReport",
626 | {
627 | status: z.enum(["not_started", "started", "done"]).optional()
628 | .describe("Filter by task status")
629 | },
630 | ({ status }: { status?: string }) => ({
631 | messages: [
632 | {
633 | role: "user",
634 | content: {
635 | type: "text",
636 | text: `Please provide a progress report on ${status ? `all ${status} tasks` : "all tasks"}.
637 |
638 | Include:
639 | 1. How many tasks are in each status category
640 | 2. Which high priority tasks need attention
641 | 3. Any categories with a high concentration of incomplete tasks`
642 | }
643 | }
644 | ]
645 | })
646 | );
647 |
648 | // Start receiving messages on stdin and sending messages on stdout
649 | const transport = new StdioServerTransport();
650 | await server.connect(transport);
```
--------------------------------------------------------------------------------
/src/http-server.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import {
4 | McpServer,
5 | ResourceTemplate,
6 | } from "@modelcontextprotocol/sdk/server/mcp.js";
7 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
8 | import express from 'express';
9 | import cors from 'cors';
10 | import { z } from "zod";
11 | import * as fs from 'fs';
12 | import axios from 'axios';
13 | import dotenv from 'dotenv';
14 |
15 | // Load environment variables from .env file
16 | dotenv.config();
17 |
18 | // Base URL for the Task API from environment variables
19 | const API_BASE_URL = process.env.TASK_MANAGER_API_BASE_URL || "https://task-master-pro-mikaelwestoo.replit.app/api";
20 | // API Key from environment variables
21 | const API_KEY = process.env.TASK_MANAGER_API_KEY;
22 | // HTTP server port - prioritize PORT for backward compatibility, then TASK_MANAGER_HTTP_PORT from .env, then default to 3000
23 | const PORT = process.env.PORT || process.env.TASK_MANAGER_HTTP_PORT || 3000;
24 |
25 | // Helper function for logging to file
26 | function logToFile(filename: string, message: string): void {
27 | const timestamp = new Date().toISOString();
28 | const logEntry = `[${timestamp}] ${message}\n`;
29 | fs.appendFileSync(filename, logEntry);
30 | }
31 |
32 | // Helper function to log errors
33 | function logError(message: string, details: any = null): void {
34 | let errorMessage = `[ERROR] ${message}`;
35 | if (details) {
36 | errorMessage += `\nDetails: ${JSON.stringify(details, null, 2)}`;
37 | }
38 | logToFile("server_error.log", errorMessage);
39 | }
40 |
41 | // Helper function to log debug info
42 | function logDebug(message: string, data: any = null): void {
43 | let debugMessage = `[DEBUG] ${message}`;
44 | if (data) {
45 | debugMessage += `\nData: ${JSON.stringify(data, null, 2)}`;
46 | }
47 | logToFile("server_debug.log", debugMessage);
48 | }
49 |
50 | // Schema definitions
51 | const TaskSchema = z.object({
52 | id: z.number().int().positive().describe("Unique task identifier"),
53 | task: z.string().describe("The task description/title"),
54 | category: z.string().describe("Task category (e.g., 'Development', 'Documentation')"),
55 | priority: z.enum(["low", "medium", "high"]).describe("Task priority level"),
56 | status: z.enum(["not_started", "started", "done"]).describe("Current task status"),
57 | create_time: z.string().describe("Task creation timestamp in ISO format"),
58 | });
59 |
60 | const TaskListSchema = z.object({
61 | tasks: z.array(TaskSchema).describe("List of tasks"),
62 | });
63 |
64 | // Helper function to make authenticated API requests
65 | async function makeApiRequest(method: string, endpoint: string, data: any = null, params: any = null): Promise<any> {
66 | const url = `${API_BASE_URL}${endpoint}`;
67 |
68 | // Validate that API_KEY is defined
69 | if (!API_KEY) {
70 | throw new Error("TASK_MANAGER_API_KEY environment variable is not defined. Please check your .env file.");
71 | }
72 |
73 | logDebug(`API Request: ${method} ${url}`);
74 |
75 | // Standard headers
76 | const headers = {
77 | "X-API-Key": API_KEY,
78 | "Content-Type": "application/json; charset=utf-8",
79 | "Accept": "application/json, text/plain, */*",
80 | "User-Agent": "TaskMcpServer/1.0",
81 | "Connection": "close",
82 | "Cache-Control": "no-cache"
83 | };
84 |
85 | try {
86 | // Log request details
87 | const logEntry = `Timestamp: ${new Date().toISOString()}\nMethod: ${method}\nURL: ${url}\nParams: ${JSON.stringify(params)}\nData: ${JSON.stringify(data)}\nHeaders: ${JSON.stringify(headers)}\n\n`;
88 | fs.appendFileSync("api_debug.log", logEntry);
89 |
90 | // Configure axios request options
91 | const requestConfig: any = {
92 | method,
93 | url,
94 | headers,
95 | data,
96 | params,
97 | maxRedirects: 0,
98 | timeout: 20000,
99 | decompress: false,
100 | validateStatus: function (status: number) {
101 | return status < 500; // Don't reject if status code is less than 500
102 | }
103 | };
104 |
105 | // Ensure proper data encoding for all requests
106 | if (data) {
107 | requestConfig.data = JSON.stringify(data);
108 | }
109 |
110 | // Add transform request for properly handling all requests
111 | requestConfig.transformRequest = [(data: any, headers: any) => {
112 | // Force proper content type
113 | headers['Content-Type'] = 'application/json; charset=utf-8';
114 | return typeof data === 'string' ? data : JSON.stringify(data);
115 | }];
116 |
117 | // Add specific URL handling for individual task endpoints
118 | if (endpoint.startsWith('/tasks/') && method === 'GET') {
119 | // Fix to retrieve individual task by adding specific query parameters
120 | requestConfig.params = { ...params, id: endpoint.split('/')[2] };
121 | }
122 |
123 | const response = await axios(requestConfig);
124 |
125 | // Check for HTTP error status codes we didn't automatically reject
126 | if (response.status >= 400 && response.status < 500) {
127 | logError(`HTTP error ${response.status} from API`, response.data);
128 |
129 | // Enhanced error logging
130 | const errorLogEntry = `Timestamp: ${new Date().toISOString()}\nError: HTTP ${response.status}\nURL: ${url}\nMethod: ${method}\nResponse: ${JSON.stringify(response.data)}\n\n`;
131 | fs.appendFileSync("api_error.log", errorLogEntry);
132 |
133 | throw new Error(`API Error (${response.status}): ${JSON.stringify(response.data)}`);
134 | }
135 |
136 | // Check if response has expected format
137 | if ((method === "POST" && endpoint === "/tasks/list") || (method === "GET" && endpoint === "/tasks")) {
138 | logDebug(`listTasks response`, response.data.tasks || []);
139 | if (!response.data || !response.data.tasks || response.data.tasks.length === 0) {
140 | logDebug("API returned empty tasks array");
141 | }
142 | }
143 |
144 | return response.data;
145 | } catch (error: any) {
146 | logError(`API Error: ${error.message}`);
147 |
148 | // Enhanced error logging with more details
149 | const errorDetails = error.response
150 | ? `Status: ${error.response.status}, Data: ${JSON.stringify(error.response.data || 'No response data')}`
151 | : (error.request ? 'No response received' : error.message);
152 |
153 | const errorLogEntry = `Timestamp: ${new Date().toISOString()}\nError: ${error.message}\nDetails: ${errorDetails}\nURL: ${url}\nMethod: ${method}\n\n`;
154 | fs.appendFileSync("api_error.log", errorLogEntry);
155 |
156 | if (error.response) {
157 | throw new Error(
158 | `API Error (${error.response.status}): ${JSON.stringify(error.response.data || 'No response data')}`,
159 | );
160 | } else if (error.request) {
161 | throw new Error(`API Request Error: No response received (possible network issue)`);
162 | }
163 | throw error;
164 | }
165 | }
166 |
167 | // Create an Express app
168 | const app = express();
169 |
170 | // Configure middleware
171 | app.use(cors());
172 | app.use(express.json());
173 | app.use(express.static('public'));
174 |
175 | // Store active transports for message routing
176 | const activeTransports = new Map<string, SSEServerTransport>();
177 |
178 | // Create an MCP server
179 | const server = new McpServer({
180 | name: "Task Management API Server",
181 | version: "1.0.0",
182 | description: "Task Management API that provides CRUD operations for tasks with categories, priorities, and statuses",
183 | });
184 |
185 | // Add resources and tools similar to index.ts
186 | // Resource: Tasks list
187 | server.resource(
188 | "tasks",
189 | new ResourceTemplate("tasks://list", { list: undefined }),
190 | async (uri: any) => {
191 | try {
192 | const tasks = await makeApiRequest("POST", "/tasks/list");
193 |
194 | // Validate the tasks structure
195 | if (!tasks || !tasks.tasks || !Array.isArray(tasks.tasks)) {
196 | logError(`Invalid tasks data structure`, tasks);
197 | return {
198 | contents: [{
199 | uri: "tasks://error",
200 | text: `Error: Received invalid task data from API`,
201 | metadata: { error: "Invalid data structure", data: tasks }
202 | }]
203 | };
204 | }
205 |
206 | // Format tasks for easy display and use
207 | return {
208 | contents: tasks.tasks.map((task: any) => ({
209 | uri: `tasks://task/${task.id}`,
210 | text: `ID: ${task.id}
211 | Task: ${task.task || 'No description'}
212 | Category: ${task.category || 'Uncategorized'}
213 | Priority: ${task.priority || 'medium'}
214 | Status: ${task.status || 'not_started'}
215 | Created: ${task.create_time || 'unknown'}`,
216 | metadata: {
217 | id: task.id,
218 | task: task.task || 'No description',
219 | category: task.category,
220 | priority: task.priority || 'medium',
221 | status: task.status || 'not_started',
222 | create_time: task.create_time,
223 | },
224 | })),
225 | };
226 | } catch (error: any) {
227 | logError(`Error fetching tasks: ${error.message}`);
228 | return {
229 | contents: [{
230 | uri: "tasks://error",
231 | text: `Error retrieving tasks: ${error.message}`,
232 | metadata: { error: error.message }
233 | }]
234 | };
235 | }
236 | }
237 | );
238 |
239 | // Resource: Individual task
240 | server.resource(
241 | "task",
242 | new ResourceTemplate("tasks://task/{taskId}", { list: undefined }),
243 | async (uri: any, params: any) => {
244 | try {
245 | const taskId = params.taskId;
246 | // Try direct task endpoint first
247 | let task;
248 | try {
249 | const taskResult = await makeApiRequest("GET", `/tasks/${taskId}`);
250 | if (taskResult && (taskResult.id || taskResult.task)) {
251 | task = taskResult;
252 | }
253 | } catch (directError) {
254 | logDebug(`Direct task fetch failed, using task list fallback: ${directError}`);
255 | // Fallback to getting all tasks and filtering
256 | const tasks = await makeApiRequest("POST", "/tasks/list");
257 | task = tasks.tasks.find((t: any) => t.id === Number(taskId) || t.id === taskId);
258 | }
259 |
260 | if (!task) {
261 | return {
262 | contents: [{
263 | uri: uri.href,
264 | text: `Task with ID ${taskId} not found`,
265 | metadata: { error: "Task not found" }
266 | }]
267 | };
268 | }
269 |
270 | // Format task for easy display
271 | return {
272 | contents: [
273 | {
274 | uri: uri.href,
275 | text: `ID: ${task.id}
276 | Task: ${task.task}
277 | Category: ${task.category}
278 | Priority: ${task.priority}
279 | Status: ${task.status}
280 | Created: ${task.create_time}`,
281 | metadata: task,
282 | },
283 | ],
284 | };
285 | } catch (error: any) {
286 | return {
287 | contents: [{
288 | uri: uri.href,
289 | text: `Error retrieving task ${params.taskId}: ${error.message}`,
290 | metadata: { error: error.message }
291 | }]
292 | };
293 | }
294 | }
295 | );
296 |
297 | // Tool: List Tasks
298 | server.tool(
299 | "listTasks",
300 | {
301 | status: z.enum(["not_started", "started", "done"]).optional()
302 | .describe("Filter tasks by status (optional)"),
303 | priority: z.enum(["low", "medium", "high"]).optional()
304 | .describe("Filter tasks by priority level (optional)")
305 | },
306 | async ({ status, priority }: { status?: string, priority?: string }) => {
307 | try {
308 | const params: any = {};
309 | if (status) params.status = status;
310 | if (priority) params.priority = priority;
311 |
312 | const tasksResponse = await makeApiRequest("POST", "/tasks/list", { status, priority });
313 |
314 | // More flexible validation for tasks data structure
315 | let tasks: any[] = [];
316 |
317 | // Handle various response formats that might come from the API
318 | if (tasksResponse) {
319 | if (Array.isArray(tasksResponse.tasks)) {
320 | // Standard format: { tasks: [...] }
321 | tasks = tasksResponse.tasks;
322 | logDebug("Found tasks array in standard format");
323 | } else if (Array.isArray(tasksResponse)) {
324 | // Direct array format: [...]
325 | tasks = tasksResponse;
326 | logDebug("Found tasks in direct array format");
327 | } else if (typeof tasksResponse === 'object' && tasksResponse !== null) {
328 | // Try to extract tasks from any available property
329 | const possibleTasksProperties = Object.entries(tasksResponse)
330 | .filter(([_, value]) => Array.isArray(value))
331 | .map(([key, value]) => ({ key, value }));
332 |
333 | if (possibleTasksProperties.length > 0) {
334 | // Use the first array property as tasks
335 | const tasksProp = possibleTasksProperties[0];
336 | tasks = tasksProp.value as any[];
337 | logDebug(`Found tasks array in property: ${tasksProp.key}`);
338 | } else {
339 | logError(`No tasks array found in response`, tasksResponse);
340 | }
341 | }
342 | }
343 |
344 | // If we still couldn't find tasks, log error and return empty array
345 | if (tasks.length === 0) {
346 | logError(`Invalid or empty tasks data structure`, tasksResponse);
347 | }
348 |
349 | // Format response in a way that's useful for AI to parse
350 | const formattedTasks = tasks.map(task => ({
351 | id: task.id,
352 | task: task.task || "No description",
353 | category: task.category,
354 | priority: task.priority || "medium",
355 | status: task.status || "not_started",
356 | createTime: task.create_time || task.created_at || task.createTime || new Date().toISOString()
357 | }));
358 |
359 | // Log the formatted response for debugging
360 | logDebug(`listTasks formatted response`, formattedTasks);
361 |
362 | return {
363 | content: [
364 | {
365 | type: "text",
366 | text: `Found ${tasks.length} tasks${status ? ` with status '${status}'` : ''}${priority ? ` and priority '${priority}'` : ''}.`
367 | },
368 | {
369 | type: "text",
370 | text: JSON.stringify(formattedTasks, null, 2)
371 | }
372 | ]
373 | };
374 | } catch (error: any) {
375 | return {
376 | content: [
377 | {
378 | type: "text",
379 | text: `Error listing tasks: ${error.message}`
380 | }
381 | ]
382 | };
383 | }
384 | }
385 | );
386 |
387 | // Tool: Create Task
388 | server.tool(
389 | "createTask",
390 | {
391 | task: z.string().min(1, "Task description is required")
392 | .describe("The task description or title"),
393 | category: z.string().min(1, "Category is required")
394 | .describe("Task category (e.g., 'Development', 'Documentation')"),
395 | priority: z.enum(["low", "medium", "high"]).optional()
396 | .describe("Task priority level (defaults to 'medium' if not specified)"),
397 | status: z.enum(["not_started", "started", "done"]).optional()
398 | .describe("Initial task status (defaults to 'not_started' if not specified)")
399 | },
400 | async ({ task, category, priority, status }: {
401 | task: string;
402 | category: string;
403 | priority?: string;
404 | status?: string
405 | }) => {
406 | try {
407 | const requestBody: any = {
408 | task,
409 | category,
410 | };
411 |
412 | if (priority) requestBody.priority = priority;
413 | if (status) requestBody.status = status;
414 |
415 | const newTask = await makeApiRequest("POST", "/tasks", requestBody);
416 |
417 | logDebug(`Created new task with ID ${newTask.id}`);
418 |
419 | return {
420 | content: [
421 | {
422 | type: "text",
423 | text: `Task created successfully with ID: ${newTask.id}`
424 | },
425 | {
426 | type: "text",
427 | text: JSON.stringify({
428 | id: newTask.id,
429 | task: newTask.task || task,
430 | category: newTask.category || category,
431 | priority: newTask.priority || priority || "medium",
432 | status: newTask.status || status || "not_started",
433 | create_time: newTask.create_time || new Date().toISOString()
434 | }, null, 2)
435 | }
436 | ]
437 | };
438 | } catch (error: any) {
439 | logError(`Error in createTask: ${error.message}`);
440 |
441 | return {
442 | content: [
443 | {
444 | type: "text",
445 | text: `Error creating task: ${error.message}`
446 | }
447 | ]
448 | };
449 | }
450 | }
451 | );
452 |
453 | // Tool: Update Task
454 | server.tool(
455 | "updateTask",
456 | {
457 | taskId: z.number().int().positive("Task ID must be a positive integer")
458 | .describe("The unique ID of the task to update"),
459 | task: z.string().optional()
460 | .describe("New task description/title (if you want to change it)"),
461 | category: z.string().optional()
462 | .describe("New task category (if you want to change it)"),
463 | priority: z.enum(["low", "medium", "high"]).optional()
464 | .describe("New task priority (if you want to change it)"),
465 | status: z.enum(["not_started", "started", "done"]).optional()
466 | .describe("New task status (if you want to change it)")
467 | },
468 | async ({ taskId, task, category, priority, status }: {
469 | taskId: number;
470 | task?: string;
471 | category?: string;
472 | priority?: string;
473 | status?: string;
474 | }) => {
475 | try {
476 | const requestBody: any = {};
477 |
478 | if (task) requestBody.task = task;
479 | if (category) requestBody.category = category;
480 | if (priority) requestBody.priority = priority;
481 | if (status) requestBody.status = status;
482 |
483 | if (Object.keys(requestBody).length === 0) {
484 | return {
485 | content: [
486 | {
487 | type: "text",
488 | text: "No updates provided. Task remains unchanged."
489 | }
490 | ]
491 | };
492 | }
493 |
494 | const updatedTask = await makeApiRequest(
495 | "PATCH",
496 | `/tasks/${taskId}`,
497 | requestBody
498 | );
499 |
500 | return {
501 | content: [
502 | {
503 | type: "text",
504 | text: `Task ${taskId} updated successfully.`
505 | },
506 | {
507 | type: "text",
508 | text: JSON.stringify({
509 | id: updatedTask.id,
510 | task: updatedTask.task,
511 | category: updatedTask.category,
512 | priority: updatedTask.priority,
513 | status: updatedTask.status,
514 | created: updatedTask.create_time
515 | }, null, 2)
516 | }
517 | ]
518 | };
519 | } catch (error: any) {
520 | return {
521 | content: [
522 | {
523 | type: "text",
524 | text: `Error updating task: ${error.message}`
525 | }
526 | ]
527 | };
528 | }
529 | }
530 | );
531 |
532 | // Tool: Delete Task
533 | server.tool(
534 | "deleteTask",
535 | {
536 | taskId: z.number().int().positive("Task ID must be a positive integer")
537 | .describe("The unique ID of the task to delete")
538 | },
539 | async ({ taskId }: { taskId: number }) => {
540 | try {
541 | const response = await makeApiRequest("DELETE", `/tasks/${taskId}`);
542 |
543 | logDebug(`Deleted task ID ${taskId}`);
544 |
545 | return {
546 | content: [
547 | {
548 | type: "text",
549 | text: response.message || `Task ${taskId} deleted successfully.`
550 | }
551 | ]
552 | };
553 | } catch (error: any) {
554 | logError(`Error in deleteTask: ${error.message}`);
555 |
556 | return {
557 | content: [
558 | {
559 | type: "text",
560 | text: `Error deleting task: ${error.message}`
561 | }
562 | ]
563 | };
564 | }
565 | }
566 | );
567 |
568 | // Prompts (same as index.ts)
569 | server.prompt(
570 | "listAllTasks",
571 | {},
572 | () => ({
573 | messages: [
574 | {
575 | role: "user",
576 | content: {
577 | type: "text",
578 | text: "Please list all tasks in my task management system. Group them by category and summarize the priorities for each category."
579 | }
580 | }
581 | ]
582 | })
583 | );
584 |
585 | server.prompt(
586 | "createTaskNaturalLanguage",
587 | {
588 | description: z.string().min(10, "Task description must be at least 10 characters")
589 | .describe("A natural language description of the task to create")
590 | },
591 | ({ description }: { description: string }) => ({
592 | messages: [
593 | {
594 | role: "user",
595 | content: {
596 | type: "text",
597 | text: `Please analyze this task description and create an appropriate task:
598 |
599 | "${description}"
600 |
601 | Extract the most suitable category, determine an appropriate priority level, and create the task with the right parameters.`
602 | }
603 | }
604 | ]
605 | })
606 | );
607 |
608 | server.prompt(
609 | "createNewTask",
610 | {
611 | task: z.string().min(1, "Task description is required")
612 | .describe("The task description or title"),
613 | category: z.string().min(1, "Category is required")
614 | .describe("Task category"),
615 | priority: z.enum(["low", "medium", "high"]).optional()
616 | .describe("Task priority level")
617 | },
618 | ({ task, category, priority }: { task: string; category: string; priority?: string }) => ({
619 | messages: [
620 | {
621 | role: "user",
622 | content: {
623 | type: "text",
624 | text: `Please create a new task in my task management system with the following details:
625 |
626 | Task: ${task}
627 | Category: ${category}
628 | ${priority ? `Priority: ${priority}` : ""}
629 |
630 | Please confirm once the task is created and provide the task ID for reference.`
631 | }
632 | }
633 | ]
634 | })
635 | );
636 |
637 | server.prompt(
638 | "taskProgressReport",
639 | {
640 | status: z.enum(["not_started", "started", "done"]).optional()
641 | .describe("Filter by task status")
642 | },
643 | ({ status }: { status?: string }) => ({
644 | messages: [
645 | {
646 | role: "user",
647 | content: {
648 | type: "text",
649 | text: `Please provide a progress report on ${status ? `all ${status} tasks` : "all tasks"}.
650 |
651 | Include:
652 | 1. How many tasks are in each status category
653 | 2. Which high priority tasks need attention
654 | 3. Any categories with a high concentration of incomplete tasks`
655 | }
656 | }
657 | ]
658 | })
659 | );
660 |
661 | // SSE endpoint
662 | app.get('/sse', async (req, res) => {
663 | const connectionId = Date.now().toString();
664 |
665 | // Set SSE headers
666 | res.writeHead(200, {
667 | 'Content-Type': 'text/event-stream',
668 | 'Cache-Control': 'no-cache',
669 | 'Connection': 'keep-alive',
670 | });
671 |
672 | // Send initial message
673 | res.write(`data: ${JSON.stringify({ type: 'connected', id: connectionId })}\n\n`);
674 |
675 | // Create and store transport
676 | const transport = new SSEServerTransport('/messages', res);
677 | activeTransports.set(connectionId, transport);
678 |
679 | // Connect the server to this transport instance
680 | await server.connect(transport);
681 |
682 | // Handle client disconnection
683 | req.on('close', () => {
684 | logDebug(`Client disconnected: ${connectionId}`);
685 | transport.close();
686 | activeTransports.delete(connectionId);
687 | });
688 | });
689 |
690 | // Messages endpoint for client-to-server communication
691 | app.post('/messages', express.json(), (req, res, next) => {
692 | const connectionId = req.headers['x-connection-id'] as string;
693 |
694 | if (!connectionId || !activeTransports.has(connectionId)) {
695 | logError('Invalid or missing connection ID', { connectionId });
696 | res.status(400).json({ error: 'Invalid or missing connection ID' });
697 | return;
698 | }
699 |
700 | const transport = activeTransports.get(connectionId);
701 | if (!transport) {
702 | logError('Transport not found', { connectionId });
703 | res.status(404).json({ error: 'Transport not found' });
704 | return;
705 | }
706 |
707 | // Handle the message and catch any errors
708 | transport.handlePostMessage(req as any, res as any, req.body)
709 | .then(() => {
710 | if (!res.headersSent) {
711 | res.status(200).end();
712 | }
713 | })
714 | .catch((error: any) => {
715 | logError('Error handling message', { error: error.message, connectionId });
716 | if (!res.headersSent) {
717 | res.status(500).json({ error: error.message });
718 | }
719 | next(error);
720 | });
721 | });
722 |
723 | // Create a simple HTML page for interacting with the server
724 | app.get('/', (req, res) => {
725 | res.send(`
726 | <!DOCTYPE html>
727 | <html lang="en">
728 | <head>
729 | <meta charset="UTF-8">
730 | <meta name="viewport" content="width=device-width, initial-scale=1.0">
731 | <title>MCP Task Manager</title>
732 | <style>
733 | body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
734 | h1 { color: #333; }
735 | pre { background: #f4f4f4; padding: 10px; border-radius: 5px; overflow-x: auto; }
736 | button { margin: 5px; padding: 8px 16px; background: #4CAF50; color: white; border: none;
737 | border-radius: 4px; cursor: pointer; }
738 | button:hover { background: #45a049; }
739 | #output { margin-top: 20px; }
740 | </style>
741 | </head>
742 | <body>
743 | <h1>Task Manager MCP Server</h1>
744 | <p>This is an HTTP + SSE implementation of the Task Manager MCP Server.</p>
745 |
746 | <div>
747 | <button id="connect">Connect</button>
748 | <button id="discover" disabled>Discover</button>
749 | <button id="list-tasks" disabled>List Tasks</button>
750 | <button id="create-task" disabled>Create Test Task</button>
751 | </div>
752 |
753 | <pre id="output">Click 'Connect' to start...</pre>
754 |
755 | <script>
756 | const output = document.getElementById('output');
757 | const connectBtn = document.getElementById('connect');
758 | const discoverBtn = document.getElementById('discover');
759 | const listTasksBtn = document.getElementById('list-tasks');
760 | const createTaskBtn = document.getElementById('create-task');
761 |
762 | let connectionId = null;
763 | let eventSource = null;
764 | let messageId = 0;
765 |
766 | function log(message) {
767 | output.textContent += message + '\\n';
768 | output.scrollTop = output.scrollHeight;
769 | }
770 |
771 | connectBtn.addEventListener('click', () => {
772 | log('Connecting to server...');
773 |
774 | eventSource = new EventSource('/sse');
775 |
776 | eventSource.onopen = () => {
777 | log('SSE connection established');
778 | };
779 |
780 | eventSource.onmessage = (event) => {
781 | const data = JSON.parse(event.data);
782 | log('Received: ' + JSON.stringify(data, null, 2));
783 |
784 | if (data.type === 'connected') {
785 | connectionId = data.id;
786 | discoverBtn.disabled = false;
787 | listTasksBtn.disabled = false;
788 | createTaskBtn.disabled = false;
789 | connectBtn.disabled = true;
790 | log('Connected with ID: ' + connectionId);
791 | }
792 | };
793 |
794 | eventSource.onerror = (error) => {
795 | log('SSE Error: ' + JSON.stringify(error));
796 | };
797 | });
798 |
799 | async function sendMessage(message) {
800 | try {
801 | const response = await fetch('/messages', {
802 | method: 'POST',
803 | headers: {
804 | 'Content-Type': 'application/json',
805 | 'X-Connection-ID': connectionId
806 | },
807 | body: JSON.stringify(message)
808 | });
809 |
810 | if (!response.ok) {
811 | const errorText = await response.text();
812 | throw new Error(\`HTTP error \${response.status}: \${errorText}\`);
813 | }
814 | } catch (error) {
815 | log('Error sending message: ' + error.message);
816 | }
817 | }
818 |
819 | discoverBtn.addEventListener('click', () => {
820 | const message = {
821 | id: 'discover_' + (++messageId),
822 | type: 'discover'
823 | };
824 |
825 | log('Sending discover request...');
826 | sendMessage(message);
827 | });
828 |
829 | listTasksBtn.addEventListener('click', () => {
830 | const message = {
831 | id: 'invoke_' + (++messageId),
832 | type: 'invoke',
833 | tool: 'listTasks',
834 | parameters: {}
835 | };
836 |
837 | log('Sending listTasks request...');
838 | sendMessage(message);
839 | });
840 |
841 | createTaskBtn.addEventListener('click', () => {
842 | const message = {
843 | id: 'invoke_' + (++messageId),
844 | type: 'invoke',
845 | tool: 'createTask',
846 | parameters: {
847 | task: 'Test task created at ' + new Date().toISOString(),
848 | category: 'Test',
849 | priority: 'medium'
850 | }
851 | };
852 |
853 | log('Sending createTask request...');
854 | sendMessage(message);
855 | });
856 | </script>
857 | </body>
858 | </html>
859 | `);
860 | });
861 |
862 | // Start the server
863 | app.listen(PORT, () => {
864 | logDebug(`MCP HTTP Server with SSE is running on http://localhost:${PORT}`);
865 | console.log(`MCP HTTP Server with SSE is running on http://localhost:${PORT}`);
866 | });
```