Connecting Claude to Tinderbox using MCP

Hi everyone,

I’ve been a passive participant in this forum so far, but since there’s been a lot of discussion about connecting Tinderbox to Large Language Models, I thought I’d share something as well. I’ve been experimenting with this from a different angle: Rather than bringing AI functionality into Tinderbox, I gave the Claude Desktop app access to Tinderbox using the Model Context Protocol (MCP) recently developed by Anthropic.

In the example above I moved the notes around a bit so that they can be seen better. But Claude is actually capable of moving notes on the map when given some information about the relevant attributes. Here I gave it an image of a handwritten sketch and asked it to recreate it on a map:

Although this is not perfect (there are several mistakes in the diagram), I think it’s still impressive. Claude can also play the role of a research assistant with the ability to take a sequence of actions, based on its own decisions about which tools to use. In the example below, Claude was given general instructions and told the locations of some existing notes in Tinderbox as well as a folder with sources in the filesystem (which is another MCP integration). It then retrieved the list of notes in that container, decided which of them to look at more closely, decided which of the files seem most relevant and finally created more notes.

In the background this is using a limited number of applescripts that Claude can call and pass arguments to. Currently these are “create note”, “link notes”, “get children”, “get siblings” and “update attribute”. It’s all adapted from an MCP server that someone else made mostly for interacting with system apps and settings (GitHub - joshrutkowski/applescript-mcp: A macOS AppleScript MCP server). Unfortunately I had to make a few changes to the code and setting up MCP servers is still a bit fiddly anyway, so it wouldn’t be practical to explain the whole process in the forum. But if people are interested, I could look into adding my Tinderbox-adapted version to Github (which I’ve never done before).

As far as I know, MCP servers can also be used with the free version of Claude and technically also with third-party LLM clients that use the OpenAI API or local models.

10 Likes

This is crazy cool, @pkus! Thank you for sharing. When I get a free hot minute, I’m going to give this a whirl.

I just discovered MCP two days ago and have immersed myself in YouTube tutorials.
This advancement in information management with potential to interact with my Tinderbox zettelkasten is awesome.
For my use case, I hope there is a way to wire-up the connections to help me with the processing of atomic notes/highlights from various sources.
I have attributes for each atomic note for zDeclarative Sentence, zIndexTerm, zAuthor, zPerson, zKeyword along with other meta data.
Currently, I have to cut and paste the $Text of the atomic note into Claude with a prompt to analyze that $Text and return to me the values for those attributes.
After reading a book resulting in several hundred atomic notes, it is a very laborious process.
But this new technology of having Claude interact with that note in Tinderbox, via MCP, and populating those attributes would be awesome.
Once those attributes are populated, the further processing of that note is easily done with existing stamps within Tinderbox.
I would be very grateful for a tutorial session on an upcoming Tinderbox Meetup addressing this capability.
Thankyou, @pkus for alerti

1 Like

I am very interested indeed in this! I will be tied up, at least intermittently, for the next two weeks, but would be happy to add whatever infrastructure is needed for this work after then.

3 Likes

Good grief. That drawing transformation is incredible. I have been manually recreating liquidtext note map exports for years; having the machine do this for me would be transformative. It’s great to seer something useful coming out of AI, superb work @pkus!

@kdjamesrd What you’re describing is definitely possible even with that initial version, although I need to improve the update_attribute tool so that it can change multiple attributes of a note at once. I’ll try to publish this server soon.

@eastgate That’s great to hear! There’s actually not much needed on the Tinderbox side. The main thing I’m missing is more ways to query Tinderbox with AppleScript, like “find all notes where the name contains …” or “all notes with the prototype …”. This might just be my limited knowledge of scripting Tinderbox though, I know you can do a lot with ‘evaluate’.

This is great!!! Would you be willing to demonstrate this on one of our weekly meetups? If co, please check out the calendar and pick a date that works for you: Tinderbox Meetup Calendar (2024, 2025)

Michael
5Cs School (next 5Cs Mastering Tinderbox 6-Week Cohort kicks off April 11th).

1 Like

Yes, I can do that. April 6 works for me if that’s still free. I should be able to make this available in some form by then.

2 Likes

If you have a target container, then you can do something like this:

set theNotes to notes in note "MyContainerName" whose value of attribute "Name" contains "MySearchString"
set targetNotes to notes in note "MyContainerName" whose value of attribute "Prototype" is "MyPrototypeName"

This looks in that container only, not in containers that it contains. If you need to look in multiple levels at once then things start getting more complicated and evaluate may be more efficient.

2 Likes

Fantastic. Let’s lock you in. :slight_smile:

Michael
5Cs School (next 5Cs Mastering Tinderbox 6-Week Cohort kicks off April 11th).

I’ve now created a repository for the MCP server: tinderbox-mcp

In the end it was more practical to create a new server from scratch instead of changing the existing AppleScript server mentioned in my first post. Disclaimer: Much of the actual code was written by Claude. I think I understand it, but I’m a historian, not a programmer, so I can’t say if it’s “good” code. Feedback is welcome, also for the applescripts.

