# Directory Structure
```
├── .env-template
├── .gitignore
├── HOWTO.md
├── package.json
├── README.md
├── src
│ ├── http-server.ts
│ ├── index.ts
│ └── test-client.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Environment variables
.env
.env.local
.env.development
.env.test
.env.production
# Dependencies
node_modules/
package-lock.json
yarn.lock
pnpm-lock.yaml
.cursor
CLAUDE.md
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
api_debug.log
api_error.log
server_debug.txt
server_error.txt
# Testing
test-reports/
coverage/
# Production build
dist/
build/
# Misc
.DS_Store
.idea/
.vscode/
*.swp
*.swo
```
--------------------------------------------------------------------------------
/.env-template:
--------------------------------------------------------------------------------
```
# Task API Server - Environment Variables Template
# Copy this file to .env and fill in your own values
# API Configuration (Required)
# ---------------------------
# URL for the Task API server
TASK_MANAGER_API_BASE_URL=https://taskmanager.mcpai.io/api
# API key for authentication
TASK_MANAGER_API_KEY=your_api_key_here
# Server Configuration (Optional)
# ------------------------------
# Port for the HTTP server (defaults to 3000 if not specified)
TASK_MANAGER_HTTP_PORT=3500
# Alternative port name - takes precedence over TASK_MANAGER_HTTP_PORT if set
# PORT=3000
# Logging Configuration (Optional)
# ------------------------------
# Enable (1) or disable (0) debug logging (defaults to 0 if not specified)
TASK_MANAGER_DEBUG=0
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Task API Server - MCP TypeScript Implementation
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.
## Overview
This MCP server connects to an external Task API service and provides a standardized interface for task management. It supports two runtime modes:
1. **STDIO Mode**: Standard input/output communication for CLI-based applications and AI agents
2. **HTTP+SSE Mode**: Web-accessible server with Server-Sent Events for browser and HTTP-based clients
The server offers a complete set of task management operations, extensive validation, and robust error handling.
## Features
- **Task Management Operations**:
- List existing tasks with filtering capabilities
- Create new tasks with customizable properties
- Update task details (description, status, category, priority)
- Delete tasks when completed or no longer needed
- **Dual Interface Modes**:
- STDIO protocol support for command-line and AI agent integration
- HTTP+SSE protocol with web interface for browser-based access
- **MCP Protocol Implementation**:
- Complete implementation of the Model Context Protocol
- Resources for task data structures
- Tools for task operations
- Error handling and informative messages
- **Quality Assurance**:
- Comprehensive test client for validation
- Automatic server shutdown after tests complete
- Detailed validation of API responses
## Getting Started
### Prerequisites
- Node.js 16.x or higher
- npm or pnpm package manager
### Installation
1. Clone the repository:
```
git clone https://github.com/yourusername/mcp-template-ts.git
cd mcp-template-ts
```
2. Install dependencies:
```
npm install
```
or using pnpm:
```
pnpm install
```
3. Create an `.env` file with your Task API credentials:
```
TASK_MANAGER_API_BASE_URL=https://your-task-api-url.com/api
TASK_MANAGER_API_KEY=your_api_key_here
TASK_MANAGER_HTTP_PORT=3000
```
4. Build the project:
```
npm run build
```
### Running the Server
#### STDIO Mode (for CLI/AI integration)
```
npm start
```
or
```
node dist/index.js
```
#### HTTP Mode (for web access)
```
npm run start:http
```
or
```
node dist/http-server.js
```
By default, the HTTP server runs on port 3000. You can change this by setting the `TASK_MANAGER_HTTP_PORT` environment variable.
### Testing
Run the comprehensive test suite to verify functionality:
```
npm test
```
This will:
1. Build the project
2. Start a server instance
3. Connect a test client to the server
4. Run through all task operations
5. Verify correct responses
6. Automatically shut down the server
## Using the MCP Client
### STDIO Client
To connect to the STDIO server from your application:
```typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import * as path from 'path';
// Create transport
const transport = new StdioClientTransport({
command: 'node',
args: [path.resolve('path/to/dist/index.js')]
});
// Initialize client
const client = new Client(
{
name: "your-client-name",
version: "1.0.0"
},
{
capabilities: {
prompts: {},
resources: {},
tools: {}
}
}
);
// Connect to server
await client.connect(transport);
// Example: List all tasks
const listTasksResult = await client.callTool({
name: "listTasks",
arguments: {}
});
// Example: Create a new task
const createTaskResult = await client.callTool({
name: "createTask",
arguments: {
task: "Complete project documentation",
category: "Documentation",
priority: "high"
}
});
// Clean up when done
await client.close();
```
### HTTP Client
To connect to the HTTP server from a browser:
```html
<!DOCTYPE html>
<html>
<head>
<title>Task Manager</title>
<script type="module">
import { Client } from 'https://cdn.jsdelivr.net/npm/@modelcontextprotocol/sdk/dist/esm/client/index.js';
import { SSEClientTransport } from 'https://cdn.jsdelivr.net/npm/@modelcontextprotocol/sdk/dist/esm/client/sse.js';
document.addEventListener('DOMContentLoaded', async () => {
// Create transport
const transport = new SSEClientTransport('http://localhost:3000/mcp');
// Initialize client
const client = new Client(
{
name: "browser-client",
version: "1.0.0"
},
{
capabilities: {
prompts: {},
resources: {},
tools: {}
}
}
);
// Connect to server
await client.connect(transport);
// Now you can use client.callTool() for tasks
});
</script>
</head>
<body>
<h1>Task Manager</h1>
<!-- Your interface elements here -->
</body>
</html>
```
## Available Tools
### listTasks
Lists all available tasks.
```typescript
const result = await client.callTool({
name: "listTasks",
arguments: {
// Optional filters
status: "pending", // Filter by status
category: "Work", // Filter by category
priority: "high" // Filter by priority
}
});
```
### createTask
Creates a new task.
```typescript
const result = await client.callTool({
name: "createTask",
arguments: {
task: "Complete the project report", // Required: task description
category: "Work", // Optional: task category
priority: "high" // Optional: low, medium, high
}
});
```
### updateTask
Updates an existing task.
```typescript
const result = await client.callTool({
name: "updateTask",
arguments: {
taskId: 123, // Required: ID of task to update
task: "Updated task description", // Optional: new description
status: "done", // Optional: pending, started, done
category: "Personal", // Optional: new category
priority: "medium" // Optional: low, medium, high
}
});
```
### deleteTask
Deletes a task.
```typescript
const result = await client.callTool({
name: "deleteTask",
arguments: {
taskId: 123 // Required: ID of task to delete
}
});
```
## Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| TASK_MANAGER_API_BASE_URL | URL for the external Task API | None (Required) |
| TASK_MANAGER_API_KEY | API key for authentication | None (Required) |
| TASK_MANAGER_HTTP_PORT | Port for the HTTP server | 3000 |
| PORT | Alternative port name (takes precedence) | None |
## Project Structure
```
mcp-template-ts/
├── dist/ # Compiled JavaScript files
├── src/ # TypeScript source files
│ ├── index.ts # STDIO server entry point
│ ├── http-server.ts # HTTP+SSE server entry point
│ ├── test-client.ts # Test client implementation
├── .env # Environment variables
├── package.json # Project dependencies
├── tsconfig.json # TypeScript configuration
└── README.md # Project documentation
```
## Development
1. Start the TypeScript compiler in watch mode:
```
npm run watch
```
2. Run tests to verify changes:
```
npm test
```
## License
This project is licensed under the MIT License - see the LICENSE file for details.
## Acknowledgments
- This project uses the [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/sdk) for MCP protocol implementation
- Built for integration with AI tooling and web applications
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "Task API Server",
"version": "1.0.0",
"description": "Task API Server TypeScript",
"license": "MIT",
"author": "MCPAI.io",
"type": "module",
"bin": {
"mcp-template-ts": "dist/index.js"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc && shx chmod +x dist/*.js",
"prepare": "npm run build",
"watch": "tsc --watch",
"test": "tsc && shx chmod +x dist/*.js && node dist/test-client.js",
"start": "node dist/index.js",
"start:http": "node dist/http-server.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.6.0",
"axios": "^1.8.1",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^5.0.1",
"zod": "^3.24.2"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/node": "^22.13.9",
"shx": "^0.3.4",
"typescript": "^5.7.3"
}
}
```
--------------------------------------------------------------------------------
/HOWTO.md:
--------------------------------------------------------------------------------
```markdown
# How to Use the MCP Task Manager API
This guide will walk you through installing, configuring, testing, and running the Model Context Protocol (MCP) Task Manager API.
## Table of Contents
1. [Overview](#overview)
2. [Installation](#installation)
3. [Configuration](#configuration)
4. [Running the Server](#running-the-server)
5. [Testing](#testing)
6. [Using the Client](#using-the-client)
7. [API Reference](#api-reference)
8. [Troubleshooting](#troubleshooting)
## Overview
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.
Key features:
- List, create, update, and delete tasks
- Filter tasks by status and priority
- Natural language task creation
- Task progress reporting
## Installation
### Prerequisites
- Node.js 16 or higher
- npm or pnpm
### Steps
1. Clone the repository:
```bash
git clone <repository-url>
cd mcp-template-ts
```
2. Install dependencies:
```bash
npm install
# or with pnpm
pnpm install
```
3. Build the project:
```bash
npm run build
# or
pnpm run build
```
## Configuration
Create a `.env` file in the project root with the following variables:
```
TASK_MANAGER_API_BASE_URL=https://your-task-api-url.com/api
TASK_MANAGER_API_KEY=your_api_key
```
Configuration notes:
- `TASK_MANAGER_API_BASE_URL` - The URL for your Task API server (default: "https://task-master-pro-mikaelwestoo.replit.app/api")
- `TASK_MANAGER_API_KEY` - Your API key for authentication (required)
## Running the Server
Run the MCP server to make it available to clients:
```bash
node dist/index.js
```
The server will start and listen for MCP commands on stdin/stdout.
To keep the server running in watch mode during development:
```bash
npm run watch
```
## Testing
Run the automated tests to verify the server is working correctly:
```bash
npm test
```
This will:
1. Start an MCP server instance
2. Connect a test client
3. Test all available tools (list, create, update, delete tasks)
4. Check resource availability
5. Report test results
The test client uses the MCP SDK to communicate with the server, simulating how an AI assistant would interact with it.
## Using the Client
You can build your own client or use the provided example client:
1. Build the client (if you've modified it):
```bash
npm run build
```
2. Run the client:
```bash
node dist/client.js
```
### Client Integration
To integrate with your own application, use the MCP SDK client:
```typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
// Create a client transport
const transport = new StdioClientTransport({
command: "node",
args: ["./dist/index.js"]
});
// Initialize client
const client = new Client(
{
name: "your-client-name",
version: "1.0.0"
},
{
capabilities: {
prompts: {},
resources: {},
tools: {}
}
}
);
// Connect to the server
await client.connect(transport);
// Use the client
const tasks = await client.callTool({
name: "listTasks",
arguments: {}
});
console.log(tasks.content[0].text);
```
## API Reference
### Tools
#### listTasks
Lists all tasks, optionally filtered by status or priority.
Parameters:
- `status` (optional): "not_started", "started", or "done"
- `priority` (optional): "low", "medium", or "high"
#### createTask
Creates a new task.
Parameters:
- `task` (required): Task description/title
- `category` (required): Task category
- `priority` (optional): "low", "medium", or "high"
- `status` (optional): "not_started", "started", or "done"
#### updateTask
Updates an existing task.
Parameters:
- `taskId` (required): ID of the task to update
- `task` (optional): New task description
- `category` (optional): New task category
- `priority` (optional): New task priority
- `status` (optional): New task status
#### deleteTask
Deletes a task.
Parameters:
- `taskId` (required): ID of the task to delete
### Prompts
#### listAllTasks
Lists all tasks grouped by category with priority summaries.
#### createTaskNaturalLanguage
Creates a task from a natural language description.
Parameters:
- `description`: Natural language description of the task
#### createNewTask
Creates a task with specific parameters.
Parameters:
- `task`: Task description
- `category`: Task category
- `priority` (optional): Task priority
#### taskProgressReport
Generates a progress report on tasks.
Parameters:
- `status` (optional): Filter by task status
### Resources
- `tasks://list`: List of all tasks
- `tasks://task/{taskId}`: Details of a specific task
## Troubleshooting
### Common Issues
1. **API Key Authentication Failed**
- Ensure you've set the correct API key in the `.env` file
- Check if the API key has the necessary permissions
2. **Cannot Connect to Task API**
- Verify the API base URL is correct in your `.env` file
- Check your network connection
- Look for error details in the api_error.log file
3. **TypeScript Build Errors**
- Run `npm install` to ensure all dependencies are installed
- Check that you're using Node.js 16+
4. **Test Client Errors**
- Check that the server is running on the expected path
- Verify the MCP SDK version is compatible with your code
For more detailed debugging, check the logs in:
- `api_debug.log`: Detailed API request logging
- `api_error.log`: API error details
```
--------------------------------------------------------------------------------
/src/test-client.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import * as path from 'path';
import { spawn } from 'child_process';
async function runTests() {
console.log('Starting MCP Task Manager API Tests');
// Start server in a child process
const serverProcess = spawn('node', [path.resolve('./dist/index.js')], {
stdio: ['pipe', 'pipe', 'pipe']
});
// Log server output for debugging
serverProcess.stderr.on('data', (data) => {
console.error(`Server stderr: ${data.toString().trim()}`);
});
// Declare transport outside try block so it's accessible in finally block
let transport;
let client;
try {
// Wait for server to initialize
await new Promise((resolve) => setTimeout(resolve, 2000));
console.log('Server should be running now. Setting up MCP client...');
// Create MCP client transport using the command and args
// This will spawn a new server process instead of using our existing one
transport = new StdioClientTransport({
command: 'node',
args: [path.resolve('./dist/index.js')]
});
// Initialize the MCP client
client = new Client(
{
name: "task-api-test-client",
version: "1.0.0"
},
{
capabilities: {
prompts: {},
resources: {},
tools: {}
}
}
);
// Connect to the server
await client.connect(transport);
console.log('Client connected to server');
// Get server capabilities
console.log('\nRetrieving server capabilities...');
const serverInfo = client.getServerVersion();
const capabilities = client.getServerCapabilities();
if (serverInfo) {
console.log('Server info:');
console.log(`Server name: ${serverInfo.name}`);
console.log(`Server version: ${serverInfo.version}`);
console.log('✅ Server info retrieved successfully');
} else {
console.log('❌ Failed to retrieve server info');
}
if (capabilities) {
console.log('Server capabilities:');
console.log(`Available tools: ${capabilities.tools ? 'Yes' : 'No'}`);
console.log(`Available resources: ${capabilities.resources ? 'Yes' : 'No'}`);
console.log('✅ Server capabilities retrieved successfully');
} else {
console.log('❌ Failed to retrieve server capabilities');
}
// Test the listTasks tool
console.log('\nTesting listTasks tool...');
try {
const listTasksResult = await client.callTool({
name: "listTasks",
arguments: {}
});
console.log('List tasks response received:');
if (Array.isArray(listTasksResult.content) && listTasksResult.content.length > 0) {
const firstContent = listTasksResult.content[0];
if (firstContent && 'text' in firstContent) {
console.log(firstContent.text);
console.log('✅ List tasks test passed');
} else {
console.log('❌ List tasks test failed - unexpected content format');
}
} else {
console.log('❌ List tasks test failed - empty content');
}
} catch (error: any) {
console.log(`❌ List tasks test failed with error: ${error.message}`);
}
// Test creating a task
console.log('\nTesting createTask tool...');
let createdTaskId: number | undefined;
try {
const createTaskResult = await client.callTool({
name: "createTask",
arguments: {
task: `Test task ${new Date().toISOString()}`,
category: "Test",
priority: "medium"
}
});
console.log('Create task response received:');
if (Array.isArray(createTaskResult.content) && createTaskResult.content.length > 0) {
const firstContent = createTaskResult.content[0];
if (firstContent && 'text' in firstContent) {
console.log(firstContent.text);
// Extract task ID if successful
const idMatch = firstContent.text.match(/ID: (\d+)/);
if (idMatch && idMatch[1]) {
createdTaskId = parseInt(idMatch[1], 10);
console.log(`✅ Create task test passed. Created task ID: ${createdTaskId}`);
} else {
console.log('❌ Create task test failed - could not extract task ID');
}
} else {
console.log('❌ Create task test failed - unexpected content format');
}
} else {
console.log('❌ Create task test failed - empty content');
}
} catch (error: any) {
console.log(`❌ Create task test failed with error: ${error.message}`);
}
// If we successfully created a task, test updating it with various field combinations
if (createdTaskId) {
// Test 1: Update task description
console.log('\nTesting updateTask - description change...');
try {
const newDescription = `Updated description ${new Date().toISOString()}`;
const updateDescResult = await client.callTool({
name: "updateTask",
arguments: {
taskId: createdTaskId,
task: newDescription
}
});
console.log('Update description response received:');
if (Array.isArray(updateDescResult.content) && updateDescResult.content.length > 0) {
const firstContent = updateDescResult.content[0];
if (firstContent && 'text' in firstContent) {
console.log(firstContent.text);
if (firstContent.text.includes('updated successfully')) {
console.log('✅ Update description test passed');
} else {
console.log('❌ Update description test failed - response does not indicate success');
}
} else {
console.log('❌ Update description test failed - unexpected content format');
}
} else {
console.log('❌ Update description test failed - empty content');
}
} catch (error: any) {
console.log(`❌ Update description test failed with error: ${error.message}`);
}
// Test 2: Update task status
console.log('\nTesting updateTask - status change...');
try {
const updateStatusResult = await client.callTool({
name: "updateTask",
arguments: {
taskId: createdTaskId,
status: "started"
}
});
console.log('Update status response received:');
if (Array.isArray(updateStatusResult.content) && updateStatusResult.content.length > 0) {
const firstContent = updateStatusResult.content[0];
if (firstContent && 'text' in firstContent) {
console.log(firstContent.text);
if (firstContent.text.includes('updated successfully')) {
console.log('✅ Update status test passed');
} else {
console.log('❌ Update status test failed - response does not indicate success');
}
// Verify the status was actually updated in the response
if (updateStatusResult.content.length > 1) {
const secondContent = updateStatusResult.content[1];
if (secondContent && 'text' in secondContent) {
const responseJson = JSON.parse(secondContent.text);
if (responseJson.status === "started") {
console.log('✅ Status verification passed - status is "started"');
} else {
console.log(`❌ Status verification failed - expected "started" but got "${responseJson.status}"`);
}
}
}
} else {
console.log('❌ Update status test failed - unexpected content format');
}
} else {
console.log('❌ Update status test failed - empty content');
}
} catch (error: any) {
console.log(`❌ Update status test failed with error: ${error.message}`);
}
// Test 3: Update task category
console.log('\nTesting updateTask - category change...');
try {
const newCategory = `Category-${Date.now().toString().slice(-5)}`;
const updateCategoryResult = await client.callTool({
name: "updateTask",
arguments: {
taskId: createdTaskId,
category: newCategory
}
});
console.log('Update category response received:');
if (Array.isArray(updateCategoryResult.content) && updateCategoryResult.content.length > 0) {
const firstContent = updateCategoryResult.content[0];
if (firstContent && 'text' in firstContent) {
console.log(firstContent.text);
if (firstContent.text.includes('updated successfully')) {
console.log('✅ Update category test passed');
} else {
console.log('❌ Update category test failed - response does not indicate success');
}
// Verify the category was actually updated in the response
if (updateCategoryResult.content.length > 1) {
const secondContent = updateCategoryResult.content[1];
if (secondContent && 'text' in secondContent) {
const responseJson = JSON.parse(secondContent.text);
if (responseJson.category === newCategory) {
console.log(`✅ Category verification passed - category is "${newCategory}"`);
} else {
console.log(`❌ Category verification failed - expected "${newCategory}" but got "${responseJson.category}"`);
}
}
}
} else {
console.log('❌ Update category test failed - unexpected content format');
}
} else {
console.log('❌ Update category test failed - empty content');
}
} catch (error: any) {
console.log(`❌ Update category test failed with error: ${error.message}`);
}
// Test 4: Update task priority
console.log('\nTesting updateTask - priority change...');
try {
const updatePriorityResult = await client.callTool({
name: "updateTask",
arguments: {
taskId: createdTaskId,
priority: "high"
}
});
console.log('Update priority response received:');
if (Array.isArray(updatePriorityResult.content) && updatePriorityResult.content.length > 0) {
const firstContent = updatePriorityResult.content[0];
if (firstContent && 'text' in firstContent) {
console.log(firstContent.text);
if (firstContent.text.includes('updated successfully')) {
console.log('✅ Update priority test passed');
} else {
console.log('❌ Update priority test failed - response does not indicate success');
}
// Verify the priority was actually updated in the response
if (updatePriorityResult.content.length > 1) {
const secondContent = updatePriorityResult.content[1];
if (secondContent && 'text' in secondContent) {
const responseJson = JSON.parse(secondContent.text);
if (responseJson.priority === "high") {
console.log('✅ Priority verification passed - priority is "high"');
} else {
console.log(`❌ Priority verification failed - expected "high" but got "${responseJson.priority}"`);
}
}
}
} else {
console.log('❌ Update priority test failed - unexpected content format');
}
} else {
console.log('❌ Update priority test failed - empty content');
}
} catch (error: any) {
console.log(`❌ Update priority test failed with error: ${error.message}`);
}
// Test 5: Update multiple fields at once
console.log('\nTesting updateTask - multiple fields at once...');
try {
const finalDesc = `Final description ${new Date().toISOString()}`;
const finalCategory = `Final-Category-${Date.now().toString().slice(-5)}`;
const updateMultipleResult = await client.callTool({
name: "updateTask",
arguments: {
taskId: createdTaskId,
task: finalDesc,
category: finalCategory,
priority: "medium",
status: "done"
}
});
console.log('Update multiple fields response received:');
if (Array.isArray(updateMultipleResult.content) && updateMultipleResult.content.length > 0) {
const firstContent = updateMultipleResult.content[0];
if (firstContent && 'text' in firstContent) {
console.log(firstContent.text);
if (firstContent.text.includes('updated successfully')) {
console.log('✅ Update multiple fields test passed');
} else {
console.log('❌ Update multiple fields test failed - response does not indicate success');
}
// Verify all fields were actually updated in the response
if (updateMultipleResult.content.length > 1) {
const secondContent = updateMultipleResult.content[1];
if (secondContent && 'text' in secondContent) {
const responseJson = JSON.parse(secondContent.text);
let verificationsPassed = true;
if (responseJson.task !== finalDesc) {
console.log(`❌ Description verification failed - expected "${finalDesc}" but got "${responseJson.task}"`);
verificationsPassed = false;
}
if (responseJson.category !== finalCategory) {
console.log(`❌ Category verification failed - expected "${finalCategory}" but got "${responseJson.category}"`);
verificationsPassed = false;
}
if (responseJson.priority !== "medium") {
console.log(`❌ Priority verification failed - expected "medium" but got "${responseJson.priority}"`);
verificationsPassed = false;
}
if (responseJson.status !== "done") {
console.log(`❌ Status verification failed - expected "done" but got "${responseJson.status}"`);
verificationsPassed = false;
}
if (verificationsPassed) {
console.log('✅ All field verifications passed');
}
}
}
} else {
console.log('❌ Update multiple fields test failed - unexpected content format');
}
} else {
console.log('❌ Update multiple fields test failed - empty content');
}
} catch (error: any) {
console.log(`❌ Update multiple fields test failed with error: ${error.message}`);
}
// Finally, test deleting the task
console.log('\nTesting deleteTask tool...');
try {
const deleteTaskResult = await client.callTool({
name: "deleteTask",
arguments: {
taskId: createdTaskId
}
});
console.log('Delete task response received:');
if (Array.isArray(deleteTaskResult.content) && deleteTaskResult.content.length > 0) {
const firstContent = deleteTaskResult.content[0];
if (firstContent && 'text' in firstContent) {
console.log(firstContent.text);
if (firstContent.text.includes('deleted successfully') ||
firstContent.text.includes('successfully deleted')) {
console.log('✅ Delete task test passed');
} else {
console.log('❌ Delete task test failed - response does not indicate success');
}
} else {
console.log('❌ Delete task test failed - unexpected content format');
}
} else {
console.log('❌ Delete task test failed - empty content');
}
} catch (error: any) {
console.log(`❌ Delete task test failed with error: ${error.message}`);
}
}
// Test accessing resources
console.log('\nTesting resources...');
try {
const resourcesList = await client.listResources();
if (resourcesList && 'resources' in resourcesList) {
const resources = resourcesList.resources;
console.log(`Available resources: ${resources.map(r => r.name).join(', ')}`);
if (resources.length > 0) {
console.log('✅ List resources test passed');
// Try to read a resource if any are available
const resourceURI = `tasks://${resources[0].name}`;
try {
const resourceResult = await client.readResource({ uri: resourceURI });
if (resourceResult && 'contents' in resourceResult) {
console.log(`Resource ${resources[0].name} retrieved successfully`);
console.log('✅ Read resource test passed');
} else {
console.log('❌ Read resource test failed - unexpected result format');
}
} catch (error: any) {
console.log(`❌ Read resource test failed with error: ${error.message}`);
}
} else {
console.log('ℹ️ No resources available to test');
}
} else {
console.log('❌ List resources test failed - unexpected result format');
}
} catch (error: any) {
console.log(`❌ List resources test failed with error: ${error.message}`);
}
console.log('\nTests completed');
} catch (error: any) {
console.error('Test execution error:', error);
} finally {
// Clean up - kill the server process
console.log('Terminating test server');
serverProcess.kill();
// Make sure to close the client which will terminate the second server process
if (client) {
try {
await client.close();
console.log('Test client closed');
} catch (closeError) {
console.error('Error closing client:', closeError);
}
}
// Force exit after a short delay to ensure all processes are terminated
setTimeout(() => {
console.log('Exiting test process');
process.exit(0);
}, 500);
}
}
// Run the tests
runTests().catch(console.error);
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import {
McpServer,
ResourceTemplate,
} from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import * as fs from 'fs';
import axios from 'axios';
import dotenv from 'dotenv';
// Load environment variables from .env file
dotenv.config();
// Base URL for the Task API from environment variables
const API_BASE_URL = process.env.TASK_MANAGER_API_BASE_URL || "https://task-master-pro-mikaelwestoo.replit.app/api";
// API Key from environment variables
const API_KEY = process.env.TASK_MANAGER_API_KEY;
// Helper function for logging to file
function logToFile(filename: string, message: string): void {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] ${message}\n`;
fs.appendFileSync(filename, logEntry);
}
// Helper function to log errors
function logError(message: string, details: any = null): void {
let errorMessage = `[ERROR] ${message}`;
if (details) {
errorMessage += `\nDetails: ${JSON.stringify(details, null, 2)}`;
}
logToFile("server_error.log", errorMessage);
}
// Helper function to log debug info
function logDebug(message: string, data: any = null): void {
let debugMessage = `[DEBUG] ${message}`;
if (data) {
debugMessage += `\nData: ${JSON.stringify(data, null, 2)}`;
}
logToFile("server_debug.log", debugMessage);
}
// Schema definitions
const TaskSchema = z.object({
id: z.number().int().positive().describe("Unique task identifier"),
task: z.string().describe("The task description/title"),
category: z.string().describe("Task category (e.g., 'Development', 'Documentation')"),
priority: z.enum(["low", "medium", "high"]).describe("Task priority level"),
status: z.enum(["not_started", "started", "done"]).describe("Current task status"),
create_time: z.string().describe("Task creation timestamp in ISO format"),
});
const TaskListSchema = z.object({
tasks: z.array(TaskSchema).describe("List of tasks"),
});
// Create an MCP server
const server = new McpServer({
name: "Task Management API Server",
version: "1.0.0",
description: "Task Management API that provides CRUD operations for tasks with categories, priorities, and statuses",
});
// Helper function to make authenticated API requests
async function makeApiRequest(method: string, endpoint: string, data: any = null, params: any = null): Promise<any> {
const url = `${API_BASE_URL}${endpoint}`;
// Validate that API_KEY is defined
if (!API_KEY) {
throw new Error("TASK_MANAGER_API_KEY environment variable is not defined. Please check your .env file.");
}
logDebug(`API Request: ${method} ${url}`);
// Standard headers
const headers = {
"X-API-Key": API_KEY,
"Content-Type": "application/json; charset=utf-8",
"Accept": "application/json, text/plain, */*",
"User-Agent": "TaskMcpServer/1.0",
"Connection": "close",
"Cache-Control": "no-cache"
};
try {
// Log request details
const logEntry = `Timestamp: ${new Date().toISOString()}\nMethod: ${method}\nURL: ${url}\nParams: ${JSON.stringify(params)}\nData: ${JSON.stringify(data)}\nHeaders: ${JSON.stringify(headers)}\n\n`;
fs.appendFileSync("api_debug.log", logEntry);
// Configure axios request options
const requestConfig: any = {
method,
url,
headers,
data,
params,
maxRedirects: 0,
timeout: 20000,
decompress: false,
validateStatus: function (status: number) {
return status < 500; // Don't reject if status code is less than 500
}
};
// Ensure proper data encoding for all requests
if (data) {
requestConfig.data = JSON.stringify(data);
}
// Add transform request for properly handling all requests
requestConfig.transformRequest = [(data: any, headers: any) => {
// Force proper content type
headers['Content-Type'] = 'application/json; charset=utf-8';
return typeof data === 'string' ? data : JSON.stringify(data);
}];
// Add specific URL handling for individual task endpoints
if (endpoint.startsWith('/tasks/') && method === 'GET') {
// Fix to retrieve individual task by adding specific query parameters
requestConfig.params = { ...params, id: endpoint.split('/')[2] };
}
const response = await axios(requestConfig);
// Check for HTTP error status codes we didn't automatically reject
if (response.status >= 400 && response.status < 500) {
logError(`HTTP error ${response.status} from API`, response.data);
// Enhanced error logging
const errorLogEntry = `Timestamp: ${new Date().toISOString()}\nError: HTTP ${response.status}\nURL: ${url}\nMethod: ${method}\nResponse: ${JSON.stringify(response.data)}\n\n`;
fs.appendFileSync("api_error.log", errorLogEntry);
throw new Error(`API Error (${response.status}): ${JSON.stringify(response.data)}`);
}
// Check if response has expected format
if ((method === "POST" && endpoint === "/tasks/list") || (method === "GET" && endpoint === "/tasks")) {
logDebug(`listTasks response`, response.data.tasks || []);
if (!response.data || !response.data.tasks || response.data.tasks.length === 0) {
logDebug("API returned empty tasks array");
}
}
return response.data;
} catch (error: any) {
logError(`API Error: ${error.message}`);
// Enhanced error logging with more details
const errorDetails = error.response
? `Status: ${error.response.status}, Data: ${JSON.stringify(error.response.data || 'No response data')}`
: (error.request ? 'No response received' : error.message);
const errorLogEntry = `Timestamp: ${new Date().toISOString()}\nError: ${error.message}\nDetails: ${errorDetails}\nURL: ${url}\nMethod: ${method}\n\n`;
fs.appendFileSync("api_error.log", errorLogEntry);
if (error.response) {
throw new Error(
`API Error (${error.response.status}): ${JSON.stringify(error.response.data || 'No response data')}`,
);
} else if (error.request) {
throw new Error(`API Request Error: No response received (possible network issue)`);
}
throw error;
}
}
// Resource: Tasks list
server.resource(
"tasks",
new ResourceTemplate("tasks://list", { list: undefined }),
async (uri: any) => {
try {
const tasks = await makeApiRequest("POST", "/tasks/list");
// Validate the tasks structure
if (!tasks || !tasks.tasks || !Array.isArray(tasks.tasks)) {
logError(`Invalid tasks data structure`, tasks);
return {
contents: [{
uri: "tasks://error",
text: `Error: Received invalid task data from API`,
metadata: { error: "Invalid data structure", data: tasks }
}]
};
}
// Format tasks for easy display and use
return {
contents: tasks.tasks.map((task: any) => ({
uri: `tasks://task/${task.id}`,
text: `ID: ${task.id}
Task: ${task.task || 'No description'}
Category: ${task.category || 'Uncategorized'}
Priority: ${task.priority || 'medium'}
Status: ${task.status || 'not_started'}
Created: ${task.create_time || 'unknown'}`,
metadata: {
id: task.id,
task: task.task || 'No description',
category: task.category,
priority: task.priority || 'medium',
status: task.status || 'not_started',
create_time: task.create_time,
},
})),
};
} catch (error: any) {
logError(`Error fetching tasks: ${error.message}`);
return {
contents: [{
uri: "tasks://error",
text: `Error retrieving tasks: ${error.message}`,
metadata: { error: error.message }
}]
};
}
}
);
// Resource: Individual task
server.resource(
"task",
new ResourceTemplate("tasks://task/{taskId}", { list: undefined }),
async (uri: any, params: any) => {
try {
const taskId = params.taskId;
// Try direct task endpoint first
let task;
try {
const taskResult = await makeApiRequest("GET", `/tasks/${taskId}`);
if (taskResult && (taskResult.id || taskResult.task)) {
task = taskResult;
}
} catch (directError) {
logDebug(`Direct task fetch failed, using task list fallback: ${directError}`);
// Fallback to getting all tasks and filtering
const tasks = await makeApiRequest("POST", "/tasks/list");
task = tasks.tasks.find((t: any) => t.id === Number(taskId) || t.id === taskId);
}
if (!task) {
return {
contents: [{
uri: uri.href,
text: `Task with ID ${taskId} not found`,
metadata: { error: "Task not found" }
}]
};
}
// Format task for easy display
return {
contents: [
{
uri: uri.href,
text: `ID: ${task.id}
Task: ${task.task}
Category: ${task.category}
Priority: ${task.priority}
Status: ${task.status}
Created: ${task.create_time}`,
metadata: task,
},
],
};
} catch (error: any) {
return {
contents: [{
uri: uri.href,
text: `Error retrieving task ${params.taskId}: ${error.message}`,
metadata: { error: error.message }
}]
};
}
}
);
// Tool: List Tasks
server.tool(
"listTasks",
{
status: z.enum(["not_started", "started", "done"]).optional()
.describe("Filter tasks by status (optional)"),
priority: z.enum(["low", "medium", "high"]).optional()
.describe("Filter tasks by priority level (optional)")
},
async ({ status, priority }: { status?: string, priority?: string }) => {
try {
const params: any = {};
if (status) params.status = status;
if (priority) params.priority = priority;
const tasksResponse = await makeApiRequest("POST", "/tasks/list", { status, priority });
// More flexible validation for tasks data structure
let tasks: any[] = [];
// Handle various response formats that might come from the API
if (tasksResponse) {
if (Array.isArray(tasksResponse.tasks)) {
// Standard format: { tasks: [...] }
tasks = tasksResponse.tasks;
logDebug("Found tasks array in standard format");
} else if (Array.isArray(tasksResponse)) {
// Direct array format: [...]
tasks = tasksResponse;
logDebug("Found tasks in direct array format");
} else if (typeof tasksResponse === 'object' && tasksResponse !== null) {
// Try to extract tasks from any available property
const possibleTasksProperties = Object.entries(tasksResponse)
.filter(([_, value]) => Array.isArray(value))
.map(([key, value]) => ({ key, value }));
if (possibleTasksProperties.length > 0) {
// Use the first array property as tasks
const tasksProp = possibleTasksProperties[0];
tasks = tasksProp.value as any[];
logDebug(`Found tasks array in property: ${tasksProp.key}`);
} else {
logError(`No tasks array found in response`, tasksResponse);
}
}
}
// If we still couldn't find tasks, log error and return empty array
if (tasks.length === 0) {
logError(`Invalid or empty tasks data structure`, tasksResponse);
}
// Format response in a way that's useful for AI to parse
const formattedTasks = tasks.map(task => ({
id: task.id,
task: task.task || "No description",
category: task.category,
priority: task.priority || "medium",
status: task.status || "not_started",
createTime: task.create_time || task.created_at || task.createTime || new Date().toISOString()
}));
// Log the formatted response for debugging
logDebug(`listTasks formatted response`, formattedTasks);
return {
content: [
{
type: "text",
text: `Found ${tasks.length} tasks${status ? ` with status '${status}'` : ''}${priority ? ` and priority '${priority}'` : ''}.`
},
{
type: "text",
text: JSON.stringify(formattedTasks, null, 2)
}
]
};
} catch (error: any) {
return {
content: [
{
type: "text",
text: `Error listing tasks: ${error.message}`
}
]
};
}
}
);
// Tool: Create Task
server.tool(
"createTask",
{
task: z.string().min(1, "Task description is required")
.describe("The task description or title"),
category: z.string().min(1, "Category is required")
.describe("Task category (e.g., 'Development', 'Documentation')"),
priority: z.enum(["low", "medium", "high"]).optional()
.describe("Task priority level (defaults to 'medium' if not specified)"),
status: z.enum(["not_started", "started", "done"]).optional()
.describe("Initial task status (defaults to 'not_started' if not specified)")
},
async ({ task, category, priority, status }: {
task: string;
category: string;
priority?: string;
status?: string
}) => {
try {
const requestBody: any = {
task,
category,
};
if (priority) requestBody.priority = priority;
if (status) requestBody.status = status;
const newTask = await makeApiRequest("POST", "/tasks", requestBody);
logDebug(`Created new task with ID ${newTask.id}`);
return {
content: [
{
type: "text",
text: `Task created successfully with ID: ${newTask.id}`
},
{
type: "text",
text: JSON.stringify({
id: newTask.id,
task: newTask.task || task,
category: newTask.category || category,
priority: newTask.priority || priority || "medium",
status: newTask.status || status || "not_started",
create_time: newTask.create_time || new Date().toISOString()
}, null, 2)
}
]
};
} catch (error: any) {
logError(`Error in createTask: ${error.message}`);
return {
content: [
{
type: "text",
text: `Error creating task: ${error.message}`
}
]
};
}
}
);
// Tool: Update Task
server.tool(
"updateTask",
{
taskId: z.number().int().positive("Task ID must be a positive integer")
.describe("The unique ID of the task to update"),
task: z.string().optional()
.describe("New task description/title (if you want to change it)"),
category: z.string().optional()
.describe("New task category (if you want to change it)"),
priority: z.enum(["low", "medium", "high"]).optional()
.describe("New task priority (if you want to change it)"),
status: z.enum(["not_started", "started", "done"]).optional()
.describe("New task status (if you want to change it)")
},
async ({ taskId, task, category, priority, status }: {
taskId: number;
task?: string;
category?: string;
priority?: string;
status?: string;
}) => {
try {
const requestBody: any = {};
if (task) requestBody.task = task;
if (category) requestBody.category = category;
if (priority) requestBody.priority = priority;
if (status) requestBody.status = status;
if (Object.keys(requestBody).length === 0) {
return {
content: [
{
type: "text",
text: "No updates provided. Task remains unchanged."
}
]
};
}
const updatedTask = await makeApiRequest(
"PATCH",
`/tasks/${taskId}`,
requestBody
);
return {
content: [
{
type: "text",
text: `Task ${taskId} updated successfully.`
},
{
type: "text",
text: JSON.stringify({
id: updatedTask.id,
task: updatedTask.task,
category: updatedTask.category,
priority: updatedTask.priority,
status: updatedTask.status,
created: updatedTask.create_time
}, null, 2)
}
]
};
} catch (error: any) {
return {
content: [
{
type: "text",
text: `Error updating task: ${error.message}`
}
]
};
}
}
);
// Tool: Delete Task
server.tool(
"deleteTask",
{
taskId: z.number().int().positive("Task ID must be a positive integer")
.describe("The unique ID of the task to delete")
},
async ({ taskId }: { taskId: number }) => {
try {
const response = await makeApiRequest("DELETE", `/tasks/${taskId}`);
logDebug(`Deleted task ID ${taskId}`);
return {
content: [
{
type: "text",
text: response.message || `Task ${taskId} deleted successfully.`
}
]
};
} catch (error: any) {
logError(`Error in deleteTask: ${error.message}`);
return {
content: [
{
type: "text",
text: `Error deleting task: ${error.message}`
}
]
};
}
}
);
// Prompt: List all tasks with category analysis
server.prompt(
"listAllTasks",
{},
() => ({
messages: [
{
role: "user",
content: {
type: "text",
text: "Please list all tasks in my task management system. Group them by category and summarize the priorities for each category."
}
}
]
})
);
// Prompt: Create task with natural language
server.prompt(
"createTaskNaturalLanguage",
{
description: z.string().min(10, "Task description must be at least 10 characters")
.describe("A natural language description of the task to create")
},
({ description }: { description: string }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `Please analyze this task description and create an appropriate task:
"${description}"
Extract the most suitable category, determine an appropriate priority level, and create the task with the right parameters.`
}
}
]
})
);
// Prompt: Create new task with specific parameters
server.prompt(
"createNewTask",
{
task: z.string().min(1, "Task description is required")
.describe("The task description or title"),
category: z.string().min(1, "Category is required")
.describe("Task category"),
priority: z.enum(["low", "medium", "high"]).optional()
.describe("Task priority level")
},
({ task, category, priority }: { task: string; category: string; priority?: string }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `Please create a new task in my task management system with the following details:
Task: ${task}
Category: ${category}
${priority ? `Priority: ${priority}` : ""}
Please confirm once the task is created and provide the task ID for reference.`
}
}
]
})
);
// Prompt: Task progress report
server.prompt(
"taskProgressReport",
{
status: z.enum(["not_started", "started", "done"]).optional()
.describe("Filter by task status")
},
({ status }: { status?: string }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `Please provide a progress report on ${status ? `all ${status} tasks` : "all tasks"}.
Include:
1. How many tasks are in each status category
2. Which high priority tasks need attention
3. Any categories with a high concentration of incomplete tasks`
}
}
]
})
);
// Start receiving messages on stdin and sending messages on stdout
const transport = new StdioServerTransport();
await server.connect(transport);
```
--------------------------------------------------------------------------------
/src/http-server.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import {
McpServer,
ResourceTemplate,
} from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from 'express';
import cors from 'cors';
import { z } from "zod";
import * as fs from 'fs';
import axios from 'axios';
import dotenv from 'dotenv';
// Load environment variables from .env file
dotenv.config();
// Base URL for the Task API from environment variables
const API_BASE_URL = process.env.TASK_MANAGER_API_BASE_URL || "https://task-master-pro-mikaelwestoo.replit.app/api";
// API Key from environment variables
const API_KEY = process.env.TASK_MANAGER_API_KEY;
// HTTP server port - prioritize PORT for backward compatibility, then TASK_MANAGER_HTTP_PORT from .env, then default to 3000
const PORT = process.env.PORT || process.env.TASK_MANAGER_HTTP_PORT || 3000;
// Helper function for logging to file
function logToFile(filename: string, message: string): void {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] ${message}\n`;
fs.appendFileSync(filename, logEntry);
}
// Helper function to log errors
function logError(message: string, details: any = null): void {
let errorMessage = `[ERROR] ${message}`;
if (details) {
errorMessage += `\nDetails: ${JSON.stringify(details, null, 2)}`;
}
logToFile("server_error.log", errorMessage);
}
// Helper function to log debug info
function logDebug(message: string, data: any = null): void {
let debugMessage = `[DEBUG] ${message}`;
if (data) {
debugMessage += `\nData: ${JSON.stringify(data, null, 2)}`;
}
logToFile("server_debug.log", debugMessage);
}
// Schema definitions
const TaskSchema = z.object({
id: z.number().int().positive().describe("Unique task identifier"),
task: z.string().describe("The task description/title"),
category: z.string().describe("Task category (e.g., 'Development', 'Documentation')"),
priority: z.enum(["low", "medium", "high"]).describe("Task priority level"),
status: z.enum(["not_started", "started", "done"]).describe("Current task status"),
create_time: z.string().describe("Task creation timestamp in ISO format"),
});
const TaskListSchema = z.object({
tasks: z.array(TaskSchema).describe("List of tasks"),
});
// Helper function to make authenticated API requests
async function makeApiRequest(method: string, endpoint: string, data: any = null, params: any = null): Promise<any> {
const url = `${API_BASE_URL}${endpoint}`;
// Validate that API_KEY is defined
if (!API_KEY) {
throw new Error("TASK_MANAGER_API_KEY environment variable is not defined. Please check your .env file.");
}
logDebug(`API Request: ${method} ${url}`);
// Standard headers
const headers = {
"X-API-Key": API_KEY,
"Content-Type": "application/json; charset=utf-8",
"Accept": "application/json, text/plain, */*",
"User-Agent": "TaskMcpServer/1.0",
"Connection": "close",
"Cache-Control": "no-cache"
};
try {
// Log request details
const logEntry = `Timestamp: ${new Date().toISOString()}\nMethod: ${method}\nURL: ${url}\nParams: ${JSON.stringify(params)}\nData: ${JSON.stringify(data)}\nHeaders: ${JSON.stringify(headers)}\n\n`;
fs.appendFileSync("api_debug.log", logEntry);
// Configure axios request options
const requestConfig: any = {
method,
url,
headers,
data,
params,
maxRedirects: 0,
timeout: 20000,
decompress: false,
validateStatus: function (status: number) {
return status < 500; // Don't reject if status code is less than 500
}
};
// Ensure proper data encoding for all requests
if (data) {
requestConfig.data = JSON.stringify(data);
}
// Add transform request for properly handling all requests
requestConfig.transformRequest = [(data: any, headers: any) => {
// Force proper content type
headers['Content-Type'] = 'application/json; charset=utf-8';
return typeof data === 'string' ? data : JSON.stringify(data);
}];
// Add specific URL handling for individual task endpoints
if (endpoint.startsWith('/tasks/') && method === 'GET') {
// Fix to retrieve individual task by adding specific query parameters
requestConfig.params = { ...params, id: endpoint.split('/')[2] };
}
const response = await axios(requestConfig);
// Check for HTTP error status codes we didn't automatically reject
if (response.status >= 400 && response.status < 500) {
logError(`HTTP error ${response.status} from API`, response.data);
// Enhanced error logging
const errorLogEntry = `Timestamp: ${new Date().toISOString()}\nError: HTTP ${response.status}\nURL: ${url}\nMethod: ${method}\nResponse: ${JSON.stringify(response.data)}\n\n`;
fs.appendFileSync("api_error.log", errorLogEntry);
throw new Error(`API Error (${response.status}): ${JSON.stringify(response.data)}`);
}
// Check if response has expected format
if ((method === "POST" && endpoint === "/tasks/list") || (method === "GET" && endpoint === "/tasks")) {
logDebug(`listTasks response`, response.data.tasks || []);
if (!response.data || !response.data.tasks || response.data.tasks.length === 0) {
logDebug("API returned empty tasks array");
}
}
return response.data;
} catch (error: any) {
logError(`API Error: ${error.message}`);
// Enhanced error logging with more details
const errorDetails = error.response
? `Status: ${error.response.status}, Data: ${JSON.stringify(error.response.data || 'No response data')}`
: (error.request ? 'No response received' : error.message);
const errorLogEntry = `Timestamp: ${new Date().toISOString()}\nError: ${error.message}\nDetails: ${errorDetails}\nURL: ${url}\nMethod: ${method}\n\n`;
fs.appendFileSync("api_error.log", errorLogEntry);
if (error.response) {
throw new Error(
`API Error (${error.response.status}): ${JSON.stringify(error.response.data || 'No response data')}`,
);
} else if (error.request) {
throw new Error(`API Request Error: No response received (possible network issue)`);
}
throw error;
}
}
// Create an Express app
const app = express();
// Configure middleware
app.use(cors());
app.use(express.json());
app.use(express.static('public'));
// Store active transports for message routing
const activeTransports = new Map<string, SSEServerTransport>();
// Create an MCP server
const server = new McpServer({
name: "Task Management API Server",
version: "1.0.0",
description: "Task Management API that provides CRUD operations for tasks with categories, priorities, and statuses",
});
// Add resources and tools similar to index.ts
// Resource: Tasks list
server.resource(
"tasks",
new ResourceTemplate("tasks://list", { list: undefined }),
async (uri: any) => {
try {
const tasks = await makeApiRequest("POST", "/tasks/list");
// Validate the tasks structure
if (!tasks || !tasks.tasks || !Array.isArray(tasks.tasks)) {
logError(`Invalid tasks data structure`, tasks);
return {
contents: [{
uri: "tasks://error",
text: `Error: Received invalid task data from API`,
metadata: { error: "Invalid data structure", data: tasks }
}]
};
}
// Format tasks for easy display and use
return {
contents: tasks.tasks.map((task: any) => ({
uri: `tasks://task/${task.id}`,
text: `ID: ${task.id}
Task: ${task.task || 'No description'}
Category: ${task.category || 'Uncategorized'}
Priority: ${task.priority || 'medium'}
Status: ${task.status || 'not_started'}
Created: ${task.create_time || 'unknown'}`,
metadata: {
id: task.id,
task: task.task || 'No description',
category: task.category,
priority: task.priority || 'medium',
status: task.status || 'not_started',
create_time: task.create_time,
},
})),
};
} catch (error: any) {
logError(`Error fetching tasks: ${error.message}`);
return {
contents: [{
uri: "tasks://error",
text: `Error retrieving tasks: ${error.message}`,
metadata: { error: error.message }
}]
};
}
}
);
// Resource: Individual task
server.resource(
"task",
new ResourceTemplate("tasks://task/{taskId}", { list: undefined }),
async (uri: any, params: any) => {
try {
const taskId = params.taskId;
// Try direct task endpoint first
let task;
try {
const taskResult = await makeApiRequest("GET", `/tasks/${taskId}`);
if (taskResult && (taskResult.id || taskResult.task)) {
task = taskResult;
}
} catch (directError) {
logDebug(`Direct task fetch failed, using task list fallback: ${directError}`);
// Fallback to getting all tasks and filtering
const tasks = await makeApiRequest("POST", "/tasks/list");
task = tasks.tasks.find((t: any) => t.id === Number(taskId) || t.id === taskId);
}
if (!task) {
return {
contents: [{
uri: uri.href,
text: `Task with ID ${taskId} not found`,
metadata: { error: "Task not found" }
}]
};
}
// Format task for easy display
return {
contents: [
{
uri: uri.href,
text: `ID: ${task.id}
Task: ${task.task}
Category: ${task.category}
Priority: ${task.priority}
Status: ${task.status}
Created: ${task.create_time}`,
metadata: task,
},
],
};
} catch (error: any) {
return {
contents: [{
uri: uri.href,
text: `Error retrieving task ${params.taskId}: ${error.message}`,
metadata: { error: error.message }
}]
};
}
}
);
// Tool: List Tasks
server.tool(
"listTasks",
{
status: z.enum(["not_started", "started", "done"]).optional()
.describe("Filter tasks by status (optional)"),
priority: z.enum(["low", "medium", "high"]).optional()
.describe("Filter tasks by priority level (optional)")
},
async ({ status, priority }: { status?: string, priority?: string }) => {
try {
const params: any = {};
if (status) params.status = status;
if (priority) params.priority = priority;
const tasksResponse = await makeApiRequest("POST", "/tasks/list", { status, priority });
// More flexible validation for tasks data structure
let tasks: any[] = [];
// Handle various response formats that might come from the API
if (tasksResponse) {
if (Array.isArray(tasksResponse.tasks)) {
// Standard format: { tasks: [...] }
tasks = tasksResponse.tasks;
logDebug("Found tasks array in standard format");
} else if (Array.isArray(tasksResponse)) {
// Direct array format: [...]
tasks = tasksResponse;
logDebug("Found tasks in direct array format");
} else if (typeof tasksResponse === 'object' && tasksResponse !== null) {
// Try to extract tasks from any available property
const possibleTasksProperties = Object.entries(tasksResponse)
.filter(([_, value]) => Array.isArray(value))
.map(([key, value]) => ({ key, value }));
if (possibleTasksProperties.length > 0) {
// Use the first array property as tasks
const tasksProp = possibleTasksProperties[0];
tasks = tasksProp.value as any[];
logDebug(`Found tasks array in property: ${tasksProp.key}`);
} else {
logError(`No tasks array found in response`, tasksResponse);
}
}
}
// If we still couldn't find tasks, log error and return empty array
if (tasks.length === 0) {
logError(`Invalid or empty tasks data structure`, tasksResponse);
}
// Format response in a way that's useful for AI to parse
const formattedTasks = tasks.map(task => ({
id: task.id,
task: task.task || "No description",
category: task.category,
priority: task.priority || "medium",
status: task.status || "not_started",
createTime: task.create_time || task.created_at || task.createTime || new Date().toISOString()
}));
// Log the formatted response for debugging
logDebug(`listTasks formatted response`, formattedTasks);
return {
content: [
{
type: "text",
text: `Found ${tasks.length} tasks${status ? ` with status '${status}'` : ''}${priority ? ` and priority '${priority}'` : ''}.`
},
{
type: "text",
text: JSON.stringify(formattedTasks, null, 2)
}
]
};
} catch (error: any) {
return {
content: [
{
type: "text",
text: `Error listing tasks: ${error.message}`
}
]
};
}
}
);
// Tool: Create Task
server.tool(
"createTask",
{
task: z.string().min(1, "Task description is required")
.describe("The task description or title"),
category: z.string().min(1, "Category is required")
.describe("Task category (e.g., 'Development', 'Documentation')"),
priority: z.enum(["low", "medium", "high"]).optional()
.describe("Task priority level (defaults to 'medium' if not specified)"),
status: z.enum(["not_started", "started", "done"]).optional()
.describe("Initial task status (defaults to 'not_started' if not specified)")
},
async ({ task, category, priority, status }: {
task: string;
category: string;
priority?: string;
status?: string
}) => {
try {
const requestBody: any = {
task,
category,
};
if (priority) requestBody.priority = priority;
if (status) requestBody.status = status;
const newTask = await makeApiRequest("POST", "/tasks", requestBody);
logDebug(`Created new task with ID ${newTask.id}`);
return {
content: [
{
type: "text",
text: `Task created successfully with ID: ${newTask.id}`
},
{
type: "text",
text: JSON.stringify({
id: newTask.id,
task: newTask.task || task,
category: newTask.category || category,
priority: newTask.priority || priority || "medium",
status: newTask.status || status || "not_started",
create_time: newTask.create_time || new Date().toISOString()
}, null, 2)
}
]
};
} catch (error: any) {
logError(`Error in createTask: ${error.message}`);
return {
content: [
{
type: "text",
text: `Error creating task: ${error.message}`
}
]
};
}
}
);
// Tool: Update Task
server.tool(
"updateTask",
{
taskId: z.number().int().positive("Task ID must be a positive integer")
.describe("The unique ID of the task to update"),
task: z.string().optional()
.describe("New task description/title (if you want to change it)"),
category: z.string().optional()
.describe("New task category (if you want to change it)"),
priority: z.enum(["low", "medium", "high"]).optional()
.describe("New task priority (if you want to change it)"),
status: z.enum(["not_started", "started", "done"]).optional()
.describe("New task status (if you want to change it)")
},
async ({ taskId, task, category, priority, status }: {
taskId: number;
task?: string;
category?: string;
priority?: string;
status?: string;
}) => {
try {
const requestBody: any = {};
if (task) requestBody.task = task;
if (category) requestBody.category = category;
if (priority) requestBody.priority = priority;
if (status) requestBody.status = status;
if (Object.keys(requestBody).length === 0) {
return {
content: [
{
type: "text",
text: "No updates provided. Task remains unchanged."
}
]
};
}
const updatedTask = await makeApiRequest(
"PATCH",
`/tasks/${taskId}`,
requestBody
);
return {
content: [
{
type: "text",
text: `Task ${taskId} updated successfully.`
},
{
type: "text",
text: JSON.stringify({
id: updatedTask.id,
task: updatedTask.task,
category: updatedTask.category,
priority: updatedTask.priority,
status: updatedTask.status,
created: updatedTask.create_time
}, null, 2)
}
]
};
} catch (error: any) {
return {
content: [
{
type: "text",
text: `Error updating task: ${error.message}`
}
]
};
}
}
);
// Tool: Delete Task
server.tool(
"deleteTask",
{
taskId: z.number().int().positive("Task ID must be a positive integer")
.describe("The unique ID of the task to delete")
},
async ({ taskId }: { taskId: number }) => {
try {
const response = await makeApiRequest("DELETE", `/tasks/${taskId}`);
logDebug(`Deleted task ID ${taskId}`);
return {
content: [
{
type: "text",
text: response.message || `Task ${taskId} deleted successfully.`
}
]
};
} catch (error: any) {
logError(`Error in deleteTask: ${error.message}`);
return {
content: [
{
type: "text",
text: `Error deleting task: ${error.message}`
}
]
};
}
}
);
// Prompts (same as index.ts)
server.prompt(
"listAllTasks",
{},
() => ({
messages: [
{
role: "user",
content: {
type: "text",
text: "Please list all tasks in my task management system. Group them by category and summarize the priorities for each category."
}
}
]
})
);
server.prompt(
"createTaskNaturalLanguage",
{
description: z.string().min(10, "Task description must be at least 10 characters")
.describe("A natural language description of the task to create")
},
({ description }: { description: string }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `Please analyze this task description and create an appropriate task:
"${description}"
Extract the most suitable category, determine an appropriate priority level, and create the task with the right parameters.`
}
}
]
})
);
server.prompt(
"createNewTask",
{
task: z.string().min(1, "Task description is required")
.describe("The task description or title"),
category: z.string().min(1, "Category is required")
.describe("Task category"),
priority: z.enum(["low", "medium", "high"]).optional()
.describe("Task priority level")
},
({ task, category, priority }: { task: string; category: string; priority?: string }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `Please create a new task in my task management system with the following details:
Task: ${task}
Category: ${category}
${priority ? `Priority: ${priority}` : ""}
Please confirm once the task is created and provide the task ID for reference.`
}
}
]
})
);
server.prompt(
"taskProgressReport",
{
status: z.enum(["not_started", "started", "done"]).optional()
.describe("Filter by task status")
},
({ status }: { status?: string }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `Please provide a progress report on ${status ? `all ${status} tasks` : "all tasks"}.
Include:
1. How many tasks are in each status category
2. Which high priority tasks need attention
3. Any categories with a high concentration of incomplete tasks`
}
}
]
})
);
// SSE endpoint
app.get('/sse', async (req, res) => {
const connectionId = Date.now().toString();
// Set SSE headers
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
// Send initial message
res.write(`data: ${JSON.stringify({ type: 'connected', id: connectionId })}\n\n`);
// Create and store transport
const transport = new SSEServerTransport('/messages', res);
activeTransports.set(connectionId, transport);
// Connect the server to this transport instance
await server.connect(transport);
// Handle client disconnection
req.on('close', () => {
logDebug(`Client disconnected: ${connectionId}`);
transport.close();
activeTransports.delete(connectionId);
});
});
// Messages endpoint for client-to-server communication
app.post('/messages', express.json(), (req, res, next) => {
const connectionId = req.headers['x-connection-id'] as string;
if (!connectionId || !activeTransports.has(connectionId)) {
logError('Invalid or missing connection ID', { connectionId });
res.status(400).json({ error: 'Invalid or missing connection ID' });
return;
}
const transport = activeTransports.get(connectionId);
if (!transport) {
logError('Transport not found', { connectionId });
res.status(404).json({ error: 'Transport not found' });
return;
}
// Handle the message and catch any errors
transport.handlePostMessage(req as any, res as any, req.body)
.then(() => {
if (!res.headersSent) {
res.status(200).end();
}
})
.catch((error: any) => {
logError('Error handling message', { error: error.message, connectionId });
if (!res.headersSent) {
res.status(500).json({ error: error.message });
}
next(error);
});
});
// Create a simple HTML page for interacting with the server
app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MCP Task Manager</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
h1 { color: #333; }
pre { background: #f4f4f4; padding: 10px; border-radius: 5px; overflow-x: auto; }
button { margin: 5px; padding: 8px 16px; background: #4CAF50; color: white; border: none;
border-radius: 4px; cursor: pointer; }
button:hover { background: #45a049; }
#output { margin-top: 20px; }
</style>
</head>
<body>
<h1>Task Manager MCP Server</h1>
<p>This is an HTTP + SSE implementation of the Task Manager MCP Server.</p>
<div>
<button id="connect">Connect</button>
<button id="discover" disabled>Discover</button>
<button id="list-tasks" disabled>List Tasks</button>
<button id="create-task" disabled>Create Test Task</button>
</div>
<pre id="output">Click 'Connect' to start...</pre>
<script>
const output = document.getElementById('output');
const connectBtn = document.getElementById('connect');
const discoverBtn = document.getElementById('discover');
const listTasksBtn = document.getElementById('list-tasks');
const createTaskBtn = document.getElementById('create-task');
let connectionId = null;
let eventSource = null;
let messageId = 0;
function log(message) {
output.textContent += message + '\\n';
output.scrollTop = output.scrollHeight;
}
connectBtn.addEventListener('click', () => {
log('Connecting to server...');
eventSource = new EventSource('/sse');
eventSource.onopen = () => {
log('SSE connection established');
};
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
log('Received: ' + JSON.stringify(data, null, 2));
if (data.type === 'connected') {
connectionId = data.id;
discoverBtn.disabled = false;
listTasksBtn.disabled = false;
createTaskBtn.disabled = false;
connectBtn.disabled = true;
log('Connected with ID: ' + connectionId);
}
};
eventSource.onerror = (error) => {
log('SSE Error: ' + JSON.stringify(error));
};
});
async function sendMessage(message) {
try {
const response = await fetch('/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Connection-ID': connectionId
},
body: JSON.stringify(message)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(\`HTTP error \${response.status}: \${errorText}\`);
}
} catch (error) {
log('Error sending message: ' + error.message);
}
}
discoverBtn.addEventListener('click', () => {
const message = {
id: 'discover_' + (++messageId),
type: 'discover'
};
log('Sending discover request...');
sendMessage(message);
});
listTasksBtn.addEventListener('click', () => {
const message = {
id: 'invoke_' + (++messageId),
type: 'invoke',
tool: 'listTasks',
parameters: {}
};
log('Sending listTasks request...');
sendMessage(message);
});
createTaskBtn.addEventListener('click', () => {
const message = {
id: 'invoke_' + (++messageId),
type: 'invoke',
tool: 'createTask',
parameters: {
task: 'Test task created at ' + new Date().toISOString(),
category: 'Test',
priority: 'medium'
}
};
log('Sending createTask request...');
sendMessage(message);
});
</script>
</body>
</html>
`);
});
// Start the server
app.listen(PORT, () => {
logDebug(`MCP HTTP Server with SSE is running on http://localhost:${PORT}`);
console.log(`MCP HTTP Server with SSE is running on http://localhost:${PORT}`);
});
```