diff --git a/package-lock.json b/package-lock.json index fa72194163..c5b6fa10d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1865,7 +1865,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -3459,7 +3458,6 @@ "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -3568,7 +3566,6 @@ "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "2.1.9", "@vitest/mocker": "2.1.9", @@ -3763,7 +3760,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/memory/README.md b/src/memory/README.md index dcc8116156..945c3e4459 100644 --- a/src/memory/README.md +++ b/src/memory/README.md @@ -182,6 +182,31 @@ The server can be configured using the following environment variables: - `MEMORY_FILE_PATH`: Path to the memory storage JSONL file (default: `memory.jsonl` in the server directory) +#### HTTP Transport + +To run the server with HTTP transport instead of stdio: + +```json +{ + "mcpServers": { + "memory": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-memory" + ], + "env": { + "MCP_TRANSPORT": "http", + "PORT": "3000" + } + } + } +} +``` + +- `MCP_TRANSPORT`: Set to `http` to use HTTP transport (default: stdio) +- `PORT`: HTTP port number (default: 3000) + # VS Code Installation Instructions For quick installation, use one of the one-click installation buttons below: diff --git a/src/memory/index.ts b/src/memory/index.ts index c7d781d2c4..885b75bfce 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -2,11 +2,36 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { z } from "zod"; import { promises as fs } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; +// Simple file lock implementation +class FileLock { + private locks = new Map>(); + + async acquire(key: string, fn: () => Promise): Promise { + while (this.locks.has(key)) { + await this.locks.get(key); + } + + let release: () => void; + const lock = new Promise(resolve => { release = resolve; }); + this.locks.set(key, lock); + + try { + return await fn(); + } finally { + this.locks.delete(key); + release!(); + } + } +} + +const fileLock = new FileLock(); + // Define memory file path using environment variable with fallback export const defaultMemoryPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'memory.jsonl'); @@ -69,39 +94,47 @@ export class KnowledgeGraphManager { constructor(private memoryFilePath: string) {} private async loadGraph(): Promise { - try { - const data = await fs.readFile(this.memoryFilePath, "utf-8"); - const lines = data.split("\n").filter(line => line.trim() !== ""); - return lines.reduce((graph: KnowledgeGraph, line) => { - const item = JSON.parse(line); - if (item.type === "entity") graph.entities.push(item as Entity); - if (item.type === "relation") graph.relations.push(item as Relation); - return graph; - }, { entities: [], relations: [] }); - } catch (error) { - if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") { - return { entities: [], relations: [] }; + return fileLock.acquire(this.memoryFilePath, async () => { + try { + const data = await fs.readFile(this.memoryFilePath, "utf-8"); + const lines = data.split("\n").filter(line => line.trim() !== ""); + return lines.reduce((graph: KnowledgeGraph, line) => { + try { + const item = JSON.parse(line); + if (item.type === "entity") graph.entities.push(item as Entity); + if (item.type === "relation") graph.relations.push(item as Relation); + } catch (parseError) { + console.error(`Skipping malformed line: ${line}`, parseError); + } + return graph; + }, { entities: [], relations: [] }); + } catch (error) { + if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") { + return { entities: [], relations: [] }; + } + throw error; } - throw error; - } + }); } private async saveGraph(graph: KnowledgeGraph): Promise { - const lines = [ - ...graph.entities.map(e => JSON.stringify({ - type: "entity", - name: e.name, - entityType: e.entityType, - observations: e.observations - })), - ...graph.relations.map(r => JSON.stringify({ - type: "relation", - from: r.from, - to: r.to, - relationType: r.relationType - })), - ]; - await fs.writeFile(this.memoryFilePath, lines.join("\n")); + return fileLock.acquire(this.memoryFilePath, async () => { + const lines = [ + ...graph.entities.map(e => JSON.stringify({ + type: "entity", + name: e.name, + entityType: e.entityType, + observations: e.observations + })), + ...graph.relations.map(r => JSON.stringify({ + type: "relation", + from: r.from, + to: r.to, + relationType: r.relationType + })), + ]; + await fs.writeFile(this.memoryFilePath, lines.join("\n") + "\n"); + }); } async createEntities(entities: Entity[]): Promise { @@ -460,9 +493,125 @@ async function main() { // Initialize knowledge graph manager with the memory file path knowledgeGraphManager = new KnowledgeGraphManager(MEMORY_FILE_PATH); - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("Knowledge Graph MCP Server running on stdio"); + // Use HTTP transport if MCP_TRANSPORT=http, otherwise stdio + if (process.env.MCP_TRANSPORT === 'http') { + const { createServer } = await import('http'); + const transports: Record = {}; + const SESSION_TIMEOUT = 30 * 60 * 1000; // 30 minutes + const MAX_REQUEST_SIZE = 10 * 1024 * 1024; // 10MB + const sessionTimers: Record = {}; + + const cleanupSession = (sessionId: string) => { + if (sessionTimers[sessionId]) { + clearTimeout(sessionTimers[sessionId]); + delete sessionTimers[sessionId]; + } + delete transports[sessionId]; + console.error('Session cleaned up:', sessionId); + }; + + const resetSessionTimer = (sessionId: string) => { + if (sessionTimers[sessionId]) { + clearTimeout(sessionTimers[sessionId]); + } + sessionTimers[sessionId] = setTimeout(() => cleanupSession(sessionId), SESSION_TIMEOUT); + }; + + const httpServer = createServer(async (req, res) => { + try { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + + if (req.method === 'POST') { + let body = ''; + let size = 0; + + req.on('data', chunk => { + size += chunk.length; + if (size > MAX_REQUEST_SIZE) { + req.destroy(); + res.writeHead(413); + res.end('Request too large'); + return; + } + body += chunk; + }); + + req.on('end', async () => { + try { + const parsedBody = body.trim() ? JSON.parse(body) : undefined; + + let transport: StreamableHTTPServerTransport; + if (sessionId && transports[sessionId]) { + transport = transports[sessionId]; + resetSessionTimer(sessionId); + } else if (!sessionId) { + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => crypto.randomUUID(), + onsessioninitialized: async (sid) => { + await server.connect(transport); + transports[sid] = transport; + resetSessionTimer(sid); + console.error('Session initialized:', sid); + }, + onsessionclosed: (sid) => { + cleanupSession(sid); + } + }); + } else { + res.writeHead(400); + res.end('Invalid session ID'); + return; + } + + await transport.handleRequest(req, res, parsedBody); + } catch (error) { + console.error('Error handling POST request:', error); + res.writeHead(500); + res.end('Internal server error'); + } + }); + + req.on('error', (error) => { + console.error('Request error:', error); + res.writeHead(400); + res.end('Bad request'); + }); + } else if (req.method === 'GET') { + if (!sessionId || !transports[sessionId]) { + res.writeHead(400); + res.end('Invalid or missing session ID'); + return; + } + resetSessionTimer(sessionId); + await transports[sessionId].handleRequest(req, res); + } else if (req.method === 'DELETE') { + if (!sessionId || !transports[sessionId]) { + res.writeHead(400); + res.end('Invalid or missing session ID'); + return; + } + await transports[sessionId].handleRequest(req, res); + cleanupSession(sessionId); + } else { + res.writeHead(405); + res.end('Method not allowed'); + } + } catch (error) { + console.error('Unhandled error in HTTP handler:', error); + res.writeHead(500); + res.end('Internal server error'); + } + }); + + const port = parseInt(process.env.PORT || '3000'); + httpServer.listen(port, () => { + console.error(`Knowledge Graph MCP Server running on HTTP port ${port}`); + }); + } else { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("Knowledge Graph MCP Server running on stdio"); + } } main().catch((error) => { diff --git a/src/sequentialthinking/README.md b/src/sequentialthinking/README.md index 322ded2726..9c2ef5e766 100644 --- a/src/sequentialthinking/README.md +++ b/src/sequentialthinking/README.md @@ -78,6 +78,32 @@ Add this to your `claude_desktop_config.json`: ``` To disable logging of thought information set env var: `DISABLE_THOUGHT_LOGGING` to `true`. + +#### HTTP Transport + +To run the server with HTTP transport instead of stdio: + +```json +{ + "mcpServers": { + "sequential-thinking": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-sequential-thinking" + ], + "env": { + "MCP_TRANSPORT": "http", + "PORT": "3000" + } + } + } +} +``` + +- `MCP_TRANSPORT`: Set to `http` to use HTTP transport (default: stdio) +- `PORT`: HTTP port number (default: 3000) + Comment ### Usage with VS Code diff --git a/src/sequentialthinking/index.ts b/src/sequentialthinking/index.ts index 809086a94c..43543dda34 100644 --- a/src/sequentialthinking/index.ts +++ b/src/sequentialthinking/index.ts @@ -2,6 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { z } from "zod"; import { SequentialThinkingServer } from './lib.js'; @@ -10,7 +11,19 @@ const server = new McpServer({ version: "0.2.0", }); -const thinkingServer = new SequentialThinkingServer(); +const thinkingSessions = new Map(); + +function getOrCreateThinkingServer(sessionId?: string): SequentialThinkingServer { + if (!sessionId) { + return new SequentialThinkingServer(); + } + + if (!thinkingSessions.has(sessionId)) { + thinkingSessions.set(sessionId, new SequentialThinkingServer()); + } + + return thinkingSessions.get(sessionId)!; +} server.registerTool( "sequentialthinking", @@ -90,13 +103,13 @@ You should: }, }, async (args) => { + const thinkingServer = getOrCreateThinkingServer(); const result = thinkingServer.processThought(args); if (result.isError) { - return result; + return { content: result.content }; } - // Parse the JSON response to get structured content const parsedContent = JSON.parse(result.content[0].text); return { @@ -107,9 +120,115 @@ You should: ); async function runServer() { - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("Sequential Thinking MCP Server running on stdio"); + if (process.env.MCP_TRANSPORT === 'http') { + const { createServer } = await import('http'); + const transports: Record = {}; + const sessionTimeouts: Record = {}; + const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes + const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10MB + + function cleanupSession(sid: string) { + delete transports[sid]; + thinkingSessions.delete(sid); + if (sessionTimeouts[sid]) { + clearTimeout(sessionTimeouts[sid]); + delete sessionTimeouts[sid]; + } + } + + function resetSessionTimeout(sid: string) { + if (sessionTimeouts[sid]) { + clearTimeout(sessionTimeouts[sid]); + } + sessionTimeouts[sid] = setTimeout(() => cleanupSession(sid), SESSION_TIMEOUT_MS); + } + + const httpServer = createServer(async (req, res) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + + if (req.method === 'POST') { + const chunks: Buffer[] = []; + let totalSize = 0; + + req.on('data', chunk => { + totalSize += chunk.length; + if (totalSize > MAX_BODY_SIZE) { + req.destroy(); + res.writeHead(413); + res.end('Request body too large'); + return; + } + chunks.push(chunk); + }); + + req.on('end', async () => { + let parsedBody; + try { + const body = Buffer.concat(chunks).toString(); + parsedBody = body.trim() ? JSON.parse(body) : undefined; + } catch (error) { + res.writeHead(400); + res.end('Invalid JSON'); + return; + } + + let transport: StreamableHTTPServerTransport; + if (sessionId && transports[sessionId]) { + transport = transports[sessionId]; + resetSessionTimeout(sessionId); + } else if (!sessionId) { + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => crypto.randomUUID(), + onsessioninitialized: async (sid) => { + await server.connect(transport); + transports[sid] = transport; + resetSessionTimeout(sid); + console.error('Session initialized:', sid); + }, + onsessionclosed: (sid) => { + cleanupSession(sid); + console.error('Session closed:', sid); + } + }); + } else { + res.writeHead(400); + res.end('Invalid session ID'); + return; + } + + await transport.handleRequest(req, res, parsedBody); + }); + } else if (req.method === 'GET') { + if (!sessionId || !transports[sessionId]) { + res.writeHead(400); + res.end('Invalid or missing session ID'); + return; + } + resetSessionTimeout(sessionId); + await transports[sessionId].handleRequest(req, res); + } else if (req.method === 'DELETE') { + if (!sessionId || !transports[sessionId]) { + res.writeHead(400); + res.end('Invalid or missing session ID'); + return; + } + await transports[sessionId].handleRequest(req, res); + cleanupSession(sessionId); + } else { + res.writeHead(405); + res.end('Method not allowed'); + } + }); + + const port = parseInt(process.env.PORT || '3000'); + httpServer.listen(port, () => { + console.error(`Sequential Thinking MCP Server running on HTTP port ${port}`); + }); + } else { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("Sequential Thinking MCP Server running on stdio"); + } } runServer().catch((error) => { diff --git a/src/sequentialthinking/lib.ts b/src/sequentialthinking/lib.ts index 31a1098644..f6ba845610 100644 --- a/src/sequentialthinking/lib.ts +++ b/src/sequentialthinking/lib.ts @@ -16,11 +16,18 @@ export class SequentialThinkingServer { private thoughtHistory: ThoughtData[] = []; private branches: Record = {}; private disableThoughtLogging: boolean; + private readonly maxHistorySize: number = 1000; constructor() { this.disableThoughtLogging = (process.env.DISABLE_THOUGHT_LOGGING || "").toLowerCase() === "true"; } + private pruneHistory(): void { + if (this.thoughtHistory.length > this.maxHistorySize) { + this.thoughtHistory = this.thoughtHistory.slice(-this.maxHistorySize); + } + } + private formatThought(thoughtData: ThoughtData): string { const { thoughtNumber, totalThoughts, thought, isRevision, revisesThought, branchFromThought, branchId } = thoughtData; @@ -58,12 +65,18 @@ export class SequentialThinkingServer { } this.thoughtHistory.push(input); + this.pruneHistory(); if (input.branchFromThought && input.branchId) { if (!this.branches[input.branchId]) { this.branches[input.branchId] = []; } this.branches[input.branchId].push(input); + + // Prune branches too + if (this.branches[input.branchId].length > this.maxHistorySize) { + this.branches[input.branchId] = this.branches[input.branchId].slice(-this.maxHistorySize); + } } if (!this.disableThoughtLogging) {