Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion LOGSEQ_API_ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ Standard JSON-RPC response with result or error.
- Adds content block to existing page
- Example: `["My Page", "This is content"]`

- **`insertBlock(targetBlockUUID, content, options)`**
- Inserts a new block relative to an existing block
- Options: `{sibling: false, properties: {...}}`
- `sibling: false` inserts as child (default)
- `sibling: true` inserts as sibling after target
- Example: `["parent-block-uuid-123", "Child content", {"sibling": false, "properties": {}}]`

- **`getAllPages()`**
- Returns array of all page objects with metadata
- Each page includes: name, properties, journal status, etc.
Expand All @@ -70,7 +77,6 @@ Standard JSON-RPC response with result or error.
#### 🔍 Likely Available (Unverified)
- **`deletePage(pageName)`** - Delete page entirely
- **`updatePage(pageName, properties)`** - Update page properties
- **`insertBlock(targetBlock, content, options)`** - Insert block at position
- **`updateBlock(blockUUID, content)`** - Update specific block content
- **`removeBlock(blockUUID)`** - Delete specific block

Expand Down Expand Up @@ -156,6 +162,22 @@ To create page with content:
1. `createPage(pageName, {}, {"createFirstBlock": true})`
2. `appendBlockInPage(pageName, content)` (if content needed)

### Nested Block Creation Pattern
To create hierarchical block structures:
1. Create parent block: `appendBlockInPage(pageName, "Parent content")`
2. Get parent block UUID from the returned block data
3. Insert child: `insertBlock(parentBlockUUID, "Child content", {"sibling": false})`
4. Insert another child: `insertBlock(parentBlockUUID, "Second child", {"sibling": false})`
5. Insert sibling: `insertBlock(parentBlockUUID, "Sibling content", {"sibling": true})`

Block hierarchy example:
```
- Parent block
- Child block 1
- Child block 2
- Sibling block
```

## Future Research Areas

- Block-level CRUD operations
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,17 @@ Add to your config file (`Settings → Developer → Edit Config`):

## 🛠️ Available Tools

The server provides 6 comprehensive tools:
The server provides 7 comprehensive tools:

| Tool | Purpose | Example Use |
|------|---------|-------------|
| **`list_pages`** | Browse your graph | "Show me all my pages" |
| **`get_page_content`** | Read page content | "Get my project notes" |
| **`create_page`** | Add new pages | "Create a meeting notes page" |
| **`create_page`** | Add new pages | "Create a meeting notes page" |
| **`update_page`** | Modify existing pages | "Update my task list" |
| **`delete_page`** | Remove pages | "Delete the old draft page" |
| **`search`** | Find content across graph | "Search for 'productivity tips'" |
| **`insert_nested_block`** | Create hierarchical block structures | "Add a child block under this task" |

---

Expand Down
5 changes: 5 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
- Full-text search across blocks, pages, and files
- Configurable result filtering and limits
- Rich result formatting with snippets and pagination
- ✅ Insert Nested Block (`insert_nested_block`)
- Create hierarchical block structures
- Insert blocks as children or siblings
- Support for block properties (markers, tags, etc.)
- Enable complex nested note-taking workflows

## Planned Features

Expand Down
39 changes: 39 additions & 0 deletions src/mcp_logseq/logseq.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,3 +295,42 @@ def update_page(self, page_name: str, content: str = None, properties: dict = No
except Exception as e:
logger.error(f"Error updating page '{page_name}': {str(e)}")
raise

def insert_block_as_child(
self,
parent_block_uuid: str,
content: str,
properties: dict = None,
sibling: bool = False
) -> Any:
"""Insert a new block as a child of an existing block, enabling nested block structures."""
url = self.get_base_url()
logger.info(f"Inserting block as {'sibling' if sibling else 'child'} of {parent_block_uuid}")

try:
options = {
"sibling": sibling
}

if properties:
options["properties"] = properties

response = requests.post(
url,
headers=self._get_headers(),
json={
"method": "logseq.Editor.insertBlock",
"args": [parent_block_uuid, content, options]
},
verify=self.verify_ssl,
timeout=self.timeout
)
response.raise_for_status()
result = response.json()

logger.info(f"Successfully inserted block under {parent_block_uuid}")
return result

except Exception as e:
logger.error(f"Error inserting nested block: {str(e)}")
raise
1 change: 1 addition & 0 deletions src/mcp_logseq/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def get_tool_handler(name: str) -> tools.ToolHandler | None:
add_tool_handler(tools.DeletePageToolHandler())
add_tool_handler(tools.UpdatePageToolHandler())
add_tool_handler(tools.SearchToolHandler())
add_tool_handler(tools.InsertNestedBlockToolHandler())
logger.info("Tool handlers registration complete")

@app.list_tools()
Expand Down
85 changes: 85 additions & 0 deletions src/mcp_logseq/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,3 +486,88 @@ def run_tool(self, args: dict) -> list[TextContent]:
type="text",
text=f"❌ Search failed: {str(e)}"
)]

