#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListPromptsRequestSchema,
GetPromptRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListResourceTemplatesRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
// Fixed chalk import for ESM
import chalk from "chalk";
import { z } from "zod";
import { PATTERNS_COOKBOOK } from "./resources/patterns-cookbook-content.js";
import { SERVER_ARCHITECTURE_GUIDE } from "./resources/server-architecture-content.js";
import { NotebookServer, NOTEBOOK_TOOL } from "./notebook/index.js";
import {
LIST_MCP_ASSETS_PROMPT,
getListMcpAssetsContent,
INTERLEAVED_THINKING_PROMPT,
getInterleavedThinkingContent,
getInterleavedResourceTemplates,
getInterleavedGuideForUri,
} from "./prompts/index.js";
// Configuration schema for Smithery
export const configSchema = z.object({
disableThoughtLogging: z
.boolean()
.optional()
.default(false)
.describe(
"Disable thought output to stderr (useful for production deployments)"
),
});
interface ThoughtData {
thought: string;
thoughtNumber: number;
totalThoughts: number;
isRevision?: boolean;
revisesThought?: number;
branchFromThought?: number;
branchId?: string;
needsMoreThoughts?: boolean;
includeGuide?: boolean;
nextThoughtNeeded: boolean;
}
class ClearThoughtServer {
private thoughtHistory: ThoughtData[] = [];
private branches: Record<string, ThoughtData[]> = {};
private disableThoughtLogging: boolean;
private patternsCookbook: string;
constructor(disableThoughtLogging: boolean = false) {
this.disableThoughtLogging = disableThoughtLogging;
// Use imported cookbook content (works for both STDIO and HTTP builds)
this.patternsCookbook = PATTERNS_COOKBOOK;
}
private validateThoughtData(input: unknown): ThoughtData {
const data = input as Record<string, unknown>;
if (!data.thought || typeof data.thought !== "string") {
throw new Error("Invalid thought: must be a string");
}
if (!data.thoughtNumber || typeof data.thoughtNumber !== "number") {
throw new Error("Invalid thoughtNumber: must be a number");
}
if (!data.totalThoughts || typeof data.totalThoughts !== "number") {
throw new Error("Invalid totalThoughts: must be a number");
}
if (typeof data.nextThoughtNeeded !== "boolean") {
throw new Error("Invalid nextThoughtNeeded: must be a boolean");
}
return {
thought: data.thought,
thoughtNumber: data.thoughtNumber,
totalThoughts: data.totalThoughts,
nextThoughtNeeded: data.nextThoughtNeeded,
isRevision: data.isRevision as boolean | undefined,
revisesThought: data.revisesThought as number | undefined,
branchFromThought: data.branchFromThought as number | undefined,
branchId: data.branchId as string | undefined,
needsMoreThoughts: data.needsMoreThoughts as boolean | undefined,
includeGuide: data.includeGuide as boolean | undefined,
};
}
private formatThought(thoughtData: ThoughtData): string {
const {
thoughtNumber,
totalThoughts,
thought,
isRevision,
revisesThought,
branchFromThought,
branchId,
} = thoughtData;
let prefix = "";
let context = "";
if (isRevision) {
prefix = chalk.yellow("π Revision");
context = ` (revising thought ${revisesThought})`;
} else if (branchFromThought) {
prefix = chalk.green("πΏ Branch");
context = ` (from thought ${branchFromThought}, ID: ${branchId})`;
} else {
prefix = chalk.blue("π Thought");
context = "";
}
const header = `${prefix} ${thoughtNumber}/${totalThoughts}${context}`;
const border = "β".repeat(Math.max(header.length, thought.length) + 4);
return `
β${border}β
β ${header} β
β${border}β€
β ${thought.padEnd(border.length - 2)} β
β${border}β`;
}
public processThought(input: unknown): {
content: Array<any>;
isError?: boolean;
} {
try {
const validatedInput = this.validateThoughtData(input);
if (validatedInput.thoughtNumber > validatedInput.totalThoughts) {
validatedInput.totalThoughts = validatedInput.thoughtNumber;
}
this.thoughtHistory.push(validatedInput);
if (validatedInput.branchFromThought && validatedInput.branchId) {
if (!this.branches[validatedInput.branchId]) {
this.branches[validatedInput.branchId] = [];
}
this.branches[validatedInput.branchId].push(validatedInput);
}
if (!this.disableThoughtLogging) {
const formattedThought = this.formatThought(validatedInput);
console.error(formattedThought);
}
// Build response content array
const content: Array<any> = [
{
type: "text",
text: JSON.stringify(
{
thoughtNumber: validatedInput.thoughtNumber,
totalThoughts: validatedInput.totalThoughts,
nextThoughtNeeded: validatedInput.nextThoughtNeeded,
branches: Object.keys(this.branches),
thoughtHistoryLength: this.thoughtHistory.length,
},
null,
2
),
},
];
// Include patterns cookbook as embedded resource when:
// 1. At the start (thoughtNumber === 1)
// 2. At the end (thoughtNumber === totalThoughts)
// 3. On-demand (includeGuide === true)
const shouldIncludeGuide =
validatedInput.thoughtNumber === 1 ||
validatedInput.thoughtNumber === validatedInput.totalThoughts ||
validatedInput.includeGuide === true;
if (shouldIncludeGuide) {
content.push({
type: "resource",
resource: {
uri: "thoughtbox://patterns-cookbook",
title: "Thoughtbox Patterns Cookbook",
mimeType: "text/markdown",
text: this.patternsCookbook,
annotations: {
audience: ["assistant"],
priority: 0.9,
},
},
});
}
return { content };
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: error instanceof Error ? error.message : String(error),
status: "failed",
},
null,
2
),
},
],
isError: true,
};
}
}
}
const CLEAR_THOUGHT_TOOL: Tool = {
name: "thoughtbox",
description: `Step-by-step thinking tool for complex problem-solving.
Supports flexible reasoning: forward thinking (1βN), backward thinking (Nβ1), branching, and revision.
Adjust your approach dynamically as understanding deepens.
Use for:
- Multi-step analysis and planning
- Problems requiring course correction
- Hypothesis generation and testing
- System design and architecture decisions
Patterns Cookbook:
Automatically provided at thought 1 with 6 core reasoning patterns, examples, and best practices.
Request anytime with includeGuide parameter.`,
inputSchema: {
type: "object",
properties: {
thought: {
type: "string",
description: "Your current thinking step",
},
nextThoughtNeeded: {
type: "boolean",
description: "Whether another thought step is needed",
},
thoughtNumber: {
type: "integer",
description:
"Current thought number (can be 1βN for forward thinking, or Nβ1 for backward/goal-driven thinking)",
minimum: 1,
},
totalThoughts: {
type: "integer",
description:
"Estimated total thoughts needed (for backward thinking, start with thoughtNumber = totalThoughts)",
minimum: 1,
},
isRevision: {
type: "boolean",
description: "Whether this revises previous thinking",
},
revisesThought: {
type: "integer",
description: "Which thought is being reconsidered",
minimum: 1,
},
branchFromThought: {
type: "integer",
description: "Branching point thought number",
minimum: 1,
},
branchId: {
type: "string",
description: "Branch identifier",
},
needsMoreThoughts: {
type: "boolean",
description: "If more thoughts are needed",
},
includeGuide: {
type: "boolean",
description:
"Request the patterns cookbook guide as embedded resource (also provided automatically at thought 1 and final thought)",
},
},
required: [
"thought",
"nextThoughtNeeded",
"thoughtNumber",
"totalThoughts",
],
},
annotations: {
audience: ["assistant"],
priority: 0.85,
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
},
};
// Exported server creation function for Smithery HTTP transport
export default function createServer({
config,
}: {
config: z.infer<typeof configSchema>;
}) {
const server = new Server(
{
name: "thoughtbox-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
prompts: {},
resources: {},
resourceTemplates: {},
},
}
);
const thinkingServer = new ClearThoughtServer(config.disableThoughtLogging);
const notebookServer = new NotebookServer();
// Note: NotebookServer uses lazy initialization - temp directories created on first use
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [CLEAR_THOUGHT_TOOL, NOTEBOOK_TOOL],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "thoughtbox") {
return thinkingServer.processThought(request.params.arguments);
}
// Handle notebook toolhost dispatcher
if (request.params.name === "notebook") {
const { operation, args } = request.params.arguments as any;
if (!operation || typeof operation !== "string") {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: false,
error: "operation parameter is required and must be a string",
},
null,
2
),
},
],
isError: true,
};
}
return notebookServer.processTool(operation, args || {});
}
return {
content: [
{
type: "text",
text: `Unknown tool: ${request.params.name}`,
},
],
isError: true,
};
});
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
prompts: [LIST_MCP_ASSETS_PROMPT, INTERLEAVED_THINKING_PROMPT],
}));
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
if (request.params.name === "list_mcp_assets") {
return {
description: LIST_MCP_ASSETS_PROMPT.description,
messages: [
{
role: "assistant",
content: {
type: "text",
text: getListMcpAssetsContent(),
},
},
],
};
}
if (request.params.name === "interleaved-thinking") {
// Extract arguments - MCP spec says arguments values are always strings
const args = request.params.arguments ?? {};
const task = args.task;
if (!task) {
throw new Error("Missing required argument 'task'");
}
// Convert string arguments to proper types
const thoughts_limit = args.thoughts_limit
? parseInt(args.thoughts_limit, 10)
: undefined;
const clear_folder = args.clear_folder === "true";
// Get the prompt content with variable substitution
const content = getInterleavedThinkingContent({
task: String(task),
thoughts_limit,
clear_folder,
});
return {
description: INTERLEAVED_THINKING_PROMPT.description,
messages: [
{
role: "user",
content: {
type: "text",
text: content,
},
},
],
};
}
throw new Error(`Unknown prompt: ${request.params.name}`);
});
// Resource handlers
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: "system://status",
name: "Notebook Server Status",
description: "Health snapshot of the notebook server",
mimeType: "application/json",
},
{
uri: "thoughtbox://notebook/operations",
name: "Notebook Operations Catalog",
description: "Complete catalog of notebook operations with schemas and examples",
mimeType: "application/json",
},
{
uri: "thoughtbox://patterns-cookbook",
name: "Thoughtbox Patterns Cookbook",
description: "Guide to core reasoning patterns for thoughtbox tool",
mimeType: "text/markdown",
},
{
uri: "thoughtbox://architecture",
name: "Server Architecture Guide",
description: "Interactive notebook explaining Thoughtbox MCP server architecture and implementation patterns",
mimeType: "text/markdown",
},
],
}));
// Resource template handlers
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
return getInterleavedResourceTemplates();
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
if (uri === "system://status") {
const status = notebookServer.getStatus();
return {
contents: [
{
uri,
mimeType: "application/json",
text: JSON.stringify(status, null, 2),
},
],
};
}
if (uri === "thoughtbox://notebook/operations") {
const catalog = notebookServer.getOperationsCatalog();
return {
contents: [
{
uri,
mimeType: "application/json",
text: catalog,
},
],
};
}
if (uri === "thoughtbox://patterns-cookbook") {
return {
contents: [
{
uri,
mimeType: "text/markdown",
text: PATTERNS_COOKBOOK,
},
],
};
}
if (uri === "thoughtbox://architecture") {
return {
contents: [
{
uri,
mimeType: "text/markdown",
text: SERVER_ARCHITECTURE_GUIDE,
},
],
};
}
// Handle interleaved thinking resource templates
if (uri.startsWith("thoughtbox://interleaved/")) {
try {
const resource = getInterleavedGuideForUri(uri);
return {
contents: [resource],
};
} catch (error) {
throw new Error(
error instanceof Error ? error.message : String(error)
);
}
}
throw new Error(`Unknown resource: ${uri}`);
});
return server;
}
// STDIO transport for backward compatibility
async function runServer() {
// Get configuration from environment variable (backward compatible)
const disableThoughtLogging =
(process.env.DISABLE_THOUGHT_LOGGING || "").toLowerCase() === "true";
// Create server using the exported function
const server = createServer({
config: {
disableThoughtLogging,
},
});
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Thoughtbox MCP Server running on stdio");
}
// Auto-run for STDIO usage (dist/index.js is never imported, only executed)
runServer().catch((error) => {
console.error("Fatal error running server:", error);
process.exit(1);
});