A note on installation: The instructions in the Readme assume that you have a Github account the Github CLI installed. You could also manually download the repository from the site and then use cd /path/to/downloaded/folder instead of cloning the repository. This makes it a little more difficult to install updated versions though.

An easy way to get to the Claude config file that you need to edit before being able to use an MCP server is through Settings → Developer → Edit config in the Claude Desktop app.

You can get Node from Node.js — Download Node.js®.

Edit: Just after posting this, I notice there will be an issue for some of you. I’m still using Tinderbox 9, and so this is what the applescripts are calling. You’d have to update the tell application "Tinderbox 9" commands at the beginning of the scripts to fit your version. Or is there a way to address Tinderbox in a version-agnostic way, like the tell application id "DNtp" for DEVONthink?

Edit2: Found the answer on the forum, the scripts should now work regardless of the version.

2 Likes

Thanks so much for sharing this.

The application id of Tinderbox is “Cere”.
This is explained in detail in the following post thread.

1 Like

The code name for pre-release Tinderbox was “Ceres”.

I have been able to create an MCP server using home-brew, python, Claude and lots of patience

First I will upload the test server and the actual server and then install guide. I will attach quick reference to new post
Test server

!/opt/homebrew/bin/python3
"""Minimal test MCP server to verify basic functionality"""

import asyncio
import sys
from mcp.server import Server
from mcp.server.stdio import stdio_server

print("Test MCP server starting...", file=sys.stderr)

# Create server
server = Server("test-tinderbox")

# Define a simple tool handler
@server.list_tools()
async def list_tools():
    print("list_tools called!", file=sys.stderr)
    return [
        {
            "name": "test_tool",
            "description": "A simple test tool",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "message": {"type": "string", "description": "Test message"}
                },
                "required": ["message"]
            }
        }
    ]

@server.call_tool()
async def call_tool(name: str, arguments: dict):
    print(f"call_tool called with {name}: {arguments}", file=sys.stderr)
    if name == "test_tool":
        return [{"type": "text", "text": f"Test response: {arguments.get('message', 'no message')}"}]
    return [{"type": "text", "text": f"Unknown tool: {name}"}]

# Run the server
async def main():
    print("Starting server main loop...", file=sys.stderr)
    async with stdio_server() as (read, write):
        print("Stdio streams ready", file=sys.stderr)
        await server.run(read, write, server.create_initialization_options())

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\nServer stopped", file=sys.stderr)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        import traceback
        traceback.print_exc(file=sys.stderr)

MCP Server

#!/opt/homebrew/bin/python3
"""
Tinderbox MCP Server - Full Read/Write Access
A Model Context Protocol server for Tinderbox 10 on macOS
Compatible with MCP 1.9.1
"""

import asyncio
import json
import subprocess
import os
import sys
from typing import Any, Dict, List, Optional
from datetime import datetime

# Debug output
print("Starting Tinderbox MCP Server...", file=sys.stderr)
print(f"Python: {sys.executable}", file=sys.stderr)
print(f"Version: {sys.version}", file=sys.stderr)

# MCP SDK imports
try:
    from mcp.server import Server
    from mcp.server.stdio import stdio_server
    print("MCP SDK imported successfully", file=sys.stderr)
except ImportError as e:
    print(f"ERROR: MCP SDK import failed. {e}", file=sys.stderr)
    print("Please run: pip install mcp", file=sys.stderr)
    sys.exit(1)

# Create server instance with proper initialization
server = Server("tinderbox-mcp")

# Store tools list for reuse
TOOLS_LIST = [
    {
        "name": "get_note",
        "description": "Get content and attributes of a specific note by path or ID",
        "inputSchema": {
            "type": "object",
            "properties": {
                "note_path": {"type": "string", "description": "Path to note (e.g., '/Container/Note Name') or note ID"},
                "document": {"type": "string", "description": "Document name (optional, uses frontmost if not specified)"}
            },
            "required": ["note_path"]
        }
    },
    {
        "name": "search_notes",
        "description": "Search for notes containing specific text",
        "inputSchema": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "Search text"},
                "in_text": {"type": "boolean", "description": "Search in note text (default: true)", "default": True},
                "in_names": {"type": "boolean", "description": "Search in note names (default: true)", "default": True},
                "document": {"type": "string", "description": "Document name (optional)"}
            },
            "required": ["query"]
        }
    },
    {
        "name": "list_notes",
        "description": "List all notes in a container or document",
        "inputSchema": {
            "type": "object",
            "properties": {
                "container_path": {"type": "string", "description": "Path to container (empty for root)"},
                "recursive": {"type": "boolean", "description": "Include notes in subcontainers (default: false)", "default": False},
                "document": {"type": "string", "description": "Document name (optional)"}
            }
        }
    },
    {
        "name": "create_note",
        "description": "Create a new note in Tinderbox",
        "inputSchema": {
            "type": "object",
            "properties": {
                "name": {"type": "string", "description": "Note name"},
                "text": {"type": "string", "description": "Note text content"},
                "container_path": {"type": "string", "description": "Path to container (empty for root)"},
                "attributes": {"type": "object", "description": "Additional attributes as key-value pairs"},
                "document": {"type": "string", "description": "Document name (optional)"}
            },
            "required": ["name"]
        }
    },
    {
        "name": "update_note",
        "description": "Update an existing note's content or attributes",
        "inputSchema": {
            "type": "object",
            "properties": {
                "note_path": {"type": "string", "description": "Path to note or note ID"},
                "text": {"type": "string", "description": "New text content (optional)"},
                "attributes": {"type": "object", "description": "Attributes to update as key-value pairs"},
                "document": {"type": "string", "description": "Document name (optional)"}
            },
            "required": ["note_path"]
        }
    },
    {
        "name": "delete_note",
        "description": "Delete a note from Tinderbox",
        "inputSchema": {
            "type": "object",
            "properties": {
                "note_path": {"type": "string", "description": "Path to note or note ID"},
                "document": {"type": "string", "description": "Document name (optional)"}
            },
            "required": ["note_path"]
        }
    },
    {
        "name": "create_link",
        "description": "Create a link between two notes",
        "inputSchema": {
            "type": "object",
            "properties": {
                "from_note": {"type": "string", "description": "Path or ID of source note"},
                "to_note": {"type": "string", "description": "Path or ID of destination note"},
                "link_type": {"type": "string", "description": "Type of link (optional)"},
                "document": {"type": "string", "description": "Document name (optional)"}
            },
            "required": ["from_note", "to_note"]
        }
    },
    {
        "name": "run_agent",
        "description": "Execute a Tinderbox agent",
        "inputSchema": {
            "type": "object",
            "properties": {
                "agent_path": {"type": "string", "description": "Path to agent"},
                "document": {"type": "string", "description": "Document name (optional)"}
            },
            "required": ["agent_path"]
        }
    }
]

class TinderboxHelper:
    """Helper class for Tinderbox AppleScript operations"""
    
    @staticmethod
    def run_applescript(script: str) -> str:
        """Execute AppleScript and return result"""
        try:
            result = subprocess.run(
                ['osascript', '-e', script],
                capture_output=True,
                text=True,
                check=True
            )
            return result.stdout.strip()
        except subprocess.CalledProcessError as e:
            raise Exception(f"AppleScript error: {e.stderr}")

    @staticmethod
    def escape_quotes(text: str) -> str:
        """Escape quotes for AppleScript"""
        return text.replace('"', '\\"').replace('\n', '\\n')

# Set up handlers using decorator style
print("Registering handlers with decorators...", file=sys.stderr)

@server.list_tools()
async def handle_list_tools():
    """Return list of available tools"""
    print("handle_list_tools called", file=sys.stderr)
    return TOOLS_LIST  # Return just the list, not wrapped in dict

@server.call_tool()
async def handle_call_tool(name: str, arguments: Dict[str, Any] = None) -> List[Dict[str, Any]]:
    """Handle tool calls"""
    print(f"Tool called: {name} with args: {arguments}", file=sys.stderr)
    
    if arguments is None:
        arguments = {}
    
    if name == "get_note":
        return await get_note(**arguments)
    elif name == "search_notes":
        return await search_notes(**arguments)
    elif name == "list_notes":
        return await list_notes(**arguments)
    elif name == "create_note":
        return await create_note(**arguments)
    elif name == "update_note":
        return await update_note(**arguments)
    elif name == "delete_note":
        return await delete_note(**arguments)
    elif name == "create_link":
        return await create_link(**arguments)
    elif name == "run_agent":
        return await run_agent(**arguments)
    else:
        return [{"type": "text", "text": f"Unknown tool: {name}"}]

# Tool implementations
async def get_note(note_path: str, document: Optional[str] = None) -> List[Dict[str, Any]]:
    """Get note content and attributes"""
    try:
        doc_specifier = f'document "{document}"' if document else 'front document'
        
        script = f'''
        tell application "Tinderbox 10"
            tell {doc_specifier}
                set theNote to note "{note_path}"
                set noteText to value of attribute "Text" of theNote
                set noteName to value of attribute "Name" of theNote
                set noteID to value of attribute "ID" of theNote
                set notePath to value of attribute "Path" of theNote
                set noteModified to value of attribute "Modified" of theNote
                set noteCreated to value of attribute "Created" of theNote
                
                return "{{" & ¬
                    "\\"name\\": \\"" & noteName & "\\"," & ¬
                    "\\"id\\": \\"" & noteID & "\\"," & ¬
                    "\\"path\\": \\"" & notePath & "\\"," & ¬
                    "\\"text\\": \\"" & noteText & "\\"," & ¬
                    "\\"created\\": \\"" & noteCreated & "\\"," & ¬
                    "\\"modified\\": \\"" & noteModified & "\\"" & ¬
                    "}}"
            end tell
        end tell
        '''
        
        result = TinderboxHelper.run_applescript(script)
        return [{"type": "text", "text": result}]
        
    except Exception as e:
        return [{"type": "text", "text": f"Error: {str(e)}"}]

async def search_notes(query: str, in_text: bool = True, 
                      in_names: bool = True, document: Optional[str] = None) -> List[Dict[str, Any]]:
    """Search for notes containing query text"""
    try:
        doc_specifier = f'document "{document}"' if document else 'front document'
        
        script = f'''
        tell application "Tinderbox 10"
            tell {doc_specifier}
                set foundNotes to {{}}
                set allNotes to notes
                
                repeat with aNote in allNotes
                    set noteText to value of attribute "Text" of aNote
                    set noteName to value of attribute "Name" of aNote
                    set notePath to value of attribute "Path" of aNote
                    
                    set foundInText to false
                    set foundInName to false
                    
                    if "{query}" is in noteText then set foundInText to true
                    if "{query}" is in noteName then set foundInName to true
                    
                    if (foundInText and {str(in_text).lower()}) or (foundInName and {str(in_names).lower()}) then
                        set end of foundNotes to "\\"" & notePath & "\\""
                    end if
                end repeat
                
                return "[" & (foundNotes as string) & "]"
            end tell
        end tell
        '''
        
        result = TinderboxHelper.run_applescript(script)
        return [{"type": "text", "text": f"Found notes: {result}"}]
        
    except Exception as e:
        return [{"type": "text", "text": f"Error: {str(e)}"}]

async def list_notes(container_path: str = "", recursive: bool = False, 
                    document: Optional[str] = None) -> List[Dict[str, Any]]:
    """List notes in a container"""
    try:
        doc_specifier = f'document "{document}"' if document else 'front document'
        container_spec = f'note "{container_path}"' if container_path else 'it'
        
        script = f'''
        tell application "Tinderbox 10"
            tell {doc_specifier}
                set noteList to {{}}
                if "{container_path}" is "" then
                    set containerNotes to notes
                else
                    set containerNotes to notes of {container_spec}
                end if
                
                repeat with aNote in containerNotes
                    set noteName to value of attribute "Name" of aNote
                    set notePath to value of attribute "Path" of aNote
                    set noteID to value of attribute "ID" of aNote
                    set end of noteList to "{{\\"name\\": \\"" & noteName & "\\", \\"path\\": \\"" & notePath & "\\", \\"id\\": \\"" & noteID & "\\"}}"
                end repeat
                
                return "[" & (my listToString(noteList, ", ")) & "]"
            end tell
        end tell
        
        on listToString(lst, delim)
            set AppleScript's text item delimiters to delim
            set str to lst as string
            set AppleScript's text item delimiters to ""
            return str
        end listToString
        '''
        
        result = TinderboxHelper.run_applescript(script)
        return [{"type": "text", "text": result}]
        
    except Exception as e:
        return [{"type": "text", "text": f"Error: {str(e)}"}]

async def create_note(name: str, text: str = "", container_path: str = "",
                     attributes: Optional[Dict[str, Any]] = None, 
                     document: Optional[str] = None) -> List[Dict[str, Any]]:
    """Create a new note"""
    try:
        doc_specifier = f'document "{document}"' if document else 'front document'
        name_escaped = TinderboxHelper.escape_quotes(name)
        text_escaped = TinderboxHelper.escape_quotes(text)
        
        # Build attribute setting commands
        attr_commands = []
        if attributes:
            for key, value in attributes.items():
                value_escaped = TinderboxHelper.escape_quotes(str(value))
                attr_commands.append(f'set value of attribute "{key}" of newNote to "{value_escaped}"')
        
        attr_script = "\n".join(attr_commands) if attr_commands else ""
        
        if container_path:
            container_spec = f'note "{container_path}" of'
        else:
            container_spec = ""
        
        script = f'''
        tell application "Tinderbox 10"
            tell {doc_specifier}
                set newNote to make new note at {container_spec} it
                set value of attribute "Name" of newNote to "{name_escaped}"
                set value of attribute "Text" of newNote to "{text_escaped}"
                {attr_script}
                set noteID to value of attribute "ID" of newNote
                set notePath to value of attribute "Path" of newNote
                return "{{\\"id\\": \\"" & noteID & "\\", \\"path\\": \\"" & notePath & "\\"}}"
            end tell
        end tell
        '''
        
        result = TinderboxHelper.run_applescript(script)
        return [{"type": "text", "text": f"Created note: {result}"}]
        
    except Exception as e:
        return [{"type": "text", "text": f"Error: {str(e)}"}]

