foldnotes-mcp 2.2.0__tar.gz
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.
- foldnotes_mcp-2.2.0/.gitignore +4 -0
- foldnotes_mcp-2.2.0/LICENSE +21 -0
- foldnotes_mcp-2.2.0/PKG-INFO +109 -0
- foldnotes_mcp-2.2.0/README.md +93 -0
- foldnotes_mcp-2.2.0/pyproject.toml +41 -0
- foldnotes_mcp-2.2.0/python-sdk/foldnotes_mcp.py +1120 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Foldsoft Pty Ltd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: foldnotes-mcp
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Summary: MCP server exposing the FoldNotes CLI (fn) as tools for Claude Desktop, Claude Code, and other MCP clients.
|
|
5
|
+
Project-URL: Homepage, https://foldnotes.io
|
|
6
|
+
Project-URL: Documentation, https://foldnotes.io/advanced/ai-integration/
|
|
7
|
+
Project-URL: Repository, https://github.com/Foldsoft/foldnotes-mcp
|
|
8
|
+
Project-URL: Discussions, https://github.com/Foldsoft/foldnotes-mcp/discussions
|
|
9
|
+
Author-email: Foldsoft Pty Ltd <support@foldnotes.io>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: claude,cli,foldnotes,mcp,model-context-protocol,notes
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: mcp>=1.2.0
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# FoldNotes MCP Server
|
|
18
|
+
|
|
19
|
+
A [Model Context Protocol](https://modelcontextprotocol.io) server that exposes the
|
|
20
|
+
FoldNotes command-line tool (`fn`) as tools any MCP-compatible client can call —
|
|
21
|
+
[Claude Desktop](https://claude.ai/download), [Claude Code](https://claude.com/claude-code),
|
|
22
|
+
or third-party clients. It's a thin, read/write bridge to your FoldNotes collection:
|
|
23
|
+
everything the CLI can do, an AI assistant can do — under the same licensing and safety limits.
|
|
24
|
+
|
|
25
|
+
Made by **Foldsoft Pty Ltd** · [foldnotes.io](https://foldnotes.io)
|
|
26
|
+
|
|
27
|
+
> **Source-available, install-only.** This repository is published so you can install and run the
|
|
28
|
+
> server — we don't accept code contributions. Questions, tips, and bug reports are welcome in
|
|
29
|
+
> [Discussions](https://github.com/Foldsoft/foldnotes-mcp/discussions); see
|
|
30
|
+
> [CONTRIBUTING](CONTRIBUTING.md) for direct support and security reporting.
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- **macOS** with the FoldNotes app, and the **`fn` CLI** installed from the app:
|
|
35
|
+
**FoldNotes → Install Command Line Tool**. The server shells out to `fn`.
|
|
36
|
+
- **Python 3.10+**.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
git clone https://github.com/Foldsoft/foldnotes-mcp.git
|
|
42
|
+
cd foldnotes-mcp/python-sdk
|
|
43
|
+
python3 -m venv venv
|
|
44
|
+
source venv/bin/activate
|
|
45
|
+
pip install mcp
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Verify:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
fn version # confirm the CLI is on your PATH
|
|
52
|
+
python3 foldnotes_mcp.py # starts the server on stdio — Ctrl-C to stop
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The server auto-discovers `fn` at `/usr/local/bin/fn` or `/opt/homebrew/bin/fn`, falling
|
|
56
|
+
back to `fn` on your `PATH`.
|
|
57
|
+
|
|
58
|
+
## Connect to Claude Desktop
|
|
59
|
+
|
|
60
|
+
Add a `foldnotes` entry to `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
61
|
+
(absolute paths to the venv's Python and the server script):
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"foldnotes": {
|
|
67
|
+
"command": "/absolute/path/to/foldnotes-mcp/python-sdk/venv/bin/python3",
|
|
68
|
+
"args": ["/absolute/path/to/foldnotes-mcp/python-sdk/foldnotes_mcp.py"]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Restart Claude Desktop; the FoldNotes tools appear under the tools menu.
|
|
75
|
+
|
|
76
|
+
## Connect to Claude Code
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
claude mcp add foldnotes -- \
|
|
80
|
+
/absolute/path/to/foldnotes-mcp/python-sdk/venv/bin/python3 \
|
|
81
|
+
/absolute/path/to/foldnotes-mcp/python-sdk/foldnotes_mcp.py
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Add `--scope user` for all projects, or `--scope project` to share a `.mcp.json`. Confirm with
|
|
85
|
+
`claude mcp list` or `/mcp` inside a session.
|
|
86
|
+
|
|
87
|
+
## What it exposes
|
|
88
|
+
|
|
89
|
+
40+ tools covering notes, tasks, tags, backlinks, daily notes, properties/schema, saved
|
|
90
|
+
queries, templates, projects, attachments, collections, and search. The full, always-current
|
|
91
|
+
list is in the docs: **[foldnotes.io → AI & Automation](https://foldnotes.io/advanced/ai-integration/)**.
|
|
92
|
+
|
|
93
|
+
Two safety rules are built in:
|
|
94
|
+
|
|
95
|
+
- **Reading is free; writing needs a licence.** Read tools always work; write tools and `bind`
|
|
96
|
+
require a valid FoldNotes licence or active trial, otherwise they change nothing.
|
|
97
|
+
- **Trash is human-owned.** The server can move a note to the trash and restore it, but cannot
|
|
98
|
+
empty the trash or permanently delete — that stays a deliberate, human-only action in the app.
|
|
99
|
+
|
|
100
|
+
## Questions & support
|
|
101
|
+
|
|
102
|
+
- **Setup help, usage questions, bugs, tips** → [Discussions](https://github.com/Foldsoft/foldnotes-mcp/discussions).
|
|
103
|
+
- **Direct support** → [support@foldnotes.io](mailto:support@foldnotes.io).
|
|
104
|
+
- **Security** → report privately via the repo's **Security → Report a vulnerability**. We don't use
|
|
105
|
+
Issues or accept pull requests — see [CONTRIBUTING](CONTRIBUTING.md).
|
|
106
|
+
|
|
107
|
+
## Licence
|
|
108
|
+
|
|
109
|
+
MIT © 2026 Foldsoft Pty Ltd — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# FoldNotes MCP Server
|
|
2
|
+
|
|
3
|
+
A [Model Context Protocol](https://modelcontextprotocol.io) server that exposes the
|
|
4
|
+
FoldNotes command-line tool (`fn`) as tools any MCP-compatible client can call —
|
|
5
|
+
[Claude Desktop](https://claude.ai/download), [Claude Code](https://claude.com/claude-code),
|
|
6
|
+
or third-party clients. It's a thin, read/write bridge to your FoldNotes collection:
|
|
7
|
+
everything the CLI can do, an AI assistant can do — under the same licensing and safety limits.
|
|
8
|
+
|
|
9
|
+
Made by **Foldsoft Pty Ltd** · [foldnotes.io](https://foldnotes.io)
|
|
10
|
+
|
|
11
|
+
> **Source-available, install-only.** This repository is published so you can install and run the
|
|
12
|
+
> server — we don't accept code contributions. Questions, tips, and bug reports are welcome in
|
|
13
|
+
> [Discussions](https://github.com/Foldsoft/foldnotes-mcp/discussions); see
|
|
14
|
+
> [CONTRIBUTING](CONTRIBUTING.md) for direct support and security reporting.
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- **macOS** with the FoldNotes app, and the **`fn` CLI** installed from the app:
|
|
19
|
+
**FoldNotes → Install Command Line Tool**. The server shells out to `fn`.
|
|
20
|
+
- **Python 3.10+**.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
git clone https://github.com/Foldsoft/foldnotes-mcp.git
|
|
26
|
+
cd foldnotes-mcp/python-sdk
|
|
27
|
+
python3 -m venv venv
|
|
28
|
+
source venv/bin/activate
|
|
29
|
+
pip install mcp
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Verify:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
fn version # confirm the CLI is on your PATH
|
|
36
|
+
python3 foldnotes_mcp.py # starts the server on stdio — Ctrl-C to stop
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The server auto-discovers `fn` at `/usr/local/bin/fn` or `/opt/homebrew/bin/fn`, falling
|
|
40
|
+
back to `fn` on your `PATH`.
|
|
41
|
+
|
|
42
|
+
## Connect to Claude Desktop
|
|
43
|
+
|
|
44
|
+
Add a `foldnotes` entry to `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
45
|
+
(absolute paths to the venv's Python and the server script):
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"foldnotes": {
|
|
51
|
+
"command": "/absolute/path/to/foldnotes-mcp/python-sdk/venv/bin/python3",
|
|
52
|
+
"args": ["/absolute/path/to/foldnotes-mcp/python-sdk/foldnotes_mcp.py"]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Restart Claude Desktop; the FoldNotes tools appear under the tools menu.
|
|
59
|
+
|
|
60
|
+
## Connect to Claude Code
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
claude mcp add foldnotes -- \
|
|
64
|
+
/absolute/path/to/foldnotes-mcp/python-sdk/venv/bin/python3 \
|
|
65
|
+
/absolute/path/to/foldnotes-mcp/python-sdk/foldnotes_mcp.py
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Add `--scope user` for all projects, or `--scope project` to share a `.mcp.json`. Confirm with
|
|
69
|
+
`claude mcp list` or `/mcp` inside a session.
|
|
70
|
+
|
|
71
|
+
## What it exposes
|
|
72
|
+
|
|
73
|
+
40+ tools covering notes, tasks, tags, backlinks, daily notes, properties/schema, saved
|
|
74
|
+
queries, templates, projects, attachments, collections, and search. The full, always-current
|
|
75
|
+
list is in the docs: **[foldnotes.io → AI & Automation](https://foldnotes.io/advanced/ai-integration/)**.
|
|
76
|
+
|
|
77
|
+
Two safety rules are built in:
|
|
78
|
+
|
|
79
|
+
- **Reading is free; writing needs a licence.** Read tools always work; write tools and `bind`
|
|
80
|
+
require a valid FoldNotes licence or active trial, otherwise they change nothing.
|
|
81
|
+
- **Trash is human-owned.** The server can move a note to the trash and restore it, but cannot
|
|
82
|
+
empty the trash or permanently delete — that stays a deliberate, human-only action in the app.
|
|
83
|
+
|
|
84
|
+
## Questions & support
|
|
85
|
+
|
|
86
|
+
- **Setup help, usage questions, bugs, tips** → [Discussions](https://github.com/Foldsoft/foldnotes-mcp/discussions).
|
|
87
|
+
- **Direct support** → [support@foldnotes.io](mailto:support@foldnotes.io).
|
|
88
|
+
- **Security** → report privately via the repo's **Security → Report a vulnerability**. We don't use
|
|
89
|
+
Issues or accept pull requests — see [CONTRIBUTING](CONTRIBUTING.md).
|
|
90
|
+
|
|
91
|
+
## Licence
|
|
92
|
+
|
|
93
|
+
MIT © 2026 Foldsoft Pty Ltd — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "foldnotes-mcp"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "MCP server exposing the FoldNotes CLI (fn) as tools for Claude Desktop, Claude Code, and other MCP clients."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Foldsoft Pty Ltd", email = "support@foldnotes.io" }]
|
|
14
|
+
keywords = ["mcp", "model-context-protocol", "foldnotes", "claude", "notes", "cli"]
|
|
15
|
+
dependencies = ["mcp>=1.2.0"]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://foldnotes.io"
|
|
19
|
+
Documentation = "https://foldnotes.io/advanced/ai-integration/"
|
|
20
|
+
Repository = "https://github.com/Foldsoft/foldnotes-mcp"
|
|
21
|
+
Discussions = "https://github.com/Foldsoft/foldnotes-mcp/discussions"
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
foldnotes-mcp = "foldnotes_mcp:main"
|
|
25
|
+
|
|
26
|
+
# Single-module package: the module lives in python-sdk/, mapped to the
|
|
27
|
+
# top-level import name `foldnotes_mcp` via `sources`.
|
|
28
|
+
[tool.hatch.version]
|
|
29
|
+
path = "python-sdk/foldnotes_mcp.py"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
only-include = ["python-sdk/foldnotes_mcp.py"]
|
|
33
|
+
sources = ["python-sdk"]
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.sdist]
|
|
36
|
+
include = [
|
|
37
|
+
"python-sdk/foldnotes_mcp.py",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"pyproject.toml",
|
|
41
|
+
]
|
|
@@ -0,0 +1,1120 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Copyright (c) 2026 Foldsoft Pty Ltd. Released under the MIT Licence — see LICENSE.
|
|
3
|
+
"""
|
|
4
|
+
FoldNotes MCP Server (Python 3.10+ with official MCP SDK)
|
|
5
|
+
|
|
6
|
+
A Model Context Protocol server that exposes FoldNotes CLI (`fn`) commands
|
|
7
|
+
as tools for AI models. Works with Claude Desktop, Claude Code, or any
|
|
8
|
+
MCP-compatible client.
|
|
9
|
+
|
|
10
|
+
Requires:
|
|
11
|
+
Python 3.10+
|
|
12
|
+
pip install mcp
|
|
13
|
+
`fn` CLI installed (via FoldNotes app or manual symlink)
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
python3 foldnotes_mcp.py
|
|
17
|
+
|
|
18
|
+
Configure in Claude Desktop's claude_desktop_config.json:
|
|
19
|
+
{
|
|
20
|
+
"mcpServers": {
|
|
21
|
+
"foldnotes": {
|
|
22
|
+
"command": "/path/to/venv/bin/python3",
|
|
23
|
+
"args": ["/path/to/python-sdk/foldnotes_mcp.py"]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
__version__ = "2.2.0"
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import os
|
|
33
|
+
import subprocess
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
from mcp.server.fastmcp import FastMCP
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# fn CLI wrapper
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
FN_PATHS = [
|
|
43
|
+
"/usr/local/bin/fn",
|
|
44
|
+
"/opt/homebrew/bin/fn",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _find_fn() -> str:
|
|
49
|
+
for path in FN_PATHS:
|
|
50
|
+
if os.path.isfile(path) and os.access(path, os.X_OK):
|
|
51
|
+
return path
|
|
52
|
+
return "fn"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
FN_BIN = _find_fn()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run_fn(args: list[str], collection: str | None = None) -> dict:
|
|
59
|
+
"""Run an fn CLI command and return parsed JSON or raw text."""
|
|
60
|
+
cmd = [FN_BIN]
|
|
61
|
+
cmd += args
|
|
62
|
+
cmd += ["--quiet", "--json"]
|
|
63
|
+
if collection:
|
|
64
|
+
cmd += ["--collection", collection]
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
68
|
+
except FileNotFoundError:
|
|
69
|
+
return {"error": f"fn CLI not found. Tried: {FN_BIN}"}
|
|
70
|
+
except subprocess.TimeoutExpired:
|
|
71
|
+
return {"error": "fn command timed out after 30 seconds"}
|
|
72
|
+
|
|
73
|
+
output = result.stdout.strip()
|
|
74
|
+
if result.returncode != 0:
|
|
75
|
+
err = result.stderr.strip() or output or "Unknown error"
|
|
76
|
+
return {"error": err}
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
return json.loads(output)
|
|
80
|
+
except json.JSONDecodeError:
|
|
81
|
+
return {"text": output}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def run_fn_raw(args: list[str], collection: str | None = None) -> str:
|
|
85
|
+
"""Run fn without --json, return raw stdout."""
|
|
86
|
+
cmd = [FN_BIN]
|
|
87
|
+
cmd += args
|
|
88
|
+
cmd += ["--quiet"]
|
|
89
|
+
if collection:
|
|
90
|
+
cmd += ["--collection", collection]
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
94
|
+
except FileNotFoundError:
|
|
95
|
+
return f"Error: fn CLI not found at {FN_BIN}"
|
|
96
|
+
except subprocess.TimeoutExpired:
|
|
97
|
+
return "Error: command timed out"
|
|
98
|
+
|
|
99
|
+
if result.returncode != 0:
|
|
100
|
+
return result.stderr.strip() or result.stdout.strip() or "Unknown error"
|
|
101
|
+
return result.stdout.strip()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _task_selector(text: str | None, task_id: str | None) -> list[str] | None:
|
|
105
|
+
"""Build the CLI args that identify a single task.
|
|
106
|
+
|
|
107
|
+
Prefers the exact paragraph UUID (`--id`, from `list_tasks`), which targets
|
|
108
|
+
one task unambiguously even when several share the same wording. Falls back
|
|
109
|
+
to a case-insensitive substring of the task text. A substring must match
|
|
110
|
+
exactly one task or the CLI refuses the command (use task_id to disambiguate).
|
|
111
|
+
Returns None when neither is supplied so the caller can surface an error.
|
|
112
|
+
"""
|
|
113
|
+
if task_id:
|
|
114
|
+
return ["--id", task_id]
|
|
115
|
+
if text:
|
|
116
|
+
return [text]
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# MCP Server
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
mcp = FastMCP("FoldNotes")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ---- Version ----
|
|
128
|
+
|
|
129
|
+
@mcp.tool()
|
|
130
|
+
def version() -> str:
|
|
131
|
+
"""Show the FoldNotes MCP server version and fn CLI version."""
|
|
132
|
+
fn_version = run_fn_raw(["--version"])
|
|
133
|
+
return json.dumps({
|
|
134
|
+
"mcp_server": __version__,
|
|
135
|
+
"fn_cli": fn_version,
|
|
136
|
+
"fn_path": FN_BIN,
|
|
137
|
+
}, indent=2)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---- Notes: List ----
|
|
141
|
+
|
|
142
|
+
@mcp.tool()
|
|
143
|
+
def list_notes(
|
|
144
|
+
tag: str | None = None,
|
|
145
|
+
property: str | None = None,
|
|
146
|
+
sort: str | None = None,
|
|
147
|
+
limit: int | None = None,
|
|
148
|
+
favourites: bool = False,
|
|
149
|
+
has_tasks: bool = False,
|
|
150
|
+
has_overdue: bool = False,
|
|
151
|
+
include_trashed: bool = False,
|
|
152
|
+
archived: bool = False,
|
|
153
|
+
include_archived: bool = False,
|
|
154
|
+
include_subtags: bool = False,
|
|
155
|
+
modified_after: str | None = None,
|
|
156
|
+
modified_before: str | None = None,
|
|
157
|
+
reverse: bool = False,
|
|
158
|
+
collection: str | None = None,
|
|
159
|
+
) -> str:
|
|
160
|
+
"""List notes in the active FoldNotes collection with filtering and sorting.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
tag: Filter by tag name (without #).
|
|
164
|
+
property: Filter by property (key=value).
|
|
165
|
+
sort: Sort by: modified, created, title.
|
|
166
|
+
limit: Max notes to return.
|
|
167
|
+
favourites: Only show favourited notes.
|
|
168
|
+
has_tasks: Only show notes with active tasks.
|
|
169
|
+
has_overdue: Only show notes with overdue tasks.
|
|
170
|
+
include_trashed: Include trashed notes in results.
|
|
171
|
+
archived: Show only archived notes.
|
|
172
|
+
include_archived: Include archived notes alongside active ones (distinct from `archived`, which shows only archived).
|
|
173
|
+
include_subtags: When filtering by tag, also match sub-tags (e.g. projects matches projects/marketing).
|
|
174
|
+
modified_after: Only notes modified after this date (YYYY-MM-DD).
|
|
175
|
+
modified_before: Only notes modified before this date (YYYY-MM-DD).
|
|
176
|
+
reverse: Reverse sort order.
|
|
177
|
+
collection: Collection name, UUID, or path (default: active).
|
|
178
|
+
"""
|
|
179
|
+
args = ["list"]
|
|
180
|
+
if tag:
|
|
181
|
+
args += ["--tag", tag]
|
|
182
|
+
if property:
|
|
183
|
+
args += ["--property", property]
|
|
184
|
+
if sort:
|
|
185
|
+
args += ["--sort", sort]
|
|
186
|
+
if limit:
|
|
187
|
+
args += ["--limit", str(limit)]
|
|
188
|
+
if favourites:
|
|
189
|
+
args.append("--favourites")
|
|
190
|
+
if has_tasks:
|
|
191
|
+
args.append("--has-tasks")
|
|
192
|
+
if has_overdue:
|
|
193
|
+
args.append("--has-overdue")
|
|
194
|
+
if include_trashed:
|
|
195
|
+
args.append("--include-trashed")
|
|
196
|
+
if archived:
|
|
197
|
+
args.append("--archived")
|
|
198
|
+
if include_archived:
|
|
199
|
+
args.append("--include-archived")
|
|
200
|
+
if include_subtags:
|
|
201
|
+
args.append("--include-subtags")
|
|
202
|
+
if modified_after:
|
|
203
|
+
args += ["--modified-after", modified_after]
|
|
204
|
+
if modified_before:
|
|
205
|
+
args += ["--modified-before", modified_before]
|
|
206
|
+
if reverse:
|
|
207
|
+
args.append("--reverse")
|
|
208
|
+
return json.dumps(run_fn(args, collection), indent=2)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---- Notes: Show ----
|
|
212
|
+
|
|
213
|
+
@mcp.tool()
|
|
214
|
+
def show_note(
|
|
215
|
+
note: str | None = None,
|
|
216
|
+
id: str | None = None,
|
|
217
|
+
body: bool = True,
|
|
218
|
+
properties: bool = False,
|
|
219
|
+
tasks: bool = False,
|
|
220
|
+
backlinks: bool = False,
|
|
221
|
+
collection: str | None = None,
|
|
222
|
+
) -> str:
|
|
223
|
+
"""Read a note's content and metadata. Resolves by title, UUID, or filename.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
note: Note title, UUID, or filename.
|
|
227
|
+
id: Note UUID (exact lookup; alternative to the fuzzy note/title).
|
|
228
|
+
body: Include full body text (default: true).
|
|
229
|
+
properties: Show only front matter / properties.
|
|
230
|
+
tasks: Show tasks in the note.
|
|
231
|
+
backlinks: Show notes that link to this one.
|
|
232
|
+
collection: Collection name, UUID, or path.
|
|
233
|
+
"""
|
|
234
|
+
args = ["show"]
|
|
235
|
+
if id:
|
|
236
|
+
args += ["--id", id]
|
|
237
|
+
elif note:
|
|
238
|
+
args.append(note)
|
|
239
|
+
if properties:
|
|
240
|
+
args.append("--properties")
|
|
241
|
+
elif tasks:
|
|
242
|
+
args.append("--tasks")
|
|
243
|
+
elif backlinks:
|
|
244
|
+
args.append("--backlinks")
|
|
245
|
+
elif body:
|
|
246
|
+
args.append("--body")
|
|
247
|
+
return json.dumps(run_fn(args, collection), indent=2)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# ---- Notes: Create ----
|
|
251
|
+
|
|
252
|
+
@mcp.tool()
|
|
253
|
+
def create_note(
|
|
254
|
+
title: str,
|
|
255
|
+
content: str | None = None,
|
|
256
|
+
tags: list[str] | None = None,
|
|
257
|
+
properties: list[str] | None = None,
|
|
258
|
+
collection: str | None = None,
|
|
259
|
+
) -> str:
|
|
260
|
+
"""Create a new note with optional content, tags, and properties.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
title: Note title (becomes the filename).
|
|
264
|
+
content: Note body content (markdown).
|
|
265
|
+
tags: Tags to add (without #).
|
|
266
|
+
properties: User properties as key=value strings.
|
|
267
|
+
collection: Collection name, UUID, or path.
|
|
268
|
+
"""
|
|
269
|
+
args = ["create", title]
|
|
270
|
+
if content:
|
|
271
|
+
args += ["--content", content]
|
|
272
|
+
for t in tags or []:
|
|
273
|
+
args += ["--tag", t]
|
|
274
|
+
for p in properties or []:
|
|
275
|
+
args += ["--property", p]
|
|
276
|
+
return json.dumps(run_fn(args, collection), indent=2)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# ---- Notes: Edit ----
|
|
280
|
+
|
|
281
|
+
@mcp.tool()
|
|
282
|
+
def edit_note(
|
|
283
|
+
note: str,
|
|
284
|
+
append: str | None = None,
|
|
285
|
+
prepend_after_heading: str | None = None,
|
|
286
|
+
content: str | None = None,
|
|
287
|
+
set_property: list[str] | None = None,
|
|
288
|
+
remove_property: list[str] | None = None,
|
|
289
|
+
favourite: bool = False,
|
|
290
|
+
unfavourite: bool = False,
|
|
291
|
+
archive: bool = False,
|
|
292
|
+
unarchive: bool = False,
|
|
293
|
+
force: bool = False,
|
|
294
|
+
collection: str | None = None,
|
|
295
|
+
) -> str:
|
|
296
|
+
"""Modify a note's content or properties. Property values are validated
|
|
297
|
+
against the schema (name casing, type, select options) unless force=True.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
note: Note title, UUID, or filename.
|
|
301
|
+
append: Text to append to the end of the note.
|
|
302
|
+
prepend_after_heading: Text to insert after the first heading.
|
|
303
|
+
content: Replace entire body with this content.
|
|
304
|
+
set_property: Properties to set as key=value strings. Validated against schema.
|
|
305
|
+
remove_property: Properties to remove by key name.
|
|
306
|
+
favourite: Mark as favourite.
|
|
307
|
+
unfavourite: Remove favourite.
|
|
308
|
+
archive: Archive the note.
|
|
309
|
+
unarchive: Unarchive the note.
|
|
310
|
+
force: Bypass property schema validation.
|
|
311
|
+
collection: Collection name, UUID, or path.
|
|
312
|
+
"""
|
|
313
|
+
args = ["edit", note]
|
|
314
|
+
if append:
|
|
315
|
+
args += ["--append", append]
|
|
316
|
+
if prepend_after_heading:
|
|
317
|
+
args += ["--prepend-after-heading", prepend_after_heading]
|
|
318
|
+
if content:
|
|
319
|
+
args += ["--content", content]
|
|
320
|
+
for p in set_property or []:
|
|
321
|
+
args += ["--set-property", p]
|
|
322
|
+
for p in remove_property or []:
|
|
323
|
+
args += ["--remove-property", p]
|
|
324
|
+
if favourite:
|
|
325
|
+
args.append("--favourite")
|
|
326
|
+
if unfavourite:
|
|
327
|
+
args.append("--unfavourite")
|
|
328
|
+
if archive:
|
|
329
|
+
args.append("--archive")
|
|
330
|
+
if unarchive:
|
|
331
|
+
args.append("--unarchive")
|
|
332
|
+
if force:
|
|
333
|
+
args.append("--force")
|
|
334
|
+
return json.dumps(run_fn(args, collection), indent=2)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# ---- Notes: Delete ----
|
|
338
|
+
|
|
339
|
+
@mcp.tool()
|
|
340
|
+
def delete_note(
|
|
341
|
+
note: str,
|
|
342
|
+
collection: str | None = None,
|
|
343
|
+
) -> str:
|
|
344
|
+
"""Soft-delete a note: moves it to the .trash/ folder. Always reversible
|
|
345
|
+
with restore_note.
|
|
346
|
+
|
|
347
|
+
Deletion via this MCP is intentionally reversible — notes go to the trash,
|
|
348
|
+
never permanently. Emptying the trash / permanent deletion is deliberately
|
|
349
|
+
NOT exposed here; that irreversible step is left to the user in the app.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
note: Note title, UUID, or filename.
|
|
353
|
+
collection: Collection name, UUID, or path.
|
|
354
|
+
"""
|
|
355
|
+
return json.dumps(run_fn(["delete", note], collection), indent=2)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@mcp.tool()
|
|
359
|
+
def restore_note(
|
|
360
|
+
note: str,
|
|
361
|
+
collection: str | None = None,
|
|
362
|
+
) -> str:
|
|
363
|
+
"""Restore a note from the trash — the inverse of delete_note.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
note: Trashed note title, UUID, or filename.
|
|
367
|
+
collection: Collection name, UUID, or path.
|
|
368
|
+
"""
|
|
369
|
+
return json.dumps(run_fn(["restore", note], collection), indent=2)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# ---- Notes: Rename ----
|
|
373
|
+
|
|
374
|
+
@mcp.tool()
|
|
375
|
+
def rename_note(
|
|
376
|
+
note: str,
|
|
377
|
+
new_name: str,
|
|
378
|
+
collection: str | None = None,
|
|
379
|
+
) -> str:
|
|
380
|
+
"""Rename a note (changes the filename on disk).
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
note: Current note title, UUID, or filename.
|
|
384
|
+
new_name: New name for the note.
|
|
385
|
+
collection: Collection name, UUID, or path.
|
|
386
|
+
"""
|
|
387
|
+
return json.dumps(run_fn(["rename", note, new_name], collection), indent=2)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# ---- Notes: Search ----
|
|
391
|
+
|
|
392
|
+
@mcp.tool()
|
|
393
|
+
def search_notes(
|
|
394
|
+
query: str,
|
|
395
|
+
context: int = 2,
|
|
396
|
+
tag: str | None = None,
|
|
397
|
+
limit: int | None = None,
|
|
398
|
+
titles_only: bool = False,
|
|
399
|
+
regex: bool = False,
|
|
400
|
+
collection: str | None = None,
|
|
401
|
+
) -> str:
|
|
402
|
+
"""Full-text search across all notes in the collection.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
query: Search query string.
|
|
406
|
+
context: Lines of context around matches (default: 2).
|
|
407
|
+
tag: Filter results to notes with this tag.
|
|
408
|
+
limit: Maximum number of results.
|
|
409
|
+
titles_only: Search titles only, not body content.
|
|
410
|
+
regex: Treat query as a regular expression.
|
|
411
|
+
collection: Collection name, UUID, or path.
|
|
412
|
+
"""
|
|
413
|
+
args = ["search", query, "--context", str(context)]
|
|
414
|
+
if tag:
|
|
415
|
+
args += ["--tag", tag]
|
|
416
|
+
if limit:
|
|
417
|
+
args += ["--limit", str(limit)]
|
|
418
|
+
if titles_only:
|
|
419
|
+
args.append("--titles-only")
|
|
420
|
+
if regex:
|
|
421
|
+
args.append("--regex")
|
|
422
|
+
return json.dumps(run_fn(args, collection), indent=2)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
# ---- Tags ----
|
|
426
|
+
|
|
427
|
+
@mcp.tool()
|
|
428
|
+
def list_tags(
|
|
429
|
+
tag: str | None = None,
|
|
430
|
+
prefix: str | None = None,
|
|
431
|
+
collection: str | None = None,
|
|
432
|
+
) -> str:
|
|
433
|
+
"""List all tags with note counts, or show notes for a specific tag.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
tag: Show notes tagged with this tag.
|
|
437
|
+
prefix: Filter tags by prefix.
|
|
438
|
+
collection: Collection name, UUID, or path.
|
|
439
|
+
"""
|
|
440
|
+
args = ["tags"]
|
|
441
|
+
if tag:
|
|
442
|
+
args.append(tag)
|
|
443
|
+
if prefix:
|
|
444
|
+
args += ["--prefix", prefix]
|
|
445
|
+
return json.dumps(run_fn(args, collection), indent=2)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# ---- Tasks: List ----
|
|
449
|
+
|
|
450
|
+
@mcp.tool()
|
|
451
|
+
def list_tasks(
|
|
452
|
+
due_today: bool = False,
|
|
453
|
+
due_this_week: bool = False,
|
|
454
|
+
overdue: bool = False,
|
|
455
|
+
status: str | None = None,
|
|
456
|
+
priority: str | None = None,
|
|
457
|
+
note: str | None = None,
|
|
458
|
+
project: str | None = None,
|
|
459
|
+
all: bool = False,
|
|
460
|
+
collection: str | None = None,
|
|
461
|
+
) -> str:
|
|
462
|
+
"""List tasks across all notes with filtering.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
due_today: Show only tasks due today.
|
|
466
|
+
due_this_week: Show tasks due this week.
|
|
467
|
+
overdue: Show only overdue tasks.
|
|
468
|
+
status: Filter by status: not-started, in-progress, done, cancelled.
|
|
469
|
+
priority: Filter by priority: high, medium, low.
|
|
470
|
+
note: Filter by note title.
|
|
471
|
+
project: Filter by project name.
|
|
472
|
+
all: Include done and cancelled tasks.
|
|
473
|
+
collection: Collection name, UUID, or path.
|
|
474
|
+
"""
|
|
475
|
+
args = ["tasks"]
|
|
476
|
+
if due_today:
|
|
477
|
+
args.append("--due-today")
|
|
478
|
+
if due_this_week:
|
|
479
|
+
args.append("--due-this-week")
|
|
480
|
+
if overdue:
|
|
481
|
+
args.append("--overdue")
|
|
482
|
+
if status:
|
|
483
|
+
args += ["--status", status]
|
|
484
|
+
if priority:
|
|
485
|
+
args += ["--priority", priority]
|
|
486
|
+
if note:
|
|
487
|
+
args += ["--note", note]
|
|
488
|
+
if project:
|
|
489
|
+
args += ["--project", project]
|
|
490
|
+
if all:
|
|
491
|
+
args.append("--all")
|
|
492
|
+
return json.dumps(run_fn(args, collection), indent=2)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
# ---- Tasks: Add ----
|
|
496
|
+
|
|
497
|
+
@mcp.tool()
|
|
498
|
+
def add_task(
|
|
499
|
+
text: str,
|
|
500
|
+
note: str,
|
|
501
|
+
due: str | None = None,
|
|
502
|
+
priority: str | None = None,
|
|
503
|
+
project: str | None = None,
|
|
504
|
+
collection: str | None = None,
|
|
505
|
+
) -> str:
|
|
506
|
+
"""Add a new task to a note.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
text: Task description text.
|
|
510
|
+
note: Target note title to add the task to.
|
|
511
|
+
due: Due date (YYYY-MM-DD, 'today', 'tomorrow', 'next monday', etc.).
|
|
512
|
+
priority: Priority level: high, medium, low.
|
|
513
|
+
project: Project name for the task.
|
|
514
|
+
collection: Collection name, UUID, or path.
|
|
515
|
+
"""
|
|
516
|
+
args = ["tasks", "add", text, "--note", note]
|
|
517
|
+
if due:
|
|
518
|
+
args += ["--due", due]
|
|
519
|
+
if priority:
|
|
520
|
+
args += ["--priority", priority]
|
|
521
|
+
if project:
|
|
522
|
+
args += ["--project", project]
|
|
523
|
+
return json.dumps(run_fn(args, collection), indent=2)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# ---- Tasks: Complete ----
|
|
527
|
+
|
|
528
|
+
@mcp.tool()
|
|
529
|
+
def complete_task(
|
|
530
|
+
note: str,
|
|
531
|
+
text: str | None = None,
|
|
532
|
+
task_id: str | None = None,
|
|
533
|
+
collection: str | None = None,
|
|
534
|
+
) -> str:
|
|
535
|
+
"""Mark a task as done.
|
|
536
|
+
|
|
537
|
+
Identify the task by `task_id` (exact, preferred) or `text` (substring).
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
note: Note title containing the task.
|
|
541
|
+
text: Task text to match (case-insensitive substring). Must match exactly
|
|
542
|
+
one task; if several match, the command is refused — pass task_id.
|
|
543
|
+
task_id: Exact task UUID from `list_tasks`. Preferred — it targets the
|
|
544
|
+
precise task even when several share the same wording.
|
|
545
|
+
collection: Collection name, UUID, or path.
|
|
546
|
+
"""
|
|
547
|
+
sel = _task_selector(text, task_id)
|
|
548
|
+
if sel is None:
|
|
549
|
+
return json.dumps({"error": "Provide either text or task_id."}, indent=2)
|
|
550
|
+
return json.dumps(run_fn(["tasks", "complete"] + sel + ["--note", note], collection), indent=2)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
# ---- Tasks: Cancel ----
|
|
554
|
+
|
|
555
|
+
@mcp.tool()
|
|
556
|
+
def cancel_task(
|
|
557
|
+
note: str,
|
|
558
|
+
text: str | None = None,
|
|
559
|
+
task_id: str | None = None,
|
|
560
|
+
collection: str | None = None,
|
|
561
|
+
) -> str:
|
|
562
|
+
"""Cancel a task.
|
|
563
|
+
|
|
564
|
+
Identify the task by `task_id` (exact, preferred) or `text` (substring).
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
note: Note title containing the task.
|
|
568
|
+
text: Task text to match (case-insensitive substring). Must match exactly
|
|
569
|
+
one task; if several match, the command is refused — pass task_id.
|
|
570
|
+
task_id: Exact task UUID from `list_tasks`. Preferred — it targets the
|
|
571
|
+
precise task even when several share the same wording.
|
|
572
|
+
collection: Collection name, UUID, or path.
|
|
573
|
+
"""
|
|
574
|
+
sel = _task_selector(text, task_id)
|
|
575
|
+
if sel is None:
|
|
576
|
+
return json.dumps({"error": "Provide either text or task_id."}, indent=2)
|
|
577
|
+
return json.dumps(run_fn(["tasks", "cancel"] + sel + ["--note", note], collection), indent=2)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
# ---- Tasks: Progress ----
|
|
581
|
+
|
|
582
|
+
@mcp.tool()
|
|
583
|
+
def start_task(
|
|
584
|
+
note: str,
|
|
585
|
+
text: str | None = None,
|
|
586
|
+
task_id: str | None = None,
|
|
587
|
+
collection: str | None = None,
|
|
588
|
+
) -> str:
|
|
589
|
+
"""Mark a task as in-progress.
|
|
590
|
+
|
|
591
|
+
Identify the task by `task_id` (exact, preferred) or `text` (substring).
|
|
592
|
+
|
|
593
|
+
Args:
|
|
594
|
+
note: Note title containing the task.
|
|
595
|
+
text: Task text to match (case-insensitive substring). Must match exactly
|
|
596
|
+
one task; if several match, the command is refused — pass task_id.
|
|
597
|
+
task_id: Exact task UUID from `list_tasks`. Preferred — it targets the
|
|
598
|
+
precise task even when several share the same wording.
|
|
599
|
+
collection: Collection name, UUID, or path.
|
|
600
|
+
"""
|
|
601
|
+
sel = _task_selector(text, task_id)
|
|
602
|
+
if sel is None:
|
|
603
|
+
return json.dumps({"error": "Provide either text or task_id."}, indent=2)
|
|
604
|
+
return json.dumps(run_fn(["tasks", "progress"] + sel + ["--note", note], collection), indent=2)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
# ---- Tasks: Reset ----
|
|
608
|
+
|
|
609
|
+
@mcp.tool()
|
|
610
|
+
def reset_task(
|
|
611
|
+
note: str,
|
|
612
|
+
text: str | None = None,
|
|
613
|
+
task_id: str | None = None,
|
|
614
|
+
collection: str | None = None,
|
|
615
|
+
) -> str:
|
|
616
|
+
"""Reset a task to not-started.
|
|
617
|
+
|
|
618
|
+
Identify the task by `task_id` (exact, preferred) or `text` (substring).
|
|
619
|
+
|
|
620
|
+
Args:
|
|
621
|
+
note: Note title containing the task.
|
|
622
|
+
text: Task text to match (case-insensitive substring). Must match exactly
|
|
623
|
+
one task; if several match, the command is refused — pass task_id.
|
|
624
|
+
task_id: Exact task UUID from `list_tasks`. Preferred — it targets the
|
|
625
|
+
precise task even when several share the same wording.
|
|
626
|
+
collection: Collection name, UUID, or path.
|
|
627
|
+
"""
|
|
628
|
+
sel = _task_selector(text, task_id)
|
|
629
|
+
if sel is None:
|
|
630
|
+
return json.dumps({"error": "Provide either text or task_id."}, indent=2)
|
|
631
|
+
return json.dumps(run_fn(["tasks", "reset"] + sel + ["--note", note], collection), indent=2)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
# ---- Tasks: Set (amend due / priority / project) ----
|
|
635
|
+
|
|
636
|
+
@mcp.tool()
|
|
637
|
+
def set_task(
|
|
638
|
+
note: str,
|
|
639
|
+
text: str | None = None,
|
|
640
|
+
task_id: str | None = None,
|
|
641
|
+
due: str | None = None,
|
|
642
|
+
clear_due: bool = False,
|
|
643
|
+
priority: str | None = None,
|
|
644
|
+
clear_priority: bool = False,
|
|
645
|
+
project: str | None = None,
|
|
646
|
+
clear_project: bool = False,
|
|
647
|
+
collection: str | None = None,
|
|
648
|
+
) -> str:
|
|
649
|
+
"""Amend an existing task's due date, priority, or project.
|
|
650
|
+
|
|
651
|
+
Identify the task by `task_id` (exact, preferred) or `text` (substring),
|
|
652
|
+
then pass at least one field to change. Amending metadata preserves the
|
|
653
|
+
task's stable UUID (identity is hashed over the prose, not the metadata).
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
note: Note title containing the task.
|
|
657
|
+
text: Task text to match (case-insensitive substring). Must match exactly
|
|
658
|
+
one task; if several match, the command is refused — pass task_id.
|
|
659
|
+
task_id: Exact task UUID from `list_tasks`. Preferred.
|
|
660
|
+
due: New due date (YYYY-MM-DD, 'today', 'tomorrow', 'next monday', ...).
|
|
661
|
+
clear_due: Remove the due date instead of setting one.
|
|
662
|
+
priority: New priority: high, medium, low.
|
|
663
|
+
clear_priority: Remove the priority.
|
|
664
|
+
project: New project name.
|
|
665
|
+
clear_project: Remove the project.
|
|
666
|
+
collection: Collection name, UUID, or path.
|
|
667
|
+
"""
|
|
668
|
+
sel = _task_selector(text, task_id)
|
|
669
|
+
if sel is None:
|
|
670
|
+
return json.dumps({"error": "Provide either text or task_id."}, indent=2)
|
|
671
|
+
args = ["tasks", "set"] + sel + ["--note", note]
|
|
672
|
+
if due:
|
|
673
|
+
args += ["--due", due]
|
|
674
|
+
if clear_due:
|
|
675
|
+
args += ["--clear-due"]
|
|
676
|
+
if priority:
|
|
677
|
+
args += ["--priority", priority]
|
|
678
|
+
if clear_priority:
|
|
679
|
+
args += ["--clear-priority"]
|
|
680
|
+
if project:
|
|
681
|
+
args += ["--project", project]
|
|
682
|
+
if clear_project:
|
|
683
|
+
args += ["--clear-project"]
|
|
684
|
+
return json.dumps(run_fn(args, collection), indent=2)
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
# ---- Tasks: Remove ----
|
|
688
|
+
|
|
689
|
+
@mcp.tool()
|
|
690
|
+
def remove_task(
|
|
691
|
+
note: str,
|
|
692
|
+
text: str | None = None,
|
|
693
|
+
task_id: str | None = None,
|
|
694
|
+
collection: str | None = None,
|
|
695
|
+
) -> str:
|
|
696
|
+
"""Delete a task line from a note.
|
|
697
|
+
|
|
698
|
+
Identify the task by `task_id` (exact, preferred) or `text` (substring).
|
|
699
|
+
This permanently removes the task line from the note body — unlike
|
|
700
|
+
delete_note it does not go to the trash. Prefer `task_id`, and confirm with
|
|
701
|
+
`list_tasks` first when matching by text. Surviving tasks keep their UUIDs.
|
|
702
|
+
|
|
703
|
+
Args:
|
|
704
|
+
note: Note title containing the task.
|
|
705
|
+
text: Task text to match (case-insensitive substring). Must match exactly
|
|
706
|
+
one task; if several match, the command is refused — pass task_id.
|
|
707
|
+
task_id: Exact task UUID from `list_tasks`. Preferred.
|
|
708
|
+
collection: Collection name, UUID, or path.
|
|
709
|
+
"""
|
|
710
|
+
sel = _task_selector(text, task_id)
|
|
711
|
+
if sel is None:
|
|
712
|
+
return json.dumps({"error": "Provide either text or task_id."}, indent=2)
|
|
713
|
+
return json.dumps(run_fn(["tasks", "remove"] + sel + ["--note", note], collection), indent=2)
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
# ---- Tasks: Projects ----
|
|
717
|
+
|
|
718
|
+
@mcp.tool()
|
|
719
|
+
def list_projects(collection: str | None = None) -> str:
|
|
720
|
+
"""List all projects (derived from task metadata).
|
|
721
|
+
|
|
722
|
+
Args:
|
|
723
|
+
collection: Collection name, UUID, or path.
|
|
724
|
+
"""
|
|
725
|
+
return json.dumps(run_fn(["tasks", "projects"], collection), indent=2)
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
# ---- Backlinks ----
|
|
729
|
+
|
|
730
|
+
@mcp.tool()
|
|
731
|
+
def backlinks(
|
|
732
|
+
note: str,
|
|
733
|
+
context: bool = False,
|
|
734
|
+
collection: str | None = None,
|
|
735
|
+
) -> str:
|
|
736
|
+
"""Show notes that reference (link to) a target note.
|
|
737
|
+
|
|
738
|
+
Args:
|
|
739
|
+
note: Target note title, UUID, or filename.
|
|
740
|
+
context: Show the paragraph containing the [[link]].
|
|
741
|
+
collection: Collection name, UUID, or path.
|
|
742
|
+
"""
|
|
743
|
+
args = ["backlinks", note]
|
|
744
|
+
if context:
|
|
745
|
+
args.append("--context")
|
|
746
|
+
return json.dumps(run_fn(args, collection), indent=2)
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
# ---- Daily Notes ----
|
|
750
|
+
|
|
751
|
+
@mcp.tool()
|
|
752
|
+
def daily_note(
|
|
753
|
+
date: str | None = None,
|
|
754
|
+
create: bool = False,
|
|
755
|
+
tasks: bool = False,
|
|
756
|
+
collection: str | None = None,
|
|
757
|
+
) -> str:
|
|
758
|
+
"""Show or create a daily note.
|
|
759
|
+
|
|
760
|
+
Args:
|
|
761
|
+
date: Date (YYYY-MM-DD, 'today', 'yesterday', 'tomorrow'). Default: today.
|
|
762
|
+
create: Create the daily note if it doesn't exist.
|
|
763
|
+
tasks: Show tasks from the daily note.
|
|
764
|
+
collection: Collection name, UUID, or path.
|
|
765
|
+
"""
|
|
766
|
+
args = ["daily"]
|
|
767
|
+
if date:
|
|
768
|
+
args += [date]
|
|
769
|
+
if create:
|
|
770
|
+
args.append("--create")
|
|
771
|
+
if tasks:
|
|
772
|
+
args.append("--tasks")
|
|
773
|
+
return json.dumps(run_fn(args, collection), indent=2)
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
@mcp.tool()
|
|
777
|
+
def daily_append(
|
|
778
|
+
text: str,
|
|
779
|
+
date: str | None = None,
|
|
780
|
+
collection: str | None = None,
|
|
781
|
+
) -> str:
|
|
782
|
+
"""Append text to a daily note (creates it if needed).
|
|
783
|
+
|
|
784
|
+
Args:
|
|
785
|
+
text: Text to append.
|
|
786
|
+
date: Date for the daily note (default: today).
|
|
787
|
+
collection: Collection name, UUID, or path.
|
|
788
|
+
"""
|
|
789
|
+
args = ["daily", "append", text]
|
|
790
|
+
if date:
|
|
791
|
+
args += ["--date", date]
|
|
792
|
+
return json.dumps(run_fn(args, collection), indent=2)
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
# ---- Collections ----
|
|
796
|
+
|
|
797
|
+
@mcp.tool()
|
|
798
|
+
def collection_info(collection: str | None = None) -> str:
|
|
799
|
+
"""Show information about the active collection (path, note/tag counts, cache status).
|
|
800
|
+
|
|
801
|
+
Args:
|
|
802
|
+
collection: Collection name, UUID, or path.
|
|
803
|
+
"""
|
|
804
|
+
return json.dumps(run_fn(["collection", "info"], collection), indent=2)
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
@mcp.tool()
|
|
808
|
+
def list_collections() -> str:
|
|
809
|
+
"""List all registered collections."""
|
|
810
|
+
return json.dumps(run_fn(["collections"]), indent=2)
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
@mcp.tool()
|
|
814
|
+
def switch_collection(reference: str) -> str:
|
|
815
|
+
"""Switch the active collection.
|
|
816
|
+
|
|
817
|
+
Args:
|
|
818
|
+
reference: Collection name, UUID, or path.
|
|
819
|
+
"""
|
|
820
|
+
return json.dumps(run_fn(["collection", "switch", reference]), indent=2)
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
# ---- Properties: Schema ----
|
|
824
|
+
|
|
825
|
+
@mcp.tool()
|
|
826
|
+
def list_properties(collection: str | None = None) -> str:
|
|
827
|
+
"""List all property definitions in the collection schema.
|
|
828
|
+
Shows name, type, options, validation rules, and usage counts.
|
|
829
|
+
|
|
830
|
+
Args:
|
|
831
|
+
collection: Collection name, UUID, or path.
|
|
832
|
+
"""
|
|
833
|
+
return json.dumps(run_fn(["properties", "list"], collection), indent=2)
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
@mcp.tool()
|
|
837
|
+
def show_property(
|
|
838
|
+
name: str,
|
|
839
|
+
collection: str | None = None,
|
|
840
|
+
) -> str:
|
|
841
|
+
"""Show details of a property definition including type, options,
|
|
842
|
+
validation rules, and which notes use it.
|
|
843
|
+
|
|
844
|
+
Args:
|
|
845
|
+
name: Property name (case-insensitive).
|
|
846
|
+
collection: Collection name, UUID, or path.
|
|
847
|
+
"""
|
|
848
|
+
return json.dumps(run_fn(["properties", "show", name], collection), indent=2)
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
@mcp.tool()
|
|
852
|
+
def add_property(
|
|
853
|
+
name: str,
|
|
854
|
+
type: str,
|
|
855
|
+
options: str | None = None,
|
|
856
|
+
required: bool = False,
|
|
857
|
+
collection: str | None = None,
|
|
858
|
+
) -> str:
|
|
859
|
+
"""Create a new property definition in the collection schema.
|
|
860
|
+
|
|
861
|
+
Args:
|
|
862
|
+
name: Property name.
|
|
863
|
+
type: Property type: text, number, date, dateTime, checkbox, singleSelect, multiSelect, url, email, phone, rating.
|
|
864
|
+
options: Comma-separated options (required for singleSelect/multiSelect).
|
|
865
|
+
required: Mark this property as required.
|
|
866
|
+
collection: Collection name, UUID, or path.
|
|
867
|
+
"""
|
|
868
|
+
args = ["properties", "add", name, "--type", type]
|
|
869
|
+
if options:
|
|
870
|
+
args += ["--property-options", options]
|
|
871
|
+
if required:
|
|
872
|
+
args.append("--required")
|
|
873
|
+
return json.dumps(run_fn(args, collection), indent=2)
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
@mcp.tool()
|
|
877
|
+
def delete_property(
|
|
878
|
+
name: str,
|
|
879
|
+
force: bool = False,
|
|
880
|
+
collection: str | None = None,
|
|
881
|
+
) -> str:
|
|
882
|
+
"""Delete a property definition from the schema. Values in notes are preserved.
|
|
883
|
+
|
|
884
|
+
Deleting a definition that is still in use is guarded: with force=False
|
|
885
|
+
(the default) the CLI does NOT delete — it reports how many notes use the
|
|
886
|
+
property and asks you to re-run with force=True to confirm. Unused
|
|
887
|
+
definitions delete without needing force. Set force=True only when you
|
|
888
|
+
intend to remove a definition you know is still referenced.
|
|
889
|
+
|
|
890
|
+
Args:
|
|
891
|
+
name: Property name (case-insensitive).
|
|
892
|
+
force: Skip the "used by N notes" confirmation and delete anyway
|
|
893
|
+
(default: false — respects the CLI safety gate).
|
|
894
|
+
collection: Collection name, UUID, or path.
|
|
895
|
+
"""
|
|
896
|
+
args = ["properties", "delete", name]
|
|
897
|
+
if force:
|
|
898
|
+
args.append("--force")
|
|
899
|
+
return json.dumps(run_fn(args, collection), indent=2)
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
@mcp.tool()
|
|
903
|
+
def property_orphans(collection: str | None = None) -> str:
|
|
904
|
+
"""Find front matter property keys that have no schema definition.
|
|
905
|
+
Useful for detecting typos, case mismatches, or legacy properties.
|
|
906
|
+
|
|
907
|
+
Args:
|
|
908
|
+
collection: Collection name, UUID, or path.
|
|
909
|
+
"""
|
|
910
|
+
return json.dumps(run_fn(["properties", "orphans"], collection), indent=2)
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
@mcp.tool()
|
|
914
|
+
def property_notes(
|
|
915
|
+
name: str,
|
|
916
|
+
value: str | None = None,
|
|
917
|
+
collection: str | None = None,
|
|
918
|
+
) -> str:
|
|
919
|
+
"""List notes that use a specific property, optionally filtered by value.
|
|
920
|
+
|
|
921
|
+
Args:
|
|
922
|
+
name: Property name (case-insensitive).
|
|
923
|
+
value: Filter by value (case-insensitive).
|
|
924
|
+
collection: Collection name, UUID, or path.
|
|
925
|
+
"""
|
|
926
|
+
args = ["properties", "notes", name]
|
|
927
|
+
if value:
|
|
928
|
+
args += ["--value", value]
|
|
929
|
+
return json.dumps(run_fn(args, collection), indent=2)
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
# ---- Open in App ----
|
|
933
|
+
|
|
934
|
+
@mcp.tool()
|
|
935
|
+
def open_note(
|
|
936
|
+
note: str | None = None,
|
|
937
|
+
id: str | None = None,
|
|
938
|
+
daily: bool = False,
|
|
939
|
+
collection: str | None = None,
|
|
940
|
+
) -> str:
|
|
941
|
+
"""Open a note in the FoldNotes app via URL scheme.
|
|
942
|
+
|
|
943
|
+
Args:
|
|
944
|
+
note: Note title, UUID, or filename.
|
|
945
|
+
id: Note UUID (exact lookup; alternative to note).
|
|
946
|
+
daily: Open today's daily note instead.
|
|
947
|
+
collection: Collection name, UUID, or path.
|
|
948
|
+
"""
|
|
949
|
+
args = ["open"]
|
|
950
|
+
if daily:
|
|
951
|
+
args.append("--daily")
|
|
952
|
+
elif id:
|
|
953
|
+
args += ["--id", id]
|
|
954
|
+
elif note:
|
|
955
|
+
args.append(note)
|
|
956
|
+
return json.dumps(run_fn(args, collection), indent=2)
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
# ---------------------------------------------------------------------------
|
|
960
|
+
# v2.2.0 additions — extra fn CLI surface wrapped as thin passthroughs:
|
|
961
|
+
# saved queries, templates, project refactoring, attachments, archive,
|
|
962
|
+
# notifications. (bind, import, collection add/remove deliberately omitted.)
|
|
963
|
+
# ---------------------------------------------------------------------------
|
|
964
|
+
|
|
965
|
+
@mcp.tool()
|
|
966
|
+
def notifications(collection: str | None = None) -> str:
|
|
967
|
+
"""Show notification settings and upcoming task reminders.
|
|
968
|
+
|
|
969
|
+
Args:
|
|
970
|
+
collection: Collection name, UUID, or path.
|
|
971
|
+
"""
|
|
972
|
+
return json.dumps(run_fn(["notifications"], collection), indent=2)
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
@mcp.tool()
|
|
976
|
+
def list_queries(collection: str | None = None) -> str:
|
|
977
|
+
"""List saved queries in the collection (.queries/).
|
|
978
|
+
|
|
979
|
+
Args:
|
|
980
|
+
collection: Collection name, UUID, or path.
|
|
981
|
+
"""
|
|
982
|
+
return json.dumps(run_fn(["query", "list"], collection), indent=2)
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
@mcp.tool()
|
|
986
|
+
def run_query(name: str, limit: int | None = None, collection: str | None = None) -> str:
|
|
987
|
+
"""Run a saved query; returns the matching notes (same shape as list_notes).
|
|
988
|
+
|
|
989
|
+
Args:
|
|
990
|
+
name: Saved query name (or unambiguous prefix); see list_queries.
|
|
991
|
+
limit: Maximum number of results.
|
|
992
|
+
collection: Collection name, UUID, or path.
|
|
993
|
+
"""
|
|
994
|
+
args = ["query", "run", name]
|
|
995
|
+
if limit is not None:
|
|
996
|
+
args += ["--limit", str(limit)]
|
|
997
|
+
return json.dumps(run_fn(args, collection), indent=2)
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
@mcp.tool()
|
|
1001
|
+
def list_templates(collection: str | None = None) -> str:
|
|
1002
|
+
"""List available note templates.
|
|
1003
|
+
|
|
1004
|
+
Args:
|
|
1005
|
+
collection: Collection name, UUID, or path.
|
|
1006
|
+
"""
|
|
1007
|
+
return json.dumps(run_fn(["templates"], collection), indent=2)
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
@mcp.tool()
|
|
1011
|
+
def rename_project(
|
|
1012
|
+
old_name: str,
|
|
1013
|
+
new_name: str,
|
|
1014
|
+
include_subtree: bool = False,
|
|
1015
|
+
collection: str | None = None,
|
|
1016
|
+
) -> str:
|
|
1017
|
+
"""Rename a project across the whole collection (tags + task keywords).
|
|
1018
|
+
|
|
1019
|
+
Args:
|
|
1020
|
+
old_name: Existing project name.
|
|
1021
|
+
new_name: New project name.
|
|
1022
|
+
include_subtree: Also rename sub-projects (old/sub -> new/sub).
|
|
1023
|
+
collection: Collection name, UUID, or path.
|
|
1024
|
+
"""
|
|
1025
|
+
args = ["projects", "rename", old_name, new_name]
|
|
1026
|
+
if include_subtree:
|
|
1027
|
+
args.append("--include-subtree")
|
|
1028
|
+
return json.dumps(run_fn(args, collection), indent=2)
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
@mcp.tool()
|
|
1032
|
+
def strip_project(name: str, collection: str | None = None) -> str:
|
|
1033
|
+
"""Remove a project's tags and task keywords across the collection.
|
|
1034
|
+
|
|
1035
|
+
Args:
|
|
1036
|
+
name: Project name to strip.
|
|
1037
|
+
collection: Collection name, UUID, or path.
|
|
1038
|
+
"""
|
|
1039
|
+
return json.dumps(run_fn(["projects", "strip", name], collection), indent=2)
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
@mcp.tool()
|
|
1043
|
+
def list_attachments(collection: str | None = None) -> str:
|
|
1044
|
+
"""List images with size and reference count.
|
|
1045
|
+
|
|
1046
|
+
Args:
|
|
1047
|
+
collection: Collection name, UUID, or path.
|
|
1048
|
+
"""
|
|
1049
|
+
return json.dumps(run_fn(["attachments", "list"], collection), indent=2)
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
@mcp.tool()
|
|
1053
|
+
def orphan_attachments(collection: str | None = None) -> str:
|
|
1054
|
+
"""List images that no note references.
|
|
1055
|
+
|
|
1056
|
+
Args:
|
|
1057
|
+
collection: Collection name, UUID, or path.
|
|
1058
|
+
"""
|
|
1059
|
+
return json.dumps(run_fn(["attachments", "orphans"], collection), indent=2)
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
@mcp.tool()
|
|
1063
|
+
def prune_attachments(force: bool = False, collection: str | None = None) -> str:
|
|
1064
|
+
"""Delete orphaned images. Without force, only reports what would be deleted.
|
|
1065
|
+
|
|
1066
|
+
Args:
|
|
1067
|
+
force: Actually delete the orphaned images (default: dry-run).
|
|
1068
|
+
collection: Collection name, UUID, or path.
|
|
1069
|
+
"""
|
|
1070
|
+
args = ["attachments", "prune"]
|
|
1071
|
+
if force:
|
|
1072
|
+
args.append("--force")
|
|
1073
|
+
return json.dumps(run_fn(args, collection), indent=2)
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
@mcp.tool()
|
|
1077
|
+
def archive_note(note: str | None = None, id: str | None = None, collection: str | None = None) -> str:
|
|
1078
|
+
"""Archive a note (excluded from list_notes by default). Same as edit --archive.
|
|
1079
|
+
|
|
1080
|
+
Args:
|
|
1081
|
+
note: Note title.
|
|
1082
|
+
id: Note UUID (alternative to note).
|
|
1083
|
+
collection: Collection name, UUID, or path.
|
|
1084
|
+
"""
|
|
1085
|
+
args = ["archive"]
|
|
1086
|
+
if id:
|
|
1087
|
+
args += ["--id", id]
|
|
1088
|
+
elif note:
|
|
1089
|
+
args.append(note)
|
|
1090
|
+
return json.dumps(run_fn(args, collection), indent=2)
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
@mcp.tool()
|
|
1094
|
+
def unarchive_note(note: str | None = None, id: str | None = None, collection: str | None = None) -> str:
|
|
1095
|
+
"""Restore an archived note. Same as edit --unarchive.
|
|
1096
|
+
|
|
1097
|
+
Args:
|
|
1098
|
+
note: Note title.
|
|
1099
|
+
id: Note UUID (alternative to note).
|
|
1100
|
+
collection: Collection name, UUID, or path.
|
|
1101
|
+
"""
|
|
1102
|
+
args = ["unarchive"]
|
|
1103
|
+
if id:
|
|
1104
|
+
args += ["--id", id]
|
|
1105
|
+
elif note:
|
|
1106
|
+
args.append(note)
|
|
1107
|
+
return json.dumps(run_fn(args, collection), indent=2)
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def main() -> None:
|
|
1111
|
+
"""Console-script entry point for ``foldnotes-mcp`` / ``uvx foldnotes-mcp``.
|
|
1112
|
+
|
|
1113
|
+
Starts the MCP server on stdio — the transport Claude Desktop and Claude
|
|
1114
|
+
Code use. Equivalent to running ``python3 foldnotes_mcp.py`` directly.
|
|
1115
|
+
"""
|
|
1116
|
+
mcp.run()
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
if __name__ == "__main__":
|
|
1120
|
+
main()
|