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
- Tinderbox 10 installed on your Mac
- Claude Desktop app installed
- Python 3 (we’ll verify which version you have)
- 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:
- Open Terminal (press
Cmd + Space
, type “Terminal”, press Enter) - 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
- Create a directory for the MCP server:
mkdir -p ~/mcp-tinderbox
cd ~/mcp-tinderbox
Step 4: Save the Server Script
- Copy the entire Python code from the “Clean Tinderbox MCP Server” artifact
- Save it as
tinderbox_mcp_server.py
in your~/mcp-tinderbox
directory - 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
- Find Claude’s configuration directory:
cd ~/Library/Application\ Support/Claude/
- Create or edit
claude_desktop_config.json
:
nano claude_desktop_config.json
- Add this configuration (adjust paths as needed):
{
"mcpServers": {
"tinderbox": {
"command": "/opt/homebrew/bin/python3",
"args": ["/Users/YOUR_USERNAME/mcp-tinderbox/tinderbox_mcp_server.py"]
}
}
}
- Replace
YOUR_USERNAME
with your actual username (find it withwhoami
)
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:
- You’ll see “Terminal wants to control Tinderbox” → Click OK
- 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!
- Make sure Tinderbox 10 is running with a document open
- Completely quit Claude Desktop (
Cmd+Q
) - Restart Claude Desktop
- 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:
- Tinderbox is running
- The document you want to work with is in front
- 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:
- First attempt: Used incorrect decorator syntax
- Second attempt: Tried manual handler registration
- 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]
toList[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
- Check Python and MCP:
which python3
python3 -c "import mcp; print(mcp.__version__)"
python3 -m pip show mcp
- Test AppleScript permissions:
osascript -e 'tell application "Tinderbox 10" to return name of front document'
- View Claude logs:
tail -f ~/Library/Logs/Claude/mcp*.log | grep tinderbox
- Test server directly:
cd ~/mcp-tinderbox
/opt/homebrew/bin/python3 tinderbox_mcp_server.py 2>&1
Key Lessons Learned
- Always check logs - Claude’s MCP logs show the real errors
- Python paths matter - Use full paths everywhere
- MCP versions matter - Different versions have different APIs
- Start simple - Test with minimal functionality first
- 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