class InsertNestedBlockToolHandler(ToolHandler):
def __init__(self):
super().__init__("insert_nested_block")

def get_tool_description(self):
return Tool(
name=self.name,
description="""Insert a new block as a child or sibling of an existing block, enabling nested hierarchical structures""",
inputSchema={
"type": "object",
"properties": {
"parent_block_uuid": {
"type": "string",
"description": "UUID of the reference block. If sibling=false, new block becomes a CHILD of this UUID. If sibling=true, new block becomes a SIBLING of this UUID (at the same level)."
},
"content": {
"type": "string",
"description": "Content text for the new block"
},
"properties": {
"type": "object",
"description": "Optional block properties (e.g., {'marker': 'TODO', 'priority': 'A'})",
"additionalProperties": True
},
"sibling": {
"type": "boolean",
"description": "false (default) = insert as CHILD under parent_block_uuid. true = insert as SIBLING after parent_block_uuid at the same level. For multiple children under same parent, ALWAYS use false with the parent's UUID.",
"default": False
}
},
"required": ["parent_block_uuid", "content"]
}
)

def run_tool(self, args: dict) -> list[TextContent]:
"""Insert a nested block under an existing block."""
if "parent_block_uuid" not in args or "content" not in args:
raise RuntimeError("parent_block_uuid and content arguments required")

parent_uuid = args["parent_block_uuid"]
content = args["content"]
properties = args.get("properties")
sibling = args.get("sibling", False)

try:
api = logseq.LogSeq(api_key=api_key)
result = api.insert_block_as_child(
parent_block_uuid=parent_uuid,
content=content,
properties=properties,
sibling=sibling
)

relationship = "sibling" if sibling else "child"
success_msg = f"✅ Successfully inserted block as {relationship}"

# Add block details if available
if result and isinstance(result, dict):
if result.get("uuid"):
success_msg += f"\n🆔 New block UUID: {result.get('uuid')}"
if result.get("content"):
content_preview = result.get('content')
if len(content_preview) > 100:
content_preview = content_preview[:100] + "..."
success_msg += f"\n📝 Content: {content_preview}"

success_msg += f"\n🔗 Inserted under parent: {parent_uuid}"

return [TextContent(
type="text",
text=success_msg
)]

except ValueError as e:
return [TextContent(
type="text",
text=f"❌ Error: {str(e)}"
)]
except Exception as e:
logger.error(f"Failed to insert nested block: {str(e)}")
return [TextContent(
type="text",
text=f"❌ Failed to insert nested block: {str(e)}"
)]
12 changes: 10 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
GetPageContentToolHandler,
DeletePageToolHandler,
UpdatePageToolHandler,
SearchToolHandler
SearchToolHandler,
InsertNestedBlockToolHandler
)

@pytest.fixture
Expand Down Expand Up @@ -76,6 +77,12 @@ def mock_logseq_responses():
],
"files": [],
"has-more?": False
},
"insert_block_success": {
"uuid": "block-child-uuid-123",
"content": "Child block content",
"parent": "parent-block-uuid-456",
"properties": {}
}
}

Expand All @@ -88,7 +95,8 @@ def tool_handlers():
"get_page_content": GetPageContentToolHandler(),
"delete_page": DeletePageToolHandler(),
"update_page": UpdatePageToolHandler(),
"search": SearchToolHandler()
"search": SearchToolHandler(),
"insert_nested_block": InsertNestedBlockToolHandler()
}

@pytest.fixture
Expand Down