async def update_note(note_path: str, text: Optional[str] = None,
                     attributes: Optional[Dict[str, Any]] = None,
                     document: Optional[str] = None) -> List[Dict[str, Any]]:
    """Update an existing note"""
    try:
        doc_specifier = f'document "{document}"' if document else 'front document'
        
        updates = []
        if text is not None:
            text_escaped = TinderboxHelper.escape_quotes(text)
            updates.append(f'set value of attribute "Text" of theNote to "{text_escaped}"')
        
        if attributes:
            for key, value in attributes.items():
                value_escaped = TinderboxHelper.escape_quotes(str(value))
                updates.append(f'set value of attribute "{key}" of theNote to "{value_escaped}"')
        
        if not updates:
            return [{"type": "text", "text": "No updates specified"}]
        
        update_script = "\n".join(updates)
        
        script = f'''
        tell application "Tinderbox 10"
            tell {doc_specifier}
                set theNote to note "{note_path}"
                {update_script}
                return "Updated successfully"
            end tell
        end tell
        '''
        
        result = TinderboxHelper.run_applescript(script)
        return [{"type": "text", "text": result}]
        
    except Exception as e:
        return [{"type": "text", "text": f"Error: {str(e)}"}]

async def delete_note(note_path: str, document: Optional[str] = None) -> List[Dict[str, Any]]:
    """Delete a note"""
    try:
        doc_specifier = f'document "{document}"' if document else 'front document'
        
        script = f'''
        tell application "Tinderbox 10"
            tell {doc_specifier}
                set theNote to note "{note_path}"
                delete theNote
                return "Deleted successfully"
            end tell
        end tell
        '''
        
        result = TinderboxHelper.run_applescript(script)
        return [{"type": "text", "text": result}]
        
    except Exception as e:
        return [{"type": "text", "text": f"Error: {str(e)}"}]

async def create_link(from_note: str, to_note: str, 
                     link_type: Optional[str] = None,
                     document: Optional[str] = None) -> List[Dict[str, Any]]:
    """Create a link between notes"""
    try:
        doc_specifier = f'document "{document}"' if document else 'front document'
        link_type_spec = f' with type "{link_type}"' if link_type else ""
        
        script = f'''
        tell application "Tinderbox 10"
            tell {doc_specifier}
                set sourceNote to note "{from_note}"
                set destNote to note "{to_note}"
                make new link from sourceNote to destNote{link_type_spec}
                return "Link created successfully"
            end tell
        end tell
        '''
        
        result = TinderboxHelper.run_applescript(script)
        return [{"type": "text", "text": result}]
        
    except Exception as e:
        return [{"type": "text", "text": f"Error: {str(e)}"}]

async def run_agent(agent_path: str, document: Optional[str] = None) -> List[Dict[str, Any]]:
    """Execute a Tinderbox agent"""
    try:
        doc_specifier = f'document "{document}"' if document else 'front document'
        
        script = f'''
        tell application "Tinderbox 10"
            tell {doc_specifier}
                set theAgent to note "{agent_path}"
                update theAgent
                return "Agent executed successfully"
            end tell
        end tell
        '''
        
        result = TinderboxHelper.run_applescript(script)
        return [{"type": "text", "text": result}]
        
    except Exception as e:
        return [{"type": "text", "text": f"Error: {str(e)}"}]

# Main execution
async def main():
    """Run the MCP server"""
    print("Starting MCP server main loop...", file=sys.stderr)
    
    try:
        async with stdio_server() as (read_stream, write_stream):
            print("Server streams established", file=sys.stderr)
            options = server.create_initialization_options()
            print(f"Server info: {options}", file=sys.stderr)
            await server.run(read_stream, write_stream, options)
    except Exception as e:
        print(f"Server error: {e}", file=sys.stderr)
        import traceback
        traceback.print_exc(file=sys.stderr)
        raise

if __name__ == "__main__":
    try:
        print("Running async event loop...", file=sys.stderr)
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\nServer stopped by user", file=sys.stderr)
    except Exception as e:
        print(f"Fatal error: {e}", file=sys.stderr)
        import traceback
        traceback.print_exc(file=sys.stderr)
        sys.exit(1)

Install

Tinderbox MCP Server Setup Guide (Updated)

This guide will help you set up the MCP (Model Context Protocol) server for Tinderbox 10, allowing Claude Desktop to read and write your Tinderbox notes directly.

What This Does

Once set up, you’ll be able to ask Claude to:

  • Search and read your Tinderbox notes
  • Create new notes with specific content
  • Update existing notes
  • Create links between notes
  • Run Tinderbox agents
  • Work with your Tinderbox data naturally in conversation

Prerequisites

  1. Tinderbox 10 installed on your Mac
  2. Claude Desktop app installed
  3. Python 3 (we’ll verify which version you have)
  4. Homebrew (recommended for easier Python management)

Step 1: Check Your Python Setup

macOS often has multiple Python installations. Let’s find out what you have:

  1. Open Terminal (press Cmd + Space, type “Terminal”, press Enter)
  2. Check your default Python:
which python3
python3 --version

Common locations:

  • /opt/homebrew/bin/python3 - Homebrew Python (recommended)
  • /usr/bin/python3 - System Python
  • /Library/Developer/CommandLineTools/usr/bin/python3 - Command Line Tools Python

Write down your Python path and version - you’ll need it later.

Step 2: Install MCP SDK

The MCP SDK needs to be installed for your specific Python version:

