213 lines
7.4 KiB
TypeScript
213 lines
7.4 KiB
TypeScript
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();
|