feat: Implement AI Chat Agent, Email Notifications, and UI enhancements
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
+212
@@ -0,0 +1,212 @@
|
||||
import { Request, Response } from "express";
|
||||
import { storage } from "./storage";
|
||||
import { User } from "../shared/schema";
|
||||
import { randomBytes } from "crypto";
|
||||
|
||||
interface JsonRpcRequest {
|
||||
jsonrpc: "2.0";
|
||||
method: string;
|
||||
params?: any;
|
||||
id: number | string;
|
||||
}
|
||||
|
||||
interface JsonRpcResponse {
|
||||
jsonrpc: "2.0";
|
||||
result?: any;
|
||||
error?: {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: any;
|
||||
};
|
||||
id: number | string | null;
|
||||
}
|
||||
|
||||
export class McpServer {
|
||||
private clients: Map<string, Response> = new Map();
|
||||
|
||||
async authenticate(req: Request): Promise<User | null> {
|
||||
let key = req.headers["x-api-key"] as string;
|
||||
if (!key && req.query.apiKey) {
|
||||
key = req.query.apiKey as string;
|
||||
}
|
||||
// Bearer token support
|
||||
if (!key && req.headers["authorization"]) {
|
||||
const auth = req.headers["authorization"];
|
||||
if (auth.startsWith("Bearer ")) {
|
||||
key = auth.substring(7);
|
||||
}
|
||||
}
|
||||
|
||||
if (!key) return null;
|
||||
return (await storage.getUserByApiKey(key)) || null;
|
||||
}
|
||||
|
||||
async handleSse(req: Request, res: Response) {
|
||||
const user = await this.authenticate(req);
|
||||
if (!user) {
|
||||
res.status(401).send("Unauthorized: Invalid API Key");
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"Access-Control-Allow-Origin": "*", // Allow local connections
|
||||
});
|
||||
|
||||
const sessionId = randomBytes(8).toString("hex");
|
||||
this.clients.set(sessionId, res);
|
||||
|
||||
const endpoint = `/api/mcp/messages`;
|
||||
res.write(`event: endpoint\ndata: ${endpoint}\n\n`);
|
||||
|
||||
// Keep alive
|
||||
const interval = setInterval(() => {
|
||||
res.write(": keepalive\n\n");
|
||||
}, 15000);
|
||||
|
||||
req.on("close", () => {
|
||||
clearInterval(interval);
|
||||
this.clients.delete(sessionId);
|
||||
});
|
||||
}
|
||||
|
||||
async handleMessage(req: Request, res: Response) {
|
||||
const user = await this.authenticate(req);
|
||||
if (!user) {
|
||||
res.status(401).json({ error: "Unauthorized" });
|
||||
return;
|
||||
}
|
||||
|
||||
const body = req.body as JsonRpcRequest;
|
||||
try {
|
||||
const result = await this.processRequest(body, user);
|
||||
res.json({
|
||||
jsonrpc: "2.0",
|
||||
result,
|
||||
id: body.id
|
||||
});
|
||||
} catch (err: any) {
|
||||
res.json({
|
||||
jsonrpc: "2.0",
|
||||
error: {
|
||||
code: -32000,
|
||||
message: err.message || "Internal Server Error"
|
||||
},
|
||||
id: body.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async processRequest(req: JsonRpcRequest, user: User): Promise<any> {
|
||||
switch (req.method) {
|
||||
case "initialize":
|
||||
return {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {}
|
||||
},
|
||||
serverInfo: {
|
||||
name: "TaskFlow MCP",
|
||||
version: "1.0.0"
|
||||
}
|
||||
};
|
||||
case "tools/list": // MCP method
|
||||
case "listTools": // Legacy fallback
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: "list_tasks",
|
||||
description: "List all tasks for the user",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
status: { type: "string", enum: ["todo", "inProgress", "done"], description: "Filter by status" },
|
||||
limit: { type: "number", description: "Limit number of tasks" }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "create_task",
|
||||
description: "Create a new task",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
title: { type: "string", description: "Title of the task" },
|
||||
description: { type: "string", description: "Description" },
|
||||
priority: { type: "string", enum: ["low", "medium", "high"] }
|
||||
},
|
||||
required: ["title"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "complete_task",
|
||||
description: "Mark a task as completed",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Task ID" }
|
||||
},
|
||||
required: ["id"]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
case "tools/call": // MCP method
|
||||
case "callTool": // Legacy
|
||||
return await this.handleToolCall(req.params.name, req.params.arguments, user);
|
||||
|
||||
case "notifications/initialized":
|
||||
return true;
|
||||
|
||||
default:
|
||||
throw new Error(`Method ${req.method} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleToolCall(name: string, args: any, user: User) {
|
||||
switch (name) {
|
||||
case "list_tasks": {
|
||||
let tasks = await storage.getTasksForUser(user.id);
|
||||
if (args?.status) {
|
||||
tasks = tasks.filter((t) => t.status === args.status);
|
||||
}
|
||||
if (args?.limit) {
|
||||
tasks = tasks.slice(0, args.limit);
|
||||
}
|
||||
return { content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }] };
|
||||
}
|
||||
case "create_task": {
|
||||
if (!args.title) throw new Error("Title is required");
|
||||
const task = await storage.createTask({
|
||||
title: args.title,
|
||||
description: args.description || "",
|
||||
priority: args.priority || "medium",
|
||||
status: "todo",
|
||||
isTracking: false,
|
||||
timeTracked: 0,
|
||||
energyLevel: "medium",
|
||||
estimatedDuration: 15,
|
||||
dueDate: null,
|
||||
notes: "",
|
||||
labelId: null,
|
||||
userId: user.id
|
||||
});
|
||||
return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
|
||||
}
|
||||
case "complete_task": {
|
||||
if (!args.id) throw new Error("ID is required");
|
||||
const task = await storage.getTask(args.id);
|
||||
if (!task || task.userId !== user.id) throw new Error("Task not found");
|
||||
const updated = await storage.updateTask(args.id, { status: "done" });
|
||||
return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
|
||||
}
|
||||
default:
|
||||
throw new Error(`Tool ${name} not implemented`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const mcpServer = new McpServer();
|
||||
Reference in New Issue
Block a user