Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions src/memory/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
213 changes: 181 additions & 32 deletions src/memory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Promise<void>>();

async acquire<T>(key: string, fn: () => Promise<T>): Promise<T> {
while (this.locks.has(key)) {
await this.locks.get(key);
}

let release: () => void;
const lock = new Promise<void>(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');

Expand Down Expand Up @@ -69,39 +94,47 @@ export class KnowledgeGraphManager {
constructor(private memoryFilePath: string) {}

private async loadGraph(): Promise<KnowledgeGraph> {
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<void> {
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<Entity[]> {
Expand Down Expand Up @@ -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<string, StreamableHTTPServerTransport> = {};
const SESSION_TIMEOUT = 30 * 60 * 1000; // 30 minutes
const MAX_REQUEST_SIZE = 10 * 1024 * 1024; // 10MB
const sessionTimers: Record<string, NodeJS.Timeout> = {};

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) => {
Expand Down
26 changes: 26 additions & 0 deletions src/sequentialthinking/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading