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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The server implements multiple tools to interact with Obsidian:
- list_files_in_dir: Lists all files and directories in a specific Obsidian directory
- get_file_contents: Return the content of a single file in your vault.
- search: Search for documents matching a specified text query across all files in the vault
- obsidian_dataview_query: runs a TABLE dataview query in obsidian
- patch_content: Insert content into an existing note relative to a heading, block reference, or frontmatter field.
- append_content: Append content to a new or existing file in the vault.
- delete_file: Delete a file or directory from your vault.
Expand Down
18 changes: 17 additions & 1 deletion src/mcp_obsidian/obsidian.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,23 @@ def call_fn():
return response.json()

return self._safe_call(call_fn)


def dataview_query_execute(self, dv_query: str) -> Any:
url = f"{self.get_base_url()}/search/"

headers = self._get_headers() | {
# Content-Type for Dataview DQL
'Content-Type': 'application/vnd.olrapi.dataview.dql+txt'
}

def call_fn():
response = requests.post(url, headers=headers, data=dv_query.encode('utf-8'),
verify=self.verify_ssl, timeout=self.timeout)
response.raise_for_status()
return response.json()

return self._safe_call(call_fn)

def get_periodic_note(self, period: str) -> Any:
"""Get current periodic note for the specified period.

Expand Down
1 change: 1 addition & 0 deletions src/mcp_obsidian/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def get_tool_handler(name: str) -> tools.ToolHandler | None:
add_tool_handler(tools.AppendContentToolHandler())
add_tool_handler(tools.DeleteFileToolHandler())
add_tool_handler(tools.ComplexSearchToolHandler())
add_tool_handler(tools.DataviewQueryToolHandler())
add_tool_handler(tools.BatchGetFileContentsToolHandler())
add_tool_handler(tools.PeriodicNotesToolHandler())
add_tool_handler(tools.RecentPeriodicNotesToolHandler())
Expand Down
56 changes: 53 additions & 3 deletions src/mcp_obsidian/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
)
import json
import os
from . import obsidian
from . import obsidian, server

api_key = os.getenv("OBSIDIAN_API_KEY", "")
obsidian_host = os.getenv("OBSIDIAN_HOST", "127.0.0.1")
Expand Down Expand Up @@ -355,11 +355,25 @@ def get_tool_description(self):
)

def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
if "query" not in args:
query_param = args.get("query")

if query_param is None:
raise RuntimeError("query argument missing in arguments")

actual_query_dict: dict
if isinstance(query_param, str):
try:
actual_query_dict = json.loads(query_param)
except json.JSONDecodeError as e:
raise RuntimeError(f"query parameter is a string but not valid JSON: {query_param}")
elif isinstance(query_param, dict):
actual_query_dict = query_param
else:
server.logger.warning(f"query parameter has unexpected type: {type(query_param)}. Query: {query_param}")
raise RuntimeError(f"query argument has unexpected type: {type(query_param)}. Expected dict or JSON string.")

api = obsidian.Obsidian(api_key=api_key, host=obsidian_host)
results = api.search_json(args.get("query", ""))
results = api.search_json(actual_query_dict)

return [
TextContent(
Expand All @@ -368,6 +382,42 @@ def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | Embedded
)
]

class DataviewQueryToolHandler(ToolHandler):
def __init__(self):
super().__init__("obsidian_dataview_query") # New function

def get_tool_description(self):
return Tool(
name=self.name,
description="Executes a Dataview query against Obsidian notes and returns the results as JSON.",
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The Dataview query string (e.g., TABLE title, status FROM \"some/path\" WHERE condition). Note: It does not support TABLE WITHOUT ID queries."
}
},
"required": ["query"]
}
)

def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
dataview_query_string = args.get("query")

if not dataview_query_string or not isinstance(dataview_query_string, str):
raise RuntimeError("query argument (string) missing or invalid in arguments for Dataview")

api = obsidian.Obsidian(api_key=api_key, host=obsidian_host)
results = api.dataview_query_execute(dataview_query_string)

return [
TextContent(
type="text",
text=json.dumps(results, indent=2)
)
]

class BatchGetFileContentsToolHandler(ToolHandler):
def __init__(self):
super().__init__("obsidian_batch_get_file_contents")
Expand Down