obsidian-native-mcp 0.2.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.
Files changed (54) hide show
  1. package/.github/workflows/ci.yml +69 -0
  2. package/.husky/pre-commit +1 -0
  3. package/.prettierignore +3 -0
  4. package/.prettierrc +7 -0
  5. package/.releaserc.json +24 -0
  6. package/DEVELOPER.md +158 -0
  7. package/LICENSE +21 -0
  8. package/README.md +179 -0
  9. package/dist/cli/index.js +22 -0
  10. package/dist/cli/index.js.map +1 -0
  11. package/dist/handlers/prompts.js +127 -0
  12. package/dist/handlers/prompts.js.map +1 -0
  13. package/dist/handlers/tools.js +113 -0
  14. package/dist/handlers/tools.js.map +1 -0
  15. package/dist/mcp/http-transport.js +124 -0
  16. package/dist/mcp/http-transport.js.map +1 -0
  17. package/dist/mcp/protocol.js +38 -0
  18. package/dist/mcp/protocol.js.map +1 -0
  19. package/dist/mcp/server.js +257 -0
  20. package/dist/mcp/server.js.map +1 -0
  21. package/dist/mcp/stdio-transport.js +41 -0
  22. package/dist/mcp/stdio-transport.js.map +1 -0
  23. package/dist/mcp/transport.js +3 -0
  24. package/dist/mcp/transport.js.map +1 -0
  25. package/dist/plugin/main.js +1201 -0
  26. package/dist/plugin/main.js.map +1 -0
  27. package/dist/plugin/manifest.json +10 -0
  28. package/dist/plugin/settings.js +63 -0
  29. package/dist/plugin/settings.js.map +1 -0
  30. package/dist/utils/fs-utils.js +268 -0
  31. package/dist/utils/fs-utils.js.map +1 -0
  32. package/dist/utils/search.js +62 -0
  33. package/dist/utils/search.js.map +1 -0
  34. package/dist/utils/vaults.js +165 -0
  35. package/dist/utils/vaults.js.map +1 -0
  36. package/eslint.config.mjs +16 -0
  37. package/manifest.json +10 -0
  38. package/package.json +48 -0
  39. package/scripts/build-plugin.mjs +35 -0
  40. package/scripts/sync-version.cjs +12 -0
  41. package/src/cli/index.ts +25 -0
  42. package/src/handlers/prompts.ts +148 -0
  43. package/src/handlers/tools.ts +146 -0
  44. package/src/mcp/http-transport.ts +138 -0
  45. package/src/mcp/protocol.ts +69 -0
  46. package/src/mcp/server.ts +272 -0
  47. package/src/mcp/stdio-transport.ts +43 -0
  48. package/src/mcp/transport.ts +8 -0
  49. package/src/plugin/main.ts +91 -0
  50. package/src/plugin/settings.ts +69 -0
  51. package/src/utils/fs-utils.ts +358 -0
  52. package/src/utils/search.ts +84 -0
  53. package/src/utils/vaults.ts +175 -0
  54. package/tsconfig.json +20 -0
