Skip to main content
Glama
index.ts31.6 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from "@modelcontextprotocol/sdk/types.js"; import { BranchManager } from './branchManager.js'; import { AutoExecutionPolicy, CommandSafetyValidator, WorkflowPlanner, AutoExecutionPolicyRule } from './autoExecution.js'; import { BranchingThoughtInput, VisualizationOptions } from './types.js'; import chalk from 'chalk'; enum SessionState { INIT = 'INIT', BRANCH_CREATED = 'BRANCH_CREATED', BRANCH_FOCUSED = 'BRANCH_FOCUSED', THOUGHT_ADDED = 'THOUGHT_ADDED', ACTIVE = 'ACTIVE', RESET = 'RESET', } class BranchingThoughtServer { private autoExecutionPolicy = new AutoExecutionPolicy({ rules: [ { type: 'add-thought', safe: true }, { type: 'focus', safe: true }, { type: 'create-branch', safe: true }, { type: 'semantic-search', safe: true }, { type: 'extract-tasks', safe: true }, // Add more as needed, user can modify at runtime ] }); private commandSafetyValidator = new CommandSafetyValidator(this.autoExecutionPolicy); private workflowPlanner = new WorkflowPlanner(); private sessionState: SessionState = SessionState.INIT; // Map session states to allowed commands private allowedCommands: Record<SessionState, string[]> = { [SessionState.INIT]: ['create-branch', 'list'], [SessionState.BRANCH_CREATED]: ['focus', 'list', 'create-branch'], [SessionState.BRANCH_FOCUSED]: [ 'add-thought', 'insights', 'crossrefs', 'hub-thoughts', 'semantic-search', 'link-thoughts', 'add-snippet', 'snippet-search', 'summarize-branch', 'doc-thought', 'extract-tasks', 'review-branch', 'visualize', 'ask', 'focus', 'list', 'create-branch', 'history', 'summarize-tasks', 'advance-task', 'assign-task' ], [SessionState.THOUGHT_ADDED]: [ 'insights', 'crossrefs', 'hub-thoughts', 'semantic-search', 'link-thoughts', 'add-snippet', 'snippet-search', 'summarize-branch', 'doc-thought', 'extract-tasks', 'review-branch', 'visualize', 'ask', 'focus', 'list', 'create-branch', 'history', 'summarize-tasks', 'advance-task', 'assign-task', 'add-thought' ], [SessionState.ACTIVE]: [ 'add-thought', 'insights', 'crossrefs', 'hub-thoughts', 'semantic-search', 'link-thoughts', 'add-snippet', 'snippet-search', 'summarize-branch', 'doc-thought', 'extract-tasks', 'review-branch', 'visualize', 'ask', 'focus', 'list', 'create-branch', 'history', 'summarize-tasks', 'advance-task', 'assign-task', 'reset-session', 'clear-cache', 'get-cache-stats' ], [SessionState.RESET]: ['create-branch', 'list'], }; private updateSessionState(commandType: string) { switch (commandType) { case 'create-branch': this.sessionState = SessionState.BRANCH_CREATED; break; case 'focus': this.sessionState = SessionState.BRANCH_FOCUSED; break; case 'add-thought': this.sessionState = SessionState.THOUGHT_ADDED; break; case 'reset-session': this.sessionState = SessionState.INIT; break; default: if ( this.sessionState === SessionState.THOUGHT_ADDED || this.sessionState === SessionState.BRANCH_FOCUSED ) { this.sessionState = SessionState.ACTIVE; } break; } } private branchManager = new BranchManager(); // Made async to allow awaiting handleCommand async processThought(input: unknown): Promise<{ content: Array<{ type: string; text: string }>; isError?: boolean }> { try { const inputData = input as any; // Handle commands if present if (inputData.command) { // Await handleCommand since it is now async return await this.handleCommand(inputData.command); } // Handle regular thought input (single or batch) let lastThought; let branch; if (Array.isArray(inputData)) { lastThought = this.branchManager.addThought(inputData); branch = this.branchManager.getBranch(lastThought.branchId)!; } else { const thoughtInput = input as BranchingThoughtInput; lastThought = this.branchManager.addThought(thoughtInput); branch = this.branchManager.getBranch(lastThought.branchId)!; } // Format the response with the branch status const formattedStatus = await this.branchManager.formatBranchStatus(branch); console.error(formattedStatus); // Display in the console return { content: [{ type: "text", text: JSON.stringify({ thoughtId: lastThought.id, branchId: lastThought.branchId, branchState: branch.state, branchPriority: branch.priority, numInsights: branch.insights.length, numCrossRefs: branch.crossRefs.length, activeBranch: this.branchManager.getActiveBranch()?.id }, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ error: error instanceof Error ? error.message : String(error), status: 'failed' }, null, 2) }] }; } } // Made async to allow await for new commands private async handleCommand(command: { type: string; branchId?: string; query?: string; topN?: number; fromThoughtId?: string; toThoughtId?: string; linkType?: string; reason?: string; tags?: string[]; author?: string; content?: string; thoughtId?: string; question?: string; status?: string; assignee?: string; due?: string; taskId?: string; parentBranchId?: string; rule?: AutoExecutionPolicyRule; // for policy management commands autoVisualize?: boolean; }): Promise<{ content: Array<{ type: string; text: string }> }> { console.error(`[CMD] Received command: ${command.type}`); try { // === Policy Management Commands === if (command.type === 'add-policy-rule' && command.rule) { this.autoExecutionPolicy.addRule(command.rule); return { content: [{ type: 'text', text: 'Policy rule added.' }] }; } if (command.type === 'remove-policy-rule' && command.rule) { this.autoExecutionPolicy.removeRule(command.rule.type, command.rule.pattern); return { content: [{ type: 'text', text: 'Policy rule removed.' }] }; } if (command.type === 'list-policy-rules') { return { content: [{ type: 'text', text: JSON.stringify(this.autoExecutionPolicy.listRules(), null, 2) }] }; } // === Multi-step Workflow Planning === const workflow = this.workflowPlanner.plan(command); let results: Array<{ type: string; text: string }> = []; for (const step of workflow) { // === Safety Validation === const isSafe = this.commandSafetyValidator.isSafe({ type: step.type, content: step.params.content }); if (!isSafe) { results.push({ type: 'text', text: `Command '${step.type}' is not marked safe for auto-execution. Skipping.` }); continue; } // Only execute if allowed in current state if (!this.allowedCommands[this.sessionState].includes(step.type)) { results.push({ type: 'text', text: `Command '${step.type}' is not allowed in the current session state (${this.sessionState}).` }); continue; } // Additional preconditions for branch-dependent commands const commandsRequiringBranch = [ 'add-thought', 'insights', 'crossrefs', 'hub-thoughts', 'semantic-search', 'link-thoughts', 'add-snippet', 'snippet-search', 'summarize-branch', 'doc-thought', 'extract-tasks', 'review-branch', 'visualize', 'ask', 'history', 'summarize-tasks', 'advance-task', 'assign-task' ]; if (commandsRequiringBranch.includes(step.type)) { const branchId = step.params.branchId || this.branchManager.getActiveBranch()?.id; if (!branchId || !this.branchManager.getBranch(branchId)) { results.push({ type: 'text', text: `Command '${step.type}' requires a valid branch. Please create and focus a branch first.` }); continue; } } // State transition after successful command this.updateSessionState(step.type); console.error(`[STATE] Session state is now: ${this.sessionState}`); // === Actual Command Execution (call original switch) === // Recurse for sub-commands or call switch for atomic ones const atomicResult = await this._executeAtomicCommand(step.type, step.params); if (atomicResult?.content) results = results.concat(atomicResult.content); } return { content: results }; } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ error: error instanceof Error ? error.message : String(error), status: 'failed' }, null, 2) }] }; } } // Extracted atomic command executor (original switch/case logic) private async _executeAtomicCommand(type: string, params: any): Promise<{ content: Array<{ type: string; text: string }> }> { switch (type) { case 'create-branch': { if (!params.branchId) throw new Error('branchId required for create-branch'); const branch = this.branchManager.createBranch(params.branchId, params.parentBranchId); const content: Array<{ type: string; text: string }> = [{ type: "text", text: `Created branch '${branch.id}'${params.parentBranchId ? ` with parent '${params.parentBranchId}'` : ''}.` }]; if (params.autoVisualize) { const options = { branchId: branch.id } as VisualizationOptions; const vizData = this.branchManager.visualizeBranch(options); content.push({ type: "text", text: JSON.stringify({ visualization: vizData }, null, 2) }); } return { content }; } case 'insights': { const branchId = params.branchId || this.branchManager.getActiveBranch()?.id; if (!branchId) { throw new Error('No active branch and no branchId provided'); } const insights = this.branchManager.getCachedInsights(branchId); return { content: [{ type: "text", text: JSON.stringify({ branchId, insights }, null, 2) }] }; } case 'list': { const branches = this.branchManager.getAllBranches(); const activeBranchId = this.branchManager.getActiveBranch()?.id; const output = branches.map(b => { const isActive = b.id === activeBranchId; const prefix = isActive ? chalk.green('→') : ' '; return `${prefix} ${b.id} [${b.state}] - ${b.thoughts[b.thoughts.length - 1]?.content.slice(0, 50)}...`; }).join('\n'); return { content: [{ type: "text", text: `Current Branches:\n${output}` }] }; } case 'focus': { if (!params.branchId) { throw new Error('branchId required for focus command'); } this.branchManager.setActiveBranch(params.branchId); const branch = this.branchManager.getBranch(params.branchId)!; const formattedStatus = await this.branchManager.formatBranchStatus(branch); console.error(formattedStatus); return { content: [{ type: "text", text: JSON.stringify({ status: 'success', message: `Now focused on branch: ${params.branchId}`, activeBranch: params.branchId }, null, 2) }] }; } case 'history': { const branchId = params.branchId || this.branchManager.getActiveBranch()?.id; if (!branchId) { throw new Error('No active branch and no branchId provided'); } const history = await this.branchManager.getBranchHistory(branchId); return { content: [{ type: "text", text: history }] }; } case 'crossrefs': { const branchId = params.branchId || this.branchManager.getActiveBranch()?.id; if (!branchId) throw new Error('No active branch and no branchId provided'); const branch = this.branchManager.getBranch(branchId)!; const crossRefs = branch.crossRefs || []; return { content: [{ type: "text", text: JSON.stringify({ branchId, crossReferences: crossRefs }, null, 2) }] }; } case 'hub-thoughts': { // List thoughts with the highest cross-branch connections/scores const branchId = params.branchId || this.branchManager.getActiveBranch()?.id; if (!branchId) throw new Error('No active branch and no branchId provided'); const branch = this.branchManager.getBranch(branchId)!; const thoughts = branch.thoughts || []; // Sort by number of crossRefs and score const sorted = [...thoughts].sort((a, b) => { const aScore = (a.crossRefs?.length || 0) + (typeof a.score === 'number' ? a.score : 0); const bScore = (b.crossRefs?.length || 0) + (typeof b.score === 'number' ? b.score : 0); return bScore - aScore; }); return { content: [{ type: "text", text: JSON.stringify({ branchId, hubThoughts: sorted.slice(0, 10) }, null, 2) }] }; } case 'semantic-search': { if (!('query' in params) || typeof params.query !== 'string') { throw new Error('semantic-search requires a query string'); } const query = params.query; const topN = typeof params.topN === 'number' ? params.topN : 5; // Perform semantic search const results = await this.branchManager.semanticSearch(query, topN); return { content: [{ type: "text", text: JSON.stringify({ query, topN, results }, null, 2) }] }; } case 'link-thoughts': { if (!('fromThoughtId' in params) || !('toThoughtId' in params) || !('linkType' in params)) { throw new Error('link-thoughts requires fromThoughtId, toThoughtId, and linkType'); } const { fromThoughtId, toThoughtId, linkType, reason } = params; const validLinkType = linkType as 'supports' | 'contradicts' | 'related' | 'expands' | 'refines'; const success = this.branchManager.linkThoughts(fromThoughtId!, toThoughtId!, validLinkType, reason); return { content: [{ type: "text", text: JSON.stringify({ status: success ? 'linked' : 'failed', fromThoughtId, toThoughtId, linkType, reason }, null, 2) }] }; } case 'add-snippet': { if (!('content' in params) || typeof params.content !== 'string' || !('tags' in params) || !Array.isArray(params.tags)) throw new Error('add-snippet requires content (string) and tags (array)'); const snippet = this.branchManager.addSnippet(params.content, params.tags, typeof params.author === 'string' ? params.author : undefined); return { content: [{ type: "text", text: JSON.stringify(snippet, null, 2) }] }; } case 'snippet-search': { if (!('query' in params) || typeof params.query !== 'string') throw new Error('snippet-search requires a query string'); const results = this.branchManager.searchSnippets(params.query, typeof params.topN === 'number' ? params.topN : 5); return { content: [{ type: "text", text: JSON.stringify({ results }, null, 2) }] }; } case 'summarize-branch': { const branchId = params.branchId || this.branchManager.getActiveBranch()?.id; if (!branchId) throw new Error('No branchId provided and no active branch.'); return { content: [{ type: "text", text: await this.branchManager.summarizeBranch(branchId) }] }; } case 'doc-thought': { if (!('thoughtId' in params) || typeof params.thoughtId !== 'string') throw new Error('doc-thought requires a thoughtId (string)'); return { content: [{ type: "text", text: await this.branchManager.summarizeThought(params.thoughtId) }] }; } case 'extract-tasks': { const branchId = params.branchId || this.branchManager.getActiveBranch()?.id; const tasks = await this.branchManager.extractTasks(branchId); return { content: [{ type: "text", text: JSON.stringify({ branchId, tasks }, null, 2) }] }; } case 'list-tasks': { const branchId = params.branchId || this.branchManager.getActiveBranch()?.id; const status = params.status; const assignee = params.assignee; const due = params.due; const tasks = await this.branchManager.queryTasks({ branchId, status, assignee, due }); return { content: [{ type: "text", text: JSON.stringify({ branchId, status, assignee, due, tasks }, null, 2) }] }; } case 'update-task-status': { const taskId = params.taskId; const status = params.status; if (!taskId || !status) throw new Error('update-task-status requires taskId and status'); const updated = await this.branchManager.updateTaskStatus(taskId, status as 'open' | 'in_progress' | 'closed'); return { content: [{ type: "text", text: updated ? `Task ${taskId} updated to status ${status}` : `Task ${taskId} not updated (stateless mode)` }] }; } case 'summarize-tasks': { const branchId = params.branchId || this.branchManager.getActiveBranch()?.id; const summary = await this.branchManager.summarizeTasks(branchId); return { content: [{ type: "text", text: summary }] }; } case 'review-branch': { const branchId = params.branchId || this.branchManager.getActiveBranch()?.id; if (!branchId) throw new Error('No branchId provided and no active branch.'); const reviews = await this.branchManager.reviewBranch(branchId); return { content: [{ type: "text", text: JSON.stringify({ branchId, reviews }, null, 2) }] }; } case 'visualize': { // Pass full visualization options const options = params as VisualizationOptions; const data = this.branchManager.visualizeBranch(options); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } case 'ask': { if (!('question' in params) || typeof params.question !== 'string') throw new Error('ask requires a question string'); const question = params.question; const answer = await this.branchManager.askQuestion(question); return { content: [{ type: "text", text: answer }] }; } case 'reset-session': { // already transitioned return { content: [{ type: "text", text: 'Session has been reset.' }] }; } case 'clear-cache': { this.branchManager.clearCache(); return { content: [{ type: "text", text: 'Cache cleared.' }] }; } case 'get-cache-stats': { const stats = this.branchManager.getCacheStats(); return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }] }; } } // Default return for unknown command types return { content: [{ type: "text", text: `Unknown command type: ${type}` }] }; } } const BRANCHING_THOUGHT_TOOL: Tool = { name: "branch-thinking", description: ` # Branch-Thinking Tool **Purpose:** Use branching commands to create, navigate, and analyze thought branches and tasks. **Usage:** Provide a JSON payload with 'type' and relevant parameters in 'args' object. The tool returns an array of items in the format { type: string, text: string }. **Supported Commands:** - create-branch: { type: 'create-branch', branchId } - focus: { type: 'focus', branchId } - add-thought: { type: 'add-thought', branchId, content } - semantic-search: { type: 'semantic-search', query, topN? } - extract-tasks: { type: 'extract-tasks', branchId? } - visualize: { type: 'visualize', branchId?, options? } - list-branches: { type: 'list-branches' } - history: { type: 'history', branchId } - insights: { type: 'insights', branchId } - crossrefs: { type: 'crossrefs', branchId } - hub-thoughts: { type: 'hub-thoughts', branchId } - link-thoughts: { type: 'link-thoughts', fromThoughtId, toThoughtId, linkType, reason? } - add-snippet: { type: 'add-snippet', content, tags, author? } - snippet-search: { type: 'snippet-search', query, topN? } - summarize-branch: { type: 'summarize-branch', branchId? } - doc-thought: { type: 'doc-thought', thoughtId } - review-branch: { type: 'review-branch', branchId? } - ask: { type: 'ask', question } - summarize-tasks: { type: 'summarize-tasks', branchId? } - advance-task: { type: 'advance-task', taskId, status } - assign-task: { type: 'assign-task', taskId, assignee } - reset-session: { type: 'reset-session' } - clear-cache: { type: 'clear-cache' } - get-cache-stats: { type: 'get-cache-stats' } **Visualization Options:** - clustering: { type: 'clustering', algorithm? } - centrality: { type: 'centrality', metric? } - overlays: { type: 'overlays', features? } - analytics: { type: 'analytics', metrics? } **Example Calls and Expected Responses:** ~~~json // Add a thought { "name": "branch-thinking", "args": { "type": "add-thought", "branchId": "research", "content": "Define MCP best practices" } } // → [{"type":"text","text":"Thought added to branch research."}] ~~~ ~~~json // Get insights { "name": "branch-thinking", "args": { "type": "insights", "branchId": "research" } } // → [{"type":"text","text":"Insights for branch research: ['Best practices cluster around workflow safety and semantic search.', 'Cross-references indicate high reuse of planning patterns.']"}] ~~~ ~~~json // Get cross-references { "name": "branch-thinking", "args": { "type": "crossrefs", "branchId": "research" } } // → [{"type":"text","text":"Cross-references for branch research: [{ from: 't1', to: 't3', type: 'supports', reason: 't1 evidence for t3' }, { from: 't2', to: 't4', type: 'related' }]"}] ~~~ ~~~json // Extract tasks { "name": "branch-thinking", "args": { "type": "extract-tasks", "branchId": "research" } } // → [{"type":"text","text":"Tasks extracted: [{ id: 'task-123', content: 'Document MCP safety rules', status: 'open' }]"}] ~~~ ~~~json // Summarize tasks { "name": "branch-thinking", "args": { "type": "summarize-tasks", "branchId": "research" } } // → [{"type":"text","text":"Task summary: 1 open, 2 in progress, 0 closed."}] ~~~ ~~~json // Advance a task { "name": "branch-thinking", "args": { "type": "advance-task", "taskId": "task-123", "status": "in_progress" } } // → [{"type":"text","text":"Task task-123 status updated to in_progress."}] ~~~ ~~~json // Assign a task { "name": "branch-thinking", "args": { "type": "assign-task", "taskId": "task-123", "assignee": "alice" } } // → [{"type":"text","text":"Task task-123 assigned to alice."}] ~~~ ~~~json // Semantic search { "name": "branch-thinking", "args": { "type": "semantic-search", "query": "workflow planning", "topN": 3 } } // → [{"type":"text","text":"Top 3 semantic matches for 'workflow planning' returned."}] ~~~ ~~~json // Link thoughts { "name": "branch-thinking", "args": { "type": "link-thoughts", "fromThoughtId": "t1", "toThoughtId": "t2", "linkType": "supports" } } // → [{"type":"text","text":"Linked thought t1 to t2 as 'supports'."}] ~~~ ~~~json // Summarize branch { "name": "branch-thinking", "args": { "type": "summarize-branch", "branchId": "research" } } // → [{"type":"text","text":"Summary for branch research: ..."}] ~~~ ~~~json // Review branch { "name": "branch-thinking", "args": { "type": "review-branch", "branchId": "research" } } // → [{"type":"text","text":"Branch research reviewed. 2 suggestions found."}] ~~~ `, inputSchema: { type: "object", properties: { content: { oneOf: [ { type: "string", description: "Single thought content as a string." }, { type: "array", items: { type: "object" }, description: "Batch mode: array of thought objects, each with content, branchId, and optional metadata." } ], description: "Thought content (string) or batch of thoughts (array of objects)." }, branchId: { type: "string", description: "Branch ID to associate with the thought(s). If omitted, a new branch may be created or the active branch used." }, parentBranchId: { type: "string", description: "Optional: ID of the parent branch for hierarchical organization." }, type: { type: "string", description: "Thought type: e.g., 'analysis', 'hypothesis', 'observation', 'task', etc. Used for filtering and scoring." }, confidence: { type: "number", description: "Optional: Confidence score (0-1) for the thought, for ranking or filtering." }, keyPoints: { type: "array", items: { type: "string" }, description: "Optional: Key points or highlights extracted from the thought." }, relatedInsights: { type: "array", items: { type: "string" }, description: "Optional: IDs of related insights, for semantic linking." }, crossRefs: { type: "array", items: { type: "object", properties: { toBranch: { type: "string", description: "Target branch ID for the cross-reference." }, type: { type: "string", description: "Type of cross-reference (e.g., 'related', 'supports', 'contradicts', etc.)." }, reason: { type: "string", description: "Optional: Reason or context for the cross-reference." }, strength: { type: "number", description: "Optional: Numeric strength/confidence of the cross-reference (0-1)." } }, required: ["toBranch", "type"] }, description: "Optional: Array of cross-references to other branches, with type, reason, and strength." }, command: { type: "object", description: "Optional: Navigation or workflow command. Used for agentic/AI interactions.", properties: { type: { type: "string", enum: ["create-branch","list","focus","history","insights","crossrefs","hub-thoughts","semantic-search","link-thoughts","add-snippet","snippet-search","summarize-branch","doc-thought","extract-tasks","review-branch","visualize","ask"], description: "Command type (see tool description for complete list and semantics)." }, branchId: { type: "string", description: "Branch ID for commands that operate on a specific branch." }, tags: { type: "array", items: { type: "string" }, description: "Tags for add-snippet or snippet-search commands." }, author: { type: "string", description: "Optional: Author or agent for add-snippet command." }, content: { type: "string", description: "Content for add-snippet, search, or ask commands." }, thoughtId: { type: "string", description: "Thought ID for doc-thought, link-thoughts, or review commands." }, question: { type: "string", description: "Free-form question for the ask command (AI/LLM query)." }, query: { type: "string", description: "Query string for semantic-search, snippet-search, or other search commands." }, topN: { type: "number", description: "Number of top results to return for semantic-search or snippet-search." }, fromThoughtId: { type: "string", description: "Source thought ID for link-thoughts or cross-linking commands." }, toThoughtId: { type: "string", description: "Target thought ID for link-thoughts or cross-linking commands." }, linkType: { type: "string", description: "Type of link for link-thoughts command (e.g., supports, contradicts, related, expands, refines)." }, reason: { type: "string", description: "Optional: Reason or context for linking thoughts." }, parentBranchId: { type: "string", description: "Optional: Parent branch ID for hierarchical organization when creating a branch." }, autoVisualize: { type: "boolean", description: "Optional: Automatically visualize the branch after creation." }, }, required: ["type"] } }, anyOf: [ { required: ["content", "type"] }, { required: ["command"] } ] } }; const server = new Server( { name: "branch-thinking-server", version: "0.1.2", }, { capabilities: { tools: {}, }, } ); const thinkingServer = new BranchingThoughtServer(); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [BRANCHING_THOUGHT_TOOL], })); server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === "branch-thinking") { return thinkingServer.processThought(request.params.arguments); } return { content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }], isError: true }; }); async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Branch Thinking MCP Server running on stdio"); } runServer().catch((error) => { console.error("Fatal error running server:", error); process.exit(1); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ssdeanx/branch-thinking-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server