/**
* Sequential Thinking MCP Server - Cloudflare Workers with Durable Objects
* Proper persistent state implementation for serverless deployment
*/
import { ThoughtInput, StoredThought } from './types.js';
import { validateThoughtInput, ValidationError } from './utils/validators.js';
// Environment bindings
export interface Env {
THINKING_SESSIONS: DurableObjectNamespace;
}
/**
* Durable Object for persistent session storage
*/
export class ThinkingSession implements DurableObject {
private state: DurableObjectState;
private sessions: Map<string, {
thoughts: StoredThought[];
branches: Map<string, StoredThought[]>;
createdAt: number;
updatedAt: number;
}>;
private currentSessionId: string;
constructor(state: DurableObjectState) {
this.state = state;
this.sessions = new Map();
this.currentSessionId = `session-${Date.now()}-${Math.random().toString(36).substring(7)}`;
}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
// CORS headers
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, x-session-id',
};
if (request.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
try {
// Initialize from storage on first request
if (this.sessions.size === 0) {
await this.loadFromStorage();
}
// Add thought
if (url.pathname === '/think' && request.method === 'POST') {
const input = await request.json() as ThoughtInput;
try {
validateThoughtInput(input);
} catch (error) {
return new Response(
JSON.stringify({
error: error instanceof Error ? error.message : 'Validation error',
code: 'VALIDATION_ERROR',
}),
{
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
}
);
}
const result = await this.addThought(input);
return new Response(JSON.stringify(result), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
// Get sequence
if (url.pathname === '/sequence' && request.method === 'GET') {
const session = this.sessions.get(this.currentSessionId);
return new Response(
JSON.stringify({
sessionId: this.currentSessionId,
thoughtCount: session?.thoughts.length || 0,
thoughts: session?.thoughts || [],
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
}
);
}
// Reset session
if (url.pathname === '/reset' && request.method === 'POST') {
this.currentSessionId = this.generateSessionId();
this.sessions.set(this.currentSessionId, {
thoughts: [],
branches: new Map(),
createdAt: Date.now(),
updatedAt: Date.now(),
});
await this.saveToStorage();
return new Response(
JSON.stringify({
message: 'New thinking session started',
sessionId: this.currentSessionId,
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
}
);
}
// Health check
if (url.pathname === '/health') {
return new Response(
JSON.stringify({
status: 'ok',
timestamp: Date.now(),
sessions: this.sessions.size,
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
}
);
}
// Info
if (url.pathname === '/' || url.pathname === '/info') {
return new Response(
JSON.stringify({
name: 'Sequential Thinking MCP Server (Cloudflare Workers + Durable Objects)',
version: '1.0.0',
description: 'Remote MCP server with persistent state',
endpoints: {
'POST /think': 'Add a thought to the sequence',
'GET /sequence': 'Get current thought sequence',
'POST /reset': 'Reset session',
'GET /health': 'Health check',
},
storage: 'Durable Objects (persistent)',
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
}
);
}
return new Response(
JSON.stringify({ error: 'Not found' }),
{
status: 404,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
}
);
} catch (error) {
console.error('Error:', error);
return new Response(
JSON.stringify({
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error',
}),
{
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
}
);
}
}
private generateSessionId(): string {
return `session-${Date.now()}-${Math.random().toString(36).substring(7)}`;
}
private async addThought(input: ThoughtInput) {
let session = this.sessions.get(this.currentSessionId);
if (!session) {
session = {
thoughts: [],
branches: new Map(),
createdAt: Date.now(),
updatedAt: Date.now(),
};
this.sessions.set(this.currentSessionId, session);
}
const storedThought: StoredThought = {
...input,
timestamp: Date.now(),
sessionId: this.currentSessionId,
};
if (input.branchId && input.branchFromThought !== undefined) {
let branchThoughts = session.branches.get(input.branchId);
if (!branchThoughts) {
branchThoughts = [];
session.branches.set(input.branchId, branchThoughts);
}
branchThoughts.push(storedThought);
} else {
session.thoughts.push(storedThought);
}
session.updatedAt = Date.now();
// Persist to Durable Storage
await this.saveToStorage();
let message = `Thought ${input.thoughtNumber}`;
if (input.isRevision && input.revisesThought !== undefined) {
message += ` (revising thought ${input.revisesThought})`;
}
if (input.branchId) {
message += ` [branch: ${input.branchId}]`;
}
message += `: ${input.thought}`;
return {
success: true,
thoughtNumber: input.thoughtNumber,
message,
sequence: session.thoughts,
totalThoughts: input.totalThoughts,
};
}
private async saveToStorage(): Promise<void> {
// Convert Map to serializable format
const data = {
currentSessionId: this.currentSessionId,
sessions: Array.from(this.sessions.entries()).map(([id, session]) => ({
id,
thoughts: session.thoughts,
branches: Array.from(session.branches.entries()),
createdAt: session.createdAt,
updatedAt: session.updatedAt,
})),
};
await this.state.storage.put('data', data);
}
private async loadFromStorage(): Promise<void> {
const data = await this.state.storage.get<{
currentSessionId: string;
sessions: Array<{
id: string;
thoughts: StoredThought[];
branches: Array<[string, StoredThought[]]>;
createdAt: number;
updatedAt: number;
}>;
}>('data');
if (data) {
this.currentSessionId = data.currentSessionId;
this.sessions = new Map(
data.sessions.map((session) => [
session.id,
{
thoughts: session.thoughts,
branches: new Map(session.branches),
createdAt: session.createdAt,
updatedAt: session.updatedAt,
},
])
);
}
}
}
/**
* Main worker - routes requests to Durable Objects
*/
export default {
async fetch(request: Request, env: Env): Promise<Response> {
try {
// Get or create a Durable Object instance
// Using a single instance for all requests (can be modified for multi-tenant)
const id = env.THINKING_SESSIONS.idFromName('default');
const stub = env.THINKING_SESSIONS.get(id);
// Forward request to the Durable Object
return await stub.fetch(request);
} catch (error) {
return new Response(
JSON.stringify({
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
);
}
},
};