joplin-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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +384 -0
  3. package/dist/bin/cli.d.ts +2 -0
  4. package/dist/bin/cli.js +7 -0
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +204 -0
  7. package/dist/lib/joplin-api-client.d.ts +23 -0
  8. package/dist/lib/joplin-api-client.js +110 -0
  9. package/dist/lib/logger.d.ts +21 -0
  10. package/dist/lib/logger.js +68 -0
  11. package/dist/lib/parse-args.d.ts +2 -0
  12. package/dist/lib/parse-args.js +81 -0
  13. package/dist/lib/tools/base-tool.d.ts +27 -0
  14. package/dist/lib/tools/base-tool.js +24 -0
  15. package/dist/lib/tools/create-folder.d.ts +9 -0
  16. package/dist/lib/tools/create-folder.js +79 -0
  17. package/dist/lib/tools/create-note.d.ts +13 -0
  18. package/dist/lib/tools/create-note.js +88 -0
  19. package/dist/lib/tools/delete-folder.d.ts +10 -0
  20. package/dist/lib/tools/delete-folder.js +138 -0
  21. package/dist/lib/tools/delete-note.d.ts +9 -0
  22. package/dist/lib/tools/delete-note.js +92 -0
  23. package/dist/lib/tools/edit-folder.d.ts +10 -0
  24. package/dist/lib/tools/edit-folder.js +136 -0
  25. package/dist/lib/tools/edit-note.d.ts +15 -0
  26. package/dist/lib/tools/edit-note.js +153 -0
  27. package/dist/lib/tools/index.d.ts +12 -0
  28. package/dist/lib/tools/index.js +12 -0
  29. package/dist/lib/tools/list-notebooks.d.ts +7 -0
  30. package/dist/lib/tools/list-notebooks.js +59 -0
  31. package/dist/lib/tools/read-multi-note.d.ts +5 -0
  32. package/dist/lib/tools/read-multi-note.js +108 -0
  33. package/dist/lib/tools/read-note.d.ts +5 -0
  34. package/dist/lib/tools/read-note.js +80 -0
  35. package/dist/lib/tools/read-notebook.d.ts +5 -0
  36. package/dist/lib/tools/read-notebook.js +66 -0
  37. package/dist/lib/tools/search-notes.d.ts +5 -0
  38. package/dist/lib/tools/search-notes.js +68 -0
  39. package/dist/tests/integration/joplin-integration.test.d.ts +1 -0
  40. package/dist/tests/integration/joplin-integration.test.js +117 -0
  41. package/dist/tests/manual/create-folder.test.d.ts +1 -0
  42. package/dist/tests/manual/create-folder.test.js +81 -0
  43. package/dist/tests/manual/create-note.test.d.ts +1 -0
  44. package/dist/tests/manual/create-note.test.js +84 -0
  45. package/dist/tests/manual/delete-folder.test.d.ts +1 -0
  46. package/dist/tests/manual/delete-folder.test.js +118 -0
  47. package/dist/tests/manual/delete-note.test.d.ts +1 -0
  48. package/dist/tests/manual/delete-note.test.js +101 -0
  49. package/dist/tests/manual/edit-folder.test.d.ts +1 -0
  50. package/dist/tests/manual/edit-folder.test.js +104 -0
  51. package/dist/tests/manual/edit-note.test.d.ts +1 -0
  52. package/dist/tests/manual/edit-note.test.js +118 -0
  53. package/dist/tests/manual/list-notebooks.test.d.ts +1 -0
  54. package/dist/tests/manual/list-notebooks.test.js +42 -0
  55. package/dist/tests/manual/read-note.test.d.ts +1 -0
  56. package/dist/tests/manual/read-note.test.js +54 -0
  57. package/dist/tests/manual/search-notes.test.d.ts +1 -0
  58. package/dist/tests/manual/search-notes.test.js +43 -0
  59. package/dist/tests/unit/create-tools.test.d.ts +1 -0
  60. package/dist/tests/unit/create-tools.test.js +223 -0
  61. package/dist/tests/unit/delete-tools.test.d.ts +1 -0
  62. package/dist/tests/unit/delete-tools.test.js +225 -0
  63. package/dist/tests/unit/edit-tools.test.d.ts +1 -0
  64. package/dist/tests/unit/edit-tools.test.js +261 -0
  65. package/dist/tests/unit/joplin-api-client.test.d.ts +1 -0
  66. package/dist/tests/unit/joplin-api-client.test.js +154 -0
  67. package/dist/vitest.config.d.ts +2 -0
  68. package/dist/vitest.config.js +22 -0
  69. package/dist/vitest.setup.d.ts +1 -0
  70. package/dist/vitest.setup.js +24 -0
  71. package/package.json +58 -0