If you have Homebrew Python (recommended):

/opt/homebrew/bin/python3 -m pip install --user mcp

If you get “externally managed environment” error:

/opt/homebrew/bin/python3 -m pip install --user --break-system-packages mcp

Verify installation:

/opt/homebrew/bin/python3 -c "import mcp; print('MCP version:', mcp.__version__)"

You should see something like “MCP version: 1.9.1”

Step 3: Create the MCP Server Directory

  1. Create a directory for the MCP server:
mkdir -p ~/mcp-tinderbox
cd ~/mcp-tinderbox

Step 4: Save the Server Script

  1. Copy the entire Python code from the “Clean Tinderbox MCP Server” artifact
  2. Save it as tinderbox_mcp_server.py in your ~/mcp-tinderbox directory
  3. Make sure the first line (shebang) matches your Python path:
    4. If using Homebrew: #!/opt/homebrew/bin/python3
    5. If using system Python: #!/usr/bin/python3

Step 5: Test the Server

Before configuring Claude, test that the server runs:

cd ~/mcp-tinderbox
/opt/homebrew/bin/python3 tinderbox_mcp_server.py

You should see:

  • “Starting Tinderbox MCP Server…”
  • “MCP SDK imported successfully”
  • “Registering handlers with decorators…”
  • “Server streams established”

If it stays running without errors, press Ctrl+C to stop it.

Step 6: Configure Claude Desktop

  1. Find Claude’s configuration directory:
cd ~/Library/Application\ Support/Claude/
  1. Create or edit claude_desktop_config.json:
nano claude_desktop_config.json
  1. Add this configuration (adjust paths as needed):
{
  "mcpServers": {
    "tinderbox": {
      "command": "/opt/homebrew/bin/python3",
      "args": ["/Users/YOUR_USERNAME/mcp-tinderbox/tinderbox_mcp_server.py"]
    }
  }
}
  1. Replace YOUR_USERNAME with your actual username (find it with whoami)

If you already have other MCP servers:

Add Tinderbox to your existing configuration:

{
  "mcpServers": {
    "existing-server": {
      "command": "...",
      "args": [...]
    },
    "tinderbox": {
      "command": "/opt/homebrew/bin/python3",
      "args": ["/Users/YOUR_USERNAME/mcp-tinderbox/tinderbox_mcp_server.py"]
    }
  }
}

Important: Add a comma after the previous server’s closing brace!

Step 7: Grant Permissions

The first time you use the MCP server, macOS will ask for permissions:

  1. You’ll see “Terminal wants to control Tinderbox” → Click OK
  2. If it doesn’t work:
    3. Go to System Settings → Privacy & Security → Accessibility
    4. Click the lock to make changes
    5. Add Terminal (or Python) and check the box
    6. Also check Automation settings for Tinderbox

Step 8: Start Using It!

  1. Make sure Tinderbox 10 is running with a document open
  2. Completely quit Claude Desktop (Cmd+Q)
  3. Restart Claude Desktop
  4. Check the tools icon - you should see “tinderbox-mcp” with 8 available tools

Test Commands

Try these in Claude:

  • “List all notes in my Tinderbox document”
  • “Create a note called ‘Test Note’ with the text ‘Hello from Claude’”
  • “Search for notes containing the word ‘project’”
  • “Get the content of note ‘Test Note’”

Daily Usage

The MCP server connects to your frontmost Tinderbox document. Make sure:

  1. Tinderbox is running
  2. The document you want to work with is in front
  3. Claude Desktop is restarted after any config changes

Troubleshooting Appendix: Journey from v1 to Working

Issues Encountered and Solutions

1. Python Environment Confusion

Problem: Multiple Python installations causing import errors

  • Homebrew Python at /opt/homebrew/bin/python3
  • Command Line Tools Python at /Library/Developer/CommandLineTools/usr/bin/python3
  • System Python at /usr/bin/python3

Solution:

  • Always use full Python paths in configurations
  • Install MCP for the specific Python you’ll use
  • Homebrew Python is recommended for easier package management

2. MCP Installation Issues

Problem: “externally managed environment” error with pip

error: externally-managed-environment
× This environment is externally managed

Solution: Use the --user flag and if needed, --break-system-packages:

python3 -m pip install --user --break-system-packages mcp

3. Path and Directory Issues

Problem: Initial guide assumed Desktop location, but better practice is home directory

  • Wrong: /Users/username/Desktop/TinderboxMCP/
  • Better: /Users/username/mcp-tinderbox/

Solution: Use consistent paths throughout setup and config

4. MCP API Version Incompatibility

Problem: Server showed as “disabled” in Claude due to API mismatches

Evolution of fixes:

  1. First attempt: Used incorrect decorator syntax
  2. Second attempt: Tried manual handler registration
  3. Final solution: Used proper @server.list_tools() and @server.call_tool() decorators

Key discovery: MCP 1.9.1 uses decorator-based registration:

@server.list_tools()
async def handle_list_tools():
    return TOOLS_LIST  # Return list directly, not wrapped in dict