@@ -0,0 +1,272 @@
1
+ import type { JSONRPCRequest, JSONRPCResponse, ToolDefinition } from "./protocol";
2
+ import { VaultFileHandler } from "../handlers/tools";
3
+ import { PromptHandler } from "../handlers/prompts";
4
+ import type { VaultRegistry } from "../utils/vaults";
5
+
6
+ const vaultParam = {
7
+ vault: {
8
+ type: "string",
9
+ description:
10
+ "Vault name (derived from the directory name). Required when multiple vaults are configured.",
11
+ },
12
+ };
13
+
14
+ export interface ServerInstance {
15
+ toolDefinitions: ToolDefinition[];
16
+ handleRequest(msg: JSONRPCRequest): Promise<JSONRPCResponse | null>;
17
+ }
18
+
19
+ export function createServer(registry: VaultRegistry): ServerInstance {
20
+ const vaultList = registry.list();
21
+ const vaultNames = vaultList.map((v) => v.name);
22
+ vaultParam.vault.description = `Vault name. Available: ${vaultNames.join(", ")}`;
23
+
24
+ const fileHandler = new VaultFileHandler(registry);
25
+ const promptHandler = new PromptHandler(registry);
26
+ const toolHandlers = fileHandler.getHandlers();
27
+
28
+ const toolDefinitions: ToolDefinition[] = [
29
+ {
30
+ name: "list_vaults",
31
+ description: "List all configured vaults with their paths.",
32
+ inputSchema: { type: "object", properties: {} },
33
+ },
34
+ {
35
+ name: "get_vault_info",
36
+ description: "Get stats for a vault (file count, path, name).",
37
+ inputSchema: {
38
+ type: "object",
39
+ properties: {
40
+ vault: {
41
+ type: "string",
42
+ description: `Vault name. Available: ${vaultNames.join(", ")}`,
43
+ },
44
+ },
45
+ },
46
+ },
47
+ {
48
+ name: "list_files",
49
+ description: "List files in the root directory or a specified subdirectory of your vault.",
50
+ inputSchema: {
51
+ type: "object",
52
+ properties: {
53
+ ...vaultParam,
54
+ directory: {
55
+ type: "string",
56
+ description: "Optional subdirectory path relative to vault root",
57
+ },
58
+ },
59
+ },
60
+ },
61
+ {
62
+ name: "get_file",
63
+ description: "Get the content of a file from your vault.",
64
+ inputSchema: {
65
+ type: "object",
66
+ properties: {
67
+ ...vaultParam,
68
+ filename: {
69
+ type: "string",
70
+ description: "Path to the file relative to vault root",
71
+ },
72
+ format: {
73
+ type: "string",
74
+ enum: ["markdown", "json"],
75
+ description: "Return format (json includes frontmatter)",
76
+ },
77
+ },
78
+ required: ["filename"],
79
+ },
80
+ },
81
+ {
82
+ name: "create_file",
83
+ description: "Create a new file in your vault or update an existing one.",
84
+ inputSchema: {
85
+ type: "object",
86
+ properties: {
87
+ ...vaultParam,
88
+ filename: {
89
+ type: "string",
90
+ description: "Path to the file relative to vault root",
91
+ },
92
+ content: { type: "string", description: "File content" },
93
+ },
94
+ required: ["filename", "content"],
95
+ },
96
+ },
97
+ {
98
+ name: "append_to_file",
99
+ description: "Append content to a new or existing file.",
100
+ inputSchema: {
101
+ type: "object",
102
+ properties: {
103
+ ...vaultParam,
104
+ filename: {
105
+ type: "string",
106
+ description: "Path to the file relative to vault root",
107
+ },
108
+ content: {
109
+ type: "string",
110
+ description: "Content to append",
111
+ },
112
+ },
113
+ required: ["filename", "content"],
114
+ },
115
+ },
116
+ {
117
+ name: "patch_file",
118
+ description:
119
+ "Insert or modify content in a file relative to a heading, block reference, or frontmatter field.",
120
+ inputSchema: {
121
+ type: "object",
122
+ properties: {
123
+ ...vaultParam,
124
+ filename: { type: "string" },
125
+ operation: {
126
+ type: "string",
127
+ enum: ["append", "prepend", "replace"],
128
+ },
129
+ targetType: {
130
+ type: "string",
131
+ enum: ["heading", "block", "frontmatter"],
132
+ },
133
+ target: { type: "string" },
134
+ content: { type: "string" },
135
+ contentType: {
136
+ type: "string",
137
+ enum: ["text/markdown", "application/json"],
138
+ },
139
+ targetDelimiter: { type: "string", default: "::" },
140
+ trimTargetWhitespace: { type: "boolean", default: true },
141
+ },
142
+ required: ["filename", "operation", "targetType", "target", "content"],
143
+ },
144
+ },
145
+ {
146
+ name: "delete_file",
147
+ description: "Delete a file from your vault.",
148
+ inputSchema: {
149
+ type: "object",
150
+ properties: {
151
+ ...vaultParam,
152
+ filename: {
153
+ type: "string",
154
+ description: "Path to the file relative to vault root",
155
+ },
156
+ },
157
+ required: ["filename"],
158
+ },
159
+ },
160
+ {
161
+ name: "search",
162
+ description: "Search for documents matching a text query in your vault.",
163
+ inputSchema: {
164
+ type: "object",
165
+ properties: {
166
+ ...vaultParam,
167
+ query: { type: "string", description: "Text to search for" },
168
+ directory: {
169
+ type: "string",
170
+ description: "Optional directory to scope the search",
171
+ },
172
+ contextLength: {
173
+ type: "number",
174
+ description: "Characters of context around each match",
175
+ },
176
+ },
177
+ required: ["query"],
178
+ },
179
+ },
180
+ ];
181
+
182
+ async function handleRequest(msg: JSONRPCRequest): Promise<JSONRPCResponse | null> {
183
+ const { method, params, id } = msg;
184
+
185
+ switch (method) {
186
+ case "initialize": {
187
+ const clientProtocol = params?.protocolVersion;
188
+ return {
189
+ jsonrpc: "2.0",
190
+ id,
191
+ result: {
192
+ protocolVersion: clientProtocol || "2024-11-05",
193
+ capabilities: { tools: {}, prompts: {} },
194
+ serverInfo: { name: "obsidian-native-mcp", version: "0.2.0" },
195
+ },
196
+ };
197
+ }
198
+
199
+ case "tools/list":
200
+ return {
201
+ jsonrpc: "2.0",
202
+ id,
203
+ result: { tools: toolDefinitions },
204
+ };
205
+
206
+ case "tools/call": {
207
+ const toolName = params?.name;
208
+ const args = params?.arguments || {};
209
+ const handler = toolHandlers[toolName as string];
210
+ if (!handler) {
211
+ return {
212
+ jsonrpc: "2.0",
213
+ id,
214
+ error: { code: -32601, message: `Unknown tool: ${toolName}` },
215
+ };
216
+ }
217
+ try {
218
+ const result = await handler(args);
219
+ return { jsonrpc: "2.0", id, result };
220
+ } catch (err: any) {
221
+ return {
222
+ jsonrpc: "2.0",
223
+ id,
224
+ error: { code: -32603, message: err.message || String(err) },
225
+ };
226
+ }
227
+ }
228
+
229
+ case "prompts/list": {
230
+ try {
231
+ const promptVault = params?.arguments?.vault;
232
+ const promptList = await promptHandler.list(promptVault);
233
+ return { jsonrpc: "2.0", id, result: { prompts: promptList } };
234
+ } catch (err: any) {
235
+ return {
236
+ jsonrpc: "2.0",
237
+ id,
238
+ error: { code: -32603, message: err.message || String(err) },
239
+ };
240
+ }
241
+ }
242
+
243
+ case "prompts/get": {
244
+ try {
245
+ const promptVault = params?.arguments?.vault;
246
+ const result = await promptHandler.get(params?.name, promptVault);
247
+ return { jsonrpc: "2.0", id, result };
248
+ } catch (err: any) {
249
+ return {
250
+ jsonrpc: "2.0",
251
+ id,
252
+ error: { code: -32603, message: err.message || String(err) },
253
+ };
254
+ }
255
+ }
256
+
257
+ case "notifications/initialized":
258
+ case "notifications/cancelled":
259
+ case "notifications/roots/list_changed":
260
+ return null;
261
+
262
+ default:
263
+ return {
264
+ jsonrpc: "2.0",
265
+ id,
266
+ error: { code: -32601, message: `Method not found: ${method}` },
267
+ };
268
+ }
269
+ }
270
+
271
+ return { toolDefinitions, handleRequest };
272
+ }
@@ -0,0 +1,43 @@
1
+ import type { JSONRPCRequest, JSONRPCResponse } from "./protocol";
2
+ import { sendMessage, parseMessages } from "./protocol";
3
+
4
+ export class StdioTransport {
5
+ private requestHandler: ((request: JSONRPCRequest) => Promise<JSONRPCResponse | null>) | null =
6
+ null;
7
+ private stdinBuffer = { buffer: "" };
8
+
9
+ onRequest(handler: (request: JSONRPCRequest) => Promise<JSONRPCResponse | null>): void {
10
+ this.requestHandler = handler;
11
+ }
12
+
13
+ sendMessage(message: JSONRPCResponse): void {
14
+ sendMessage(message);
15
+ }
16
+
17
+ start(): void {
18
+ process.stdin.on("data", async (chunk: Buffer) => {
19
+ this.stdinBuffer.buffer += chunk.toString();
20
+
21
+ const { messages, remaining } = parseMessages(this.stdinBuffer.buffer);
22
+ this.stdinBuffer.buffer = remaining;
23
+
24
+ for (const jsonStr of messages) {
25
+ try {
26
+ const msg = JSON.parse(jsonStr);
27
+ if (msg.jsonrpc !== "2.0") continue;
28
+
29
+ if ("method" in msg && "id" in msg) {
30
+ const response = await this.requestHandler?.(msg as JSONRPCRequest);
31
+ if (response) this.sendMessage(response);
32
+ }
33
+ } catch (err: any) {
34
+ console.error("Error processing message:", err.message);
35
+ }
36
+ }
37
+ });
38
+ }
39
+
40
+ close(): void {
41
+ process.stdin.removeAllListeners("data");
42
+ }
43
+ }
@@ -0,0 +1,8 @@
1
+ import type { JSONRPCRequest, JSONRPCResponse } from "./protocol";
2
+
3
+ export interface Transport {
4
+ sendMessage(message: JSONRPCResponse): void;
5
+ onRequest(handler: (request: JSONRPCRequest) => Promise<JSONRPCResponse | null>): void;
6
+ start(): void;
7
+ close(): void;
8
+ }
@@ -0,0 +1,91 @@
1
+ import { Plugin } from "obsidian";
2
+ import { HttpTransport } from "../mcp/http-transport";
3
+ import { createServer } from "../mcp/server";
4
+ import { VaultRegistry } from "../utils/vaults";
5
+ import { NativeMcpSettingTab } from "./settings";
6
+
7
+ interface PluginSettings {
8
+ selectedVaults: string[];
9
+ }
10
+
11
+ const DEFAULT_SETTINGS: PluginSettings = {
12
+ selectedVaults: [],
13
+ };
14
+
15
+ export default class NativeMcpPlugin extends Plugin {
16
+ private transport: HttpTransport | null = null;
17
+ private settings: PluginSettings = DEFAULT_SETTINGS;
18
+ private registry = new VaultRegistry();
19
+
20
+ async onload(): Promise<void> {
21
+ await this.loadSettings();
22
+
23
+ const allVaults = VaultRegistry.discoverFromObsidian();
24
+
25
+ if (this.settings.selectedVaults.length > 0) {
26
+ const selected = allVaults.filter((v) => this.settings.selectedVaults.includes(v.name));
27
+ if (selected.length > 0) {
28
+ this.registry.configure(selected);
29
+ }
30
+ }
31
+
32
+ if (this.registry.list().length > 0) {
33
+ await this.startServer();
34
+ }
35
+
36
+ this.addSettingTab(new NativeMcpSettingTab(this.app, this));
37
+ }
38
+
39
+ async startServer(): Promise<void> {
40
+ this.transport?.close();
41
+
42
+ const server = createServer(this.registry);
43
+ this.transport = new HttpTransport();
44
+ this.transport.onRequest(async (msg) => server.handleRequest(msg));
45
+ await this.transport.start();
46
+ }
47
+
48
+ restartServer(): void {
49
+ this.startServer();
50
+ }
51
+
52
+ getServerUrl(): string | null {
53
+ return this.transport?.url ?? null;
54
+ }
55
+
56
+ getRegistry(): VaultRegistry {
57
+ return this.registry;
58
+ }
59
+
60
+ async loadSettings(): Promise<void> {
61
+ this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
62
+ }
63
+
64
+ async saveSettings(): Promise<void> {
65
+ await this.saveData(this.settings);
66
+ }
67
+
68
+ getSettings(): PluginSettings {
69
+ return this.settings;
70
+ }
71
+
72
+ async updateSelectedVaults(names: string[]): Promise<void> {
73
+ this.settings.selectedVaults = names;
74
+ await this.saveSettings();
75
+
76
+ const allVaults = VaultRegistry.discoverFromObsidian();
77
+ const selected = allVaults.filter((v) => names.includes(v.name));
78
+ this.registry.configure(selected);
79
+
80
+ if (selected.length > 0) {
81
+ await this.startServer();
82
+ } else {
83
+ this.transport?.close();
84
+ this.transport = null;
85
+ }
86
+ }
87
+
88
+ onunload(): void {
89
+ this.transport?.close();
90
+ }
91
+ }
@@ -0,0 +1,69 @@
1
+ import { App, PluginSettingTab, Setting } from "obsidian";
2
+ import { VaultRegistry } from "../utils/vaults";
3
+ import type NativeMcpPlugin from "./main";
4
+
5
+ export class NativeMcpSettingTab extends PluginSettingTab {
6
+ private plugin: NativeMcpPlugin;
7
+
8
+ constructor(app: App, plugin: NativeMcpPlugin) {
9
+ super(app, plugin);
10
+ this.plugin = plugin;
11
+ }
12
+
13
+ display(): void {
14
+ const { containerEl } = this;
15
+ containerEl.empty();
16
+
17
+ containerEl.createEl("h2", { text: "Obsidian Native MCP" });
18
+
19
+ const url = this.plugin.getServerUrl();
20
+ if (url) {
21
+ new Setting(containerEl)
22
+ .setName("MCP Server URL")
23
+ .setDesc("Add this URL to your Claude Desktop config")
24
+ .addText((text) => {
25
+ text.setValue(url).setDisabled(true);
26
+ text.inputEl.style.width = "100%";
27
+ })
28
+ .addButton((btn) => {
29
+ btn.setButtonText("Copy").onClick(async () => {
30
+ await (navigator as any).clipboard.writeText(url);
31
+ btn.setButtonText("Copied!");
32
+ setTimeout(() => btn.setButtonText("Copy"), 2000);
33
+ });
34
+ });
35
+ } else {
36
+ new Setting(containerEl)
37
+ .setName("Server inactive")
38
+ .setDesc("Select at least one vault below to start the MCP server.");
39
+ }
40
+
41
+ containerEl.createEl("h3", { text: "Vaults" });
42
+
43
+ const allVaults = VaultRegistry.discoverFromObsidian();
44
+ const selected = this.plugin.getSettings().selectedVaults;
45
+
46
+ if (allVaults.length === 0) {
47
+ containerEl.createEl("p", {
48
+ text: "No Obsidian vaults found. Make sure Obsidian has been started at least once.",
49
+ });
50
+ return;
51
+ }
52
+
53
+ for (const vault of allVaults) {
54
+ const isSelected = selected.includes(vault.name);
55
+ new Setting(containerEl)
56
+ .setName(vault.name)
57
+ .setDesc(vault.path)
58
+ .addToggle((toggle) =>
59
+ toggle.setValue(isSelected).onChange(async (value) => {
60
+ const newSelected = value
61
+ ? [...selected, vault.name]
62
+ : selected.filter((n) => n !== vault.name);
63
+ await this.plugin.updateSelectedVaults(newSelected);
64
+ this.display();
65
+ }),
66
+ );
67
+ }
68
+ }
69
+ }