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.
- package/LICENSE +21 -0
- package/README.md +384 -0
- package/dist/bin/cli.d.ts +2 -0
- package/dist/bin/cli.js +7 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +204 -0
- package/dist/lib/joplin-api-client.d.ts +23 -0
- package/dist/lib/joplin-api-client.js +110 -0
- package/dist/lib/logger.d.ts +21 -0
- package/dist/lib/logger.js +68 -0
- package/dist/lib/parse-args.d.ts +2 -0
- package/dist/lib/parse-args.js +81 -0
- package/dist/lib/tools/base-tool.d.ts +27 -0
- package/dist/lib/tools/base-tool.js +24 -0
- package/dist/lib/tools/create-folder.d.ts +9 -0
- package/dist/lib/tools/create-folder.js +79 -0
- package/dist/lib/tools/create-note.d.ts +13 -0
- package/dist/lib/tools/create-note.js +88 -0
- package/dist/lib/tools/delete-folder.d.ts +10 -0
- package/dist/lib/tools/delete-folder.js +138 -0
- package/dist/lib/tools/delete-note.d.ts +9 -0
- package/dist/lib/tools/delete-note.js +92 -0
- package/dist/lib/tools/edit-folder.d.ts +10 -0
- package/dist/lib/tools/edit-folder.js +136 -0
- package/dist/lib/tools/edit-note.d.ts +15 -0
- package/dist/lib/tools/edit-note.js +153 -0
- package/dist/lib/tools/index.d.ts +12 -0
- package/dist/lib/tools/index.js +12 -0
- package/dist/lib/tools/list-notebooks.d.ts +7 -0
- package/dist/lib/tools/list-notebooks.js +59 -0
- package/dist/lib/tools/read-multi-note.d.ts +5 -0
- package/dist/lib/tools/read-multi-note.js +108 -0
- package/dist/lib/tools/read-note.d.ts +5 -0
- package/dist/lib/tools/read-note.js +80 -0
- package/dist/lib/tools/read-notebook.d.ts +5 -0
- package/dist/lib/tools/read-notebook.js +66 -0
- package/dist/lib/tools/search-notes.d.ts +5 -0
- package/dist/lib/tools/search-notes.js +68 -0
- package/dist/tests/integration/joplin-integration.test.d.ts +1 -0
- package/dist/tests/integration/joplin-integration.test.js +117 -0
- package/dist/tests/manual/create-folder.test.d.ts +1 -0
- package/dist/tests/manual/create-folder.test.js +81 -0
- package/dist/tests/manual/create-note.test.d.ts +1 -0
- package/dist/tests/manual/create-note.test.js +84 -0
- package/dist/tests/manual/delete-folder.test.d.ts +1 -0
- package/dist/tests/manual/delete-folder.test.js +118 -0
- package/dist/tests/manual/delete-note.test.d.ts +1 -0
- package/dist/tests/manual/delete-note.test.js +101 -0
- package/dist/tests/manual/edit-folder.test.d.ts +1 -0
- package/dist/tests/manual/edit-folder.test.js +104 -0
- package/dist/tests/manual/edit-note.test.d.ts +1 -0
- package/dist/tests/manual/edit-note.test.js +118 -0
- package/dist/tests/manual/list-notebooks.test.d.ts +1 -0
- package/dist/tests/manual/list-notebooks.test.js +42 -0
- package/dist/tests/manual/read-note.test.d.ts +1 -0
- package/dist/tests/manual/read-note.test.js +54 -0
- package/dist/tests/manual/search-notes.test.d.ts +1 -0
- package/dist/tests/manual/search-notes.test.js +43 -0
- package/dist/tests/unit/create-tools.test.d.ts +1 -0
- package/dist/tests/unit/create-tools.test.js +223 -0
- package/dist/tests/unit/delete-tools.test.d.ts +1 -0
- package/dist/tests/unit/delete-tools.test.js +225 -0
- package/dist/tests/unit/edit-tools.test.d.ts +1 -0
- package/dist/tests/unit/edit-tools.test.js +261 -0
- package/dist/tests/unit/joplin-api-client.test.d.ts +1 -0
- package/dist/tests/unit/joplin-api-client.test.js +154 -0
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +22 -0
- package/dist/vitest.setup.d.ts +1 -0
- package/dist/vitest.setup.js +24 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jordan Burke
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# Joplin MCP Server
|
|
2
|
+
|
|
3
|
+
This is a Node.js implementation of an MCP (Model Context Protocol) server for Joplin.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
Create a `.env` file with the following variables:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
JOPLIN_PORT=41184
|
|
17
|
+
JOPLIN_TOKEN=your_joplin_token
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
You can find your Joplin token in the Joplin desktop app under:
|
|
21
|
+
Tools > Options > Web Clipper
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### Local Development
|
|
26
|
+
|
|
27
|
+
Start the server:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm start
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
You can also specify a custom environment file:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm start -- --env-file .env.custom
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Using npx (Recommended)
|
|
40
|
+
|
|
41
|
+
After publishing to npm, you can use npx to run the server without installation:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Using command line arguments
|
|
45
|
+
npx joplin-mcp-server --port 41184 --token your_joplin_token
|
|
46
|
+
|
|
47
|
+
# Using environment file
|
|
48
|
+
npx joplin-mcp-server --env-file /path/to/your/.env
|
|
49
|
+
|
|
50
|
+
# Mixed approach (args override env file)
|
|
51
|
+
npx joplin-mcp-server --env-file .env --port 41185
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Command Line Options
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
OPTIONS:
|
|
58
|
+
--env-file <file> Load environment variables from file
|
|
59
|
+
--port <port> Joplin port (default: 41184)
|
|
60
|
+
--token <token> Joplin API token
|
|
61
|
+
--help, -h Show help message
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### MCP Client Configuration
|
|
65
|
+
|
|
66
|
+
Usage in Augment Code:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
name: joplin
|
|
70
|
+
command: npx joplin-mcp-server --port 41184 --token your_token
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Usage in mcp.json (Cursor and other tools):
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"joplin": {
|
|
78
|
+
"command": "npx",
|
|
79
|
+
"args": ["joplin-mcp-server", "--port", "41184", "--token", "your_joplin_token"]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Or using environment file:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"joplin": {
|
|
89
|
+
"command": "npx",
|
|
90
|
+
"args": ["joplin-mcp-server", "--env-file", "/path/to/your/.env"]
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Legacy Usage (if installed locally)
|
|
96
|
+
|
|
97
|
+
Usage in Augment Code:
|
|
98
|
+
name: `joplin`
|
|
99
|
+
command: `node /path/to/your/mcp-joplin/index.js --env-file /path/to/your/mcp-joplin/.env`
|
|
100
|
+
|
|
101
|
+
Usage in mcp.json (cursor other tools)
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
"joplin":{
|
|
105
|
+
"command":"node",
|
|
106
|
+
"args":[
|
|
107
|
+
"/path/to/your/mcp-joplin/index.js",
|
|
108
|
+
"--env-file",
|
|
109
|
+
"/path/to/your/mcp-joplin/.env"
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Logging
|
|
115
|
+
|
|
116
|
+
The server logs all incoming commands and outgoing responses. Logs are stored in two places:
|
|
117
|
+
|
|
118
|
+
1. **Console output**: Basic information is displayed in the console
|
|
119
|
+
2. **Log files**: Detailed logs are saved in the `logs` directory with timestamps
|
|
120
|
+
|
|
121
|
+
You can adjust the log level by setting the `LOG_LEVEL` environment variable:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
LOG_LEVEL=debug npm start
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Available log levels (from most to least verbose):
|
|
128
|
+
|
|
129
|
+
- `debug`: All messages including detailed command and response data
|
|
130
|
+
- `info`: Standard operational messages (default)
|
|
131
|
+
- `warn`: Warnings and errors only
|
|
132
|
+
- `error`: Only error messages
|
|
133
|
+
|
|
134
|
+
## Available Tools
|
|
135
|
+
|
|
136
|
+
### list_notebooks
|
|
137
|
+
|
|
138
|
+
Retrieves the complete notebook hierarchy from Joplin.
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
# Example output:
|
|
142
|
+
Notebook 1 (id: "abc123")
|
|
143
|
+
Subnotebook 1.1 (id: "def456")
|
|
144
|
+
Subnotebook 1.2 (id: "ghi789")
|
|
145
|
+
Notebook 2 (id: "jkl012")
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### search_notes
|
|
149
|
+
|
|
150
|
+
Searches for notes in Joplin and returns matching notebooks.
|
|
151
|
+
|
|
152
|
+
**Parameters:**
|
|
153
|
+
|
|
154
|
+
- `query`: The search query string
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
# Example usage:
|
|
158
|
+
search_notes query="project meeting"
|
|
159
|
+
|
|
160
|
+
# Example output:
|
|
161
|
+
Found 2 notes matching query: "project meeting"
|
|
162
|
+
NOTE: To read a notebook, use the notebook ID (not the note title)
|
|
163
|
+
|
|
164
|
+
- Note: "Weekly Project Meeting" (note_id: "abc123")
|
|
165
|
+
Notebook: "Work" (notebook_id: "58a0a29f68bc4141b49c99f5d367638a")
|
|
166
|
+
Updated: 3/15/2025, 10:30:45 AM
|
|
167
|
+
Snippet: Notes from our weekly project meeting. Topics discussed: timeline, resources, next steps...
|
|
168
|
+
To read this notebook: read_notebook notebook_id="58a0a29f68bc4141b49c99f5d367638a"
|
|
169
|
+
|
|
170
|
+
- Note: "Project Kickoff Meeting" (note_id: "def456")
|
|
171
|
+
Notebook: "Projects" (notebook_id: "72b1c45d89ef3212a67b98f4e5d23a1b")
|
|
172
|
+
Updated: 3/10/2025, 2:15:30 PM
|
|
173
|
+
Snippet: Initial project meeting with stakeholders. Key decisions: project scope, team members...
|
|
174
|
+
To read this notebook: read_notebook notebook_id="72b1c45d89ef3212a67b98f4e5d23a1b"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
> **Important**: Note the difference between note titles and IDs. When using the `read_notebook` command, you must use the notebook ID (a long alphanumeric string), not the notebook title.
|
|
178
|
+
|
|
179
|
+
### read_notebook
|
|
180
|
+
|
|
181
|
+
Reads the contents of a specific notebook.
|
|
182
|
+
|
|
183
|
+
**Parameters:**
|
|
184
|
+
|
|
185
|
+
- `notebook_id`: The ID of the notebook to read
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
# Example usage:
|
|
189
|
+
read_notebook notebook_id="58a0a29f68bc4141b49c99f5d367638a"
|
|
190
|
+
|
|
191
|
+
# Example output:
|
|
192
|
+
# Notebook: "Work" (notebook_id: "58a0a29f68bc4141b49c99f5d367638a")
|
|
193
|
+
Contains 3 notes:
|
|
194
|
+
NOTE: This is showing the contents of notebook "Work", not a specific note.
|
|
195
|
+
|
|
196
|
+
- Note: "Weekly Project Meeting" (note_id: "def456")
|
|
197
|
+
Updated: 3/15/2025, 10:30:45 AM
|
|
198
|
+
|
|
199
|
+
- ✅ Note: "Call client" (note_id: "ghi789")
|
|
200
|
+
Updated: 3/14/2025, 3:45:12 PM
|
|
201
|
+
|
|
202
|
+
- ☐ Note: "Prepare presentation" (note_id: "jkl012")
|
|
203
|
+
Updated: 3/13/2025, 9:20:33 AM
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
> **Common Error**: If you try to use a note title (like "todo") instead of a notebook ID, you'll get an error. Always use the notebook ID (the long alphanumeric string) shown in the search results or notebook list.
|
|
207
|
+
|
|
208
|
+
### read_note
|
|
209
|
+
|
|
210
|
+
Reads the full content of a specific note.
|
|
211
|
+
|
|
212
|
+
**Parameters:**
|
|
213
|
+
|
|
214
|
+
- `note_id`: The ID of the note to read
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
# Example usage:
|
|
218
|
+
read_note note_id="def456"
|
|
219
|
+
|
|
220
|
+
# Example output:
|
|
221
|
+
# Note: "Weekly Project Meeting"
|
|
222
|
+
Note ID: def456
|
|
223
|
+
Notebook: "Work" (notebook_id: "58a0a29f68bc4141b49c99f5d367638a")
|
|
224
|
+
Created: 3/15/2025, 10:00:12 AM
|
|
225
|
+
Updated: 3/15/2025, 10:30:45 AM
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
# Weekly Project Meeting
|
|
230
|
+
|
|
231
|
+
## Agenda
|
|
232
|
+
1. Project status update
|
|
233
|
+
2. Timeline review
|
|
234
|
+
3. Resource allocation
|
|
235
|
+
4. Next steps
|
|
236
|
+
|
|
237
|
+
## Notes
|
|
238
|
+
- Project is on track for Q2 delivery
|
|
239
|
+
- Need to allocate additional resources to the UI team
|
|
240
|
+
- Next meeting scheduled for next Friday
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
Related commands:
|
|
245
|
+
- To view the notebook containing this note: read_notebook notebook_id="58a0a29f68bc4141b49c99f5d367638a"
|
|
246
|
+
- To search for more notes: search_notes query="your search term"
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
> **Note**: The `read_note` command shows the full content of a specific note, while the `read_notebook` command shows a list of notes in a notebook. Use `search_notes` to find notes and get their IDs.
|
|
250
|
+
|
|
251
|
+
### read_multinote
|
|
252
|
+
|
|
253
|
+
Reads the full content of multiple notes at once.
|
|
254
|
+
|
|
255
|
+
**Parameters:**
|
|
256
|
+
|
|
257
|
+
- `note_ids`: An array of note IDs to read
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
# Example usage:
|
|
261
|
+
read_multinote note_ids=["def456", "ghi789", "jkl012"]
|
|
262
|
+
|
|
263
|
+
# Example output:
|
|
264
|
+
# Reading 3 notes
|
|
265
|
+
|
|
266
|
+
## Note 1 of 3 (ID: def456)
|
|
267
|
+
|
|
268
|
+
### Note: "Weekly Project Meeting"
|
|
269
|
+
Notebook: "Work" (notebook_id: "58a0a29f68bc4141b49c99f5d367638a")
|
|
270
|
+
Created: 3/15/2025, 10:00:12 AM
|
|
271
|
+
Updated: 3/15/2025, 10:30:45 AM
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
# Weekly Project Meeting
|
|
276
|
+
|
|
277
|
+
## Agenda
|
|
278
|
+
1. Project status update
|
|
279
|
+
2. Timeline review
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Note 2 of 3 (ID: ghi789)
|
|
284
|
+
|
|
285
|
+
### Note: "Call client"
|
|
286
|
+
Notebook: "Work" (notebook_id: "58a0a29f68bc4141b49c99f5d367638a")
|
|
287
|
+
Status: Completed
|
|
288
|
+
Created: 3/14/2025, 3:00:00 PM
|
|
289
|
+
Updated: 3/14/2025, 3:45:12 PM
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
Discussed project timeline and next steps.
|
|
294
|
+
Client is happy with progress.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Note 3 of 3 (ID: jkl012)
|
|
299
|
+
|
|
300
|
+
### Note: "Prepare presentation"
|
|
301
|
+
Notebook: "Work" (notebook_id: "58a0a29f68bc4141b49c99f5d367638a")
|
|
302
|
+
Status: Not completed
|
|
303
|
+
Due: 3/20/2025, 9:00:00 AM
|
|
304
|
+
Created: 3/13/2025, 9:00:00 AM
|
|
305
|
+
Updated: 3/13/2025, 9:20:33 AM
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
# Presentation Outline
|
|
310
|
+
- Introduction
|
|
311
|
+
- Project overview
|
|
312
|
+
- Timeline
|
|
313
|
+
- Budget
|
|
314
|
+
- Next steps
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
# Summary
|
|
319
|
+
Total notes requested: 3
|
|
320
|
+
Successfully retrieved: 3
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
> **Tip**: When you search for notes or view a notebook, you'll see a suggestion for using `read_multinote` with the exact IDs of the notes found. This makes it easy to read multiple related notes at once.
|
|
324
|
+
|
|
325
|
+
## Development
|
|
326
|
+
|
|
327
|
+
### Local Development Setup
|
|
328
|
+
|
|
329
|
+
To test the npx command locally during development:
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
# In the project root directory
|
|
333
|
+
cd /path/to/mcp-joplin
|
|
334
|
+
npm install
|
|
335
|
+
npm link
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
After linking, you can test your local changes immediately:
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
# Test the CLI
|
|
342
|
+
npx joplin-mcp-server --help
|
|
343
|
+
npx joplin-mcp-server --port 41184 --token your_token
|
|
344
|
+
|
|
345
|
+
# Make code changes, then test again (no rebuild needed)
|
|
346
|
+
npx joplin-mcp-server --help
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Making Changes
|
|
350
|
+
|
|
351
|
+
1. Edit any `.js` files in the project
|
|
352
|
+
2. Run tests: `npm test`
|
|
353
|
+
3. Test the CLI: `npx joplin-mcp-server --help`
|
|
354
|
+
|
|
355
|
+
No build step is required - changes are immediately available through the npm link.
|
|
356
|
+
|
|
357
|
+
### Running Tests
|
|
358
|
+
|
|
359
|
+
Create a `.env.test.local` file with your test configuration, then run:
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
npm test
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Publishing to npm
|
|
366
|
+
|
|
367
|
+
To make this package available via npx:
|
|
368
|
+
|
|
369
|
+
1. Update the version in `package.json`
|
|
370
|
+
2. Run `npm publish`
|
|
371
|
+
|
|
372
|
+
Users can then run it with `npx joplin-mcp-server`
|
|
373
|
+
|
|
374
|
+
### Unlinking (if needed)
|
|
375
|
+
|
|
376
|
+
To remove the local link:
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
npm unlink -g joplin-mcp-server
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## License
|
|
383
|
+
|
|
384
|
+
MIT
|
package/dist/bin/cli.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { dirname, resolve } from "path";
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = dirname(__filename);
|
|
6
|
+
// Import and run the compiled index.js
|
|
7
|
+
const { default: main } = await import(resolve(__dirname, "../index.js"));
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import parseArgs from "./lib/parse-args.js";
|
|
8
|
+
import JoplinAPIClient from "./lib/joplin-api-client.js";
|
|
9
|
+
import { ListNotebooks, SearchNotes, ReadNotebook, ReadNote, ReadMultiNote, CreateNote, CreateFolder, EditNote, EditFolder, DeleteNote, DeleteFolder, } from "./lib/tools/index.js";
|
|
10
|
+
// Parse command line arguments
|
|
11
|
+
parseArgs();
|
|
12
|
+
// Set default port if not specified
|
|
13
|
+
if (!process.env.JOPLIN_PORT) {
|
|
14
|
+
process.env.JOPLIN_PORT = "41184";
|
|
15
|
+
}
|
|
16
|
+
// Check for required environment variables
|
|
17
|
+
if (!process.env.JOPLIN_TOKEN) {
|
|
18
|
+
process.stderr.write("Error: JOPLIN_TOKEN is required. Use --token <token> or set JOPLIN_TOKEN environment variable.\n");
|
|
19
|
+
process.stderr.write("Find your token in Joplin: Tools > Options > Web Clipper\n");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
// Create the Joplin API client
|
|
23
|
+
const apiClient = new JoplinAPIClient({
|
|
24
|
+
port: parseInt(process.env.JOPLIN_PORT || "41184"),
|
|
25
|
+
token: process.env.JOPLIN_TOKEN,
|
|
26
|
+
});
|
|
27
|
+
// Create the MCP server
|
|
28
|
+
const server = new McpServer({
|
|
29
|
+
name: "joplin-mcp-server",
|
|
30
|
+
version: "1.0.0",
|
|
31
|
+
});
|
|
32
|
+
// Register the list_notebooks tool
|
|
33
|
+
server.tool("list_notebooks", "Retrieve the complete notebook hierarchy from Joplin", {}, async () => {
|
|
34
|
+
const result = await new ListNotebooks(apiClient).call();
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: "text", text: result }],
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
// Register the search_notes tool
|
|
40
|
+
server.tool("search_notes", "Search for notes in Joplin and return matching notebooks", { query: z.string() }, async ({ query }) => {
|
|
41
|
+
const result = await new SearchNotes(apiClient).call(query);
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: "text", text: result }],
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
// Register the read_notebook tool
|
|
47
|
+
server.tool("read_notebook", "Read the contents of a specific notebook", { notebook_id: z.string() }, async ({ notebook_id }) => {
|
|
48
|
+
const result = await new ReadNotebook(apiClient).call(notebook_id);
|
|
49
|
+
return {
|
|
50
|
+
content: [{ type: "text", text: result }],
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
// Register the read_note tool
|
|
54
|
+
server.tool("read_note", "Read the full content of a specific note", { note_id: z.string() }, async ({ note_id }) => {
|
|
55
|
+
const result = await new ReadNote(apiClient).call(note_id);
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: "text", text: result }],
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
// Register the read_multinote tool
|
|
61
|
+
server.tool("read_multinote", "Read the full content of multiple notes at once", { note_ids: z.array(z.string()) }, async ({ note_ids }) => {
|
|
62
|
+
const result = await new ReadMultiNote(apiClient).call(note_ids);
|
|
63
|
+
return {
|
|
64
|
+
content: [{ type: "text", text: result }],
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
// Register the create_note tool
|
|
68
|
+
server.tool("create_note", "Create a new note in Joplin", {
|
|
69
|
+
title: z.string().optional(),
|
|
70
|
+
body: z.string().optional(),
|
|
71
|
+
body_html: z.string().optional(),
|
|
72
|
+
parent_id: z.string().optional(),
|
|
73
|
+
is_todo: z.boolean().optional(),
|
|
74
|
+
image_data_url: z.string().optional(),
|
|
75
|
+
}, async (params) => {
|
|
76
|
+
const result = await new CreateNote(apiClient).call(params);
|
|
77
|
+
return {
|
|
78
|
+
content: [{ type: "text", text: result }],
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
// Register the create_folder tool
|
|
82
|
+
server.tool("create_folder", "Create a new folder/notebook in Joplin", {
|
|
83
|
+
title: z.string(),
|
|
84
|
+
parent_id: z.string().optional(),
|
|
85
|
+
}, async (params) => {
|
|
86
|
+
const result = await new CreateFolder(apiClient).call(params);
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: "text", text: result }],
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
// Register the edit_note tool
|
|
92
|
+
server.tool("edit_note", "Edit/update an existing note in Joplin", {
|
|
93
|
+
note_id: z.string(),
|
|
94
|
+
title: z.string().optional(),
|
|
95
|
+
body: z.string().optional(),
|
|
96
|
+
body_html: z.string().optional(),
|
|
97
|
+
parent_id: z.string().optional(),
|
|
98
|
+
is_todo: z.boolean().optional(),
|
|
99
|
+
todo_completed: z.boolean().optional(),
|
|
100
|
+
todo_due: z.number().optional(),
|
|
101
|
+
}, async (params) => {
|
|
102
|
+
const result = await new EditNote(apiClient).call(params);
|
|
103
|
+
return {
|
|
104
|
+
content: [{ type: "text", text: result }],
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
// Register the edit_folder tool
|
|
108
|
+
server.tool("edit_folder", "Edit/update an existing folder/notebook in Joplin", {
|
|
109
|
+
folder_id: z.string(),
|
|
110
|
+
title: z.string().optional(),
|
|
111
|
+
parent_id: z.string().optional(),
|
|
112
|
+
}, async (params) => {
|
|
113
|
+
const result = await new EditFolder(apiClient).call(params);
|
|
114
|
+
return {
|
|
115
|
+
content: [{ type: "text", text: result }],
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
// Register the delete_note tool
|
|
119
|
+
server.tool("delete_note", "Delete a note from Joplin (requires confirmation)", {
|
|
120
|
+
note_id: z.string(),
|
|
121
|
+
confirm: z.boolean().optional(),
|
|
122
|
+
}, async (params) => {
|
|
123
|
+
const result = await new DeleteNote(apiClient).call(params);
|
|
124
|
+
return {
|
|
125
|
+
content: [{ type: "text", text: result }],
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
// Register the delete_folder tool
|
|
129
|
+
server.tool("delete_folder", "Delete a folder/notebook from Joplin (requires confirmation)", {
|
|
130
|
+
folder_id: z.string(),
|
|
131
|
+
confirm: z.boolean().optional(),
|
|
132
|
+
force: z.boolean().optional(),
|
|
133
|
+
}, async (params) => {
|
|
134
|
+
const result = await new DeleteFolder(apiClient).call(params);
|
|
135
|
+
return {
|
|
136
|
+
content: [{ type: "text", text: result }],
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
// Create logs directory if it doesn't exist
|
|
140
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
141
|
+
const __dirname = path.dirname(__filename);
|
|
142
|
+
const logsDir = path.join(__dirname, "logs");
|
|
143
|
+
if (!fs.existsSync(logsDir)) {
|
|
144
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
145
|
+
}
|
|
146
|
+
// Create a log file for this session
|
|
147
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
148
|
+
const logFile = path.join(logsDir, `mcp-server-${timestamp}.log`);
|
|
149
|
+
// Log server startup (commented out for debugging)
|
|
150
|
+
// logger.info(`Starting MCP server (version 1.0.0)`);
|
|
151
|
+
// logger.info(`Log file: ${logFile}`);
|
|
152
|
+
// Create a custom transport wrapper to log commands and responses
|
|
153
|
+
class LoggingTransport extends StdioServerTransport {
|
|
154
|
+
commandCounter;
|
|
155
|
+
constructor() {
|
|
156
|
+
super();
|
|
157
|
+
this.commandCounter = 0;
|
|
158
|
+
}
|
|
159
|
+
async sendMessage(message) {
|
|
160
|
+
// Log outgoing message (response)
|
|
161
|
+
const logEntry = {
|
|
162
|
+
timestamp: new Date().toISOString(),
|
|
163
|
+
direction: "RESPONSE",
|
|
164
|
+
message,
|
|
165
|
+
};
|
|
166
|
+
// Log to console (commented out for debugging)
|
|
167
|
+
// logger.debug(`Sending response: ${JSON.stringify(message)}`);
|
|
168
|
+
// Log to file
|
|
169
|
+
fs.appendFileSync(logFile, JSON.stringify(logEntry) + "\n");
|
|
170
|
+
// Call the original method
|
|
171
|
+
const parent = Object.getPrototypeOf(Object.getPrototypeOf(this));
|
|
172
|
+
return parent.sendMessage.call(this, message);
|
|
173
|
+
}
|
|
174
|
+
async handleMessage(message) {
|
|
175
|
+
// Log incoming message (command)
|
|
176
|
+
this.commandCounter++;
|
|
177
|
+
const logEntry = {
|
|
178
|
+
timestamp: new Date().toISOString(),
|
|
179
|
+
direction: "COMMAND",
|
|
180
|
+
commandNumber: this.commandCounter,
|
|
181
|
+
message,
|
|
182
|
+
};
|
|
183
|
+
// Log to console (commented out for debugging)
|
|
184
|
+
// logger.info(`Received command #${this.commandCounter}: ${message.method || 'unknown method'}`);
|
|
185
|
+
// logger.debug(`Command details: ${JSON.stringify(message)}`);
|
|
186
|
+
// Log to file
|
|
187
|
+
fs.appendFileSync(logFile, JSON.stringify(logEntry) + "\n");
|
|
188
|
+
// Call the original method
|
|
189
|
+
const parent = Object.getPrototypeOf(Object.getPrototypeOf(this));
|
|
190
|
+
return parent.handleMessage.call(this, message);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Start the server with logging transport
|
|
194
|
+
const transport = new LoggingTransport();
|
|
195
|
+
// Log connection status (commented out for debugging)
|
|
196
|
+
// logger.info('Connecting to transport...');
|
|
197
|
+
try {
|
|
198
|
+
await server.connect(transport);
|
|
199
|
+
// logger.info('MCP server started and ready to receive commands');
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
process.stderr.write(`Failed to start MCP server: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
interface JoplinAPIClientConfig {
|
|
2
|
+
port?: number;
|
|
3
|
+
token: string;
|
|
4
|
+
}
|
|
5
|
+
interface RequestOptions {
|
|
6
|
+
query?: Record<string, any>;
|
|
7
|
+
[key: string]: any;
|
|
8
|
+
}
|
|
9
|
+
declare class JoplinAPIClient {
|
|
10
|
+
private readonly baseURL;
|
|
11
|
+
private readonly token;
|
|
12
|
+
constructor({ port, token }: JoplinAPIClientConfig);
|
|
13
|
+
serviceAvailable(): Promise<boolean>;
|
|
14
|
+
getAllItems<T = any>(path: string, options?: RequestOptions): Promise<T[]>;
|
|
15
|
+
get<T = any>(path: string, options?: RequestOptions): Promise<T>;
|
|
16
|
+
post<T = any>(path: string, body: any, options?: RequestOptions): Promise<T>;
|
|
17
|
+
delete<T = any>(path: string, options?: RequestOptions): Promise<T>;
|
|
18
|
+
put<T = any>(path: string, body: any, options?: RequestOptions): Promise<T>;
|
|
19
|
+
private requestOptions;
|
|
20
|
+
private mergeRequestOptions;
|
|
21
|
+
private except;
|
|
22
|
+
}
|
|
23
|
+
export default JoplinAPIClient;
|