5. Import Errors

Problem: NameError: name 'types' is not defined

  • Initial code tried to import from mcp.types import Tool, TextContent
  • These types don’t exist in MCP 1.9.1

Solution:

  • Remove all type imports
  • Use plain dictionaries for return values
  • Change return type hints from List[types.TextContent] to List[Dict[str, Any]]

6. Server Lifecycle Issues

Problem: Server exiting immediately after start

  • Launcher creation logic caused early exit

Solution: Remove conditional launcher creation, always run server:

if __name__ == "__main__":
    server = TinderboxMCP()
    asyncio.run(server.run())

7. JSON Configuration Syntax

Problem: Claude Desktop is strict about JSON formatting

  • Tabs vs spaces matter
  • Trailing commas cause failures
  • Missing commas between servers break parsing

Solution: Always validate JSON and use consistent spacing

Debug Commands That Helped

  1. Check Python and MCP:
which python3
python3 -c "import mcp; print(mcp.__version__)"
python3 -m pip show mcp
  1. Test AppleScript permissions:
osascript -e 'tell application "Tinderbox 10" to return name of front document'
  1. View Claude logs:
tail -f ~/Library/Logs/Claude/mcp*.log | grep tinderbox
  1. Test server directly:
cd ~/mcp-tinderbox
/opt/homebrew/bin/python3 tinderbox_mcp_server.py 2>&1

Key Lessons Learned

  1. Always check logs - Claude’s MCP logs show the real errors
  2. Python paths matter - Use full paths everywhere
  3. MCP versions matter - Different versions have different APIs
  4. Start simple - Test with minimal functionality first
  5. Permissions are crucial - Both Accessibility and Automation needed

Final Working Configuration

  • Python: Homebrew Python 3.13 at /opt/homebrew/bin/python3
  • MCP: Version 1.9.1 installed with --user flag
  • Directory: ~/mcp-tinderbox/
  • Handlers: Decorator-based registration
  • Returns: Plain dictionaries, no special types
3 Likes

Quick Reference

Tinderbox MCP Quick Reference

Available Commands in Claude

Reading Notes

  • Get a specific note: “Show me the content of note ‘Project Overview’”
  • List all notes: “List all notes in my Tinderbox document”
  • List notes in container: “Show all notes in the ‘Research’ folder”
  • Search notes: “Find all notes containing ‘deadline’”

Creating & Updating

  • Create note: “Create a note called ‘Meeting Notes’ with the text ‘Discussion points…’”
  • Create in container: “Create a note called ‘Todo’ in the ‘Tasks’ container”
  • Update note: “Update the ‘Status’ note to say ‘Project completed’”
  • Set attributes: “Create a note with StartDate set to today”

Organization

  • Create link: “Link the ‘Overview’ note to ‘Details’”
  • Run agent: “Run the ‘Overdue Tasks’ agent”
  • Delete note: “Delete the note called ‘Old Draft’”

Important Notes

  1. Frontmost Document: The MCP server always works with the frontmost Tinderbox document
  2. Note Paths: Use exact names or full paths like “/Projects/Website/Homepage”
  3. Case Sensitive: Note names are case-sensitive
  4. Quotes in Names: If note names contain quotes, escape them

Quick Troubleshooting

“Server disconnected”

  • Restart Claude Desktop completely (Cmd+Q then reopen)

“Note not found”

  • Check exact spelling and capitalization
  • Make sure the document is frontmost in Tinderbox

“Permission denied”

  • Grant accessibility permissions to Terminal/Python
  • Check System Settings → Privacy & Security → Automation

Example Workflows

Daily Review

"List all notes modified today"
"Show me notes in the 'Inbox' container"
"Create a note called 'Daily Review - [date]' with today's accomplishments"

Research Organization

"Search for notes containing 'hypothesis'"
"Create a link from 'Literature Review' to all notes containing 'source'"
"Update the 'Research Status' note with current progress"

Task Management

"Run the 'Due This Week' agent"
"Create a note called 'New Task' in the 'Todo' container with priority high"
"List all notes with Status='pending'"

Server Control

Start Server (if not using Claude auto-start):

cd ~/mcp-tinderbox
/opt/homebrew/bin/python3 tinderbox_mcp_server.py

Check if MCP is working:
Look for “tinderbox-mcp” in Claude’s tools (puzzle piece icon)

View logs (for debugging):

tail -f ~/Library/Logs/Claude/mcp*.log
2 Likes

Forgot the server set up script

I have Claude connected to both my obsidian vault and which ever Tinderbox document I have open

#!/usr/bin/env python3
"""
Test script for Tinderbox MCP Server
Run this to verify your setup is working correctly
"""

import subprocess
import sys

print("Starting Tinderbox MCP Server...", file=sys.stderr)
print(f"Python version: {sys.version}", file=sys.stderr)
print("Imports successful", file=sys.stderr)

