obsidian-cli-mcp 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ import asyncio
2
+ import sys
3
+
4
+ from .client import ObsidianClient
5
+ from .exceptions import ObsidianCLIError, ObsidianError, ObsidianNotFoundError
6
+ from .server import create_server
7
+
8
+ __all__ = [
9
+ "ObsidianCLIError",
10
+ "ObsidianClient",
11
+ "ObsidianError",
12
+ "ObsidianNotFoundError",
13
+ "create_server",
14
+ "main",
15
+ ]
16
+
17
+
18
+ async def _run_server() -> None:
19
+ try:
20
+ server = create_server()
21
+ await server.run_stdio_async()
22
+ except ObsidianNotFoundError as e:
23
+ print(f"Error starting obsidian-mcp: {e}", file=sys.stderr)
24
+ sys.exit(1)
25
+ except KeyboardInterrupt:
26
+ pass
27
+
28
+
29
+ def main() -> None:
30
+ """CLI 入口点,启动 MCP Stdio 通信服务。"""
31
+ asyncio.run(_run_server())
@@ -0,0 +1,38 @@
1
+ import asyncio
2
+ from dataclasses import dataclass
3
+
4
+
5
+ @dataclass
6
+ class CommandExecutionResult:
7
+ returncode: int | None
8
+ stdout: str
9
+ stderr: str
10
+
11
+
12
+ async def run_cli_command(
13
+ program: str, args: list[str], *, timeout: int = 30
14
+ ) -> CommandExecutionResult:
15
+ """运行 CLI 子进程命令并等待输出。"""
16
+ try:
17
+ process = await asyncio.create_subprocess_exec(
18
+ program,
19
+ *args,
20
+ stdout=asyncio.subprocess.PIPE,
21
+ stderr=asyncio.subprocess.PIPE,
22
+ )
23
+
24
+ stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
25
+
26
+ return CommandExecutionResult(
27
+ returncode=process.returncode,
28
+ stdout=stdout.decode("utf-8", errors="replace").strip(),
29
+ stderr=stderr.decode("utf-8", errors="replace").strip(),
30
+ )
31
+ except TimeoutError:
32
+ process.kill()
33
+ await process.wait()
34
+ return CommandExecutionResult(
35
+ returncode=-1,
36
+ stdout="",
37
+ stderr=f"Command timed out after {timeout} seconds",
38
+ )
@@ -0,0 +1,111 @@
1
+ import json
2
+ import os
3
+ import shutil
4
+ from typing import Any
5
+
6
+ from .cli import CommandExecutionResult, run_cli_command
7
+ from .exceptions import ObsidianCLIError, ObsidianNotFoundError
8
+
9
+ # 允许保留空字符串的参数(如清空属性值、创建空白内容等)
10
+ ALLOW_EMPTY_VALUE_KEYS = {"content", "value"}
11
+
12
+
13
+ class ObsidianClient:
14
+ """Obsidian CLI 异步客户端。负责参数序列化、环境变量回退、执行控制与结果解析。"""
15
+
16
+ def __init__(
17
+ self,
18
+ vault: str | None = None,
19
+ cli_path: str | None = None,
20
+ timeout: int = 30,
21
+ ) -> None:
22
+ self.vault = vault or os.getenv("OBSIDIAN_VAULT")
23
+ resolved_cli = (
24
+ cli_path or os.getenv("OBSIDIAN_CLI_PATH") or shutil.which("obsidian")
25
+ )
26
+ if not resolved_cli:
27
+ raise ObsidianNotFoundError(
28
+ "Obsidian CLI executable not found. Please install Obsidian CLI or set OBSIDIAN_CLI_PATH."
29
+ )
30
+ self.cli_path = resolved_cli
31
+ self.timeout = timeout
32
+
33
+ def _build_args(
34
+ self,
35
+ command: str,
36
+ params: dict[str, Any] | None = None,
37
+ vault: str | None = None,
38
+ ) -> list[str]:
39
+ """将 Python 字典转换为 Obsidian CLI 参数列表。
40
+
41
+ 规则:
42
+ - True: 转换为裸 flag(例如 verbose=True -> 'verbose')
43
+ - False / None: 忽略该参数
44
+ - 纯空白字符串(若非 content/value 白名单): 忽略该参数,防止意外触发 CLI 对活跃文件的回退行为
45
+ - 其余类型: 转换为 key=value 字符串
46
+ """
47
+ args = [command]
48
+ merged_params = dict(params or {})
49
+
50
+ # 确定目标 vault:参数指定 > 实例/环境变量默认 > 不传(回退到活跃库)
51
+ target_vault = vault if vault is not None else self.vault
52
+ if target_vault and target_vault.strip() and "vault" not in merged_params:
53
+ merged_params["vault"] = target_vault.strip()
54
+
55
+ for key, val in merged_params.items():
56
+ if val is None or val is False:
57
+ continue
58
+ if val is True:
59
+ args.append(key)
60
+ elif (
61
+ isinstance(val, str)
62
+ and not val.strip()
63
+ and key not in ALLOW_EMPTY_VALUE_KEYS
64
+ ):
65
+ continue
66
+ else:
67
+ args.append(f"{key}={val}")
68
+
69
+ return args
70
+
71
+ async def execute(
72
+ self,
73
+ command: str,
74
+ params: dict[str, Any] | None = None,
75
+ *,
76
+ parse_json: bool = False,
77
+ vault: str | None = None,
78
+ timeout: int | None = None,
79
+ ) -> Any:
80
+ """执行 Obsidian CLI 命令并返回文本或解析后的 JSON。"""
81
+ cli_args = self._build_args(command, params, vault=vault)
82
+ res: CommandExecutionResult = await run_cli_command(
83
+ self.cli_path, cli_args, timeout=timeout or self.timeout
84
+ )
85
+
86
+ # 检查错误:非零退出码或 stdout 以 Error: 开头
87
+ if res.returncode != 0:
88
+ err_msg = (
89
+ res.stderr
90
+ or res.stdout
91
+ or f"Command '{command}' failed with exit code {res.returncode}"
92
+ )
93
+ raise ObsidianCLIError(
94
+ err_msg, returncode=res.returncode, stderr=res.stderr, stdout=res.stdout
95
+ )
96
+
97
+ if res.stdout.startswith("Error:"):
98
+ raise ObsidianCLIError(
99
+ res.stdout,
100
+ returncode=res.returncode,
101
+ stderr=res.stderr,
102
+ stdout=res.stdout,
103
+ )
104
+
105
+ if parse_json:
106
+ try:
107
+ return json.loads(res.stdout)
108
+ except json.JSONDecodeError:
109
+ return res.stdout
110
+
111
+ return res.stdout
@@ -0,0 +1,22 @@
1
+ class ObsidianError(Exception):
2
+ """Base exception for obsidian-mcp."""
3
+
4
+
5
+ class ObsidianNotFoundError(ObsidianError):
6
+ """Raised when obsidian CLI executable is not found."""
7
+
8
+
9
+ class ObsidianCLIError(ObsidianError):
10
+ """Raised when obsidian CLI command execution fails."""
11
+
12
+ def __init__(
13
+ self,
14
+ message: str,
15
+ returncode: int | None = None,
16
+ stderr: str = "",
17
+ stdout: str = "",
18
+ ) -> None:
19
+ super().__init__(message)
20
+ self.returncode = returncode
21
+ self.stderr = stderr
22
+ self.stdout = stdout
File without changes
@@ -0,0 +1,20 @@
1
+ from mcp.server.mcpserver import MCPServer
2
+
3
+ from .client import ObsidianClient
4
+ from .tools import register_tools
5
+
6
+
7
+ def create_server(
8
+ vault: str | None = None,
9
+ cli_path: str | None = None,
10
+ timeout: int = 30,
11
+ ) -> MCPServer:
12
+ """创建并配置 Obsidian MCP 服务器实例。"""
13
+ client = ObsidianClient(vault=vault, cli_path=cli_path, timeout=timeout)
14
+ server = MCPServer(
15
+ name="obsidian-cli-mcp",
16
+ version="0.1.0",
17
+ instructions="Obsidian CLI MCP server providing full control over Obsidian vaults.",
18
+ )
19
+ register_tools(server, client)
20
+ return server