@disco_trooper/apple-notes-mcp 1.1.0 → 1.3.0
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.
- package/README.md +104 -24
- package/package.json +11 -12
- package/src/config/claude.test.ts +47 -0
- package/src/config/claude.ts +106 -0
- package/src/config/constants.ts +11 -2
- package/src/config/paths.test.ts +40 -0
- package/src/config/paths.ts +86 -0
- package/src/db/arrow-fix.test.ts +101 -0
- package/src/db/lancedb.test.ts +254 -2
- package/src/db/lancedb.ts +385 -38
- package/src/embeddings/cache.test.ts +150 -0
- package/src/embeddings/cache.ts +204 -0
- package/src/embeddings/index.ts +22 -4
- package/src/embeddings/local.ts +57 -17
- package/src/embeddings/openrouter.ts +233 -11
- package/src/errors/index.test.ts +64 -0
- package/src/errors/index.ts +62 -0
- package/src/graph/export.test.ts +81 -0
- package/src/graph/export.ts +163 -0
- package/src/graph/extract.test.ts +90 -0
- package/src/graph/extract.ts +52 -0
- package/src/graph/queries.test.ts +156 -0
- package/src/graph/queries.ts +224 -0
- package/src/index.ts +309 -23
- package/src/notes/conversion.ts +62 -0
- package/src/notes/crud.test.ts +41 -8
- package/src/notes/crud.ts +75 -64
- package/src/notes/read.test.ts +58 -3
- package/src/notes/read.ts +142 -210
- package/src/notes/resolve.ts +174 -0
- package/src/notes/tables.ts +69 -40
- package/src/search/chunk-indexer.test.ts +353 -0
- package/src/search/chunk-indexer.ts +207 -0
- package/src/search/chunk-search.test.ts +327 -0
- package/src/search/chunk-search.ts +298 -0
- package/src/search/index.ts +4 -6
- package/src/search/indexer.ts +164 -109
- package/src/setup.ts +46 -67
- package/src/types/index.ts +4 -0
- package/src/utils/chunker.test.ts +182 -0
- package/src/utils/chunker.ts +170 -0
- package/src/utils/content-filter.test.ts +225 -0
- package/src/utils/content-filter.ts +275 -0
- package/src/utils/debug.ts +0 -2
- package/src/utils/runtime.test.ts +70 -0
- package/src/utils/runtime.ts +40 -0
- package/src/utils/text.test.ts +32 -0
- package/CLAUDE.md +0 -56
- package/src/server.ts +0 -427
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge graph query operations.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getVectorStore } from "../db/lancedb.js";
|
|
6
|
+
import { createDebugLogger } from "../utils/debug.js";
|
|
7
|
+
import { NoteNotFoundError } from "../errors/index.js";
|
|
8
|
+
import { generatePreview } from "../search/index.js";
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_SEARCH_LIMIT,
|
|
11
|
+
DEFAULT_RELATED_NOTES_LIMIT,
|
|
12
|
+
GRAPH_TAG_WEIGHT,
|
|
13
|
+
GRAPH_LINK_WEIGHT,
|
|
14
|
+
GRAPH_SIMILAR_WEIGHT,
|
|
15
|
+
} from "../config/constants.js";
|
|
16
|
+
import type { SearchResult } from "../types/index.js";
|
|
17
|
+
|
|
18
|
+
const debug = createDebugLogger("GRAPH");
|
|
19
|
+
|
|
20
|
+
export interface TagCount {
|
|
21
|
+
tag: string;
|
|
22
|
+
count: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* List all tags with occurrence counts.
|
|
27
|
+
* Sorted by count descending.
|
|
28
|
+
*/
|
|
29
|
+
export async function listTags(): Promise<TagCount[]> {
|
|
30
|
+
debug("Listing all tags");
|
|
31
|
+
|
|
32
|
+
const store = getVectorStore();
|
|
33
|
+
const records = await store.getAll();
|
|
34
|
+
|
|
35
|
+
// Aggregate tag counts
|
|
36
|
+
const counts = new Map<string, number>();
|
|
37
|
+
for (const record of records) {
|
|
38
|
+
for (const tag of record.tags ?? []) {
|
|
39
|
+
counts.set(tag, (counts.get(tag) ?? 0) + 1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Sort by count descending
|
|
44
|
+
const result = Array.from(counts.entries())
|
|
45
|
+
.map(([tag, count]) => ({ tag, count }))
|
|
46
|
+
.sort((a, b) => b.count - a.count);
|
|
47
|
+
|
|
48
|
+
debug(`Found ${result.length} unique tags`);
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SearchByTagOptions {
|
|
53
|
+
folder?: string;
|
|
54
|
+
limit?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Find notes with a specific tag.
|
|
59
|
+
*/
|
|
60
|
+
export async function searchByTag(
|
|
61
|
+
tag: string,
|
|
62
|
+
options: SearchByTagOptions = {}
|
|
63
|
+
): Promise<SearchResult[]> {
|
|
64
|
+
const { folder, limit = DEFAULT_SEARCH_LIMIT } = options;
|
|
65
|
+
|
|
66
|
+
debug(`Searching for tag: ${tag}`);
|
|
67
|
+
|
|
68
|
+
const store = getVectorStore();
|
|
69
|
+
const records = await store.getAll();
|
|
70
|
+
|
|
71
|
+
// Filter by tag (case-insensitive)
|
|
72
|
+
let matches = records.filter((r) =>
|
|
73
|
+
(r.tags ?? []).includes(tag.toLowerCase())
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Filter by folder if specified
|
|
77
|
+
if (folder) {
|
|
78
|
+
const normalizedFolder = folder.toLowerCase();
|
|
79
|
+
matches = matches.filter(
|
|
80
|
+
(r) => r.folder.toLowerCase() === normalizedFolder
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Transform to SearchResult and limit
|
|
85
|
+
const results: SearchResult[] = matches.slice(0, limit).map((r, i) => ({
|
|
86
|
+
id: r.id,
|
|
87
|
+
title: r.title,
|
|
88
|
+
folder: r.folder,
|
|
89
|
+
preview: generatePreview(r.content),
|
|
90
|
+
modified: r.modified,
|
|
91
|
+
score: 1 / (1 + i),
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
debug(`Found ${results.length} notes with tag: ${tag}`);
|
|
95
|
+
return results;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type RelationshipType = "tag" | "link" | "similar";
|
|
99
|
+
|
|
100
|
+
export interface RelatedNote {
|
|
101
|
+
id: string;
|
|
102
|
+
title: string;
|
|
103
|
+
folder: string;
|
|
104
|
+
relationship: RelationshipType;
|
|
105
|
+
score: number;
|
|
106
|
+
sharedTags?: string[];
|
|
107
|
+
direction?: "outgoing" | "incoming";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface FindRelatedOptions {
|
|
111
|
+
types?: RelationshipType[];
|
|
112
|
+
limit?: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Find notes related to a source note by tags, links, or semantic similarity.
|
|
117
|
+
*/
|
|
118
|
+
export async function findRelatedNotes(
|
|
119
|
+
sourceId: string,
|
|
120
|
+
options: FindRelatedOptions = {}
|
|
121
|
+
): Promise<RelatedNote[]> {
|
|
122
|
+
const { types = ["tag", "link", "similar"], limit = DEFAULT_RELATED_NOTES_LIMIT } = options;
|
|
123
|
+
|
|
124
|
+
debug(`Finding related notes for: ${sourceId}`);
|
|
125
|
+
|
|
126
|
+
const store = getVectorStore();
|
|
127
|
+
const allRecords = await store.getAll();
|
|
128
|
+
|
|
129
|
+
// Find source note
|
|
130
|
+
const source = allRecords.find((r) => r.id === sourceId);
|
|
131
|
+
if (!source) {
|
|
132
|
+
throw new NoteNotFoundError(sourceId);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const results: RelatedNote[] = [];
|
|
136
|
+
const seen = new Set<string>();
|
|
137
|
+
|
|
138
|
+
// Find by shared tags
|
|
139
|
+
if (types.includes("tag") && source.tags?.length > 0) {
|
|
140
|
+
for (const record of allRecords) {
|
|
141
|
+
if (record.id === sourceId || seen.has(record.id)) continue;
|
|
142
|
+
|
|
143
|
+
const shared = (record.tags ?? []).filter((t) => source.tags.includes(t));
|
|
144
|
+
if (shared.length > 0) {
|
|
145
|
+
results.push({
|
|
146
|
+
id: record.id,
|
|
147
|
+
title: record.title,
|
|
148
|
+
folder: record.folder,
|
|
149
|
+
relationship: "tag",
|
|
150
|
+
score: GRAPH_TAG_WEIGHT * (shared.length / source.tags.length),
|
|
151
|
+
sharedTags: shared,
|
|
152
|
+
});
|
|
153
|
+
seen.add(record.id);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Find by outlinks (notes this note links to)
|
|
159
|
+
if (types.includes("link")) {
|
|
160
|
+
for (const linkTitle of source.outlinks ?? []) {
|
|
161
|
+
const linked = allRecords.find(
|
|
162
|
+
(r) =>
|
|
163
|
+
r.title.toLowerCase() === linkTitle.toLowerCase() &&
|
|
164
|
+
r.id !== sourceId &&
|
|
165
|
+
!seen.has(r.id)
|
|
166
|
+
);
|
|
167
|
+
if (linked) {
|
|
168
|
+
results.push({
|
|
169
|
+
id: linked.id,
|
|
170
|
+
title: linked.title,
|
|
171
|
+
folder: linked.folder,
|
|
172
|
+
relationship: "link",
|
|
173
|
+
score: GRAPH_LINK_WEIGHT,
|
|
174
|
+
direction: "outgoing",
|
|
175
|
+
});
|
|
176
|
+
seen.add(linked.id);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Find backlinks (notes that link to this note)
|
|
181
|
+
for (const record of allRecords) {
|
|
182
|
+
if (record.id === sourceId || seen.has(record.id)) continue;
|
|
183
|
+
|
|
184
|
+
const linksToSource = (record.outlinks ?? []).some(
|
|
185
|
+
(l) => l.toLowerCase() === source.title.toLowerCase()
|
|
186
|
+
);
|
|
187
|
+
if (linksToSource) {
|
|
188
|
+
results.push({
|
|
189
|
+
id: record.id,
|
|
190
|
+
title: record.title,
|
|
191
|
+
folder: record.folder,
|
|
192
|
+
relationship: "link",
|
|
193
|
+
score: GRAPH_LINK_WEIGHT,
|
|
194
|
+
direction: "incoming",
|
|
195
|
+
});
|
|
196
|
+
seen.add(record.id);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Find by semantic similarity
|
|
202
|
+
if (types.includes("similar") && source.vector?.length > 0) {
|
|
203
|
+
const similarResults = await store.search(source.vector, limit + 1);
|
|
204
|
+
|
|
205
|
+
for (const similar of similarResults) {
|
|
206
|
+
if (similar.id === sourceId || seen.has(similar.id ?? "")) continue;
|
|
207
|
+
|
|
208
|
+
results.push({
|
|
209
|
+
id: similar.id ?? "",
|
|
210
|
+
title: similar.title,
|
|
211
|
+
folder: similar.folder,
|
|
212
|
+
relationship: "similar",
|
|
213
|
+
score: GRAPH_SIMILAR_WEIGHT * similar.score,
|
|
214
|
+
});
|
|
215
|
+
seen.add(similar.id ?? "");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Sort by score and limit
|
|
220
|
+
results.sort((a, b) => b.score - a.score);
|
|
221
|
+
|
|
222
|
+
debug(`Found ${results.length} related notes`);
|
|
223
|
+
return results.slice(0, limit);
|
|
224
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { hasConfig, getEnvPath } from "./config/paths.js";
|
|
3
|
+
import { checkBunRuntime, isTTY } from "./utils/runtime.js";
|
|
4
|
+
import * as dotenv from "dotenv";
|
|
5
|
+
|
|
6
|
+
// Load config from unified location
|
|
7
|
+
dotenv.config({ path: getEnvPath() });
|
|
8
|
+
|
|
1
9
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
10
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
11
|
import {
|
|
@@ -5,28 +13,68 @@ import {
|
|
|
5
13
|
ListToolsRequestSchema,
|
|
6
14
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
7
15
|
import { z } from "zod";
|
|
8
|
-
import "dotenv/config";
|
|
9
16
|
|
|
10
17
|
// Import constants
|
|
11
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
DEFAULT_SEARCH_LIMIT,
|
|
20
|
+
MAX_SEARCH_LIMIT,
|
|
21
|
+
MAX_INPUT_LENGTH,
|
|
22
|
+
MAX_TITLE_LENGTH
|
|
23
|
+
} from "./config/constants.js";
|
|
12
24
|
import { validateEnv } from "./config/env.js";
|
|
13
25
|
|
|
14
26
|
// Import implementations
|
|
15
|
-
import { getVectorStore } from "./db/lancedb.js";
|
|
27
|
+
import { getVectorStore, getChunkStore } from "./db/lancedb.js";
|
|
16
28
|
import { getNoteByTitle, getAllFolders } from "./notes/read.js";
|
|
17
|
-
import { createNote, updateNote, deleteNote, moveNote } from "./notes/crud.js";
|
|
29
|
+
import { createNote, updateNote, deleteNote, moveNote, editTable } from "./notes/crud.js";
|
|
18
30
|
import { searchNotes } from "./search/index.js";
|
|
19
31
|
import { indexNotes, reindexNote } from "./search/indexer.js";
|
|
32
|
+
import { fullChunkIndex, hasChunkIndex } from "./search/chunk-indexer.js";
|
|
33
|
+
import { searchChunks } from "./search/chunk-search.js";
|
|
34
|
+
import { listTags, searchByTag, findRelatedNotes } from "./graph/queries.js";
|
|
35
|
+
import { exportGraph } from "./graph/export.js";
|
|
20
36
|
|
|
21
37
|
// Debug logging and error handling
|
|
22
38
|
import { createDebugLogger } from "./utils/debug.js";
|
|
23
39
|
import { sanitizeErrorMessage } from "./utils/errors.js";
|
|
24
40
|
const debug = createDebugLogger("MCP");
|
|
25
41
|
|
|
42
|
+
// Runtime and config checks
|
|
43
|
+
checkBunRuntime();
|
|
44
|
+
|
|
45
|
+
if (!hasConfig()) {
|
|
46
|
+
if (isTTY()) {
|
|
47
|
+
// Interactive terminal - run setup wizard
|
|
48
|
+
console.log("No configuration found. Starting setup wizard...\n");
|
|
49
|
+
const { spawn } = await import("node:child_process");
|
|
50
|
+
const setupPath = new URL("./setup.ts", import.meta.url).pathname;
|
|
51
|
+
const child = spawn("bun", ["run", setupPath], {
|
|
52
|
+
stdio: "inherit",
|
|
53
|
+
});
|
|
54
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
55
|
+
// Wait for setup to complete
|
|
56
|
+
await new Promise(() => {}); // Setup will exit the process
|
|
57
|
+
} else {
|
|
58
|
+
// Non-interactive (MCP server mode) - show error
|
|
59
|
+
console.error(`
|
|
60
|
+
╭─────────────────────────────────────────────────────────────╮
|
|
61
|
+
│ apple-notes-mcp: Configuration required │
|
|
62
|
+
│ │
|
|
63
|
+
│ Run this command in your terminal first: │
|
|
64
|
+
│ │
|
|
65
|
+
│ apple-notes-mcp │
|
|
66
|
+
│ │
|
|
67
|
+
│ The setup wizard will guide you through configuration. │
|
|
68
|
+
╰─────────────────────────────────────────────────────────────╯
|
|
69
|
+
`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
26
74
|
// Tool parameter schemas
|
|
27
75
|
const SearchNotesSchema = z.object({
|
|
28
|
-
query: z.string().min(1, "Query cannot be empty"),
|
|
29
|
-
folder: z.string().optional(),
|
|
76
|
+
query: z.string().min(1, "Query cannot be empty").max(MAX_INPUT_LENGTH),
|
|
77
|
+
folder: z.string().max(200).optional(),
|
|
30
78
|
limit: z.number().min(1).max(MAX_SEARCH_LIMIT).default(DEFAULT_SEARCH_LIMIT),
|
|
31
79
|
mode: z.enum(["hybrid", "keyword", "semantic"]).default("hybrid"),
|
|
32
80
|
include_content: z.boolean().default(false),
|
|
@@ -38,33 +86,63 @@ const IndexNotesSchema = z.object({
|
|
|
38
86
|
});
|
|
39
87
|
|
|
40
88
|
const ReindexNoteSchema = z.object({
|
|
41
|
-
title: z.string(),
|
|
89
|
+
title: z.string().min(1).max(MAX_TITLE_LENGTH),
|
|
42
90
|
});
|
|
43
91
|
|
|
44
92
|
const GetNoteSchema = z.object({
|
|
45
|
-
title: z.string(),
|
|
93
|
+
title: z.string().min(1).max(MAX_TITLE_LENGTH),
|
|
46
94
|
});
|
|
47
95
|
|
|
48
96
|
const CreateNoteSchema = z.object({
|
|
49
|
-
title: z.string(),
|
|
50
|
-
content: z.string(),
|
|
51
|
-
folder: z.string().optional(),
|
|
97
|
+
title: z.string().min(1).max(MAX_TITLE_LENGTH),
|
|
98
|
+
content: z.string().min(1).max(MAX_INPUT_LENGTH),
|
|
99
|
+
folder: z.string().max(200).optional(),
|
|
52
100
|
});
|
|
53
101
|
|
|
54
102
|
const UpdateNoteSchema = z.object({
|
|
55
|
-
title: z.string(),
|
|
56
|
-
content: z.string(),
|
|
103
|
+
title: z.string().min(1).max(MAX_TITLE_LENGTH),
|
|
104
|
+
content: z.string().min(1).max(MAX_INPUT_LENGTH),
|
|
57
105
|
reindex: z.boolean().default(true),
|
|
58
106
|
});
|
|
59
107
|
|
|
60
108
|
const DeleteNoteSchema = z.object({
|
|
61
|
-
title: z.string(),
|
|
109
|
+
title: z.string().min(1).max(MAX_TITLE_LENGTH),
|
|
62
110
|
confirm: z.boolean(),
|
|
63
111
|
});
|
|
64
112
|
|
|
65
113
|
const MoveNoteSchema = z.object({
|
|
66
|
-
title: z.string(),
|
|
67
|
-
folder: z.string(),
|
|
114
|
+
title: z.string().min(1).max(MAX_TITLE_LENGTH),
|
|
115
|
+
folder: z.string().min(1).max(200),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const EditTableSchema = z.object({
|
|
119
|
+
title: z.string().min(1).max(MAX_TITLE_LENGTH),
|
|
120
|
+
table_index: z.number().min(0).default(0),
|
|
121
|
+
edits: z.array(z.object({
|
|
122
|
+
row: z.number().min(0),
|
|
123
|
+
column: z.number().min(0),
|
|
124
|
+
value: z.string().max(10000),
|
|
125
|
+
})).min(1).max(100),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Knowledge Graph tool schemas
|
|
129
|
+
const ListTagsSchema = z.object({});
|
|
130
|
+
|
|
131
|
+
const SearchByTagSchema = z.object({
|
|
132
|
+
tag: z.string().min(1).max(100),
|
|
133
|
+
folder: z.string().max(200).optional(),
|
|
134
|
+
limit: z.number().min(1).max(MAX_SEARCH_LIMIT).default(DEFAULT_SEARCH_LIMIT),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const RelatedNotesSchema = z.object({
|
|
138
|
+
title: z.string().min(1).max(MAX_TITLE_LENGTH),
|
|
139
|
+
types: z.array(z.enum(["tag", "link", "similar"])).default(["tag", "link", "similar"]),
|
|
140
|
+
limit: z.number().min(1).max(50).default(10),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const ExportGraphSchema = z.object({
|
|
144
|
+
format: z.enum(["json", "graphml"]),
|
|
145
|
+
folder: z.string().max(200).optional(),
|
|
68
146
|
});
|
|
69
147
|
|
|
70
148
|
// Create MCP server
|
|
@@ -231,6 +309,87 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
231
309
|
required: ["title", "folder"],
|
|
232
310
|
},
|
|
233
311
|
},
|
|
312
|
+
{
|
|
313
|
+
name: "edit-table",
|
|
314
|
+
description: "Edit cells in a table within a note. Use for updating table data without rewriting the entire note.",
|
|
315
|
+
inputSchema: {
|
|
316
|
+
type: "object",
|
|
317
|
+
properties: {
|
|
318
|
+
title: { type: "string", description: "Note title (use folder/title for disambiguation)" },
|
|
319
|
+
table_index: { type: "number", description: "Which table to edit (0 = first table, default: 0)" },
|
|
320
|
+
edits: {
|
|
321
|
+
type: "array",
|
|
322
|
+
description: "Array of cell edits",
|
|
323
|
+
items: {
|
|
324
|
+
type: "object",
|
|
325
|
+
properties: {
|
|
326
|
+
row: { type: "number", description: "Row index (0 = header row)" },
|
|
327
|
+
column: { type: "number", description: "Column index (0 = first column)" },
|
|
328
|
+
value: { type: "string", description: "New cell value" },
|
|
329
|
+
},
|
|
330
|
+
required: ["row", "column", "value"],
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
required: ["title", "edits"],
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
// Knowledge Graph tools
|
|
338
|
+
{
|
|
339
|
+
name: "list-tags",
|
|
340
|
+
description: "List all tags with occurrence counts",
|
|
341
|
+
inputSchema: {
|
|
342
|
+
type: "object",
|
|
343
|
+
properties: {},
|
|
344
|
+
required: [],
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
name: "search-by-tag",
|
|
349
|
+
description: "Find notes with a specific tag",
|
|
350
|
+
inputSchema: {
|
|
351
|
+
type: "object",
|
|
352
|
+
properties: {
|
|
353
|
+
tag: { type: "string", description: "Tag to search for (without #)" },
|
|
354
|
+
folder: { type: "string", description: "Filter by folder (optional)" },
|
|
355
|
+
limit: { type: "number", description: "Max results (default: 20)" },
|
|
356
|
+
},
|
|
357
|
+
required: ["tag"],
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: "related-notes",
|
|
362
|
+
description: "Find notes related to a source note by tags, links, or semantic similarity",
|
|
363
|
+
inputSchema: {
|
|
364
|
+
type: "object",
|
|
365
|
+
properties: {
|
|
366
|
+
title: { type: "string", description: "Source note title (use folder/title or id:xxx)" },
|
|
367
|
+
types: {
|
|
368
|
+
type: "array",
|
|
369
|
+
items: { type: "string", enum: ["tag", "link", "similar"] },
|
|
370
|
+
description: "Relationship types to include (default: all)"
|
|
371
|
+
},
|
|
372
|
+
limit: { type: "number", description: "Max results (default: 10)" },
|
|
373
|
+
},
|
|
374
|
+
required: ["title"],
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
name: "export-graph",
|
|
379
|
+
description: "Export knowledge graph to JSON or GraphML format for visualization",
|
|
380
|
+
inputSchema: {
|
|
381
|
+
type: "object",
|
|
382
|
+
properties: {
|
|
383
|
+
format: {
|
|
384
|
+
type: "string",
|
|
385
|
+
enum: ["json", "graphml"],
|
|
386
|
+
description: "Export format: json (for D3.js, custom viz) or graphml (for Gephi, yEd)"
|
|
387
|
+
},
|
|
388
|
+
folder: { type: "string", description: "Filter by folder (optional)" },
|
|
389
|
+
},
|
|
390
|
+
required: ["format"],
|
|
391
|
+
},
|
|
392
|
+
},
|
|
234
393
|
],
|
|
235
394
|
};
|
|
236
395
|
});
|
|
@@ -245,6 +404,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
245
404
|
// Read tools
|
|
246
405
|
case "search-notes": {
|
|
247
406
|
const params = SearchNotesSchema.parse(args);
|
|
407
|
+
|
|
408
|
+
// Use chunk-based search if chunk index exists (better for long notes)
|
|
409
|
+
const useChunkSearch = await hasChunkIndex();
|
|
410
|
+
|
|
411
|
+
if (useChunkSearch) {
|
|
412
|
+
debug("Using chunk-based search");
|
|
413
|
+
const chunkResults = await searchChunks(params.query, {
|
|
414
|
+
folder: params.folder,
|
|
415
|
+
limit: params.limit,
|
|
416
|
+
mode: params.mode,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
if (chunkResults.length === 0) {
|
|
420
|
+
return textResponse("No notes found matching your query.");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Transform chunk results to match expected format
|
|
424
|
+
const results = chunkResults.map((r) => ({
|
|
425
|
+
id: r.note_id,
|
|
426
|
+
title: r.note_title,
|
|
427
|
+
folder: r.folder,
|
|
428
|
+
preview: r.matchedChunk.slice(0, 200) + (r.matchedChunk.length > 200 ? "..." : ""),
|
|
429
|
+
modified: r.modified,
|
|
430
|
+
score: r.score,
|
|
431
|
+
matchedChunkIndex: r.matchedChunkIndex,
|
|
432
|
+
}));
|
|
433
|
+
|
|
434
|
+
return textResponse(JSON.stringify(results, null, 2));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Fall back to legacy search if no chunk index
|
|
438
|
+
debug("Using legacy search (no chunk index)");
|
|
248
439
|
const results = await searchNotes(params.query, {
|
|
249
440
|
folder: params.folder,
|
|
250
441
|
limit: params.limit,
|
|
@@ -276,6 +467,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
276
467
|
}
|
|
277
468
|
}
|
|
278
469
|
|
|
470
|
+
// Run chunk indexing for full mode (for semantic search on long notes)
|
|
471
|
+
if (params.mode === "full") {
|
|
472
|
+
debug("Running chunk indexing for full mode...");
|
|
473
|
+
const chunkResult = await fullChunkIndex();
|
|
474
|
+
message += `\nChunk index: ${chunkResult.totalChunks} chunks from ${chunkResult.totalNotes} notes in ${(chunkResult.timeMs / 1000).toFixed(1)}s`;
|
|
475
|
+
}
|
|
476
|
+
|
|
279
477
|
return textResponse(message);
|
|
280
478
|
}
|
|
281
479
|
|
|
@@ -287,8 +485,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
287
485
|
|
|
288
486
|
case "list-notes": {
|
|
289
487
|
const store = getVectorStore();
|
|
290
|
-
const
|
|
291
|
-
|
|
488
|
+
const noteCount = await store.count();
|
|
489
|
+
|
|
490
|
+
let message = `${noteCount} notes indexed.`;
|
|
491
|
+
|
|
492
|
+
// Show chunk statistics if chunk index exists
|
|
493
|
+
const hasChunks = await hasChunkIndex();
|
|
494
|
+
if (hasChunks) {
|
|
495
|
+
const chunkStore = getChunkStore();
|
|
496
|
+
const chunkCount = await chunkStore.count();
|
|
497
|
+
message += ` ${chunkCount} chunks indexed for semantic search.`;
|
|
498
|
+
} else {
|
|
499
|
+
message += " Run index-notes with mode='full' to enable chunk-based search.";
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return textResponse(message);
|
|
292
503
|
}
|
|
293
504
|
|
|
294
505
|
case "get-note": {
|
|
@@ -323,19 +534,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
323
534
|
|
|
324
535
|
case "update-note": {
|
|
325
536
|
const params = UpdateNoteSchema.parse(args);
|
|
326
|
-
await updateNote(params.title, params.content);
|
|
537
|
+
const result = await updateNote(params.title, params.content);
|
|
538
|
+
|
|
539
|
+
// Build location string for messages
|
|
540
|
+
const location = `${result.folder}/${result.newTitle}`;
|
|
541
|
+
const renamedMsg = result.titleChanged
|
|
542
|
+
? ` (renamed from "${result.originalTitle}")`
|
|
543
|
+
: "";
|
|
327
544
|
|
|
328
545
|
if (params.reindex) {
|
|
329
546
|
try {
|
|
330
|
-
|
|
331
|
-
|
|
547
|
+
// Use new title for reindexing (Apple Notes may have renamed it)
|
|
548
|
+
const reindexTitle = `${result.folder}/${result.newTitle}`;
|
|
549
|
+
await reindexNote(reindexTitle);
|
|
550
|
+
return textResponse(`Updated and reindexed note: "${location}"${renamedMsg}`);
|
|
332
551
|
} catch (reindexError) {
|
|
333
552
|
debug("Reindex after update failed:", reindexError);
|
|
334
|
-
return textResponse(`Updated note: "${
|
|
553
|
+
return textResponse(`Updated note: "${location}"${renamedMsg} (reindexing failed, run index-notes to update)`);
|
|
335
554
|
}
|
|
336
555
|
}
|
|
337
556
|
|
|
338
|
-
return textResponse(`Updated note: "${
|
|
557
|
+
return textResponse(`Updated note: "${location}"${renamedMsg}`);
|
|
339
558
|
}
|
|
340
559
|
|
|
341
560
|
case "delete-note": {
|
|
@@ -353,6 +572,73 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
353
572
|
return textResponse(`Moved note: "${params.title}" to folder "${params.folder}"`);
|
|
354
573
|
}
|
|
355
574
|
|
|
575
|
+
case "edit-table": {
|
|
576
|
+
const params = EditTableSchema.parse(args);
|
|
577
|
+
await editTable(params.title, params.table_index, params.edits);
|
|
578
|
+
return textResponse(`Updated ${params.edits.length} cell(s) in table ${params.table_index}`);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Knowledge Graph tools
|
|
582
|
+
case "list-tags": {
|
|
583
|
+
ListTagsSchema.parse(args);
|
|
584
|
+
const tags = await listTags();
|
|
585
|
+
|
|
586
|
+
if (tags.length === 0) {
|
|
587
|
+
return textResponse("No tags found. Add #tags to your notes and reindex.");
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return textResponse(JSON.stringify(tags, null, 2));
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
case "search-by-tag": {
|
|
594
|
+
const params = SearchByTagSchema.parse(args);
|
|
595
|
+
const results = await searchByTag(params.tag, {
|
|
596
|
+
folder: params.folder,
|
|
597
|
+
limit: params.limit,
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
if (results.length === 0) {
|
|
601
|
+
return textResponse(`No notes found with tag: #${params.tag}`);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return textResponse(JSON.stringify(results, null, 2));
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
case "related-notes": {
|
|
608
|
+
const params = RelatedNotesSchema.parse(args);
|
|
609
|
+
|
|
610
|
+
// Resolve note to get ID
|
|
611
|
+
const note = await getNoteByTitle(params.title);
|
|
612
|
+
if (!note) {
|
|
613
|
+
return errorResponse(`Note not found: "${params.title}"`);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const results = await findRelatedNotes(note.id, {
|
|
617
|
+
types: params.types,
|
|
618
|
+
limit: params.limit,
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
if (results.length === 0) {
|
|
622
|
+
return textResponse("No related notes found.");
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return textResponse(JSON.stringify(results, null, 2));
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
case "export-graph": {
|
|
629
|
+
const params = ExportGraphSchema.parse(args);
|
|
630
|
+
const result = await exportGraph({
|
|
631
|
+
format: params.format,
|
|
632
|
+
folder: params.folder,
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
if (typeof result === "string") {
|
|
636
|
+
return textResponse(result);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return textResponse(JSON.stringify(result, null, 2));
|
|
640
|
+
}
|
|
641
|
+
|
|
356
642
|
default:
|
|
357
643
|
return errorResponse(`Unknown tool: ${name}`);
|
|
358
644
|
}
|