def run_applescript(script):
    """Execute AppleScript and return result"""
    try:
        result = subprocess.run(
            ['osascript', '-e', script],
            capture_output=True,
            text=True,
            check=True
        )
        return True, result.stdout.strip()
    except subprocess.CalledProcessError as e:
        return False, e.stderr.strip()

def test_tinderbox_connection():
    """Test basic Tinderbox connectivity"""
    print("🔍 Testing Tinderbox MCP Connection...\n")
    
    # Test 1: Check if Tinderbox is running
    print("1. Checking if Tinderbox is running...")
    script = '''
    tell application "System Events"
        return exists (process "Tinderbox 10")
    end tell
    '''
    success, result = run_applescript(script)
    
    if not success or result != "true":
        print("❌ Tinderbox 10 is not running. Please start Tinderbox and try again.")
        return False
    print("✅ Tinderbox is running")
    
    # Test 2: Check if there's an open document
    print("\n2. Checking for open Tinderbox document...")
    script = '''
    tell application "Tinderbox 10"
        return (count of documents) > 0
    end tell
    '''
    success, result = run_applescript(script)
    
    if not success or result != "true":
        print("❌ No Tinderbox document is open. Please open a document and try again.")
        return False
    print("✅ Found open document")
    
    # Test 3: Try to read document name
    print("\n3. Testing document access...")
    script = '''
    tell application "Tinderbox 10"
        return name of front document
    end tell
    '''
    success, result = run_applescript(script)
    
    if not success:
        print(f"❌ Cannot access document: {result}")
        print("\n⚠️  You may need to grant Terminal permission to control Tinderbox:")
        print("   1. Go to System Settings → Privacy & Security → Accessibility")
        print("   2. Click the lock and enter your password")
        print("   3. Add and check Terminal (or Python)")
        return False
    
    print(f"✅ Successfully accessed document: {result}")
    
    # Test 4: Try to create a test note
    print("\n4. Testing note creation...")
    script = '''
    tell application "Tinderbox 10"
        tell front document
            set testNote to make new note
            set value of attribute "Name" of testNote to "MCP Test Note"
            set value of attribute "Text" of testNote to "This is a test note created by the MCP test script. You can delete this."
            return value of attribute "ID" of testNote
        end tell
    end tell
    '''
    success, result = run_applescript(script)
    
    if not success:
        print(f"❌ Cannot create note: {result}")
        return False
    
    print("✅ Successfully created test note")
    
    # Test 5: Try to read the test note back
    print("\n5. Testing note reading...")
    script = '''
    tell application "Tinderbox 10"
        tell front document
            set testNote to note "MCP Test Note"
            return value of attribute "Text" of testNote
        end tell
    end tell
    '''
    success, result = run_applescript(script)
    
    if not success:
        print(f"❌ Cannot read note: {result}")
        return False
    
    print("✅ Successfully read note content")
    
    # Test 6: Clean up - delete test note
    print("\n6. Cleaning up test note...")
    script = '''
    tell application "Tinderbox 10"
        tell front document
            set testNote to note "MCP Test Note"
            delete testNote
            return "deleted"
        end tell
    end tell
    '''
    success, result = run_applescript(script)
    
    if success:
        print("✅ Test note deleted")
    else:
        print("⚠️  Could not delete test note (you can delete it manually)")
    
    return True

def test_python_setup():
    """Test Python environment setup"""
    print("\n📦 Checking Python setup...\n")
    
    # Check Python version
    print(f"Python version: {sys.version}")
    
    # Check if MCP is installed
    try:
        import mcp
        print("✅ MCP SDK is installed")
        return True
    except ImportError:
        print("❌ MCP SDK is not installed")
        print("\nTo install it, run this command in Terminal:")
        print("   python3 -m pip install mcp")
        return False

def main():
    print("=" * 50)
    print("Tinderbox MCP Server Test")
    print("=" * 50)
    
    # Test Python setup
    if not test_python_setup():
        print("\n❌ Please install the MCP SDK first")
        return
    
    print("\n" + "=" * 50)
    
    # Test Tinderbox connection
    if test_tinderbox_connection():
        print("\n" + "=" * 50)
        print("✅ All tests passed! Your Tinderbox MCP setup is working correctly.")
        print("\nNext steps:")
        print("1. Make sure claude_desktop_config.json is configured")
        print("2. Run the server with: python3 tinderbox_mcp_server.py")
        print("3. Restart Claude Desktop")
        print("4. Try asking Claude to work with your Tinderbox notes!")
    else:
        print("\n" + "=" * 50)
        print("❌ Some tests failed. Please fix the issues above and try again.")

if __name__ == "__main__":
    main()
    input("\nPress Enter to close...")



4 Likes

@Jake_Bernstein, per our call todday, you’ll love this.

Wow @acbmd, humongous effort here :clap: :clap:

1 Like

Andrew, would you be open to joining a future meetup, e.g., June 15, to review this with the community?

I am open to sharing at a meetup. June 15 is a Sunday. Can you confirm date, time, length of time for review and what things you think the community would want to know? Will you want a demo of it working or installing? Andy