pkm-mcp-server 1.2.1 → 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/CHANGELOG.md +15 -0
- package/README.md +8 -3
- package/cli.js +26 -0
- package/index.js +458 -451
- package/init.js +577 -0
- package/package.json +7 -6
- package/sample-project/CLAUDE.md +15 -0
- package/templates/note.md +7 -0
package/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
|
|
3
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -19,72 +18,73 @@ import { createHandlers } from "./handlers.js";
|
|
|
19
18
|
const require = createRequire(import.meta.url);
|
|
20
19
|
const { version: PKG_VERSION } = require("./package.json");
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
export async function startServer() {
|
|
22
|
+
// Get vault path from environment
|
|
23
|
+
const VAULT_PATH = process.env.VAULT_PATH || (os.homedir() + "/Documents/PKM");
|
|
24
24
|
|
|
25
|
-
// Template registry (populated at startup)
|
|
26
|
-
let templateRegistry = new Map();
|
|
27
|
-
let templateDescriptions = "";
|
|
25
|
+
// Template registry (populated at startup)
|
|
26
|
+
let templateRegistry = new Map();
|
|
27
|
+
let templateDescriptions = "";
|
|
28
28
|
|
|
29
|
-
// Semantic index (populated at startup if OPENAI_API_KEY is set)
|
|
30
|
-
let semanticIndex = null;
|
|
29
|
+
// Semantic index (populated at startup if OPENAI_API_KEY is set)
|
|
30
|
+
let semanticIndex = null;
|
|
31
31
|
|
|
32
|
-
// Activity log (populated at startup)
|
|
33
|
-
let activityLog = null;
|
|
34
|
-
const SESSION_ID = crypto.randomUUID();
|
|
32
|
+
// Activity log (populated at startup)
|
|
33
|
+
let activityLog = null;
|
|
34
|
+
const SESSION_ID = crypto.randomUUID();
|
|
35
35
|
|
|
36
|
-
// Handler map (populated at startup)
|
|
37
|
-
let handlers;
|
|
36
|
+
// Handler map (populated at startup)
|
|
37
|
+
let handlers;
|
|
38
38
|
|
|
39
|
-
// Create the server
|
|
40
|
-
const server = new Server(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
);
|
|
39
|
+
// Create the server
|
|
40
|
+
const server = new Server(
|
|
41
|
+
{ name: "pkm-mcp-server", version: PKG_VERSION },
|
|
42
|
+
{ capabilities: { tools: {} } }
|
|
43
|
+
);
|
|
44
44
|
|
|
45
|
-
// List available tools
|
|
46
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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"]
|
|
66
68
|
},
|
|
67
|
-
|
|
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)." }
|
|
68
70
|
},
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
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.
|
|
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
88
|
|
|
89
89
|
Available templates:
|
|
90
90
|
${templateDescriptions || "(Loading...)"}
|
|
@@ -96,437 +96,444 @@ Built-in variables (auto-substituted):
|
|
|
96
96
|
Required: frontmatter.tags - provide at least one tag for the note.
|
|
97
97
|
Optional: frontmatter.status, frontmatter.priority, frontmatter.project, frontmatter.deciders, frontmatter.due, frontmatter.source (depending on template type).
|
|
98
98
|
Pass custom <%...%> variables via the 'variables' parameter.`,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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. For tasks: pending, active, done, cancelled" },
|
|
119
|
+
priority: { type: "string", description: "Priority level. For tasks: low, normal, high, urgent" },
|
|
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." }
|
|
106
141
|
},
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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" }
|
|
112
154
|
},
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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. Field values are validated against the note's type (e.g. task status must be: pending, active, done, cancelled; task priority must be: low, normal, high, urgent).",
|
|
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
|
|
124
169
|
}
|
|
125
170
|
},
|
|
126
|
-
|
|
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. Field values are validated against the note's type (e.g. task status must be: pending, active, done, cancelled; task priority must be: low, normal, high, urgent).",
|
|
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')" }
|
|
171
|
+
required: ["path", "fields"]
|
|
196
172
|
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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"]
|
|
207
185
|
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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"
|
|
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')" }
|
|
235
196
|
}
|
|
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
197
|
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
include_inline: { type: "boolean", description: "Also parse inline #tags from note bodies (default: false, frontmatter only)", default: false }
|
|
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
|
+
}
|
|
281
208
|
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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')"
|
|
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" }
|
|
312
218
|
},
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
+
}
|
|
316
236
|
},
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
+
},
|
|
320
269
|
}
|
|
321
270
|
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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)" }
|
|
360
331
|
},
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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 }
|
|
364
344
|
},
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
345
|
+
required: ["old_path", "new_path"]
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
name: "vault_capture",
|
|
350
|
+
description: "Signal that something is worth capturing in the PKM vault. " +
|
|
351
|
+
"Returns immediately — a background agent handles the actual note creation. " +
|
|
352
|
+
"Use this when you identify a decision, task, or research finding worth preserving.",
|
|
353
|
+
inputSchema: {
|
|
354
|
+
type: "object",
|
|
355
|
+
properties: {
|
|
356
|
+
type: {
|
|
357
|
+
type: "string",
|
|
358
|
+
enum: ["adr", "task", "research", "bug"],
|
|
359
|
+
description: "The type of capture: adr (decision), task, research (finding/pattern), bug (issue/fix)"
|
|
360
|
+
},
|
|
361
|
+
title: {
|
|
362
|
+
type: "string",
|
|
363
|
+
description: "Brief descriptive title (e.g., 'Use sqlite-vec over Chroma')"
|
|
364
|
+
},
|
|
365
|
+
content: {
|
|
366
|
+
type: "string",
|
|
367
|
+
description: "The substance of the capture — context, rationale, details. 1-5 sentences."
|
|
368
|
+
},
|
|
369
|
+
priority: {
|
|
370
|
+
type: "string",
|
|
371
|
+
enum: ["low", "normal", "high", "urgent"],
|
|
372
|
+
description: "Priority level (tasks only, default: normal)"
|
|
373
|
+
},
|
|
374
|
+
project: {
|
|
375
|
+
type: "string",
|
|
376
|
+
description: "Project name for vault routing (e.g., 'Obsidian-MCP'). If omitted, inferred from session context."
|
|
377
|
+
}
|
|
368
378
|
},
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
379
|
+
required: ["type", "title", "content"]
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
];
|
|
383
|
+
|
|
384
|
+
if (semanticIndex?.isAvailable) {
|
|
385
|
+
tools.push({
|
|
386
|
+
name: "vault_semantic_search",
|
|
387
|
+
description: "Search the vault using semantic similarity. Finds conceptually related notes even when they use different words. Requires OPENAI_API_KEY.",
|
|
388
|
+
inputSchema: {
|
|
389
|
+
type: "object",
|
|
390
|
+
properties: {
|
|
391
|
+
query: { type: "string", description: "Natural language search query (e.g., 'managing information overload')" },
|
|
392
|
+
limit: { type: "number", description: "Max results to return (default: 5)", default: 5 },
|
|
393
|
+
folder: { type: "string", description: "Optional: limit search to this folder (e.g., '01-Projects')" },
|
|
394
|
+
threshold: { type: "number", description: "Minimum similarity score 0-1 (default: no threshold)" }
|
|
373
395
|
},
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
396
|
+
required: ["query"]
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
tools.push({
|
|
400
|
+
name: "vault_suggest_links",
|
|
401
|
+
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]].",
|
|
402
|
+
inputSchema: {
|
|
403
|
+
type: "object",
|
|
404
|
+
properties: {
|
|
405
|
+
content: { type: "string", description: "Text content to find link suggestions for. Takes precedence over path." },
|
|
406
|
+
path: { type: "string", description: "Path to an existing note to suggest links for. Used if content is not provided." },
|
|
407
|
+
limit: { type: "number", description: "Max suggestions to return (default: 5)", default: 5 },
|
|
408
|
+
folder: { type: "string", description: "Optional: limit suggestions to this folder (e.g., '01-Projects')" },
|
|
409
|
+
threshold: { type: "number", description: "Minimum similarity score 0-1 (default: no threshold)" }
|
|
377
410
|
}
|
|
378
|
-
}
|
|
379
|
-
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return { tools };
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Handle tool calls
|
|
419
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
420
|
+
const { name, arguments: args } = request.params;
|
|
421
|
+
|
|
422
|
+
// Log activity (skip vault_activity to avoid noise)
|
|
423
|
+
if (name !== "vault_activity") {
|
|
424
|
+
try { activityLog?.log(name, args); } catch (e) { console.error(`Activity log: ${e.message}`); }
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
const handler = handlers.get(name);
|
|
429
|
+
if (!handler) {
|
|
430
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
380
431
|
}
|
|
432
|
+
return await handler(args);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
return {
|
|
435
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
436
|
+
isError: true
|
|
437
|
+
};
|
|
381
438
|
}
|
|
382
|
-
|
|
439
|
+
});
|
|
383
440
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
limit: { type: "number", description: "Max results to return (default: 5)", default: 5 },
|
|
393
|
-
folder: { type: "string", description: "Optional: limit search to this folder (e.g., '01-Projects')" },
|
|
394
|
-
threshold: { type: "number", description: "Minimum similarity score 0-1 (default: no threshold)" }
|
|
395
|
-
},
|
|
396
|
-
required: ["query"]
|
|
441
|
+
// Initialize and start the server
|
|
442
|
+
async function initializeServer() {
|
|
443
|
+
// Validate vault path exists
|
|
444
|
+
try {
|
|
445
|
+
const stat = await fs.stat(VAULT_PATH);
|
|
446
|
+
if (!stat.isDirectory()) {
|
|
447
|
+
console.error(`Error: VAULT_PATH is not a directory: ${VAULT_PATH}`);
|
|
448
|
+
process.exit(1);
|
|
397
449
|
}
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
type: "object",
|
|
404
|
-
properties: {
|
|
405
|
-
content: { type: "string", description: "Text content to find link suggestions for. Takes precedence over path." },
|
|
406
|
-
path: { type: "string", description: "Path to an existing note to suggest links for. Used if content is not provided." },
|
|
407
|
-
limit: { type: "number", description: "Max suggestions to return (default: 5)", default: 5 },
|
|
408
|
-
folder: { type: "string", description: "Optional: limit suggestions to this folder (e.g., '01-Projects')" },
|
|
409
|
-
threshold: { type: "number", description: "Minimum similarity score 0-1 (default: no threshold)" }
|
|
410
|
-
}
|
|
450
|
+
} catch (e) {
|
|
451
|
+
if (e.code === "ENOENT") {
|
|
452
|
+
console.error(`Error: VAULT_PATH does not exist: ${VAULT_PATH}`);
|
|
453
|
+
console.error("Set the VAULT_PATH environment variable to your Obsidian vault directory.");
|
|
454
|
+
process.exit(1);
|
|
411
455
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
return { tools };
|
|
416
|
-
});
|
|
456
|
+
throw e;
|
|
457
|
+
}
|
|
417
458
|
|
|
418
|
-
|
|
419
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
420
|
-
const { name, arguments: args } = request.params;
|
|
459
|
+
templateRegistry = await loadTemplates(VAULT_PATH);
|
|
421
460
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const handler = handlers.get(name);
|
|
429
|
-
if (!handler) {
|
|
430
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
461
|
+
if (templateRegistry.size > 0) {
|
|
462
|
+
templateDescriptions = Array.from(templateRegistry.values())
|
|
463
|
+
.map(t => `- **${t.shortName}**: ${t.description}`)
|
|
464
|
+
.join("\n");
|
|
465
|
+
} else {
|
|
466
|
+
templateDescriptions = "(No templates found - add .md files to 05-Templates/)";
|
|
431
467
|
}
|
|
432
|
-
return await handler(args);
|
|
433
|
-
} catch (error) {
|
|
434
|
-
return {
|
|
435
|
-
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
436
|
-
isError: true
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
});
|
|
440
468
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
469
|
+
const openaiApiKey = process.env.OPENAI_API_KEY;
|
|
470
|
+
if (openaiApiKey) {
|
|
471
|
+
try {
|
|
472
|
+
semanticIndex = new SemanticIndex({ vaultPath: VAULT_PATH, openaiApiKey });
|
|
473
|
+
await semanticIndex.initialize();
|
|
474
|
+
console.error("Semantic index initialized");
|
|
475
|
+
} catch (err) {
|
|
476
|
+
console.error(`Semantic index init failed (non-fatal): ${err.message}`);
|
|
477
|
+
semanticIndex = null;
|
|
478
|
+
}
|
|
479
|
+
} else {
|
|
480
|
+
console.error("OPENAI_API_KEY not set — semantic search disabled");
|
|
449
481
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
activityLog = new ActivityLog({ vaultPath: VAULT_PATH, sessionId: SESSION_ID });
|
|
485
|
+
await activityLog.initialize();
|
|
486
|
+
console.error(`Activity log initialized (session: ${SESSION_ID.slice(0, 8)})`);
|
|
487
|
+
} catch (err) {
|
|
488
|
+
console.error(`Activity log init failed (non-fatal): ${err.message}`);
|
|
489
|
+
activityLog = null;
|
|
455
490
|
}
|
|
456
|
-
throw e;
|
|
457
|
-
}
|
|
458
491
|
|
|
459
|
-
|
|
492
|
+
handlers = await createHandlers({
|
|
493
|
+
vaultPath: VAULT_PATH,
|
|
494
|
+
templateRegistry,
|
|
495
|
+
semanticIndex,
|
|
496
|
+
activityLog,
|
|
497
|
+
sessionId: SESSION_ID,
|
|
498
|
+
});
|
|
460
499
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
.join("\n");
|
|
465
|
-
} else {
|
|
466
|
-
templateDescriptions = "(No templates found - add .md files to 05-Templates/)";
|
|
500
|
+
const transport = new StdioServerTransport();
|
|
501
|
+
await server.connect(transport);
|
|
502
|
+
console.error(`PKM MCP Server running... (${templateRegistry.size} templates loaded${semanticIndex?.isAvailable ? ", semantic search enabled" : ""}, activity log ${activityLog ? "enabled" : "disabled"})`);
|
|
467
503
|
}
|
|
468
504
|
|
|
469
|
-
|
|
470
|
-
|
|
505
|
+
let shuttingDown = false;
|
|
506
|
+
|
|
507
|
+
async function shutdown() {
|
|
508
|
+
if (shuttingDown) return;
|
|
509
|
+
shuttingDown = true;
|
|
510
|
+
console.error("Shutting down...");
|
|
511
|
+
const forceTimer = setTimeout(() => {
|
|
512
|
+
console.error("Shutdown timed out, forcing exit");
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}, 5000);
|
|
515
|
+
forceTimer.unref();
|
|
471
516
|
try {
|
|
472
|
-
semanticIndex
|
|
473
|
-
|
|
474
|
-
console.error(
|
|
475
|
-
} catch (err) {
|
|
476
|
-
console.error(`Semantic index init failed (non-fatal): ${err.message}`);
|
|
477
|
-
semanticIndex = null;
|
|
517
|
+
if (semanticIndex) await semanticIndex.shutdown();
|
|
518
|
+
} catch (e) {
|
|
519
|
+
console.error(`Semantic index shutdown error: ${e.message}`);
|
|
478
520
|
}
|
|
479
|
-
|
|
480
|
-
|
|
521
|
+
if (activityLog) activityLog.shutdown();
|
|
522
|
+
clearTimeout(forceTimer);
|
|
523
|
+
process.exit(0);
|
|
481
524
|
}
|
|
482
525
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
await activityLog.initialize();
|
|
486
|
-
console.error(`Activity log initialized (session: ${SESSION_ID.slice(0, 8)})`);
|
|
487
|
-
} catch (err) {
|
|
488
|
-
console.error(`Activity log init failed (non-fatal): ${err.message}`);
|
|
489
|
-
activityLog = null;
|
|
490
|
-
}
|
|
526
|
+
process.on("SIGINT", () => shutdown());
|
|
527
|
+
process.on("SIGTERM", () => shutdown());
|
|
491
528
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
semanticIndex,
|
|
496
|
-
activityLog,
|
|
497
|
-
sessionId: SESSION_ID,
|
|
529
|
+
initializeServer().catch(err => {
|
|
530
|
+
console.error("Fatal:", err);
|
|
531
|
+
process.exit(1);
|
|
498
532
|
});
|
|
499
|
-
|
|
500
|
-
const transport = new StdioServerTransport();
|
|
501
|
-
await server.connect(transport);
|
|
502
|
-
console.error(`PKM MCP Server running... (${templateRegistry.size} templates loaded${semanticIndex?.isAvailable ? ", semantic search enabled" : ""}, activity log ${activityLog ? "enabled" : "disabled"})`);
|
|
503
533
|
}
|
|
504
534
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
shuttingDown = true;
|
|
510
|
-
console.error("Shutting down...");
|
|
511
|
-
const forceTimer = setTimeout(() => {
|
|
512
|
-
console.error("Shutdown timed out, forcing exit");
|
|
513
|
-
process.exit(1);
|
|
514
|
-
}, 5000);
|
|
515
|
-
forceTimer.unref();
|
|
516
|
-
try {
|
|
517
|
-
if (semanticIndex) await semanticIndex.shutdown();
|
|
518
|
-
} catch (e) {
|
|
519
|
-
console.error(`Semantic index shutdown error: ${e.message}`);
|
|
520
|
-
}
|
|
521
|
-
if (activityLog) activityLog.shutdown();
|
|
522
|
-
clearTimeout(forceTimer);
|
|
523
|
-
process.exit(0);
|
|
535
|
+
// Auto-start when run directly (backward compat: existing users may have
|
|
536
|
+
// "args": ["/path/to/index.js"] in their settings.json)
|
|
537
|
+
if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
|
|
538
|
+
startServer();
|
|
524
539
|
}
|
|
525
|
-
|
|
526
|
-
process.on("SIGINT", () => shutdown());
|
|
527
|
-
process.on("SIGTERM", () => shutdown());
|
|
528
|
-
|
|
529
|
-
initializeServer().catch(err => {
|
|
530
|
-
console.error("Fatal:", err);
|
|
531
|
-
process.exit(1);
|
|
532
|
-
});
|