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,108 @@
1
+ import BaseTool from "./base-tool.js";
2
+ class ReadMultiNote extends BaseTool {
3
+ async call(noteIds) {
4
+ if (!noteIds || !Array.isArray(noteIds) || noteIds.length === 0) {
5
+ return 'Please provide an array of note IDs. Example: read_multinote note_ids=["id1", "id2", "id3"]';
6
+ }
7
+ // Validate that all IDs look like valid note IDs
8
+ const invalidIds = noteIds.filter((id) => !id || id.length < 10 || !id.match(/[a-f0-9]/i));
9
+ if (invalidIds.length > 0) {
10
+ return `Error: Some IDs do not appear to be valid note IDs: ${invalidIds.join(", ")}\n\nNote IDs are long alphanumeric strings like "58a0a29f68bc4141b49c99f5d367638a".\n\nUse search_notes to find notes and their IDs.`;
11
+ }
12
+ const resultLines = [];
13
+ const notFound = [];
14
+ const errors = [];
15
+ const successful = [];
16
+ // Add a header
17
+ resultLines.push(`# Reading ${noteIds.length} notes\n`);
18
+ // Process each note ID
19
+ for (let i = 0; i < noteIds.length; i++) {
20
+ const noteId = noteIds[i];
21
+ resultLines.push(`## Note ${i + 1} of ${noteIds.length} (ID: ${noteId})\n`);
22
+ try {
23
+ // Get the note details with all relevant fields
24
+ const note = await this.apiClient.get(`/notes/${noteId}`, {
25
+ query: {
26
+ fields: "id,title,body,parent_id,created_time,updated_time,is_todo,todo_completed,todo_due",
27
+ },
28
+ });
29
+ // Validate note response
30
+ if (!note || typeof note !== "object" || !note.id) {
31
+ errors.push(noteId);
32
+ resultLines.push(`Error: Unexpected response format from Joplin API when fetching note ${noteId}\n`);
33
+ continue;
34
+ }
35
+ successful.push(noteId);
36
+ // Get the notebook info to show where this note is located
37
+ let notebookInfo = "Unknown notebook";
38
+ if (note.parent_id) {
39
+ try {
40
+ const notebook = await this.apiClient.get(`/folders/${note.parent_id}`, {
41
+ query: { fields: "id,title" },
42
+ });
43
+ if (notebook && notebook.title) {
44
+ notebookInfo = `"${notebook.title}" (notebook_id: "${note.parent_id}")`;
45
+ }
46
+ }
47
+ catch (error) {
48
+ process.stderr.write(`Error fetching notebook info for note ${noteId}: ${error}\n`);
49
+ // Continue even if we can't get the notebook info
50
+ }
51
+ }
52
+ // Add note metadata
53
+ resultLines.push(`### Note: "${note.title}"`);
54
+ resultLines.push(`Notebook: ${notebookInfo}`);
55
+ // Add todo status if applicable
56
+ if (note.is_todo) {
57
+ const status = note.todo_completed ? "Completed" : "Not completed";
58
+ resultLines.push(`Status: ${status}`);
59
+ if (note.todo_due) {
60
+ const dueDate = this.formatDate(note.todo_due);
61
+ resultLines.push(`Due: ${dueDate}`);
62
+ }
63
+ }
64
+ // Add timestamps
65
+ const createdDate = this.formatDate(note.created_time);
66
+ const updatedDate = this.formatDate(note.updated_time);
67
+ resultLines.push(`Created: ${createdDate}`);
68
+ resultLines.push(`Updated: ${updatedDate}`);
69
+ // Add a separator before the note content
70
+ resultLines.push("\n---\n");
71
+ // Add the note body
72
+ if (note.body) {
73
+ resultLines.push(note.body);
74
+ }
75
+ else {
76
+ resultLines.push("(This note has no content)");
77
+ }
78
+ // Add a separator after the note
79
+ resultLines.push("\n---\n");
80
+ }
81
+ catch (error) {
82
+ process.stderr.write(`Error reading note ${noteId}: ${error}\n`);
83
+ if (error.response && error.response.status === 404) {
84
+ notFound.push(noteId);
85
+ resultLines.push(`Note with ID "${noteId}" not found.\n`);
86
+ }
87
+ else {
88
+ errors.push(noteId);
89
+ resultLines.push(`Error reading note: ${error.message || "Unknown error"}\n`);
90
+ }
91
+ }
92
+ }
93
+ // Add a summary at the end
94
+ resultLines.push("# Summary");
95
+ resultLines.push(`Total notes requested: ${noteIds.length}`);
96
+ resultLines.push(`Successfully retrieved: ${successful.length}`);
97
+ if (notFound.length > 0) {
98
+ resultLines.push(`Notes not found: ${notFound.length}`);
99
+ resultLines.push(`IDs not found: ${notFound.join(", ")}`);
100
+ }
101
+ if (errors.length > 0) {
102
+ resultLines.push(`Errors encountered: ${errors.length}`);
103
+ resultLines.push(`IDs with errors: ${errors.join(", ")}`);
104
+ }
105
+ return resultLines.join("\n");
106
+ }
107
+ }
108
+ export default ReadMultiNote;
@@ -0,0 +1,5 @@
1
+ import BaseTool from "./base-tool.js";
2
+ declare class ReadNote extends BaseTool {
3
+ call(noteId: string): Promise<string>;
4
+ }
5
+ export default ReadNote;
@@ -0,0 +1,80 @@
1
+ import BaseTool from "./base-tool.js";
2
+ class ReadNote extends BaseTool {
3
+ async call(noteId) {
4
+ const validationError = this.validateId(noteId, "note");
5
+ if (validationError) {
6
+ return validationError;
7
+ }
8
+ try {
9
+ // Get the note details with all relevant fields
10
+ const note = await this.apiClient.get(`/notes/${noteId}`, {
11
+ query: {
12
+ fields: "id,title,body,parent_id,created_time,updated_time,is_todo,todo_completed,todo_due",
13
+ },
14
+ });
15
+ // Validate note response
16
+ if (!note || typeof note !== "object" || !note.id) {
17
+ return `Error: Unexpected response format from Joplin API when fetching note`;
18
+ }
19
+ // Get the notebook info to show where this note is located
20
+ let notebookInfo = "Unknown notebook";
21
+ if (note.parent_id) {
22
+ try {
23
+ const notebook = await this.apiClient.get(`/folders/${note.parent_id}`, {
24
+ query: { fields: "id,title" },
25
+ });
26
+ if (notebook && notebook.title) {
27
+ notebookInfo = `"${notebook.title}" (notebook_id: "${note.parent_id}")`;
28
+ }
29
+ }
30
+ catch (error) {
31
+ process.stderr.write(`Error fetching notebook info: ${error}\n`);
32
+ // Continue even if we can't get the notebook info
33
+ }
34
+ }
35
+ // Format the note content
36
+ const resultLines = [];
37
+ // Add note header with metadata
38
+ resultLines.push(`# Note: "${note.title}"`);
39
+ resultLines.push(`Note ID: ${note.id}`);
40
+ resultLines.push(`Notebook: ${notebookInfo}`);
41
+ // Add todo status if applicable
42
+ if (note.is_todo) {
43
+ const status = note.todo_completed ? "Completed" : "Not completed";
44
+ resultLines.push(`Status: ${status}`);
45
+ if (note.todo_due) {
46
+ const dueDate = this.formatDate(note.todo_due);
47
+ resultLines.push(`Due: ${dueDate}`);
48
+ }
49
+ }
50
+ // Add timestamps
51
+ const createdDate = this.formatDate(note.created_time);
52
+ const updatedDate = this.formatDate(note.updated_time);
53
+ resultLines.push(`Created: ${createdDate}`);
54
+ resultLines.push(`Updated: ${updatedDate}`);
55
+ // Add a separator before the note content
56
+ resultLines.push("\n---\n");
57
+ // Add the note body
58
+ if (note.body) {
59
+ resultLines.push(note.body);
60
+ }
61
+ else {
62
+ resultLines.push("(This note has no content)");
63
+ }
64
+ // Add a footer with helpful commands
65
+ resultLines.push("\n---\n");
66
+ resultLines.push("Related commands:");
67
+ resultLines.push(`- To view the notebook containing this note: read_notebook notebook_id="${note.parent_id}"`);
68
+ resultLines.push('- To search for more notes: search_notes query="your search term"');
69
+ return resultLines.join("\n");
70
+ }
71
+ catch (error) {
72
+ if (error.response && error.response.status === 404) {
73
+ return `Note with ID "${noteId}" not found.\n\nThis might happen if:\n1. The ID is incorrect\n2. You're using a notebook ID instead of a note ID\n3. The note has been deleted\n\nUse search_notes to find notes and their IDs.`;
74
+ }
75
+ return (this.formatError(error, "reading note") +
76
+ `\n\nMake sure you're using a valid note ID.\nUse search_notes to find notes and their IDs.`);
77
+ }
78
+ }
79
+ }
80
+ export default ReadNote;
@@ -0,0 +1,5 @@
1
+ import BaseTool from "./base-tool.js";
2
+ declare class ReadNotebook extends BaseTool {
3
+ call(notebookId: string): Promise<string>;
4
+ }
5
+ export default ReadNotebook;
@@ -0,0 +1,66 @@
1
+ import BaseTool from "./base-tool.js";
2
+ class ReadNotebook extends BaseTool {
3
+ async call(notebookId) {
4
+ const validationError = this.validateId(notebookId, "notebook");
5
+ if (validationError) {
6
+ return validationError;
7
+ }
8
+ try {
9
+ // First, get the notebook details
10
+ const notebook = await this.apiClient.get(`/folders/${notebookId}`, {
11
+ query: { fields: "id,title,parent_id" },
12
+ });
13
+ // Validate notebook response
14
+ if (!notebook || typeof notebook !== "object" || !notebook.id) {
15
+ return `Error: Unexpected response format from Joplin API when fetching notebook`;
16
+ }
17
+ // Get all notes in this notebook
18
+ const notes = await this.apiClient.get(`/folders/${notebookId}/notes`, {
19
+ query: { fields: "id,title,updated_time,is_todo,todo_completed" },
20
+ });
21
+ // Validate notes response
22
+ if (!notes || typeof notes !== "object") {
23
+ return `Error: Unexpected response format from Joplin API when fetching notes`;
24
+ }
25
+ if (!notes.items || !Array.isArray(notes.items) || notes.items.length === 0) {
26
+ return `Notebook "${notebook.title}" (notebook_id: "${notebook.id}") is empty.\n\nTry another notebook ID or use list_notebooks to see all available notebooks.`;
27
+ }
28
+ // Format the notebook contents
29
+ const resultLines = [];
30
+ resultLines.push(`# Notebook: "${notebook.title}" (notebook_id: "${notebook.id}")`);
31
+ resultLines.push(`Contains ${notes.items.length} notes:\n`);
32
+ resultLines.push(`NOTE: This is showing the contents of notebook "${notebook.title}", not a specific note.\n`);
33
+ // If multiple notes were found, add a hint about read_multinote
34
+ if (notes.items.length > 1) {
35
+ const noteIds = notes.items.map((note) => note.id);
36
+ resultLines.push(`TIP: To read all ${notes.items.length} notes at once, use:\n`);
37
+ resultLines.push(`read_multinote note_ids=${JSON.stringify(noteIds)}\n`);
38
+ }
39
+ // Sort notes by updated_time (newest first)
40
+ const sortedNotes = [...notes.items].sort((a, b) => b.updated_time - a.updated_time);
41
+ sortedNotes.forEach((note) => {
42
+ const updatedDate = this.formatDate(note.updated_time);
43
+ // Add checkbox for todos
44
+ if (note.is_todo) {
45
+ const checkboxStatus = note.todo_completed ? "āœ…" : "☐";
46
+ resultLines.push(`- ${checkboxStatus} Note: "${note.title}" (note_id: "${note.id}")`);
47
+ }
48
+ else {
49
+ resultLines.push(`- Note: "${note.title}" (note_id: "${note.id}")`);
50
+ }
51
+ resultLines.push(` Updated: ${updatedDate}`);
52
+ resultLines.push(` To read this note: read_note note_id="${note.id}"`);
53
+ resultLines.push(""); // Empty line between notes
54
+ });
55
+ return resultLines.join("\n");
56
+ }
57
+ catch (error) {
58
+ if (error.response && error.response.status === 404) {
59
+ return `Notebook with ID "${notebookId}" not found.\n\nThis might happen if:\n1. The ID is incorrect\n2. You're using a note title instead of a notebook ID\n3. The notebook has been deleted\n\nUse list_notebooks to see all available notebooks with their IDs.`;
60
+ }
61
+ return (this.formatError(error, "reading notebook") +
62
+ `\n\nMake sure you're using a valid notebook ID, not a note title.\nUse list_notebooks to see all available notebooks with their IDs.`);
63
+ }
64
+ }
65
+ }
66
+ export default ReadNotebook;
@@ -0,0 +1,5 @@
1
+ import BaseTool from "./base-tool.js";
2
+ declare class SearchNotes extends BaseTool {
3
+ call(query: string): Promise<string>;
4
+ }
5
+ export default SearchNotes;
@@ -0,0 +1,68 @@
1
+ import BaseTool from "./base-tool.js";
2
+ class SearchNotes extends BaseTool {
3
+ async call(query) {
4
+ if (!query) {
5
+ return "Please provide a search query.";
6
+ }
7
+ try {
8
+ // Search for notes with the given query
9
+ const searchResults = await this.apiClient.get("/search", {
10
+ query: {
11
+ query,
12
+ fields: "id,title,body,parent_id,updated_time",
13
+ },
14
+ });
15
+ // Handle case where the API doesn't return the expected structure
16
+ if (!searchResults || typeof searchResults !== "object") {
17
+ return `Error: Unexpected response format from Joplin API`;
18
+ }
19
+ // Handle case where no items were found
20
+ if (!searchResults.items || !Array.isArray(searchResults.items) || searchResults.items.length === 0) {
21
+ return `No notes found matching query: "${query}"`;
22
+ }
23
+ // Get all folders to be able to show notebook names
24
+ const folders = await this.apiClient.getAllItems("/folders", {
25
+ query: {
26
+ fields: "id,title",
27
+ },
28
+ });
29
+ // Create a map of folder IDs to folder titles for quick lookup
30
+ const folderMap = {};
31
+ folders.forEach((folder) => {
32
+ folderMap[folder.id] = folder.title;
33
+ });
34
+ // Format the search results
35
+ const resultLines = [];
36
+ resultLines.push(`Found ${searchResults.items.length} notes matching query: "${query}"\n`);
37
+ resultLines.push(`NOTE: To read a notebook, use the notebook ID (not the note title)\n`);
38
+ // If multiple notes were found, add a hint about read_multinote
39
+ if (searchResults.items.length > 1) {
40
+ const noteIds = searchResults.items.map((note) => note.id);
41
+ resultLines.push(`TIP: To read all ${searchResults.items.length} notes at once, use:\n`);
42
+ resultLines.push(`read_multinote note_ids=${JSON.stringify(noteIds)}\n`);
43
+ }
44
+ searchResults.items.forEach((note) => {
45
+ const notebookTitle = folderMap[note.parent_id || ""] || "Unknown notebook";
46
+ const notebookId = note.parent_id || "unknown";
47
+ const updatedDate = this.formatDate(note.updated_time);
48
+ resultLines.push(`- Note: "${note.title}" (note_id: "${note.id}")`);
49
+ resultLines.push(` Notebook: "${notebookTitle}" (notebook_id: "${notebookId}")`);
50
+ resultLines.push(` Updated: ${updatedDate}`);
51
+ // Add a snippet of the note body if available
52
+ if (note.body) {
53
+ const snippet = note.body.substring(0, 100).replace(/\n/g, " ") + (note.body.length > 100 ? "..." : "");
54
+ resultLines.push(` Snippet: ${snippet}`);
55
+ }
56
+ // Add hints for using related commands
57
+ resultLines.push(` To read this note: read_note note_id="${note.id}"`);
58
+ resultLines.push(` To read this notebook: read_notebook notebook_id="${notebookId}"`);
59
+ resultLines.push(""); // Empty line between notes
60
+ });
61
+ return resultLines.join("\n");
62
+ }
63
+ catch (error) {
64
+ return this.formatError(error, "searching notes");
65
+ }
66
+ }
67
+ }
68
+ export default SearchNotes;
@@ -0,0 +1,117 @@
1
+ import { describe, it, expect, beforeAll } from "vitest";
2
+ import JoplinAPIClient from "../../lib/joplin-api-client.js";
3
+ import { ListNotebooks, SearchNotes, ReadNotebook } from "../../lib/tools/index.js";
4
+ describe("Joplin Integration Tests", () => {
5
+ let client;
6
+ let skipTests = false;
7
+ beforeAll(async () => {
8
+ // Check if we have the required environment variables for integration tests
9
+ if (!process.env.JOPLIN_TOKEN || !process.env.JOPLIN_PORT) {
10
+ console.warn("Skipping integration tests: JOPLIN_TOKEN or JOPLIN_PORT not set");
11
+ skipTests = true;
12
+ return;
13
+ }
14
+ client = new JoplinAPIClient({
15
+ port: parseInt(process.env.JOPLIN_PORT),
16
+ token: process.env.JOPLIN_TOKEN,
17
+ });
18
+ // Check if Joplin is actually running and accessible
19
+ try {
20
+ const isAvailable = await client.serviceAvailable();
21
+ if (!isAvailable) {
22
+ console.warn("Skipping integration tests: Joplin service not available");
23
+ skipTests = true;
24
+ }
25
+ }
26
+ catch (error) {
27
+ console.warn("Skipping integration tests: Cannot connect to Joplin");
28
+ skipTests = true;
29
+ }
30
+ });
31
+ describe("Joplin API Client Integration", () => {
32
+ it("should connect to Joplin service", async () => {
33
+ if (skipTests)
34
+ return;
35
+ const isAvailable = await client.serviceAvailable();
36
+ expect(isAvailable).toBe(true);
37
+ });
38
+ it("should fetch folders from Joplin", async () => {
39
+ if (skipTests)
40
+ return;
41
+ const folders = await client.get("/folders", {
42
+ query: { limit: 5, fields: "id,title,parent_id" },
43
+ });
44
+ expect(folders).toHaveProperty("items");
45
+ expect(Array.isArray(folders.items)).toBe(true);
46
+ expect(folders).toHaveProperty("has_more");
47
+ });
48
+ it("should fetch notes from Joplin", async () => {
49
+ if (skipTests)
50
+ return;
51
+ const notes = await client.get("/notes", {
52
+ query: { limit: 5, fields: "id,title,parent_id" },
53
+ });
54
+ expect(notes).toHaveProperty("items");
55
+ expect(Array.isArray(notes.items)).toBe(true);
56
+ });
57
+ });
58
+ describe("Tools Integration", () => {
59
+ it("should list notebooks using ListNotebooks tool", async () => {
60
+ if (skipTests)
61
+ return;
62
+ const tool = new ListNotebooks(client);
63
+ const result = await tool.call();
64
+ expect(typeof result).toBe("string");
65
+ expect(result).toContain("Joplin Notebooks:");
66
+ expect(result).toContain("notebook_id:");
67
+ });
68
+ it("should search notes using SearchNotes tool", async () => {
69
+ if (skipTests)
70
+ return;
71
+ // First, let's check if there are any notes to search
72
+ const notes = await client.get("/notes", { query: { limit: 1 } });
73
+ if (notes.items.length === 0) {
74
+ console.warn("Skipping search test: No notes found in Joplin");
75
+ return;
76
+ }
77
+ const tool = new SearchNotes(client);
78
+ const result = await tool.call("test");
79
+ expect(typeof result).toBe("string");
80
+ // The result should either contain matching notes or indicate no matches
81
+ expect(result).toMatch(/(Found \d+ notes|No notes found)/);
82
+ });
83
+ it("should read a notebook using ReadNotebook tool", async () => {
84
+ if (skipTests)
85
+ return;
86
+ // First get a notebook ID
87
+ const folders = await client.get("/folders", {
88
+ query: { limit: 1, fields: "id,title" },
89
+ });
90
+ if (folders.items.length === 0) {
91
+ console.warn("Skipping read notebook test: No notebooks found");
92
+ return;
93
+ }
94
+ const notebookId = folders.items[0].id;
95
+ const tool = new ReadNotebook(client);
96
+ const result = await tool.call(notebookId);
97
+ expect(typeof result).toBe("string");
98
+ expect(result).toContain("Notebook:");
99
+ expect(result).toContain(notebookId);
100
+ });
101
+ });
102
+ describe("Error Handling", () => {
103
+ it("should handle invalid API calls gracefully", async () => {
104
+ if (skipTests)
105
+ return;
106
+ await expect(client.get("/invalid-endpoint")).rejects.toThrow();
107
+ });
108
+ it("should handle invalid notebook ID in ReadNotebook tool", async () => {
109
+ if (skipTests)
110
+ return;
111
+ const tool = new ReadNotebook(client);
112
+ const result = await tool.call("invalid-notebook-id");
113
+ expect(typeof result).toBe("string");
114
+ expect(result).toContain("Error:");
115
+ });
116
+ });
117
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,81 @@
1
+ import dotenv from "dotenv";
2
+ import JoplinAPIClient from "../../lib/joplin-api-client.js";
3
+ import { CreateFolder, ListNotebooks } from "../../lib/tools/index.js";
4
+ // Load environment variables
5
+ dotenv.config();
6
+ // Check for required environment variables
7
+ const requiredEnvVars = ["JOPLIN_PORT", "JOPLIN_TOKEN"];
8
+ for (const envVar of requiredEnvVars) {
9
+ if (!process.env[envVar]) {
10
+ console.error(`Error: ${envVar} environment variable is required`);
11
+ process.exit(1);
12
+ }
13
+ }
14
+ // Create the Joplin API client
15
+ const apiClient = new JoplinAPIClient({
16
+ port: parseInt(process.env.JOPLIN_PORT),
17
+ token: process.env.JOPLIN_TOKEN,
18
+ });
19
+ // Create the tools
20
+ const createFolder = new CreateFolder(apiClient);
21
+ const listNotebooks = new ListNotebooks(apiClient);
22
+ // Test the create folder functionality
23
+ async function testCreateFolder() {
24
+ try {
25
+ // Check if Joplin is available
26
+ const available = await apiClient.serviceAvailable();
27
+ if (!available) {
28
+ console.error("Error: Joplin service is not available");
29
+ process.exit(1);
30
+ }
31
+ // Parse command line arguments
32
+ const args = process.argv.slice(2);
33
+ const title = args[0] || `Test Notebook ${new Date().toISOString()}`;
34
+ const parentId = args[1]; // Optional parent notebook ID
35
+ console.log("Creating notebook with:");
36
+ console.log(` Title: "${title}"`);
37
+ if (parentId) {
38
+ console.log(` Parent Notebook ID: "${parentId}"`);
39
+ }
40
+ else {
41
+ console.log(" Location: Top level (no parent specified)");
42
+ }
43
+ console.log("");
44
+ // Create the folder
45
+ const createOptions = { title };
46
+ if (parentId) {
47
+ createOptions.parent_id = parentId;
48
+ }
49
+ const result = await createFolder.call(createOptions);
50
+ console.log(result);
51
+ console.log("\nšŸ“‹ Updated notebook hierarchy:");
52
+ const notebooks = await listNotebooks.call();
53
+ console.log(notebooks);
54
+ }
55
+ catch (error) {
56
+ console.error("Error testing create folder:", error);
57
+ }
58
+ }
59
+ // Show usage if help requested
60
+ if (process.argv.includes("--help") || process.argv.includes("-h")) {
61
+ console.log(`
62
+ Usage: tsx tests/manual/create-folder.test.ts [title] [parent_id]
63
+
64
+ Arguments:
65
+ title - Notebook title (optional, defaults to timestamped title)
66
+ parent_id - ID of parent notebook to create subfolder in (optional, creates at top level if omitted)
67
+
68
+ Examples:
69
+ tsx tests/manual/create-folder.test.ts
70
+ tsx tests/manual/create-folder.test.ts "My Notebook"
71
+ tsx tests/manual/create-folder.test.ts "My Subfolder" "a1b2c3d4e5f6..."
72
+
73
+ First run list_notebooks to see available notebook IDs:
74
+ npm run test:manual:list-notebooks
75
+ `);
76
+ process.exit(0);
77
+ }
78
+ // Run if called directly
79
+ if (import.meta.url === `file://${process.argv[1]}`) {
80
+ testCreateFolder();
81
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,84 @@
1
+ import dotenv from "dotenv";
2
+ import JoplinAPIClient from "../../lib/joplin-api-client.js";
3
+ import { CreateNote, ListNotebooks } from "../../lib/tools/index.js";
4
+ // Load environment variables
5
+ dotenv.config();
6
+ // Check for required environment variables
7
+ const requiredEnvVars = ["JOPLIN_PORT", "JOPLIN_TOKEN"];
8
+ for (const envVar of requiredEnvVars) {
9
+ if (!process.env[envVar]) {
10
+ console.error(`Error: ${envVar} environment variable is required`);
11
+ process.exit(1);
12
+ }
13
+ }
14
+ // Create the Joplin API client
15
+ const apiClient = new JoplinAPIClient({
16
+ port: parseInt(process.env.JOPLIN_PORT),
17
+ token: process.env.JOPLIN_TOKEN,
18
+ });
19
+ // Create the tools
20
+ const createNote = new CreateNote(apiClient);
21
+ const listNotebooks = new ListNotebooks(apiClient);
22
+ // Test the create note functionality
23
+ async function testCreateNote() {
24
+ try {
25
+ // Check if Joplin is available
26
+ const available = await apiClient.serviceAvailable();
27
+ if (!available) {
28
+ console.error("Error: Joplin service is not available");
29
+ process.exit(1);
30
+ }
31
+ // Parse command line arguments
32
+ const args = process.argv.slice(2);
33
+ const title = args[0] || `Test Note ${new Date().toISOString()}`;
34
+ const body = args[1] || `This is a test note created at ${new Date().toLocaleString()}`;
35
+ const notebookId = args[2]; // Optional notebook ID
36
+ console.log("Creating note with:");
37
+ console.log(` Title: "${title}"`);
38
+ console.log(` Body: "${body}"`);
39
+ if (notebookId) {
40
+ console.log(` Notebook ID: "${notebookId}"`);
41
+ }
42
+ else {
43
+ console.log(" Location: Root level (no notebook specified)");
44
+ }
45
+ console.log("");
46
+ // Create the note
47
+ const createOptions = { title, body };
48
+ if (notebookId) {
49
+ createOptions.parent_id = notebookId;
50
+ }
51
+ const result = await createNote.call(createOptions);
52
+ console.log(result);
53
+ console.log("\nšŸ“‹ Available notebooks:");
54
+ const notebooks = await listNotebooks.call();
55
+ console.log(notebooks);
56
+ }
57
+ catch (error) {
58
+ console.error("Error testing create note:", error);
59
+ }
60
+ }
61
+ // Show usage if help requested
62
+ if (process.argv.includes("--help") || process.argv.includes("-h")) {
63
+ console.log(`
64
+ Usage: tsx tests/manual/create-note.test.ts [title] [body] [notebook_id]
65
+
66
+ Arguments:
67
+ title - Note title (optional, defaults to timestamped title)
68
+ body - Note body content (optional, defaults to timestamped content)
69
+ notebook_id - ID of notebook to create note in (optional, creates in root if omitted)
70
+
71
+ Examples:
72
+ tsx tests/manual/create-note.test.ts
73
+ tsx tests/manual/create-note.test.ts "My Note" "My note content"
74
+ tsx tests/manual/create-note.test.ts "My Note" "My note content" "a1b2c3d4e5f6..."
75
+
76
+ First run list_notebooks to see available notebook IDs:
77
+ npm run test:manual:list-notebooks
78
+ `);
79
+ process.exit(0);
80
+ }
81
+ // Run if called directly
82
+ if (import.meta.url === `file://${process.argv[1]}`) {
83
+ testCreateNote();
84
+ }
@@ -0,0 +1 @@
1
+ export {};