@@ -0,0 +1,261 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import JoplinAPIClient from "../../lib/joplin-api-client.js";
3
+ import EditNote from "../../lib/tools/edit-note.js";
4
+ import EditFolder from "../../lib/tools/edit-folder.js";
5
+ // Mock JoplinAPIClient
6
+ vi.mock("../../lib/joplin-api-client.js", () => {
7
+ const mockClient = {
8
+ get: vi.fn(),
9
+ put: vi.fn(),
10
+ };
11
+ return { default: vi.fn(() => mockClient) };
12
+ });
13
+ describe("Edit Tools", () => {
14
+ let mockApiClient;
15
+ let editNote;
16
+ let editFolder;
17
+ beforeEach(() => {
18
+ vi.clearAllMocks();
19
+ mockApiClient = new JoplinAPIClient({ token: "test-token" });
20
+ editNote = new EditNote(mockApiClient);
21
+ editFolder = new EditFolder(mockApiClient);
22
+ });
23
+ describe("EditNote", () => {
24
+ const mockCurrentNote = {
25
+ id: "note-123",
26
+ title: "Original Title",
27
+ body: "Original content",
28
+ parent_id: null,
29
+ is_todo: false,
30
+ todo_completed: false,
31
+ updated_time: 1234567890000,
32
+ };
33
+ const mockUpdatedNote = {
34
+ id: "note-123",
35
+ title: "Updated Title",
36
+ body: "Updated content",
37
+ parent_id: null,
38
+ is_todo: false,
39
+ todo_completed: false,
40
+ updated_time: 1234567891000,
41
+ };
42
+ it("should update note title", async () => {
43
+ mockApiClient.get.mockResolvedValue(mockCurrentNote);
44
+ mockApiClient.put.mockResolvedValue(mockUpdatedNote);
45
+ const result = await editNote.call({
46
+ note_id: "a1b2c3d4e5f6789012345678901234567890abcd",
47
+ title: "Updated Title",
48
+ });
49
+ expect(mockApiClient.get).toHaveBeenCalledWith("/notes/a1b2c3d4e5f6789012345678901234567890abcd", {
50
+ query: { fields: "id,title,body,parent_id,is_todo,todo_completed,todo_due,updated_time" },
51
+ });
52
+ expect(mockApiClient.put).toHaveBeenCalledWith("/notes/a1b2c3d4e5f6789012345678901234567890abcd", {
53
+ title: "Updated Title",
54
+ });
55
+ expect(result).toContain("✅ Successfully updated note!");
56
+ expect(result).toContain('Title: "Original Title" → "Updated Title"');
57
+ });
58
+ it("should update note body", async () => {
59
+ const updatedNote = { ...mockCurrentNote, body: "New content", updated_time: 1234567891000 };
60
+ mockApiClient.get.mockResolvedValue(mockCurrentNote);
61
+ mockApiClient.put.mockResolvedValue(updatedNote);
62
+ const result = await editNote.call({
63
+ note_id: "a1b2c3d4e5f6789012345678901234567890abcd",
64
+ body: "New content",
65
+ });
66
+ expect(mockApiClient.put).toHaveBeenCalledWith("/notes/a1b2c3d4e5f6789012345678901234567890abcd", {
67
+ body: "New content",
68
+ });
69
+ expect(result).toContain("Content: Updated");
70
+ });
71
+ it("should convert note to todo", async () => {
72
+ const updatedNote = { ...mockCurrentNote, is_todo: true, updated_time: 1234567891000 };
73
+ mockApiClient.get.mockResolvedValue(mockCurrentNote);
74
+ mockApiClient.put.mockResolvedValue(updatedNote);
75
+ const result = await editNote.call({
76
+ note_id: "a1b2c3d4e5f6789012345678901234567890abcd",
77
+ is_todo: true,
78
+ });
79
+ expect(mockApiClient.put).toHaveBeenCalledWith("/notes/a1b2c3d4e5f6789012345678901234567890abcd", {
80
+ is_todo: true,
81
+ });
82
+ expect(result).toContain("Type: Regular note → Todo");
83
+ });
84
+ it("should mark todo as completed", async () => {
85
+ const currentTodo = { ...mockCurrentNote, is_todo: true, todo_completed: false };
86
+ const updatedTodo = { ...currentTodo, todo_completed: true, updated_time: 1234567891000 };
87
+ mockApiClient.get.mockResolvedValue(currentTodo);
88
+ mockApiClient.put.mockResolvedValue(updatedTodo);
89
+ const result = await editNote.call({
90
+ note_id: "a1b2c3d4e5f6789012345678901234567890abcd",
91
+ todo_completed: true,
92
+ });
93
+ expect(mockApiClient.put).toHaveBeenCalledWith("/notes/a1b2c3d4e5f6789012345678901234567890abcd", {
94
+ todo_completed: true,
95
+ });
96
+ expect(result).toContain("Todo Status: Not completed → Completed");
97
+ });
98
+ it("should move note to different notebook", async () => {
99
+ const updatedNote = {
100
+ ...mockCurrentNote,
101
+ parent_id: "a1b2c3d4e5f6789012345678901234567890abce",
102
+ updated_time: 1234567891000,
103
+ };
104
+ const mockNotebook = { title: "New Notebook" };
105
+ mockApiClient.get.mockResolvedValueOnce(mockCurrentNote).mockResolvedValueOnce(mockNotebook);
106
+ mockApiClient.put.mockResolvedValue(updatedNote);
107
+ const result = await editNote.call({
108
+ note_id: "a1b2c3d4e5f6789012345678901234567890abcd",
109
+ parent_id: "a1b2c3d4e5f6789012345678901234567890abce",
110
+ });
111
+ expect(mockApiClient.put).toHaveBeenCalledWith("/notes/a1b2c3d4e5f6789012345678901234567890abcd", {
112
+ parent_id: "a1b2c3d4e5f6789012345678901234567890abce",
113
+ });
114
+ expect(result).toContain('Location: Root level → "New Notebook"');
115
+ });
116
+ it("should validate required note_id", async () => {
117
+ const result = await editNote.call({});
118
+ expect(result).toContain("Please provide note edit options");
119
+ expect(mockApiClient.get).not.toHaveBeenCalled();
120
+ });
121
+ it("should validate note_id format", async () => {
122
+ const result = await editNote.call({
123
+ note_id: "short",
124
+ title: "New Title",
125
+ });
126
+ expect(result).toContain("does not appear to be a valid note ID");
127
+ expect(mockApiClient.get).not.toHaveBeenCalled();
128
+ });
129
+ it("should require at least one update field", async () => {
130
+ const result = await editNote.call({
131
+ note_id: "a1b2c3d4e5f6789012345678901234567890abcd",
132
+ });
133
+ expect(result).toContain("Please provide at least one field to update");
134
+ expect(mockApiClient.get).not.toHaveBeenCalled();
135
+ });
136
+ it("should handle note not found", async () => {
137
+ mockApiClient.get.mockResolvedValue(null);
138
+ const result = await editNote.call({
139
+ note_id: "a1b2c3d4e5f6789012345678901234567890abcd",
140
+ title: "New Title",
141
+ });
142
+ expect(result).toContain("Note with ID");
143
+ expect(result).toContain("not found");
144
+ });
145
+ it("should validate parent_id format", async () => {
146
+ const result = await editNote.call({
147
+ note_id: "a1b2c3d4e5f6789012345678901234567890abcd",
148
+ parent_id: "short",
149
+ });
150
+ expect(result).toContain("does not appear to be a valid notebook ID");
151
+ expect(mockApiClient.get).not.toHaveBeenCalled();
152
+ });
153
+ });
154
+ describe("EditFolder", () => {
155
+ const mockCurrentFolder = {
156
+ id: "folder-123",
157
+ title: "Original Folder",
158
+ parent_id: null,
159
+ };
160
+ const mockUpdatedFolder = {
161
+ id: "folder-123",
162
+ title: "Updated Folder",
163
+ parent_id: null,
164
+ updated_time: 1234567891000,
165
+ };
166
+ it("should update folder title", async () => {
167
+ mockApiClient.get.mockResolvedValue(mockCurrentFolder);
168
+ mockApiClient.put.mockResolvedValue(mockUpdatedFolder);
169
+ const result = await editFolder.call({
170
+ folder_id: "folder-123",
171
+ title: "Updated Folder",
172
+ });
173
+ expect(mockApiClient.get).toHaveBeenCalledWith("/folders/folder-123", {
174
+ query: { fields: "id,title,parent_id" },
175
+ });
176
+ expect(mockApiClient.put).toHaveBeenCalledWith("/folders/folder-123", {
177
+ title: "Updated Folder",
178
+ });
179
+ expect(result).toContain("✅ Successfully updated notebook!");
180
+ expect(result).toContain('Title: "Original Folder" → "Updated Folder"');
181
+ });
182
+ it("should move folder to different parent", async () => {
183
+ const updatedFolder = { ...mockCurrentFolder, parent_id: "parent-456", updated_time: 1234567891000 };
184
+ const mockParent = { title: "Parent Folder" };
185
+ mockApiClient.get.mockResolvedValueOnce(mockCurrentFolder).mockResolvedValueOnce(mockParent);
186
+ mockApiClient.put.mockResolvedValue(updatedFolder);
187
+ const result = await editFolder.call({
188
+ folder_id: "folder-123",
189
+ parent_id: "parent-456",
190
+ });
191
+ expect(mockApiClient.put).toHaveBeenCalledWith("/folders/folder-123", {
192
+ parent_id: "parent-456",
193
+ });
194
+ expect(result).toContain('Location: Top level → Inside "Parent Folder"');
195
+ });
196
+ it("should validate required folder_id", async () => {
197
+ const result = await editFolder.call({});
198
+ expect(result).toContain("Please provide folder edit options");
199
+ expect(mockApiClient.get).not.toHaveBeenCalled();
200
+ });
201
+ it("should validate folder_id format", async () => {
202
+ const result = await editFolder.call({
203
+ folder_id: "short",
204
+ title: "New Title",
205
+ });
206
+ expect(result).toContain("does not appear to be a valid folder ID");
207
+ expect(mockApiClient.get).not.toHaveBeenCalled();
208
+ });
209
+ it("should require at least one update field", async () => {
210
+ const result = await editFolder.call({
211
+ folder_id: "a1b2c3d4e5f6789012345678901234567890abcd",
212
+ });
213
+ expect(result).toContain("Please provide at least one field to update");
214
+ expect(mockApiClient.get).not.toHaveBeenCalled();
215
+ });
216
+ it("should validate empty title", async () => {
217
+ const result = await editFolder.call({
218
+ folder_id: "a1b2c3d4e5f6789012345678901234567890abcd",
219
+ title: " ",
220
+ });
221
+ expect(result).toContain("Title must be a non-empty string");
222
+ expect(mockApiClient.get).not.toHaveBeenCalled();
223
+ });
224
+ it("should prevent self-parenting", async () => {
225
+ const result = await editFolder.call({
226
+ folder_id: "a1b2c3d4e5f6789012345678901234567890abcd",
227
+ parent_id: "a1b2c3d4e5f6789012345678901234567890abcd",
228
+ });
229
+ expect(result).toContain("A folder cannot be its own parent");
230
+ expect(mockApiClient.get).not.toHaveBeenCalled();
231
+ });
232
+ it("should handle folder not found", async () => {
233
+ mockApiClient.get.mockResolvedValue(null);
234
+ const result = await editFolder.call({
235
+ folder_id: "a1b2c3d4e5f6789012345678901234567890abcd",
236
+ title: "New Title",
237
+ });
238
+ expect(result).toContain("Folder with ID");
239
+ expect(result).toContain("not found");
240
+ });
241
+ it("should validate parent_id format", async () => {
242
+ const result = await editFolder.call({
243
+ folder_id: "a1b2c3d4e5f6789012345678901234567890abcd",
244
+ parent_id: "short",
245
+ });
246
+ expect(result).toContain("does not appear to be a valid parent notebook ID");
247
+ expect(mockApiClient.get).not.toHaveBeenCalled();
248
+ });
249
+ it("should handle 409 conflict errors", async () => {
250
+ const error = new Error("Conflict");
251
+ error.response = { status: 409 };
252
+ mockApiClient.get.mockResolvedValue(mockCurrentFolder);
253
+ mockApiClient.put.mockRejectedValue(error);
254
+ const result = await editFolder.call({
255
+ folder_id: "folder-123",
256
+ title: "Existing Name",
257
+ });
258
+ expect(result).toContain('folder with the title "Existing Name" might already exist');
259
+ });
260
+ });
261
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,154 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import JoplinAPIClient from "../../lib/joplin-api-client.js";
3
+ // Mock axios
4
+ vi.mock("axios", () => {
5
+ const mockAxios = {
6
+ get: vi.fn(),
7
+ post: vi.fn(),
8
+ put: vi.fn(),
9
+ delete: vi.fn(),
10
+ };
11
+ return { default: mockAxios };
12
+ });
13
+ import axios from "axios";
14
+ describe("JoplinAPIClient", () => {
15
+ let client;
16
+ beforeEach(() => {
17
+ vi.clearAllMocks();
18
+ client = new JoplinAPIClient({
19
+ port: 41184,
20
+ token: "test-token",
21
+ });
22
+ });
23
+ describe("constructor", () => {
24
+ it("should create client with default port", () => {
25
+ const defaultClient = new JoplinAPIClient({ token: "test-token" });
26
+ expect(defaultClient.baseURL).toBe("http://127.0.0.1:41184");
27
+ expect(defaultClient.token).toBe("test-token");
28
+ });
29
+ it("should create client with custom port", () => {
30
+ expect(client.baseURL).toBe("http://127.0.0.1:41184");
31
+ expect(client.token).toBe("test-token");
32
+ });
33
+ });
34
+ describe("serviceAvailable", () => {
35
+ it("should return true when service is available", async () => {
36
+ ;
37
+ axios.get.mockResolvedValue({
38
+ status: 200,
39
+ data: "JoplinClipperServer",
40
+ });
41
+ const result = await client.serviceAvailable();
42
+ expect(result).toBe(true);
43
+ expect(axios.get).toHaveBeenCalledWith("http://127.0.0.1:41184/ping");
44
+ });
45
+ it("should return false when service is not available", async () => {
46
+ ;
47
+ axios.get.mockRejectedValue(new Error("Connection failed"));
48
+ const result = await client.serviceAvailable();
49
+ expect(result).toBe(false);
50
+ });
51
+ it("should return false when response is incorrect", async () => {
52
+ ;
53
+ axios.get.mockResolvedValue({
54
+ status: 200,
55
+ data: "Wrong response",
56
+ });
57
+ const result = await client.serviceAvailable();
58
+ expect(result).toBe(false);
59
+ });
60
+ });
61
+ describe("get", () => {
62
+ it("should make GET request with token", async () => {
63
+ const mockData = { items: [], has_more: false };
64
+ axios.get.mockResolvedValue({ data: mockData });
65
+ const result = await client.get("/folders");
66
+ expect(result).toEqual(mockData);
67
+ expect(axios.get).toHaveBeenCalledWith("http://127.0.0.1:41184/folders", { params: { token: "test-token" } });
68
+ });
69
+ it("should make GET request with additional query params", async () => {
70
+ const mockData = { items: [], has_more: false };
71
+ axios.get.mockResolvedValue({ data: mockData });
72
+ const result = await client.get("/folders", {
73
+ query: { limit: 10, fields: "id,title" },
74
+ });
75
+ expect(result).toEqual(mockData);
76
+ expect(axios.get).toHaveBeenCalledWith("http://127.0.0.1:41184/folders", {
77
+ params: {
78
+ token: "test-token",
79
+ limit: 10,
80
+ fields: "id,title",
81
+ },
82
+ });
83
+ });
84
+ });
85
+ describe("post", () => {
86
+ it("should make POST request with token", async () => {
87
+ const mockData = { id: "123", title: "Test Note" };
88
+ const requestBody = { title: "Test Note", body: "Test content" };
89
+ axios.post.mockResolvedValue({ data: mockData });
90
+ const result = await client.post("/notes", requestBody);
91
+ expect(result).toEqual(mockData);
92
+ expect(axios.post).toHaveBeenCalledWith("http://127.0.0.1:41184/notes", requestBody, {
93
+ params: { token: "test-token" },
94
+ });
95
+ });
96
+ });
97
+ describe("getAllItems", () => {
98
+ it("should fetch all paginated items", async () => {
99
+ const page1 = {
100
+ items: [{ id: "1", title: "Item 1" }],
101
+ has_more: true,
102
+ };
103
+ const page2 = {
104
+ items: [{ id: "2", title: "Item 2" }],
105
+ has_more: false,
106
+ };
107
+ axios.get.mockResolvedValueOnce({ data: page1 }).mockResolvedValueOnce({ data: page2 });
108
+ const result = await client.getAllItems("/folders");
109
+ expect(result).toEqual([
110
+ { id: "1", title: "Item 1" },
111
+ { id: "2", title: "Item 2" },
112
+ ]);
113
+ expect(axios.get).toHaveBeenCalledTimes(2);
114
+ });
115
+ it("should throw error on invalid response format", async () => {
116
+ ;
117
+ axios.get.mockResolvedValue({ data: "invalid response" });
118
+ await expect(client.getAllItems("/folders")).rejects.toThrow("Unexpected response format from Joplin API for path: /folders");
119
+ });
120
+ });
121
+ describe("requestOptions", () => {
122
+ it("should merge options correctly", () => {
123
+ const options = client.requestOptions({
124
+ query: { limit: 10 },
125
+ });
126
+ expect(options).toEqual({
127
+ query: {
128
+ token: "test-token",
129
+ limit: 10,
130
+ },
131
+ });
132
+ });
133
+ it("should handle empty options", () => {
134
+ const options = client.requestOptions();
135
+ expect(options).toEqual({
136
+ query: {
137
+ token: "test-token",
138
+ },
139
+ });
140
+ });
141
+ });
142
+ describe("error handling", () => {
143
+ it("should handle GET errors", async () => {
144
+ const error = new Error("Network error");
145
+ axios.get.mockRejectedValue(error);
146
+ await expect(client.get("/folders")).rejects.toThrow("Network error");
147
+ });
148
+ it("should handle POST errors", async () => {
149
+ const error = new Error("Server error");
150
+ axios.post.mockRejectedValue(error);
151
+ await expect(client.post("/notes", {})).rejects.toThrow("Server error");
152
+ });
153
+ });
154
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("vite").UserConfig;
2
+ export default _default;
@@ -0,0 +1,22 @@
1
+ import { defineConfig } from "vitest/config";
2
+ export default defineConfig({
3
+ test: {
4
+ // Test files pattern
5
+ include: ["tests/**/*.{test,spec}.{ts,tsx}"],
6
+ exclude: ["node_modules", "dist", ".idea", ".git", ".cache", "tests/manual/**/*"],
7
+ // Test environment
8
+ environment: "node",
9
+ // Globals
10
+ globals: true,
11
+ // Test timeout
12
+ testTimeout: 10000,
13
+ // Coverage settings
14
+ coverage: {
15
+ provider: "v8",
16
+ reporter: ["text", "json", "html"],
17
+ exclude: ["node_modules/", "bin/", "logs/", "**/*.config.{js,ts}", "**/*.test.{js,ts}", "**/*.spec.{js,ts}"],
18
+ },
19
+ // Setup files
20
+ setupFiles: ["./vitest.setup.ts"],
21
+ },
22
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import { beforeAll, afterAll } from "vitest";
2
+ import dotenv from "dotenv";
3
+ import { resolve } from "path";
4
+ // Load test environment variables
5
+ beforeAll(() => {
6
+ // Try to load .env.test.local, fallback to .env.test, then .env
7
+ const envFiles = [".env.test.local", ".env.test", ".env"];
8
+ for (const envFile of envFiles) {
9
+ try {
10
+ dotenv.config({ path: resolve(process.cwd(), envFile) });
11
+ break;
12
+ }
13
+ catch (error) {
14
+ // Continue to next file if current one doesn't exist
15
+ }
16
+ }
17
+ // Set test defaults if not provided
18
+ if (!process.env.JOPLIN_PORT) {
19
+ process.env.JOPLIN_PORT = "41184";
20
+ }
21
+ });
22
+ afterAll(() => {
23
+ // Cleanup if needed
24
+ });
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "joplin-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Joplin",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "joplin-mcp-server": "./dist/bin/cli.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "clean": "rm -rf dist",
13
+ "prepublishOnly": "npm run clean && npm run build",
14
+ "start": "tsx index.ts",
15
+ "start:js": "node dist/index.js",
16
+ "test": "vitest",
17
+ "test:ui": "vitest --ui",
18
+ "test:run": "vitest run",
19
+ "test:coverage": "vitest --coverage",
20
+ "test:manual:search": "tsx tests/manual/search-notes.test.ts",
21
+ "test:manual:read-note": "tsx tests/manual/read-note.test.ts",
22
+ "test:manual:create-note": "tsx tests/manual/create-note.test.ts",
23
+ "test:manual:create-folder": "tsx tests/manual/create-folder.test.ts",
24
+ "test:manual:edit-note": "tsx tests/manual/edit-note.test.ts",
25
+ "test:manual:edit-folder": "tsx tests/manual/edit-folder.test.ts",
26
+ "test:manual:delete-note": "tsx tests/manual/delete-note.test.ts",
27
+ "test:manual:delete-folder": "tsx tests/manual/delete-folder.test.ts",
28
+ "test:manual:list-notebooks": "tsx tests/manual/list-notebooks.test.ts",
29
+ "typecheck": "tsc --noEmit",
30
+ "format": "prettier --write .",
31
+ "format:check": "prettier --check ."
32
+ },
33
+ "keywords": [
34
+ "mcp",
35
+ "joplin"
36
+ ],
37
+ "files": [
38
+ "dist/**/*",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "author": "Jordan Burke <jordan.burke@gmail.com>",
43
+ "license": "MIT",
44
+ "dependencies": {
45
+ "@modelcontextprotocol/sdk": "^1.8.0",
46
+ "axios": "^1.6.7",
47
+ "dotenv": "^16.4.5",
48
+ "zod": "^3.23.8"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^22.15.21",
52
+ "@vitest/ui": "^3.1.4",
53
+ "prettier": "^3.5.3",
54
+ "tsx": "^4.19.4",
55
+ "typescript": "^5.8.3",
56
+ "vitest": "^3.1.4"
57
+ }
58
+ }