pkm-mcp-server 1.0.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/CHANGELOG.md +52 -0
- package/LICENSE +21 -0
- package/README.md +246 -0
- package/activity.js +147 -0
- package/embeddings.js +672 -0
- package/graph.js +340 -0
- package/handlers.js +871 -0
- package/helpers.js +855 -0
- package/index.js +498 -0
- package/package.json +63 -0
- package/sample-project/CLAUDE.md +193 -0
- package/templates/adr.md +52 -0
- package/templates/daily-note.md +19 -0
- package/templates/devlog.md +35 -0
- package/templates/fleeting-note.md +11 -0
- package/templates/literature-note.md +25 -0
- package/templates/meeting-notes.md +28 -0
- package/templates/moc.md +22 -0
- package/templates/permanent-note.md +26 -0
- package/templates/project-index.md +38 -0
- package/templates/research-note.md +35 -0
- package/templates/task.md +22 -0
- package/templates/troubleshooting-log.md +32 -0
- package/utils.js +31 -0
package/index.js
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import {
|
|
6
|
+
CallToolRequestSchema,
|
|
7
|
+
ListToolsRequestSchema,
|
|
8
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import crypto from "crypto";
|
|
10
|
+
import os from "os";
|
|
11
|
+
import { createRequire } from "module";
|
|
12
|
+
import fs from "fs/promises";
|
|
13
|
+
import { SemanticIndex } from "./embeddings.js";
|
|
14
|
+
import { ActivityLog } from "./activity.js";
|
|
15
|
+
import { loadTemplates } from "./helpers.js";
|
|
16
|
+
import { createHandlers } from "./handlers.js";
|
|
17
|
+
|
|
18
|
+
// Read version from package.json
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const { version: PKG_VERSION } = require("./package.json");
|
|
21
|
+
|
|
22
|
+
// Get vault path from environment
|
|
23
|
+
const VAULT_PATH = process.env.VAULT_PATH || (os.homedir() + "/Documents/PKM");
|
|
24
|
+
|
|
25
|
+
// Template registry (populated at startup)
|
|
26
|
+
let templateRegistry = new Map();
|
|
27
|
+
let templateDescriptions = "";
|
|
28
|
+
|
|
29
|
+
// Semantic index (populated at startup if OPENAI_API_KEY is set)
|
|
30
|
+
let semanticIndex = null;
|
|
31
|
+
|
|
32
|
+
// Activity log (populated at startup)
|
|
33
|
+
let activityLog = null;
|
|
34
|
+
const SESSION_ID = crypto.randomUUID();
|
|
35
|
+
|
|
36
|
+
// Handler map (populated at startup)
|
|
37
|
+
let handlers;
|
|
38
|
+
|
|
39
|
+
// Create the server
|
|
40
|
+
const server = new Server(
|
|
41
|
+
{ name: "pkm-mcp-server", version: PKG_VERSION },
|
|
42
|
+
{ capabilities: { tools: {} } }
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// List available tools
|
|
46
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
47
|
+
const tools = [
|
|
48
|
+
{
|
|
49
|
+
name: "vault_read",
|
|
50
|
+
description: "Read the contents of a markdown file from the vault. Supports pagination: read a single section by heading, last N lines, last N heading-level sections, chunk number, or line range. Files exceeding ~80k characters auto-redirect to peek data (file structure/outline) unless a pagination param or force=true is specified.",
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
path: { type: "string", description: "Path relative to vault root (e.g., '01-Projects/MyApp/_index.md')" },
|
|
55
|
+
heading: { type: "string", description: "Read only the section under this heading (exact match, case-sensitive). Returns heading line + content until next same-or-higher-level heading." },
|
|
56
|
+
tail: { type: "number", description: "Return the last N lines of the file. Frontmatter is always prepended." },
|
|
57
|
+
tail_sections: { type: "number", description: "Return the last N sections at the specified heading level. Frontmatter is always prepended." },
|
|
58
|
+
section_level: { type: "number", description: "Heading level for tail_sections (1=`#`, 2=`##`, etc). Default: 2.", default: 2 },
|
|
59
|
+
chunk: { type: "number", description: "Read a specific chunk of the file (1-indexed). Each chunk is ~80k characters. Use vault_peek to see total chunks." },
|
|
60
|
+
lines: {
|
|
61
|
+
type: "object",
|
|
62
|
+
description: "Read a range of lines from the file (1-indexed, inclusive).",
|
|
63
|
+
properties: {
|
|
64
|
+
start: { type: "number", description: "Start line (1-indexed, inclusive)" },
|
|
65
|
+
end: { type: "number", description: "End line (1-indexed, inclusive)" }
|
|
66
|
+
},
|
|
67
|
+
required: ["start", "end"]
|
|
68
|
+
},
|
|
69
|
+
force: { type: "boolean", description: "Bypass auto-redirect for large files. WARNING: only use when full content is essential, as large files degrade model performance. Hard-capped at ~400k chars (~100k tokens)." }
|
|
70
|
+
},
|
|
71
|
+
required: ["path"]
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "vault_peek",
|
|
76
|
+
description: "Inspect a file's metadata and structure without reading full content. Returns file size, frontmatter, heading outline with line numbers and approximate section sizes, and a brief preview. Use this to plan which sections to read from large files.",
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: {
|
|
80
|
+
path: { type: "string", description: "Path relative to vault root (supports fuzzy resolution: 'devlog' resolves to full path)" }
|
|
81
|
+
},
|
|
82
|
+
required: ["path"]
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "vault_write",
|
|
87
|
+
description: `Create a new note from a template. Notes must be created from templates to ensure proper frontmatter.
|
|
88
|
+
|
|
89
|
+
Available templates:
|
|
90
|
+
${templateDescriptions || "(Loading...)"}
|
|
91
|
+
|
|
92
|
+
Built-in variables (auto-substituted):
|
|
93
|
+
- <% tp.date.now("YYYY-MM-DD") %> - Current date
|
|
94
|
+
- <% tp.file.title %> - Derived from output path filename
|
|
95
|
+
|
|
96
|
+
Required: frontmatter.tags - provide at least one tag for the note.
|
|
97
|
+
Optional: frontmatter.status, frontmatter.priority, frontmatter.project, frontmatter.deciders, frontmatter.due, frontmatter.source (depending on template type).
|
|
98
|
+
Pass custom <%...%> variables via the 'variables' parameter.`,
|
|
99
|
+
inputSchema: {
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {
|
|
102
|
+
template: {
|
|
103
|
+
type: "string",
|
|
104
|
+
description: "Template name (filename without .md from 05-Templates/)",
|
|
105
|
+
enum: Array.from(templateRegistry.keys())
|
|
106
|
+
},
|
|
107
|
+
path: { type: "string", description: "Output path relative to vault root" },
|
|
108
|
+
variables: {
|
|
109
|
+
type: "object",
|
|
110
|
+
description: "Custom variables for <%...%> patterns in body (key-value string pairs)",
|
|
111
|
+
additionalProperties: { type: "string" }
|
|
112
|
+
},
|
|
113
|
+
frontmatter: {
|
|
114
|
+
type: "object",
|
|
115
|
+
description: "Frontmatter fields to set (e.g., {tags: ['tag1', 'tag2'], status: 'active'})",
|
|
116
|
+
properties: {
|
|
117
|
+
tags: { type: "array", items: { type: "string" }, description: "Tags for the note (required)" },
|
|
118
|
+
status: { type: "string", description: "Note status" },
|
|
119
|
+
priority: { type: "string", description: "Priority level (for tasks)" },
|
|
120
|
+
project: { type: "string", description: "Project name (for devlogs)" },
|
|
121
|
+
deciders: { type: "string", description: "Decision makers (for ADRs)" },
|
|
122
|
+
due: { type: "string", description: "Due date (for tasks)" },
|
|
123
|
+
source: { type: "string", description: "Source reference (for tasks)" }
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
createDirs: { type: "boolean", description: "Create parent directories if they don't exist", default: true }
|
|
127
|
+
},
|
|
128
|
+
required: ["template", "path"]
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: "vault_append",
|
|
133
|
+
description: "Append content to an existing file, optionally under a specific heading. When 'position' is specified, heading is required and must exist in the file.",
|
|
134
|
+
inputSchema: {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
path: { type: "string", description: "Path relative to vault root" },
|
|
138
|
+
content: { type: "string", description: "Content to append" },
|
|
139
|
+
heading: { type: "string", description: "Optional: append under this heading (e.g., '## Recent Activity')" },
|
|
140
|
+
position: { type: "string", enum: ["after_heading", "before_heading", "end_of_section"], description: "Where to insert relative to heading. after_heading: right after the heading line. before_heading: right before the heading line. end_of_section: at the end of the section (before the next same-or-higher-level heading, or EOF). Requires heading." }
|
|
141
|
+
},
|
|
142
|
+
required: ["path", "content"]
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "vault_edit",
|
|
147
|
+
description: "Edit a file by replacing an exact string match. The old_string must appear exactly once in the file for safety.",
|
|
148
|
+
inputSchema: {
|
|
149
|
+
type: "object",
|
|
150
|
+
properties: {
|
|
151
|
+
path: { type: "string", description: "Path relative to vault root" },
|
|
152
|
+
old_string: { type: "string", description: "Exact string to find (must match exactly once)" },
|
|
153
|
+
new_string: { type: "string", description: "Replacement string" }
|
|
154
|
+
},
|
|
155
|
+
required: ["path", "old_string", "new_string"]
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "vault_update_frontmatter",
|
|
160
|
+
description: "Update YAML frontmatter fields in an existing note. Parses existing frontmatter, updates specified fields, preserves everything else. Set a field to null to remove it. Protected fields (type, created, tags) cannot be removed.",
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: "object",
|
|
163
|
+
properties: {
|
|
164
|
+
path: { type: "string", description: "Path relative to vault root (exact path required)" },
|
|
165
|
+
fields: {
|
|
166
|
+
type: "object",
|
|
167
|
+
description: "Fields to update. Set value to null to remove a field. Arrays (like tags) are replaced wholesale.",
|
|
168
|
+
additionalProperties: true
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
required: ["path", "fields"]
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: "vault_search",
|
|
176
|
+
description: "Search for text across all markdown files in the vault",
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: "object",
|
|
179
|
+
properties: {
|
|
180
|
+
query: { type: "string", description: "Search query (case-insensitive)" },
|
|
181
|
+
folder: { type: "string", description: "Optional: limit search to this folder" },
|
|
182
|
+
limit: { type: "number", description: "Max results to return", default: 10 }
|
|
183
|
+
},
|
|
184
|
+
required: ["query"]
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "vault_list",
|
|
189
|
+
description: "List files and folders in the vault",
|
|
190
|
+
inputSchema: {
|
|
191
|
+
type: "object",
|
|
192
|
+
properties: {
|
|
193
|
+
path: { type: "string", description: "Path relative to vault root (default: root)", default: "" },
|
|
194
|
+
recursive: { type: "boolean", description: "List recursively", default: false },
|
|
195
|
+
pattern: { type: "string", description: "Glob pattern to filter (e.g., '*.md')" }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: "vault_recent",
|
|
201
|
+
description: "Get recently modified files",
|
|
202
|
+
inputSchema: {
|
|
203
|
+
type: "object",
|
|
204
|
+
properties: {
|
|
205
|
+
limit: { type: "number", description: "Number of files to return", default: 10 },
|
|
206
|
+
folder: { type: "string", description: "Optional: limit to this folder" }
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
name: "vault_links",
|
|
212
|
+
description: "Get incoming and outgoing links for a note",
|
|
213
|
+
inputSchema: {
|
|
214
|
+
type: "object",
|
|
215
|
+
properties: {
|
|
216
|
+
path: { type: "string", description: "Path to the note" },
|
|
217
|
+
direction: { type: "string", enum: ["incoming", "outgoing", "both"], default: "both" }
|
|
218
|
+
},
|
|
219
|
+
required: ["path"]
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: "vault_neighborhood",
|
|
224
|
+
description: "Explore the graph neighborhood around a note by traversing wikilinks. Returns notes grouped by hop distance from the starting note, with frontmatter metadata for each node. Useful for understanding clusters, finding related context, and discovering connections.",
|
|
225
|
+
inputSchema: {
|
|
226
|
+
type: "object",
|
|
227
|
+
properties: {
|
|
228
|
+
path: { type: "string", description: "Path to the starting note (relative to vault root)" },
|
|
229
|
+
depth: { type: "number", description: "Traversal depth — how many hops to follow (default: 2)", default: 2 },
|
|
230
|
+
direction: {
|
|
231
|
+
type: "string",
|
|
232
|
+
enum: ["both", "outgoing", "incoming"],
|
|
233
|
+
description: "Link direction to follow (default: both)",
|
|
234
|
+
default: "both"
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
required: ["path"]
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: "vault_query",
|
|
242
|
+
description: "Query notes by YAML frontmatter metadata (type, status, tags, dates, custom fields, sorting)",
|
|
243
|
+
inputSchema: {
|
|
244
|
+
type: "object",
|
|
245
|
+
properties: {
|
|
246
|
+
type: { type: "string", description: "Filter by note type (exact match)" },
|
|
247
|
+
status: { type: "string", description: "Filter by status (exact match)" },
|
|
248
|
+
tags: { type: "array", items: { type: "string" }, description: "ALL tags must be present (case-insensitive)" },
|
|
249
|
+
tags_any: { type: "array", items: { type: "string" }, description: "ANY tag must be present (case-insensitive)" },
|
|
250
|
+
created_after: { type: "string", description: "Notes created on or after this date (YYYY-MM-DD)" },
|
|
251
|
+
created_before: { type: "string", description: "Notes created on or before this date (YYYY-MM-DD)" },
|
|
252
|
+
folder: { type: "string", description: "Limit search to this folder" },
|
|
253
|
+
limit: { type: "number", description: "Max results to return", default: 50 },
|
|
254
|
+
custom_fields: {
|
|
255
|
+
type: "object",
|
|
256
|
+
description: "Filter by arbitrary frontmatter fields (exact match). Use null to match missing fields.",
|
|
257
|
+
additionalProperties: true
|
|
258
|
+
},
|
|
259
|
+
sort_by: {
|
|
260
|
+
type: "string",
|
|
261
|
+
description: "Sort results by this frontmatter field. Smart ordering: priority uses rank (urgent>high>normal>low), dates sort chronologically, others alphabetically. Nulls sort last."
|
|
262
|
+
},
|
|
263
|
+
sort_order: {
|
|
264
|
+
type: "string",
|
|
265
|
+
enum: ["asc", "desc"],
|
|
266
|
+
description: "Sort direction (default: asc)",
|
|
267
|
+
default: "asc"
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: "vault_tags",
|
|
274
|
+
description: "Discover all tags used across the vault with per-note occurrence counts. Useful for exploring tag conventions, finding hierarchical tag trees, and understanding vault organization.",
|
|
275
|
+
inputSchema: {
|
|
276
|
+
type: "object",
|
|
277
|
+
properties: {
|
|
278
|
+
folder: { type: "string", description: "Optional: limit to this folder (e.g., '01-Projects')" },
|
|
279
|
+
pattern: { type: "string", description: "Glob-like filter: 'pkm/*' (hierarchical), '*research*' (substring), 'dev*' (prefix)" },
|
|
280
|
+
include_inline: { type: "boolean", description: "Also parse inline #tags from note bodies (default: false, frontmatter only)", default: false }
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: "vault_activity",
|
|
286
|
+
description: "Query or clear the activity log. Shows tool calls made across sessions with timestamps and arguments. Use action 'query' to retrieve entries, 'clear' to delete entries.",
|
|
287
|
+
inputSchema: {
|
|
288
|
+
type: "object",
|
|
289
|
+
properties: {
|
|
290
|
+
action: {
|
|
291
|
+
type: "string",
|
|
292
|
+
enum: ["query", "clear"],
|
|
293
|
+
description: "Action to perform (default: query)",
|
|
294
|
+
default: "query"
|
|
295
|
+
},
|
|
296
|
+
limit: {
|
|
297
|
+
type: "number",
|
|
298
|
+
description: "Max entries to return (query only, default: 50)",
|
|
299
|
+
default: 50
|
|
300
|
+
},
|
|
301
|
+
tool: {
|
|
302
|
+
type: "string",
|
|
303
|
+
description: "Filter by tool name (e.g., 'vault_read', 'vault_write')"
|
|
304
|
+
},
|
|
305
|
+
session: {
|
|
306
|
+
type: "string",
|
|
307
|
+
description: "Filter by session ID"
|
|
308
|
+
},
|
|
309
|
+
since: {
|
|
310
|
+
type: "string",
|
|
311
|
+
description: "Filter entries on or after this ISO timestamp (e.g., '2026-02-08')"
|
|
312
|
+
},
|
|
313
|
+
before: {
|
|
314
|
+
type: "string",
|
|
315
|
+
description: "Filter entries before this ISO timestamp"
|
|
316
|
+
},
|
|
317
|
+
path: {
|
|
318
|
+
type: "string",
|
|
319
|
+
description: "Filter by file path substring in arguments"
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: "vault_trash",
|
|
326
|
+
description: "Soft-delete a file by moving it to .trash/ (Obsidian convention). Reports files with broken incoming links as warnings. Use vault_move to relocate files instead.",
|
|
327
|
+
inputSchema: {
|
|
328
|
+
type: "object",
|
|
329
|
+
properties: {
|
|
330
|
+
path: { type: "string", description: "Path to the file to trash (exact path required)" }
|
|
331
|
+
},
|
|
332
|
+
required: ["path"]
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: "vault_move",
|
|
337
|
+
description: "Move or rename a markdown file within the vault. Automatically updates wikilinks in all files that reference the moved file. Both paths must be exact.",
|
|
338
|
+
inputSchema: {
|
|
339
|
+
type: "object",
|
|
340
|
+
properties: {
|
|
341
|
+
old_path: { type: "string", description: "Current file path (exact path required)" },
|
|
342
|
+
new_path: { type: "string", description: "Destination path (exact, must not already exist)" },
|
|
343
|
+
update_links: { type: "boolean", description: "Update wikilinks in other files pointing to this note (default: true)", default: true }
|
|
344
|
+
},
|
|
345
|
+
required: ["old_path", "new_path"]
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
];
|
|
349
|
+
|
|
350
|
+
if (semanticIndex?.isAvailable) {
|
|
351
|
+
tools.push({
|
|
352
|
+
name: "vault_semantic_search",
|
|
353
|
+
description: "Search the vault using semantic similarity. Finds conceptually related notes even when they use different words. Requires OPENAI_API_KEY.",
|
|
354
|
+
inputSchema: {
|
|
355
|
+
type: "object",
|
|
356
|
+
properties: {
|
|
357
|
+
query: { type: "string", description: "Natural language search query (e.g., 'managing information overload')" },
|
|
358
|
+
limit: { type: "number", description: "Max results to return (default: 5)", default: 5 },
|
|
359
|
+
folder: { type: "string", description: "Optional: limit search to this folder (e.g., '01-Projects')" },
|
|
360
|
+
threshold: { type: "number", description: "Minimum similarity score 0-1 (default: no threshold)" }
|
|
361
|
+
},
|
|
362
|
+
required: ["query"]
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
tools.push({
|
|
366
|
+
name: "vault_suggest_links",
|
|
367
|
+
description: "Suggest relevant notes to link to based on content similarity. Accepts text content or a file path, finds semantically related notes, and excludes notes already linked via [[wikilinks]].",
|
|
368
|
+
inputSchema: {
|
|
369
|
+
type: "object",
|
|
370
|
+
properties: {
|
|
371
|
+
content: { type: "string", description: "Text content to find link suggestions for. Takes precedence over path." },
|
|
372
|
+
path: { type: "string", description: "Path to an existing note to suggest links for. Used if content is not provided." },
|
|
373
|
+
limit: { type: "number", description: "Max suggestions to return (default: 5)", default: 5 },
|
|
374
|
+
folder: { type: "string", description: "Optional: limit suggestions to this folder (e.g., '01-Projects')" },
|
|
375
|
+
threshold: { type: "number", description: "Minimum similarity score 0-1 (default: no threshold)" }
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return { tools };
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Handle tool calls
|
|
385
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
386
|
+
const { name, arguments: args } = request.params;
|
|
387
|
+
|
|
388
|
+
// Log activity (skip vault_activity to avoid noise)
|
|
389
|
+
if (name !== "vault_activity") {
|
|
390
|
+
try { activityLog?.log(name, args); } catch (e) { console.error(`Activity log: ${e.message}`); }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
const handler = handlers.get(name);
|
|
395
|
+
if (!handler) {
|
|
396
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
397
|
+
}
|
|
398
|
+
return await handler(args);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
return {
|
|
401
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
402
|
+
isError: true
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// Initialize and start the server
|
|
408
|
+
async function initializeServer() {
|
|
409
|
+
// Validate vault path exists
|
|
410
|
+
try {
|
|
411
|
+
const stat = await fs.stat(VAULT_PATH);
|
|
412
|
+
if (!stat.isDirectory()) {
|
|
413
|
+
console.error(`Error: VAULT_PATH is not a directory: ${VAULT_PATH}`);
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
} catch (e) {
|
|
417
|
+
if (e.code === "ENOENT") {
|
|
418
|
+
console.error(`Error: VAULT_PATH does not exist: ${VAULT_PATH}`);
|
|
419
|
+
console.error("Set the VAULT_PATH environment variable to your Obsidian vault directory.");
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
throw e;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
templateRegistry = await loadTemplates(VAULT_PATH);
|
|
426
|
+
|
|
427
|
+
if (templateRegistry.size > 0) {
|
|
428
|
+
templateDescriptions = Array.from(templateRegistry.values())
|
|
429
|
+
.map(t => `- **${t.shortName}**: ${t.description}`)
|
|
430
|
+
.join("\n");
|
|
431
|
+
} else {
|
|
432
|
+
templateDescriptions = "(No templates found - add .md files to 05-Templates/)";
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const openaiApiKey = process.env.OPENAI_API_KEY;
|
|
436
|
+
if (openaiApiKey) {
|
|
437
|
+
try {
|
|
438
|
+
semanticIndex = new SemanticIndex({ vaultPath: VAULT_PATH, openaiApiKey });
|
|
439
|
+
await semanticIndex.initialize();
|
|
440
|
+
console.error("Semantic index initialized");
|
|
441
|
+
} catch (err) {
|
|
442
|
+
console.error(`Semantic index init failed (non-fatal): ${err.message}`);
|
|
443
|
+
semanticIndex = null;
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
console.error("OPENAI_API_KEY not set — semantic search disabled");
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
activityLog = new ActivityLog({ vaultPath: VAULT_PATH, sessionId: SESSION_ID });
|
|
451
|
+
await activityLog.initialize();
|
|
452
|
+
console.error(`Activity log initialized (session: ${SESSION_ID.slice(0, 8)})`);
|
|
453
|
+
} catch (err) {
|
|
454
|
+
console.error(`Activity log init failed (non-fatal): ${err.message}`);
|
|
455
|
+
activityLog = null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
handlers = await createHandlers({
|
|
459
|
+
vaultPath: VAULT_PATH,
|
|
460
|
+
templateRegistry,
|
|
461
|
+
semanticIndex,
|
|
462
|
+
activityLog,
|
|
463
|
+
sessionId: SESSION_ID,
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const transport = new StdioServerTransport();
|
|
467
|
+
await server.connect(transport);
|
|
468
|
+
console.error(`PKM MCP Server running... (${templateRegistry.size} templates loaded${semanticIndex?.isAvailable ? ", semantic search enabled" : ""}, activity log ${activityLog ? "enabled" : "disabled"})`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
let shuttingDown = false;
|
|
472
|
+
|
|
473
|
+
async function shutdown() {
|
|
474
|
+
if (shuttingDown) return;
|
|
475
|
+
shuttingDown = true;
|
|
476
|
+
console.error("Shutting down...");
|
|
477
|
+
const forceTimer = setTimeout(() => {
|
|
478
|
+
console.error("Shutdown timed out, forcing exit");
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}, 5000);
|
|
481
|
+
forceTimer.unref();
|
|
482
|
+
try {
|
|
483
|
+
if (semanticIndex) await semanticIndex.shutdown();
|
|
484
|
+
} catch (e) {
|
|
485
|
+
console.error(`Semantic index shutdown error: ${e.message}`);
|
|
486
|
+
}
|
|
487
|
+
if (activityLog) activityLog.shutdown();
|
|
488
|
+
clearTimeout(forceTimer);
|
|
489
|
+
process.exit(0);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
process.on("SIGINT", () => shutdown());
|
|
493
|
+
process.on("SIGTERM", () => shutdown());
|
|
494
|
+
|
|
495
|
+
initializeServer().catch(err => {
|
|
496
|
+
console.error("Fatal:", err);
|
|
497
|
+
process.exit(1);
|
|
498
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pkm-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Obsidian vault integration with Claude Code — 18 tools for notes, search, and graph traversal",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"pkm-mcp-server": "index.js"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"*.js",
|
|
18
|
+
"!eslint.config.js",
|
|
19
|
+
"CHANGELOG.md",
|
|
20
|
+
"templates/",
|
|
21
|
+
"sample-project/"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"start": "node index.js",
|
|
25
|
+
"test": "node --test tests/*.test.js",
|
|
26
|
+
"lint": "eslint *.js tests/",
|
|
27
|
+
"prepublishOnly": "npm test && npm run lint"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/AdrianV101/Obsidian-MCP.git"
|
|
32
|
+
},
|
|
33
|
+
"author": "Adrian Verhoosel",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"keywords": [
|
|
36
|
+
"mcp",
|
|
37
|
+
"obsidian",
|
|
38
|
+
"pkm",
|
|
39
|
+
"knowledge-management",
|
|
40
|
+
"claude-code",
|
|
41
|
+
"model-context-protocol",
|
|
42
|
+
"claude",
|
|
43
|
+
"ai",
|
|
44
|
+
"markdown",
|
|
45
|
+
"notes",
|
|
46
|
+
"semantic-search",
|
|
47
|
+
"wikilinks"
|
|
48
|
+
],
|
|
49
|
+
"homepage": "https://github.com/AdrianV101/Obsidian-MCP#readme",
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/AdrianV101/Obsidian-MCP/issues"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
55
|
+
"better-sqlite3": "^12.6.2",
|
|
56
|
+
"js-yaml": "^4.1.0",
|
|
57
|
+
"sqlite-vec": "0.1.7-alpha.2"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@eslint/js": "^10.0.1",
|
|
61
|
+
"eslint": "^10.0.0"
|
|
62
|
+
}
|
|
63
|
+
}
|